Commit 4ecc4ead authored by catch's avatar catch

Issue #2382557 by Wim Leers: Change JS settings into a separate asset type

parent 86ced511
......@@ -52,7 +52,18 @@ drupal:
drupalSettings:
version: VERSION
settings: {}
drupalSettings:
# These placeholder values will be set by system_js_settings_alter().
path:
baseUrl: null
scriptPath: null
pathPrefix: null
currentPath: null
currentPathIsAdmin: null
isFront: null
currentLanguage: null
locale:
pluralDelimiter: null
drupal.active-link:
version: VERSION
......
......@@ -179,18 +179,13 @@ function _batch_progress_page() {
),
),
// Adds JavaScript code and settings for clients where JavaScript is enabled.
'js' => array(
array(
'type' => 'setting',
'data' => array(
'batch' => array(
'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
'initMessage' => $current_set['init_message'],
'uri' => $url,
),
),
),
),
'drupalSettings' => [
'batch' => [
'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
'initMessage' => $current_set['init_message'],
'uri' => $url,
],
],
'library' => array(
'core/drupal.batch',
),
......
......@@ -1456,50 +1456,20 @@ function _drupal_add_js($data = NULL, $options = NULL) {
switch ($options['type']) {
case 'setting':
// If the setting array doesn't exist, add defaults values.
if (!isset($javascript['settings'])) {
$javascript['settings'] = array(
if (!isset($javascript['drupalSettings'])) {
$javascript['drupalSettings'] = array(
'type' => 'setting',
'scope' => 'header',
'group' => JS_SETTING,
'every_page' => TRUE,
'weight' => 0,
'browsers' => array(),
);
// url() generates the script and prefix using hook_url_outbound_alter().
// Instead of running the hook_url_outbound_alter() again here, extract
// them from url().
// @todo Make this less hacky: http://drupal.org/node/1547376.
$request = \Drupal::request();
$scriptPath = $request->getScriptName();
$pathPrefix = '';
$current_query = $request->query->all();
_url('', array('script' => &$scriptPath, 'prefix' => &$pathPrefix));
$current_path = \Drupal::routeMatch()->getRouteName() ? Url::fromRouteMatch(\Drupal::routeMatch())->getInternalPath() : '';
$current_path_is_admin = \Drupal::service('router.admin_context')->isAdminRoute();
$path = array(
'baseUrl' => $request->getBaseUrl() . '/',
'scriptPath' => $scriptPath,
'pathPrefix' => $pathPrefix,
'currentPath' => $current_path,
'currentPathIsAdmin' => $current_path_is_admin,
'isFront' => drupal_is_front_page(),
'currentLanguage' => \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId(),
);
if (!empty($current_query)) {
ksort($current_query);
$path['currentQuery'] = (object) $current_query;
}
$javascript['settings']['data'][] = array(
'path' => $path,
'locale' => array(
'pluralDelimiter' => LOCALE_PLURAL_DELIMITER,
),
'data' => array(),
);
}
// All JavaScript settings are placed in the header of the page with
// the library weight so that inline scripts appear afterwards.
$javascript['settings']['data'][] = $data;
$javascript['drupalSettings']['data'] = NestedArray::mergeDeepArray([$javascript['drupalSettings']['data'], $data], TRUE);
break;
case 'inline':
......@@ -1603,21 +1573,21 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
uasort($items, 'drupal_sort_css_js');
// Don't add settings if there is no other JavaScript on the page, unless
// this is an AJAX request.
if (!empty($items['settings']) || $is_ajax) {
if (!empty($items['drupalSettings']) || $is_ajax) {
$theme_key = \Drupal::theme()->getActiveTheme()->getName();
// Provide the page with information about the theme that's used, so that
// a later AJAX request can be rendered using the same theme.
// @see \Drupal\Core\Theme\AjaxBasePageNegotiator
$setting['ajaxPageState']['theme'] = $theme_key;
$ajaxPageState['theme'] = $theme_key;
// Checks that the DB is available before filling theme_token.
if (!defined('MAINTENANCE_MODE')) {
$setting['ajaxPageState']['theme_token'] = \Drupal::csrfToken()->get($theme_key);
$ajaxPageState['theme_token'] = \Drupal::csrfToken()->get($theme_key);
}
// Provide the page with information about the individual JavaScript files
// used, information not otherwise available when aggregation is enabled.
$setting['ajaxPageState']['js'] = array_fill_keys(array_keys($javascript), 1);
unset($setting['ajaxPageState']['js']['settings']);
$ajaxPageState['js'] = array_fill_keys(array_keys($javascript), 1);
unset($ajaxPageState['js']['drupalSettings']);
// Provide the page with information about the individual CSS files used,
// information not otherwise available when CSS aggregation is enabled.
......@@ -1631,23 +1601,35 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
$css = _drupal_add_css();
if (!empty($css)) {
// Cast the array to an object to be on the safe side even if not empty.
$setting['ajaxPageState']['css'] = (object) array_fill_keys(array_keys($css), 1);
$ajaxPageState['css'] = (object) array_fill_keys(array_keys($css), 1);
}
_drupal_add_js($setting, 'setting');
_drupal_add_js(['ajaxPageState' => $ajaxPageState], 'setting');
// If we're outputting the header scope, then this might be the final time
// that drupal_get_js() is running, so add the settings to this output as well
// as to the _drupal_add_js() cache. If $items['settings'] doesn't exist, it's
// because drupal_get_js() was intentionally passed a $javascript argument
// stripped of settings, potentially in order to override how settings get
// output, so in this case, do not add the setting to this output.
if ($scope == 'header' && isset($items['settings'])) {
$items['settings']['data'][] = $setting;
// as to the _drupal_add_js() cache. If $items['drupalSettings'] doesn't
// exist, it's because drupal_get_js() was intentionally passed a
// $javascript argument stripped of settings, potentially in order to
// override how settings get output, so in this case, do not add the
// setting to this output.
if ($scope == 'header' && isset($items['drupalSettings'])) {
$items['drupalSettings']['data']['ajaxPageState'] = $ajaxPageState;
}
}
}
// Process the 'drupalSettings' JavaScript asset, if any.
if (!empty($items['drupalSettings'])) {
$settings = $items['drupalSettings']['data'];
// Allow modules and themes to alter the JavaScript settings.
\Drupal::moduleHandler()->alter('js_settings', $settings);
\Drupal::theme()->alter('js_settings', $settings);
$items['drupalSettings']['data'] = $settings;
}
// Render the HTML needed to load the JavaScript.
$elements = array(
'#type' => 'scripts',
......@@ -1658,30 +1640,25 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
}
/**
* Merges an array of settings arrays into a single settings array.
* Merges two #attached arrays.
*
* This function merges the items in the same way that
* 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
*
* would. This means integer indices are preserved just like string indices are,
* 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['#attached']['js'][] = array(
* 'type' => 'setting',
* 'data' => array('foo' => array('a', 'b', 'c')),
* );
* $page['a']['#attached']['drupalSettings']['foo'] = ['a', 'b', 'c'];
* }
* function module2_page_attachments(&$page) {
* $page['#attached']['js'][] = array(
* 'type' => 'setting',
* 'data' => array('foo' => array('d')),
* );
* $page['#attached']['drupalSettings']['foo'] = ['d'];
* }
* // When the page is rendered after the above code, and the browser runs the
* // resulting <SCRIPT> tags, the value of drupalSettings.foo is
......@@ -1690,32 +1667,12 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
*
* By following jQuery.extend() merge logic rather than common PHP array merge
* logic, the following are ensured:
* - _drupal_add_js() is idempotent: calling it twice with the same parameters
* does not change the output sent to the browser.
* - Attaching JavaScript settings is idempotent: attaching the same settings
* twice does not change the output sent to the browser.
* - If pieces of the page are rendered in separate PHP requests and the
* returned settings are merged by JavaScript, the resulting settings are the
* same as if rendered in one PHP request and merged by PHP.
*
* @param $settings_items
* An array of settings arrays, as returned by:
* @code
* $js = _drupal_add_js();
* $settings_items = $js['settings']['data'];
* @endcode
*
* @return
* A merged $settings array, suitable for JSON encoding and returning to the
* browser.
*
* @see _drupal_add_js()
*/
function drupal_merge_js_settings($settings_items) {
return NestedArray::mergeDeepArray($settings_items, TRUE);
}
/**
* Merges two #attached arrays.
*
* @param array $a
* An #attached array.
* @param array $b
......@@ -1725,6 +1682,12 @@ function drupal_merge_js_settings($settings_items) {
* The merged #attached array.
*/
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);
}
......@@ -1820,6 +1783,13 @@ function drupal_process_attached($elements, $dependency_check = FALSE) {
unset($elements['#attached'][$type]);
}
// Convert every JavaScript settings asset into a regular JavaScript asset.
// @todo Clean this up in https://www.drupal.org/node/2382533
if (!empty($elements['#attached']['drupalSettings'])) {
_drupal_add_js($elements['#attached']['drupalSettings'], ['type' => 'setting']);
unset($elements['#attached']['drupalSettings']);
}
// Add additional types of attachments specified in the render() structure.
// Libraries, JavaScript and CSS have been added already, as they require
// special handling.
......@@ -2027,6 +1997,9 @@ function _drupal_add_library($library_name, $every_page = NULL) {
'js' => $library['js'],
'css' => $library['css'],
);
if (isset($library['drupalSettings'])) {
$elements['#attached']['drupalSettings'] = $library['drupalSettings'];
}
foreach (array('js', 'css') as $type) {
foreach ($elements['#attached'][$type] as $data => $options) {
// Set the every_page flag if one was passed.
......@@ -2186,7 +2159,7 @@ function drupal_attach_tabledrag(&$element, array $options) {
// If a subgroup or source isn't set, assume it is the same as the group.
$target = isset($options['subgroup']) ? $options['subgroup'] : $group;
$source = isset($options['source']) ? $options['source'] : $target;
$settings['tableDrag'][$options['table_id']][$group][$tabledrag_id] = array(
$element['#attached']['drupalSettings'][$options['table_id']][$group][$tabledrag_id] = array(
'target' => $target,
'source' => $source,
'relationship' => $options['relationship'],
......@@ -2196,7 +2169,6 @@ function drupal_attach_tabledrag(&$element, array $options) {
);
$element['#attached']['library'][] = 'core/drupal.tabledrag';
$element['#attached']['js'][] = array('data' => $settings, 'type' => 'setting');
}
/**
......
......@@ -129,8 +129,8 @@ protected function ajaxRender(Request $request) {
// HTML in the page. We pass TRUE as the $skip_alter argument to prevent the
// data from being altered again, as we already altered it above. Settings
// are handled separately, afterwards.
if (isset($items['js']['settings'])) {
unset($items['js']['settings']);
if (isset($items['js']['drupalSettings'])) {
unset($items['js']['drupalSettings']);
}
$styles = drupal_get_css($items['css'], TRUE);
$scripts_footer = drupal_get_js('footer', $items['js'], TRUE, TRUE);
......@@ -153,17 +153,15 @@ protected function ajaxRender(Request $request) {
// Prepend a command to merge changes and additions to drupalSettings.
$scripts = _drupal_add_js();
if (!empty($scripts['settings'])) {
$settings = drupal_merge_js_settings($scripts['settings']['data']);
if (!empty($scripts['drupalSettings'])) {
$settings = $scripts['drupalSettings']['data'];
// During Ajax requests basic path-specific settings are excluded from
// new drupalSettings values. The original page where this request comes
// from already has the right values for the keys below. An Ajax request
// would update them with values for the Ajax request and incorrectly
// override the page's values.
// from already has the right values. An Ajax request would update them
// with values for the Ajax request and incorrectly override the page's
// values.
// @see _drupal_add_js()
foreach (array('basePath', 'currentPath', 'scriptPath', 'pathPrefix') as $item) {
unset($settings[$item]);
}
unset($settings['path']);
$this->addCommand(new SettingsCommand($settings, TRUE), TRUE);
}
......
......@@ -68,7 +68,7 @@ public function render(array $js_assets) {
switch ($js_asset['type']) {
case 'setting':
$element['#value_prefix'] = $embed_prefix;
$element['#value'] = 'var drupalSettings = ' . Json::encode(drupal_merge_js_settings($js_asset['data'])) . ";";
$element['#value'] = 'var drupalSettings = ' . Json::encode($js_asset['data']) . ";";
$element['#value_suffix'] = $embed_suffix;
break;
......
......@@ -84,7 +84,7 @@ public function buildByExtension($extension) {
}
foreach ($libraries as $id => &$library) {
if (!isset($library['js']) && !isset($library['css']) && !isset($library['settings'])) {
if (!isset($library['js']) && !isset($library['css']) && !isset($library['drupalSettings'])) {
throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for '%s' in %s", $id, $library_file));
}
$library += array('dependencies' => array(), 'js' => array(), 'css' => array());
......@@ -198,14 +198,6 @@ public function buildByExtension($extension) {
}
}
// @todo Introduce drupal_add_settings().
if (isset($library['settings'])) {
$library['js'][] = array(
'type' => 'setting',
'data' => $library['settings'],
);
unset($library['settings']);
}
// @todo Convert all uses of #attached[library][]=array('provider','name')
// into #attached[library][]='provider/name' and remove this.
foreach ($library['dependencies'] as $i => $dependency) {
......
......@@ -123,8 +123,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
$form['#attached']['library'][] = 'core/drupal.timezone';
// We add these strings as settings because JavaScript translation does not
// work during installation.
$js = array('copyFieldValue' => array('edit-site-mail' => array('edit-account-mail')));
$form['#attached']['js'][] = array('data' => $js, 'type' => 'setting');
$form['#attached']['drupalSettings']['copyFieldValue']['edit-site-mail'] = ['edit-account-mail'];
// Cache a fully-built schema. This is necessary for any invocation of
// index.php because: (1) setting cache table entries requires schema
......
......@@ -163,17 +163,9 @@ public static function processMachineName(&$element, FormStateInterface $form_st
NestedArray::setValue($form_state->getCompleteForm(), $parents, $source['#field_suffix']);
}
$js_settings = array(
'type' => 'setting',
'data' => array(
'machineName' => array(
'#' . $source['#id'] => $element['#machine_name'],
),
'langcode' => $language->getId(),
),
);
$element['#attached']['library'][] = 'core/drupal.machine-name';
$element['#attached']['js'][] = $js_settings;
$element['#attached']['drupalSettings']['machineName']['#' . $source['#id']] = $element['#machine_name'];
$element['#attached']['drupalSettings']['langcode'] = $language->getId();
return $element;
}
......
......@@ -296,10 +296,7 @@ public static function preRenderAjaxForm($element) {
unset($settings['progress']['path']);
}
$element['#attached']['js'][] = array(
'type' => 'setting',
'data' => array('ajax' => array($element['#id'] => $settings)),
);
$element['#attached']['drupalSettings']['ajax'][$element['#id']] = $settings;
// Indicate that Ajax processing was successful.
$element['#ajax_processed'] = TRUE;
......
......@@ -139,10 +139,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
$placement = FALSE;
if ($this->request->query->has('block-placement')) {
$placement = $this->request->query->get('block-placement');
$form['#attached']['js'][] = array(
'type' => 'setting',
'data' => array('blockPlacement' => $placement),
);
$form['#attached']['drupalSettings']['blockPlacement'] = $placement;
}
$entities = $this->load();
$form['#theme'] = array('block_list');
......
......@@ -59,17 +59,14 @@ public function demo($theme) {
'#title' => $this->themeHandler->getName($theme),
'#type' => 'page',
'#attached' => array(
'js' => array(
array(
// The block demonstration page is not marked as an administrative
// page by \Drupal::service('router.admin_context')->isAdminRoute()
// function in order to use the frontend theme. Since JavaScript
// relies on a proper separation of admin pages, it needs to know
// this is an actual administrative page.
'data' => array('path' => array('currentPathIsAdmin' => TRUE)),
'type' => 'setting',
)
),
'drupalSettings' => [
// The block demonstration page is not marked as an administrative
// page by \Drupal::service('router.admin_context')->isAdminRoute()
// function in order to use the frontend theme. Since JavaScript
// relies on a proper separation of admin pages, it needs to know this
// is an actual administrative page.
'path' => ['currentPathIsAdmin' => TRUE],
],
'library' => array(
'block/drupal.block.admin',
),
......
......@@ -140,14 +140,11 @@ public function settingsForm(array $form, FormStateInterface $form_state, Editor
'#type' => 'container',
'#attached' => array(
'library' => array('ckeditor/drupal.ckeditor.admin'),
'js' => array(
array(
'type' => 'setting',
'data' => array('ckeditor' => array(
'toolbarAdmin' => drupal_render($ckeditor_settings_toolbar),
)),
)
),
'drupalSettings' => [
'ckeditor' => [
'toolbarAdmin' => drupal_render($ckeditor_settings_toolbar),
],
],
),
'#attributes' => array('class' => array('ckeditor-toolbar-configuration')),
);
......@@ -218,14 +215,7 @@ public function settingsForm(array $form, FormStateInterface $form_state, Editor
$form['hidden_ckeditor'] = array(
'#markup' => '<div id="ckeditor-hidden" class="hidden"></div>',
'#attached' => array(
'js' => array(
array(
'type' => 'setting',
'data' => array('ckeditor' => array(
'hiddenCKEditorConfig' => $config,
)),
),
),
'drupalSettings' => ['ckeditor' => ['hiddenCKEditorConfig' => $config]],
),
);
......
......@@ -213,18 +213,13 @@ function color_scheme_form($complete_form, FormStateInterface $form_state, $them
'color/admin',
),
// Add custom JavaScript.
'js' => array(
array(
'data' => array(
'color' => array(
'reference' => color_get_palette($theme, TRUE),
'schemes' => $schemes,
),
'gradients' => $info['gradients'],
),
'type' => 'setting',
),
),
'drupalSettings' => [
'color' => [
'reference' => color_get_palette($theme, TRUE),
'schemes' => $schemes,
],
'gradients' => $info['gradients'],
],
),
);
......
......@@ -364,23 +364,16 @@ public static function attachNewCommentsLinkMetadata(array $element, array $cont
$query = $page_number ? array('page' => $page_number) : NULL;
// Attach metadata.
$element['#attached']['js'][] = array(
'type' => 'setting',
'data' => array(
'comment' => array(
'newCommentsLinks' => array(
$context['entity_type'] => array(
$context['field_name'] => array(
$context['entity_id'] => array(
'new_comment_count' => (int)$new,
'first_new_comment_link' => \Drupal::urlGenerator()->generateFromPath('node/' . $entity->id(), array('query' => $query, 'fragment' => 'new')),
)
)
),
)
),
),
);
$element['#attached']['drupalSettings']['comment']['newCommentsLinks'] = [
$context['entity_type'] => [
$context['field_name'] => [
$context['entity_id'] => [
'new_comment_count' => (int)$new,
'first_new_comment_link' => \Drupal::urlGenerator()->generateFromPath('node/' . $entity->id(), ['query' => $query, 'fragment' => 'new']),
],
],
],
];
return $element;
}
......
......@@ -52,9 +52,9 @@ function content_translation_field_sync_widget(FieldDefinitionInterface $field)
'library' => array(
'content_translation/drupal.content_translation.admin',
),
'js' => array(
array('data' => array('contentTranslationDependentOptions' => $settings), 'type' => 'setting'),
),
'drupalSettings' => [
'contentTranslationDependentOptions' => $settings,
],
),
);
}
......@@ -126,7 +126,7 @@ function _content_translation_form_language_content_settings_form_alter(array &$
}
$settings = array('dependent_selectors' => $dependent_options_settings);
$form['#attached']['js'][] = array('data' => array('contentTranslationDependentOptions' => $settings), 'type' => 'setting');
$form['#attached']['drupalSettings']['contentTranslationDependentOptions'] = $settings;
$form['#validate'][] = 'content_translation_form_language_content_settings_validate';
$form['#submit'][] = 'content_translation_form_language_content_settings_submit';
}
......
......@@ -96,10 +96,7 @@ public function getAttachments(array $format_ids) {
return array();
}
$attachments['js'][] = array(
'type' => 'setting',
'data' => $settings,
);
$attachments['drupalSettings'] = $settings;
return $attachments;
}
......
......@@ -90,27 +90,26 @@ public function testManager() {
'library' => array(
0 => 'editor_test/unicorn',
</