diff --git a/core/misc/ajax.es6.js b/core/misc/ajax.es6.js
index a438fbc60c..739afa1974 100644
--- a/core/misc/ajax.es6.js
+++ b/core/misc/ajax.es6.js
@@ -811,6 +811,40 @@
}
};
+ /**
+ * An animated progress throbber and container element for AJAX operations.
+ *
+ * @param {string} [message]
+ * (optional) The message shown on the UI.
+ * @return {string}
+ * The HTML markup for the throbber.
+ */
+ Drupal.theme.ajaxProgressThrobber = (message) => {
+ // Build markup without adding extra white space since it affects rendering.
+ const messageMarkup = typeof message === 'string' ? Drupal.theme('ajaxProgressMessage', message) : '';
+ const throbber = '
';
+
+ return `${throbber}${messageMarkup}
`;
+ };
+
+ /**
+ * An animated progress throbber and container element for AJAX operations.
+ *
+ * @return {string}
+ * The HTML markup for the throbber.
+ */
+ Drupal.theme.ajaxProgressIndicatorFullscreen = () => '
';
+
+ /**
+ * Formats text accompanying the AJAX progress throbber.
+ *
+ * @param {string} message
+ * The message shown on the UI.
+ * @return {string}
+ * The HTML markup for the throbber.
+ */
+ Drupal.theme.ajaxProgressMessage = message => `${message}
`;
+
/**
* Sets the progress bar progress indicator.
*/
@@ -831,10 +865,7 @@
* Sets the throbber progress indicator.
*/
Drupal.Ajax.prototype.setProgressIndicatorThrobber = function () {
- this.progress.element = $('');
- if (this.progress.message) {
- this.progress.element.find('.throbber').after(`${this.progress.message}
`);
- }
+ this.progress.element = $(Drupal.theme('ajaxProgressThrobber', this.progress.message));
$(this.element).after(this.progress.element);
};
@@ -842,7 +873,7 @@
* Sets the fullscreen progress indicator.
*/
Drupal.Ajax.prototype.setProgressIndicatorFullscreen = function () {
- this.progress.element = $('
');
+ this.progress.element = $(Drupal.theme('ajaxProgressIndicatorFullscreen'));
$('body').after(this.progress.element);
};
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index 814d82f8f2..abe0ec2928 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -368,6 +368,21 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
}
};
+ Drupal.theme.ajaxProgressThrobber = function (message) {
+ var messageMarkup = typeof message === 'string' ? Drupal.theme('ajaxProgressMessage', message) : '';
+ var throbber = '
';
+
+ return '' + throbber + messageMarkup + '
';
+ };
+
+ Drupal.theme.ajaxProgressIndicatorFullscreen = function () {
+ return '
';
+ };
+
+ Drupal.theme.ajaxProgressMessage = function (message) {
+ return '' + message + '
';
+ };
+
Drupal.Ajax.prototype.setProgressIndicatorBar = function () {
var progressBar = new Drupal.ProgressBar('ajax-progress-' + this.element.id, $.noop, this.progress.method, $.noop);
if (this.progress.message) {
@@ -382,15 +397,12 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
};
Drupal.Ajax.prototype.setProgressIndicatorThrobber = function () {
- this.progress.element = $('');
- if (this.progress.message) {
- this.progress.element.find('.throbber').after('' + this.progress.message + '
');
- }
+ this.progress.element = $(Drupal.theme('ajaxProgressThrobber', this.progress.message));
$(this.element).after(this.progress.element);
};
Drupal.Ajax.prototype.setProgressIndicatorFullscreen = function () {
- this.progress.element = $('
');
+ this.progress.element = $(Drupal.theme('ajaxProgressIndicatorFullscreen'));
$('body').after(this.progress.element);
};
diff --git a/core/modules/field_ui/field_ui.es6.js b/core/modules/field_ui/field_ui.es6.js
index 965a2e4c36..cf12841711 100644
--- a/core/modules/field_ui/field_ui.es6.js
+++ b/core/modules/field_ui/field_ui.es6.js
@@ -219,7 +219,7 @@
if (rowNames.length) {
// Add a throbber next each of the ajaxElements.
- $(ajaxElements).after('');
+ $(ajaxElements).after(Drupal.theme.ajaxProgressThrobber());
// Fire the Ajax update.
$('input[name=refresh_rows]').val(rowNames.join(' '));
diff --git a/core/modules/field_ui/field_ui.js b/core/modules/field_ui/field_ui.js
index d0e8a6d712..5cbeb6458b 100644
--- a/core/modules/field_ui/field_ui.js
+++ b/core/modules/field_ui/field_ui.js
@@ -126,7 +126,7 @@
});
if (rowNames.length) {
- $(ajaxElements).after('');
+ $(ajaxElements).after(Drupal.theme.ajaxProgressThrobber());
$('input[name=refresh_rows]').val(rowNames.join(' '));
$('input[data-drupal-selector="edit-refresh"]').trigger('mousedown');
diff --git a/core/modules/system/tests/modules/hold_test/hold_test.info.yml b/core/modules/system/tests/modules/hold_test/hold_test.info.yml
new file mode 100644
index 0000000000..f767751422
--- /dev/null
+++ b/core/modules/system/tests/modules/hold_test/hold_test.info.yml
@@ -0,0 +1,6 @@
+name: Hold test
+type: module
+description: 'Support testing with request/response hold.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/system/tests/modules/hold_test/hold_test.install b/core/modules/system/tests/modules/hold_test/hold_test.install
new file mode 100644
index 0000000000..183463deb3
--- /dev/null
+++ b/core/modules/system/tests/modules/hold_test/hold_test.install
@@ -0,0 +1,14 @@
+hold(static::HOLD_REQUEST);
+ }
+
+ /**
+ * Response hold.
+ */
+ public function onRespond() {
+ $this->hold(static::HOLD_RESPONSE);
+ }
+
+ /**
+ * Hold process by type.
+ *
+ * @param string $type
+ * Type of hold.
+ */
+ protected function hold($type) {
+ $path = \Drupal::root() . "/sites/default/files/simpletest/hold_test_$type.txt";
+ do {
+ $status = (bool) file_get_contents($path);
+ } while ($status && (NULL === usleep(100000)));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ $events[KernelEvents::REQUEST][] = ['onRequest'];
+ $events[KernelEvents::RESPONSE][] = ['onRespond'];
+ return $events;
+ }
+
+}
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ThrobberTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ThrobberTest.php
new file mode 100644
index 0000000000..e2ecaea7a4
--- /dev/null
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/ThrobberTest.php
@@ -0,0 +1,112 @@
+drupalCreateUser([
+ 'administer views',
+ ]);
+ $this->drupalLogin($admin_user);
+ }
+
+ /**
+ * Tests theming throbber element.
+ */
+ public function testThemingThrobberElement() {
+ $session = $this->getSession();
+ $web_assert = $this->assertSession();
+ $page = $session->getPage();
+
+ $custom_ajax_progress_indicator_fullscreen = <<';
+ };
+JS;
+ $custom_ajax_progress_throbber = <<';
+ };
+JS;
+ $custom_ajax_progress_message = <<Hold door!';
+ };
+JS;
+
+ $this->drupalGet('admin/structure/views/view/content');
+ $this->waitForNoElement('.ajax-progress-fullscreen');
+
+ // Test theming fullscreen throbber.
+ $session->executeScript($custom_ajax_progress_indicator_fullscreen);
+ hold_test_response(TRUE);
+ $page->clickLink('Content: Published (grouped)');
+ $this->assertNotNull($web_assert->waitForElement('css', '.custom-ajax-progress-fullscreen'), 'Custom ajaxProgressIndicatorFullscreen.');
+ hold_test_response(FALSE);
+ $this->waitForNoElement('.custom-ajax-progress-fullscreen');
+
+ // Test theming throbber message.
+ $web_assert->waitForElementVisible('css', '[data-drupal-selector="edit-options-group-info-add-group"]');
+ $session->executeScript($custom_ajax_progress_message);
+ hold_test_response(TRUE);
+ $page->pressButton('Add another item');
+ $this->assertNotNull($web_assert->waitForElement('css', '.ajax-progress-throbber .custom-ajax-progress-message'), 'Custom ajaxProgressMessage.');
+ hold_test_response(FALSE);
+ $this->waitForNoElement('.ajax-progress-throbber');
+
+ // Test theming throbber.
+ $web_assert->waitForElementVisible('css', '[data-drupal-selector="edit-options-group-info-group-items-3-title"]');
+ $session->executeScript($custom_ajax_progress_throbber);
+ hold_test_response(TRUE);
+ $page->pressButton('Add another item');
+ $this->assertNotNull($web_assert->waitForElement('css', '.custom-ajax-progress-throbber'), 'Custom ajaxProgressThrobber.');
+ hold_test_response(FALSE);
+ $this->waitForNoElement('.custom-ajax-progress-throbber');
+ }
+
+ /**
+ * Waits for an element to be removed from the page.
+ *
+ * @param string $selector
+ * CSS selector.
+ * @param int $timeout
+ * (optional) Timeout in milliseconds, defaults to 10000.
+ *
+ * @todo Remove in https://www.drupal.org/node/2892440.
+ */
+ protected function waitForNoElement($selector, $timeout = 10000) {
+ $condition = "(typeof jQuery !== 'undefined' && jQuery('$selector').length === 0)";
+ $this->assertJsCondition($condition, $timeout);
+ }
+
+}