ToolbarVisualView.es6.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. /**
  2. * @file
  3. * A Backbone view for the toolbar element. Listens to mouse & touch.
  4. */
  5. (function ($, Drupal, drupalSettings, Backbone) {
  6. Drupal.toolbar.ToolbarVisualView = Backbone.View.extend(/** @lends Drupal.toolbar.ToolbarVisualView# */{
  7. /**
  8. * Event map for the `ToolbarVisualView`.
  9. *
  10. * @return {object}
  11. * A map of events.
  12. */
  13. events() {
  14. // Prevents delay and simulated mouse events.
  15. const touchEndToClick = function (event) {
  16. event.preventDefault();
  17. event.target.click();
  18. };
  19. return {
  20. 'click .toolbar-bar .toolbar-tab .trigger': 'onTabClick',
  21. 'click .toolbar-toggle-orientation button': 'onOrientationToggleClick',
  22. 'touchend .toolbar-bar .toolbar-tab .trigger': touchEndToClick,
  23. 'touchend .toolbar-toggle-orientation button': touchEndToClick,
  24. };
  25. },
  26. /**
  27. * Backbone view for the toolbar element. Listens to mouse & touch.
  28. *
  29. * @constructs
  30. *
  31. * @augments Backbone.View
  32. *
  33. * @param {object} options
  34. * Options for the view object.
  35. * @param {object} options.strings
  36. * Various strings to use in the view.
  37. */
  38. initialize(options) {
  39. this.strings = options.strings;
  40. this.listenTo(this.model, 'change:activeTab change:orientation change:isOriented change:isTrayToggleVisible', this.render);
  41. this.listenTo(this.model, 'change:mqMatches', this.onMediaQueryChange);
  42. this.listenTo(this.model, 'change:offsets', this.adjustPlacement);
  43. this.listenTo(this.model, 'change:activeTab change:orientation change:isOriented', this.updateToolbarHeight);
  44. // Add the tray orientation toggles.
  45. this.$el
  46. .find('.toolbar-tray .toolbar-lining')
  47. .append(Drupal.theme('toolbarOrientationToggle'));
  48. // Trigger an activeTab change so that listening scripts can respond on
  49. // page load. This will call render.
  50. this.model.trigger('change:activeTab');
  51. },
  52. /**
  53. * Update the toolbar element height.
  54. *
  55. * @constructs
  56. *
  57. * @augments Backbone.View
  58. */
  59. updateToolbarHeight() {
  60. const toolbarTabOuterHeight = $('#toolbar-bar').find('.toolbar-tab').outerHeight() || 0;
  61. const toolbarTrayHorizontalOuterHeight = $('.is-active.toolbar-tray-horizontal').outerHeight() || 0;
  62. this.model.set('height', toolbarTabOuterHeight + toolbarTrayHorizontalOuterHeight);
  63. $('body').css({
  64. 'padding-top': this.model.get('height'),
  65. });
  66. this.triggerDisplace();
  67. },
  68. // Trigger a recalculation of viewport displacing elements. Use setTimeout
  69. // to ensure this recalculation happens after changes to visual elements
  70. // have processed.
  71. triggerDisplace() {
  72. _.defer(() => {
  73. Drupal.displace(true);
  74. });
  75. },
  76. /**
  77. * @inheritdoc
  78. *
  79. * @return {Drupal.toolbar.ToolbarVisualView}
  80. * The `ToolbarVisualView` instance.
  81. */
  82. render() {
  83. this.updateTabs();
  84. this.updateTrayOrientation();
  85. this.updateBarAttributes();
  86. $('body').removeClass('toolbar-loading');
  87. // Load the subtrees if the orientation of the toolbar is changed to
  88. // vertical. This condition responds to the case that the toolbar switches
  89. // from horizontal to vertical orientation. The toolbar starts in a
  90. // vertical orientation by default and then switches to horizontal during
  91. // initialization if the media query conditions are met. Simply checking
  92. // that the orientation is vertical here would result in the subtrees
  93. // always being loaded, even when the toolbar initialization ultimately
  94. // results in a horizontal orientation.
  95. //
  96. // @see Drupal.behaviors.toolbar.attach() where admin menu subtrees
  97. // loading is invoked during initialization after media query conditions
  98. // have been processed.
  99. if (this.model.changed.orientation === 'vertical' || this.model.changed.activeTab) {
  100. this.loadSubtrees();
  101. }
  102. return this;
  103. },
  104. /**
  105. * Responds to a toolbar tab click.
  106. *
  107. * @param {jQuery.Event} event
  108. * The event triggered.
  109. */
  110. onTabClick(event) {
  111. // If this tab has a tray associated with it, it is considered an
  112. // activatable tab.
  113. if (event.target.hasAttribute('data-toolbar-tray')) {
  114. const activeTab = this.model.get('activeTab');
  115. const clickedTab = event.target;
  116. // Set the event target as the active item if it is not already.
  117. this.model.set('activeTab', (!activeTab || clickedTab !== activeTab) ? clickedTab : null);
  118. event.preventDefault();
  119. event.stopPropagation();
  120. }
  121. },
  122. /**
  123. * Toggles the orientation of a toolbar tray.
  124. *
  125. * @param {jQuery.Event} event
  126. * The event triggered.
  127. */
  128. onOrientationToggleClick(event) {
  129. const orientation = this.model.get('orientation');
  130. // Determine the toggle-to orientation.
  131. const antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
  132. const locked = antiOrientation === 'vertical';
  133. // Remember the locked state.
  134. if (locked) {
  135. localStorage.setItem('Drupal.toolbar.trayVerticalLocked', 'true');
  136. }
  137. else {
  138. localStorage.removeItem('Drupal.toolbar.trayVerticalLocked');
  139. }
  140. // Update the model.
  141. this.model.set({
  142. locked,
  143. orientation: antiOrientation,
  144. }, {
  145. validate: true,
  146. override: true,
  147. });
  148. event.preventDefault();
  149. event.stopPropagation();
  150. },
  151. /**
  152. * Updates the display of the tabs: toggles a tab and the associated tray.
  153. */
  154. updateTabs() {
  155. const $tab = $(this.model.get('activeTab'));
  156. // Deactivate the previous tab.
  157. $(this.model.previous('activeTab'))
  158. .removeClass('is-active')
  159. .prop('aria-pressed', false);
  160. // Deactivate the previous tray.
  161. $(this.model.previous('activeTray'))
  162. .removeClass('is-active');
  163. // Activate the selected tab.
  164. if ($tab.length > 0) {
  165. $tab
  166. .addClass('is-active')
  167. // Mark the tab as pressed.
  168. .prop('aria-pressed', true);
  169. const name = $tab.attr('data-toolbar-tray');
  170. // Store the active tab name or remove the setting.
  171. const id = $tab.get(0).id;
  172. if (id) {
  173. localStorage.setItem('Drupal.toolbar.activeTabID', JSON.stringify(id));
  174. }
  175. // Activate the associated tray.
  176. const $tray = this.$el.find(`[data-toolbar-tray="${name}"].toolbar-tray`);
  177. if ($tray.length) {
  178. $tray.addClass('is-active');
  179. this.model.set('activeTray', $tray.get(0));
  180. }
  181. else {
  182. // There is no active tray.
  183. this.model.set('activeTray', null);
  184. }
  185. }
  186. else {
  187. // There is no active tray.
  188. this.model.set('activeTray', null);
  189. localStorage.removeItem('Drupal.toolbar.activeTabID');
  190. }
  191. },
  192. /**
  193. * Update the attributes of the toolbar bar element.
  194. */
  195. updateBarAttributes() {
  196. const isOriented = this.model.get('isOriented');
  197. if (isOriented) {
  198. this.$el.find('.toolbar-bar').attr('data-offset-top', '');
  199. }
  200. else {
  201. this.$el.find('.toolbar-bar').removeAttr('data-offset-top');
  202. }
  203. // Toggle between a basic vertical view and a more sophisticated
  204. // horizontal and vertical display of the toolbar bar and trays.
  205. this.$el.toggleClass('toolbar-oriented', isOriented);
  206. },
  207. /**
  208. * Updates the orientation of the active tray if necessary.
  209. */
  210. updateTrayOrientation() {
  211. const orientation = this.model.get('orientation');
  212. // The antiOrientation is used to render the view of action buttons like
  213. // the tray orientation toggle.
  214. const antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
  215. // Toggle toolbar's parent classes before other toolbar classes to avoid
  216. // potential flicker and re-rendering.
  217. $('body')
  218. .toggleClass('toolbar-vertical', (orientation === 'vertical'))
  219. .toggleClass('toolbar-horizontal', (orientation === 'horizontal'));
  220. const removeClass = (antiOrientation === 'horizontal') ? 'toolbar-tray-horizontal' : 'toolbar-tray-vertical';
  221. const $trays = this.$el.find('.toolbar-tray')
  222. .removeClass(removeClass)
  223. .addClass(`toolbar-tray-${orientation}`);
  224. // Update the tray orientation toggle button.
  225. const iconClass = `toolbar-icon-toggle-${orientation}`;
  226. const iconAntiClass = `toolbar-icon-toggle-${antiOrientation}`;
  227. const $orientationToggle = this.$el.find('.toolbar-toggle-orientation')
  228. .toggle(this.model.get('isTrayToggleVisible'));
  229. $orientationToggle.find('button')
  230. .val(antiOrientation)
  231. .attr('title', this.strings[antiOrientation])
  232. .text(this.strings[antiOrientation])
  233. .removeClass(iconClass)
  234. .addClass(iconAntiClass);
  235. // Update data offset attributes for the trays.
  236. const dir = document.documentElement.dir;
  237. const edge = (dir === 'rtl') ? 'right' : 'left';
  238. // Remove data-offset attributes from the trays so they can be refreshed.
  239. $trays.removeAttr('data-offset-left data-offset-right data-offset-top');
  240. // If an active vertical tray exists, mark it as an offset element.
  241. $trays.filter('.toolbar-tray-vertical.is-active').attr(`data-offset-${edge}`, '');
  242. // If an active horizontal tray exists, mark it as an offset element.
  243. $trays.filter('.toolbar-tray-horizontal.is-active').attr('data-offset-top', '');
  244. },
  245. /**
  246. * Sets the tops of the trays so that they align with the bottom of the bar.
  247. */
  248. adjustPlacement() {
  249. const $trays = this.$el.find('.toolbar-tray');
  250. if (!this.model.get('isOriented')) {
  251. $trays.removeClass('toolbar-tray-horizontal').addClass('toolbar-tray-vertical');
  252. }
  253. },
  254. /**
  255. * Calls the endpoint URI that builds an AJAX command with the rendered
  256. * subtrees.
  257. *
  258. * The rendered admin menu subtrees HTML is cached on the client in
  259. * localStorage until the cache of the admin menu subtrees on the server-
  260. * side is invalidated. The subtreesHash is stored in localStorage as well
  261. * and compared to the subtreesHash in drupalSettings to determine when the
  262. * admin menu subtrees cache has been invalidated.
  263. */
  264. loadSubtrees() {
  265. const $activeTab = $(this.model.get('activeTab'));
  266. const orientation = this.model.get('orientation');
  267. // Only load and render the admin menu subtrees if:
  268. // (1) They have not been loaded yet.
  269. // (2) The active tab is the administration menu tab, indicated by the
  270. // presence of the data-drupal-subtrees attribute.
  271. // (3) The orientation of the tray is vertical.
  272. if (!this.model.get('areSubtreesLoaded') && typeof $activeTab.data('drupal-subtrees') !== 'undefined' && orientation === 'vertical') {
  273. const subtreesHash = drupalSettings.toolbar.subtreesHash;
  274. const theme = drupalSettings.ajaxPageState.theme;
  275. const endpoint = Drupal.url(`toolbar/subtrees/${subtreesHash}`);
  276. const cachedSubtreesHash = localStorage.getItem(`Drupal.toolbar.subtreesHash.${theme}`);
  277. const cachedSubtrees = JSON.parse(localStorage.getItem(`Drupal.toolbar.subtrees.${theme}`));
  278. const isVertical = this.model.get('orientation') === 'vertical';
  279. // If we have the subtrees in localStorage and the subtree hash has not
  280. // changed, then use the cached data.
  281. if (isVertical && subtreesHash === cachedSubtreesHash && cachedSubtrees) {
  282. Drupal.toolbar.setSubtrees.resolve(cachedSubtrees);
  283. }
  284. // Only make the call to get the subtrees if the orientation of the
  285. // toolbar is vertical.
  286. else if (isVertical) {
  287. // Remove the cached menu information.
  288. localStorage.removeItem(`Drupal.toolbar.subtreesHash.${theme}`);
  289. localStorage.removeItem(`Drupal.toolbar.subtrees.${theme}`);
  290. // The AJAX response's command will trigger the resolve method of the
  291. // Drupal.toolbar.setSubtrees Promise.
  292. Drupal.ajax({ url: endpoint }).execute();
  293. // Cache the hash for the subtrees locally.
  294. localStorage.setItem(`Drupal.toolbar.subtreesHash.${theme}`, subtreesHash);
  295. }
  296. }
  297. },
  298. });
  299. }(jQuery, Drupal, drupalSettings, Backbone));