Commit 77c613e3 authored by catch's avatar catch

Issue #2382533 by Wim Leers: Attach assets only via the asset library system

parent cd0cb0d7
......@@ -871,9 +871,7 @@ function _drupal_add_html_head_link($attributes, $header = FALSE) {
* If CSS aggregation/compression is enabled, all cascading style sheets added
* with $options['preprocess'] set to TRUE will be merged into one aggregate
* file and compressed by removing all extraneous white space.
* Preprocessed inline stylesheets will not be aggregated into this single file;
* instead, they are just compressed upon output on the page. Externally hosted
* stylesheets are never aggregated or compressed.
* Externally hosted stylesheets are never aggregated or compressed.
*
* The reason for aggregating the files is outlined quite thoroughly here:
* http://www.die.net/musings/page_load_time/ "Load fewer external objects. Due
......@@ -899,18 +897,15 @@ function _drupal_add_html_head_link($attributes, $header = FALSE) {
* override module-supplied CSS files based on their filenames, and this
* prefixing helps prevent confusing name collisions for theme developers.
* See drupal_get_css() where the overrides are performed.
* - 'inline': A string of CSS that should be placed in the given scope. Note
* that it is better practice to use 'file' stylesheets, rather than
* 'inline', as the CSS would then be aggregated and cached.
* - 'external': The absolute path to an external CSS file that is not hosted
* on the local server. These files will not be aggregated if CSS
* aggregation is enabled.
* @param $options
* (optional) A string defining the 'type' of CSS that is being added in the
* $data parameter ('file', 'inline', or 'external'), or an array which can
* have any or all of the following keys:
* - 'type': The type of stylesheet being added. Available options are 'file',
* 'inline' or 'external'. Defaults to 'file'.
* $data parameter ('file' or 'external'), or an array which can have any or
* all of the following keys:
* - 'type': The type of stylesheet being added. Available options are 'file'
* or 'external'. Defaults to 'file'.
* - 'basename': Force a basename for the file being added. Modules are
* expected to use stylesheets with unique filenames, but integration of
* external libraries may make this impossible. The basename of
......@@ -1027,12 +1022,6 @@ function _drupal_add_css($data = NULL, $options = NULL) {
// Add the data to the CSS array depending on the type.
switch ($options['type']) {
case 'inline':
// For inline stylesheets, we don't want to use the $data as the array
// key as $data could be a very long string of CSS.
$css[] = $options;
break;
case 'file':
// Local CSS files are keyed by basename; if a file with the same
// basename is added more than once, it gets overridden.
......@@ -1278,21 +1267,12 @@ function drupal_clean_id_identifier($id) {
}
/**
* Adds a JavaScript file, setting, or inline code to the page.
* Adds a JavaScript file or setting to the page.
*
* The behavior of this function depends on the parameters it is called with.
* Generally, it handles the addition of JavaScript to the page, either as
* reference to an existing file or as inline code. The following actions can be
* performed using this function:
* Generally, it handles the addition of JavaScript to the page. The following
* actions can be performed using this function:
* - Add a file ('file'): Adds a reference to a JavaScript file to the page.
* - Add inline JavaScript code ('inline'): Executes a piece of JavaScript code
* on the current page by placing the code directly in the page (for example,
* to tell the user that a new message arrived, by opening a pop up, alert
* box, etc.). This should only be used for JavaScript that cannot be executed
* from a file. When adding inline code, make sure that you are not relying on
* $() being the jQuery function. Wrap your code in
* @code (function ($) {... })(jQuery); @endcode
* or use jQuery() instead of $().
* - Add external JavaScript ('external'): Allows the inclusion of external
* JavaScript files that are not hosted on the local server. Note that these
* external JavaScript references do not get aggregated when preprocessing is
......@@ -1305,10 +1285,6 @@ function drupal_clean_id_identifier($id) {
* @code
* _drupal_add_js('core/misc/collapse.js');
* _drupal_add_js('core/misc/collapse.js', 'file');
* _drupal_add_js('jQuery(document).ready(function () { alert("Hello!"); });', 'inline');
* _drupal_add_js('jQuery(document).ready(function () { alert("Hello!"); });',
* array('type' => 'inline', 'scope' => 'footer', 'weight' => 5)
* );
* _drupal_add_js('http://example.com/example.js', 'external');
* _drupal_add_js(array('myModule' => array('key' => 'value')), 'setting');
* @endcode
......@@ -1318,7 +1294,6 @@ function drupal_clean_id_identifier($id) {
*
* If JavaScript aggregation is enabled, all JavaScript files added with
* $options['preprocess'] set to TRUE will be merged into one aggregate file.
* Preprocessed inline JavaScript will not be aggregated into this single file.
* Externally hosted JavaScripts are never aggregated.
*
* The reason for aggregating the files is outlined quite thoroughly here:
......@@ -1338,7 +1313,6 @@ function drupal_clean_id_identifier($id) {
* (optional) If given, the value depends on the $options parameter, or
* $options['type'] if $options is passed as an associative array:
* - 'file': Path to the file relative to base_path().
* - 'inline': The JavaScript code that should be placed in the given scope.
* - 'external': The absolute path to an external JavaScript file that is not
* hosted on the local server. These files will not be aggregated if
* JavaScript aggregation is enabled.
......@@ -1350,11 +1324,11 @@ function drupal_clean_id_identifier($id) {
* added to the existing settings array.
* @param $options
* (optional) A string defining the type of JavaScript that is being added in
* the $data parameter ('file'/'setting'/'inline'/'external'), or an
* associative array. JavaScript settings should always pass the string
* 'setting' only. Other types can have the following elements in the array:
* the $data parameter ('file'/'setting'/'external'), or an associative array.
* JavaScript settings should always pass the string 'setting' only. Other
* types can have the following elements in the array:
* - type: The type of JavaScript that is to be added to the page. Allowed
* values are 'file', 'inline', 'external' or 'setting'. Defaults
* values are 'file', 'external' or 'setting'. Defaults
* to 'file'.
* - scope: The location in which you want to place the script. Possible
* values are 'header' or 'footer'. If your theme implements different
......@@ -1467,15 +1441,9 @@ function _drupal_add_js($data = NULL, $options = NULL) {
'data' => array(),
);
}
// All JavaScript settings are placed in the header of the page with
// the library weight so that inline scripts appear afterwards.
$javascript['drupalSettings']['data'] = NestedArray::mergeDeepArray([$javascript['drupalSettings']['data'], $data], TRUE);
break;
case 'inline':
$javascript[] = $options;
break;
default: // 'file' and 'external'
// Local and external files must keep their name as the associative key
// so the same JavaScript file is not added twice.
......@@ -1515,8 +1483,8 @@ function drupal_js_defaults($data = NULL) {
*
* References to JavaScript files are placed in a certain order: first, all
* 'core' files, then all 'module' and finally all 'theme' JavaScript files
* are added to the page. Then, all settings are output, followed by 'inline'
* JavaScript code. If running update.php, all preprocessing is disabled.
* are added to the page. Then, all settings are output. If running update.php,
* all preprocessing is disabled.
*
* Note that hook_js_alter(&$javascript) is called during this function call
* to allow alterations of the JavaScript during its presentation. Calls to
......@@ -1694,44 +1662,40 @@ function drupal_merge_attached(array $a, array $b) {
/**
* Adds attachments to a render() structure.
*
* Libraries, JavaScript, CSS and other types of custom structures are attached
* to elements using the #attached property. The #attached property is an
* associative array, where the keys are the the attachment types and the values
* are the attached data. For example:
* Libraries, JavaScript settings, feeds, HTML <head> tags and HTML <head> links
* are attached to elements using the #attached property. The #attached property
* is an associative array, where the keys are the attachment types and the
* values are the attached data. For example:
*
* @code
* $build['#attached'] = array(
* 'library' => array(array('taxonomy', 'taxonomy')),
* 'css' => array(drupal_get_path('module', 'taxonomy') . '/css/taxonomy.module.css'),
* );
* $build['#attached'] = [
* 'library' => ['core/jquery']
* ];
* @endcode
*
* 'js', 'css', and 'library' are types that get special handling. For any
* other kind of attached data, the array key must be the full name of the
* callback function and each value an array of arguments. For example:
* The available keys are:
* - 'library' (asset libraries)
* - 'drupalSettings' (JavaScript settings)
* - 'feed' (RSS feeds)
* - 'html_head' (tags in HTML <head>)
* - 'html_head_link' (<link> tags in HTML <head>)
* - 'http_header' (HTTP headers)
*
* For example:
* @code
* $build['#attached']['http_header'] = array(
* array('Content-Type', 'application/rss+xml; charset=utf-8'),
* );
* @endcode
*
* External 'js' and 'css' files can also be loaded. For example:
* @code
* $build['#attached']['js'] = array(
* 'http://code.jquery.com/jquery-1.4.2.min.js' => array(
* 'type' => 'external',
* ),
* );
* @endcode
*
* @param $elements
* @param array $elements
* The structured array describing the data being rendered.
* @param $dependency_check
* @param bool $dependency_check
* When TRUE, will exit if a given library's dependencies are missing. When
* set to FALSE, will continue to add the libraries, even though one or more
* dependencies are missing. Defaults to FALSE.
*
* @return
* @return bool
* FALSE if there were any missing library dependencies; TRUE if all library
* dependencies were met.
*
......@@ -1740,12 +1704,10 @@ function drupal_merge_attached(array $a, array $b) {
* @see _drupal_add_css()
* @see drupal_render()
*/
function drupal_process_attached($elements, $dependency_check = FALSE) {
function drupal_process_attached(array $elements, $dependency_check = FALSE) {
// Add defaults to the special attached structures that should be processed differently.
$elements['#attached'] += array(
'library' => array(),
'js' => array(),
'css' => array(),
);
// Add the libraries first.
......@@ -1761,30 +1723,8 @@ function drupal_process_attached($elements, $dependency_check = FALSE) {
}
unset($elements['#attached']['library']);
// Add both the JavaScript and the CSS.
// The parameters for _drupal_add_js() and _drupal_add_css() require special
// handling.
foreach (array('js', 'css') as $type) {
foreach ($elements['#attached'][$type] as $data => $options) {
// If the value is not an array, it's a filename and passed as first
// (and only) argument.
if (!is_array($options)) {
$data = $options;
$options = NULL;
}
// In some cases, the first parameter ($data) is an array. Arrays can't be
// passed as keys in PHP, so we have to get $data from the value array.
if (is_numeric($data)) {
$data = $options['data'];
unset($options['data']);
}
call_user_func('_drupal_add_' . $type, $data, $options);
}
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
// @todo Clean this up in https://www.drupal.org/node/2368797
if (!empty($elements['#attached']['drupalSettings'])) {
_drupal_add_js($elements['#attached']['drupalSettings'], ['type' => 'setting']);
unset($elements['#attached']['drupalSettings']);
......@@ -1994,22 +1934,21 @@ function _drupal_add_library($library_name, $every_page = NULL) {
// Add all components within the library.
$elements['#attached'] = array(
'library' => $library['dependencies'],
'js' => $library['js'],
'css' => $library['css'],
);
if (isset($library['drupalSettings'])) {
$elements['#attached']['drupalSettings'] = $library['drupalSettings'];
}
$added[$extension][$name] = drupal_process_attached($elements, TRUE);
// Add both the JavaScript and the CSS.
// The parameters for _drupal_add_js() and _drupal_add_css() require special
// handling.
foreach (array('js', 'css') as $type) {
foreach ($elements['#attached'][$type] as $data => $options) {
// Set the every_page flag if one was passed.
if (isset($every_page)) {
$elements['#attached'][$type][$data]['every_page'] = $every_page;
}
foreach ($library[$type] as $options) {
call_user_func('_drupal_add_' . $type, $options['data'], $options);
}
unset($elements['#attached'][$type]);
}
$added[$extension][$name] = drupal_process_attached($elements, TRUE);
}
else {
// Requested library does not exist.
......
......@@ -158,21 +158,6 @@ public function render(array $css_assets) {
}
break;
// Output a STYLE tag for an inline CSS asset. The asset's 'data'
// property contains the CSS content.
case 'inline':
$element = $style_element_defaults;
$element['#value'] = $css_asset['data'];
$element['#attributes']['media'] = $css_asset['media'];
$element['#browsers'] = $css_asset['browsers'];
// For inline CSS to validate as XHTML, all CSS containing XHTML needs
// to be wrapped in CDATA. To make that backwards compatible with HTML
// 4, we need to comment out the CDATA-tag.
$element['#value_prefix'] = "\n/* <![CDATA[ */\n";
$element['#value_suffix'] = "\n/* ]]> */\n";
$elements[] = $element;
break;
// Output a LINK tag for an external CSS asset. The asset's 'data'
// property contains the full URL.
case 'external':
......
......@@ -72,14 +72,8 @@ public function render(array $js_assets) {
$element['#value_suffix'] = $embed_suffix;
break;
case 'inline':
$element['#value_prefix'] = $embed_prefix;
$element['#value'] = $js_asset['data'];
$element['#value_suffix'] = $embed_suffix;
break;
case 'file':
$query_string = empty($js_asset['version']) ? $default_query_string : 'v=' . $js_asset['version'];
$query_string = $js_asset['version'] == -1 ? $default_query_string : 'v=' . $js_asset['version'];
$query_string_separator = (strpos($js_asset['data'], '?') !== FALSE) ? '&' : '?';
$element['#attributes']['src'] = file_create_url($js_asset['data']);
// Only add the cache-busting query string if this isn't an aggregate
......
......@@ -197,12 +197,6 @@ public function buildByExtension($extension) {
$library[$type][] = $options;
}
}
// @todo Convert all uses of #attached[library][]=array('provider','name')
// into #attached[library][]='provider/name' and remove this.
foreach ($library['dependencies'] as $i => $dependency) {
$library['dependencies'][$i] = $dependency;
}
}
return $libraries;
......
......@@ -30,7 +30,7 @@ drupal.comment-new-indicator:
- core/jquery
- core/jquery.once
- core/drupal
- history/drupal.history
- history/api
- core/drupal.displace
drupal.node-new-comments-link:
......@@ -41,4 +41,4 @@ drupal.node-new-comments-link:
- core/jquery
- core/jquery.once
- core/drupal
- history/drupal.history
- history/api
drupal.history:
api:
version: VERSION
js:
js/history.js: {}
......@@ -7,3 +7,10 @@ drupal.history:
- core/drupalSettings
- core/drupal
- core/drupal.ajax
mark-as-read:
version: VERSION
js:
js/mark-as-read.js: { scope: footer }
dependencies:
- history/api
......@@ -138,11 +138,8 @@ function history_node_view_alter(array &$build, EntityInterface $node, EntityVie
// When the window's "load" event is triggered, mark the node as read.
// This still allows for Drupal behaviors (which are triggered on the
// "DOMContentReady" event) to add "new" and "updated" indicators.
$build['#attached']['js'][] = array(
'data' => 'window.addEventListener("load",function(){Drupal.history.markAsRead(' . $node->id() . ');},false);',
'type' => 'inline',
);
$build['#attached']['library'][] = 'history/drupal.history';
$build['#attached']['library'][] = 'history/mark-as-read';
$build['#attached']['drupalSettings']['history']['nodesToMarkAsRead'][$node->id()] = TRUE;
}
}
......
/**
* Marks the nodes listed in drupalSettings.history.nodesToMarkAsRead as read.
*
* Uses the History module JavaScript API.
*/
(function (window, Drupal, drupalSettings) {
"use strict";
// When the window's "load" event is triggered, mark all enumerated nodes as
// read. This still allows for Drupal behaviors (which are triggered on the
// "DOMContentReady" event) to add "new" and "updated" indicators.
window.addEventListener('load', function() {
if (drupalSettings.history && drupalSettings.history.nodesToMarkAsRead) {
Object.keys(drupalSettings.history.nodesToMarkAsRead).forEach(Drupal.history.markAsRead);
}
});
})(window, Drupal, drupalSettings);
......@@ -119,8 +119,9 @@ function testHistory() {
$this->drupalGet('node/' . $nid);
// JavaScript present to record the node read.
$settings = $this->getDrupalSettings();
$this->assertTrue(isset($settings['ajaxPageState']['js']['core/modules/history/js/history.js']), 'drupal.history library is present.');
$this->assertRaw('Drupal.history.markAsRead(' . $nid . ')', 'History module JavaScript API call to mark node as read present on page.');
$this->assertTrue(isset($settings['ajaxPageState']['js']['core/modules/history/js/history.js']), 'history/api library is present.');
$this->assertTrue(isset($settings['ajaxPageState']['js']['core/modules/history/js/mark-as-read.js']), 'history/mark-as-read library is present.');
$this->assertEqual([$nid => TRUE], $settings['history']['nodesToMarkAsRead'], 'drupalSettings to mark node as read are present.');
// Simulate JavaScript: perform HTTP request to mark node as read.
$response = $this->markNodeAsRead($nid);
......
......@@ -99,7 +99,8 @@ function quickedit_library_alter(array &$library, $name, $theme = NULL) {
}
if (isset($info['quickedit_stylesheets']) && is_array($info['quickedit_stylesheets'])) {
foreach ($info['quickedit_stylesheets'] as $path) {
$library['css'][$theme_path . '/' . $path] = array(
$library['css'][] = array(
'data' => $theme_path . '/' . $path,
'group' => CSS_AGGREGATE_THEME,
'weight' => CSS_THEME,
);
......
......@@ -35,44 +35,18 @@ public function testAJAXRender() {
* Tests AjaxResponse::prepare() AJAX commands ordering.
*/
public function testOrder() {
$path = drupal_get_path('module', 'system');
$expected_commands = array();
// Expected commands, in a very specific order.
$expected_commands[0] = new SettingsCommand(array('ajax' => 'test'), TRUE);
drupal_static_reset('_drupal_add_css');
$attached = array(
'#attached' => array(
'css' => array(
$path . '/css/system.admin.css' => array(),
$path . '/css/system.maintenance.css' => array()
),
),
);
drupal_render($attached);
drupal_process_attached($attached);
$build['#attached']['library'][] = 'ajax_test/order-css-command';
drupal_process_attached($build);
$expected_commands[1] = new AddCssCommand(drupal_get_css(_drupal_add_css(), TRUE));
drupal_static_reset('_drupal_add_js');
$attached = array(
'#attached' => array(
'js' => array(
$path . '/system.js' => array(),
),
),
);
drupal_render($attached);
drupal_process_attached($attached);
$build['#attached']['library'][] = 'ajax_test/order-js-command';
drupal_process_attached($build);
$expected_commands[2] = new PrependCommand('head', drupal_get_js('header', _drupal_add_js(), TRUE));
drupal_static_reset('_drupal_add_js');
$attached = array(
'#attached' => array(
'js' => array(
$path . '/system.modules.js' => array('scope' => 'footer'),
),
),
);
drupal_render($attached);
drupal_process_attached($attached);
$expected_commands[3] = new AppendCommand('body', drupal_get_js('footer', _drupal_add_js(), TRUE));
$expected_commands[4] = new HtmlCommand('body', 'Hello, world!');
......
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\JavaScriptTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\Crypt;
use Drupal\simpletest\KernelTestBase;
/**
* Tests #attached assets: attached asset libraries and JavaScript settings.
*
* i.e. tests:
*
* @code
* $build['#attached']['library'] = …
* $build['#attached']['drupalSettings'] = …
* @endcode
*
* @group Common
* @group Asset
*/
class AttachedAssetsTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('language', 'simpletest', 'common_test', 'system');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Disable preprocessing.
\Drupal::config('system.performance')
->set('css.preprocess', FALSE)
->set('js.preprocess', FALSE)
->save();
// Reset _drupal_add_css() and _drupal_add_js() statics before each test.
drupal_static_reset('_drupal_add_css');
drupal_static_reset('_drupal_add_js');
$this->installSchema('system', 'router');
\Drupal::service('router.builder')->rebuild();
}
/**
* Tests that default CSS and JavaScript is empty.
*/
function testDefault() {
$build['#attached'] = [];
drupal_process_attached($build);
$this->assertEqual(array(), _drupal_add_css(), 'Default CSS is empty.');
$this->assertEqual(array(), _drupal_add_js(), 'Default JavaScript is empty.');
}
/**
* Tests non-existing libraries.
*/
function testLibraryUnknown() {
$build['#attached']['library'][] = 'unknown/unknown';
drupal_process_attached($build);
$scripts = drupal_get_js();
$this->assertTrue(strpos($scripts, 'unknown') === FALSE, 'Unknown library was not added to the page.');
}
/**
* Tests adding a CSS and a JavaScript file.
*/
function testAddFiles() {
$build['#attached']['library'][] = 'common_test/files';
drupal_process_attached($build);
$css = _drupal_add_css();
$js = _drupal_add_js();
$this->assertTrue(array_key_exists('bar.css', $css), 'CSS files are correctly added.');
$this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/foo.js', $js), 'JavaScript files are correctly added.');
$rendered_css = drupal_get_css();
$rendered_js = drupal_get_js();
$query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0';
$this->assertNotIdentical(strpos($rendered_css, '<link rel="stylesheet" href="' . file_create_url('core/modules/system/tests/modules/common_test/bar.css') . '?' . $query_string . '" media="all" />'), FALSE, 'Rendering an external CSS file.');
$this->assertNotIdentical(strpos($rendered_js, '<script src="' . file_create_url('core/modules/system/tests/modules/common_test/foo.js') . '?' . $query_string . '"></script>'), FALSE, 'Rendering an external JavaScript file.');
}
/**
* Tests adding JavaScript settings.
*/
function testAddJsSettings() {
// Add a file in order to test default settings.
$build['#attached']['library'][] = 'core/drupalSettings';
drupal_process_attached($build);
$javascript = _drupal_add_js();
$this->assertTrue(array_key_exists('currentPath', $javascript['drupalSettings']['data']['path']), 'The current path JavaScript setting is set correctly.');
$javascript = _drupal_add_js(array('drupal' => 'rocks', 'dries' => 280342800), 'setting');
$this->assertEqual(280342800, $javascript['drupalSettings']['data']['dries'], 'JavaScript setting is set correctly.');
$this->assertEqual('rocks', $javascript['drupalSettings']['data']['drupal'], 'The other JavaScript setting is set correctly.');
}
/**
* Tests adding external CSS and JavaScript files.
*/
function testAddExternalFiles() {
$build['#attached']['library'][] = 'common_test/external';
drupal_process_attached($build);
$css = _drupal_add_css();
$js = _drupal_add_js();
$this->assertTrue(array_key_exists('http://example.com/stylesheet.css', $css), 'External CSS files are correctly added.');
$this->assertTrue(array_key_exists('http://example.com/script.js', $js), 'External JavaScript files are correctly added.');
$rendered_css = drupal_get_css();
$rendered_js = drupal_get_js();
$this->assertNotIdentical(strpos($rendered_css, '<link rel="stylesheet" href="http://example.com/stylesheet.css" media="all" />'), FALSE, 'Rendering an external CSS file.');
$this->assertNotIdentical(strpos($rendered_js, '<script src="http://example.com/script.js"></script>'), FALSE, 'Rendering an external JavaScript file.');
}
/**
* Tests adding JavaScript files with additional attributes.
*/
function testAttributes() {
$build['#attached']['library'][] = 'common_test/js-attributes';
drupal_process_attached($build);
$rendered_js = drupal_get_js();
$expected_1 = '<script src="http://example.com/deferred-external.js" foo="bar" defer></script>';
$expected_2 = '<script src="' . file_create_url('core/modules/system/tests/modules/common_test/deferred-internal.js') . '?v=1" defer bar="foo"></script>';
$this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered external JavaScript with correct defer and random attributes.');
$this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered internal JavaScript with correct defer and random attributes.');
}
/**
* Tests that attributes are maintained when JS aggregation is enabled.
*/
function testAggregatedAttributes() {
// Enable aggregation.
\Drupal::config('system.performance')->set('js.preprocess', 1)->save();