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