Skip to content
Snippets Groups Projects
Commit 6c5374eb authored by catch's avatar catch
Browse files

Issue #3324062 by effulgentsia, nod_, longwave, alexpott, catch, lauriii,...

Issue #3324062 by effulgentsia, nod_, longwave, alexpott, catch, lauriii, andypost: [Regression] Changes to Drupal.ajax in 9.5.x have caused regressions in paragraphs_features module
parent 85eeb1bd
No related branches found
No related tags found
No related merge requests found
......@@ -505,6 +505,9 @@
ajax.options = {
url: ajax.url,
data: ajax.submit,
isInProgress() {
return ajax.ajaxing;
},
beforeSerialize(elementSettings, options) {
return ajax.beforeSerialize(elementSettings, options);
},
......@@ -552,6 +555,17 @@
// finished executing.
.then(() => {
ajax.ajaxing = false;
// jQuery normally triggers the ajaxSuccess, ajaxComplete, and
// ajaxStop events after the "success" function passed to $.ajax()
// returns, but we prevented that via
// $.event.special[EVENT_NAME].trigger in order to wait for the
// commands to finish executing. Now that they have, re-trigger
// those events.
$(document).trigger('ajaxSuccess', [xmlhttprequest, this]);
$(document).trigger('ajaxComplete', [xmlhttprequest, this]);
if (--$.active === 0) {
$(document).trigger('ajaxStop');
}
})
);
},
......@@ -1735,4 +1749,50 @@
});
},
};
/**
* Delay jQuery's global completion events until after commands have executed.
*
* jQuery triggers the ajaxSuccess, ajaxComplete, and ajaxStop events after
* a successful response is returned and local success and complete events
* are triggered. However, Drupal Ajax responses contain commands that run
* asynchronously in a queue, so the following stops these events from getting
* triggered until after the Promise that executes the command queue is
* resolved.
*/
const stopEvent = (xhr, settings) => {
return (
// Only interfere with Drupal's Ajax responses.
xhr.getResponseHeader('X-Drupal-Ajax-Token') === '1' &&
// The isInProgress() function might not be defined if the Ajax request
// was initiated without Drupal.ajax() or new Drupal.Ajax().
settings.isInProgress &&
// Until this is false, the Ajax request isn't completely done (the
// response's commands might still be running).
settings.isInProgress()
);
};
$.extend(true, $.event.special, {
ajaxSuccess: {
trigger(event, xhr, settings) {
if (stopEvent(xhr, settings)) {
return false;
}
},
},
ajaxComplete: {
trigger(event, xhr, settings) {
if (stopEvent(xhr, settings)) {
// jQuery decrements its internal active ajax counter even when we
// stop the ajaxComplete event, but we don't want that counter
// decremented, because for our purposes this request is still active
// while commands are executing. By incrementing it here, the net
// effect is that it remains unchanged. By remaining above 0, the
// ajaxStop event is also prevented.
$.active++;
return false;
}
},
},
});
})(jQuery, window, Drupal, drupalSettings, loadjs, window.tabbable);
......@@ -42,3 +42,9 @@ command_promise:
- core/jquery
- core/drupal
- core/drupal.ajax
global_events:
js:
js/global_events.js: {}
dependencies:
- core/drupal.ajax
......@@ -101,3 +101,17 @@ ajax_test.promise:
_form: '\Drupal\ajax_test\Form\AjaxTestFormPromise'
requirements:
_access: 'TRUE'
ajax_test.global_events:
path: '/ajax-test/global-events'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::globalEvents'
requirements:
_access: 'TRUE'
ajax_test.global_events_clear_log:
path: '/ajax-test/global-events/clear-log'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::globalEventsClearLog'
requirements:
_access: 'TRUE'
/**
* @file
* For testing that jQuery's ajaxSuccess, ajaxComplete, and ajaxStop events
* are triggered only after commands in a Drupal Ajax response are executed.
*/
(($, Drupal) => {
['ajaxSuccess', 'ajaxComplete', 'ajaxStop'].forEach((eventName) => {
$(document)[eventName](() => {
$('#test_global_events_log').append(eventName);
$('#test_global_events_log2').append(eventName);
});
});
})(jQuery, Drupal);
......@@ -354,4 +354,37 @@ protected function getRenderTypes() {
return $render_info;
}
/**
* Returns a page from which to test Ajax global events.
*
* @return array
* The render array.
*/
public function globalEvents() {
return [
'#attached' => [
'library' => [
'ajax_test/global_events',
],
],
'#markup' => implode('', [
'<div id="test_global_events_log"></div>',
'<a id="test_global_events_drupal_ajax_link" class="use-ajax" href="' . Url::fromRoute('ajax_test.global_events_clear_log')->toString() . '">Drupal Ajax</a>',
'<div id="test_global_events_log2"></div>',
]),
];
}
/**
* Returns an AjaxResponse with command to clear the 'test_global_events_log'.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The JSON response object.
*/
public function globalEventsClearLog() {
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand('#test_global_events_log', ''));
return $response;
}
}
......@@ -153,6 +153,51 @@ public function testInsertAjaxResponse() {
$this->assertInsert('empty', $expected, $custom_wrapper_new_content);
}
/**
* Tests that jQuery's global Ajax events are triggered at the correct time.
*/
public function testGlobalEvents() {
$session = $this->getSession();
$assert = $this->assertSession();
$expected_event_order = implode('', ['ajaxSuccess', 'ajaxComplete', 'ajaxStop']);
$this->drupalGet('ajax-test/global-events');
// Ensure that a non-Drupal Ajax request triggers the expected events, in
// the correct order, a single time.
$session->executeScript('jQuery.get(Drupal.url("core/COPYRIGHT.txt"))');
$assert->assertWaitOnAjaxRequest();
$assert->elementTextEquals('css', '#test_global_events_log', $expected_event_order);
$assert->elementTextEquals('css', '#test_global_events_log2', $expected_event_order);
// Ensure that an Ajax request to a Drupal Ajax response, but that was not
// initiated with Drupal.Ajax(), triggers the expected events, in the
// correct order, a single time. We expect $expected_event_order to appear
// twice in each log element, because Drupal Ajax response commands (such
// as the one to clear the log element) are only executed for requests
// initiated with Drupal.Ajax(), and these elements already contain the
// text that was added above.
$session->executeScript('jQuery.get(Drupal.url("ajax-test/global-events/clear-log"))');
$assert->assertWaitOnAjaxRequest();
$assert->elementTextEquals('css', '#test_global_events_log', str_repeat($expected_event_order, 2));
$assert->elementTextEquals('css', '#test_global_events_log2', str_repeat($expected_event_order, 2));
// Ensure that a Drupal Ajax request triggers the expected events, in the
// correct order, a single time.
// - We expect the first log element to list the events exactly once,
// because the Ajax response clears it, and we expect the events to be
// triggered after the commands are executed.
// - We expect the second log element to list the events exactly three
// times, because it already contains the two from the code that was
// already executed above. This additional log element that isn't cleared
// by the response's command ensures that the events weren't triggered
// additional times before the response commands were executed.
$this->click('#test_global_events_drupal_ajax_link');
$assert->assertWaitOnAjaxRequest();
$assert->elementTextEquals('css', '#test_global_events_log', $expected_event_order);
$assert->elementTextEquals('css', '#test_global_events_log2', str_repeat($expected_event_order, 3));
}
/**
* Assert insert.
*
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment