Commit 5f46023c authored by catch's avatar catch
Browse files

Issue #865536 by ericduran, effulgentsia, cosmicdreams, sreynen, jessebeach et...

Issue #865536 by ericduran, effulgentsia, cosmicdreams, sreynen, jessebeach et al: Added drupal_add_js() is missing the 'browsers' option.
parent 02ebc310
......@@ -70,7 +70,7 @@
define('CSS_THEME', 100);
/**
* The default group for JavaScript libraries, settings or jQuery plugins added
* The default group for JavaScript libraries or jQuery plugins added
* to the page.
*/
define('JS_LIBRARY', -100);
......@@ -85,6 +85,11 @@
*/
define('JS_THEME', 100);
/**
* The default group for JavaScript settings added to the page.
*/
define('JS_SETTING', 200);
/**
* Error code indicating that the request made by drupal_http_request() exceeded
* the specified timeout.
......@@ -3921,6 +3926,9 @@ function drupal_region_class($region) {
* a JavaScript file. Defaults to TRUE.
* - preprocess: If TRUE and JavaScript aggregation is enabled, the script
* file will be aggregated. Defaults to TRUE.
* - browsers: An array containing information specifying which browsers
* should load the JavaScript item. See
* drupal_pre_render_conditional_comments() for details.
*
* @return
* The current array of JavaScript files, settings, and in-line code,
......@@ -3966,9 +3974,10 @@ function drupal_add_js($data = NULL, $options = NULL) {
),
'type' => 'setting',
'scope' => 'header',
'group' => JS_LIBRARY,
'group' => JS_SETTING,
'every_page' => TRUE,
'weight' => 0,
'browsers' => array(),
),
'core/misc/drupal.js' => array(
'data' => 'core/misc/drupal.js',
......@@ -3980,6 +3989,7 @@ function drupal_add_js($data = NULL, $options = NULL) {
'preprocess' => TRUE,
'cache' => TRUE,
'defer' => FALSE,
'browsers' => array(),
),
);
// Register all required libraries.
......@@ -4027,6 +4037,7 @@ function drupal_js_defaults($data = NULL) {
'preprocess' => TRUE,
'version' => NULL,
'data' => $data,
'browsers' => array(),
);
}
......@@ -4082,13 +4093,66 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
}
}
$output = '';
// The index counter is used to keep aggregated and non-aggregated files in
// order by weight.
$index = 1;
$processed = array();
$files = array();
$preprocess_js = (variable_get('preprocess_js', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update'));
// Sort the JavaScript so that it appears in the correct order.
uasort($items, 'drupal_sort_css_js');
// 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($items), 1);
unset($setting['ajaxPageState']['js']['settings']);
drupal_add_js($setting, 'setting');
// If we're outputting the header scope, then this might be the final time
// that drupal_get_js() is running, so add the setting 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;
}
// Render the HTML needed to load the JavaScript.
$elements = array(
'#type' => 'scripts',
'#items' => $items,
);
return drupal_render($elements);
}
/**
* #pre_render callback to add the elements needed for JavaScript tags to be rendered.
*
* This function evaluates the aggregation enabled/disabled condition on a group
* by group basis by testing whether an aggregate file has been made for the
* group rather than by testing the site-wide aggregation setting. This allows
* this function to work correctly even if modules have implemented custom
* logic for grouping and aggregating files.
*
* @param $element
* A render array containing:
* - #items: The JavaScript items as returned by drupal_add_js() and
* altered by drupal_get_js().
* - #group_callback: A function to call to group #items. Following
* this function, #aggregate_callback is called to aggregate items within
* the same group into a single file.
* - #aggregate_callback: A function to call to aggregate the items within
* the groups arranged by the #group_callback function.
*
* @return
* A render array that will render to a string of JavaScript tags.
*
* @see drupal_get_js()
*/
function drupal_pre_render_scripts($elements) {
// Group and aggregate the items.
if (isset($elements['#group_callback'])) {
$elements['#groups'] = $elements['#group_callback']($elements['#items']);
}
if (isset($elements['#aggregate_callback'])) {
$elements['#aggregate_callback']($elements['#groups']);
}
// A dummy query-string is added to filenames, to gain control over
// browser-caching. The string changes on every update or full cache
......@@ -4108,110 +4172,182 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
// third-party code might require the use of a different query string.
$js_version_string = variable_get('drupal_js_version_query_string', 'v=');
// Sort the JavaScript so that it appears in the correct order.
uasort($items, 'drupal_sort_css_js');
// 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($items), 1);
unset($setting['ajaxPageState']['js']['settings']);
drupal_add_js($setting, 'setting');
// If we're outputting the header scope, then this might be the final time
// that drupal_get_js() is running, so add the setting 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 off 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;
}
// Loop through the JavaScript to construct the rendered output.
$element = array(
// Defaults for each SCRIPT element.
$element_defaults = array(
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => '',
'#attributes' => array(
'type' => 'text/javascript',
),
);
foreach ($items as $item) {
$query_string = empty($item['version']) ? $default_query_string : $js_version_string . $item['version'];
switch ($item['type']) {
case 'setting':
$js_element = $element;
$js_element['#value_prefix'] = $embed_prefix;
$js_element['#value'] = 'jQuery.extend(Drupal.settings, ' . drupal_json_encode(drupal_array_merge_deep_array($item['data'])) . ");";
$js_element['#value_suffix'] = $embed_suffix;
$output .= theme('html_tag', array('element' => $js_element));
break;
// Loop through each group.
foreach ($elements['#groups'] as $group) {
// If a group of files has been aggregated into a single file,
// $group['data'] contains the URI of the aggregate file. Add a single
// script element for this file.
if ($group['type'] == 'file' && isset($group['data'])) {
$element = $element_defaults;
$element['#attributes']['src'] = file_create_url($group['data']);
$element['#browsers'] = $group['browsers'];
$elements[] = $element;
}
// For non-file types, and non-aggregated files, add a script element per
// item.
else {
foreach ($group['items'] as $item) {
// Element properties that do not depend on item type.
$element = $element_defaults;
if (!empty($item['defer'])) {
$element['#attributes']['defer'] = 'defer';
}
$element['#browsers'] = $item['browsers'];
case 'inline':
$js_element = $element;
if ($item['defer']) {
$js_element['#attributes']['defer'] = 'defer';
// Element properties that depend on item type.
switch ($item['type']) {
case 'setting':
$element['#value_prefix'] = $embed_prefix;
$element['#value'] = 'jQuery.extend(Drupal.settings, ' . drupal_json_encode(drupal_array_merge_deep_array($item['data'])) . ");";
$element['#value_suffix'] = $embed_suffix;
break;
case 'inline':
$element['#value_prefix'] = $embed_prefix;
$element['#value'] = $item['data'];
$element['#value_suffix'] = $embed_suffix;
break;
case 'file':
$query_string = empty($item['version']) ? $default_query_string : $js_version_string . $item['version'];
$query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?';
$element['#attributes']['src'] = file_create_url($item['data']) . $query_string_separator . ($item['cache'] ? $query_string : REQUEST_TIME);
break;
case 'external':
$element['#attributes']['src'] = $item['data'];
break;
}
$js_element['#value_prefix'] = $embed_prefix;
$js_element['#value'] = $item['data'];
$js_element['#value_suffix'] = $embed_suffix;
$processed[$index++] = theme('html_tag', array('element' => $js_element));
break;
$elements[] = $element;
}
}
}
return $elements;
}
/**
* Default callback to group JavaScript items.
*
* This function arranges the JavaScript items that are in the #items property
* of the scripts element into groups. When aggregation is enabled, files within
* a group are aggregated into a single file, significantly improving page
* loading performance by minimizing network traffic overhead.
*
* This function puts multiple items into the same group if they are groupable
* and if they are for the same browsers. Items of the 'file' type are groupable
* if their 'preprocess' flag is TRUE. Items of the 'inline', 'settings', or
* 'external' type are not groupable.
*
* This function also ensures that the process of grouping items does not change
* their relative order. This requirement may result in multiple groups for the
* same type and browsers, if needed to accommodate other items in
* between.
*
* @param $javascript
* An array of JavaScript items, as returned by drupal_add_js(), but after
* alteration performed by drupal_get_js().
*
* @return
* An array of JavaScript groups. Each group contains the same keys (e.g.,
* 'data', etc.) as a JavaScript item from the $javascript parameter, with the
* value of each key applying to the group as a whole. Each group also
* contains an 'items' key, which is the subset of items from $javascript that
* are in the group.
*
* @see drupal_pre_render_scripts()
*/
function drupal_group_js($javascript) {
$groups = array();
// If a group can contain multiple items, we track the information that must
// be the same for each item in the group, so that when we iterate the next
// item, we can determine if it can be put into the current group, or if a
// new group needs to be made for it.
$current_group_keys = NULL;
$index = -1;
foreach ($javascript as $item) {
// The browsers for which the JavaScript item needs to be loaded is part of
// the information that determines when a new group is needed, but the order
// of keys in the array doesn't matter, and we don't want a new group if all
// that's different is that order.
ksort($item['browsers']);
switch ($item['type']) {
case 'file':
$js_element = $element;
if (!$item['preprocess'] || !$preprocess_js) {
if ($item['defer']) {
$js_element['#attributes']['defer'] = 'defer';
}
$query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?';
$js_element['#attributes']['src'] = file_create_url($item['data']) . $query_string_separator . ($item['cache'] ? $query_string : REQUEST_TIME);
$processed[$index++] = theme('html_tag', array('element' => $js_element));
}
else {
// By increasing the index for each aggregated file, we maintain
// the relative ordering of JS by weight. We also set the key such
// that groups are split by items sharing the same 'group' value and
// 'every_page' flag. While this potentially results in more aggregate
// files, it helps make each one more reusable across a site visit,
// leading to better front-end performance of a website as a whole.
// See drupal_add_js() for details.
$key = 'aggregate_' . $item['group'] . '_' . $item['every_page'] . '_' . $index;
$processed[$key] = '';
$files[$key][$item['data']] = $item;
}
// Group file items if their 'preprocess' flag is TRUE.
// Help ensure maximum reuse of aggregate files by only grouping
// together items that share the same 'group' value and 'every_page'
// flag. See drupal_add_js() for details about that.
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['every_page'], $item['browsers']) : FALSE;
break;
case 'external':
$js_element = $element;
// Preprocessing for external JavaScript files is ignored.
if ($item['defer']) {
$js_element['#attributes']['defer'] = 'defer';
}
$js_element['#attributes']['src'] = $item['data'];
$processed[$index++] = theme('html_tag', array('element' => $js_element));
case 'setting':
case 'inline':
// Do not group external, settings, and inline items.
$group_keys = FALSE;
break;
}
// If the group keys don't match the most recent group we're working with,
// then a new group must be made.
if ($group_keys !== $current_group_keys) {
$index++;
// Initialize the new group with the same properties as the first item
// being placed into it. The item's 'data' and 'weight' properties are
// unique to the item and should not be carried over to the group.
$groups[$index] = $item;
unset($groups[$index]['data'], $groups[$index]['weight']);
$groups[$index]['items'] = array();
$current_group_keys = $group_keys ? $group_keys : NULL;
}
// Add the item to the current group.
$groups[$index]['items'][] = $item;
}
// Aggregate any remaining JS files that haven't already been output.
if ($preprocess_js && count($files) > 0) {
foreach ($files as $key => $file_set) {
$uri = drupal_build_js_cache($file_set);
// Only include the file if was written successfully. Errors are logged
// using watchdog.
if ($uri) {
$preprocess_file = file_create_url($uri);
$js_element = $element;
$js_element['#attributes']['src'] = $preprocess_file;
$processed[$key] = theme('html_tag', array('element' => $js_element));
return $groups;
}
/**
* Default callback to aggregate JavaScript files.
*
* Having the browser load fewer JavaScript files results in much faster page
* loads than when it loads many small files. This function aggregates files
* within the same group into a single file unless the site-wide setting to do
* so is disabled (commonly the case during site development). To optimize
* download, it also compresses the aggregate files by removing comments,
* whitespace, and other unnecessary content.
*
* @param $js_groups
* An array of JavaScript groups as returned by drupal_group_js(). For each
* group that is aggregated, this function sets the value of the group's
* 'data' key to the URI of the aggregate file.
*
* @see drupal_group_js()
* @see drupal_pre_render_scripts()
*/
function drupal_aggregate_js(&$js_groups) {
// Only aggregate when the site is configured to do so, and not during an
// update.
if (variable_get('preprocess_js', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')) {
foreach ($js_groups as $key => $group) {
if ($group['type'] == 'file' && $group['preprocess']) {
$js_groups[$key]['data'] = drupal_build_js_cache($group['items']);
}
}
}
// Keep the order of JS files consistent as some are preprocessed and others are not.
// Make sure any inline or JS setting variables appear last after libraries have loaded.
return implode('', $processed) . $output;
}
/**
......@@ -4745,10 +4881,10 @@ function drupal_build_js_cache($files) {
if (empty($uri) || !file_exists($uri)) {
// Build aggregate JS file.
foreach ($files as $path => $info) {
foreach ($files as $info) {
if ($info['preprocess']) {
// Append a ';' and a newline after each JS file to prevent them from running together.
$contents .= file_get_contents($path) . ";\n";
$contents .= file_get_contents($info['data']) . ";\n";
}
}
// Prefix filename to prevent blocking by firewalls which reject files
......
......@@ -1315,6 +1315,75 @@ class JavaScriptTestCase extends DrupalWebTestCase {
$this->assertEqual($javascript['core/misc/collapse.js']['weight'], 2, t('Adding a JavaScript file with a different weight caches the given weight.'));
}
/**
* Test adding JavaScript within conditional comments.
*
* @see drupal_pre_render_conditional_comments()
*/
function testBrowserConditionalComments() {
$default_query_string = variable_get('css_js_query_string', '0');
drupal_add_js('core/misc/collapse.js', array('browsers' => array('IE' => 'lte IE 8', '!IE' => FALSE)));
drupal_add_js('jQuery(function () { });', array('type' => 'inline', 'browsers' => array('IE' => FALSE)));
$javascript = drupal_get_js();
$expected_1 = "<!--[if lte IE 8]>\n" . '<script type="text/javascript" src="' . file_create_url('core/misc/collapse.js') . '?' . $default_query_string . '"></script>' . "\n<![endif]-->";
$expected_2 = "<!--[if !IE]><!-->\n" . '<script type="text/javascript">' . "\n<!--//--><![CDATA[//><!--\n" . 'jQuery(function () { });' . "\n//--><!]]>\n" . '</script>' . "\n<!--<![endif]-->";
$this->assertTrue(strpos($javascript, $expected_1) > 0, t('Rendered JavaScript within downlevel-hidden conditional comments.'));
$this->assertTrue(strpos($javascript, $expected_2) > 0, t('Rendered JavaScript within downlevel-revealed conditional comments.'));
}
/**
* Test JavaScript versioning.
*/
function testVersionQueryString() {
drupal_add_js('core/misc/collapse.js', array('version' => 'foo'));
drupal_add_js('core/misc/ajax.js', array('version' => 'bar'));
$javascript = drupal_get_js();
$this->assertTrue(strpos($javascript, 'core/misc/collapse.js?v=foo') > 0 && strpos($javascript, 'core/misc/ajax.js?v=bar') > 0 , t('JavaScript version identifiers correctly appended to URLs'));
}
/**
* Test JavaScript grouping and aggregation.
*/
function testAggregation() {
$default_query_string = variable_get('css_js_query_string', '0');
// To optimize aggregation, items with the 'every_page' option are ordered
// ahead of ones without. The order of JavaScript execution must be the
// same regardless of whether aggregation is enabled, so ensure this
// expected order, first with aggregation off.
drupal_add_js('core/misc/ajax.js');
drupal_add_js('core/misc/authorize.js', array('every_page' => TRUE));
drupal_add_js('core/misc/autocomplete.js');
drupal_add_js('core/misc/batch.js', array('every_page' => TRUE));
$javascript = drupal_get_js();
$expected = implode("\n", array(
'<script type="text/javascript" src="' . file_create_url('core/misc/authorize.js') . '?' . $default_query_string . '"></script>',
'<script type="text/javascript" src="' . file_create_url('core/misc/batch.js') . '?' . $default_query_string . '"></script>',
'<script type="text/javascript" src="' . file_create_url('core/misc/ajax.js') . '?' . $default_query_string . '"></script>',
'<script type="text/javascript" src="' . file_create_url('core/misc/autocomplete.js') . '?' . $default_query_string . '"></script>',
));
$this->assertTrue(strpos($javascript, $expected) > 0, t('Unaggregated JavaScript is added in the expected group order.'));
// Now ensure that with aggregation on, one file is made for the
// 'every_page' files, and one file is made for the others.
drupal_static_reset('drupal_add_js');
variable_set('preprocess_js', 1);
drupal_add_js('core/misc/ajax.js');
drupal_add_js('core/misc/authorize.js', array('every_page' => TRUE));
drupal_add_js('core/misc/autocomplete.js');
drupal_add_js('core/misc/batch.js', array('every_page' => TRUE));
$js_items = drupal_add_js();
$javascript = drupal_get_js();
$expected = implode("\n", array(
'<script type="text/javascript" src="' . file_create_url(drupal_build_js_cache(array('core/misc/authorize.js' => $js_items['core/misc/authorize.js'], 'core/misc/batch.js' => $js_items['core/misc/batch.js']))) . '"></script>',
'<script type="text/javascript" src="' . file_create_url(drupal_build_js_cache(array('core/misc/ajax.js' => $js_items['core/misc/ajax.js'], 'core/misc/autocomplete.js' => $js_items['core/misc/autocomplete.js']))) . '"></script>',
));
$this->assertTrue(strpos($javascript, $expected) > 0, t('JavaScript is aggregated in the expected groups and order.'));
}
/**
* Test JavaScript ordering.
*/
......
......@@ -315,6 +315,12 @@ function system_element_info() {
'#group_callback' => 'drupal_group_css',
'#aggregate_callback' => 'drupal_aggregate_css',
);
$types['scripts'] = array(
'#items' => array(),
'#pre_render' => array('drupal_pre_render_scripts'),
'#group_callback' => 'drupal_group_js',
'#aggregate_callback' => 'drupal_aggregate_js',
);
// Input elements.
$types['submit'] = array(
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment