Skip to content
Snippets Groups Projects
Commit f85ec8e8 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

(cherry picked from commit 6c5374eb)
parent 18e211cc
No related branches found
No related tags found
17 merge requests!8506Draft: Issue #3456536 by ibrahim tameme,!5646Issue #3350972 by nod_: [random test failure]...,!5600Issue #3350972 by nod_: [random test failure]...,!5343Issue #3305066 by quietone, Rename RedirectLeadingSlashesSubscriber,!3603#ISSUE 3346218 Add a different message on edit comment,!3555Issue #2473873: Views entity operations lack cacheability support, resulting in incorrect dropbuttons,!3494Issue #3327018 by Spokje, longwave, xjm, mondrake: Update PHPStan to 1.9.3 and...,!3410Issue #3340128: UserLoginForm::submitForm has some dead code,!3389Issue #3325184 by Spokje, andypost, xjm, smustgrave: $this->configFactory is...,!3381Issue #3332363: Refactor Claro's menus-and-lists stylesheet,!3307Issue #3326193: CKEditor 5 can grow past the viewport when there is a lot of content,!3236Issue #3332419: Refactor Claro's messages stylesheet,!3231Draft: Issue #3049525 by longwave, fougere, larowlan, kim.pepper, AaronBauman, Wim...,!3212Issue #3294003: Refactor Claro's entity-meta stylesheet,!3194Issue #3330981: Fix PHPStan L1 error "Relying on entity queries to check access by default is deprecated...",!3143Issue #3313342: [PHP 8.1] Deprecated function: strpos(): Passing null to parameter #1 LayoutBuilderUiCacheContext.php on line 28,!2972Issue #1845004: Replace custom password hashing library with PHP 5.5 password_hash()
......@@ -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