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, $el: null,
dir: null, dir: null,
lastWidth: 0, /**
hiddenBreadcrumbs: 0, * Total width of all breadcrumbs
*/
totalWidth: 0, totalWidth: 0,
breadcrumbs: [], breadcrumbs: [],
onClick: null, onClick: null,
@ -116,7 +117,6 @@
} }
this._updateTotalWidth(); this._updateTotalWidth();
this.resize($(window).width(), true);
}, },
/** /**
@ -150,93 +150,93 @@
return crumbs; return crumbs;
}, },
/**
* Calculate the total breadcrumb width when
* all crumbs are expanded
*/
_updateTotalWidth: function () { _updateTotalWidth: function () {
var self = this; this.totalWidth = 0;
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;
for (var i = 0; i < this.breadcrumbs.length; i++ ) { 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();
} }
this._resize();
$.each($('#controls .actions'), function(index, action) {
self.totalWidth += $(action).outerWidth();
});
}, },
/** /**
* Show/hide breadcrumbs to fit the given width * Show/hide breadcrumbs to fit the given width
*/ */
resize: function (width, firstRun) { setMaxWidth: function (availableWidth) {
var i, $crumb; 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; return;
} }
// window was shrinked since last time or first run ? // reset crumbs
if ((width < this.lastWidth || firstRun) && width < this.totalWidth) { this.$el.find('.crumb.ellipsized').remove();
if (this.hiddenBreadcrumbs === 0 && this.breadcrumbs.length > 1) {
// start by hiding the first breadcrumb after home, // unhide all
// that one will have extra three dots displayed this.$el.find('.crumb.hidden').removeClass('hidden');
$crumb = this.breadcrumbs[1];
this.totalWidth -= $crumb.get(0).offsetWidth; if (this.totalWidth <= this.availableWidth) {
$crumb.find('a').addClass('hidden'); // no need to compute breadcrumbs, there is enough space
$crumb.append('<span class="ellipsis">...</span>'); return;
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;
}
} }
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); 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() { init: function() {
if ( $('#file_upload_start').exists() ) { if ( $('#file_upload_start').exists() ) {
var file_upload_param = { var file_upload_param = {
dropZone: $('#content'), // restrict dropZone to content div dropZone: $('#content'), // restrict dropZone to content div
autoUpload: false, autoUpload: false,
@ -444,7 +455,7 @@ OC.Upload = {
OC.Upload.log('progress handle fileuploadstart', e, data); OC.Upload.log('progress handle fileuploadstart', e, data);
$('#uploadprogresswrapper input.stop').show(); $('#uploadprogresswrapper input.stop').show();
$('#uploadprogressbar').progressbar({value: 0}); $('#uploadprogressbar').progressbar({value: 0});
$('#uploadprogressbar').fadeIn(); OC.Upload._showProgressBar();
}); });
fileupload.on('fileuploadprogress', function(e, data) { fileupload.on('fileuploadprogress', function(e, data) {
OC.Upload.log('progress handle fileuploadprogress', e, data); OC.Upload.log('progress handle fileuploadprogress', e, data);
@ -458,15 +469,13 @@ OC.Upload = {
fileupload.on('fileuploadstop', function(e, data) { fileupload.on('fileuploadstop', function(e, data) {
OC.Upload.log('progress handle fileuploadstop', e, data); OC.Upload.log('progress handle fileuploadstop', e, data);
$('#uploadprogresswrapper input.stop').fadeOut(); OC.Upload._hideProgressBar();
$('#uploadprogressbar').fadeOut();
}); });
fileupload.on('fileuploadfail', function(e, data) { fileupload.on('fileuploadfail', function(e, data) {
OC.Upload.log('progress handle fileuploadfail', e, data); OC.Upload.log('progress handle fileuploadfail', e, data);
//if user pressed cancel hide upload progress bar and cancel button //if user pressed cancel hide upload progress bar and cancel button
if (data.errorThrown === 'abort') { if (data.errorThrown === 'abort') {
$('#uploadprogresswrapper input.stop').fadeOut(); OC.Upload._hideProgressBar();
$('#uploadprogressbar').fadeOut();
} }
}); });
@ -649,7 +658,7 @@ OC.Upload = {
//IE < 10 does not fire the necessary events for the progress bar. //IE < 10 does not fire the necessary events for the progress bar.
if ($('html.lte9').length === 0) { if ($('html.lte9').length === 0) {
$('#uploadprogressbar').progressbar({value: 0}); $('#uploadprogressbar').progressbar({value: 0});
$('#uploadprogressbar').fadeIn(); OC.Upload._showProgressBar();
} }
var eventSource = new OC.EventSource( var eventSource = new OC.EventSource(
@ -668,12 +677,12 @@ OC.Upload = {
}); });
eventSource.listen('success', function(data) { eventSource.listen('success', function(data) {
var file = data; var file = data;
$('#uploadprogressbar').fadeOut(); OC.Upload._hideProgressBar();
FileList.add(file, {hidden: hidden, animate: true}); FileList.add(file, {hidden: hidden, animate: true});
}); });
eventSource.listen('error', function(error) { eventSource.listen('error', function(error) {
$('#uploadprogressbar').fadeOut(); OC.Upload._hideProgressBar();
var message = (error && error.message) || t('core', 'Error fetching URL'); var message = (error && error.message) || t('core', 'Error fetching URL');
OC.Notification.show(message); OC.Notification.show(message);
//hide notification after 10 sec //hide notification after 10 sec

View File

@ -150,11 +150,10 @@
this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this)); this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this));
$(window).resize(function() { this._onResize = _.debounce(_.bind(this._onResize, this), 100);
// TODO: debounce this ? $(window).resize(this._onResize);
var width = $(this).width();
self.breadcrumb.resize(width, false); this.$el.on('show', this._onResize);
});
this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this)); this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this));
this.$fileList.on('change', 'td.filename>input:checkbox', _.bind(this._onClickFileCheckbox, 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 * Event handler for when the URL changed
*/ */
@ -1530,6 +1545,9 @@
// handle upload events // handle upload events
var fileUploadStart = this.$el.find('#file_upload_start'); var fileUploadStart = this.$el.find('#file_upload_start');
// detect the progress bar resize
fileUploadStart.on('resized', this._onResize);
fileUploadStart.on('fileuploaddrop', function(e, data) { fileUploadStart.on('fileuploaddrop', function(e, data) {
OC.Upload.log('filelist handle fileuploaddrop', e, data); OC.Upload.log('filelist handle fileuploaddrop', e, data);

View File

@ -19,7 +19,6 @@
* *
*/ */
/* global BreadCrumb */
describe('OCA.Files.BreadCrumb tests', function() { describe('OCA.Files.BreadCrumb tests', function() {
var BreadCrumb = OCA.Files.BreadCrumb; var BreadCrumb = OCA.Files.BreadCrumb;
@ -131,48 +130,42 @@ describe('OCA.Files.BreadCrumb tests', function() {
}); });
}); });
describe('Resizing', function() { describe('Resizing', function() {
var bc, widthStub, dummyDir, var bc, dummyDir, widths, oldUpdateTotalWidth;
oldUpdateTotalWidth;
beforeEach(function() { 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; oldUpdateTotalWidth = BreadCrumb.prototype._updateTotalWidth;
BreadCrumb.prototype._updateTotalWidth = function() { BreadCrumb.prototype._updateTotalWidth = function() {
// need to set display:block for correct offsetWidth (no CSS loaded here) // pre-set a width to simulate consistent measurement
$('div.crumb').css({ $('div.crumb').each(function(index){
'display': 'block', $(this).css('width', widths[index]);
'float': 'left'
}); });
return oldUpdateTotalWidth.apply(this, arguments); return oldUpdateTotalWidth.apply(this, arguments);
}; };
bc = new BreadCrumb(); bc = new BreadCrumb();
widthStub = sinon.stub($.fn, 'width');
// append dummy navigation and controls // append dummy navigation and controls
// as they are currently used for measurements // as they are currently used for measurements
$('#testArea').append( $('#testArea').append(
'<div id="navigation" style="width: 80px"></div>',
'<div id="controls"></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(bc.$el);
$('#controls').append('<div class="actions"><div>Dummy action with a given width</div></div>');
}); });
afterEach(function() { afterEach(function() {
BreadCrumb.prototype._updateTotalWidth = oldUpdateTotalWidth; BreadCrumb.prototype._updateTotalWidth = oldUpdateTotalWidth;
widthStub.restore();
bc = null; bc = null;
}); });
it('Hides breadcrumbs to fit window', function() { it('Hides breadcrumbs to fit max allowed width', function() {
var $crumbs; var $crumbs;
widthStub.returns(500); bc.setMaxWidth(500);
// triggers resize implicitly // triggers resize implicitly
bc.setDirectory(dummyDir); bc.setDirectory(dummyDir);
$crumbs = bc.$el.find('.crumb'); $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(4).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(true); expect($crumbs.eq(5).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(6).hasClass('hidden')).toEqual(false); 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; var $crumbs;
widthStub.returns(500); // enough space
bc.setMaxWidth(1800);
expect(bc.$el.find('.ellipsis').length).toEqual(0);
// triggers resize implicitly // triggers resize implicitly
bc.setDirectory(dummyDir); bc.setDirectory(dummyDir);
$crumbs = bc.$el.find('.crumb');
// simulate increase // simulate increase
$('#testArea').css('width', 1800); bc.setMaxWidth(950);
bc.resize(1800);
$crumbs = bc.$el.find('.crumb');
// first one is always visible // first one is always visible
expect($crumbs.eq(0).hasClass('hidden')).toEqual(false); expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
// second one has ellipsis // second one has ellipsis
@ -213,37 +210,35 @@ describe('OCA.Files.BreadCrumb tests', function() {
// subsequent elements are hidden // subsequent elements are hidden
expect($crumbs.eq(2).hasClass('hidden')).toEqual(true); expect($crumbs.eq(2).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(3).hasClass('hidden')).toEqual(true); expect($crumbs.eq(3).hasClass('hidden')).toEqual(true);
expect($crumbs.eq(4).hasClass('hidden')).toEqual(true);
// the rest is visible // the rest is visible
expect($crumbs.eq(4).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(5).hasClass('hidden')).toEqual(false); expect($crumbs.eq(5).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(6).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; var $crumbs;
$('#testArea').css('width', 2000); bc.setMaxWidth(500);
widthStub.returns(2000);
// triggers resize implicitly // triggers resize implicitly
bc.setDirectory(dummyDir); bc.setDirectory(dummyDir);
$crumbs = bc.$el.find('.crumb'); $crumbs = bc.$el.find('.crumb');
// simulate decrease // ellipsis
bc.resize(500); expect(bc.$el.find('.ellipsis').length).toEqual(1);
$('#testArea').css('width', 500);
// 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); expect($crumbs.eq(0).hasClass('hidden')).toEqual(false);
// second one has ellipsis
expect($crumbs.eq(1).hasClass('hidden')).toEqual(false); expect($crumbs.eq(1).hasClass('hidden')).toEqual(false);
expect($crumbs.eq(1).find('.ellipsis').length).toEqual(1); expect($crumbs.eq(2).hasClass('hidden')).toEqual(false);
// there is only one ellipsis in total expect($crumbs.eq(3).hasClass('hidden')).toEqual(false);
expect($crumbs.find('.ellipsis').length).toEqual(1); expect($crumbs.eq(4).hasClass('hidden')).toEqual(false);
// subsequent elements are hidden expect($crumbs.eq(5).hasClass('hidden')).toEqual(false);
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(6).hasClass('hidden')).toEqual(false); expect($crumbs.eq(6).hasClass('hidden')).toEqual(false);
}); });
}); });

View File

@ -21,6 +21,7 @@
describe('OCA.Files.FileList tests', function() { describe('OCA.Files.FileList tests', function() {
var testFiles, alertStub, notificationStub, fileList; var testFiles, alertStub, notificationStub, fileList;
var bcResizeStub;
/** /**
* Generate test file data * Generate test file data
@ -52,6 +53,9 @@ describe('OCA.Files.FileList tests', function() {
beforeEach(function() { beforeEach(function() {
alertStub = sinon.stub(OC.dialogs, 'alert'); alertStub = sinon.stub(OC.dialogs, 'alert');
notificationStub = sinon.stub(OC.Notification, 'show'); 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 // init parameters and test table elements
$('#testArea').append( $('#testArea').append(
@ -125,6 +129,7 @@ describe('OCA.Files.FileList tests', function() {
notificationStub.restore(); notificationStub.restore();
alertStub.restore(); alertStub.restore();
bcResizeStub.restore();
}); });
describe('Getters', function() { describe('Getters', function() {
it('Returns the current directory', function() { it('Returns the current directory', function() {