Merge pull request #9174 from owncloud/breadcrumbfix

Breadcrumb width calculation fix
This commit is contained in:
Jan-Christoph Borchardt 2014-06-27 13:51:18 +02:00
commit b975f0e718
9 changed files with 172 additions and 142 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
*/
@ -1538,6 +1553,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() {

View File

@ -110,7 +110,6 @@
min-width: initial !important;
left: 0 !important;
padding-left: 0;
padding-right: 0 !important;
}
/* position controls for apps with app-navigation */
#app-navigation+#app-content #controls {

View File

@ -261,11 +261,9 @@ input[type="submit"].enabled {
/* position controls for apps with app-navigation */
#app-navigation+#app-content #controls {
left: 250px;
padding-right: 250px;
}
.viewer-mode #app-navigation+#app-content #controls {
left: 0;
padding-right: 0;
}
#controls .button,

View File

@ -85,7 +85,7 @@ window.Snap.prototype = {
beforeEach(function() {
// test area for elements that need absolute selector access or measure widths/heights
// which wouldn't work for detached or hidden elements
$testArea = $('<div id="testArea" style="position: absolute; width: 1280px; height: 800px; top: -3000px; left: -3000px;"></div>');
$testArea = $('<div id="testArea" style="position: absolute; width: 1280px; height: 800px; top: -3000px; left: -3000px; opacity: 0;"></div>');
$('body').append($testArea);
// enforce fake XHR, tests should not depend on the server and
// must use fake responses for expected calls

View File

@ -110,15 +110,16 @@ module.exports = function(config) {
// core mocks
files.push(corePath + 'tests/specHelper.js');
var srcFile, i;
// add core library files
for ( var i = 0; i < coreModule.libraries.length; i++ ) {
var srcFile = corePath + coreModule.libraries[i];
for ( i = 0; i < coreModule.libraries.length; i++ ) {
srcFile = corePath + coreModule.libraries[i];
files.push(srcFile);
}
// add core modules files
for ( var i = 0; i < coreModule.modules.length; i++ ) {
var srcFile = corePath + coreModule.modules[i];
for ( i = 0; i < coreModule.modules.length; i++ ) {
srcFile = corePath + coreModule.modules[i];
files.push(srcFile);
if (enableCoverage) {
preprocessors[srcFile] = 'coverage';
@ -155,12 +156,15 @@ module.exports = function(config) {
}
// add source files for apps to test
for ( var i = 0; i < appsToTest.length; i++ ) {
for ( i = 0; i < appsToTest.length; i++ ) {
addApp(appsToTest[i]);
}
// serve images to avoid warnings
files.push({pattern: 'core/img/**/*', watched: false, included: false, served: true});
// include core CSS
files.push({pattern: 'core/css/*.css', watched: true, included: true, served: true});
config.set({
@ -180,7 +184,9 @@ module.exports = function(config) {
proxies: {
// prevent warnings for images
'/context.html//core/img/': 'http://localhost:9876/base/core/img/'
'/context.html//core/img/': 'http://localhost:9876/base/core/img/',
'/context.html//core/css/': 'http://localhost:9876/base/core/css/',
'/context.html//core/fonts/': 'http://localhost:9876/base/core/fonts/'
},
// test results reporter to use