Merge pull request #8588 from nextcloud/fix-breadcrumbs-width-calculation

Fix breadcrumbs width calculation
This commit is contained in:
Roeland Jago Douma 2018-03-01 20:13:26 +01:00 committed by GitHub
commit 6b931eb64b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 426 additions and 55 deletions

View file

@ -36,6 +36,7 @@
this.$menu = $('<div class="popovermenu menu-center"><ul></ul></div>');
this.crumbSelector = '.crumb:not(.hidden):not(.crumbhome):not(.crumbmenu)';
this.hiddenCrumbSelector = '.crumb.hidden:not(.crumbhome):not(.crumbmenu)';
options = options || {};
if (options.onClick) {
this.onClick = options.onClick;
@ -238,31 +239,21 @@
return crumbs;
},
/**
* Show/hide breadcrumbs to fit the given width
* Mostly used by tests
*
* @param {int} availableWidth available width
*/
setMaxWidth: function (availableWidth) {
if (this.availableWidth !== availableWidth) {
this.availableWidth = availableWidth;
this._resize();
}
},
/**
* Calculate real width based on individual crumbs
* More accurate and works with tests
*
* @param {boolean} ignoreHidden ignore hidden crumbs
*/
getTotalWidth: function(ignoreHidden) {
// The width has to be calculated by adding up the width of all the
// crumbs; getting the width of the breadcrumb element is not a
// valid approach, as the returned value could be clamped to its
// parent width.
var totalWidth = 0;
for (var i = 0; i < this.breadcrumbs.length; i++ ) {
var $crumb = $(this.breadcrumbs[i]);
if(!$crumb.hasClass('hidden') || ignoreHidden === true) {
totalWidth += $crumb.outerWidth();
totalWidth += $crumb.outerWidth(true);
}
}
return totalWidth;
@ -282,19 +273,19 @@
* Get the crumb to show
*/
_getCrumbElement: function() {
var hidden = this.$el.find('.crumb.hidden').length;
var hidden = this.$el.find(this.hiddenCrumbSelector).length;
var shown = this.$el.find(this.crumbSelector).length;
// Get the outer one with priority to the highest
var elmt = (1 - shown % 2) * (hidden - 1);
return this.$el.find('.crumb.hidden:eq('+elmt+')');
return this.$el.find(this.hiddenCrumbSelector + ':eq('+elmt+')');
},
/**
* Show the middle crumb
*/
_showCrumb: function() {
if(this.$el.find('.crumb.hidden').length === 1) {
this.$el.find('.crumb.hidden').removeClass('hidden');
if(this.$el.find(this.hiddenCrumbSelector).length === 1) {
this.$el.find(this.hiddenCrumbSelector).removeClass('hidden');
}
this._getCrumbElement().removeClass('hidden');
},
@ -311,9 +302,7 @@
* Update the popovermenu
*/
_updateMenu: function() {
var menuItems = this.$el.find('.crumb.hidden');
// Hide the crumb menu if no elements
this.$el.find('.crumbmenu').toggleClass('hidden', menuItems.length === 0);
var menuItems = this.$el.find(this.hiddenCrumbSelector);
this.$menu.find('li').addClass('in-breadcrumb');
for (var i = 0; i < menuItems.length; i++) {
@ -329,25 +318,47 @@
return;
}
// Used for testing since this.$el.parent fails
if (!this.availableWidth) {
this.usedWidth = this.$el.parent().width() - this.$el.parent().find('.actions.creatable').width();
} else {
this.usedWidth = this.availableWidth;
// Always hide the menu to ensure that it does not interfere with
// the width calculations; otherwise, the result could be different
// depending on whether the menu was previously being shown or not.
this.$el.find('.crumbmenu').addClass('hidden');
// Show the crumbs to compress the siblings before hidding again the
// crumbs. This is needed when the siblings expand to fill all the
// available width, as in that case their old width would limit the
// available width for the crumbs.
// Note that the crumbs shown always overflow the parent width
// (except, of course, when they all fit in).
while (this.$el.find(this.hiddenCrumbSelector).length > 0
&& this.getTotalWidth() <= this.$el.parent().width()) {
this._showCrumb();
}
var siblingsWidth = 0;
this.$el.prevAll(':visible').each(function () {
siblingsWidth += $(this).outerWidth(true);
});
this.$el.nextAll(':visible').each(function () {
siblingsWidth += $(this).outerWidth(true);
});
var availableWidth = this.$el.parent().width() - siblingsWidth;
// If container is smaller than content
// AND if there are crumbs left to hide
while (this.getTotalWidth() > this.usedWidth
while (this.getTotalWidth() > availableWidth
&& this.$el.find(this.crumbSelector).length > 0) {
// As soon as one of the crumbs is hidden the menu will be
// shown. This is needed for proper results in further width
// checks.
// Note that the menu is not shown only when all the crumbs were
// being shown and they all fit the available space; if any of
// the crumbs was not being shown then those shown would
// overflow the available width, so at least one will be hidden
// and thus the menu will be shown.
this.$el.find('.crumbmenu').removeClass('hidden');
this._hideCrumb();
}
// If container is bigger than content + element to be shown
// AND if there is at least one hidden crumb
while (this.$el.find('.crumb.hidden').length > 0
&& this.getTotalWidth() + this._getCrumbElement().width() < this.usedWidth) {
this._showCrumb();
}
this._updateMenu();
}

View file

@ -175,10 +175,6 @@ describe('OCA.Files.BreadCrumb tests', function() {
beforeEach(function() {
dummyDir = '/one/two/three/four/five'
$('div.crumb').each(function(index){
$(this).css('width', 50);
});
bc = new BreadCrumb();
// append dummy navigation and controls
// as they are currently used for measurements
@ -187,12 +183,23 @@ describe('OCA.Files.BreadCrumb tests', function() {
);
$('#controls').append(bc.$el);
// Shrink to show popovermenu
bc.setMaxWidth(300);
// triggers resize implicitly
bc.setDirectory(dummyDir);
$('div.crumb').each(function(index){
$(this).css('width', 50);
$(this).css('padding', 0);
$(this).css('margin', 0);
});
$('div.crumbhome').css('width', 51);
$('div.crumbmenu').css('width', 51);
$('#controls').width(1000);
bc._resize();
// Shrink to show popovermenu
$('#controls').width(300);
bc._resize();
$crumbmenuLink = bc.$el.find('.crumbmenu > a');
$popovermenu = $crumbmenuLink.next('.popovermenu');
});
@ -236,7 +243,11 @@ describe('OCA.Files.BreadCrumb tests', function() {
});
describe('Resizing', function() {
var bc, dummyDir, widths;
var bc, dummyDir, widths, paddings, margins;
// cit() will skip tests if running on PhantomJS because it does not
// have proper support for flexboxes.
var cit = window.isPhantom?xit:it;
beforeEach(function() {
dummyDir = '/short name/longer name/looooooooooooonger/' +
@ -257,22 +268,30 @@ describe('OCA.Files.BreadCrumb tests', function() {
// results on different browsers due to font engine differences
// 51px is default size for menu and home
widths = [51, 51, 106, 112, 160, 257, 251, 91];
// using hard-coded paddings and margins to avoid depending on the
// current CSS values used in the server
paddings = [0, 0, 0, 0, 0, 0, 0, 0];
margins = [0, 0, 0, 0, 0, 0, 0, 0];
$('div.crumb').each(function(index){
$(this).css('width', widths[index]);
$(this).css('padding', paddings[index]);
$(this).css('margin', margins[index]);
});
});
afterEach(function() {
bc = null;
});
it('Hides breadcrumbs to fit max allowed width', function() {
it('Hides breadcrumbs to fit available width', function() {
var $crumbs;
bc.setMaxWidth(500);
$('#controls').width(500);
bc._resize();
$crumbs = bc.$el.find('.crumb');
// Menu and home are always visible
// Second, third, fourth and fifth crumb are hidden and everything
// else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
@ -283,14 +302,15 @@ describe('OCA.Files.BreadCrumb tests', function() {
expect($crumbs.eq(6).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
});
it('Hides breadcrumbs to fit max allowed width', function() {
it('Hides breadcrumbs to fit available width', function() {
var $crumbs;
bc.setMaxWidth(700);
$('#controls').width(700);
bc._resize();
$crumbs = bc.$el.find('.crumb');
// Menu and home are always visible
// Third and fourth crumb are hidden and everything else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
@ -301,23 +321,363 @@ describe('OCA.Files.BreadCrumb tests', function() {
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
});
it('Updates the breadcrumbs when reducing max allowed width', function() {
it('Hides breadcrumbs to fit available width taking paddings into account', function() {
var $crumbs;
// Each element is 20px wider
paddings = [10, 10, 10, 10, 10, 10, 10, 10];
$('div.crumb').each(function(index){
$(this).css('padding', paddings[index]);
});
$('#controls').width(700);
bc._resize();
$crumbs = bc.$el.find('.crumb');
// Second, third and fourth crumb are hidden and everything else is
// visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
});
it('Hides breadcrumbs to fit available width taking margins into account', function() {
var $crumbs;
// Each element is 20px wider
margins = [10, 10, 10, 10, 10, 10, 10, 10];
$('div.crumb').each(function(index){
$(this).css('margin', margins[index]);
});
$('#controls').width(700);
bc._resize();
$crumbs = bc.$el.find('.crumb');
// Second, third and fourth crumb are hidden and everything else is
// visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
});
it('Hides breadcrumbs to fit available width left by siblings', function() {
var $crumbs;
$('#controls').width(700);
bc._resize();
$crumbs = bc.$el.find('.crumb');
// Third and fourth crumb are hidden and everything else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
// Visible sibling widths add up to 200px
var $previousSibling = $('<div class="otherSibling"></div>');
// Set both the width and the min-width to even differences in width
// handling in the browsers used to run the tests.
$previousSibling.css('width', '50px');
$previousSibling.css('min-width', '50px');
$('#controls').prepend($previousSibling);
var $creatableActions = $('<div class="actions creatable"></div>');
// Set both the width and the min-width to even differences in width
// handling in the browsers used to run the tests.
$creatableActions.css('width', '100px');
$creatableActions.css('min-width', '100px');
$('#controls').append($creatableActions);
var $nextHiddenSibling = $('<div class="otherSibling hidden"></div>');
// Set both the width and the min-width to even differences in width
// handling in the browsers used to run the tests.
$nextHiddenSibling.css('width', '200px');
$nextHiddenSibling.css('min-width', '200px');
$('#controls').append($nextHiddenSibling);
var $nextSibling = $('<div class="otherSibling"></div>');
// Set both the width and the min-width to even differences in width
// handling in the browsers used to run the tests.
$nextSibling.css('width', '50px');
$nextSibling.css('min-width', '50px');
$('#controls').append($nextSibling);
bc._resize();
// Second, third, fourth and fifth crumb are hidden and everything
// else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
});
it('Hides breadcrumbs to fit available width left by siblings with paddings and margins', function() {
var $crumbs;
$('#controls').width(700);
bc._resize();
$crumbs = bc.$el.find('.crumb');
// Third and fourth crumb are hidden and everything else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
// Visible sibling widths add up to 200px
var $previousSibling = $('<div class="otherSibling"></div>');
// Set both the width and the min-width to even differences in width
// handling in the browsers used to run the tests.
$previousSibling.css('width', '10px');
$previousSibling.css('min-width', '10px');
$previousSibling.css('margin', '20px');
$('#controls').prepend($previousSibling);
var $creatableActions = $('<div class="actions creatable"></div>');
// Set both the width and the min-width to even differences in width
// handling in the browsers used to run the tests.
$creatableActions.css('width', '20px');
$creatableActions.css('min-width', '20px');
$creatableActions.css('margin-left', '40px');
$creatableActions.css('padding-right', '40px');
$('#controls').append($creatableActions);
var $nextHiddenSibling = $('<div class="otherSibling hidden"></div>');
// Set both the width and the min-width to even differences in width
// handling in the browsers used to run the tests.
$nextHiddenSibling.css('width', '200px');
$nextHiddenSibling.css('min-width', '200px');
$('#controls').append($nextHiddenSibling);
var $nextSibling = $('<div class="otherSibling"></div>');
// Set both the width and the min-width to even differences in width
// handling in the browsers used to run the tests.
$nextSibling.css('width', '10px');
$nextSibling.css('min-width', '10px');
$nextSibling.css('padding', '20px');
$('#controls').append($nextSibling);
bc._resize();
// Second, third, fourth and fifth crumb are hidden and everything
// else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
});
it('Updates the breadcrumbs when reducing available width', function() {
var $crumbs;
// enough space
bc.setMaxWidth(1800);
$('#controls').width(1800);
bc._resize();
$crumbs = bc.$el.find('.crumb');
// Menu is hidden
expect($crumbs.eq(0).hasClass('hidden')).toEqual(true);
// triggers resize implicitly
bc.setDirectory(dummyDir);
// simulate decrease
$('#controls').width(950);
bc._resize();
// Third crumb is hidden and everything else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
});
it('Updates the breadcrumbs when reducing available width taking into account the menu width', function() {
var $crumbs;
// enough space
$('#controls').width(1800);
bc._resize();
$crumbs = bc.$el.find('.crumb');
// Menu is hidden
expect($crumbs.eq(0).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
// simulate decrease
bc.setMaxWidth(950);
// 650 is enough for all the crumbs except the third and fourth
// ones, but not enough for the menu and all the crumbs except the
// third and fourth ones; the second one has to be hidden too.
$('#controls').width(650);
bc._resize();
// Menu and home are always visible
// Second, third and fourth crumb are hidden and everything else is
// visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
});
it('Updates the breadcrumbs when increasing available width', function() {
var $crumbs;
// limited space
$('#controls').width(850);
bc._resize();
$crumbs = bc.$el.find('.crumb');
// Third and fourth crumb are hidden and everything else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
// simulate increase
$('#controls').width(1000);
bc._resize();
// Third crumb is hidden and everything else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
});
it('Updates the breadcrumbs when increasing available width taking into account the menu width', function() {
var $crumbs;
// limited space
$('#controls').width(850);
bc._resize();
$crumbs = bc.$el.find('.crumb');
// Third and fourth crumb are hidden and everything else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
// simulate increase
// 1030 is enough for all the crumbs if the menu is hidden.
$('#controls').width(1030);
bc._resize();
// Menu is hidden and everything else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
});
cit('Updates the breadcrumbs when increasing available width with an expanding sibling', function() {
var $crumbs;
// The sibling expands to fill all the width left by the breadcrumbs
var $nextSibling = $('<div class="sibling"></div>');
// Set both the width and the min-width to even differences in width
// handling in the browsers used to run the tests.
$nextSibling.css('width', '10px');
$nextSibling.css('min-width', '10px');
$nextSibling.css('display', 'flex');
$nextSibling.css('flex', '1 1');
var $nextSiblingChild = $('<div class="siblingChild"></div>');
$nextSiblingChild.css('margin-left', 'auto');
$nextSibling.append($nextSiblingChild);
$('#controls').append($nextSibling);
// limited space
$('#controls').width(850);
bc._resize();
$crumbs = bc.$el.find('.crumb');
// Third and fourth crumb are hidden and everything else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(7).hasClass('hidden')).toEqual(false);
// simulate increase
$('#controls').width(1000);
bc._resize();
// Third crumb is hidden and everything else is visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);