Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / tour / js / tour.es6.js
diff --git a/web/core/modules/tour/js/tour.es6.js b/web/core/modules/tour/js/tour.es6.js
new file mode 100644 (file)
index 0000000..f3b1107
--- /dev/null
@@ -0,0 +1,268 @@
+/**
+ * @file
+ * Attaches behaviors for the Tour module's toolbar tab.
+ */
+
+(function ($, Backbone, Drupal, document) {
+  const queryString = decodeURI(window.location.search);
+
+  /**
+   * Attaches the tour's toolbar tab behavior.
+   *
+   * It uses the query string for:
+   * - tour: When ?tour=1 is present, the tour will start automatically after
+   *   the page has loaded.
+   * - tips: Pass ?tips=class in the url to filter the available tips to the
+   *   subset which match the given class.
+   *
+   * @example
+   * http://example.com/foo?tour=1&tips=bar
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attach tour functionality on `tour` events.
+   */
+  Drupal.behaviors.tour = {
+    attach(context) {
+      $('body').once('tour').each(() => {
+        const model = new Drupal.tour.models.StateModel();
+        new Drupal.tour.views.ToggleTourView({
+          el: $(context).find('#toolbar-tab-tour'),
+          model,
+        });
+
+        model
+          // Allow other scripts to respond to tour events.
+          .on('change:isActive', (model, isActive) => {
+            $(document).trigger((isActive) ? 'drupalTourStarted' : 'drupalTourStopped');
+          })
+          // Initialization: check whether a tour is available on the current
+          // page.
+          .set('tour', $(context).find('ol#tour'));
+
+        // Start the tour immediately if toggled via query string.
+        if (/tour=?/i.test(queryString)) {
+          model.set('isActive', true);
+        }
+      });
+    },
+  };
+
+  /**
+   * @namespace
+   */
+  Drupal.tour = Drupal.tour || {
+
+    /**
+     * @namespace Drupal.tour.models
+     */
+    models: {},
+
+    /**
+     * @namespace Drupal.tour.views
+     */
+    views: {},
+  };
+
+  /**
+   * Backbone Model for tours.
+   *
+   * @constructor
+   *
+   * @augments Backbone.Model
+   */
+  Drupal.tour.models.StateModel = Backbone.Model.extend(/** @lends Drupal.tour.models.StateModel# */{
+
+    /**
+     * @type {object}
+     */
+    defaults: /** @lends Drupal.tour.models.StateModel# */{
+
+      /**
+       * Indicates whether the Drupal root window has a tour.
+       *
+       * @type {Array}
+       */
+      tour: [],
+
+      /**
+       * Indicates whether the tour is currently running.
+       *
+       * @type {bool}
+       */
+      isActive: false,
+
+      /**
+       * Indicates which tour is the active one (necessary to cleanly stop).
+       *
+       * @type {Array}
+       */
+      activeTour: [],
+    },
+  });
+
+  Drupal.tour.views.ToggleTourView = Backbone.View.extend(/** @lends Drupal.tour.views.ToggleTourView# */{
+
+    /**
+     * @type {object}
+     */
+    events: { click: 'onClick' },
+
+    /**
+     * Handles edit mode toggle interactions.
+     *
+     * @constructs
+     *
+     * @augments Backbone.View
+     */
+    initialize() {
+      this.listenTo(this.model, 'change:tour change:isActive', this.render);
+      this.listenTo(this.model, 'change:isActive', this.toggleTour);
+    },
+
+    /**
+     * @inheritdoc
+     *
+     * @return {Drupal.tour.views.ToggleTourView}
+     *   The `ToggleTourView` view.
+     */
+    render() {
+      // Render the visibility.
+      this.$el.toggleClass('hidden', this._getTour().length === 0);
+      // Render the state.
+      const isActive = this.model.get('isActive');
+      this.$el.find('button')
+        .toggleClass('is-active', isActive)
+        .prop('aria-pressed', isActive);
+      return this;
+    },
+
+    /**
+     * Model change handler; starts or stops the tour.
+     */
+    toggleTour() {
+      if (this.model.get('isActive')) {
+        const $tour = this._getTour();
+        this._removeIrrelevantTourItems($tour, this._getDocument());
+        const that = this;
+        if ($tour.find('li').length) {
+          $tour.joyride({
+            autoStart: true,
+            postRideCallback() {
+              that.model.set('isActive', false);
+            },
+            // HTML segments for tip layout.
+            template: {
+              link: '<a href=\"#close\" class=\"joyride-close-tip\">&times;</a>',
+              button: '<a href=\"#\" class=\"button button--primary joyride-next-tip\"></a>',
+            },
+          });
+          this.model.set({ isActive: true, activeTour: $tour });
+        }
+      }
+      else {
+        this.model.get('activeTour').joyride('destroy');
+        this.model.set({ isActive: false, activeTour: [] });
+      }
+    },
+
+    /**
+     * Toolbar tab click event handler; toggles isActive.
+     *
+     * @param {jQuery.Event} event
+     *   The click event.
+     */
+    onClick(event) {
+      this.model.set('isActive', !this.model.get('isActive'));
+      event.preventDefault();
+      event.stopPropagation();
+    },
+
+    /**
+     * Gets the tour.
+     *
+     * @return {jQuery}
+     *   A jQuery element pointing to a `<ol>` containing tour items.
+     */
+    _getTour() {
+      return this.model.get('tour');
+    },
+
+    /**
+     * Gets the relevant document as a jQuery element.
+     *
+     * @return {jQuery}
+     *   A jQuery element pointing to the document within which a tour would be
+     *   started given the current state.
+     */
+    _getDocument() {
+      return $(document);
+    },
+
+    /**
+     * Removes tour items for elements that don't have matching page elements.
+     *
+     * Or that are explicitly filtered out via the 'tips' query string.
+     *
+     * @example
+     * <caption>This will filter out tips that do not have a matching
+     * page element or don't have the "bar" class.</caption>
+     * http://example.com/foo?tips=bar
+     *
+     * @param {jQuery} $tour
+     *   A jQuery element pointing to a `<ol>` containing tour items.
+     * @param {jQuery} $document
+     *   A jQuery element pointing to the document within which the elements
+     *   should be sought.
+     *
+     * @see Drupal.tour.views.ToggleTourView#_getDocument
+     */
+    _removeIrrelevantTourItems($tour, $document) {
+      let removals = false;
+      const tips = /tips=([^&]+)/.exec(queryString);
+      $tour
+        .find('li')
+        .each(function () {
+          const $this = $(this);
+          const itemId = $this.attr('data-id');
+          const itemClass = $this.attr('data-class');
+          // If the query parameter 'tips' is set, remove all tips that don't
+          // have the matching class.
+          if (tips && !$(this).hasClass(tips[1])) {
+            removals = true;
+            $this.remove();
+            return;
+          }
+          // Remove tip from the DOM if there is no corresponding page element.
+          if ((!itemId && !itemClass) ||
+            (itemId && $document.find(`#${itemId}`).length) ||
+            (itemClass && $document.find(`.${itemClass}`).length)) {
+            return;
+          }
+          removals = true;
+          $this.remove();
+        });
+
+      // If there were removals, we'll have to do some clean-up.
+      if (removals) {
+        const total = $tour.find('li').length;
+        if (!total) {
+          this.model.set({ tour: [] });
+        }
+
+        $tour
+          .find('li')
+          // Rebuild the progress data.
+          .each(function (index) {
+            const progress = Drupal.t('!tour_item of !total', { '!tour_item': index + 1, '!total': total });
+            $(this).find('.tour-progress').text(progress);
+          })
+          // Update the last item to have "End tour" as the button.
+          .eq(-1)
+          .attr('data-text', Drupal.t('End tour'));
+      }
+    },
+
+  });
+}(jQuery, Backbone, Drupal, document));