Chris@0: /** Chris@0: * @file Chris@0: * A Backbone view for the toolbar element. Listens to mouse & touch. Chris@0: */ Chris@0: Chris@17: (function($, Drupal, drupalSettings, Backbone) { Chris@17: Drupal.toolbar.ToolbarVisualView = Backbone.View.extend( Chris@17: /** @lends Drupal.toolbar.ToolbarVisualView# */ { Chris@17: /** Chris@17: * Event map for the `ToolbarVisualView`. Chris@17: * Chris@17: * @return {object} Chris@17: * A map of events. Chris@17: */ Chris@17: events() { Chris@17: // Prevents delay and simulated mouse events. Chris@17: const touchEndToClick = function(event) { Chris@17: event.preventDefault(); Chris@17: event.target.click(); Chris@17: }; Chris@0: Chris@17: return { Chris@17: 'click .toolbar-bar .toolbar-tab .trigger': 'onTabClick', Chris@17: 'click .toolbar-toggle-orientation button': Chris@17: 'onOrientationToggleClick', Chris@17: 'touchend .toolbar-bar .toolbar-tab .trigger': touchEndToClick, Chris@17: 'touchend .toolbar-toggle-orientation button': touchEndToClick, Chris@17: }; Chris@17: }, Chris@0: Chris@17: /** Chris@17: * Backbone view for the toolbar element. Listens to mouse & touch. Chris@17: * Chris@17: * @constructs Chris@17: * Chris@17: * @augments Backbone.View Chris@17: * Chris@17: * @param {object} options Chris@17: * Options for the view object. Chris@17: * @param {object} options.strings Chris@17: * Various strings to use in the view. Chris@17: */ Chris@17: initialize(options) { Chris@17: this.strings = options.strings; Chris@0: Chris@17: this.listenTo( Chris@17: this.model, Chris@17: 'change:activeTab change:orientation change:isOriented change:isTrayToggleVisible', Chris@17: this.render, Chris@17: ); Chris@17: this.listenTo(this.model, 'change:mqMatches', this.onMediaQueryChange); Chris@17: this.listenTo(this.model, 'change:offsets', this.adjustPlacement); Chris@17: this.listenTo( Chris@17: this.model, Chris@17: 'change:activeTab change:orientation change:isOriented', Chris@17: this.updateToolbarHeight, Chris@17: ); Chris@0: Chris@17: // Add the tray orientation toggles. Chris@17: this.$el Chris@17: .find('.toolbar-tray .toolbar-lining') Chris@17: .append(Drupal.theme('toolbarOrientationToggle')); Chris@0: Chris@17: // Trigger an activeTab change so that listening scripts can respond on Chris@17: // page load. This will call render. Chris@17: this.model.trigger('change:activeTab'); Chris@17: }, Chris@0: Chris@17: /** Chris@17: * Update the toolbar element height. Chris@17: * Chris@17: * @constructs Chris@17: * Chris@17: * @augments Backbone.View Chris@17: */ Chris@17: updateToolbarHeight() { Chris@17: const toolbarTabOuterHeight = Chris@17: $('#toolbar-bar') Chris@17: .find('.toolbar-tab') Chris@17: .outerHeight() || 0; Chris@17: const toolbarTrayHorizontalOuterHeight = Chris@17: $('.is-active.toolbar-tray-horizontal').outerHeight() || 0; Chris@17: this.model.set( Chris@17: 'height', Chris@17: toolbarTabOuterHeight + toolbarTrayHorizontalOuterHeight, Chris@17: ); Chris@0: Chris@17: $('body').css({ Chris@17: 'padding-top': this.model.get('height'), Chris@17: }); Chris@0: Chris@17: this.triggerDisplace(); Chris@17: }, Chris@0: Chris@17: // Trigger a recalculation of viewport displacing elements. Use setTimeout Chris@17: // to ensure this recalculation happens after changes to visual elements Chris@17: // have processed. Chris@17: triggerDisplace() { Chris@17: _.defer(() => { Chris@17: Drupal.displace(true); Chris@17: }); Chris@17: }, Chris@0: Chris@17: /** Chris@17: * @inheritdoc Chris@17: * Chris@17: * @return {Drupal.toolbar.ToolbarVisualView} Chris@17: * The `ToolbarVisualView` instance. Chris@17: */ Chris@17: render() { Chris@17: this.updateTabs(); Chris@17: this.updateTrayOrientation(); Chris@17: this.updateBarAttributes(); Chris@0: Chris@17: $('body').removeClass('toolbar-loading'); Chris@0: Chris@17: // Load the subtrees if the orientation of the toolbar is changed to Chris@17: // vertical. This condition responds to the case that the toolbar switches Chris@17: // from horizontal to vertical orientation. The toolbar starts in a Chris@17: // vertical orientation by default and then switches to horizontal during Chris@17: // initialization if the media query conditions are met. Simply checking Chris@17: // that the orientation is vertical here would result in the subtrees Chris@17: // always being loaded, even when the toolbar initialization ultimately Chris@17: // results in a horizontal orientation. Chris@17: // Chris@17: // @see Drupal.behaviors.toolbar.attach() where admin menu subtrees Chris@17: // loading is invoked during initialization after media query conditions Chris@17: // have been processed. Chris@17: if ( Chris@17: this.model.changed.orientation === 'vertical' || Chris@17: this.model.changed.activeTab Chris@17: ) { Chris@17: this.loadSubtrees(); Chris@17: } Chris@0: Chris@17: return this; Chris@17: }, Chris@0: Chris@17: /** Chris@17: * Responds to a toolbar tab click. Chris@17: * Chris@17: * @param {jQuery.Event} event Chris@17: * The event triggered. Chris@17: */ Chris@17: onTabClick(event) { Chris@17: // If this tab has a tray associated with it, it is considered an Chris@17: // activatable tab. Chris@18: if (event.currentTarget.hasAttribute('data-toolbar-tray')) { Chris@17: const activeTab = this.model.get('activeTab'); Chris@18: const clickedTab = event.currentTarget; Chris@0: Chris@17: // Set the event target as the active item if it is not already. Chris@17: this.model.set( Chris@17: 'activeTab', Chris@17: !activeTab || clickedTab !== activeTab ? clickedTab : null, Chris@17: ); Chris@0: Chris@17: event.preventDefault(); Chris@17: event.stopPropagation(); Chris@17: } Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Toggles the orientation of a toolbar tray. Chris@17: * Chris@17: * @param {jQuery.Event} event Chris@17: * The event triggered. Chris@17: */ Chris@17: onOrientationToggleClick(event) { Chris@17: const orientation = this.model.get('orientation'); Chris@17: // Determine the toggle-to orientation. Chris@17: const antiOrientation = Chris@17: orientation === 'vertical' ? 'horizontal' : 'vertical'; Chris@17: const locked = antiOrientation === 'vertical'; Chris@17: // Remember the locked state. Chris@17: if (locked) { Chris@17: localStorage.setItem('Drupal.toolbar.trayVerticalLocked', 'true'); Chris@17: } else { Chris@17: localStorage.removeItem('Drupal.toolbar.trayVerticalLocked'); Chris@17: } Chris@17: // Update the model. Chris@17: this.model.set( Chris@17: { Chris@17: locked, Chris@17: orientation: antiOrientation, Chris@17: }, Chris@17: { Chris@17: validate: true, Chris@17: override: true, Chris@17: }, Chris@17: ); Chris@0: Chris@0: event.preventDefault(); Chris@0: event.stopPropagation(); Chris@17: }, Chris@0: Chris@17: /** Chris@17: * Updates the display of the tabs: toggles a tab and the associated tray. Chris@17: */ Chris@17: updateTabs() { Chris@17: const $tab = $(this.model.get('activeTab')); Chris@17: // Deactivate the previous tab. Chris@17: $(this.model.previous('activeTab')) Chris@17: .removeClass('is-active') Chris@17: .prop('aria-pressed', false); Chris@17: // Deactivate the previous tray. Chris@17: $(this.model.previous('activeTray')).removeClass('is-active'); Chris@0: Chris@17: // Activate the selected tab. Chris@17: if ($tab.length > 0) { Chris@17: $tab Chris@17: .addClass('is-active') Chris@17: // Mark the tab as pressed. Chris@17: .prop('aria-pressed', true); Chris@17: const name = $tab.attr('data-toolbar-tray'); Chris@17: // Store the active tab name or remove the setting. Chris@17: const id = $tab.get(0).id; Chris@17: if (id) { Chris@17: localStorage.setItem( Chris@17: 'Drupal.toolbar.activeTabID', Chris@17: JSON.stringify(id), Chris@17: ); Chris@17: } Chris@17: // Activate the associated tray. Chris@17: const $tray = this.$el.find( Chris@17: `[data-toolbar-tray="${name}"].toolbar-tray`, Chris@17: ); Chris@17: if ($tray.length) { Chris@17: $tray.addClass('is-active'); Chris@17: this.model.set('activeTray', $tray.get(0)); Chris@17: } else { Chris@17: // There is no active tray. Chris@17: this.model.set('activeTray', null); Chris@17: } Chris@17: } else { Chris@0: // There is no active tray. Chris@0: this.model.set('activeTray', null); Chris@17: localStorage.removeItem('Drupal.toolbar.activeTabID'); Chris@0: } Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Update the attributes of the toolbar bar element. Chris@17: */ Chris@17: updateBarAttributes() { Chris@17: const isOriented = this.model.get('isOriented'); Chris@17: if (isOriented) { Chris@17: this.$el.find('.toolbar-bar').attr('data-offset-top', ''); Chris@17: } else { Chris@17: this.$el.find('.toolbar-bar').removeAttr('data-offset-top'); Chris@17: } Chris@17: // Toggle between a basic vertical view and a more sophisticated Chris@17: // horizontal and vertical display of the toolbar bar and trays. Chris@17: this.$el.toggleClass('toolbar-oriented', isOriented); Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Updates the orientation of the active tray if necessary. Chris@17: */ Chris@17: updateTrayOrientation() { Chris@17: const orientation = this.model.get('orientation'); Chris@17: Chris@17: // The antiOrientation is used to render the view of action buttons like Chris@17: // the tray orientation toggle. Chris@17: const antiOrientation = Chris@17: orientation === 'vertical' ? 'horizontal' : 'vertical'; Chris@17: Chris@17: // Toggle toolbar's parent classes before other toolbar classes to avoid Chris@17: // potential flicker and re-rendering. Chris@17: $('body') Chris@17: .toggleClass('toolbar-vertical', orientation === 'vertical') Chris@17: .toggleClass('toolbar-horizontal', orientation === 'horizontal'); Chris@17: Chris@17: const removeClass = Chris@17: antiOrientation === 'horizontal' Chris@17: ? 'toolbar-tray-horizontal' Chris@17: : 'toolbar-tray-vertical'; Chris@17: const $trays = this.$el Chris@17: .find('.toolbar-tray') Chris@17: .removeClass(removeClass) Chris@17: .addClass(`toolbar-tray-${orientation}`); Chris@17: Chris@17: // Update the tray orientation toggle button. Chris@17: const iconClass = `toolbar-icon-toggle-${orientation}`; Chris@17: const iconAntiClass = `toolbar-icon-toggle-${antiOrientation}`; Chris@17: const $orientationToggle = this.$el Chris@17: .find('.toolbar-toggle-orientation') Chris@17: .toggle(this.model.get('isTrayToggleVisible')); Chris@17: $orientationToggle Chris@17: .find('button') Chris@17: .val(antiOrientation) Chris@17: .attr('title', this.strings[antiOrientation]) Chris@17: .text(this.strings[antiOrientation]) Chris@17: .removeClass(iconClass) Chris@17: .addClass(iconAntiClass); Chris@17: Chris@17: // Update data offset attributes for the trays. Chris@17: const dir = document.documentElement.dir; Chris@17: const edge = dir === 'rtl' ? 'right' : 'left'; Chris@17: // Remove data-offset attributes from the trays so they can be refreshed. Chris@17: $trays.removeAttr('data-offset-left data-offset-right data-offset-top'); Chris@17: // If an active vertical tray exists, mark it as an offset element. Chris@17: $trays Chris@17: .filter('.toolbar-tray-vertical.is-active') Chris@17: .attr(`data-offset-${edge}`, ''); Chris@17: // If an active horizontal tray exists, mark it as an offset element. Chris@17: $trays Chris@17: .filter('.toolbar-tray-horizontal.is-active') Chris@17: .attr('data-offset-top', ''); Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Sets the tops of the trays so that they align with the bottom of the bar. Chris@17: */ Chris@17: adjustPlacement() { Chris@17: const $trays = this.$el.find('.toolbar-tray'); Chris@17: if (!this.model.get('isOriented')) { Chris@17: $trays Chris@17: .removeClass('toolbar-tray-horizontal') Chris@17: .addClass('toolbar-tray-vertical'); Chris@17: } Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Calls the endpoint URI that builds an AJAX command with the rendered Chris@17: * subtrees. Chris@17: * Chris@17: * The rendered admin menu subtrees HTML is cached on the client in Chris@17: * localStorage until the cache of the admin menu subtrees on the server- Chris@17: * side is invalidated. The subtreesHash is stored in localStorage as well Chris@17: * and compared to the subtreesHash in drupalSettings to determine when the Chris@17: * admin menu subtrees cache has been invalidated. Chris@17: */ Chris@17: loadSubtrees() { Chris@17: const $activeTab = $(this.model.get('activeTab')); Chris@17: const orientation = this.model.get('orientation'); Chris@17: // Only load and render the admin menu subtrees if: Chris@17: // (1) They have not been loaded yet. Chris@17: // (2) The active tab is the administration menu tab, indicated by the Chris@17: // presence of the data-drupal-subtrees attribute. Chris@17: // (3) The orientation of the tray is vertical. Chris@17: if ( Chris@17: !this.model.get('areSubtreesLoaded') && Chris@17: typeof $activeTab.data('drupal-subtrees') !== 'undefined' && Chris@17: orientation === 'vertical' Chris@17: ) { Chris@17: const subtreesHash = drupalSettings.toolbar.subtreesHash; Chris@17: const theme = drupalSettings.ajaxPageState.theme; Chris@17: const endpoint = Drupal.url(`toolbar/subtrees/${subtreesHash}`); Chris@17: const cachedSubtreesHash = localStorage.getItem( Chris@17: `Drupal.toolbar.subtreesHash.${theme}`, Chris@17: ); Chris@17: const cachedSubtrees = JSON.parse( Chris@17: localStorage.getItem(`Drupal.toolbar.subtrees.${theme}`), Chris@17: ); Chris@17: const isVertical = this.model.get('orientation') === 'vertical'; Chris@17: // If we have the subtrees in localStorage and the subtree hash has not Chris@17: // changed, then use the cached data. Chris@17: if ( Chris@17: isVertical && Chris@17: subtreesHash === cachedSubtreesHash && Chris@17: cachedSubtrees Chris@17: ) { Chris@17: Drupal.toolbar.setSubtrees.resolve(cachedSubtrees); Chris@17: } Chris@17: // Only make the call to get the subtrees if the orientation of the Chris@17: // toolbar is vertical. Chris@17: else if (isVertical) { Chris@17: // Remove the cached menu information. Chris@17: localStorage.removeItem(`Drupal.toolbar.subtreesHash.${theme}`); Chris@17: localStorage.removeItem(`Drupal.toolbar.subtrees.${theme}`); Chris@17: // The AJAX response's command will trigger the resolve method of the Chris@17: // Drupal.toolbar.setSubtrees Promise. Chris@17: Drupal.ajax({ url: endpoint }).execute(); Chris@17: // Cache the hash for the subtrees locally. Chris@17: localStorage.setItem( Chris@17: `Drupal.toolbar.subtreesHash.${theme}`, Chris@17: subtreesHash, Chris@17: ); Chris@17: } Chris@17: } Chris@17: }, Chris@0: }, Chris@17: ); Chris@17: })(jQuery, Drupal, drupalSettings, Backbone);