diff options
Diffstat (limited to 'core/misc')
-rw-r--r-- | core/misc/cspell/dictionary.txt | 2 | ||||
-rw-r--r-- | core/misc/dialog/off-canvas/css/button.pcss.css | 2 | ||||
-rw-r--r-- | core/misc/dialog/off-canvas/css/tabledrag.pcss.css | 6 | ||||
-rw-r--r-- | core/misc/dialog/off-canvas/css/wrapper.css | 2 | ||||
-rw-r--r-- | core/misc/dialog/off-canvas/css/wrapper.pcss.css | 2 | ||||
-rw-r--r-- | core/misc/drupal.js | 5 | ||||
-rw-r--r-- | core/misc/htmx/htmx-assets.js | 174 | ||||
-rw-r--r-- | core/misc/htmx/htmx-behaviors.js | 41 | ||||
-rw-r--r-- | core/misc/tabledrag.js | 6 | ||||
-rw-r--r-- | core/misc/timezone.js | 1 |
10 files changed, 226 insertions, 15 deletions
diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt index 24999a781acb..69b32dfa7187 100644 --- a/core/misc/cspell/dictionary.txt +++ b/core/misc/cspell/dictionary.txt @@ -427,6 +427,8 @@ rowspans rtsp ruleset sameorigin +sandboxed +sandboxing savepoints sayre schemaapi diff --git a/core/misc/dialog/off-canvas/css/button.pcss.css b/core/misc/dialog/off-canvas/css/button.pcss.css index 50288fc1e01e..42a9eba3d168 100644 --- a/core/misc/dialog/off-canvas/css/button.pcss.css +++ b/core/misc/dialog/off-canvas/css/button.pcss.css @@ -104,7 +104,7 @@ } } - @nest .no-touchevents & .button--small { + .no-touchevents & .button--small { padding: 2px 1em; font-size: 13px; } diff --git a/core/misc/dialog/off-canvas/css/tabledrag.pcss.css b/core/misc/dialog/off-canvas/css/tabledrag.pcss.css index 9bce191ba33f..3fadc12cddfb 100644 --- a/core/misc/dialog/off-canvas/css/tabledrag.pcss.css +++ b/core/misc/dialog/off-canvas/css/tabledrag.pcss.css @@ -86,16 +86,16 @@ width: 5px; } - @nest .touchevents & .draggable td { + .touchevents & .draggable td { padding: 0 10px; } - @nest .touchevents & .draggable .menu-item__link { + .touchevents & .draggable .menu-item__link { display: inline-block; padding: 10px 0; } - @nest .touchevents & a.tabledrag-handle { + .touchevents & a.tabledrag-handle { width: 40px; height: 44px; } diff --git a/core/misc/dialog/off-canvas/css/wrapper.css b/core/misc/dialog/off-canvas/css/wrapper.css index 706d5a0e9d48..c5dbc71f1300 100644 --- a/core/misc/dialog/off-canvas/css/wrapper.css +++ b/core/misc/dialog/off-canvas/css/wrapper.css @@ -11,7 +11,7 @@ * @internal */ #drupal-off-canvas-wrapper { - --off-canvas-wrapper-box-shadow: 0 0 0.25rem 2px rgba(0, 0, 0, 0.3); + --off-canvas-wrapper-box-shadow: 0 0 0.25rem 2px rgb(0, 0, 0, 0.3); --off-canvas-wrapper-border-color: #2d2d2d; --off-canvas-wrapper-border-width: 1px; diff --git a/core/misc/dialog/off-canvas/css/wrapper.pcss.css b/core/misc/dialog/off-canvas/css/wrapper.pcss.css index c16a7e652df1..c16b30a7d0bb 100644 --- a/core/misc/dialog/off-canvas/css/wrapper.pcss.css +++ b/core/misc/dialog/off-canvas/css/wrapper.pcss.css @@ -5,7 +5,7 @@ * @internal */ #drupal-off-canvas-wrapper { - --off-canvas-wrapper-box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.3); + --off-canvas-wrapper-box-shadow: 0 0 4px 2px rgb(0, 0, 0, 0.3); --off-canvas-wrapper-border-color: #2d2d2d; --off-canvas-wrapper-border-width: 1px; diff --git a/core/misc/drupal.js b/core/misc/drupal.js index 416c4f415a59..641c461a802e 100644 --- a/core/misc/drupal.js +++ b/core/misc/drupal.js @@ -404,7 +404,6 @@ window.Drupal = { behaviors: {}, locale: {} }; * * @see https://github.com/angular/angular.js/blob/v1.4.4/src/ng/urlUtils.js * @see https://grack.com/blog/2009/11/17/absolutizing-url-in-javascript - * @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L53 */ Drupal.url.toAbsolute = function (url) { const urlParsingNode = document.createElement('a'); @@ -419,9 +418,7 @@ window.Drupal = { behaviors: {}, locale: {} }; urlParsingNode.setAttribute('href', url); - // IE <= 7 normalizes the URL when assigned to the anchor node similar to - // the other browsers. - return urlParsingNode.cloneNode(false).href; + return urlParsingNode.href; }; /** diff --git a/core/misc/htmx/htmx-assets.js b/core/misc/htmx/htmx-assets.js new file mode 100644 index 000000000000..bc5f00df4327 --- /dev/null +++ b/core/misc/htmx/htmx-assets.js @@ -0,0 +1,174 @@ +/** + * @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); diff --git a/core/misc/htmx/htmx-behaviors.js b/core/misc/htmx/htmx-behaviors.js new file mode 100644 index 000000000000..da3ea2eb9952 --- /dev/null +++ b/core/misc/htmx/htmx-behaviors.js @@ -0,0 +1,41 @@ +/** + * @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); diff --git a/core/misc/tabledrag.js b/core/misc/tabledrag.js index b9f32bd54138..5786ebd9431a 100644 --- a/core/misc/tabledrag.js +++ b/core/misc/tabledrag.js @@ -1064,8 +1064,7 @@ const nextRow = $nextRow.get(0); sourceRow = changedRow; if ( - previousRow && - previousRow.matches('.draggable') && + previousRow?.matches('.draggable') && $previousRow.find(`.${group}`).length ) { if (this.indentEnabled) { @@ -1079,8 +1078,7 @@ sourceRow = previousRow; } } else if ( - nextRow && - nextRow.matches('.draggable') && + nextRow?.matches('.draggable') && $nextRow.find(`.${group}`).length ) { if (this.indentEnabled) { diff --git a/core/misc/timezone.js b/core/misc/timezone.js index a8c1a5f7ac58..97f5e5d05a8c 100644 --- a/core/misc/timezone.js +++ b/core/misc/timezone.js @@ -68,7 +68,6 @@ $.ajax({ async: false, url: Drupal.url(path), - data: { date: dateString }, dataType: 'json', success(data) { if (data) { |