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