Security update for Core, with self-updated composer
[yaffs-website] / web / core / misc / tabbingmanager.es6.js
diff --git a/web/core/misc/tabbingmanager.es6.js b/web/core/misc/tabbingmanager.es6.js
new file mode 100644 (file)
index 0000000..075dda5
--- /dev/null
@@ -0,0 +1,363 @@
+/**
+ * @file
+ * Manages page tabbing modifications made by modules.
+ */
+
+/**
+ * Allow modules to respond to the constrain event.
+ *
+ * @event drupalTabbingConstrained
+ */
+
+/**
+ * Allow modules to respond to the tabbingContext release event.
+ *
+ * @event drupalTabbingContextReleased
+ */
+
+/**
+ * Allow modules to respond to the constrain event.
+ *
+ * @event drupalTabbingContextActivated
+ */
+
+/**
+ * Allow modules to respond to the constrain event.
+ *
+ * @event drupalTabbingContextDeactivated
+ */
+
+(function ($, Drupal) {
+  /**
+   * Provides an API for managing page tabbing order modifications.
+   *
+   * @constructor Drupal~TabbingManager
+   */
+  function TabbingManager() {
+    /**
+     * Tabbing sets are stored as a stack. The active set is at the top of the
+     * stack. We use a JavaScript array as if it were a stack; we consider the
+     * first element to be the bottom and the last element to be the top. This
+     * allows us to use JavaScript's built-in Array.push() and Array.pop()
+     * methods.
+     *
+     * @type {Array.<Drupal~TabbingContext>}
+     */
+    this.stack = [];
+  }
+
+  /**
+   * Add public methods to the TabbingManager class.
+   */
+  $.extend(TabbingManager.prototype, /** @lends Drupal~TabbingManager# */{
+
+    /**
+     * Constrain tabbing to the specified set of elements only.
+     *
+     * Makes elements outside of the specified set of elements unreachable via
+     * the tab key.
+     *
+     * @param {jQuery} elements
+     *   The set of elements to which tabbing should be constrained. Can also
+     *   be a jQuery-compatible selector string.
+     *
+     * @return {Drupal~TabbingContext}
+     *   The TabbingContext instance.
+     *
+     * @fires event:drupalTabbingConstrained
+     */
+    constrain(elements) {
+      // Deactivate all tabbingContexts to prepare for the new constraint. A
+      // tabbingContext instance will only be reactivated if the stack is
+      // unwound to it in the _unwindStack() method.
+      const il = this.stack.length;
+      for (let i = 0; i < il; i++) {
+        this.stack[i].deactivate();
+      }
+
+      // The "active tabbing set" are the elements tabbing should be constrained
+      // to.
+      const $elements = $(elements).find(':tabbable').addBack(':tabbable');
+
+      const tabbingContext = new TabbingContext({
+        // The level is the current height of the stack before this new
+        // tabbingContext is pushed on top of the stack.
+        level: this.stack.length,
+        $tabbableElements: $elements,
+      });
+
+      this.stack.push(tabbingContext);
+
+      // Activates the tabbingContext; this will manipulate the DOM to constrain
+      // tabbing.
+      tabbingContext.activate();
+
+      // Allow modules to respond to the constrain event.
+      $(document).trigger('drupalTabbingConstrained', tabbingContext);
+
+      return tabbingContext;
+    },
+
+    /**
+     * Restores a former tabbingContext when an active one is released.
+     *
+     * The TabbingManager stack of tabbingContext instances will be unwound
+     * from the top-most released tabbingContext down to the first non-released
+     * tabbingContext instance. This non-released instance is then activated.
+     */
+    release() {
+      // Unwind as far as possible: find the topmost non-released
+      // tabbingContext.
+      let toActivate = this.stack.length - 1;
+      while (toActivate >= 0 && this.stack[toActivate].released) {
+        toActivate--;
+      }
+
+      // Delete all tabbingContexts after the to be activated one. They have
+      // already been deactivated, so their effect on the DOM has been reversed.
+      this.stack.splice(toActivate + 1);
+
+      // Get topmost tabbingContext, if one exists, and activate it.
+      if (toActivate >= 0) {
+        this.stack[toActivate].activate();
+      }
+    },
+
+    /**
+     * Makes all elements outside of the tabbingContext's set untabbable.
+     *
+     * Elements made untabbable have their original tabindex and autofocus
+     * values stored so that they might be restored later when this
+     * tabbingContext is deactivated.
+     *
+     * @param {Drupal~TabbingContext} tabbingContext
+     *   The TabbingContext instance that has been activated.
+     */
+    activate(tabbingContext) {
+      const $set = tabbingContext.$tabbableElements;
+      const level = tabbingContext.level;
+      // Determine which elements are reachable via tabbing by default.
+      const $disabledSet = $(':tabbable')
+        // Exclude elements of the active tabbing set.
+        .not($set);
+      // Set the disabled set on the tabbingContext.
+      tabbingContext.$disabledElements = $disabledSet;
+      // Record the tabindex for each element, so we can restore it later.
+      const il = $disabledSet.length;
+      for (let i = 0; i < il; i++) {
+        this.recordTabindex($disabledSet.eq(i), level);
+      }
+      // Make all tabbable elements outside of the active tabbing set
+      // unreachable.
+      $disabledSet
+        .prop('tabindex', -1)
+        .prop('autofocus', false);
+
+      // Set focus on an element in the tabbingContext's set of tabbable
+      // elements. First, check if there is an element with an autofocus
+      // attribute. Select the last one from the DOM order.
+      let $hasFocus = $set.filter('[autofocus]').eq(-1);
+      // If no element in the tabbable set has an autofocus attribute, select
+      // the first element in the set.
+      if ($hasFocus.length === 0) {
+        $hasFocus = $set.eq(0);
+      }
+      $hasFocus.trigger('focus');
+    },
+
+    /**
+     * Restores that tabbable state of a tabbingContext's disabled elements.
+     *
+     * Elements that were made untabbable have their original tabindex and
+     * autofocus values restored.
+     *
+     * @param {Drupal~TabbingContext} tabbingContext
+     *   The TabbingContext instance that has been deactivated.
+     */
+    deactivate(tabbingContext) {
+      const $set = tabbingContext.$disabledElements;
+      const level = tabbingContext.level;
+      const il = $set.length;
+      for (let i = 0; i < il; i++) {
+        this.restoreTabindex($set.eq(i), level);
+      }
+    },
+
+    /**
+     * Records the tabindex and autofocus values of an untabbable element.
+     *
+     * @param {jQuery} $el
+     *   The set of elements that have been disabled.
+     * @param {number} level
+     *   The stack level for which the tabindex attribute should be recorded.
+     */
+    recordTabindex($el, level) {
+      const tabInfo = $el.data('drupalOriginalTabIndices') || {};
+      tabInfo[level] = {
+        tabindex: $el[0].getAttribute('tabindex'),
+        autofocus: $el[0].hasAttribute('autofocus'),
+      };
+      $el.data('drupalOriginalTabIndices', tabInfo);
+    },
+
+    /**
+     * Restores the tabindex and autofocus values of a reactivated element.
+     *
+     * @param {jQuery} $el
+     *   The element that is being reactivated.
+     * @param {number} level
+     *   The stack level for which the tabindex attribute should be restored.
+     */
+    restoreTabindex($el, level) {
+      const tabInfo = $el.data('drupalOriginalTabIndices');
+      if (tabInfo && tabInfo[level]) {
+        const data = tabInfo[level];
+        if (data.tabindex) {
+          $el[0].setAttribute('tabindex', data.tabindex);
+        }
+        // If the element did not have a tabindex at this stack level then
+        // remove it.
+        else {
+          $el[0].removeAttribute('tabindex');
+        }
+        if (data.autofocus) {
+          $el[0].setAttribute('autofocus', 'autofocus');
+        }
+
+        // Clean up $.data.
+        if (level === 0) {
+          // Remove all data.
+          $el.removeData('drupalOriginalTabIndices');
+        }
+        else {
+          // Remove the data for this stack level and higher.
+          let levelToDelete = level;
+          while (tabInfo.hasOwnProperty(levelToDelete)) {
+            delete tabInfo[levelToDelete];
+            levelToDelete++;
+          }
+          $el.data('drupalOriginalTabIndices', tabInfo);
+        }
+      }
+    },
+  });
+
+  /**
+   * Stores a set of tabbable elements.
+   *
+   * This constraint can be removed with the release() method.
+   *
+   * @constructor Drupal~TabbingContext
+   *
+   * @param {object} options
+   *   A set of initiating values
+   * @param {number} options.level
+   *   The level in the TabbingManager's stack of this tabbingContext.
+   * @param {jQuery} options.$tabbableElements
+   *   The DOM elements that should be reachable via the tab key when this
+   *   tabbingContext is active.
+   * @param {jQuery} options.$disabledElements
+   *   The DOM elements that should not be reachable via the tab key when this
+   *   tabbingContext is active.
+   * @param {bool} options.released
+   *   A released tabbingContext can never be activated again. It will be
+   *   cleaned up when the TabbingManager unwinds its stack.
+   * @param {bool} options.active
+   *   When true, the tabbable elements of this tabbingContext will be reachable
+   *   via the tab key and the disabled elements will not. Only one
+   *   tabbingContext can be active at a time.
+   */
+  function TabbingContext(options) {
+    $.extend(this, /** @lends Drupal~TabbingContext# */{
+
+      /**
+       * @type {?number}
+       */
+      level: null,
+
+      /**
+       * @type {jQuery}
+       */
+      $tabbableElements: $(),
+
+      /**
+       * @type {jQuery}
+       */
+      $disabledElements: $(),
+
+      /**
+       * @type {bool}
+       */
+      released: false,
+
+      /**
+       * @type {bool}
+       */
+      active: false,
+    }, options);
+  }
+
+  /**
+   * Add public methods to the TabbingContext class.
+   */
+  $.extend(TabbingContext.prototype, /** @lends Drupal~TabbingContext# */{
+
+    /**
+     * Releases this TabbingContext.
+     *
+     * Once a TabbingContext object is released, it can never be activated
+     * again.
+     *
+     * @fires event:drupalTabbingContextReleased
+     */
+    release() {
+      if (!this.released) {
+        this.deactivate();
+        this.released = true;
+        Drupal.tabbingManager.release(this);
+        // Allow modules to respond to the tabbingContext release event.
+        $(document).trigger('drupalTabbingContextReleased', this);
+      }
+    },
+
+    /**
+     * Activates this TabbingContext.
+     *
+     * @fires event:drupalTabbingContextActivated
+     */
+    activate() {
+      // A released TabbingContext object can never be activated again.
+      if (!this.active && !this.released) {
+        this.active = true;
+        Drupal.tabbingManager.activate(this);
+        // Allow modules to respond to the constrain event.
+        $(document).trigger('drupalTabbingContextActivated', this);
+      }
+    },
+
+    /**
+     * Deactivates this TabbingContext.
+     *
+     * @fires event:drupalTabbingContextDeactivated
+     */
+    deactivate() {
+      if (this.active) {
+        this.active = false;
+        Drupal.tabbingManager.deactivate(this);
+        // Allow modules to respond to the constrain event.
+        $(document).trigger('drupalTabbingContextDeactivated', this);
+      }
+    },
+  });
+
+  // Mark this behavior as processed on the first pass and return if it is
+  // already processed.
+  if (Drupal.tabbingManager) {
+    return;
+  }
+
+  /**
+   * @type {Drupal~TabbingManager}
+   */
+  Drupal.tabbingManager = new TabbingManager();
+}(jQuery, Drupal));