diff --git a/core/.eslintrc.json b/core/.eslintrc.json index 369539d27f..ed22445ab5 100644 --- a/core/.eslintrc.json +++ b/core/.eslintrc.json @@ -13,6 +13,7 @@ "domready": true, "jQuery": true, "_": true, + "loadjs": true, "matchMedia": true, "Backbone": true, "Modernizr": true, diff --git a/core/assets/vendor/loadjs/loadjs.min.js b/core/assets/vendor/loadjs/loadjs.min.js new file mode 100644 index 0000000000..24f62cadc9 --- /dev/null +++ b/core/assets/vendor/loadjs/loadjs.min.js @@ -0,0 +1 @@ +loadjs=function(){function e(e,n){e=e.push?e:[e];var t,r,i,c,o=[],f=e.length,a=f;for(t=function(e,t){t.length&&o.push(e),--a||n(o)};f--;)r=e[f],i=s[r],i?t(r,i):(c=u[r]=u[r]||[],c.push(t))}function n(e,n){if(e){var t=u[e];if(s[e]=n,t)for(;t.length;)t[0](e,n),t.splice(0,1)}}function t(e,n,r,i){var o,s,u=document,f=r.async,a=(r.numRetries||0)+1,h=r.before||c;i=i||0,/(^css!|\.css$)/.test(e)?(o=!0,s=u.createElement("link"),s.rel="stylesheet",s.href=e.replace(/^css!/,"")):(s=u.createElement("script"),s.src=e,s.async=void 0===f||f),s.onload=s.onerror=s.onbeforeload=function(c){var u=c.type[0];if(o&&"hideFocus"in s)try{s.sheet.cssText.length||(u="e")}catch(e){u="e"}if("e"==u&&(i+=1)selector = $selector; + $this->styles = $scripts; + $this->method = $method; + } + + /** + * {@inheritdoc} + */ + public function render() { + + return [ + 'command' => 'add_js', + 'selector' => $this->selector, + 'data' => $this->styles, + 'method' => $this->method, + ]; + } + +} diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php index ee5208b078..269e62ba9d 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php @@ -174,11 +174,13 @@ protected function buildAttachmentsCommands(AjaxResponse $response, Request $req } 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)); + $scripts_attributes = array_map(function ($render_array) { return $render_array['#attributes']; }, $js_header_render_array); + $resource_commands[] = new AddJsCommand('head', $scripts_attributes, 'insertBefore'); } 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)); + $scripts_attributes = array_map(function ($render_array) { return $render_array['#attributes']; }, $js_footer_render_array); + $resource_commands[] = new AddJsCommand('body', $scripts_attributes, 'appendChild'); } 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 6248e46efe..e438c030d1 100644 --- a/core/misc/ajax.es6.js +++ b/core/misc/ajax.es6.js @@ -11,7 +11,8 @@ * included to provide Ajax capabilities. */ -(function ($, window, Drupal, drupalSettings) { +(function ($, window, Drupal, drupalSettings, loadjs) { + /** * Attaches the Ajax behavior to each Ajax form element. * @@ -856,15 +857,27 @@ // Track if any command is altering the focus so we can avoid changing the // focus set by the Ajax command. - let focusChanged = false; - for (const i in response) { - if (response.hasOwnProperty(i) && 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') { - focusChanged = true; + var focusChanged = false; + + var that = this; + Object.keys(response).reduce(function (deferredCommand, i) { + return deferredCommand.then(function () { + var command = response[i].command; + + if (command && that.commands[command]) { + if (command === 'invoke' && response[i].method === 'focus') { + focusChanged = true; + } + + if (command === 'add_js') { + return that.commands[command](that, response[i], status); + } + else { + that.commands[command](that, response[i], status); + } } - } - } + }); + }, $.Deferred().resolve()); // 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 @@ -1337,5 +1350,36 @@ } while (match); } }, + add_js(ajax, response) { + const deferred = $.Deferred(); + const scriptsSrc = response.data.map((script) => { + loadjs(script.src, script.src, { + async: !!script.async, + before(path, scriptEl) { + let selector = 'body'; + if (response.selector) { + selector = response.selector; + } + if (script.defer) { + scriptEl.defer = true; + } + /** + * To avoid synchronous XMLHttpRequest on the main thread and break + * load dependency, it should not use jQuery. + */ + document.querySelector(selector)[response.method](scriptEl); + // return `false` to bypass default DOM insertion mechanism + return false; + }, + }); + return script.src; + }); + loadjs.ready(scriptsSrc, { + success() { + deferred.resolve(); + }, + }); + return deferred.promise(); + }, }; -}(jQuery, window, Drupal, drupalSettings)); +})(jQuery, window, Drupal, drupalSettings, loadjs); diff --git a/core/misc/ajax.js b/core/misc/ajax.js index 5ea52425be..2564268898 100644 --- a/core/misc/ajax.js +++ b/core/misc/ajax.js @@ -6,7 +6,7 @@ **/ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } -(function ($, window, Drupal, drupalSettings) { +(function ($, window, Drupal, drupalSettings, loadjs) { Drupal.behaviors.AJAX = { attach: function attach(context, settings) { function loadAjaxBehavior(base) { @@ -402,14 +402,25 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr var elementParents = $(this.element).parents('[data-drupal-selector]').addBack().toArray(); var focusChanged = false; - for (var i in response) { - if (response.hasOwnProperty(i) && 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') { - focusChanged = true; + + var that = this; + Object.keys(response).reduce(function (deferredCommand, i) { + return deferredCommand.then(function () { + var command = response[i].command; + + if (command && that.commands[command]) { + if (command === 'invoke' && response[i].method === 'focus') { + focusChanged = true; + } + + if (command === 'add_js') { + return that.commands[command](that, response[i], status); + } else { + that.commands[command](that, response[i], status); + } } - } - } + }); + }, $.Deferred().resolve()); if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) { var target = false; @@ -586,6 +597,34 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr document.styleSheets[0].addImport(match[1]); } while (match); } + }, + add_js: function add_js(ajax, response) { + var deferred = $.Deferred(); + var scriptsSrc = response.data.map(function (script) { + loadjs(script.src, script.src, { + async: !!script.async, + before: function before(path, scriptEl) { + var selector = 'body'; + if (response.selector) { + selector = response.selector; + } + if (script.defer) { + scriptEl.defer = true; + } + + document.querySelector(selector)[response.method](scriptEl); + + return false; + } + }); + return script.src; + }); + loadjs.ready(scriptsSrc, { + success: function success() { + deferred.resolve(); + } + }); + return deferred.promise(); } }; -})(jQuery, window, Drupal, drupalSettings); \ No newline at end of file +})(jQuery, window, Drupal, drupalSettings, loadjs); \ No newline at end of file diff --git a/core/modules/quickedit/js/quickedit.es6.js b/core/modules/quickedit/js/quickedit.es6.js index 84fee16cdf..f44b1f98f0 100644 --- a/core/modules/quickedit/js/quickedit.es6.js +++ b/core/modules/quickedit/js/quickedit.es6.js @@ -518,8 +518,8 @@ }); // 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) { + const realInsert = Drupal.AjaxCommands.prototype.add_js; + loadEditorsAjax.commands.add_js = function (ajax, response, status) { _.defer(callback); realInsert(ajax, response, status); }; diff --git a/core/modules/quickedit/js/quickedit.js b/core/modules/quickedit/js/quickedit.js index 5c2cd95af0..c057cb427b 100644 --- a/core/modules/quickedit/js/quickedit.js +++ b/core/modules/quickedit/js/quickedit.js @@ -263,8 +263,8 @@ submit: { 'editors[]': missingEditors } }); - var realInsert = Drupal.AjaxCommands.prototype.insert; - loadEditorsAjax.commands.insert = function (ajax, response, status) { + var realInsert = Drupal.AjaxCommands.prototype.add_js; + loadEditorsAjax.commands.add_js = function (ajax, response, status) { _.defer(callback); realInsert(ajax, response, status); }; diff --git a/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php b/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php index 41f26a841f..cc0ba0fde7 100644 --- a/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php +++ b/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php @@ -198,7 +198,7 @@ public function testUserWithPermission() { // First command: settings. $this->assertIdentical('settings', $ajax_commands[0]['command'], 'The first AJAX command is a settings command.'); // Second command: insert libraries into DOM. - $this->assertIdentical('insert', $ajax_commands[1]['command'], 'The second AJAX command is an append command.'); + $this->assertIdentical('add_js', $ajax_commands[1]['command'], 'The second AJAX command is an append command.'); $this->assertTrue(in_array('quickedit/quickedit.inPlaceEditor.form', explode(',', $ajax_commands[0]['settings']['ajaxPageState']['libraries'])), 'The quickedit.inPlaceEditor.form library is loaded.'); // Retrieving the form for this field should result in a 200 response, diff --git a/core/modules/system/src/Tests/Ajax/DialogTest.php b/core/modules/system/src/Tests/Ajax/DialogTest.php index ec947825fe..f9f4edfd9b 100644 --- a/core/modules/system/src/Tests/Ajax/DialogTest.php +++ b/core/modules/system/src/Tests/Ajax/DialogTest.php @@ -148,9 +148,9 @@ public function testDialog() { $this->assertTrue(in_array('core/drupal.dialog.ajax', explode(',', $ajax_result[0]['settings']['ajaxPageState']['libraries'])), 'core/drupal.dialog.ajax library is added to the page.'); $dialog_css_exists = strpos($ajax_result[1]['data'], 'dialog.css') !== FALSE; $this->assertTrue($dialog_css_exists, 'jQuery UI dialog CSS added to the page.'); - $dialog_js_exists = strpos($ajax_result[2]['data'], 'dialog-min.js') !== FALSE; + $dialog_js_exists = strpos(json_encode($ajax_result[2]['data']), 'dialog-min.js') !== FALSE; $this->assertTrue($dialog_js_exists, 'jQuery UI dialog JS added to the page.'); - $dialog_js_exists = strpos($ajax_result[2]['data'], 'dialog.ajax.js') !== FALSE; + $dialog_js_exists = strpos(json_encode($ajax_result[2]['data']), 'dialog.ajax.js') !== FALSE; $this->assertTrue($dialog_js_exists, 'Drupal dialog JS added to the page.'); // Check that the response matches the expected value. diff --git a/core/modules/system/src/Tests/Ajax/FrameworkTest.php b/core/modules/system/src/Tests/Ajax/FrameworkTest.php index 14bae06442..b2e561804c 100644 --- a/core/modules/system/src/Tests/Ajax/FrameworkTest.php +++ b/core/modules/system/src/Tests/Ajax/FrameworkTest.php @@ -4,9 +4,8 @@ use Drupal\Core\Ajax\AddCssCommand; use Drupal\Core\Ajax\AlertCommand; -use Drupal\Core\Ajax\AppendCommand; +use Drupal\Core\Ajax\AddJsCommand; use Drupal\Core\Ajax\HtmlCommand; -use Drupal\Core\Ajax\PrependCommand; use Drupal\Core\Ajax\SettingsCommand; use Drupal\Core\Asset\AttachedAssets; @@ -48,8 +47,8 @@ public function testOrder() { list($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('head', $js_header_render_array, 'insertBefore'); + $expected_commands[3] = new AddJsCommand('body', $js_footer_render_array); $expected_commands[4] = new HtmlCommand('body', 'Hello, world!'); // Load any page with at least one CSS file, at least one JavaScript file @@ -68,8 +67,8 @@ public function testOrder() { $commands = $this->drupalPostAjaxForm(NULL, [], NULL, 'ajax-test/order', [], [], NULL, []); $this->assertCommand(array_slice($commands, 0, 1), $expected_commands[0]->render(), 'Settings command is first.'); $this->assertCommand(array_slice($commands, 1, 1), $expected_commands[1]->render(), 'CSS command is second (and CSS files are ordered correctly).'); - $this->assertCommand(array_slice($commands, 2, 1), $expected_commands[2]->render(), 'Header JS command is third.'); - $this->assertCommand(array_slice($commands, 3, 1), $expected_commands[3]->render(), 'Footer JS command is fourth.'); + $this->assertEqual(array_slice($commands, 2, 1)[0]['data'][0]['src'], $expected_commands[2]->render()['data'][0]['#attributes']['src'], 'Header JS command is third.'); + $this->assertEqual(array_slice($commands, 3, 1)[0]['data'][0]['src'], $expected_commands[3]->render()['data'][0]['#attributes']['src'], 'Footer JS command is fourth.'); $this->assertCommand(array_slice($commands, 4, 1), $expected_commands[4]->render(), 'HTML command is fifth.'); }