summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorbnjmnm <benm@umich.edu>2022-08-03 19:11:14 -0400
committerbnjmnm <benm@umich.edu>2022-08-03 19:11:14 -0400
commitf976d5d4be91c9fe8e874ffdc91e1194ea7f6f0c (patch)
tree166ff58d867caa552cd00ec295c8b7a537053ef9
parent02defec9a54e2f42d5c4e75466e6dde3f9b8217a (diff)
downloaddrupal-f976d5d4be91c9fe8e874ffdc91e1194ea7f6f0c.tar.gz
drupal-f976d5d4be91c9fe8e874ffdc91e1194ea7f6f0c.zip
Issue #1988968 by nod_, droplet, bnjmnm, viappidu, extect, jansete, olli, martin107, pmagunia, bartlangelaan, Wim Leers, zrpnr, KapilV, yogeshmpawar, Spleshka, Phil Wolstenholme, DuaelFr, agata.guc, alwaysworking, dawid_nawrot, andriic, keithdoyle9, gapple, lauriii, Martijn de Wit, jefuri, JMOmandown, larowlan, rubens.arjr, borisson_, joseph.olstad, jberube, gilgabar, Poindexterous, Aless86, jessebeach, bojanz, phma, aheimlich, heddn, phenaproxima, acbramley, codebymikey, cmlara, das-peter, matthiasm11, acolden, xjm, jrockowitz, pianomansam, clairemistry, John Pitcairn: Drupal.ajax does not guarantee that "add new JS file to page" commands have finished before calling said JS
-rw-r--r--core/core.libraries.yml1
-rw-r--r--core/lib/Drupal/Core/Ajax/AddJsCommand.php61
-rw-r--r--core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php4
-rw-r--r--core/misc/ajax.es6.js228
-rw-r--r--core/misc/ajax.js106
-rw-r--r--core/modules/ckeditor/tests/src/FunctionalJavascript/BigPipeRegressionTest.php2
-rw-r--r--core/modules/media_library/js/media_library.ui.es6.js42
-rw-r--r--core/modules/media_library/js/media_library.ui.js28
-rw-r--r--core/modules/quickedit/js/quickedit.es6.js9
-rw-r--r--core/modules/quickedit/js/quickedit.js10
-rw-r--r--core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml9
-rw-r--r--core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml8
-rw-r--r--core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.es6.js31
-rw-r--r--core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.js17
-rw-r--r--core/modules/system/tests/modules/ajax_test/src/Ajax/AjaxTestCommandReturnPromise.php26
-rw-r--r--core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestFormPromise.php73
-rw-r--r--core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php7
-rw-r--r--core/tests/Drupal/Nightwatch/Tests/ajaxExecutionOrderTest.js34
18 files changed, 545 insertions, 151 deletions
diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index b26bf5fff627..d7a0461893f9 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -370,6 +370,7 @@ drupal.ajax:
- core/drupal.progress
- core/once
- core/tabbable
+ - core/loadjs
drupal.announce:
version: VERSION
diff --git a/core/lib/Drupal/Core/Ajax/AddJsCommand.php b/core/lib/Drupal/Core/Ajax/AddJsCommand.php
new file mode 100644
index 000000000000..5d6f623c4f94
--- /dev/null
+++ b/core/lib/Drupal/Core/Ajax/AddJsCommand.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\Core\Ajax;
+
+/**
+ * An AJAX command for adding JS to the page via AJAX.
+ *
+ * This command will make sure all the files are loaded before continuing
+ * executing the next AJAX command. This command is implemented by
+ * Drupal.AjaxCommands.prototype.add_js() defined in misc/ajax.js.
+ *
+ * @see misc/ajax.js
+ *
+ * @ingroup ajax
+ */
+class AddJsCommand implements CommandInterface {
+
+ /**
+ * An array containing attributes of the scripts to be added to the page.
+ *
+ * @var string[]
+ */
+ protected $scripts;
+
+ /**
+ * A CSS selector string.
+ *
+ * If the command is a response to a request from an #ajax form element then
+ * this value will default to 'body'.
+ *
+ * @var string
+ */
+ protected $selector;
+
+ /**
+ * Constructs an AddJsCommand.
+ *
+ * @param array $scripts
+ * An array containing the attributes of the 'script' tags to be added to
+ * the page. i.e. `['src' => 'someURL', 'defer' => TRUE]` becomes
+ * `<script src="someURL" defer>`.
+ * @param string $selector
+ * A CSS selector of the element where the script tags will be appended.
+ */
+ public function __construct(array $scripts, string $selector = 'body') {
+ $this->scripts = $scripts;
+ $this->selector = $selector;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render() {
+ return [
+ 'command' => 'add_js',
+ 'selector' => $this->selector,
+ 'data' => $this->scripts,
+ ];
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
index 4f6acbf9664f..eaeed18ef2ca 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
@@ -174,11 +174,11 @@ class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
}
if ($js_assets_header) {
$js_header_render_array = $this->jsCollectionRenderer->render($js_assets_header);
- $resource_commands[] = new PrependCommand('head', $this->renderer->renderPlain($js_header_render_array));
+ $resource_commands[] = new AddJsCommand(array_column($js_header_render_array, '#attributes'), 'head');
}
if ($js_assets_footer) {
$js_footer_render_array = $this->jsCollectionRenderer->render($js_assets_footer);
- $resource_commands[] = new AppendCommand('body', $this->renderer->renderPlain($js_footer_render_array));
+ $resource_commands[] = new AddJsCommand(array_column($js_footer_render_array, '#attributes'));
}
foreach (array_reverse($resource_commands) as $resource_command) {
$response->addCommand($resource_command, TRUE);
diff --git a/core/misc/ajax.es6.js b/core/misc/ajax.es6.js
index 08923ae52d04..c8c8eb1cd39e 100644
--- a/core/misc/ajax.es6.js
+++ b/core/misc/ajax.es6.js
@@ -11,7 +11,14 @@
* included to provide Ajax capabilities.
*/
-(function ($, window, Drupal, drupalSettings, { isFocusable, tabbable }) {
+(function (
+ $,
+ window,
+ Drupal,
+ drupalSettings,
+ loadjs,
+ { isFocusable, tabbable },
+) {
/**
* Attaches the Ajax behavior to each Ajax form element.
*
@@ -537,10 +544,23 @@
}
}
- return ajax.success(response, status);
+ return (
+ // Ensure that the return of the success callback is a Promise.
+ // When the return is a Promise, using resolve will unwrap it, and
+ // when the return is not a Promise we make sure it can be used as
+ // one. This is useful for code that overrides the success method.
+ Promise.resolve(ajax.success(response, status))
+ // Ajaxing status is back to false when all the AJAX commands have
+ // finished executing.
+ .then(() => {
+ ajax.ajaxing = false;
+ })
+ );
},
- complete(xmlhttprequest, status) {
+ error(xmlhttprequest, status, error) {
ajax.ajaxing = false;
+ },
+ complete(xmlhttprequest, status) {
if (status === 'error' || status === 'parsererror') {
return ajax.error(xmlhttprequest, ajax.url);
}
@@ -951,12 +971,45 @@
};
/**
+ * Helper method to make sure commands are executed in sequence.
+ *
+ * @param {Array.<Drupal.AjaxCommands~commandDefinition>} response
+ * Drupal Ajax response.
+ * @param {number} status
+ * XMLHttpRequest status.
+ *
+ * @return {Promise}
+ * The promise that will resolve once all commands have finished executing.
+ */
+ Drupal.Ajax.prototype.commandExecutionQueue = function (response, status) {
+ const ajaxCommands = this.commands;
+ return Object.keys(response || {}).reduce(
+ // Add all commands to a single execution queue.
+ (executionQueue, key) =>
+ executionQueue.then(() => {
+ const { command } = response[key];
+ if (command && ajaxCommands[command]) {
+ // When a command returns a promise, the remaining commands will not
+ // execute until that promise has been fulfilled. This is typically
+ // used to ensure JavaScript files added via the 'add_js' command
+ // have loaded before subsequent commands execute.
+ return ajaxCommands[command](this, response[key], status);
+ }
+ }),
+ Promise.resolve(),
+ );
+ };
+
+ /**
* Handler for the form redirection completion.
*
* @param {Array.<Drupal.AjaxCommands~commandDefinition>} response
* Drupal Ajax response.
* @param {number} status
* XMLHttpRequest status.
+ *
+ * @return {Promise}
+ * The promise that will resolve once all commands have finished executing.
*/
Drupal.Ajax.prototype.success = function (response, status) {
// Remove the progress element.
@@ -979,55 +1032,61 @@
// Track if any command is altering the focus so we can avoid changing the
// focus set by the Ajax command.
- let focusChanged = false;
- Object.keys(response || {}).forEach((i) => {
- if (response[i].command && this.commands[response[i].command]) {
- this.commands[response[i].command](this, response[i], status);
- if (
- (response[i].command === 'invoke' &&
- response[i].method === 'focus') ||
- response[i].command === 'focusFirst'
- ) {
- focusChanged = true;
- }
- }
+ const focusChanged = Object.keys(response || {}).some((key) => {
+ const { command, method } = response[key];
+ return (
+ command === 'focusFirst' || (command === 'invoke' && method === 'focus')
+ );
});
- // If the focus hasn't be changed by the ajax commands, try to refocus the
- // triggering element or one of its parents if that element does not exist
- // anymore.
- if (
- !focusChanged &&
- this.element &&
- !$(this.element).data('disable-refocus')
- ) {
- let target = false;
-
- for (let n = elementParents.length - 1; !target && n >= 0; n--) {
- target = document.querySelector(
- `[data-drupal-selector="${elementParents[n].getAttribute(
- 'data-drupal-selector',
- )}"]`,
- );
- }
-
- if (target) {
- $(target).trigger('focus');
- }
- }
-
- // Reattach behaviors, if they were detached in beforeSerialize(). The
- // attachBehaviors() called on the new content from processing the response
- // commands is not sufficient, because behaviors from the entire form need
- // to be reattached.
- if (this.$form && document.body.contains(this.$form.get(0))) {
- const settings = this.settings || drupalSettings;
- Drupal.attachBehaviors(this.$form.get(0), settings);
- }
-
- // Remove any response-specific settings so they don't get used on the next
- // call by mistake.
- this.settings = null;
+ return (
+ this.commandExecutionQueue(response, status)
+ // If the focus hasn't been changed by the AJAX commands, try to refocus
+ // the triggering element or one of its parents if that element does not
+ // exist anymore.
+ .then(() => {
+ if (
+ !focusChanged &&
+ this.element &&
+ !$(this.element).data('disable-refocus')
+ ) {
+ let target = false;
+
+ for (let n = elementParents.length - 1; !target && n >= 0; n--) {
+ target = document.querySelector(
+ `[data-drupal-selector="${elementParents[n].getAttribute(
+ 'data-drupal-selector',
+ )}"]`,
+ );
+ }
+ if (target) {
+ $(target).trigger('focus');
+ }
+ }
+ // Reattach behaviors, if they were detached in beforeSerialize(). The
+ // attachBehaviors() called on the new content from processing the
+ // response commands is not sufficient, because behaviors from the
+ // entire form need to be reattached.
+ if (this.$form && document.body.contains(this.$form.get(0))) {
+ const settings = this.settings || drupalSettings;
+ Drupal.attachBehaviors(this.$form.get(0), settings);
+ }
+ // Remove any response-specific settings so they don't get used on the
+ // next call by mistake.
+ this.settings = null;
+ })
+ .catch((error) =>
+ // eslint-disable-next-line no-console
+ console.error(
+ Drupal.t(
+ 'An error occurred during the execution of the Ajax response: !error',
+ {
+ '!error': error,
+ },
+ ),
+ ),
+ )
+ );
};
/**
@@ -1610,5 +1669,72 @@
}
messages.add(response.message, response.messageOptions);
},
+
+ /**
+ * Command to add JS.
+ *
+ * @param {Drupal.Ajax} [ajax]
+ * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
+ * @param {object} response
+ * The response from the Ajax request.
+ * @param {Array} response.data
+ * An array of objects of script attributes.
+ * @param {number} [status]
+ * The XMLHttpRequest status.
+ */
+ add_js(ajax, response, status) {
+ const parentEl = document.querySelector(response.selector || 'body');
+ const settings = ajax.settings || drupalSettings;
+ const allUniqueBundleIds = response.data.map((script) => {
+ // loadjs requires a unique ID, and an AJAX instance's `instanceIndex`
+ // is guaranteed to be unique.
+ // @see Drupal.behaviors.AJAX.detach
+ const uniqueBundleId = script.src + ajax.instanceIndex;
+ loadjs(script.src, uniqueBundleId, {
+ // The default loadjs behavior is to load script with async, in Drupal
+ // we need to explicitly tell scripts to load async, this is set in
+ // the before callback below if necessary.
+ async: false,
+ before(path, scriptEl) {
+ // This allows all attributes to be added, like defer, async and
+ // crossorigin.
+ Object.keys(script).forEach((attributeKey) => {
+ scriptEl.setAttribute(attributeKey, script[attributeKey]);
+ });
+
+ // By default, loadjs appends the script to the head. When scripts
+ // are loaded via AJAX, their location has no impact on
+ // functionality. But, since non-AJAX loaded scripts can choose
+ // their parent element, we provide that option here for the sake of
+ // consistency.
+ parentEl.appendChild(scriptEl);
+ // Return false to bypass loadjs' default DOM insertion mechanism.
+ return false;
+ },
+ });
+ return uniqueBundleId;
+ });
+ // Returns the promise so that the next AJAX command waits on the
+ // completion of this one to execute, ensuring the JS is loaded before
+ // executing.
+ return new Promise((resolve, reject) => {
+ loadjs.ready(allUniqueBundleIds, {
+ success() {
+ Drupal.attachBehaviors(parentEl, settings);
+ // All JS files were loaded and new and old behaviors have
+ // been attached. Resolve the promise and let the remaining
+ // commands execute.
+ resolve();
+ },
+ error(depsNotFound) {
+ const message = Drupal.t(
+ `The following files could not be loaded: @dependencies`,
+ { '@dependencies': depsNotFound.join(', ') },
+ );
+ reject(message);
+ },
+ });
+ });
+ },
};
-})(jQuery, window, Drupal, drupalSettings, window.tabbable);
+})(jQuery, window, Drupal, drupalSettings, loadjs, window.tabbable);
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index b2f572c9d592..75c97f62abe8 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -5,7 +5,7 @@
* @preserve
**/
-(function ($, window, Drupal, drupalSettings, _ref) {
+(function ($, window, Drupal, drupalSettings, loadjs, _ref) {
let {
isFocusable,
tabbable
@@ -229,12 +229,16 @@
}
}
- return ajax.success(response, status);
+ return Promise.resolve(ajax.success(response, status)).then(() => {
+ ajax.ajaxing = false;
+ });
},
- complete(xmlhttprequest, status) {
+ error(xmlhttprequest, status, error) {
ajax.ajaxing = false;
+ },
+ complete(xmlhttprequest, status) {
if (status === 'error' || status === 'parsererror') {
return ajax.error(xmlhttprequest, ajax.url);
}
@@ -414,6 +418,19 @@
$('body').append(this.progress.element);
};
+ Drupal.Ajax.prototype.commandExecutionQueue = function (response, status) {
+ const ajaxCommands = this.commands;
+ return Object.keys(response || {}).reduce((executionQueue, key) => executionQueue.then(() => {
+ const {
+ command
+ } = response[key];
+
+ if (command && ajaxCommands[command]) {
+ return ajaxCommands[command](this, response[key], status);
+ }
+ }), Promise.resolve());
+ };
+
Drupal.Ajax.prototype.success = function (response, status) {
if (this.progress.element) {
$(this.progress.element).remove();
@@ -425,35 +442,35 @@
$(this.element).prop('disabled', false);
const elementParents = $(this.element).parents('[data-drupal-selector]').addBack().toArray();
- let focusChanged = false;
- Object.keys(response || {}).forEach(i => {
- if (response[i].command && this.commands[response[i].command]) {
- this.commands[response[i].command](this, response[i], status);
-
- if (response[i].command === 'invoke' && response[i].method === 'focus' || response[i].command === 'focusFirst') {
- focusChanged = true;
- }
- }
+ const focusChanged = Object.keys(response || {}).some(key => {
+ const {
+ command,
+ method
+ } = response[key];
+ return command === 'focusFirst' || command === 'invoke' && method === 'focus';
});
+ return this.commandExecutionQueue(response, status).then(() => {
+ if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) {
+ let target = false;
- if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) {
- let target = false;
+ for (let n = elementParents.length - 1; !target && n >= 0; n--) {
+ target = document.querySelector(`[data-drupal-selector="${elementParents[n].getAttribute('data-drupal-selector')}"]`);
+ }
- for (let n = elementParents.length - 1; !target && n >= 0; n--) {
- target = document.querySelector(`[data-drupal-selector="${elementParents[n].getAttribute('data-drupal-selector')}"]`);
+ if (target) {
+ $(target).trigger('focus');
+ }
}
- if (target) {
- $(target).trigger('focus');
+ if (this.$form && document.body.contains(this.$form.get(0))) {
+ const settings = this.settings || drupalSettings;
+ Drupal.attachBehaviors(this.$form.get(0), settings);
}
- }
- if (this.$form && document.body.contains(this.$form.get(0))) {
- const settings = this.settings || drupalSettings;
- Drupal.attachBehaviors(this.$form.get(0), settings);
- }
-
- this.settings = null;
+ this.settings = null;
+ }).catch(error => console.error(Drupal.t('An error occurred during the execution of the Ajax response: !error', {
+ '!error': error
+ })));
};
Drupal.Ajax.prototype.getEffect = function (response) {
@@ -664,7 +681,44 @@
}
messages.add(response.message, response.messageOptions);
+ },
+
+ add_js(ajax, response, status) {
+ const parentEl = document.querySelector(response.selector || 'body');
+ const settings = ajax.settings || drupalSettings;
+ const allUniqueBundleIds = response.data.map(script => {
+ const uniqueBundleId = script.src + ajax.instanceIndex;
+ loadjs(script.src, uniqueBundleId, {
+ async: false,
+
+ before(path, scriptEl) {
+ Object.keys(script).forEach(attributeKey => {
+ scriptEl.setAttribute(attributeKey, script[attributeKey]);
+ });
+ parentEl.appendChild(scriptEl);
+ return false;
+ }
+
+ });
+ return uniqueBundleId;
+ });
+ return new Promise((resolve, reject) => {
+ loadjs.ready(allUniqueBundleIds, {
+ success() {
+ Drupal.attachBehaviors(parentEl, settings);
+ resolve();
+ },
+
+ error(depsNotFound) {
+ const message = Drupal.t(`The following files could not be loaded: @dependencies`, {
+ '@dependencies': depsNotFound.join(', ')
+ });
+ reject(message);
+ }
+
+ });
+ });
}
};
-})(jQuery, window, Drupal, drupalSettings, window.tabbable); \ No newline at end of file
+})(jQuery, window, Drupal, drupalSettings, loadjs, window.tabbable); \ No newline at end of file
diff --git a/core/modules/ckeditor/tests/src/FunctionalJavascript/BigPipeRegressionTest.php b/core/modules/ckeditor/tests/src/FunctionalJavascript/BigPipeRegressionTest.php
index da99c5c2ab0c..188aef4ff485 100644
--- a/core/modules/ckeditor/tests/src/FunctionalJavascript/BigPipeRegressionTest.php
+++ b/core/modules/ckeditor/tests/src/FunctionalJavascript/BigPipeRegressionTest.php
@@ -110,7 +110,7 @@ class BigPipeRegressionTest extends WebDriverTestBase {
// Confirm that CKEditor loaded.
$javascript = <<<JS
(function(){
- return Object.keys(CKEDITOR.instances).length > 0;
+ return window.CKEDITOR && Object.keys(CKEDITOR.instances).length > 0;
}())
JS;
$this->assertJsCondition($javascript);
diff --git a/core/modules/media_library/js/media_library.ui.es6.js b/core/modules/media_library/js/media_library.ui.es6.js
index 95bc01ac3fe5..569b44312110 100644
--- a/core/modules/media_library/js/media_library.ui.es6.js
+++ b/core/modules/media_library/js/media_library.ui.es6.js
@@ -83,37 +83,21 @@
// Override the AJAX success callback to shift focus to the media
// library content.
ajaxObject.success = function (response, status) {
- // Remove the progress element.
- if (this.progress.element) {
- $(this.progress.element).remove();
- }
- if (this.progress.object) {
- this.progress.object.stopMonitoring();
- }
- $(this.element).prop('disabled', false);
-
- // Execute the AJAX commands.
- Object.keys(response || {}).forEach((i) => {
- if (response[i].command && this.commands[response[i].command]) {
- this.commands[response[i].command](this, response[i], status);
+ return Promise.resolve(
+ Drupal.Ajax.prototype.success.call(ajaxObject, response, status),
+ ).then(() => {
+ // Set focus to the first tabbable element in the media library
+ // content.
+ const mediaLibraryContent = document.getElementById(
+ 'media-library-content',
+ );
+ if (mediaLibraryContent) {
+ const tabbableContent = tabbable(mediaLibraryContent);
+ if (tabbableContent.length) {
+ tabbableContent[0].focus();
+ }
}
});
-
- // Set focus to the first tabbable element in the media library
- // content.
- const mediaLibraryContent = document.getElementById(
- 'media-library-content',
- );
- if (mediaLibraryContent) {
- const tabbableContent = tabbable(mediaLibraryContent);
- if (tabbableContent.length) {
- tabbableContent[0].focus();
- }
- }
-
- // Remove any response-specific settings so they don't get used on
- // the next call by mistake.
- this.settings = null;
};
ajaxObject.execute();
diff --git a/core/modules/media_library/js/media_library.ui.js b/core/modules/media_library/js/media_library.ui.js
index 96ed3914fae3..b7d5c61386e0 100644
--- a/core/modules/media_library/js/media_library.ui.js
+++ b/core/modules/media_library/js/media_library.ui.js
@@ -42,31 +42,17 @@
});
ajaxObject.success = function (response, status) {
- if (this.progress.element) {
- $(this.progress.element).remove();
- }
+ return Promise.resolve(Drupal.Ajax.prototype.success.call(ajaxObject, response, status)).then(() => {
+ const mediaLibraryContent = document.getElementById('media-library-content');
- if (this.progress.object) {
- this.progress.object.stopMonitoring();
- }
+ if (mediaLibraryContent) {
+ const tabbableContent = tabbable(mediaLibraryContent);
- $(this.element).prop('disabled', false);
- Object.keys(response || {}).forEach(i => {
- if (response[i].command && this.commands[response[i].command]) {
- this.commands[response[i].command](this, response[i], status);
+ if (tabbableContent.length) {
+ tabbableContent[0].focus();
+ }
}
});
- const mediaLibraryContent = document.getElementById('media-library-content');
-
- if (mediaLibraryContent) {
- const tabbableContent = tabbable(mediaLibraryContent);
-
- if (tabbableContent.length) {
- tabbableContent[0].focus();
- }
- }
-
- this.settings = null;
};
ajaxObject.execute();
diff --git a/core/modules/quickedit/js/quickedit.es6.js b/core/modules/quickedit/js/quickedit.es6.js
index fe0dc6a8b669..d399a1323635 100644
--- a/core/modules/quickedit/js/quickedit.es6.js
+++ b/core/modules/quickedit/js/quickedit.es6.js
@@ -180,16 +180,9 @@
url: Drupal.url('quickedit/attachments'),
submit: { 'editors[]': missingEditors },
});
- // Implement a scoped insert AJAX command: calls the callback after all AJAX
- // command functions have been executed (hence the deferred calling).
- const realInsert = Drupal.AjaxCommands.prototype.insert;
- loadEditorsAjax.commands.insert = function (ajax, response, status) {
- _.defer(callback);
- realInsert(ajax, response, status);
- };
// Trigger the AJAX request, which will should return AJAX commands to
// insert any missing attachments.
- loadEditorsAjax.execute();
+ loadEditorsAjax.execute().then(callback);
}
/**
diff --git a/core/modules/quickedit/js/quickedit.js b/core/modules/quickedit/js/quickedit.js
index 5b7ef22fb998..91d07d9acca7 100644
--- a/core/modules/quickedit/js/quickedit.js
+++ b/core/modules/quickedit/js/quickedit.js
@@ -82,15 +82,7 @@
'editors[]': missingEditors
}
});
- const realInsert = Drupal.AjaxCommands.prototype.insert;
-
- loadEditorsAjax.commands.insert = function (ajax, response, status) {
- _.defer(callback);
-
- realInsert(ajax, response, status);
- };
-
- loadEditorsAjax.execute();
+ loadEditorsAjax.execute().then(callback);
}
function initializeEntityContextualLink(contextualLink) {
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml
index 9d390854bba2..eb4ae93d92e4 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml
@@ -33,3 +33,12 @@ focus.first:
dependencies:
- core/drupal
- core/once
+
+command_promise:
+ version: VERSION
+ js:
+ js/command_promise-ajax.js: {}
+ dependencies:
+ - core/jquery
+ - core/drupal
+ - core/drupal.ajax
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
index 01bf512adb96..0ee765e664bc 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
@@ -93,3 +93,11 @@ ajax_test.focus_first_form:
_form: '\Drupal\ajax_test\Form\AjaxTestFocusFirstForm'
requirements:
_access: 'TRUE'
+
+ajax_test.promise:
+ path: '/ajax-test/promise-form'
+ defaults:
+ _title: 'Ajax Form Command Promise'
+ _form: '\Drupal\ajax_test\Form\AjaxTestFormPromise'
+ requirements:
+ _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.es6.js b/core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.es6.js
new file mode 100644
index 000000000000..8f4e40d85ba5
--- /dev/null
+++ b/core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.es6.js
@@ -0,0 +1,31 @@
+/**
+ * @file
+ * Testing behavior for the add_js command.
+ */
+
+(($, Drupal) => {
+ /**
+ * Test Ajax execution Order.
+ *
+ * @param {Drupal.Ajax} [ajax]
+ * {@link Drupal.Ajax} object created by {@link Drupal.Ajax}.
+ * @param {object} response
+ * The response from the Ajax request.
+ * @param {string} response.selector
+ * A jQuery selector string.
+ *
+ * @return {Promise}
+ * The promise that will resolve once this command has finished executing.
+ */
+ Drupal.AjaxCommands.prototype.ajaxCommandReturnPromise = function (
+ ajax,
+ response,
+ ) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ this.insert(ajax, response);
+ resolve();
+ }, Math.random() * 500);
+ });
+ };
+})(jQuery, Drupal);
diff --git a/core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.js b/core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.js
new file mode 100644
index 000000000000..162ac73f3c65
--- /dev/null
+++ b/core/modules/system/tests/modules/ajax_test/js/command_promise-ajax.js
@@ -0,0 +1,17 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(($, Drupal) => {
+ Drupal.AjaxCommands.prototype.ajaxCommandReturnPromise = function (ajax, response) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ this.insert(ajax, response);
+ resolve();
+ }, Math.random() * 500);
+ });
+ };
+})(jQuery, Drupal); \ No newline at end of file
diff --git a/core/modules/system/tests/modules/ajax_test/src/Ajax/AjaxTestCommandReturnPromise.php b/core/modules/system/tests/modules/ajax_test/src/Ajax/AjaxTestCommandReturnPromise.php
new file mode 100644
index 000000000000..fbb0dd94b797
--- /dev/null
+++ b/core/modules/system/tests/modules/ajax_test/src/Ajax/AjaxTestCommandReturnPromise.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\ajax_test\Ajax;
+
+use Drupal\Core\Ajax\AppendCommand;
+
+/**
+ * Test Ajax command.
+ */
+class AjaxTestCommandReturnPromise extends AppendCommand {
+
+ /**
+ * Implements Drupal\Core\Ajax\CommandInterface:render().
+ */
+ public function render() {
+
+ return [
+ 'command' => 'ajaxCommandReturnPromise',
+ 'method' => 'append',
+ 'selector' => $this->selector,
+ 'data' => $this->getRenderedContent(),
+ 'settings' => $this->settings,
+ ];
+ }
+
+}
diff --git a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestFormPromise.php b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestFormPromise.php
new file mode 100644
index 000000000000..038b0b5bcd7b
--- /dev/null
+++ b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestFormPromise.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Drupal\ajax_test\Form;
+
+use Drupal\ajax_test\Ajax\AjaxTestCommandReturnPromise;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\AppendCommand;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Test form for ajax_test_form_promise.
+ *
+ * @internal
+ */
+class AjaxTestFormPromise extends FormBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'ajax_test_form_promise';
+ }
+
+ /**
+ * Form for testing the addition of various types of elements via Ajax.
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $form['#attached']['library'][] = 'ajax_test/command_promise';
+ $form['custom']['#prefix'] = '<div id="ajax_test_form_promise_wrapper">';
+ $form['custom']['#suffix'] = '</div>';
+
+ // Button to test for the execution order of Ajax commands.
+ $form['test_execution_order_button'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Execute commands button'),
+ '#button_type' => 'primary',
+ '#ajax' => [
+ 'callback' => [static::class, 'executeCommands'],
+ 'progress' => [
+ 'type' => 'throbber',
+ 'message' => NULL,
+ ],
+ 'wrapper' => 'ajax_test_form_promise_wrapper',
+ ],
+ ];
+ return $form;
+ }
+
+ /**
+ * Ajax callback for the "Execute commands button" button.
+ */
+ public static function executeCommands(array $form, FormStateInterface $form_state) {
+ $selector = '#ajax_test_form_promise_wrapper';
+ $response = new AjaxResponse();
+
+ $response->addCommand(new AppendCommand($selector, '1'));
+ $response->addCommand(new AjaxTestCommandReturnPromise($selector, '2'));
+ $response->addCommand(new AppendCommand($selector, '3'));
+ $response->addCommand(new AppendCommand($selector, '4'));
+ $response->addCommand(new AjaxTestCommandReturnPromise($selector, '5'));
+
+ return $response;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // An empty implementation, as we never submit the actual form.
+ }
+
+}
diff --git a/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php b/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php
index de95fb576168..669037f82d05 100644
--- a/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php
+++ b/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php
@@ -4,10 +4,9 @@ namespace Drupal\Tests\system\Functional\Ajax;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Ajax\AddCssCommand;
+use Drupal\Core\Ajax\AddJsCommand;
use Drupal\Core\Ajax\AlertCommand;
-use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Ajax\HtmlCommand;
-use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\SettingsCommand;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
@@ -61,8 +60,8 @@ class FrameworkTest extends BrowserTestBase {
[$js_assets_header, $js_assets_footer] = $asset_resolver->getJsAssets($assets, FALSE);
$js_header_render_array = $js_collection_renderer->render($js_assets_header);
$js_footer_render_array = $js_collection_renderer->render($js_assets_footer);
- $expected_commands[2] = new PrependCommand('head', $js_header_render_array);
- $expected_commands[3] = new AppendCommand('body', $js_footer_render_array);
+ $expected_commands[2] = new AddJsCommand(array_column($js_header_render_array, '#attributes'), 'head');
+ $expected_commands[3] = new AddJsCommand(array_column($js_footer_render_array, '#attributes'));
$expected_commands[4] = new HtmlCommand('body', 'Hello, world!');
// Verify AJAX command order — this should always be the order:
diff --git a/core/tests/Drupal/Nightwatch/Tests/ajaxExecutionOrderTest.js b/core/tests/Drupal/Nightwatch/Tests/ajaxExecutionOrderTest.js
new file mode 100644
index 000000000000..da54f6015a6c
--- /dev/null
+++ b/core/tests/Drupal/Nightwatch/Tests/ajaxExecutionOrderTest.js
@@ -0,0 +1,34 @@
+module.exports = {
+ '@tags': ['core', 'ajax'],
+ before(browser) {
+ browser.drupalInstall().drupalLoginAsAdmin(() => {
+ browser
+ .drupalRelativeURL('/admin/modules')
+ .setValue('input[type="search"]', 'Ajax test')
+ .waitForElementVisible('input[name="modules[ajax_test][enable]"]', 1000)
+ .click('input[name="modules[ajax_test][enable]"]')
+ .submitForm('input[type="submit"]') // Submit module form.
+ .waitForElementVisible(
+ '.system-modules-confirm-form input[value="Continue"]',
+ 2000,
+ )
+ .submitForm('input[value="Continue"]') // Confirm installation of dependencies.
+ .waitForElementVisible('.system-modules', 10000);
+ });
+ },
+ after(browser) {
+ browser.drupalUninstall();
+ },
+ 'Test Execution Order': (browser) => {
+ browser
+ .drupalRelativeURL('/ajax-test/promise-form')
+ .waitForElementVisible('body', 1000)
+ .click('[data-drupal-selector="edit-test-execution-order-button"]')
+ .waitForElementVisible('#ajax_test_form_promise_wrapper', 1000)
+ .assert.containsText(
+ '#ajax_test_form_promise_wrapper',
+ '12345',
+ 'Ajax commands execution order confirmed',
+ );
+ },
+};