breadcrumb.js 9.7 KB

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