Skip to content
Snippets Groups Projects
Commit 83b07ce0 authored by ambient.impact's avatar ambient.impact
Browse files

Issue #3397370: Improved Turbo Improve drupalSettings updating:

Now uses a MutationObserver to determine which element was added rather
than the order of elements in the DOM. Also throws errors if more than
one or no elements were added.
parent 8760ea7e
No related branches found
No related tags found
No related merge requests found
...@@ -7,6 +7,115 @@ ...@@ -7,6 +7,115 @@
*/ */
class DrupalSettings { class DrupalSettings {
/**
* Zero more Drupal settings JSON elements found in the DOM.
*
* @type {HTMLScriptElement[]}
*/
#foundElements = [];
/**
* <script> element MutationObserver.
*
* @type {MutationObserver}
*/
#scriptObserver;
constructor() {
/**
* Reference to the current instance.
*
* @type {DrupalSettings}
*/
const that = this;
/**
* Reference to our #scriptObserverCallback private method.
*
* @type {Function}
*/
const scriptObserverCallback = this.#scriptObserverCallback;
this.#scriptObserver = new MutationObserver(function(mutations) {
scriptObserverCallback.call(that, mutations);
});
/**
* Reference to our <script> element MutationObserver.
*
* @type {MutationObserver}
*/
const scriptObserver = this.#scriptObserver;
document.documentElement.addEventListener('turbo:visit', function(event) {
// Reset the array of found script elements before starting the
// MutationObserver.
that.#foundElements = [];
// Start observing when a Turbo visit starts. This event is triggered
// before any changes are made to the DOM, including the <head>, so no
// new <script> elements will have been merged in yet.
//
// Also note that while this <script> element can be rendered by Drupal
// in the <body>, we explicitly move JavaScript to the <head> which also
// causes Drupal to render this <script> in the <head> as well, so we
// don't observe the <body>.
scriptObserver.observe(document.head, {
childList: true,
// Important: do *not* use subtree: true as that will open up
// potential XSS exploits.
//
// @see core/misc/drupalSettingsLoader.js
// Explains the need for XSS hardening.
});
});
};
/**
* <script> element MutationObserver callback.
*
* @param {MutationRecord} mutations
*/
#scriptObserverCallback(mutations) {
for (let i = 0; i < mutations.length; i++) {
if (!('addedNodes' in mutations[i])) {
continue;
}
/**
* Nodes added as part of this mutation record.
*
* @type {NodeList}
*/
const addedNodes = mutations[i].addedNodes;
for (let j = 0; j < addedNodes.length; j++) {
// Ignore any non-<script> element nodes or <script> elements not
// matching the selector for the drupalSettings JSON element.
if (
!(addedNodes[j] instanceof HTMLScriptElement) ||
addedNodes[j].getAttribute(
'data-drupal-selector',
) !== 'drupal-settings-json'
) {
continue;
}
this.#foundElements.push(addedNodes[j]);
}
}
};
/** /**
* Find the Drupal settings JSON element(s). * Find the Drupal settings JSON element(s).
* *
...@@ -39,13 +148,36 @@ ...@@ -39,13 +148,36 @@
* merging? * merging?
* *
* @throws Error * @throws Error
* If the settings element text could not be parsed into JSON. * If no new settings elements were added, if more than one element was
* added, or if the new settings element text could not be parsed into
* valid JSON.
* *
* @return {Promise} * @return {Promise}
* A Promise that fulfills when drupalSettings has been updated. * A Promise that fulfills when drupalSettings has been updated.
*/ */
update() { update() {
// Stop observing for new <script> elements at this point because our job
// is done and we don't want to do unnecessary work, potentially
// affecting performance.
this.#scriptObserver.disconnect();
if (this.#foundElements.length === 0) {
console.error(Drupal.t(
'Could not find a Drupal settings script element added during the visit!',
));
}
if (this.#foundElements.length > 1) {
console.error(Drupal.t(
'More than one Drupal settings script element seems to have been added!',
), this.#foundElements);
}
/** /**
* One or more settings elements. * One or more settings elements.
* *
...@@ -53,10 +185,10 @@ ...@@ -53,10 +185,10 @@
*/ */
const $elements = this.#findElements(); const $elements = this.#findElements();
// Remove all but the last settings element. // Remove all but the newest settings element.
$elements.not($elements.last()).remove(); $elements.not(this.#foundElements[0]).remove();
const $element = $elements.last(); const $element = $(this.#foundElements[0]);
/** /**
* New drupalSettings values or null if they can't be read. * New drupalSettings values or null if they can't be read.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment