176 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			176 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * @file
							 | 
						||
| 
								 | 
							
								 * Adds assets the current page requires.
							 | 
						||
| 
								 | 
							
								 *
							 | 
						||
| 
								 | 
							
								 * This script fires a custom `htmx:drupal:load` event when the request has
							 | 
						||
| 
								 | 
							
								 * settled and all script and css files have been successfully loaded on the
							 | 
						||
| 
								 | 
							
								 * page.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								(function (Drupal, drupalSettings, loadjs, htmx) {
							 | 
						||
| 
								 | 
							
								  // Disable htmx loading of script tags since we're handling it.
							 | 
						||
| 
								 | 
							
								  htmx.config.allowScriptTags = false;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Used to hold the loadjs promise.
							 | 
						||
| 
								 | 
							
								   *
							 | 
						||
| 
								 | 
							
								   * It's declared in htmx:beforeSwap and checked in htmx:afterSettle to trigger
							 | 
						||
| 
								 | 
							
								   * the custom htmx:drupal:load event.
							 | 
						||
| 
								 | 
							
								   *
							 | 
						||
| 
								 | 
							
								   * @type {WeakMap<XMLHttpRequest, Promise>}
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  const requestAssetsLoaded = new WeakMap();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Helper function to merge two objects recursively.
							 | 
						||
| 
								 | 
							
								   *
							 | 
						||
| 
								 | 
							
								   * @param current
							 | 
						||
| 
								 | 
							
								   *   The object to receive the merged values.
							 | 
						||
| 
								 | 
							
								   * @param sources
							 | 
						||
| 
								 | 
							
								   *   The objects to merge into current.
							 | 
						||
| 
								 | 
							
								   *
							 | 
						||
| 
								 | 
							
								   * @return object
							 | 
						||
| 
								 | 
							
								   *   The merged object.
							 | 
						||
| 
								 | 
							
								   *
							 | 
						||
| 
								 | 
							
								   * @see https://youmightnotneedjquery.com/#deep_extend
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  function mergeSettings(current, ...sources) {
							 | 
						||
| 
								 | 
							
								    if (!current) {
							 | 
						||
| 
								 | 
							
								      return {};
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    sources
							 | 
						||
| 
								 | 
							
								      .filter((obj) => Boolean(obj))
							 | 
						||
| 
								 | 
							
								      .forEach((obj) => {
							 | 
						||
| 
								 | 
							
								        Object.entries(obj).forEach(([key, value]) => {
							 | 
						||
| 
								 | 
							
								          switch (Object.prototype.toString.call(value)) {
							 | 
						||
| 
								 | 
							
								            case '[object Object]':
							 | 
						||
| 
								 | 
							
								              current[key] = current[key] || {};
							 | 
						||
| 
								 | 
							
								              current[key] = mergeSettings(current[key], value);
							 | 
						||
| 
								 | 
							
								              break;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            case '[object Array]':
							 | 
						||
| 
								 | 
							
								              current[key] = mergeSettings(new Array(value.length), value);
							 | 
						||
| 
								 | 
							
								              break;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            default:
							 | 
						||
| 
								 | 
							
								              current[key] = value;
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return current;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Send the current ajax page state with each request.
							 | 
						||
| 
								 | 
							
								   *
							 | 
						||
| 
								 | 
							
								   * @param configRequestEvent
							 | 
						||
| 
								 | 
							
								   *   HTMX event for request configuration.
							 | 
						||
| 
								 | 
							
								   *
							 | 
						||
| 
								 | 
							
								   * @see system_js_settings_alter()
							 | 
						||
| 
								 | 
							
								   * @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor::processAttachments
							 | 
						||
| 
								 | 
							
								   * @see https://htmx.org/api/#on
							 | 
						||
| 
								 | 
							
								   * @see https://htmx.org/events/#htmx:configRequest
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  htmx.on('htmx:configRequest', ({ detail }) => {
							 | 
						||
| 
								 | 
							
								    const url = new URL(detail.path, document.location.href);
							 | 
						||
| 
								 | 
							
								    if (Drupal.url.isLocal(url.toString())) {
							 | 
						||
| 
								 | 
							
								      // Allow Drupal to return new JavaScript and CSS files to load without
							 | 
						||
| 
								 | 
							
								      // returning the ones already loaded.
							 | 
						||
| 
								 | 
							
								      // @see \Drupal\Core\StackMiddleWare\AjaxPageState
							 | 
						||
| 
								 | 
							
								      // @see \Drupal\Core\Theme\AjaxBasePageNegotiator
							 | 
						||
| 
								 | 
							
								      // @see \Drupal\Core\Asset\LibraryDependencyResolverInterface::getMinimalRepresentativeSubset()
							 | 
						||
| 
								 | 
							
								      // @see system_js_settings_alter()
							 | 
						||
| 
								 | 
							
								      const pageState = drupalSettings.ajaxPageState;
							 | 
						||
| 
								 | 
							
								      detail.parameters['ajax_page_state[theme]'] = pageState.theme;
							 | 
						||
| 
								 | 
							
								      detail.parameters['ajax_page_state[theme_token]'] = pageState.theme_token;
							 | 
						||
| 
								 | 
							
								      detail.parameters['ajax_page_state[libraries]'] = pageState.libraries;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // @see https://htmx.org/events/#htmx:beforeSwap
							 | 
						||
| 
								 | 
							
								  htmx.on('htmx:beforeSwap', ({ detail }) => {
							 | 
						||
| 
								 | 
							
								    // Custom event to detach behaviors.
							 | 
						||
| 
								 | 
							
								    htmx.trigger(detail.elt, 'htmx:drupal:unload');
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // We need to parse the response to find all the assets to load.
							 | 
						||
| 
								 | 
							
								    // htmx cleans up too many things to be able to rely on their dom fragment.
							 | 
						||
| 
								 | 
							
								    let responseHTML = Document.parseHTMLUnsafe(detail.serverResponse);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // Update drupalSettings
							 | 
						||
| 
								 | 
							
								    // Use direct child elements to harden against XSS exploits when CSP is on.
							 | 
						||
| 
								 | 
							
								    const settingsElement = responseHTML.querySelector(
							 | 
						||
| 
								 | 
							
								      ':is(head, body) > script[type="application/json"][data-drupal-selector="drupal-settings-json"]',
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								    if (settingsElement !== null) {
							 | 
						||
| 
								 | 
							
								      mergeSettings(drupalSettings, JSON.parse(settingsElement.textContent));
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // Load all assets files. We sent ajax_page_state in the request so this is only the diff with the current page.
							 | 
						||
| 
								 | 
							
								    const assetsTags = responseHTML.querySelectorAll(
							 | 
						||
| 
								 | 
							
								      'link[rel="stylesheet"][href], script[src]',
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								    const bundleIds = Array.from(assetsTags)
							 | 
						||
| 
								 | 
							
								      .filter(({ href, src }) => !loadjs.isDefined(href ?? src))
							 | 
						||
| 
								 | 
							
								      .map(({ href, src, type, attributes }) => {
							 | 
						||
| 
								 | 
							
								        const bundleId = href ?? src;
							 | 
						||
| 
								 | 
							
								        let prefix = 'css!';
							 | 
						||
| 
								 | 
							
								        if (src) {
							 | 
						||
| 
								 | 
							
								          prefix = type === 'module' ? 'module!' : 'js!';
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        loadjs(prefix + bundleId, bundleId, {
							 | 
						||
| 
								 | 
							
								          // JS files are loaded in order, so this needs to be false when 'src'
							 | 
						||
| 
								 | 
							
								          // is defined.
							 | 
						||
| 
								 | 
							
								          async: !src,
							 | 
						||
| 
								 | 
							
								          // Copy asset tag attributes to the new element.
							 | 
						||
| 
								 | 
							
								          before(path, element) {
							 | 
						||
| 
								 | 
							
								            // This allows all attributes to be added, like defer, async and
							 | 
						||
| 
								 | 
							
								            // crossorigin.
							 | 
						||
| 
								 | 
							
								            Object.values(attributes).forEach((attr) => {
							 | 
						||
| 
								 | 
							
								              element.setAttribute(attr.name, attr.value);
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								          },
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return bundleId;
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // Helps with memory management.
							 | 
						||
| 
								 | 
							
								    responseHTML = null;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // Nothing to load, we resolve the promise right away.
							 | 
						||
| 
								 | 
							
								    let assetsLoaded = Promise.resolve();
							 | 
						||
| 
								 | 
							
								    // If there are assets to load, use loadjs to manage this process.
							 | 
						||
| 
								 | 
							
								    if (bundleIds.length) {
							 | 
						||
| 
								 | 
							
								      // Trigger the event once all the dependencies have loaded.
							 | 
						||
| 
								 | 
							
								      assetsLoaded = new Promise((resolve, reject) => {
							 | 
						||
| 
								 | 
							
								        loadjs.ready(bundleIds, {
							 | 
						||
| 
								 | 
							
								          success: resolve,
							 | 
						||
| 
								 | 
							
								          error(depsNotFound) {
							 | 
						||
| 
								 | 
							
								            const message = Drupal.t(
							 | 
						||
| 
								 | 
							
								              `The following files could not be loaded: @dependencies`,
							 | 
						||
| 
								 | 
							
								              { '@dependencies': depsNotFound.join(', ') },
							 | 
						||
| 
								 | 
							
								            );
							 | 
						||
| 
								 | 
							
								            reject(message);
							 | 
						||
| 
								 | 
							
								          },
							 | 
						||
| 
								 | 
							
								        });
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    requestAssetsLoaded.set(detail.xhr, assetsLoaded);
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Trigger the Drupal processing once all assets have been loaded.
							 | 
						||
| 
								 | 
							
								  // @see https://htmx.org/events/#htmx:afterSettle
							 | 
						||
| 
								 | 
							
								  htmx.on('htmx:afterSettle', ({ detail }) => {
							 | 
						||
| 
								 | 
							
								    requestAssetsLoaded.get(detail.xhr).then(() => {
							 | 
						||
| 
								 | 
							
								      // Some HTMX swaps put the incoming element before or after detail.elt.
							 | 
						||
| 
								 | 
							
								      htmx.trigger(detail.elt.parentNode, 'htmx:drupal:load');
							 | 
						||
| 
								 | 
							
								      // This should be automatic but don't wait for the garbage collector.
							 | 
						||
| 
								 | 
							
								      requestAssetsLoaded.delete(detail.xhr);
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								})(Drupal, drupalSettings, loadjs, htmx);
							 |