diff --git a/core/misc/ajax.js b/core/misc/ajax.js index fefe9f3031..46d65bdafd 100644 --- a/core/misc/ajax.js +++ b/core/misc/ajax.js @@ -1027,8 +1027,6 @@ // $(response.data) as new HTML rather than a CSS selector. Also, if // response.data contains top-level text nodes, they get lost with either // $(response.data) or $('
').replaceWith(response.data). - var $new_content_wrapped = $('
').html(response.data); - var $new_content = $new_content_wrapped.contents(); // For legacy reasons, the effects processing code assumes that // $new_content consists of a single top-level element. Also, it has not @@ -1041,10 +1039,44 @@ // of a single top-level element, and only use the container
created // above when it doesn't. For more information, please see // https://www.drupal.org/node/736066. - if ($new_content.length !== 1 || $new_content.get(0).nodeType !== 1) { - $new_content = $new_content_wrapped; + + var $responseDataWrapped = $('
').html(response.data); + var $new_content = $('
'); + var elementsReturned = $responseDataWrapped.contents().length; + + var createWrapper = function () { + return $('
'); + }; + + var intermediateWrapper = null; + $responseDataWrapped.contents().each(function (index, value) { + if (intermediateWrapper && value.nodeType !== 1) { + intermediateWrapper.append(value); + } + if (!intermediateWrapper && value.nodeType !== 1) { + if ($.trim(value.nodeValue).length > 0) { + intermediateWrapper = createWrapper(); + intermediateWrapper.append(value); + } + } + if (!intermediateWrapper && value.nodeType === 1) { + $new_content.append(value); + intermediateWrapper = null; + } + if (intermediateWrapper && value.nodeType === 1) { + $new_content.append(intermediateWrapper, value); + intermediateWrapper = null; + } + }); + + // If we only had one element returned, and not an Node.ELEMENT_NODE + // we need to add it to $new_content + if (elementsReturned === 1) { + $new_content.append(intermediateWrapper); } + $new_content = $new_content.children(); + // If removing content from the wrapper, detach behaviors first. switch (method) { case 'html': diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml index f1c73064bd..772a05f734 100644 --- a/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml @@ -1,3 +1,8 @@ +ajax_insert: + js: + js/insert-ajax.js: {} + dependencies: + - core/drupal.ajax order: drupalSettings: ajax: test diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml index e8d06c0a9f..89191a1c63 100644 --- a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml @@ -6,6 +6,14 @@ ajax_test.dialog_contents: requirements: _access: 'TRUE' +ajax_test.ajax_render_types: + path: '/ajax-test/dialog-contents-types/{type}' + defaults: + _title: 'AJAX Dialog contents routing' + _controller: '\Drupal\ajax_test\Controller\AjaxTestController::renderTypes' + requirements: + _access: 'TRUE' + ajax_test.dialog_form: path: '/ajax-test/dialog-form' defaults: @@ -21,6 +29,13 @@ ajax_test.dialog: requirements: _access: 'TRUE' +ajax_test.insert_links: + path: '/ajax-test/insert' + defaults: + _controller: '\Drupal\ajax_test\Controller\AjaxTestController::insertLinks' + requirements: + _access: 'TRUE' + ajax_test.dialog_close: path: '/ajax-test/dialog-close' defaults: diff --git a/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js b/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js new file mode 100644 index 0000000000..b2b9277393 --- /dev/null +++ b/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js @@ -0,0 +1,25 @@ +/** + * @file + * Provides method to test ajax requests. + */ + +(function ($, window, Drupal, drupalSettings) { + 'use strict'; + + Drupal.behaviors.insertTest = { + attach: function (context, settings) { + $('.ajax-insert').once('ajax-insert').on('click', function (event) { + event.preventDefault(); + var ajaxSettings = { + url: event.currentTarget.getAttribute('href'), + wrapper: 'ajax-target', + base: false, + element: false, + method: 'html' + }; + var myAjaxObject = Drupal.ajax(ajaxSettings); + myAjaxObject.execute(); + }); + } + }; +})(jQuery, window, Drupal, drupalSettings); diff --git a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php index 63cc0abf16..6eb44410b6 100644 --- a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php +++ b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php @@ -43,6 +43,79 @@ public static function dialogContents() { } /** + * Example content for testing whether response should be wrapped in div. + * + * @param string $type + * Type of response. + * + * @return array + * Renderable array of AJAX response contents. + */ + public static function renderTypes($type) { + switch ($type) { + case 'pre-wrapped': + $markup = '
' . $type . '
'; + break; + + case 'pre-wrapped-leading-whitespace': + $markup = '
' . $type . '
'; + break; + + case 'not-wrapped': + $markup = $type; + break; + + case 'mixed': + $markup = ' foo foo bar

some string

additional wrapped strings,

final string

'; + break; + } + + $content = [ + '#title' => 'AJAX Dialog & contents', + 'content' => [ + '#type' => 'inline_template', + '#template' => !empty($markup) ? $markup : '', + ], + ]; + + return $content; + } + + /** + * Returns a render array of links that directly Drupal.ajax(). + */ + public function insertLinks() { + $types = [ + 'pre-wrapped', + 'pre-wrapped-leading-whitespace', + 'not-wrapped', + 'mixed', + ]; + + $build['links'] = [ + 'ajax_target' => [ + '#markup' => '
Target
', + ], + 'links' => [ + '#theme' => 'links', + '#attached' => ['library' => ['ajax_test/ajax_insert']], + ], + ]; + + foreach ($types as $type) { + $build['links']['links']['#links'][$type] = [ + 'title' => "Link $type", + 'url' => Url::fromRoute('ajax_test.ajax_render_types', ['type' => $type]), + 'attributes' => [ + 'class' => ['ajax-insert'], + ], + ]; + } + + return $build; + } + + /** * Returns a render array that will be rendered by AjaxRenderer. * * Verifies that the response incorporates JavaScript settings generated diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php index e05940537c..386133cf34 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php @@ -82,4 +82,35 @@ public function testDrupalSettingsCachingRegression() { $this->assertNotContains($fake_library, $libraries); } + /** + * Tests that various AJAX responses are correctly wrapped. + */ + public function testWrap() { + $assert = $this->assertSession(); + $this->drupalGet('ajax-test/insert'); + + // Test that no additional wrapper is added when inserting already wrapped + // response data. + $this->clickLink('Link pre-wrapped'); + $assert->assertWaitOnAjaxRequest(); + $assert->responseContains('
pre-wrapped
'); + + // Test that no additional empty leading div is added when the return + // value had a leading space. + $this->clickLink('Link pre-wrapped-leading-whitespace'); + $assert->assertWaitOnAjaxRequest(); + $assert->responseContains('
pre-wrapped-leading-whitespace
'); + + // Test that unwrapped response data (text node) is inserted wrapped. + $this->clickLink('Link not-wrapped'); + $assert->assertWaitOnAjaxRequest(); + $assert->responseContains('
not-wrapped
'); + + // Test that wrappend and unwrapped response data is inserted correctly. + $this->clickLink('Link mixed'); + $assert->assertWaitOnAjaxRequest(); + $this->createScreenshot('/tmp/drupal/na4.jpg'); + $assert->responseContains('
foo foo bar

some string

additional wrapped strings,

final string

'); + } + }