Source: modules/toolbar/js/views/ToolbarVisualView.es6.js

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