Loading core/.eslintrc.json +1 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ "_": true, "Cookies": true, "Backbone": true, "htmx": true, "loadjs": true, "Shepherd": true, "Sortable": true, Loading core/core.libraries.yml +18 −0 Original line number Diff line number Diff line Loading @@ -614,6 +614,24 @@ drupal.form: - core/drupal.debounce - core/once drupal.htmx: version: VERSION js: misc/htmx/htmx-assets.js: {} misc/htmx/htmx-behaviors.js: {} dependencies: - core/htmx - core/drupal - core/drupalSettings - core/loadjs drupalSettings: # These placeholder values will be set by system_js_settings_alter(). ajaxPageState: libraries: null theme: null theme_token: null ajaxTrustedUrl: {} drupal.machine-name: version: VERSION js: Loading core/misc/htmx/htmx-assets.js 0 → 100644 +174 −0 Original line number Diff line number Diff line /** * @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(() => { htmx.trigger(detail.elt, 'htmx:drupal:load'); // This should be automatic but don't wait for the garbage collector. requestAssetsLoaded.delete(detail.xhr); }); }); })(Drupal, drupalSettings, loadjs, htmx); core/misc/htmx/htmx-behaviors.js 0 → 100644 +41 −0 Original line number Diff line number Diff line /** * @file * Connect Drupal.behaviors to htmx inserted content. */ (function (Drupal, htmx, drupalSettings) { // Flag used to prevent running htmx initialization twice on elements we know // have already been processed. let attachFromHtmx = false; // This is a custom event that triggers once the htmx request settled and // all JS and CSS assets have been loaded successfully. // @see https://htmx.org/api/#on // @see htmx-assets.js htmx.on('htmx:drupal:load', ({ detail }) => { attachFromHtmx = true; Drupal.attachBehaviors(detail.elt, drupalSettings); attachFromHtmx = false; }); // When htmx removes elements from the DOM, make sure they're detached first. // This event is currently an alias of htmx:beforeSwap htmx.on('htmx:drupal:unload', ({ detail }) => { Drupal.detachBehaviors(detail.elt, drupalSettings, 'unload'); }); /** * Initialize HTMX library on content added by Drupal Ajax Framework. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Initialize htmx behavior. */ Drupal.behaviors.htmx = { attach(context) { if (!attachFromHtmx && context !== document) { htmx.process(context); } }, }; })(Drupal, htmx, drupalSettings); core/modules/system/src/Hook/SystemHooks.php +5 −1 Original line number Diff line number Diff line Loading @@ -276,7 +276,11 @@ public function jsSettingsAlter(&$settings, AttachedAssetsInterface $assets): vo // before doing so. Also add the loaded libraries to ajaxPageState. /** @var \Drupal\Core\Asset\LibraryDependencyResolver $library_dependency_resolver */ $library_dependency_resolver = \Drupal::service('library.dependency_resolver'); if (isset($settings['ajaxPageState']) || in_array('core/drupal.ajax', $library_dependency_resolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()))) { $loaded_libraries = []; if (!isset($settings['ajaxPageState'])) { $loaded_libraries = $library_dependency_resolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()); } if (isset($settings['ajaxPageState']) || in_array('core/drupal.ajax', $loaded_libraries) || in_array('core/drupal.htmx', $loaded_libraries)) { if (!defined('MAINTENANCE_MODE')) { // The theme token is only validated when the theme requested is not the // default, so don't generate it unless necessary. Loading Loading
core/.eslintrc.json +1 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ "_": true, "Cookies": true, "Backbone": true, "htmx": true, "loadjs": true, "Shepherd": true, "Sortable": true, Loading
core/core.libraries.yml +18 −0 Original line number Diff line number Diff line Loading @@ -614,6 +614,24 @@ drupal.form: - core/drupal.debounce - core/once drupal.htmx: version: VERSION js: misc/htmx/htmx-assets.js: {} misc/htmx/htmx-behaviors.js: {} dependencies: - core/htmx - core/drupal - core/drupalSettings - core/loadjs drupalSettings: # These placeholder values will be set by system_js_settings_alter(). ajaxPageState: libraries: null theme: null theme_token: null ajaxTrustedUrl: {} drupal.machine-name: version: VERSION js: Loading
core/misc/htmx/htmx-assets.js 0 → 100644 +174 −0 Original line number Diff line number Diff line /** * @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(() => { htmx.trigger(detail.elt, 'htmx:drupal:load'); // This should be automatic but don't wait for the garbage collector. requestAssetsLoaded.delete(detail.xhr); }); }); })(Drupal, drupalSettings, loadjs, htmx);
core/misc/htmx/htmx-behaviors.js 0 → 100644 +41 −0 Original line number Diff line number Diff line /** * @file * Connect Drupal.behaviors to htmx inserted content. */ (function (Drupal, htmx, drupalSettings) { // Flag used to prevent running htmx initialization twice on elements we know // have already been processed. let attachFromHtmx = false; // This is a custom event that triggers once the htmx request settled and // all JS and CSS assets have been loaded successfully. // @see https://htmx.org/api/#on // @see htmx-assets.js htmx.on('htmx:drupal:load', ({ detail }) => { attachFromHtmx = true; Drupal.attachBehaviors(detail.elt, drupalSettings); attachFromHtmx = false; }); // When htmx removes elements from the DOM, make sure they're detached first. // This event is currently an alias of htmx:beforeSwap htmx.on('htmx:drupal:unload', ({ detail }) => { Drupal.detachBehaviors(detail.elt, drupalSettings, 'unload'); }); /** * Initialize HTMX library on content added by Drupal Ajax Framework. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Initialize htmx behavior. */ Drupal.behaviors.htmx = { attach(context) { if (!attachFromHtmx && context !== document) { htmx.process(context); } }, }; })(Drupal, htmx, drupalSettings);
core/modules/system/src/Hook/SystemHooks.php +5 −1 Original line number Diff line number Diff line Loading @@ -276,7 +276,11 @@ public function jsSettingsAlter(&$settings, AttachedAssetsInterface $assets): vo // before doing so. Also add the loaded libraries to ajaxPageState. /** @var \Drupal\Core\Asset\LibraryDependencyResolver $library_dependency_resolver */ $library_dependency_resolver = \Drupal::service('library.dependency_resolver'); if (isset($settings['ajaxPageState']) || in_array('core/drupal.ajax', $library_dependency_resolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()))) { $loaded_libraries = []; if (!isset($settings['ajaxPageState'])) { $loaded_libraries = $library_dependency_resolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()); } if (isset($settings['ajaxPageState']) || in_array('core/drupal.ajax', $loaded_libraries) || in_array('core/drupal.htmx', $loaded_libraries)) { if (!defined('MAINTENANCE_MODE')) { // The theme token is only validated when the theme requested is not the // default, so don't generate it unless necessary. Loading