Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.795 diff -u -r1.795 common.inc --- includes/common.inc 17 Sep 2008 07:11:56 -0000 1.795 +++ includes/common.inc 18 Sep 2008 00:35:27 -0000 @@ -25,6 +25,26 @@ define('SAVED_DELETED', 3); /** + * Weight of JavaScript for files loaded from core (files in /misc/). + */ +define('JS_CORE_WEIGHT', -20); + +/** + * Weight of JavaScript files loaded by modules. + */ +define('JS_MODULE_WEIGHT', 0); + +/** + * Weight of JavaScript files loaded by themes. + */ +define('JS_THEME_WEIGHT', 20); + +/** + * Weight of JavaScript libraries needed by other scripts, eg. jQuery. + */ +define('JS_LIBRARY_WEIGHT', -100); + +/** * Set content for a specified region. * * @param $region @@ -1490,9 +1510,9 @@ function l($text, $path, $options = array()) { // Merge in defaults. $options += array( - 'attributes' => array(), - 'html' => FALSE, - ); + 'attributes' => array(), + 'html' => FALSE, + ); // Append active class. if ($path == $_GET['q'] || ($path == '' && drupal_is_front_page())) { @@ -1957,16 +1977,17 @@ * reference to an existing file or as inline code. The following actions can be * performed using this function: * - * - Add a file ('core', 'module' and 'theme'): - * Adds a reference to a JavaScript file to the page. JavaScript files - * are placed in a certain order, from 'core' first, to 'module' and finally - * 'theme' so that files, that are added later, can override previously added - * files with ease. + * - Add a file ('file'): + * Adds a reference to a JavaScript file to the page and supports files on + * external servers. JavaScript files are placed in a certain order depending + * on their given weight. Files from core, modules, and themes have predefined + * constants for their weights of JS_CORE_WEIGHT (-20), JS_MODULE_WEIGHT (0), + * and JS_THEME_WEIGHT (20). * * - Add inline JavaScript code ('inline'): * Executes a piece of JavaScript code on the current page by placing the code - * directly in the page. This can, for example, be useful to tell the user that - * a new message arrived, by opening a pop up, alert box etc. + * directly in the page. This can, for example, be useful to tell the user + * that a new message arrived, by opening a pop up, alert box, etc. * * - Add settings ('setting'): * Adds a setting to Drupal's global storage of JavaScript settings. Per-page @@ -1974,92 +1995,192 @@ * will be accessible at Drupal.settings. * * @param $data - * (optional) If given, the value depends on the $type parameter: - * - 'core', 'module' or 'theme': Path to the file relative to base_path(). + * (optional) If given, the value depends on the $options parameter: + * - 'file': Either the path to the file relative to base_path(), or the + * absolute URL to the Javascript file on an external server. * - 'inline': The JavaScript code that should be placed in the given scope. * - 'setting': An array with configuration options as associative array. The - * array is directly placed in Drupal.settings. You might want to wrap your - * actual configuration settings in another variable to prevent the pollution - * of the Drupal.settings namespace. - * @param $type - * (optional) The type of JavaScript that should be added to the page. Allowed - * values are 'core', 'module', 'theme', 'inline' and 'setting'. You - * can, however, specify any value. It is treated as a reference to a JavaScript - * file. Defaults to 'module'. - * @param $scope - * (optional) The location in which you want to place the script. Possible - * values are 'header' and 'footer' by default. If your theme implements - * different locations, however, you can also use these. - * @param $defer - * (optional) If set to TRUE, the defer attribute is set on the \n"; + if (!empty($data)) { + $renderable_js[] = array( + 'type' => 'setting', + 'data' => $data, + ); + } break; case 'inline': - foreach ($data as $info) { - $output .= '\n"; + foreach ($data as $js => $options) { + $renderable_js[] = array( + 'type' => 'inline', + 'data' => $js, + 'attributes' => $options['attributes'], + ); } break; + case 'file': default: - // If JS preprocessing is off, we still need to output the scripts. - // Additionally, go through any remaining scripts if JS preprocessing is on and output the non-cached ones. foreach ($data as $path => $info) { - if (!$info['preprocess'] || !$is_writable || !$preprocess_js) { - $no_preprocess[$type] .= '\n"; + // Create the source of the Javscript, checking if base_path() + // is required, as well as placing the dummy query string at the + // end of the path. + if (valid_url($path, TRUE)) { + $info['attributes']['src'] = $path; } else { - $files[$path] = $info; + $info['attributes']['src'] = base_path() . $path . ($info['cache'] ? $query_string : '?' . time()); } + $renderable_js[] = array( + 'type' => 'file', + 'attributes' => $info['attributes'] + ); } } } + + return theme('scripts', $renderable_js, $scope); +} - // Aggregate any remaining JS files that haven't already been output. - if ($is_writable && $preprocess_js && count($files) > 0) { - $filename = md5(serialize($files) . $query_string) . '.js'; - $preprocess_file = drupal_build_js_cache($files, $filename); - $preprocessed .= '' . "\n"; - } - - // 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. - $output = $preprocessed . implode('', $no_preprocess) . $output; +/** + * Themes a list of scripts, which are then rendered to the page. + * + * @param $scripts + * An array of scripts to be output. + * @param $scope + * The scope of the JavaScript: where it's going to be located + * on the page. + * @return + * HTML containing the script tags. + */ +function theme_scripts($scripts, $scope) { + $output = ''; + foreach ($scripts as $script) { + switch ($script['type']) { + case 'setting': + $output .= '\n"; + break; + case 'inline': + $output .= '' . $script['data'] . "\n"; + break; + case 'file': + $output .= '\n"; + } + } return $output; } @@ -2264,7 +2501,7 @@ function drupal_add_tabledrag($table_id, $action, $relationship, $group, $subgroup = NULL, $source = NULL, $hidden = TRUE, $limit = 0) { static $js_added = FALSE; if (!$js_added) { - drupal_add_js('misc/tabledrag.js', 'core'); + drupal_add_js('misc/tabledrag.js', array('weight' => JS_CORE_WEIGHT)); $js_added = TRUE; } @@ -2284,15 +2521,15 @@ /** * Aggregate JS files, putting them in the files directory. - * + * * @param $files * An array of JS files to aggregate and compress into one file. * @param $filename * The name of the aggregate JS file. * @return - * The name of the JS file. + * The name of the JS file or FALSE on failure. */ -function drupal_build_js_cache($files, $filename) { +function drupal_aggregate_js($files, $filename) { $contents = ''; // Create the js/ within the files folder. @@ -2300,11 +2537,15 @@ file_check_directory($jspath, FILE_CREATE_DIRECTORY); if (!file_exists($jspath . '/' . $filename)) { - // Build aggregate JS file. foreach ($files as $path => $info) { - if ($info['preprocess']) { - // Append a ';' after each JS file to prevent them from running together. - $contents .= file_get_contents($path) . ';'; + $tmp = file_get_contents($path); + if ($tmp) { + // Append a ';' after each JS file to prevent them from running + // together. + $contents .= $tmp . ';'; + } + else { + return FALSE; } } @@ -2316,10 +2557,36 @@ } /** + * Cache locally external JS files. + * + * @param $path + * A URL to an external JavaScript file. + * @return + * A string representing the local version or FALSE if caching failed. + */ +function drupal_cache_external_js($path) { + + // Create the js/ within the files folder. + $jspath = file_create_path('js'); + if (!file_check_directory($jspath, FILE_CREATE_DIRECTORY)) { + return FALSE; + } + + $request = drupal_http_request($path); + if ($request->code == '200') { + $filename = md5($path) . '.js'; + return file_save_data($request->data, $jspath . '/' . $filename, FILE_EXISTS_REPLACE); + } + + return FALSE; +} + +/** * Delete all cached JS files. */ function drupal_clear_js_cache() { file_scan_directory(file_create_path('js'), '.*', array('.', '..', 'CVS'), 'file_delete', TRUE); + // TODO: Move this locale.module-specific code into locale.module somehow. variable_set('javascript_parsed', array()); } @@ -2327,6 +2594,11 @@ * Converts a PHP variable into its Javascript equivalent. * * We use HTML-safe strings, i.e. with <, > and & escaped. + * @param $var + * The variable to encode. + * @return + * A string, which is a JavaScript-encoded version of $var. + * @see drupal_json */ function drupal_to_js($var) { // json_encode() does not escape <, > and &, so we do it with str_replace() @@ -2341,6 +2613,7 @@ * * @param $var * (optional) If set, the variable will be converted to JSON and output. + * @see drupal_to_js */ function drupal_json($var = NULL) { // We are returning JavaScript, so tell the browser. @@ -3083,6 +3356,10 @@ 'form_element' => array( 'arguments' => array('element' => NULL, 'value' => NULL), ), + // theme_scripts() exists in form.inc. + 'scripts' => array( + 'arguments' => array('scripts' => NULL, 'scope' => NULL), + ), ); } Index: includes/batch.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/batch.inc,v retrieving revision 1.20 diff -u -r1.20 batch.inc --- includes/batch.inc 24 Jun 2008 21:51:02 -0000 1.20 +++ includes/batch.inc 18 Sep 2008 00:35:27 -0000 @@ -80,7 +80,7 @@ // and the initialization and error messages. $current_set = _batch_current_set(); drupal_set_title($current_set['title']); - drupal_add_js('misc/progress.js', 'core', 'header', FALSE, FALSE); + drupal_add_js('misc/progress.js', array('cache' => FALSE, 'weight' => JS_CORE_WEIGHT, 'attributes' => array('defer' => 'defer'))); $url = url($batch['url'], array('query' => array('id' => $batch['id']))); $js_setting = array( @@ -91,7 +91,7 @@ ), ); drupal_add_js($js_setting, 'setting'); - drupal_add_js('misc/batch.js', 'core', 'header', FALSE, FALSE); + drupal_add_js('misc/batch.js', array('cache' => FALSE, 'weight' => JS_CORE_WEIGHT, 'attributes' => array('defer' => 'defer'))); $output = '
'; return $output; Index: includes/theme.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/theme.inc,v retrieving revision 1.435 diff -u -r1.435 theme.inc --- includes/theme.inc 15 Sep 2008 20:48:07 -0000 1.435 +++ includes/theme.inc 18 Sep 2008 00:35:27 -0000 @@ -158,7 +158,7 @@ // Add scripts used by this theme. foreach ($final_scripts as $script) { - drupal_add_js($script, 'theme'); + drupal_add_js($script, array('weight' => JS_THEME_WEIGHT)); } $theme_engine = NULL; @@ -1285,7 +1285,7 @@ // Add sticky headers, if applicable. if (count($header)) { - drupal_add_js('misc/tableheader.js'); + drupal_add_js('misc/tableheader.js', array('weight' => JS_CORE_WEIGHT)); // Add 'sticky-enabled' class to the table to identify it for JS. // This is needed to target tables constructed by this function. $attributes['class'] = empty($attributes['class']) ? 'sticky-enabled' : ($attributes['class'] . ' sticky-enabled'); @@ -1402,7 +1402,7 @@ * Returns a header cell for tables that have a select all functionality. */ function theme_table_select_header_cell() { - drupal_add_js('misc/tableselect.js'); + drupal_add_js('misc/tableselect.js', array('weight' => JS_CORE_WEIGHT)); return array('class' => 'select-all'); } Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.283 diff -u -r1.283 form.inc --- includes/form.inc 17 Sep 2008 07:11:56 -0000 1.283 +++ includes/form.inc 18 Sep 2008 00:35:27 -0000 @@ -1488,7 +1488,7 @@ */ function theme_fieldset($element) { if ($element['#collapsible']) { - drupal_add_js('misc/collapse.js'); + drupal_add_js('misc/collapse.js', array('weight' => JS_CORE_WEIGHT)); if (!isset($element['#attributes']['class'])) { $element['#attributes']['class'] = ''; @@ -1788,8 +1788,8 @@ // Adding the same javascript settings twice will cause a recursion error, // we avoid the problem by checking if the javascript has already been added. if (isset($element['#ahah']['path']) && isset($element['#ahah']['event']) && !isset($js_added[$element['#id']])) { - drupal_add_js('misc/jquery.form.js'); - drupal_add_js('misc/ahah.js'); + drupal_add_js('misc/jquery.form.js', array('weight' => JS_CORE_WEIGHT)); + drupal_add_js('misc/ahah.js', array('weight' => JS_CORE_WEIGHT)); $ahah_binding = array( 'url' => url($element['#ahah']['path']), @@ -1814,7 +1814,7 @@ // Add progress.js if we're doing a bar display. if ($ahah_binding['progress']['type'] == 'bar') { - drupal_add_js('misc/progress.js'); + drupal_add_js('misc/progress.js', array('weight' => JS_CORE_WEIGHT)); } drupal_add_js(array('ahah' => array($element['#id'] => $ahah_binding)), 'setting'); @@ -2001,7 +2001,7 @@ $output = ''; if ($element['#autocomplete_path']) { - drupal_add_js('misc/autocomplete.js'); + drupal_add_js('misc/autocomplete.js', array('weight' => JS_CORE_WEIGHT)); $class[] = 'form-autocomplete'; $extra = ''; } @@ -2053,16 +2053,14 @@ // Add teaser behavior (must come before resizable) if (!empty($element['#teaser'])) { - drupal_add_js('misc/teaser.js'); - // Note: arrays are merged in drupal_get_js(). - drupal_add_js(array('teaserCheckbox' => array($element['#id'] => $element['#teaser_checkbox'])), 'setting'); - drupal_add_js(array('teaser' => array($element['#id'] => $element['#teaser'])), 'setting'); + drupal_add_js('misc/teaser.js', array('weight' => JS_CORE_WEIGHT)); + drupal_add_js(array('teaserCheckbox' => array($element['#id'] => $element['#teaser_checkbox']), 'teaser' => array($element['#id'] => $element['#teaser'])), 'setting'); $class[] = 'teaser'; } // Add resizable behavior if ($element['#resizable'] !== FALSE) { - drupal_add_js('misc/textarea.js'); + drupal_add_js('misc/textarea.js', array('weight' => JS_CORE_WEIGHT)); $class[] = 'resizable'; } Index: modules/node/node.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.admin.inc,v retrieving revision 1.26 diff -u -r1.26 node.admin.inc --- modules/node/node.admin.inc 15 Sep 2008 20:48:08 -0000 1.26 +++ modules/node/node.admin.inc 18 Sep 2008 00:35:27 -0000 @@ -254,7 +254,7 @@ $form['filters']['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset')); } - drupal_add_js('misc/form.js', 'core'); + drupal_add_js('misc/form.js', array('weight' => JS_CORE_WEIGHT)); return $form; } Index: modules/simpletest/simpletest.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.module,v retrieving revision 1.12 diff -u -r1.12 simpletest.module --- modules/simpletest/simpletest.module 10 Sep 2008 04:13:01 -0000 1.12 +++ modules/simpletest/simpletest.module 18 Sep 2008 00:35:27 -0000 @@ -203,7 +203,7 @@ */ function theme_simpletest_test_form($form) { drupal_add_css(drupal_get_path('module', 'simpletest') .'/simpletest.css', 'module'); - drupal_add_js(drupal_get_path('module', 'simpletest') .'/simpletest.js', 'module'); + drupal_add_js(drupal_get_path('module', 'simpletest') .'/simpletest.js'); $header = array( array('data' => t('Run'), 'class' => 'simpletest_run checkbox'), array('data' => t('Test'), 'class' => 'simpletest_test'), Index: modules/user/user.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.admin.inc,v retrieving revision 1.26 diff -u -r1.26 user.admin.inc --- modules/user/user.admin.inc 17 Sep 2008 07:11:59 -0000 1.26 +++ modules/user/user.admin.inc 18 Sep 2008 00:35:27 -0000 @@ -83,7 +83,7 @@ ); } - drupal_add_js('misc/form.js', 'core'); + drupal_add_js('misc/form.js', array('weight' => JS_CORE_WEIGHT)); return $form; } Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.922 diff -u -r1.922 user.module --- modules/user/user.module 17 Sep 2008 07:11:59 -0000 1.922 +++ modules/user/user.module 18 Sep 2008 00:35:27 -0000 @@ -2127,7 +2127,7 @@ global $user; // Only need to do once per page. if (!$complete) { - drupal_add_js(drupal_get_path('module', 'user') . '/user.js', 'module'); + drupal_add_js(drupal_get_path('module', 'user') . '/user.js'); drupal_add_js(array( 'password' => array( Index: modules/block/block-admin-display-form.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/modules/block/block-admin-display-form.tpl.php,v retrieving revision 1.6 diff -u -r1.6 block-admin-display-form.tpl.php --- modules/block/block-admin-display-form.tpl.php 15 May 2008 21:30:02 -0000 1.6 +++ modules/block/block-admin-display-form.tpl.php 18 Sep 2008 00:35:27 -0000 @@ -26,7 +26,7 @@ ?> JS_CORE_WEIGHT)); drupal_add_js(drupal_get_path('module', 'block') . '/block.js'); foreach ($block_regions as $region => $title) { drupal_add_tabledrag('blocks', 'match', 'sibling', 'block-region-select', 'block-region-' . $region, NULL, FALSE); Index: modules/simpletest/tests/common.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v retrieving revision 1.7 diff -u -r1.7 common.test --- modules/simpletest/tests/common.test 17 Sep 2008 07:01:31 -0000 1.7 +++ modules/simpletest/tests/common.test 18 Sep 2008 00:35:27 -0000 @@ -251,3 +251,373 @@ } } } + +class JavaScriptAddRemoveTestCase extends DrupalWebTestCase { + /** + * Implementation of getInfo(). + */ + function getInfo() { + return array( + 'name' => t('JavaScript add/remove test'), + 'description' => t('Tests the various options to add/remove JavaScript from a page.'), + 'group' => t('System'), + ); + } + + function test_drupal_add_js() { + // Clear out all cached JavaScript files. + drupal_clear_js_cache(); + + // Setup some variables needed throughout this test. + $jspath = file_create_path('js'); + file_check_directory($jspath, FILE_CREATE_DIRECTORY); + + // Reset the JavaScript. + $original_js = drupal_add_js(NULL, array(), TRUE); + + // Regex for reading relevant values from