diff --git a/includes/common.inc b/includes/common.inc index 5f7cdb8..5044105 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 JS 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 JS 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 JS 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,194 @@ 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( + '#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) { + switch ($group['type']) { case 'inline': - $js_element = $element; - if ($item['defer']) { - $js_element['#attributes']['defer'] = 'defer'; + case 'setting': + case 'external': + foreach ($group['items'] as $item) { + $js_element = $element; + if (!empty($item['defer'])) { + $js_element['#attributes']['defer'] = 'defer'; + } + // We still need to do conditional on the $group['type'] because + // we render the element differently depending on the type. + if ($group['type'] == 'setting') { + $js_element['#value'] = 'jQuery.extend(Drupal.settings, ' . drupal_json_encode(drupal_array_merge_deep_array($item['data'])) . ");"; + } + elseif ($group['type'] == 'external') { + $js_element['#attributes']['src'] = $item['data']; + } + else { + $js_element['#value'] = $item['data']; + } + // If the type is not external we want to add the $embed_prefix and + // $embed_suffix to the value. + if ($group['type'] != 'external') { + $js_element['#value_prefix'] = $embed_prefix; + $js_element['#value_suffix'] = $embed_suffix; + } + $js_element['#browsers'] = $group['browsers']; + $elements[] = $js_element; } - $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; - 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)); + // The group has been aggregated into a single file. + if (isset($group['data'])) { + $js_element = $element; + $query_string = empty($group['version']) ? $default_query_string : $js_version_string . $group['version']; + $query_string_separator = (strpos($group['data'], '?') !== FALSE) ? '&' : '?'; + $js_element['#attributes']['src'] = file_create_url($group['data']) . $query_string_separator . ($group['cache'] ? $query_string : REQUEST_TIME); + $js_element['#browsers'] = $group['browsers']; + $elements[] = $js_element; } + // The group has not been aggregated into a single file. + // For example, aggregation might be disabled for a site + // during development. 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; + foreach($group['items'] as $item) { + $js_element = $element; + if ($item['defer']) { + $js_element['#attributes']['defer'] = 'defer'; + } + $query_string = empty($item['version']) ? $default_query_string : $js_version_string . $item['version']; + $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); + $js_element['#browsers'] = $item['browsers']; + $elements[] = $js_element; + } } break; + } + } + + return $elements; +} + +/** + * Default callback to group JS items. + * + * This function arranges the JS 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' type + * are never groupable, items of the 'settings' type are never groupable and + * items of the 'external' type are never 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 JS items, as returned by drupal_add_js(), but after + * alteration performed by drupal_get_js(). + * + * @return + * An array of JS groups. Each group contains the same keys (e.g., + * 'data', etc.) as a JS 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 JS 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': + // 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 JS files and inline content. + * + * Having the browser load fewer JS 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 JS groups as returned by drupal_group_js(). This function + * modifies the group's 'data' property for each group that is aggregated. + * + * @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 +4905,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/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(