Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / contextual / js / contextual.es6.js
diff --git a/web/core/modules/contextual/js/contextual.es6.js b/web/core/modules/contextual/js/contextual.es6.js
new file mode 100644 (file)
index 0000000..8c2b1ae
--- /dev/null
@@ -0,0 +1,252 @@
+/**
+ * @file
+ * Attaches behaviors for the Contextual module.
+ */
+
+(function ($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
+  const options = $.extend(drupalSettings.contextual,
+    // Merge strings on top of drupalSettings so that they are not mutable.
+    {
+      strings: {
+        open: Drupal.t('Open'),
+        close: Drupal.t('Close'),
+      },
+    },
+  );
+
+  // Clear the cached contextual links whenever the current user's set of
+  // permissions changes.
+  const cachedPermissionsHash = storage.getItem('Drupal.contextual.permissionsHash');
+  const permissionsHash = drupalSettings.user.permissionsHash;
+  if (cachedPermissionsHash !== permissionsHash) {
+    if (typeof permissionsHash === 'string') {
+      _.chain(storage).keys().each((key) => {
+        if (key.substring(0, 18) === 'Drupal.contextual.') {
+          storage.removeItem(key);
+        }
+      });
+    }
+    storage.setItem('Drupal.contextual.permissionsHash', permissionsHash);
+  }
+
+  /**
+   * Initializes a contextual link: updates its DOM, sets up model and views.
+   *
+   * @param {jQuery} $contextual
+   *   A contextual links placeholder DOM element, containing the actual
+   *   contextual links as rendered by the server.
+   * @param {string} html
+   *   The server-side rendered HTML for this contextual link.
+   */
+  function initContextual($contextual, html) {
+    const $region = $contextual.closest('.contextual-region');
+    const contextual = Drupal.contextual;
+
+    $contextual
+      // Update the placeholder to contain its rendered contextual links.
+      .html(html)
+      // Use the placeholder as a wrapper with a specific class to provide
+      // positioning and behavior attachment context.
+      .addClass('contextual')
+      // Ensure a trigger element exists before the actual contextual links.
+      .prepend(Drupal.theme('contextualTrigger'));
+
+    // Set the destination parameter on each of the contextual links.
+    const destination = `destination=${Drupal.encodePath(drupalSettings.path.currentPath)}`;
+    $contextual.find('.contextual-links a').each(function () {
+      const url = this.getAttribute('href');
+      const glue = (url.indexOf('?') === -1) ? '?' : '&';
+      this.setAttribute('href', url + glue + destination);
+    });
+
+    // Create a model and the appropriate views.
+    const model = new contextual.StateModel({
+      title: $region.find('h2').eq(0).text().trim(),
+    });
+    const viewOptions = $.extend({ el: $contextual, model }, options);
+    contextual.views.push({
+      visual: new contextual.VisualView(viewOptions),
+      aural: new contextual.AuralView(viewOptions),
+      keyboard: new contextual.KeyboardView(viewOptions),
+    });
+    contextual.regionViews.push(new contextual.RegionView(
+      $.extend({ el: $region, model }, options)),
+    );
+
+    // Add the model to the collection. This must happen after the views have
+    // been associated with it, otherwise collection change event handlers can't
+    // trigger the model change event handler in its views.
+    contextual.collection.add(model);
+
+    // Let other JavaScript react to the adding of a new contextual link.
+    $(document).trigger('drupalContextualLinkAdded', {
+      $el: $contextual,
+      $region,
+      model,
+    });
+
+    // Fix visual collisions between contextual link triggers.
+    adjustIfNestedAndOverlapping($contextual);
+  }
+
+  /**
+   * Determines if a contextual link is nested & overlapping, if so: adjusts it.
+   *
+   * This only deals with two levels of nesting; deeper levels are not touched.
+   *
+   * @param {jQuery} $contextual
+   *   A contextual links placeholder DOM element, containing the actual
+   *   contextual links as rendered by the server.
+   */
+  function adjustIfNestedAndOverlapping($contextual) {
+    const $contextuals = $contextual
+      // @todo confirm that .closest() is not sufficient
+      .parents('.contextual-region').eq(-1)
+      .find('.contextual');
+
+    // Early-return when there's no nesting.
+    if ($contextuals.length <= 1) {
+      return;
+    }
+
+    // If the two contextual links overlap, then we move the second one.
+    const firstTop = $contextuals.eq(0).offset().top;
+    const secondTop = $contextuals.eq(1).offset().top;
+    if (firstTop === secondTop) {
+      const $nestedContextual = $contextuals.eq(1);
+
+      // Retrieve height of nested contextual link.
+      let height = 0;
+      const $trigger = $nestedContextual.find('.trigger');
+      // Elements with the .visually-hidden class have no dimensions, so this
+      // class must be temporarily removed to the calculate the height.
+      $trigger.removeClass('visually-hidden');
+      height = $nestedContextual.height();
+      $trigger.addClass('visually-hidden');
+
+      // Adjust nested contextual link's position.
+      $nestedContextual.css({ top: $nestedContextual.position().top + height });
+    }
+  }
+
+  /**
+   * Attaches outline behavior for regions associated with contextual links.
+   *
+   * Events
+   *   Contextual triggers an event that can be used by other scripts.
+   *   - drupalContextualLinkAdded: Triggered when a contextual link is added.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *  Attaches the outline behavior to the right context.
+   */
+  Drupal.behaviors.contextual = {
+    attach(context) {
+      const $context = $(context);
+
+      // Find all contextual links placeholders, if any.
+      let $placeholders = $context.find('[data-contextual-id]').once('contextual-render');
+      if ($placeholders.length === 0) {
+        return;
+      }
+
+      // Collect the IDs for all contextual links placeholders.
+      const ids = [];
+      $placeholders.each(function () {
+        ids.push($(this).attr('data-contextual-id'));
+      });
+
+      // Update all contextual links placeholders whose HTML is cached.
+      const uncachedIDs = _.filter(ids, (contextualID) => {
+        const html = storage.getItem(`Drupal.contextual.${contextualID}`);
+        if (html && html.length) {
+          // Initialize after the current execution cycle, to make the AJAX
+          // request for retrieving the uncached contextual links as soon as
+          // possible, but also to ensure that other Drupal behaviors have had
+          // the chance to set up an event listener on the Backbone collection
+          // Drupal.contextual.collection.
+          window.setTimeout(() => {
+            initContextual($context.find(`[data-contextual-id="${contextualID}"]`), html);
+          });
+          return false;
+        }
+        return true;
+      });
+
+      // Perform an AJAX request to let the server render the contextual links
+      // for each of the placeholders.
+      if (uncachedIDs.length > 0) {
+        $.ajax({
+          url: Drupal.url('contextual/render'),
+          type: 'POST',
+          data: { 'ids[]': uncachedIDs },
+          dataType: 'json',
+          success(results) {
+            _.each(results, (html, contextualID) => {
+              // Store the metadata.
+              storage.setItem(`Drupal.contextual.${contextualID}`, html);
+              // If the rendered contextual links are empty, then the current
+              // user does not have permission to access the associated links:
+              // don't render anything.
+              if (html.length > 0) {
+                // Update the placeholders to contain its rendered contextual
+                // links. Usually there will only be one placeholder, but it's
+                // possible for multiple identical placeholders exist on the
+                // page (probably because the same content appears more than
+                // once).
+                $placeholders = $context.find(`[data-contextual-id="${contextualID}"]`);
+
+                // Initialize the contextual links.
+                for (let i = 0; i < $placeholders.length; i++) {
+                  initContextual($placeholders.eq(i), html);
+                }
+              }
+            });
+          },
+        });
+      }
+    },
+  };
+
+  /**
+   * Namespace for contextual related functionality.
+   *
+   * @namespace
+   */
+  Drupal.contextual = {
+
+    /**
+     * The {@link Drupal.contextual.View} instances associated with each list
+     * element of contextual links.
+     *
+     * @type {Array}
+     */
+    views: [],
+
+    /**
+     * The {@link Drupal.contextual.RegionView} instances associated with each
+     * contextual region element.
+     *
+     * @type {Array}
+     */
+    regionViews: [],
+  };
+
+  /**
+   * A Backbone.Collection of {@link Drupal.contextual.StateModel} instances.
+   *
+   * @type {Backbone.Collection}
+   */
+  Drupal.contextual.collection = new Backbone.Collection([], { model: Drupal.contextual.StateModel });
+
+  /**
+   * A trigger is an interactive element often bound to a click handler.
+   *
+   * @return {string}
+   *   A string representing a DOM fragment.
+   */
+  Drupal.theme.contextualTrigger = function () {
+    return '<button class="trigger visually-hidden focusable" type="button"></button>';
+  };
+}(jQuery, Drupal, drupalSettings, _, Backbone, window.JSON, window.sessionStorage));