diff --git a/includes/common.inc b/includes/common.inc
index 5f7cdb8..ad0b878 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -70,7 +70,7 @@ define('CSS_DEFAULT', 0);
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);
@@ -86,6 +86,11 @@ define('JS_DEFAULT', 0);
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.
*/
@@ -3933,6 +3938,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,
@@ -3978,9 +3986,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(),
),
'misc/drupal.js' => array(
'data' => 'misc/drupal.js',
@@ -3992,6 +4001,7 @@ function drupal_add_js($data = NULL, $options = NULL) {
'preprocess' => TRUE,
'cache' => TRUE,
'defer' => FALSE,
+ 'browsers' => array(),
),
);
// Register all required libraries.
@@ -4039,6 +4049,7 @@ function drupal_js_defaults($data = NULL) {
'preprocess' => TRUE,
'version' => NULL,
'data' => $data,
+ 'browsers' => array(),
);
}
@@ -4094,13 +4105,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
@@ -4120,110 +4184,184 @@ 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) {
+ $preprocess_js = (variable_get('preprocess_js', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update'));
+ // For each group that needs aggregation, aggregate its items.
+ foreach ($js_groups as $key => $group) {
+ if ($group['type'] == 'file') {
+ // If a file group can be aggregated into a single file, do so, and set
+ // the group's data property to the file path of the aggregate file.
+ if ($group['preprocess'] & $preprocess_js) {
+ $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;
}
/**
@@ -4757,10 +4895,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
diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test
index 435811c..5de414c 100644
--- a/modules/simpletest/tests/common.test
+++ b/modules/simpletest/tests/common.test
@@ -1316,6 +1316,75 @@ class JavaScriptTestCase extends DrupalWebTestCase {
}
/**
+ * 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('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 = "";
+ $expected_2 = "\n" . '' . "\n";
+
+ $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('misc/collapse.js', array('version' => 'foo'));
+ drupal_add_js('misc/ajax.js', array('version' => 'bar'));
+ $javascript = drupal_get_js();
+ $this->assertTrue(strpos($javascript, 'misc/collapse.js?v=foo') > 0 && strpos($javascript, '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('misc/ajax.js');
+ drupal_add_js('misc/authorize.js', array('every_page' => TRUE));
+ drupal_add_js('misc/autocomplete.js');
+ drupal_add_js('misc/batch.js', array('every_page' => TRUE));
+ $javascript = drupal_get_js();
+ $expected = implode("\n", array(
+ '',
+ '',
+ '',
+ '',
+ ));
+ $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('misc/ajax.js');
+ drupal_add_js('misc/authorize.js', array('every_page' => TRUE));
+ drupal_add_js('misc/autocomplete.js');
+ drupal_add_js('misc/batch.js', array('every_page' => TRUE));
+ $js_items = drupal_add_js();
+ $javascript = drupal_get_js();
+ $expected = implode("\n", array(
+ '',
+ '',
+ ));
+ $this->assertTrue(strpos($javascript, $expected) > 0, t('JavaScript is aggregated in the expected groups and order.'));
+ }
+
+ /**
* Test JavaScript ordering.
*/
function testRenderOrder() {
diff --git a/modules/system/system.module b/modules/system/system.module
index 0ef688e..c04fbe6 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -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(