diff --git a/core/includes/common.inc b/core/includes/common.inc index bfcbce6361fa..c6203598c3d3 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -20,6 +20,7 @@ use Drupal\Component\Utility\SortArray; use Drupal\Component\Utility\String; use Drupal\Component\Utility\Tags; use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Asset\AttachedAssets; use Drupal\Core\Cache\Cache; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Site\Settings; @@ -32,6 +33,7 @@ use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Routing\GeneratorNotInitializedException; use Drupal\Core\Template\Attribute; use Drupal\Core\Render\Element; +use Drupal\Core\Render\Renderer; use Drupal\Core\Session\AnonymousUserSession; /** @@ -852,7 +854,7 @@ function drupal_js_defaults($data = NULL) { * Merges two #attached arrays. * * The values under the 'drupalSettings' key are merged in a special way, to - * match the behavior of + * match the behavior of: * * @code * jQuery.extend(true, {}, $settings_items[0], $settings_items[1], ...) @@ -889,15 +891,12 @@ function drupal_js_defaults($data = NULL) { * * @return array * The merged #attached array. + * + * @deprecated To be removed in Drupal 8.0.x. Use + * \Drupal\Core\Render\Renderer::mergeAttachments() instead. */ function drupal_merge_attached(array $a, array $b) { - // If both #attached arrays contain drupalSettings, then merge them correctly; - // adding the same settings multiple times needs to behave idempotently. - if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) { - $a['drupalSettings'] = NestedArray::mergeDeepArray([$a['drupalSettings'], $b['drupalSettings']], TRUE); - unset($b['drupalSettings']); - } - return NestedArray::mergeDeep($a, $b); + return Renderer::mergeAttachments($a, $b); } /** diff --git a/core/lib/Drupal/Core/Ajax/AfterCommand.php b/core/lib/Drupal/Core/Ajax/AfterCommand.php index 8674c73ff31f..95227830031f 100644 --- a/core/lib/Drupal/Core/Ajax/AfterCommand.php +++ b/core/lib/Drupal/Core/Ajax/AfterCommand.php @@ -34,7 +34,7 @@ class AfterCommand extends InsertCommand { 'command' => 'insert', 'method' => 'after', 'selector' => $this->selector, - 'data' => $this->html, + 'data' => $this->getRenderedContent(), 'settings' => $this->settings, ); } diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php index 13b827bd8454..54f8d72b4e26 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Ajax; use Drupal\Core\Asset\AttachedAssets; +use Drupal\Core\Render\Renderer; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -71,6 +72,15 @@ class AjaxResponse extends JsonResponse { else { $this->commands[] = $command->render(); } + if ($command instanceof CommandWithAttachedAssetsInterface) { + $assets = $command->getAttachedAssets(); + $attachments = [ + 'library' => $assets->getLibraries(), + 'drupalSettings' => $assets->getSettings(), + ]; + $attachments = Renderer::mergeAttachments($this->attachments, $attachments); + $this->setAttachments($attachments); + } return $this; } diff --git a/core/lib/Drupal/Core/Ajax/AppendCommand.php b/core/lib/Drupal/Core/Ajax/AppendCommand.php index d758720ce6a2..bf5636263e38 100644 --- a/core/lib/Drupal/Core/Ajax/AppendCommand.php +++ b/core/lib/Drupal/Core/Ajax/AppendCommand.php @@ -34,7 +34,7 @@ class AppendCommand extends InsertCommand { 'command' => 'insert', 'method' => 'append', 'selector' => $this->selector, - 'data' => $this->html, + 'data' => $this->getRenderedContent(), 'settings' => $this->settings, ); } diff --git a/core/lib/Drupal/Core/Ajax/BeforeCommand.php b/core/lib/Drupal/Core/Ajax/BeforeCommand.php index 7e3e2eab1def..8a97a88bdf25 100644 --- a/core/lib/Drupal/Core/Ajax/BeforeCommand.php +++ b/core/lib/Drupal/Core/Ajax/BeforeCommand.php @@ -34,7 +34,7 @@ class BeforeCommand extends InsertCommand { 'command' => 'insert', 'method' => 'before', 'selector' => $this->selector, - 'data' => $this->html, + 'data' => $this->getRenderedContent(), 'settings' => $this->settings, ); } diff --git a/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsInterface.php b/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsInterface.php new file mode 100644 index 000000000000..f55ce6cd6893 --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsInterface.php @@ -0,0 +1,28 @@ +attachedAssets = new AttachedAssets(); + if (is_array($this->content)) { + $html = \Drupal::service('renderer')->render($this->content); + $this->attachedAssets = AttachedAssets::createFromRenderArray($this->content); + return $html; + } + else { + return $this->content; + } + } + + /** + * Gets the attached assets. + * + * @return \Drupal\Core\Asset\AttachedAssets|null + * The attached assets for this command. + */ + public function getAttachedAssets() { + return $this->attachedAssets; + } + +} diff --git a/core/lib/Drupal/Core/Ajax/HtmlCommand.php b/core/lib/Drupal/Core/Ajax/HtmlCommand.php index b9c956844f03..82de657f3b84 100644 --- a/core/lib/Drupal/Core/Ajax/HtmlCommand.php +++ b/core/lib/Drupal/Core/Ajax/HtmlCommand.php @@ -34,7 +34,7 @@ class HtmlCommand extends InsertCommand { 'command' => 'insert', 'method' => 'html', 'selector' => $this->selector, - 'data' => $this->html, + 'data' => $this->getRenderedContent(), 'settings' => $this->settings, ); } diff --git a/core/lib/Drupal/Core/Ajax/InsertCommand.php b/core/lib/Drupal/Core/Ajax/InsertCommand.php index d1f0133f8faf..a0b10dbf0aec 100644 --- a/core/lib/Drupal/Core/Ajax/InsertCommand.php +++ b/core/lib/Drupal/Core/Ajax/InsertCommand.php @@ -21,7 +21,9 @@ use Drupal\Core\Ajax\CommandInterface; * * @ingroup ajax */ -class InsertCommand implements CommandInterface { +class InsertCommand implements CommandInterface, CommandWithAttachedAssetsInterface { + + use CommandWithAttachedAssetsTrait; /** * A CSS selector string. @@ -34,11 +36,13 @@ class InsertCommand implements CommandInterface { protected $selector; /** - * The HTML content that will replace the matched element(s). + * The content for the matched element(s). * - * @var string + * Either a render array or an HTML string. + * + * @var string|array */ - protected $html; + protected $content; /** * A settings array to be passed to any any attached JavaScript behavior. @@ -52,14 +56,15 @@ class InsertCommand implements CommandInterface { * * @param string $selector * A CSS selector. - * @param string $html - * String of HTML that will replace the matched element(s). + * @param string|array $content + * The content that will be inserted in the matched element(s), either a + * render array or an HTML string. * @param array $settings * An array of JavaScript settings to be passed to any attached behaviors. */ - public function __construct($selector, $html, array $settings = NULL) { + public function __construct($selector, $content, array $settings = NULL) { $this->selector = $selector; - $this->html = $html; + $this->content = $content; $this->settings = $settings; } @@ -72,7 +77,7 @@ class InsertCommand implements CommandInterface { 'command' => 'insert', 'method' => NULL, 'selector' => $this->selector, - 'data' => $this->html, + 'data' => $this->getRenderedContent(), 'settings' => $this->settings, ); } diff --git a/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php index 9ee09cc8c991..d373cf8feeea 100644 --- a/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php +++ b/core/lib/Drupal/Core/Ajax/OpenDialogCommand.php @@ -14,7 +14,9 @@ use Drupal\Core\Ajax\CommandInterface; * * @ingroup ajax */ -class OpenDialogCommand implements CommandInterface { +class OpenDialogCommand implements CommandInterface, CommandWithAttachedAssetsInterface { + + use CommandWithAttachedAssetsTrait; /** * The selector of the dialog. @@ -31,11 +33,13 @@ class OpenDialogCommand implements CommandInterface { protected $title; /** - * HTML content that will placed in the dialog. + * The content for the dialog. * - * @var string + * Either a render array or an HTML string. + * + * @var string|array */ - protected $html; + protected $content; /** * Stores dialog-specific options passed directly to jQuery UI dialogs. Any @@ -60,8 +64,9 @@ class OpenDialogCommand implements CommandInterface { * The selector of the dialog. * @param string $title * The title of the dialog. - * @param string $html - * HTML that will be placed in the dialog. + * @param string|array $content + * The content that will be placed in the dialog, either a render array + * or an HTML string. * @param array $dialog_options * (optional) Options to be passed to the dialog implementation. Any * jQuery UI option can be used. See http://api.jqueryui.com/dialog. @@ -70,10 +75,10 @@ class OpenDialogCommand implements CommandInterface { * on the content of the dialog. If left empty, the settings will be * populated automatically from the current request. */ - public function __construct($selector, $title, $html, array $dialog_options = array(), $settings = NULL) { + public function __construct($selector, $title, $content, array $dialog_options = array(), $settings = NULL) { $dialog_options += array('title' => $title); $this->selector = $selector; - $this->html = $html; + $this->content = $content; $this->dialogOptions = $dialog_options; $this->settings = $settings; } @@ -131,7 +136,7 @@ class OpenDialogCommand implements CommandInterface { 'command' => 'openDialog', 'selector' => $this->selector, 'settings' => $this->settings, - 'data' => $this->html, + 'data' => $this->getRenderedContent(), 'dialogOptions' => $this->dialogOptions, ); } diff --git a/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php index ee785c35f718..e9b2c4fed954 100644 --- a/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php +++ b/core/lib/Drupal/Core/Ajax/OpenModalDialogCommand.php @@ -25,8 +25,9 @@ class OpenModalDialogCommand extends OpenDialogCommand { * * @param string $title * The title of the dialog. - * @param string $html - * HTML that will be placed in the dialog. + * @param string|array $content + * The content that will be placed in the dialog, either a render array + * or an HTML string. * @param array $dialog_options * (optional) Settings to be passed to the dialog implementation. Any * jQuery UI option can be used. See http://api.jqueryui.com/dialog. @@ -35,8 +36,8 @@ class OpenModalDialogCommand extends OpenDialogCommand { * on the content of the dialog. If left empty, the settings will be * populated automatically from the current request. */ - public function __construct($title, $html, array $dialog_options = array(), $settings = NULL) { + public function __construct($title, $content, array $dialog_options = array(), $settings = NULL) { $dialog_options['modal'] = TRUE; - parent::__construct('#drupal-modal', $title, $html, $dialog_options, $settings); + parent::__construct('#drupal-modal', $title, $content, $dialog_options, $settings); } } diff --git a/core/lib/Drupal/Core/Ajax/PrependCommand.php b/core/lib/Drupal/Core/Ajax/PrependCommand.php index d97719778bfe..6409a6a8dc5f 100644 --- a/core/lib/Drupal/Core/Ajax/PrependCommand.php +++ b/core/lib/Drupal/Core/Ajax/PrependCommand.php @@ -34,7 +34,7 @@ class PrependCommand extends InsertCommand { 'command' => 'insert', 'method' => 'prepend', 'selector' => $this->selector, - 'data' => $this->html, + 'data' => $this->getRenderedContent(), 'settings' => $this->settings, ); } diff --git a/core/lib/Drupal/Core/Ajax/ReplaceCommand.php b/core/lib/Drupal/Core/Ajax/ReplaceCommand.php index fb2b9e50b313..91514a314c0e 100644 --- a/core/lib/Drupal/Core/Ajax/ReplaceCommand.php +++ b/core/lib/Drupal/Core/Ajax/ReplaceCommand.php @@ -35,7 +35,7 @@ class ReplaceCommand extends InsertCommand { 'command' => 'insert', 'method' => 'replaceWith', 'selector' => $this->selector, - 'data' => $this->html, + 'data' => $this->getRenderedContent(), 'settings' => $this->settings, ); } diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 357ee6a03943..69e99a0acf83 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Render; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheContexts; use Drupal\Core\Cache\CacheFactoryInterface; @@ -582,4 +583,18 @@ class Renderer implements RendererInterface { return $a; } + /** + * {@inheritdoc} + */ + public static function mergeAttachments(array $a, array $b) { + // If both #attached arrays contain drupalSettings, then merge them + // correctly; adding the same settings multiple times needs to behave + // idempotently. + if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) { + $a['drupalSettings'] = NestedArray::mergeDeepArray([$a['drupalSettings'], $b['drupalSettings']], TRUE); + unset($b['drupalSettings']); + } + return NestedArray::mergeDeep($a, $b); + } + } diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index 3625e6638532..418298b23f3e 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -304,4 +304,48 @@ interface RendererInterface { */ public static function mergeBubbleableMetadata(array $a, array $b); + /** + * Merges two attachments arrays (which live under the '#attached' key). + * + * The values under the 'drupalSettings' key are merged in a special way, to + * match the behavior of: + * + * @code + * jQuery.extend(true, {}, $settings_items[0], $settings_items[1], ...) + * @endcode + * + * This means integer indices are preserved just like string indices are, + * rather than re-indexed as is common in PHP array merging. + * + * Example: + * @code + * function module1_page_attachments(&$page) { + * $page['a']['#attached']['drupalSettings']['foo'] = ['a', 'b', 'c']; + * } + * function module2_page_attachments(&$page) { + * $page['#attached']['drupalSettings']['foo'] = ['d']; + * } + * // When the page is rendered after the above code, and the browser runs the + * // resulting