diff --git a/includes/common.inc b/includes/common.inc index 3e8d6d9..ba2df94 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -70,8 +70,7 @@ define('CSS_DEFAULT', 0); define('CSS_THEME', 100); /** - * The default group for JavaScript libraries or jQuery plugins added to the - * page. + * The default group for JavaScript and jQuery libraries added to the page. */ define('JS_LIBRARY', -100); @@ -4288,79 +4287,13 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS } } - // Sort the JavaScript so that it appears in the correct order. - uasort($items, 'drupal_sort_css_js'); - - // In Drupal 8, there's a JS_SETTING group for making setting variables - // appear last after libraries have loaded. In Drupal 7, this is forced - // without that group. We do not use the $key => $item type of iteration, - // because PHP uses an internal array pointer for that, and we're modifying - // the array order inside the loop. - foreach (array_keys($items) as $key) { - if ($items[$key]['type'] == 'setting') { - $item = $items[$key]; - unset($items[$key]); - $items[$key] = $item; - } - } - - // 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']); - } + $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')); // A dummy query-string is added to filenames, to gain control over // browser-caching. The string changes on every update or full cache @@ -4380,8 +4313,27 @@ function drupal_pre_render_scripts($elements) { // third-party code might require the use of a different query string. $js_version_string = variable_get('drupal_js_version_query_string', 'v='); - // Defaults for each SCRIPT element. - $element_defaults = array( + // 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' => '', @@ -4389,173 +4341,108 @@ function drupal_pre_render_scripts($elements) { 'type' => 'text/javascript', ), ); + foreach ($items as $item) { + $query_string = empty($item['version']) ? $default_query_string : $js_version_string . $item['version']; - // 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']; + $js_element = $element; - // 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; + // Ensure browser defaults are populated. + $item['browsers'] += array( + 'IE' => TRUE, + '!IE' => TRUE, + ); - case 'inline': - $element['#value_prefix'] = $embed_prefix; - $element['#value'] = $item['data']; - $element['#value_suffix'] = $embed_suffix; - break; + // Generate a unique key that represents browsers so they can be grouped. + $browsers_key = ($item['browsers']['IE'] ? drupal_html_class($item['browsers']['IE']) : '0') . '_' . ($item['browsers']['!IE'] ? '1' : '0'); - 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; + switch ($item['type']) { + case 'setting': + $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 .= drupal_render($js_element); + break; - case 'external': - $element['#attributes']['src'] = $item['data']; - break; + case 'inline': + if ($item['defer']) { + $js_element['#attributes']['defer'] = 'defer'; } + $js_element['#value_prefix'] = $embed_prefix; + $js_element['#value'] = $item['data']; + $js_element['#value_suffix'] = $embed_suffix; + $js_element['#browsers'] = $item['browsers']; + $processed[$index++] = drupal_render($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': - // 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; + 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); + $js_element['#browsers'] = $item['browsers']; + // If item renders in all browsers, then there is no need for + // conditional comments. Increase and use the index as the key. + if ($item['browsers']['IE'] === TRUE && $item['browsers']['!IE']) { + $processed[$index++] = drupal_render($js_element); + } + // If item renders for specific browsers, group by browsers key and + // append each item so they are grouped properly. + else { + $processed[$browsers_key] = (!empty($processed[$browsers_key]) ? $processed[$browsers_key] : '') . drupal_render($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 + // 'every_page' flag and browser conditions. 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 more + // details. + $key = $item['type'] . '_' . $item['every_page'] . '_' . $browsers_key . '_' . $index; + $processed[$key] = ''; + $files[$key][$item['data']] = $item; + } break; case 'external': - case 'setting': - case 'inline': - // Do not group external, settings, and inline items. - $group_keys = FALSE; + // Preprocessing for external JavaScript files is ignored. + if ($item['defer']) { + $js_element['#attributes']['defer'] = 'defer'; + } + $js_element['#attributes']['src'] = $item['data']; + $js_element['#browsers'] = $item['browsers']; + $processed[$index++] = drupal_render($js_element); 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; } - 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']); + // 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; + // The key contains browsers array as part of it's unique value, use + // the first file set's value for this preprocessed version. + $file_set_values = array_values($file_set); + $item = array_shift($file_set_values); + $js_element['#browsers'] = $item['browsers']; + $processed[$key] = drupal_render($js_element); } } } + + // 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; } /** @@ -5095,10 +4982,10 @@ function drupal_build_js_cache($files) { if (empty($uri) || !file_exists($uri)) { // Build aggregate JS file. - foreach ($files as $info) { + foreach ($files as $path => $info) { if ($info['preprocess']) { // Append a ';' and a newline after each JS file to prevent them from running together. - $contents .= file_get_contents($info['data']) . ";\n"; + $contents .= file_get_contents($path) . ";\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 fc526cb..64131f3 100644 --- a/modules/simpletest/tests/common.test +++ b/modules/simpletest/tests/common.test @@ -1456,30 +1456,96 @@ class JavaScriptTestCase extends DrupalWebTestCase { 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)); + drupal_add_js('misc/collapse.js', array('browsers' => array( + 'IE' => 'lt IE 8', + '!IE' => FALSE, + ))); + drupal_add_js('misc/states.js', array('browsers' => array( + 'IE' => 'lte IE 6', + '!IE' => FALSE, + ))); + drupal_add_js('misc/textarea.js', array('browsers' => array( + 'IE' => 'gt IE 7', + '!IE' => TRUE, + ))); + drupal_add_js('misc/progress.js', array('browsers' => array( + 'IE' => 'lt IE 8', + '!IE' => FALSE, + ))); $javascript = drupal_get_js(); $expected = implode("\n", array( '', '', '', '', + "\n\n", + "\n", + "\n", + "", + '', + "", )); - $this->assertTrue(strpos($javascript, $expected) > 0, t('Unaggregated JavaScript is added in the expected group order.')); + $this->assertTrue(strpos($javascript, $expected) !== FALSE, 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'); + // Enable aggregation. variable_set('preprocess_js', 1); + + // Ensure a file is made for the 'every_page' files, one for different + // browser conditions and another for the rest. + drupal_static_reset('drupal_add_js'); + + // drupal_add_js() adds jquery.js, jquery.once.js and drupal.js if it's + // array is empty. This affects expect aggregation values, inject a fake + // settings array so they don't get added. + $settings = drupal_js_defaults(array()); + $settings['type'] = 'setting'; + $settings['group'] = JS_LIBRARY; + $drupal_add_js = &drupal_static('drupal_add_js', array()); + $drupal_add_js['settings'] = $settings; + + // Duplicate order and options above, but check for aggregated values. 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)); + drupal_add_js('misc/collapse.js', array('browsers' => array( + 'IE' => 'lt IE 8', + '!IE' => FALSE, + ))); + drupal_add_js('misc/states.js', array('browsers' => array( + 'IE' => 'lte IE 6', + '!IE' => FALSE, + ))); + drupal_add_js('misc/textarea.js', array('browsers' => array( + 'IE' => 'gt IE 7', + '!IE' => TRUE, + ))); + drupal_add_js('misc/progress.js', array('browsers' => array( + 'IE' => 'lt IE 8', + '!IE' => FALSE, + ))); $js_items = drupal_add_js(); $javascript = drupal_get_js(); $expected = implode("\n", array( '', '', + "\n\n", + "\n", + "", + '', + "", )); - $this->assertTrue(strpos($javascript, $expected) > 0, t('JavaScript is aggregated in the expected groups and order.')); + $this->assertTrue(strpos($javascript, $expected) !== FALSE, t('JavaScript is aggregated in the expected groups and order.')); } /** diff --git a/modules/system/system.module b/modules/system/system.module index bfeeb50..d4f3bc4 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -322,12 +322,6 @@ 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(