Loading core/modules/big_pipe/big_pipe.libraries.yml +0 −1 Original line number Diff line number Diff line Loading @@ -5,6 +5,5 @@ big_pipe: drupalSettings: bigPipePlaceholderIds: [] dependencies: - core/once - core/drupal.ajax - core/drupalSettings core/modules/big_pipe/js/big_pipe.js +89 −88 Original line number Diff line number Diff line Loading @@ -3,15 +3,39 @@ * Renders BigPipe placeholders using Drupal's Ajax system. */ (function (Drupal, drupalSettings) { ((Drupal, drupalSettings) => { /** * Maps textContent of <script type="application/vnd.drupal-ajax"> to an AJAX response. * CSS selector for script elements to process on page load. * * @type {string} */ const replacementsSelector = `script[data-big-pipe-replacement-for-placeholder-with-id]`; /** * Ajax object that will process all the BigPipe responses. * * Create a Drupal.Ajax object without associating an element, a progress * indicator or a URL. * * @type {Drupal.Ajax} */ const ajaxObject = Drupal.ajax({ url: '', base: false, element: false, progress: false, }); /** * Maps textContent of <script type="application/vnd.drupal-ajax"> to an AJAX * response. * * @param {string} content * The text content of a <script type="application/vnd.drupal-ajax"> DOM node. * The text content of a <script type="application/vnd.drupal-ajax"> DOM * node. * @return {Array|boolean} * The parsed Ajax response containing an array of Ajax commands, or false in * case the DOM node hasn't fully arrived yet. * The parsed Ajax response containing an array of Ajax commands, or false * in case the DOM node hasn't fully arrived yet. */ function mapTextContentToAjaxResponse(content) { if (content === '') { Loading @@ -30,108 +54,85 @@ * * These Ajax commands replace placeholders with HTML and load missing CSS/JS. * * @param {HTMLScriptElement} placeholderReplacement * @param {HTMLScriptElement} replacement * Script tag created by BigPipe. */ function bigPipeProcessPlaceholderReplacement(placeholderReplacement) { const placeholderId = placeholderReplacement.getAttribute( 'data-big-pipe-replacement-for-placeholder-with-id', ); const content = placeholderReplacement.textContent.trim(); function processReplacement(replacement) { const id = replacement.dataset.bigPipeReplacementForPlaceholderWithId; // Because we use a mutation observer the content is guaranteed to be // complete at this point. const content = replacement.textContent.trim(); // Ignore any placeholders that are not in the known placeholder list. Used // to avoid someone trying to XSS the site via the placeholdering mechanism. if ( typeof drupalSettings.bigPipePlaceholderIds[placeholderId] !== 'undefined' ) { if (typeof drupalSettings.bigPipePlaceholderIds[id] === 'undefined') { return; } // Immediately remove the replacement to prevent it being processed twice. delete drupalSettings.bigPipePlaceholderIds[id]; const response = mapTextContentToAjaxResponse(content); // If we try to parse the content too early (when the JSON containing Ajax // commands is still arriving), textContent will be empty or incomplete. if (response === false) { /** * Mark as unprocessed so this will be retried later. * @see bigPipeProcessDocument() */ once.remove('big-pipe', placeholderReplacement); } else { // Create a Drupal.Ajax object without associating an element, a // progress indicator or a URL. const ajaxObject = Drupal.ajax({ url: '', base: false, element: false, progress: false, }); // Then, simulate an AJAX response having arrived, and let the Ajax // system handle it. ajaxObject.success(response, 'success'); } } return; } // The frequency with which to check for newly arrived BigPipe placeholders. // Hence 50 ms means we check 20 times per second. Setting this to 100 ms or // more would cause the user to see content appear noticeably slower. const interval = drupalSettings.bigPipeInterval || 50; // The internal ID to contain the watcher service. let timeoutID; // Then, simulate an AJAX response having arrived, and let the Ajax system // handle it. ajaxObject.success(response, 'success'); } /** * Processes a streamed HTML document receiving placeholder replacements. * Check that the element is valid to process and process it. * * @param {HTMLDocument} context * The HTML document containing <script type="application/vnd.drupal-ajax"> * tags generated by BigPipe. * * @return {bool} * Returns true when processing has been finished and a stop signal has been * found. * @param {HTMLElement} node * The node added to the body element. */ function bigPipeProcessDocument(context) { // Make sure we have BigPipe-related scripts before processing further. if (!context.querySelector('script[data-big-pipe-event="start"]')) { return false; function checkMutationAndProcess(node) { if ( node.nodeType === Node.ELEMENT_NODE && node.nodeName === 'SCRIPT' && node.dataset && node.dataset.bigPipeReplacementForPlaceholderWithId ) { processReplacement(node); } } // Attach Drupal behaviors early, if possible. once('big-pipe-early-behaviors', 'body', context).forEach((el) => { Drupal.attachBehaviors(el); /** * Handles the mutation callback. * * @param {MutationRecord[]} mutations * The list of mutations registered by the browser. */ function processMutations(mutations) { mutations.forEach(({ addedNodes }) => { addedNodes.forEach(checkMutationAndProcess); }); once( 'big-pipe', 'script[data-big-pipe-replacement-for-placeholder-with-id]', context, ).forEach(bigPipeProcessPlaceholderReplacement); // If we see the stop signal, clear the timeout: all placeholder // replacements are guaranteed to be received and processed. if (context.querySelector('script[data-big-pipe-event="stop"]')) { if (timeoutID) { clearTimeout(timeoutID); } return true; } return false; } const observer = new MutationObserver(processMutations); function bigPipeProcess() { timeoutID = setTimeout(() => { if (!bigPipeProcessDocument(document)) { bigPipeProcess(); } }, interval); } // Attach behaviors early, if possible. Drupal.attachBehaviors(document.body); // If loaded asynchronously there might already be replacement elements // in the DOM before the mutation observer is started. document.querySelectorAll(replacementsSelector).forEach(processReplacement); bigPipeProcess(); // Start observing the body element for new children. observer.observe(document.body, { childList: true }); // If something goes wrong, make sure everything is cleaned up and has had a // chance to be processed with everything loaded. window.addEventListener('load', () => { if (timeoutID) { clearTimeout(timeoutID); // As soon as the document is loaded, no more replacements will be added. // Immediately fetch and process all pending mutations and stop the observer. window.addEventListener('DOMContentLoaded', () => { const mutations = observer.takeRecords(); observer.disconnect(); if (mutations.length) { processMutations(mutations); } bigPipeProcessDocument(document); // No more mutations will be processed, remove the leftover Ajax object. Drupal.ajax.instances[ajaxObject.instanceIndex] = null; }); })(Drupal, drupalSettings); core/modules/ckeditor5/js/ckeditor5.admin.js +60 −58 Original line number Diff line number Diff line Loading @@ -530,8 +530,11 @@ */ Drupal.behaviors.ckeditor5Admin = { attach(context) { once('ckeditor5-admin-toolbar', '#ckeditor5-toolbar-app').forEach( (container) => { once( 'ckeditor5-admin-toolbar', '#ckeditor5-toolbar-app', context, ).forEach((container) => { const selectedTextarea = context.querySelector( '#ckeditor5-toolbar-buttons-selected', ); Loading Loading @@ -591,8 +594,7 @@ }); render(container, selected, available, dividers); }, ); }); // Safari's focus outlines take into account absolute positioned elements. // When a toolbar option is blurred, the portion of the focus outline // surrounding the absolutely positioned tooltip does not go away. To Loading core/profiles/standard/tests/src/FunctionalJavascript/StandardJavascriptTest.php +2 −1 Original line number Diff line number Diff line Loading @@ -54,7 +54,8 @@ protected function assertBigPipePlaceholderReplacementCount($expected_count): vo $web_assert = $this->assertSession(); $web_assert->waitForElement('css', 'script[data-big-pipe-event="stop"]'); $page = $this->getSession()->getPage(); $this->assertCount($expected_count, $this->getDrupalSettings()['bigPipePlaceholderIds']); // Settings are removed as soon as they are processed. $this->assertCount(0, $this->getDrupalSettings()['bigPipePlaceholderIds']); $this->assertCount($expected_count, $page->findAll('css', 'script[data-big-pipe-replacement-for-placeholder-with-id]')); } Loading Loading
core/modules/big_pipe/big_pipe.libraries.yml +0 −1 Original line number Diff line number Diff line Loading @@ -5,6 +5,5 @@ big_pipe: drupalSettings: bigPipePlaceholderIds: [] dependencies: - core/once - core/drupal.ajax - core/drupalSettings
core/modules/big_pipe/js/big_pipe.js +89 −88 Original line number Diff line number Diff line Loading @@ -3,15 +3,39 @@ * Renders BigPipe placeholders using Drupal's Ajax system. */ (function (Drupal, drupalSettings) { ((Drupal, drupalSettings) => { /** * Maps textContent of <script type="application/vnd.drupal-ajax"> to an AJAX response. * CSS selector for script elements to process on page load. * * @type {string} */ const replacementsSelector = `script[data-big-pipe-replacement-for-placeholder-with-id]`; /** * Ajax object that will process all the BigPipe responses. * * Create a Drupal.Ajax object without associating an element, a progress * indicator or a URL. * * @type {Drupal.Ajax} */ const ajaxObject = Drupal.ajax({ url: '', base: false, element: false, progress: false, }); /** * Maps textContent of <script type="application/vnd.drupal-ajax"> to an AJAX * response. * * @param {string} content * The text content of a <script type="application/vnd.drupal-ajax"> DOM node. * The text content of a <script type="application/vnd.drupal-ajax"> DOM * node. * @return {Array|boolean} * The parsed Ajax response containing an array of Ajax commands, or false in * case the DOM node hasn't fully arrived yet. * The parsed Ajax response containing an array of Ajax commands, or false * in case the DOM node hasn't fully arrived yet. */ function mapTextContentToAjaxResponse(content) { if (content === '') { Loading @@ -30,108 +54,85 @@ * * These Ajax commands replace placeholders with HTML and load missing CSS/JS. * * @param {HTMLScriptElement} placeholderReplacement * @param {HTMLScriptElement} replacement * Script tag created by BigPipe. */ function bigPipeProcessPlaceholderReplacement(placeholderReplacement) { const placeholderId = placeholderReplacement.getAttribute( 'data-big-pipe-replacement-for-placeholder-with-id', ); const content = placeholderReplacement.textContent.trim(); function processReplacement(replacement) { const id = replacement.dataset.bigPipeReplacementForPlaceholderWithId; // Because we use a mutation observer the content is guaranteed to be // complete at this point. const content = replacement.textContent.trim(); // Ignore any placeholders that are not in the known placeholder list. Used // to avoid someone trying to XSS the site via the placeholdering mechanism. if ( typeof drupalSettings.bigPipePlaceholderIds[placeholderId] !== 'undefined' ) { if (typeof drupalSettings.bigPipePlaceholderIds[id] === 'undefined') { return; } // Immediately remove the replacement to prevent it being processed twice. delete drupalSettings.bigPipePlaceholderIds[id]; const response = mapTextContentToAjaxResponse(content); // If we try to parse the content too early (when the JSON containing Ajax // commands is still arriving), textContent will be empty or incomplete. if (response === false) { /** * Mark as unprocessed so this will be retried later. * @see bigPipeProcessDocument() */ once.remove('big-pipe', placeholderReplacement); } else { // Create a Drupal.Ajax object without associating an element, a // progress indicator or a URL. const ajaxObject = Drupal.ajax({ url: '', base: false, element: false, progress: false, }); // Then, simulate an AJAX response having arrived, and let the Ajax // system handle it. ajaxObject.success(response, 'success'); } } return; } // The frequency with which to check for newly arrived BigPipe placeholders. // Hence 50 ms means we check 20 times per second. Setting this to 100 ms or // more would cause the user to see content appear noticeably slower. const interval = drupalSettings.bigPipeInterval || 50; // The internal ID to contain the watcher service. let timeoutID; // Then, simulate an AJAX response having arrived, and let the Ajax system // handle it. ajaxObject.success(response, 'success'); } /** * Processes a streamed HTML document receiving placeholder replacements. * Check that the element is valid to process and process it. * * @param {HTMLDocument} context * The HTML document containing <script type="application/vnd.drupal-ajax"> * tags generated by BigPipe. * * @return {bool} * Returns true when processing has been finished and a stop signal has been * found. * @param {HTMLElement} node * The node added to the body element. */ function bigPipeProcessDocument(context) { // Make sure we have BigPipe-related scripts before processing further. if (!context.querySelector('script[data-big-pipe-event="start"]')) { return false; function checkMutationAndProcess(node) { if ( node.nodeType === Node.ELEMENT_NODE && node.nodeName === 'SCRIPT' && node.dataset && node.dataset.bigPipeReplacementForPlaceholderWithId ) { processReplacement(node); } } // Attach Drupal behaviors early, if possible. once('big-pipe-early-behaviors', 'body', context).forEach((el) => { Drupal.attachBehaviors(el); /** * Handles the mutation callback. * * @param {MutationRecord[]} mutations * The list of mutations registered by the browser. */ function processMutations(mutations) { mutations.forEach(({ addedNodes }) => { addedNodes.forEach(checkMutationAndProcess); }); once( 'big-pipe', 'script[data-big-pipe-replacement-for-placeholder-with-id]', context, ).forEach(bigPipeProcessPlaceholderReplacement); // If we see the stop signal, clear the timeout: all placeholder // replacements are guaranteed to be received and processed. if (context.querySelector('script[data-big-pipe-event="stop"]')) { if (timeoutID) { clearTimeout(timeoutID); } return true; } return false; } const observer = new MutationObserver(processMutations); function bigPipeProcess() { timeoutID = setTimeout(() => { if (!bigPipeProcessDocument(document)) { bigPipeProcess(); } }, interval); } // Attach behaviors early, if possible. Drupal.attachBehaviors(document.body); // If loaded asynchronously there might already be replacement elements // in the DOM before the mutation observer is started. document.querySelectorAll(replacementsSelector).forEach(processReplacement); bigPipeProcess(); // Start observing the body element for new children. observer.observe(document.body, { childList: true }); // If something goes wrong, make sure everything is cleaned up and has had a // chance to be processed with everything loaded. window.addEventListener('load', () => { if (timeoutID) { clearTimeout(timeoutID); // As soon as the document is loaded, no more replacements will be added. // Immediately fetch and process all pending mutations and stop the observer. window.addEventListener('DOMContentLoaded', () => { const mutations = observer.takeRecords(); observer.disconnect(); if (mutations.length) { processMutations(mutations); } bigPipeProcessDocument(document); // No more mutations will be processed, remove the leftover Ajax object. Drupal.ajax.instances[ajaxObject.instanceIndex] = null; }); })(Drupal, drupalSettings);
core/modules/ckeditor5/js/ckeditor5.admin.js +60 −58 Original line number Diff line number Diff line Loading @@ -530,8 +530,11 @@ */ Drupal.behaviors.ckeditor5Admin = { attach(context) { once('ckeditor5-admin-toolbar', '#ckeditor5-toolbar-app').forEach( (container) => { once( 'ckeditor5-admin-toolbar', '#ckeditor5-toolbar-app', context, ).forEach((container) => { const selectedTextarea = context.querySelector( '#ckeditor5-toolbar-buttons-selected', ); Loading Loading @@ -591,8 +594,7 @@ }); render(container, selected, available, dividers); }, ); }); // Safari's focus outlines take into account absolute positioned elements. // When a toolbar option is blurred, the portion of the focus outline // surrounding the absolutely positioned tooltip does not go away. To Loading
core/profiles/standard/tests/src/FunctionalJavascript/StandardJavascriptTest.php +2 −1 Original line number Diff line number Diff line Loading @@ -54,7 +54,8 @@ protected function assertBigPipePlaceholderReplacementCount($expected_count): vo $web_assert = $this->assertSession(); $web_assert->waitForElement('css', 'script[data-big-pipe-event="stop"]'); $page = $this->getSession()->getPage(); $this->assertCount($expected_count, $this->getDrupalSettings()['bigPipePlaceholderIds']); // Settings are removed as soon as they are processed. $this->assertCount(0, $this->getDrupalSettings()['bigPipePlaceholderIds']); $this->assertCount($expected_count, $page->findAll('css', 'script[data-big-pipe-replacement-for-placeholder-with-id]')); } Loading