diff --git a/core/core.libraries.yml b/core/core.libraries.yml index c52ee38c39b3bea76a472936389cdbccdb90a090..fbab5df2836d52789db7d19bf405adff89f8381e 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -528,6 +528,16 @@ jquery.once: assets/vendor/jquery-once/jquery.once.min.js: { weight: -19, minified: true } dependencies: - core/jquery + - core/jquery.once.bc + +jquery.once.bc: + version: VERSION + js: + misc/jquery.once.bc.js: { weight: -19 } + dependencies: + - core/jquery + - core/once + - core/drupal.object.assign jquery.ui: version: &jquery_ui_version 1.12.1 diff --git a/core/misc/jquery.once.bc.es6.js b/core/misc/jquery.once.bc.es6.js new file mode 100644 index 0000000000000000000000000000000000000000..c6fe6bd5aacdc2d9f2be34265978c927eb9ca464 --- /dev/null +++ b/core/misc/jquery.once.bc.es6.js @@ -0,0 +1,33 @@ +/** + * @file + * This file allows calls to `once()` and `once.remove()` to also populate the + * jQuery.once registry. + * + * It allows contributed code still using jQuery.once to behave as expected: + * @example + * once('core-once-call', 'body'); + * + * // The following will work in a contrib module still using jQuery.once: + * $('body').once('core-once-call'); // => returns empty object + */ + +(($, once) => { + // We'll replace the whole library so keep a version in cache for later. + const drupalOnce = once; + + // When calling once(), also populate jQuery.once registry. + function augmentedOnce(id, selector, context) { + $(selector, context).once(id); + return drupalOnce(id, selector, context); + } + + // When calling once.remove(), also remove it from jQuery.once registry. + function remove(id, selector, context) { + $(selector, context).removeOnce(id); + return drupalOnce.remove(id, selector, context); + } + + // Expose the rest of @drupal/once API and replace @drupal/once library with + // the version augmented with jQuery.once calls. + window.once = Object.assign(augmentedOnce, drupalOnce, { remove }); +})(jQuery, once); diff --git a/core/misc/jquery.once.bc.js b/core/misc/jquery.once.bc.js new file mode 100644 index 0000000000000000000000000000000000000000..1e7a47597a95458c69681c6abe3e60bf95182128 --- /dev/null +++ b/core/misc/jquery.once.bc.js @@ -0,0 +1,24 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($, once) { + var drupalOnce = once; + + function augmentedOnce(id, selector, context) { + $(selector, context).once(id); + return drupalOnce(id, selector, context); + } + + function remove(id, selector, context) { + $(selector, context).removeOnce(id); + return drupalOnce.remove(id, selector, context); + } + + window.once = Object.assign(augmentedOnce, drupalOnce, { + remove: remove + }); +})(jQuery, once); \ No newline at end of file diff --git a/core/modules/system/tests/modules/js_once_test/js_once_test.info.yml b/core/modules/system/tests/modules/js_once_test/js_once_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..c12f79e1f01ead108ea975ff2bf358d7741a7df2 --- /dev/null +++ b/core/modules/system/tests/modules/js_once_test/js_once_test.info.yml @@ -0,0 +1,5 @@ +name: 'JS once Test' +type: module +description: 'Module for the jsOnceTest.' +package: Testing +version: VERSION diff --git a/core/modules/system/tests/modules/js_once_test/js_once_test.routing.yml b/core/modules/system/tests/modules/js_once_test/js_once_test.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..c73a876b3ae6a7de9ffa01d3aa8c20c8a224822f --- /dev/null +++ b/core/modules/system/tests/modules/js_once_test/js_once_test.routing.yml @@ -0,0 +1,14 @@ +js_once_test: + path: '/js_once_test' + defaults: + _controller: '\Drupal\js_once_test\Controller\JsOnceTestController::onceTest' + _title: 'OnceTest' + requirements: + _access: 'TRUE' +js_once_test.with_bc: + path: '/js_once_with_bc_test' + defaults: + _controller: '\Drupal\js_once_test\Controller\JsOnceTestController::onceBcTest' + _title: 'OnceBcTest' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/js_once_test/src/Controller/JsOnceTestController.php b/core/modules/system/tests/modules/js_once_test/src/Controller/JsOnceTestController.php new file mode 100644 index 0000000000000000000000000000000000000000..c47505cd3bba5dc382511b7c4892f98f0198dec5 --- /dev/null +++ b/core/modules/system/tests/modules/js_once_test/src/Controller/JsOnceTestController.php @@ -0,0 +1,58 @@ +<?php + +namespace Drupal\js_once_test\Controller; + +use Drupal\Core\Controller\ControllerBase; + +/** + * Controller for testing the @drupal/once library integration. + */ +class JsOnceTestController extends ControllerBase { + + /** + * Provides elements for testing @drupal/once. + * + * @return array + * The render array. + */ + public function onceTest() { + $output = [ + '#attached' => ['library' => ['core/once']], + ]; + foreach (range(1, 5) as $item) { + $output['item' . $item] = [ + '#type' => 'html_tag', + '#tag' => 'div', + '#value' => 'Item ' . $item, + '#attributes' => [ + 'data-drupal-item' => $item, + ], + ]; + } + return $output; + } + + /** + * Provides elements for testing jQuery Once BC support. + * + * @return array + * The render array. + */ + public function onceBcTest() { + $output = [ + '#attached' => ['library' => ['core/jquery.once']], + ]; + foreach (range(1, 5) as $item) { + $output['item' . $item] = [ + '#type' => 'html_tag', + '#tag' => 'div', + '#value' => 'Item ' . $item, + '#attributes' => [ + 'data-drupal-item' => $item, + ], + ]; + } + return $output; + } + +} diff --git a/core/tests/Drupal/Nightwatch/Tests/jsOnceTest.js b/core/tests/Drupal/Nightwatch/Tests/jsOnceTest.js new file mode 100644 index 0000000000000000000000000000000000000000..b1f4a715352ddcab9f4e0dad7d9a426a24d17f7c --- /dev/null +++ b/core/tests/Drupal/Nightwatch/Tests/jsOnceTest.js @@ -0,0 +1,210 @@ +module.exports = { + '@tags': ['core'], + before(browser) { + browser.drupalInstall().drupalLoginAsAdmin(() => { + browser + .drupalRelativeURL('/admin/modules') + .setValue('input[type="search"]', 'JS Once Test') + .waitForElementVisible( + 'input[name="modules[js_once_test][enable]"]', + 1000, + ) + .click('input[name="modules[js_once_test][enable]"]') + .click('input[type="submit"]'); // Submit module form. + }); + }, + after(browser) { + browser.drupalUninstall(); + }, + 'Test simple once call': (browser) => { + browser + .drupalRelativeURL('/js_once_test') + .waitForElementVisible('[data-drupal-item]', 1000) + // prettier-ignore + .execute( + function () { + return once('js_once_test', '[data-drupal-item]'); + }, + (result) => { + browser.assert.strictEqual( + result.value.length, + 5, + '5 items returned and "once-d"', + ); + }, + ) + // Check that follow-up calls to once return an empty array. + .execute( + function () { + return once('js_once_test', '[data-drupal-item]'); + }, + (result) => { + browser.assert.strictEqual( + result.value.length, + 0, + '0 items returned', + ); + }, + ) + .execute( + function () { + return once( + 'js_once_test_extra', + '[data-drupal-item="1"],[data-drupal-item="2"]', + ); + }, + (result) => { + browser.assert.strictEqual( + result.value.length, + 2, + '2 items returned and "once-d"', + ); + }, + ) + .execute( + function () { + return once( + 'js_once_test_extra', + '[data-drupal-item="1"],[data-drupal-item="2"]', + ); + }, + (result) => { + browser.assert.strictEqual( + result.value.length, + 0, + '0 items returned', + ); + }, + ) + .execute( + function () { + return once.remove('js_once_test', '[data-drupal-item]'); + }, + (result) => { + browser.assert.strictEqual( + result.value.length, + 5, + '5 items returned and "de-once-d"', + ); + }, + ) + .execute( + function () { + return once.remove('js_once_test', '[data-drupal-item]'); + }, + (result) => { + browser.assert.strictEqual( + result.value.length, + 0, + '0 items returned', + ); + }, + ) + .execute( + function () { + return once.remove( + 'js_once_test_extra', + '[data-drupal-item="1"],[data-drupal-item="2"]', + ); + }, + (result) => { + browser.assert.strictEqual( + result.value.length, + 2, + '2 items returned and "de-once-d"', + ); + }, + ) + .execute( + function () { + return once.remove( + 'js_once_test_extra', + '[data-drupal-item="1"],[data-drupal-item="2"]', + ); + }, + (result) => { + browser.assert.strictEqual( + result.value.length, + 0, + '0 items returned', + ); + }, + ) + .drupalLogAndEnd({ onlyOnError: false }); + }, + 'Test BC layer with jQuery Once calls': (browser) => { + browser + .drupalRelativeURL('/js_once_with_bc_test') + .waitForElementVisible('[data-drupal-item]', 1000) + // prettier-ignore + .execute( + function () { + // A core script calls once on some elements. + once('js_once_test', '[data-drupal-item]'); + // A contrib module not yet using @drupal/once calls jQuery Once. + return jQuery('[data-drupal-item]').once('js_once_test'); + }, + (result) => { + browser.assert.strictEqual( + result.value.length, + 0, + 'Calls to once() are taken into account when using jQuery.once()', + ); + }, + ) + // Once calls don't take into account calls to jQuery.once by design. + .execute( + function () { + // Calling jQuery.once before @drupal/once will lead to duplicate + // processing. + jQuery('[data-drupal-item]').once('js_once_test_extra'); + // A core script calls once on some elements. + return once('js_once_test_extra', '[data-drupal-item]'); + }, + (result) => { + browser.assert.strictEqual( + result.value.length, + 5, + '5 items returned by once() after a call to jQuery.once()', + ); + }, + ) + .execute( + function () { + once('js_once_test_remove', '[data-drupal-item]'); + // A core script calls once on some elements. + once.remove('js_once_test_remove', '[data-drupal-item]'); + // A contrib module not yet using @drupal/once calls the jQuery Once + // remove() function. + return jQuery('[data-drupal-item]').removeOnce('js_once_test_remove'); + }, + (result) => { + browser.assert.strictEqual( + result.value.length, + 0, + 'Calls to once.remove() are taken into account when using jQuery.removeOnce()', + ); + }, + ) + // Once.remove calls don't take into account calls to jQuery.removeOnce by + // design. + .execute( + function () { + once('js_once_test_remove_fail', '[data-drupal-item]'); + // Calling jQuery.removeOnce before @drupal/once will lead to + // duplicate processing. + jQuery('[data-drupal-item]').removeOnce('js_once_test_remove_fail'); + // A core script calls once.remove on some elements. + return once.remove('js_once_test_remove_fail', '[data-drupal-item]'); + }, + (result) => { + browser.assert.strictEqual( + result.value.length, + 5, + '5 items returned by once.remove() after a call to jQuery.removeOnce()', + ); + }, + ) + .drupalLogAndEnd({ onlyOnError: false }); + }, +};