refreshless-8.x-1.x-dev/modules/refreshless_turbo/js/drupal_settings.js

modules/refreshless_turbo/js/drupal_settings.js
(function(Drupal, drupalSettings, $) {

  'use strict';

  /**
   * Our event namespace.
   *
   * @type {String}
   *
   * @see https://learn.jquery.com/events/event-basics/#namespacing-events
   */
  const eventNamespace = 'refreshless-turbo-drupal-settings-manager';

  /**
   * RefreshLess Turbo drupalSettings updater class.
   */
  class DrupalSettings {

    /**
     * The context element to attach to; usually the <html> element.
     *
     * @type {HTMLElement}
     */
    #context;

    /**
     * The most recent drupalSettings JSON <script> element, or undefined.
     *
     * @type {HTMLScriptElement|undefined}
     */
    #element;

    /**
     * The drupalSettings <script> element selector.
     *
     * Note that this doesn't include the leading '>' combinator for XSS
     * hardening.
     *
     * @type {String}
     *
     * @see core/misc/drupalSettingsLoader.js
     *   Explains the need for XSS hardening.
     */
    #selector = 'script[type="application/json"][data-drupal-selector="drupal-settings-json"]';

    constructor(context) {

      this.#context = context;

      this.#bindEventHandlers();

    };

    /**
     * Destroy this instance.
     */
    destroy() {

      this.#unbindEventHandlers();

    }

    /**
     * Bind all of our event handlers.
     */
    #bindEventHandlers() {

      // @see https://ambientimpact.com/web/snippets/javascript-template-literal-as-object-property-name
      $(this.#context).on({
        [`refreshless:scripts-merged.${eventNamespace}`]: (event) => {
          this.#mergedHandler(event);
        },
      });

    }

    /**
     * Unbind all of our event handlers.
     */
    #unbindEventHandlers() {

      $(this.#context).off(`.${eventNamespace}`);

    }

    /**
     * 'refreshless:scripts-merged' event handler.
     *
     * Note that we could listen to the 'refreshless:before-scripts-merge' event
     * but that would make it more difficult to determine if the <script> is
     * a direct child of the <body> or an attempted XSS attack nested deeper in.
     *
     * @param {jQuery.Event} event
     */
    #mergedHandler(event) {

      // If an element has already been found, don't attempt to find another.
      // Because this event is triggered twice - once for the <head> and then
      // again for the <body>, this replicates the Drupal core behaviour of
      // using the one found in the <head>, only using one found in the <body>
      // if there isn't one in the <head>.
      if (typeof this.#element !== 'undefined') {
        return;
      }

      for (const element of event.detail.new) {

        if (
          // Skip <script> elements that aren't the drupalSettings JSON.
          $(element).is(this.#selector) === false ||
          // XSS hardening: skip any drupalSettings JSON that isn't a direct
          // child of the <head> or <body> as that could indicate a potential
          // <script> element injected into content as part of an attack.
          $(element).parent(event.detail.context).length === 0
        ) {
          continue;
        }

        // Now that we've found a new drupalSettings JSON element, we should
        // remove any previous element.
        $(element).siblings(this.#selector).remove();

        this.#element = element;

        return;

      }

    }

    /**
     * Update the drupalSettings object from the most recent <script> element.
     *
     * This invoked externally rather than on an event to ensure
     *
     * @throws Error
     *   If no settings element was found or if the settings element text could
     *   not be parsed into valid JSON.
     *
     * @return {Promise}
     *   A Promise that fulfills when drupalSettings has been updated.
     *
     * @todo Remove the need to invoke externally and do all this in event
     *   handlers.
     *
     * @todo Should we instead discard the current settings object rather than
     *   merging?
     */
    update() {

      if (typeof this.#element === 'undefined') {

        throw new Error(Drupal.t(
          'Could not find a Drupal settings <script> element!',
        ));

      }

      const $script = $(this.#element);

      /**
       * Clone of the previous drupalSettings before merging.
       *
       * @type {Object}
       */
      const previousSettings = $.extend(true, {}, drupalSettings);

      /**
       * New drupalSettings values or null if they can't be read.
       *
       * @type {Object|null}
       */
      const newSettings = JSON.parse($script.text());

      if (newSettings === null) {

        throw new Error(Drupal.t(
          `Could not parse the drupalSettings JSON from the <script> element. Got: @text`,
            {'@text': $script.text()},
        ));

      }

      $.extend(true, drupalSettings, newSettings);

      const resolvedValues = {
        new:      newSettings,
        previous: previousSettings,
        merged:   drupalSettings,
      }

      const settingsEvent = new CustomEvent(
        'refreshless:drupal-settings-update', {detail: resolvedValues},
      );

      this.#context.dispatchEvent(settingsEvent);

      // Unset the element now that we're done with it. This is necessary for
      // this.#mergedHandler() to replicate core's behaviour correctly.
      this.#element = undefined;

      return Promise.resolve(resolvedValues);

    };

  }

  // Merge Drupal.RefreshLess.classes into the existing Drupal global.
  $.extend(true, Drupal, {RefreshLess: {classes: {
    TurboDrupalSettings: DrupalSettings,
  }}});

})(Drupal, drupalSettings, jQuery);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc