538 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			538 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * @file
							 | 
						||
| 
								 | 
							
								 * Attaches behaviors for the Contextual module.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								(function ($, Drupal, drupalSettings, 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;
							 | 
						||
| 
								 | 
							
								  if (cachedPermissionsHash !== permissionsHash) {
							 | 
						||
| 
								 | 
							
								    if (typeof permissionsHash === 'string') {
							 | 
						||
| 
								 | 
							
								      Object.keys(storage).forEach((key) => {
							 | 
						||
| 
								 | 
							
								        if (key.startsWith('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[0].style.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
							 | 
						||
| 
								 | 
							
								      // 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(
							 | 
						||
| 
								 | 
							
								      Drupal.url(drupalSettings.path.currentPath + window.location.search),
							 | 
						||
| 
								 | 
							
								    )}`;
							 | 
						||
| 
								 | 
							
								    $contextual.find('.contextual-links a').each(function () {
							 | 
						||
| 
								 | 
							
								      const url = this.getAttribute('href');
							 | 
						||
| 
								 | 
							
								      const glue = url.includes('?') ? '&' : '?';
							 | 
						||
| 
								 | 
							
								      this.setAttribute('href', url + glue + destination);
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								    let title = '';
							 | 
						||
| 
								 | 
							
								    const $regionHeading = $region.find('h2');
							 | 
						||
| 
								 | 
							
								    if ($regionHeading.length) {
							 | 
						||
| 
								 | 
							
								      title = $regionHeading[0].textContent.trim();
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    options.title = title;
							 | 
						||
| 
								 | 
							
								    const contextualModelView = new Drupal.contextual.ContextualModelView(
							 | 
						||
| 
								 | 
							
								      $contextual,
							 | 
						||
| 
								 | 
							
								      $region,
							 | 
						||
| 
								 | 
							
								      options,
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								    contextual.instances.push(contextualModelView);
							 | 
						||
| 
								 | 
							
								    // 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 = $(
							 | 
						||
| 
								 | 
							
								        once('contextual-render', '[data-contextual-id]', context),
							 | 
						||
| 
								 | 
							
								      );
							 | 
						||
| 
								 | 
							
								      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?.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 collection
							 | 
						||
| 
								 | 
							
								          // Drupal.contextual.collection.
							 | 
						||
| 
								 | 
							
								          window.setTimeout(() => {
							 | 
						||
| 
								 | 
							
								            initContextual(
							 | 
						||
| 
								 | 
							
								              $context
							 | 
						||
| 
								 | 
							
								                .find(`[data-contextual-id="${contextualID.id}"]:empty`)
							 | 
						||
| 
								 | 
							
								                .eq(0),
							 | 
						||
| 
								 | 
							
								              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) {
							 | 
						||
| 
								 | 
							
								            Object.entries(results).forEach(([contextualID, html]) => {
							 | 
						||
| 
								 | 
							
								              // 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
							 | 
						||
| 
								 | 
							
								   *
							 | 
						||
| 
								 | 
							
								   * @private
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  Drupal.contextual = {
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * The {@link Drupal.contextual.View} instances associated with each list
							 | 
						||
| 
								 | 
							
								     * element of contextual links.
							 | 
						||
| 
								 | 
							
								     *
							 | 
						||
| 
								 | 
							
								     * @type {Array}
							 | 
						||
| 
								 | 
							
								     *
							 | 
						||
| 
								 | 
							
								     * @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
							 | 
						||
| 
								 | 
							
								     *  replacement.
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								    views: [],
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * The {@link Drupal.contextual.RegionView} instances associated with each
							 | 
						||
| 
								 | 
							
								     * contextual region element.
							 | 
						||
| 
								 | 
							
								     *
							 | 
						||
| 
								 | 
							
								     * @type {Array}
							 | 
						||
| 
								 | 
							
								     *
							 | 
						||
| 
								 | 
							
								     * @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
							 | 
						||
| 
								 | 
							
								     *  replacement.
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								    regionViews: [],
							 | 
						||
| 
								 | 
							
								    instances: new Proxy([], {
							 | 
						||
| 
								 | 
							
								      set: function set(obj, prop, value) {
							 | 
						||
| 
								 | 
							
								        obj[prop] = value;
							 | 
						||
| 
								 | 
							
								        window.dispatchEvent(new Event('contextual-instances-added'));
							 | 
						||
| 
								 | 
							
								        return true;
							 | 
						||
| 
								 | 
							
								      },
							 | 
						||
| 
								 | 
							
								      deleteProperty(target, prop) {
							 | 
						||
| 
								 | 
							
								        if (prop in target) {
							 | 
						||
| 
								 | 
							
								          delete target[prop];
							 | 
						||
| 
								 | 
							
								          window.dispatchEvent(new Event('contextual-instances-removed'));
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      },
							 | 
						||
| 
								 | 
							
								    }),
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Models the state of a contextual link's trigger, list & region.
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								    ContextualModelView: class {
							 | 
						||
| 
								 | 
							
								      constructor($contextual, $region, options) {
							 | 
						||
| 
								 | 
							
								        this.title = options.title || '';
							 | 
						||
| 
								 | 
							
								        this.regionIsHovered = false;
							 | 
						||
| 
								 | 
							
								        this._hasFocus = false;
							 | 
						||
| 
								 | 
							
								        this._isOpen = false;
							 | 
						||
| 
								 | 
							
								        this._isLocked = false;
							 | 
						||
| 
								 | 
							
								        this.strings = options.strings;
							 | 
						||
| 
								 | 
							
								        this.timer = NaN;
							 | 
						||
| 
								 | 
							
								        this.modelId = btoa(Math.random()).substring(0, 12);
							 | 
						||
| 
								 | 
							
								        this.$region = $region;
							 | 
						||
| 
								 | 
							
								        this.$contextual = $contextual;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (!document.body.classList.contains('touchevents')) {
							 | 
						||
| 
								 | 
							
								          this.$region.on({
							 | 
						||
| 
								 | 
							
								            mouseenter: () => {
							 | 
						||
| 
								 | 
							
								              this.regionIsHovered = true;
							 | 
						||
| 
								 | 
							
								            },
							 | 
						||
| 
								 | 
							
								            mouseleave: () => {
							 | 
						||
| 
								 | 
							
								              this.close().blur();
							 | 
						||
| 
								 | 
							
								              this.regionIsHovered = false;
							 | 
						||
| 
								 | 
							
								            },
							 | 
						||
| 
								 | 
							
								            'mouseleave mouseenter': () => this.render(),
							 | 
						||
| 
								 | 
							
								          });
							 | 
						||
| 
								 | 
							
								          this.$contextual.on('mouseenter', () => {
							 | 
						||
| 
								 | 
							
								            this.focus();
							 | 
						||
| 
								 | 
							
								            this.render();
							 | 
						||
| 
								 | 
							
								          });
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        this.$contextual.on(
							 | 
						||
| 
								 | 
							
								          {
							 | 
						||
| 
								 | 
							
								            click: () => {
							 | 
						||
| 
								 | 
							
								              this.toggleOpen();
							 | 
						||
| 
								 | 
							
								            },
							 | 
						||
| 
								 | 
							
								            touchend: () => {
							 | 
						||
| 
								 | 
							
								              Drupal.contextual.ContextualModelView.touchEndToClick();
							 | 
						||
| 
								 | 
							
								            },
							 | 
						||
| 
								 | 
							
								            focus: () => {
							 | 
						||
| 
								 | 
							
								              this.focus();
							 | 
						||
| 
								 | 
							
								            },
							 | 
						||
| 
								 | 
							
								            blur: () => {
							 | 
						||
| 
								 | 
							
								              this.blur();
							 | 
						||
| 
								 | 
							
								            },
							 | 
						||
| 
								 | 
							
								            'click blur touchend focus': () => this.render(),
							 | 
						||
| 
								 | 
							
								          },
							 | 
						||
| 
								 | 
							
								          '.trigger',
							 | 
						||
| 
								 | 
							
								        );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        this.$contextual.on(
							 | 
						||
| 
								 | 
							
								          {
							 | 
						||
| 
								 | 
							
								            click: () => {
							 | 
						||
| 
								 | 
							
								              this.close().blur();
							 | 
						||
| 
								 | 
							
								            },
							 | 
						||
| 
								 | 
							
								            touchend: (event) => {
							 | 
						||
| 
								 | 
							
								              Drupal.contextual.ContextualModelView.touchEndToClick(event);
							 | 
						||
| 
								 | 
							
								            },
							 | 
						||
| 
								 | 
							
								            focus: () => {
							 | 
						||
| 
								 | 
							
								              this.focus();
							 | 
						||
| 
								 | 
							
								            },
							 | 
						||
| 
								 | 
							
								            blur: () => {
							 | 
						||
| 
								 | 
							
								              this.waitCloseThenBlur();
							 | 
						||
| 
								 | 
							
								            },
							 | 
						||
| 
								 | 
							
								            'click blur touchend focus': () => this.render(),
							 | 
						||
| 
								 | 
							
								          },
							 | 
						||
| 
								 | 
							
								          '.contextual-links a',
							 | 
						||
| 
								 | 
							
								        );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        this.render();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        // Let other JavaScript react to the adding of a new contextual link.
							 | 
						||
| 
								 | 
							
								        $(document).trigger('drupalContextualLinkAdded', {
							 | 
						||
| 
								 | 
							
								          $el: $contextual,
							 | 
						||
| 
								 | 
							
								          $region,
							 | 
						||
| 
								 | 
							
								          model: this,
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Updates the rendered representation of the current contextual links.
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      render() {
							 | 
						||
| 
								 | 
							
								        const { isOpen } = this;
							 | 
						||
| 
								 | 
							
								        const isVisible = this.isLocked || this.regionIsHovered || isOpen;
							 | 
						||
| 
								 | 
							
								        this.$region.toggleClass('focus', this.hasFocus);
							 | 
						||
| 
								 | 
							
								        this.$contextual
							 | 
						||
| 
								 | 
							
								          .toggleClass('open', isOpen)
							 | 
						||
| 
								 | 
							
								          // Update the visibility of the trigger.
							 | 
						||
| 
								 | 
							
								          .find('.trigger')
							 | 
						||
| 
								 | 
							
								          .toggleClass('visually-hidden', !isVisible);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        this.$contextual.find('.contextual-links').prop('hidden', !isOpen);
							 | 
						||
| 
								 | 
							
								        const trigger = this.$contextual.find('.trigger').get(0);
							 | 
						||
| 
								 | 
							
								        trigger.textContent = Drupal.t('@action @title configuration options', {
							 | 
						||
| 
								 | 
							
								          '@action': !isOpen ? this.strings.open : this.strings.close,
							 | 
						||
| 
								 | 
							
								          '@title': this.title,
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								        trigger.setAttribute('aria-pressed', isOpen);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Prevents delay and simulated mouse events.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * @param {jQuery.Event} event the touch end event.
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      static touchEndToClick(event) {
							 | 
						||
| 
								 | 
							
								        event.preventDefault();
							 | 
						||
| 
								 | 
							
								        event.target.click();
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Set up a timeout to allow a user to tab between the trigger and the
							 | 
						||
| 
								 | 
							
								       * contextual links without the menu dismissing.
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      waitCloseThenBlur() {
							 | 
						||
| 
								 | 
							
								        this.timer = window.setTimeout(() => {
							 | 
						||
| 
								 | 
							
								          this.isOpen = false;
							 | 
						||
| 
								 | 
							
								          this.hasFocus = false;
							 | 
						||
| 
								 | 
							
								          this.render();
							 | 
						||
| 
								 | 
							
								        }, 150);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Opens or closes the contextual link.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * If it is opened, then also give focus.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * @return {Drupal.contextual.ContextualModelView}
							 | 
						||
| 
								 | 
							
								       *   The current contextual model view.
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      toggleOpen() {
							 | 
						||
| 
								 | 
							
								        const newIsOpen = !this.isOpen;
							 | 
						||
| 
								 | 
							
								        this.isOpen = newIsOpen;
							 | 
						||
| 
								 | 
							
								        if (newIsOpen) {
							 | 
						||
| 
								 | 
							
								          this.focus();
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        return this;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Gives focus to this contextual link.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * Also closes + removes focus from every other contextual link.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * @return {Drupal.contextual.ContextualModelView}
							 | 
						||
| 
								 | 
							
								       *   The current contextual model view.
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      focus() {
							 | 
						||
| 
								 | 
							
								        const { modelId } = this;
							 | 
						||
| 
								 | 
							
								        Drupal.contextual.instances.forEach((model) => {
							 | 
						||
| 
								 | 
							
								          if (model.modelId !== modelId) {
							 | 
						||
| 
								 | 
							
								            model.close().blur();
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								        window.clearTimeout(this.timer);
							 | 
						||
| 
								 | 
							
								        this.hasFocus = true;
							 | 
						||
| 
								 | 
							
								        return this;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Removes focus from this contextual link, unless it is open.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * @return {Drupal.contextual.ContextualModelView}
							 | 
						||
| 
								 | 
							
								       *   The current contextual model view.
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      blur() {
							 | 
						||
| 
								 | 
							
								        if (!this.isOpen) {
							 | 
						||
| 
								 | 
							
								          this.hasFocus = false;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        return this;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Closes this contextual link.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * Does not call blur() because we want to allow a contextual link to have
							 | 
						||
| 
								 | 
							
								       * focus, yet be closed for example when hovering.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * @return {Drupal.contextual.ContextualModelView}
							 | 
						||
| 
								 | 
							
								       *   The current contextual model view.
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      close() {
							 | 
						||
| 
								 | 
							
								        this.isOpen = false;
							 | 
						||
| 
								 | 
							
								        return this;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Gets the current focus state.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * @return {boolean} the focus state.
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      get hasFocus() {
							 | 
						||
| 
								 | 
							
								        return this._hasFocus;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Sets the current focus state.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * @param {boolean} value - new focus state
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      set hasFocus(value) {
							 | 
						||
| 
								 | 
							
								        this._hasFocus = value;
							 | 
						||
| 
								 | 
							
								        this.$region.toggleClass('focus', this._hasFocus);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Gets the current open state.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * @return {boolean} the open state.
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      get isOpen() {
							 | 
						||
| 
								 | 
							
								        return this._isOpen;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Sets the current open state.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * @param {boolean} value - new open state
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      set isOpen(value) {
							 | 
						||
| 
								 | 
							
								        this._isOpen = value;
							 | 
						||
| 
								 | 
							
								        // Nested contextual region handling: hide any nested contextual triggers.
							 | 
						||
| 
								 | 
							
								        this.$region
							 | 
						||
| 
								 | 
							
								          .closest('.contextual-region')
							 | 
						||
| 
								 | 
							
								          .find('.contextual .trigger:not(:first)')
							 | 
						||
| 
								 | 
							
								          .toggle(!this.isOpen);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Gets the current locked state.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * @return {boolean} the locked state.
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      get isLocked() {
							 | 
						||
| 
								 | 
							
								        return this._isLocked;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      /**
							 | 
						||
| 
								 | 
							
								       * Sets the current locked state.
							 | 
						||
| 
								 | 
							
								       *
							 | 
						||
| 
								 | 
							
								       * @param {boolean} value - new locked state
							 | 
						||
| 
								 | 
							
								       */
							 | 
						||
| 
								 | 
							
								      set isLocked(value) {
							 | 
						||
| 
								 | 
							
								        if (value !== this._isLocked) {
							 | 
						||
| 
								 | 
							
								          this._isLocked = value;
							 | 
						||
| 
								 | 
							
								          this.render();
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    },
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * 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, window.JSON, window.sessionStorage);
							 |