server/apps/files/js/breadcrumb.js
Roeland Jago Douma 6b931eb64b
Merge pull request #8588 from nextcloud/fix-breadcrumbs-width-calculation
Fix breadcrumbs width calculation
2018-03-01 20:13:26 +01:00

368 lines
10 KiB
JavaScript

/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
(function() {
/**
* @class BreadCrumb
* @memberof OCA.Files
* @classdesc Breadcrumbs that represent the current path.
*
* @param {Object} [options] options
* @param {Function} [options.onClick] click event handler
* @param {Function} [options.onDrop] drop event handler
* @param {Function} [options.getCrumbUrl] callback that returns
* the URL of a given breadcrumb
*/
var BreadCrumb = function(options){
this.$el = $('<div class="breadcrumb"></div>');
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;
}
if (options.onDrop) {
this.onDrop = options.onDrop;
this.onOver = options.onOver;
this.onOut = options.onOut;
}
if (options.getCrumbUrl) {
this.getCrumbUrl = options.getCrumbUrl;
}
this._detailViews = [];
};
/**
* @memberof OCA.Files
*/
BreadCrumb.prototype = {
$el: null,
dir: null,
dirInfo: null,
/**
* Total width of all breadcrumbs
* @type int
* @private
*/
totalWidth: 0,
breadcrumbs: [],
onClick: null,
onDrop: null,
onOver: null,
onOut: null,
/**
* Sets the directory to be displayed as breadcrumb.
* This will re-render the breadcrumb.
* @param dir path to be displayed as breadcrumb
*/
setDirectory: function(dir) {
dir = dir.replace(/\\/g, '/');
dir = dir || '/';
if (dir !== this.dir) {
this.dir = dir;
this.render();
}
},
setDirectoryInfo: function(dirInfo) {
if (dirInfo !== this.dirInfo) {
this.dirInfo = dirInfo;
this.render();
}
},
/**
* @param {Backbone.View} detailView
*/
addDetailView: function(detailView) {
this._detailViews.push(detailView);
},
/**
* Returns the full URL to the given directory
*
* @param {Object.<String, String>} part crumb data as map
* @param {int} index crumb index
* @return full URL
*/
getCrumbUrl: function(part, index) {
return '#';
},
/**
* Renders the breadcrumb elements
*/
render: function() {
// Menu is destroyed on every change, we need to init it
OC.unregisterMenu($('.crumbmenu > .icon-more'), $('.crumbmenu > .popovermenu'));
var parts = this._makeCrumbs(this.dir || '/');
var $crumb;
var $menuItem;
this.$el.empty();
this.breadcrumbs = [];
for (var i = 0; i < parts.length; i++) {
var part = parts[i];
var $image;
var $link = $('<a></a>');
$crumb = $('<div class="crumb svg"></div>');
if(part.dir) {
$link.attr('href', this.getCrumbUrl(part, i));
}
if(part.name) {
$link.text(part.name);
}
$link.addClass(part.linkclass);
$crumb.append($link);
$crumb.data('dir', part.dir);
// Ignore menu button
$crumb.data('crumb-id', i - 1);
$crumb.addClass(part.class);
if (part.img) {
$image = $('<img class="svg"></img>');
$image.attr('src', part.img);
$image.attr('alt', part.alt);
$link.append($image);
}
this.breadcrumbs.push($crumb);
this.$el.append($crumb);
// Only add feedback if not menu
if (this.onClick && i !== 0) {
$link.on('click', this.onClick);
}
}
// Menu creation
this._createMenu();
for (var j = 0; j < parts.length; j++) {
var menuPart = parts[j];
if(menuPart.dir) {
$menuItem = $('<li class="crumblist"><a><span class="icon-folder"></span><span></span></a></li>');
$menuItem.data('dir', menuPart.dir);
$menuItem.find('a').attr('href', this.getCrumbUrl(part, j));
$menuItem.find('span:eq(1)').text(menuPart.name);
this.$menu.children('ul').append($menuItem);
if (this.onClick) {
$menuItem.on('click', this.onClick);
}
}
}
_.each(this._detailViews, function(view) {
view.render({
dirInfo: this.dirInfo
});
$crumb.append(view.$el);
}, this);
// in case svg is not supported by the browser we need to execute the fallback mechanism
if (!OC.Util.hasSVGSupport()) {
OC.Util.replaceSVG(this.$el);
}
// setup drag and drop
if (this.onDrop) {
this.$el.find('.crumb:not(:last-child):not(.crumbmenu), .crumblist:not(:last-child)').droppable({
drop: this.onDrop,
over: this.onOver,
out: this.onOut,
tolerance: 'pointer',
hoverClass: 'canDrop',
greedy: true
});
}
// Menu is destroyed on every change, we need to init it
OC.registerMenu($('.crumbmenu > .icon-more'), $('.crumbmenu > .popovermenu'));
this._resize();
},
/**
* Makes a breadcrumb structure based on the given path
*
* @param {String} dir path to split into a breadcrumb structure
* @return {Object.<String, String>} map of {dir: path, name: displayName}
*/
_makeCrumbs: function(dir) {
var crumbs = [];
var pathToHere = '';
// trim leading and trailing slashes
dir = dir.replace(/^\/+|\/+$/g, '');
var parts = dir.split('/');
if (dir === '') {
parts = [];
}
// menu part
crumbs.push({
class: 'crumbmenu hidden',
linkclass: 'icon-more menutoggle'
});
// root part
crumbs.push({
name: t('core', 'Home'),
dir: '/',
class: 'crumbhome',
linkclass: 'icon-home'
});
for (var i = 0; i < parts.length; i++) {
var part = parts[i];
pathToHere = pathToHere + '/' + part;
crumbs.push({
dir: pathToHere,
name: part
});
}
return crumbs;
},
/**
* Calculate real width based on individual crumbs
*
* @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(true);
}
}
return totalWidth;
},
/**
* Hide the middle crumb
*/
_hideCrumb: function() {
var length = this.$el.find(this.crumbSelector).length;
// Get the middle one floored down
var elmt = Math.floor(length / 2 - 0.5);
this.$el.find(this.crumbSelector+':eq('+elmt+')').addClass('hidden');
},
/**
* Get the crumb to show
*/
_getCrumbElement: function() {
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(this.hiddenCrumbSelector + ':eq('+elmt+')');
},
/**
* Show the middle crumb
*/
_showCrumb: function() {
if(this.$el.find(this.hiddenCrumbSelector).length === 1) {
this.$el.find(this.hiddenCrumbSelector).removeClass('hidden');
}
this._getCrumbElement().removeClass('hidden');
},
/**
* Create and append the popovermenu
*/
_createMenu: function() {
this.$el.find('.crumbmenu').append(this.$menu);
this.$menu.children('ul').empty();
},
/**
* Update the popovermenu
*/
_updateMenu: function() {
var menuItems = this.$el.find(this.hiddenCrumbSelector);
this.$menu.find('li').addClass('in-breadcrumb');
for (var i = 0; i < menuItems.length; i++) {
var crumbId = $(menuItems[i]).data('crumb-id');
this.$menu.find('li:eq('+crumbId+')').removeClass('in-breadcrumb');
}
},
_resize: function() {
if (this.breadcrumbs.length <= 2) {
// home & menu
return;
}
// 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() > 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();
}
this._updateMenu();
}
};
OCA.Files.BreadCrumb = BreadCrumb;
})();