Breadcrumb width calculation fix

Rewrote the breadcrumb calculation to be more readable.

Breadcrumb now has a setMaxWidth() method to set the maximum allowed
width which is used to fit the breadcrumbs.

The breadcrumb width is now based on the container width, passed through
setMaxWidth() by the FileList class.

Now using fixed widths for the test crumbs to simulate consistent
widths across browsers which rendering engines might usually yield
different results.
This commit is contained in:
Vincent Petry 2014-05-23 19:02:50 +02:00
parent 8aa51a69fa
commit ec4cf96f0d
5 changed files with 159 additions and 132 deletions

View File

@ -41,8 +41,9 @@
$el: null,
dir: null,
lastWidth: 0,
hiddenBreadcrumbs: 0,
/**
* Total width of all breadcrumbs
*/
totalWidth: 0,
breadcrumbs: [],
onClick: null,
@ -116,7 +117,6 @@
}
this._updateTotalWidth();
this.resize($(window).width(), true);
},
/**
@ -150,93 +150,93 @@
return crumbs;
},
/**
* Calculate the total breadcrumb width when
* all crumbs are expanded
*/
_updateTotalWidth: function () {
var self = this;
this.lastWidth = 0;
// initialize with some extra space
this.totalWidth = 64;
// FIXME: this class should not know about global elements
if ( $('#navigation').length ) {
this.totalWidth += $('#navigation').outerWidth();
}
if ( $('#app-navigation').length && !$('#app-navigation').hasClass('hidden')) {
this.totalWidth += $('#app-navigation').outerWidth();
}
this.hiddenBreadcrumbs = 0;
this.totalWidth = 0;
for (var i = 0; i < this.breadcrumbs.length; i++ ) {
this.totalWidth += $(this.breadcrumbs[i]).get(0).offsetWidth;
var $crumb = $(this.breadcrumbs[i]);
$crumb.data('real-width', $crumb.width());
this.totalWidth += $crumb.width();
}
$.each($('#controls .actions'), function(index, action) {
self.totalWidth += $(action).outerWidth();
});
this._resize();
},
/**
* Show/hide breadcrumbs to fit the given width
*/
resize: function (width, firstRun) {
var i, $crumb;
setMaxWidth: function (availableWidth) {
if (this.availableWidth !== availableWidth) {
this.availableWidth = availableWidth;
this._resize();
}
},
if (width === this.lastWidth) {
_resize: function() {
var i, $crumb, $ellipsisCrumb;
if (!this.availableWidth) {
this.availableWidth = this.$el.width();
}
if (this.breadcrumbs.length <= 1) {
return;
}
// window was shrinked since last time or first run ?
if ((width < this.lastWidth || firstRun) && width < this.totalWidth) {
if (this.hiddenBreadcrumbs === 0 && this.breadcrumbs.length > 1) {
// start by hiding the first breadcrumb after home,
// that one will have extra three dots displayed
$crumb = this.breadcrumbs[1];
this.totalWidth -= $crumb.get(0).offsetWidth;
$crumb.find('a').addClass('hidden');
$crumb.append('<span class="ellipsis">...</span>');
this.totalWidth += $crumb.get(0).offsetWidth;
this.hiddenBreadcrumbs = 2;
}
i = this.hiddenBreadcrumbs;
// hide subsequent breadcrumbs if the space is still not enough
while (width < this.totalWidth && i > 1 && i < this.breadcrumbs.length - 1) {
$crumb = this.breadcrumbs[i];
this.totalWidth -= $crumb.get(0).offsetWidth;
$crumb.addClass('hidden');
this.hiddenBreadcrumbs = i;
i++;
}
// window is bigger than last time
} else if (width > this.lastWidth && this.hiddenBreadcrumbs > 0) {
i = this.hiddenBreadcrumbs;
while (width > this.totalWidth && i > 0) {
if (this.hiddenBreadcrumbs === 1) {
// special handling for last one as it has the three dots
$crumb = this.breadcrumbs[1];
if ($crumb) {
this.totalWidth -= $crumb.get(0).offsetWidth;
$crumb.find('.ellipsis').remove();
$crumb.find('a').removeClass('hidden');
this.totalWidth += $crumb.get(0).offsetWidth;
}
} else {
$crumb = this.breadcrumbs[i];
$crumb.removeClass('hidden');
this.totalWidth += $crumb.get(0).offsetWidth;
if (this.totalWidth > width) {
this.totalWidth -= $crumb.get(0).offsetWidth;
$crumb.addClass('hidden');
break;
}
}
i--;
this.hiddenBreadcrumbs = i;
}
// reset crumbs
this.$el.find('.crumb.ellipsized').remove();
// unhide all
this.$el.find('.crumb.hidden').removeClass('hidden');
if (this.totalWidth <= this.availableWidth) {
// no need to compute breadcrumbs, there is enough space
return;
}
this.lastWidth = width;
// running width, considering the hidden crumbs
var currentTotalWidth = $(this.breadcrumbs[0]).data('real-width');
var firstHidden = true;
// insert ellipsis after root part (root part is always visible)
$ellipsisCrumb = $('<div class="crumb ellipsized svg"><span class="ellipsis">...</span></div>');
$(this.breadcrumbs[0]).after($ellipsisCrumb);
currentTotalWidth += $ellipsisCrumb.width();
i = this.breadcrumbs.length - 1;
// find the first section that would cause the overflow
// then hide everything in front of that
//
// this ensures that the last crumb section stays visible
// for most of the cases and is always the last one to be
// hidden when the screen becomes very narrow
while (i > 0) {
$crumb = $(this.breadcrumbs[i]);
// if the current breadcrumb would cause overflow
if (!firstHidden || currentTotalWidth + $crumb.data('real-width') > this.availableWidth) {
// hide it
$crumb.addClass('hidden');
if (firstHidden) {
// set the path of this one as title for the ellipsis
this.$el.find('.crumb.ellipsized')
.attr('title', $crumb.attr('data-dir'))
.tipsy();
}
// and all the previous ones (going backwards)
firstHidden = false;
} else {
// add to total width
currentTotalWidth += $crumb.data('real-width');
}
i--;
}
if (!OC.Util.hasSVGSupport()) {
OC.Util.replaceSVG(this.$el);
}
}
};

View File

@ -179,9 +179,20 @@ OC.Upload = {
callbacks.onNoConflicts(selection);
},
_hideProgressBar: function() {
$('#uploadprogresswrapper input.stop').fadeOut();
$('#uploadprogressbar').fadeOut(function() {
$('#file_upload_start').trigger(new $.Event('resized'));
});
},
_showProgressBar: function() {
$('#uploadprogressbar').fadeIn();
$('#file_upload_start').trigger(new $.Event('resized'));
},
init: function() {
if ( $('#file_upload_start').exists() ) {
var file_upload_param = {
dropZone: $('#content'), // restrict dropZone to content div
autoUpload: false,
@ -444,7 +455,7 @@ OC.Upload = {
OC.Upload.log('progress handle fileuploadstart', e, data);
$('#uploadprogresswrapper input.stop').show();
$('#uploadprogressbar').progressbar({value: 0});
$('#uploadprogressbar').fadeIn();
OC.Upload._showProgressBar();
});
fileupload.on('fileuploadprogress', function(e, data) {
OC.Upload.log('progress handle fileuploadprogress', e, data);
@ -458,15 +469,13 @@ OC.Upload = {
fileupload.on('fileuploadstop', function(e, data) {
OC.Upload.log('progress handle fileuploadstop', e, data);
$('#uploadprogresswrapper input.stop').fadeOut();
$('#uploadprogressbar').fadeOut();
OC.Upload._hideProgressBar();
});
fileupload.on('fileuploadfail', function(e, data) {
OC.Upload.log('progress handle fileuploadfail', e, data);
//if user pressed cancel hide upload progress bar and cancel button
if (data.errorThrown === 'abort') {
$('#uploadprogresswrapper input.stop').fadeOut();
$('#uploadprogressbar').fadeOut();
OC.Upload._hideProgressBar();
}
});
@ -649,7 +658,7 @@ OC.Upload = {
//IE < 10 does not fire the necessary events for the progress bar.
if ($('html.lte9').length === 0) {
$('#uploadprogressbar').progressbar({value: 0});
$('#uploadprogressbar').fadeIn();
OC.Upload._showProgressBar();
}
var eventSource = new OC.EventSource(
@ -668,12 +677,12 @@ OC.Upload = {
});
eventSource.listen('success', function(data) {
var file = data;
$('#uploadprogressbar').fadeOut();
OC.Upload._hideProgressBar();
FileList.add(file, {hidden: hidden, animate: true});
});
eventSource.listen('error', function(error) {
$('#uploadprogressbar').fadeOut();
OC.Upload._hideProgressBar();
var message = (error && error.message) || t('core', 'Error fetching URL');
OC.Notification.show(message);
//hide notification after 10 sec

View File

@ -150,11 +150,10 @@
this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this));
$(window).resize(function() {
// TODO: debounce this ?
var width = $(this).width();
self.breadcrumb.resize(width, false);
});
this._onResize = _.debounce(_.bind(this._onResize, this), 100);
$(window).resize(this._onResize);
this.$el.on('show', this._onResize);
this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this));
this.$fileList.on('change', 'td.filename>input:checkbox', _.bind(this._onClickFileCheckbox, this));
@ -176,6 +175,22 @@
}
},
/**
* Event handler for when the window size changed
*/
_onResize: function() {
var containerWidth = this.$el.width();
var actionsWidth = 0;
$.each(this.$el.find('#controls .actions'), function(index, action) {
actionsWidth += $(action).outerWidth();
});
// substract app navigation toggle when visible
containerWidth -= $('#app-navigation-toggle').width();
this.breadcrumb.setMaxWidth(containerWidth - actionsWidth - 10);
},
/**
* Event handler for when the URL changed
*/
@ -1530,6 +1545,9 @@
// handle upload events
var fileUploadStart = this.$el.find('#file_upload_start');
// detect the progress bar resize
fileUploadStart.on('resized', this._onResize);
fileUploadStart.on('fileuploaddrop', function(e, data) {
OC.Upload.log('filelist handle fileuploaddrop', e, data);

View File

@ -19,7 +19,6 @@
*
*/
/* global BreadCrumb */
describe('OCA.Files.BreadCrumb tests', function() {
var BreadCrumb = OCA.Files.BreadCrumb;
@ -131,48 +130,42 @@ describe('OCA.Files.BreadCrumb tests', function() {
});
});
describe('Resizing', function() {
var bc, widthStub, dummyDir,
oldUpdateTotalWidth;
var bc, dummyDir, widths, oldUpdateTotalWidth;
beforeEach(function() {
dummyDir = '/short name/longer name/looooooooooooonger/even longer long long long longer long/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/last one';
dummyDir = '/short name/longer name/looooooooooooonger/' +
'even longer long long long longer long/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/last one';
// using hard-coded widths (pre-measured) to avoid getting different
// results on different browsers due to font engine differences
widths = [41, 106, 112, 160, 257, 251, 91];
oldUpdateTotalWidth = BreadCrumb.prototype._updateTotalWidth;
BreadCrumb.prototype._updateTotalWidth = function() {
// need to set display:block for correct offsetWidth (no CSS loaded here)
$('div.crumb').css({
'display': 'block',
'float': 'left'
// pre-set a width to simulate consistent measurement
$('div.crumb').each(function(index){
$(this).css('width', widths[index]);
});
return oldUpdateTotalWidth.apply(this, arguments);
};
bc = new BreadCrumb();
widthStub = sinon.stub($.fn, 'width');
// append dummy navigation and controls
// as they are currently used for measurements
$('#testArea').append(
'<div id="navigation" style="width: 80px"></div>',
'<div id="controls"></div>'
);
// make sure we know the test screen width
$('#testArea').css('width', 1280);
// use test area as we need it for measurements
$('#controls').append(bc.$el);
$('#controls').append('<div class="actions"><div>Dummy action with a given width</div></div>');
});
afterEach(function() {
BreadCrumb.prototype._updateTotalWidth = oldUpdateTotalWidth;
widthStub.restore();
bc = null;
});
it('Hides breadcrumbs to fit window', function() {
it('Hides breadcrumbs to fit max allowed width', function() {
var $crumbs;
widthStub.returns(500);
bc.setMaxWidth(500);
// triggers resize implicitly
bc.setDirectory(dummyDir);
$crumbs = bc.$el.find('.crumb');
@ -190,19 +183,23 @@ describe('OCA.Files.BreadCrumb tests', function() {
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 ellipsis on window size increase', function() {
it('Updates the breadcrumbs when reducing max allowed width', function() {
var $crumbs;
widthStub.returns(500);
// enough space
bc.setMaxWidth(1800);
expect(bc.$el.find('.ellipsis').length).toEqual(0);
// triggers resize implicitly
bc.setDirectory(dummyDir);
$crumbs = bc.$el.find('.crumb');
// simulate increase
$('#testArea').css('width', 1800);
bc.resize(1800);
bc.setMaxWidth(950);
$crumbs = bc.$el.find('.crumb');
// first one is always visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
// second one has ellipsis
@ -213,37 +210,35 @@ describe('OCA.Files.BreadCrumb tests', function() {
// subsequent elements are hidden
expect($crumbs.eq(2).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
// the rest is visible
expect($crumbs.eq(4).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
});
it('Updates ellipsis on window size decrease', function() {
it('Removes the ellipsis when there is enough space', function() {
var $crumbs;
$('#testArea').css('width', 2000);
widthStub.returns(2000);
bc.setMaxWidth(500);
// triggers resize implicitly
bc.setDirectory(dummyDir);
$crumbs = bc.$el.find('.crumb');
// simulate decrease
bc.resize(500);
$('#testArea').css('width', 500);
// ellipsis
expect(bc.$el.find('.ellipsis').length).toEqual(1);
// first one is always visible
// simulate increase
bc.setMaxWidth(1800);
// no ellipsis
expect(bc.$el.find('.ellipsis').length).toEqual(0);
// all are visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
// second one has ellipsis
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).find('.ellipsis').length).toEqual(1);
// there is only one ellipsis in total
expect($crumbs.find('.ellipsis').length).toEqual(1);
// subsequent elements are hidden
expect($crumbs.eq(2).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
// the rest is visible
expect($crumbs.eq(5).hasClass('hidden')).toEqual(true);
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);
});
});

View File

@ -21,6 +21,7 @@
describe('OCA.Files.FileList tests', function() {
var testFiles, alertStub, notificationStub, fileList;
var bcResizeStub;
/**
* Generate test file data
@ -52,6 +53,9 @@ describe('OCA.Files.FileList tests', function() {
beforeEach(function() {
alertStub = sinon.stub(OC.dialogs, 'alert');
notificationStub = sinon.stub(OC.Notification, 'show');
// prevent resize algo to mess up breadcrumb order while
// testing
bcResizeStub = sinon.stub(OCA.Files.BreadCrumb.prototype, '_resize');
// init parameters and test table elements
$('#testArea').append(
@ -125,6 +129,7 @@ describe('OCA.Files.FileList tests', function() {
notificationStub.restore();
alertStub.restore();
bcResizeStub.restore();
});
describe('Getters', function() {
it('Returns the current directory', function() {