text: /**
   * @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);
    }
  
    /**
     * 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 });
      }
    }
  
    /**
     * 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);
    }
  
    /**
     * 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({
            id: $(this).attr('data-contextual-id'),
            token: $(this).attr('data-contextual-token'),
          });
        });
  
        const uncachedIDs = [];
        const uncachedTokens = [];
        ids.forEach(contextualID => {
          const html = storage.getItem(`Drupal.contextual.${contextualID.id}`);
          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.id}"]`),
                html,
              );
            });
            return;
          }
          uncachedIDs.push(contextualID.id);
          uncachedTokens.push(contextualID.token);
        });
  
        // 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, 'tokens[]': uncachedTokens },
            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>';
    };
  
    /**
     * Bind Ajax contextual links when added.
     *
     * @param {jQuery.Event} event
     *   The `drupalContextualLinkAdded` event.
     * @param {object} data
     *   An object containing the data relevant to the event.
     *
     * @listens event:drupalContextualLinkAdded
     */
    $(document).on('drupalContextualLinkAdded', (event, data) => {
      Drupal.ajax.bindAjaxLinks(data.$el[0]);
    });
  })(
    jQuery,
    Drupal,
    drupalSettings,
    _,
    Backbone,
    window.JSON,
    window.sessionStorage,
  );
  