breadcrumb.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. /**
  2. * ownCloud
  3. *
  4. * @author Vincent Petry
  5. * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
  6. *
  7. * This library is free software; you can redistribute it and/or
  8. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  9. * License as published by the Free Software Foundation; either
  10. * version 3 of the License, or any later version.
  11. *
  12. * This library is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public
  18. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  19. *
  20. */
  21. (function() {
  22. /**
  23. * @class BreadCrumb
  24. * @memberof OCA.Files
  25. * @classdesc Breadcrumbs that represent the current path.
  26. *
  27. * @param {Object} [options] options
  28. * @param {Function} [options.onClick] click event handler
  29. * @param {Function} [options.onDrop] drop event handler
  30. * @param {Function} [options.getCrumbUrl] callback that returns
  31. * the URL of a given breadcrumb
  32. */
  33. var BreadCrumb = function(options){
  34. this.$el = $('<nav></nav>');
  35. this.$menu = $('<div class="popovermenu menu-center"><ul></ul></div>');
  36. this.crumbSelector = '.crumb:not(.hidden):not(.crumbhome):not(.crumbmenu)';
  37. this.hiddenCrumbSelector = '.crumb.hidden:not(.crumbhome):not(.crumbmenu)';
  38. options = options || {};
  39. if (options.onClick) {
  40. this.onClick = options.onClick;
  41. }
  42. if (options.onDrop) {
  43. this.onDrop = options.onDrop;
  44. this.onOver = options.onOver;
  45. this.onOut = options.onOut;
  46. }
  47. if (options.getCrumbUrl) {
  48. this.getCrumbUrl = options.getCrumbUrl;
  49. }
  50. this._detailViews = [];
  51. };
  52. /**
  53. * @memberof OCA.Files
  54. */
  55. BreadCrumb.prototype = {
  56. $el: null,
  57. dir: null,
  58. dirInfo: null,
  59. /**
  60. * Total width of all breadcrumbs
  61. * @type int
  62. * @private
  63. */
  64. totalWidth: 0,
  65. breadcrumbs: [],
  66. onClick: null,
  67. onDrop: null,
  68. onOver: null,
  69. onOut: null,
  70. /**
  71. * Sets the directory to be displayed as breadcrumb.
  72. * This will re-render the breadcrumb.
  73. * @param dir path to be displayed as breadcrumb
  74. */
  75. setDirectory: function(dir) {
  76. dir = dir.replace(/\\/g, '/');
  77. dir = dir || '/';
  78. if (dir !== this.dir) {
  79. this.dir = dir;
  80. this.render();
  81. }
  82. },
  83. setDirectoryInfo: function(dirInfo) {
  84. if (dirInfo !== this.dirInfo) {
  85. this.dirInfo = dirInfo;
  86. this.render();
  87. }
  88. },
  89. /**
  90. * @param {Backbone.View} detailView
  91. */
  92. addDetailView: function(detailView) {
  93. this._detailViews.push(detailView);
  94. },
  95. /**
  96. * Returns the full URL to the given directory
  97. *
  98. * @param {Object.<String, String>} part crumb data as map
  99. * @param {number} index crumb index
  100. * @return full URL
  101. */
  102. getCrumbUrl: function(part, index) {
  103. return '#';
  104. },
  105. /**
  106. * Renders the breadcrumb elements
  107. */
  108. render: function() {
  109. // Menu is destroyed on every change, we need to init it
  110. OC.unregisterMenu($('.crumbmenu > .icon-more'), $('.crumbmenu > .popovermenu'));
  111. var parts = this._makeCrumbs(this.dir || '/');
  112. var $crumb;
  113. var $menuItem;
  114. this.$el.empty();
  115. this.breadcrumbs = [];
  116. var $crumbList = $('<ul class="breadcrumb"></ul>');
  117. for (var i = 0; i < parts.length; i++) {
  118. var part = parts[i];
  119. var $image;
  120. var $link = $('<a></a>');
  121. $crumb = $('<li class="crumb svg"></li>');
  122. if(part.dir) {
  123. $link.attr('href', this.getCrumbUrl(part, i));
  124. }
  125. if(part.name) {
  126. $link.text(part.name);
  127. }
  128. $link.addClass(part.linkclass);
  129. $crumb.append($link);
  130. $crumb.data('dir', part.dir);
  131. // Ignore menu button
  132. $crumb.data('crumb-id', i - 1);
  133. $crumb.addClass(part.class);
  134. if (part.img) {
  135. $image = $('<img class="svg"></img>');
  136. $image.attr('src', part.img);
  137. $image.attr('alt', part.alt);
  138. $link.append($image);
  139. }
  140. this.breadcrumbs.push($crumb);
  141. $crumbList.append($crumb);
  142. // Only add feedback if not menu
  143. if (this.onClick && i !== 0) {
  144. $link.on('click', this.onClick);
  145. }
  146. }
  147. this.$el.append($crumbList);
  148. // Menu creation
  149. this._createMenu();
  150. for (var j = 0; j < parts.length; j++) {
  151. var menuPart = parts[j];
  152. if(menuPart.dir) {
  153. $menuItem = $('<li class="crumblist"><a><span class="icon-folder"></span><span></span></a></li>');
  154. $menuItem.data('dir', menuPart.dir);
  155. $menuItem.find('a').attr('href', this.getCrumbUrl(part, j));
  156. $menuItem.find('span:eq(1)').text(menuPart.name);
  157. this.$menu.children('ul').append($menuItem);
  158. if (this.onClick) {
  159. $menuItem.on('click', this.onClick);
  160. }
  161. }
  162. }
  163. _.each(this._detailViews, function(view) {
  164. view.render({
  165. dirInfo: this.dirInfo
  166. });
  167. $crumb.append(view.$el);
  168. $menuItem.append(view.$el.clone(true));
  169. }, this);
  170. // setup drag and drop
  171. if (this.onDrop) {
  172. this.$el.find('.crumb:not(:last-child):not(.crumbmenu), .crumblist:not(:last-child)').droppable({
  173. drop: this.onDrop,
  174. over: this.onOver,
  175. out: this.onOut,
  176. tolerance: 'pointer',
  177. hoverClass: 'canDrop',
  178. greedy: true
  179. });
  180. }
  181. // Menu is destroyed on every change, we need to init it
  182. OC.registerMenu($('.crumbmenu > .icon-more'), $('.crumbmenu > .popovermenu'));
  183. this._resize();
  184. },
  185. /**
  186. * Makes a breadcrumb structure based on the given path
  187. *
  188. * @param {String} dir path to split into a breadcrumb structure
  189. * @param {String} [rootIcon=icon-home] icon to use for root
  190. * @return {Object.<String, String>} map of {dir: path, name: displayName}
  191. */
  192. _makeCrumbs: function(dir, rootIcon) {
  193. var crumbs = [];
  194. var pathToHere = '';
  195. // trim leading and trailing slashes
  196. dir = dir.replace(/^\/+|\/+$/g, '');
  197. var parts = dir.split('/');
  198. if (dir === '') {
  199. parts = [];
  200. }
  201. // menu part
  202. crumbs.push({
  203. class: 'crumbmenu hidden',
  204. linkclass: 'icon-more menutoggle'
  205. });
  206. // root part
  207. crumbs.push({
  208. name: t('files', 'Home'),
  209. dir: '/',
  210. class: 'crumbhome',
  211. linkclass: rootIcon || 'icon-home'
  212. });
  213. for (var i = 0; i < parts.length; i++) {
  214. var part = parts[i];
  215. pathToHere = pathToHere + '/' + part;
  216. crumbs.push({
  217. dir: pathToHere,
  218. name: part
  219. });
  220. }
  221. return crumbs;
  222. },
  223. /**
  224. * Calculate real width based on individual crumbs
  225. *
  226. * @param {boolean} ignoreHidden ignore hidden crumbs
  227. */
  228. getTotalWidth: function(ignoreHidden) {
  229. // The width has to be calculated by adding up the width of all the
  230. // crumbs; getting the width of the breadcrumb element is not a
  231. // valid approach, as the returned value could be clamped to its
  232. // parent width.
  233. var totalWidth = 0;
  234. for (var i = 0; i < this.breadcrumbs.length; i++ ) {
  235. var $crumb = $(this.breadcrumbs[i]);
  236. if(!$crumb.hasClass('hidden') || ignoreHidden === true) {
  237. totalWidth += $crumb.outerWidth(true);
  238. }
  239. }
  240. return totalWidth;
  241. },
  242. /**
  243. * Hide the middle crumb
  244. */
  245. _hideCrumb: function() {
  246. var length = this.$el.find(this.crumbSelector).length;
  247. // Get the middle one floored down
  248. var elmt = Math.floor(length / 2 - 0.5);
  249. this.$el.find(this.crumbSelector+':eq('+elmt+')').addClass('hidden');
  250. },
  251. /**
  252. * Get the crumb to show
  253. */
  254. _getCrumbElement: function() {
  255. var hidden = this.$el.find(this.hiddenCrumbSelector).length;
  256. var shown = this.$el.find(this.crumbSelector).length;
  257. // Get the outer one with priority to the highest
  258. var elmt = (1 - shown % 2) * (hidden - 1);
  259. return this.$el.find(this.hiddenCrumbSelector + ':eq('+elmt+')');
  260. },
  261. /**
  262. * Show the middle crumb
  263. */
  264. _showCrumb: function() {
  265. if(this.$el.find(this.hiddenCrumbSelector).length === 1) {
  266. this.$el.find(this.hiddenCrumbSelector).removeClass('hidden');
  267. }
  268. this._getCrumbElement().removeClass('hidden');
  269. },
  270. /**
  271. * Create and append the popovermenu
  272. */
  273. _createMenu: function() {
  274. this.$el.find('.crumbmenu').append(this.$menu);
  275. this.$menu.children('ul').empty();
  276. },
  277. /**
  278. * Update the popovermenu
  279. */
  280. _updateMenu: function() {
  281. var menuItems = this.$el.find(this.hiddenCrumbSelector);
  282. this.$menu.find('li').addClass('in-breadcrumb');
  283. for (var i = 0; i < menuItems.length; i++) {
  284. var crumbId = $(menuItems[i]).data('crumb-id');
  285. this.$menu.find('li:eq('+crumbId+')').removeClass('in-breadcrumb');
  286. }
  287. },
  288. _resize: function() {
  289. if (this.breadcrumbs.length <= 2) {
  290. // home & menu
  291. return;
  292. }
  293. // Always hide the menu to ensure that it does not interfere with
  294. // the width calculations; otherwise, the result could be different
  295. // depending on whether the menu was previously being shown or not.
  296. this.$el.find('.crumbmenu').addClass('hidden');
  297. // Show the crumbs to compress the siblings before hiding again the
  298. // crumbs. This is needed when the siblings expand to fill all the
  299. // available width, as in that case their old width would limit the
  300. // available width for the crumbs.
  301. // Note that the crumbs shown always overflow the parent width
  302. // (except, of course, when they all fit in).
  303. while (this.$el.find(this.hiddenCrumbSelector).length > 0
  304. && Math.round(this.getTotalWidth()) <= Math.round(this.$el.parent().width())) {
  305. this._showCrumb();
  306. }
  307. var siblingsWidth = 0;
  308. this.$el.prevAll(':visible').each(function () {
  309. siblingsWidth += $(this).outerWidth(true);
  310. });
  311. this.$el.nextAll(':visible').each(function () {
  312. siblingsWidth += $(this).outerWidth(true);
  313. });
  314. var availableWidth = this.$el.parent().width() - siblingsWidth;
  315. // If container is smaller than content
  316. // AND if there are crumbs left to hide
  317. while (Math.round(this.getTotalWidth()) > Math.round(availableWidth)
  318. && this.$el.find(this.crumbSelector).length > 0) {
  319. // As soon as one of the crumbs is hidden the menu will be
  320. // shown. This is needed for proper results in further width
  321. // checks.
  322. // Note that the menu is not shown only when all the crumbs were
  323. // being shown and they all fit the available space; if any of
  324. // the crumbs was not being shown then those shown would
  325. // overflow the available width, so at least one will be hidden
  326. // and thus the menu will be shown.
  327. this.$el.find('.crumbmenu').removeClass('hidden');
  328. this._hideCrumb();
  329. }
  330. this._updateMenu();
  331. }
  332. };
  333. OCA.Files.BreadCrumb = BreadCrumb;
  334. })();