diff --git a/includes/common.inc b/includes/common.inc index 31923f2..f034fc8 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -70,6 +70,11 @@ define('CSS_DEFAULT', 0); define('CSS_THEME', 100); /** + * The default group for JavaScript settings added to the page. + */ +define('JS_SETTING', -200); + +/** * The default group for JavaScript and jQuery libraries added to the page. */ define('JS_LIBRARY', -100); @@ -4104,71 +4109,46 @@ 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. + * - need_jquery: If TRUE jquery.js and drupal.js are added automatically. + * Defaults to TRUE. * * @return * The current array of JavaScript files, settings, and in-line code, - * including Drupal defaults, anything previously added with calls to - * drupal_add_js(), and this function call's additions. + * anything previously added with calls to drupal_add_js(), this function + * call's additions, and Drupal's default settings. The default jQuery and + * Drupal libraries are only conditionally added later in + * drupal_add_js_page_defaults(). * * @see drupal_get_js() + * @see ajax_render() + * @see drupal_add_js_page_defaults() */ -function drupal_add_js($data = NULL, $options = NULL) { - $javascript = &drupal_static(__FUNCTION__, array()); +function &drupal_add_js($data = NULL, $options = NULL) { + $javascript = &drupal_static(__FUNCTION__); - // Construct the options, taking the defaults into consideration. - if (isset($options)) { - if (!is_array($options)) { - $options = array('type' => $options); - } - } - else { - $options = array(); + // Initialize default settings on first call. + if (!isset($javascript)) { + $javascript = drupal_js_settings_default(); } - $options += drupal_js_defaults($data); - - // Preprocess can only be set if caching is enabled. - $options['preprocess'] = $options['cache'] ? $options['preprocess'] : FALSE; - - // Tweak the weight so that files of the same weight are included in the - // order of the calls to drupal_add_js(). - $options['weight'] += count($javascript) / 1000; if (isset($data)) { - // Add jquery.js and drupal.js, as well as the basePath setting, the - // first time a JavaScript file is added. - if (empty($javascript)) { - // url() generates the prefix using hook_url_outbound_alter(). Instead of - // running the hook_url_outbound_alter() again here, extract the prefix - // from url(). - url('', array('prefix' => &$prefix)); - $javascript = array( - 'settings' => array( - 'data' => array( - array('basePath' => base_path()), - array('pathPrefix' => empty($prefix) ? '' : $prefix), - ), - 'type' => 'setting', - 'scope' => 'header', - 'group' => JS_LIBRARY, - 'every_page' => TRUE, - 'weight' => 0, - ), - 'misc/drupal.js' => array( - 'data' => 'misc/drupal.js', - 'type' => 'file', - 'scope' => 'header', - 'group' => JS_LIBRARY, - 'every_page' => TRUE, - 'weight' => -1, - 'preprocess' => TRUE, - 'cache' => TRUE, - 'defer' => FALSE, - ), - ); - // Register all required libraries. - drupal_add_library('system', 'jquery', TRUE); - drupal_add_library('system', 'jquery.once', TRUE); + // Construct the options, taking the defaults into consideration. + if (isset($options)) { + if (!is_array($options)) { + $options = array('type' => $options); + } + } + else { + $options = array(); } + $options += drupal_js_defaults($data); + + // Preprocess can only be set if caching is enabled. + $options['preprocess'] = $options['cache'] ? $options['preprocess'] : FALSE; + + // Tweak the weight so that files of the same weight are included in the + // order of the calls to drupal_add_js(). + $options['weight'] += count($javascript) / 1000; switch ($options['type']) { case 'setting': @@ -4187,10 +4167,82 @@ function drupal_add_js($data = NULL, $options = NULL) { $javascript[$options['data']] = $options; } } + return $javascript; } /** + * The default JavaScript settings for Drupal. + * + * @see drupal_add_js() + */ +function drupal_js_settings_default() { + $javascript = array(); + + // url() generates the prefix using hook_url_outbound_alter(). + // Instead of running the hook_url_outbound_alter() again here, extract + // the prefix from url(). + // @todo Make this less hacky: http://drupal.org/node/1547376. + url('', array('prefix' => &$prefix)); + + $javascript['settings'] = array( + 'data' => array( + array('basePath' => base_path()), + array('pathPrefix' => empty($prefix) ? '' : $prefix), + ), + 'type' => 'setting', + 'scope' => 'header', + 'group' => JS_SETTING, + 'every_page' => TRUE, + 'weight' => 0, + 'browsers' => array(), + ); + + return $javascript; +} + +/** + * Conditionally adds the default Drupal/jQuery libraries to the page. + * + * @see drupal_add_js() + * @see drupal_get_js() + * @see template_process_html() + */ +function drupal_add_js_page_defaults() { + $javascript = &drupal_add_js(); + + // If any JavaScript (except settings) has been added, include the required + // base libraries, if it's needed. + if (array_diff_key($javascript, array('settings' => 0))) { + $need_jquery = TRUE; + if (!variable_get('javascript_always_use_jquery', TRUE)) { + $need_jquery = FALSE; + foreach ($javascript as $key => $item) { + if ($item['type'] != 'setting' && !isset($item['need_jquery']) || $item['need_jquery']) { + $need_jquery = TRUE; + break; + } + } + } + + if ($need_jquery) { + drupal_add_library('system', 'jquery', TRUE); + drupal_add_library('system', 'jquery.once', TRUE); + + drupal_add_js('misc/drupal.js', array( + 'group' => JS_LIBRARY, + 'every_page' => TRUE, + 'weight' => -1, + )); + } + } + // Otherwise, remove all settings. + else { + $javascript = array(); + } +} + +/** * Constructs an array of the defaults that are used for JavaScript items. * * @param $data @@ -4211,6 +4263,7 @@ function drupal_js_defaults($data = NULL) { 'preprocess' => TRUE, 'version' => NULL, 'data' => $data, + 'need_jquery' => TRUE, ); } @@ -4240,7 +4293,7 @@ function drupal_js_defaults($data = NULL) { * $javascript, useful when the calling function passes a $javascript array * that has already been altered. * - * @return + * @return string * All JavaScript code segments and includes for the scope as HTML tags. * * @see drupal_add_js() @@ -4255,15 +4308,10 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS return ''; } - // Allow modules to alter the JavaScript. - if (!$skip_alter) { - drupal_alter('js', $javascript); - } - // Filter out elements of the given scope. $items = array(); foreach ($javascript as $key => $item) { - if ($item['scope'] == $scope) { + if (isset($item['scope']) && $item['scope'] == $scope) { $items[$key] = $item; } } @@ -4294,23 +4342,28 @@ 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='); + // Allow modules to alter the JavaScript for the given scope. + if (!$skip_alter) { + drupal_alter('js', $items); + } + // 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; + // If there is no 'settings' key or no actual JavaScript has been added + // to the page, then ajaxPageState settings are not needed either. + // @see drupal_add_js_page_defaults() + if (isset($items['settings']) && ($files = array_diff_key($items, array('settings' => 0)))) { + $setting['ajaxPageState']['js'] = array_fill_keys(array_keys($files), 1); + drupal_add_js($setting, 'setting'); + + // If the 'header' scope is output, then this is the final call to + // drupal_get_js() within page rendering, so directly inject the setting. + if ($scope == 'header') { + $items['settings']['data'][] = $setting; + } } // Loop through the JavaScript to construct the rendered output. diff --git a/includes/theme.inc b/includes/theme.inc index d1864c8..acd5c64 100644 --- a/includes/theme.inc +++ b/includes/theme.inc @@ -201,6 +201,30 @@ function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callb drupal_add_js($script, array('group' => JS_THEME, 'every_page' => TRUE)); } + // Add scripts that don't need jQuery or Drupal settings. + $scripts_special = array(); + + // Grab scripts from base theme + foreach ($base_theme as $base) { + if (!empty($base->info['scripts_special'])) { + foreach ($base->info['scripts_special'] as $name => $script) { + $scripts_special[$name] = $script; + } + } + } + + // Add scripts used by this theme. + if (!empty($theme->info['scripts_special'])) { + foreach ($theme->info['scripts_special'] as $name => $script) { + $scripts_special[$name] = $script; + } + } + + // Add scripts used by this theme. + foreach ($scripts_special as $script) { + drupal_add_js($script, array('group' => JS_THEME, 'every_page' => TRUE, 'need_jquery' => FALSE)); + } + $theme_engine = NULL; // Initialize the theme. @@ -2614,7 +2638,10 @@ function template_process_html(&$variables) { $variables['page_top'] = drupal_render($variables['page']['page_top']); $variables['page_bottom'] = drupal_render($variables['page']['page_bottom']); // Place the rendered HTML for the page body into a top level variable. - $variables['page'] = $variables['page']['#children']; + $variables['page'] = $variables['page']['#children']; + + // Conditionally add the default Drupal/jQuery libraries to the page. + drupal_add_js_page_defaults(); $variables['page_bottom'] .= drupal_get_js('footer'); $variables['head'] = drupal_get_html_head(); diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test index 82e3055..68adf64 100644 --- a/modules/simpletest/tests/common.test +++ b/modules/simpletest/tests/common.test @@ -1208,14 +1208,16 @@ class JavaScriptTestCase extends DrupalWebTestCase { * Test default JavaScript is empty. */ function testDefault() { - $this->assertEqual(array(), drupal_add_js(), t('Default JavaScript is empty.')); + $this->assertEqual(drupal_js_settings_default(), drupal_add_js(), t('Default JavaScript is empty.')); } /** * Test adding a JavaScript file. */ function testAddFile() { - $javascript = drupal_add_js('misc/collapse.js'); + drupal_add_js('misc/collapse.js'); + drupal_add_js_page_defaults(); + $javascript = drupal_add_js(); $this->assertTrue(array_key_exists('misc/jquery.js', $javascript), t('jQuery is added when a file is added.')); $this->assertTrue(array_key_exists('misc/drupal.js', $javascript), t('Drupal.js is added when file is added.')); $this->assertTrue(array_key_exists('misc/collapse.js', $javascript), t('JavaScript files are correctly added.')); @@ -1228,7 +1230,9 @@ class JavaScriptTestCase extends DrupalWebTestCase { * Test adding settings. */ function testAddSetting() { - $javascript = drupal_add_js(array('drupal' => 'rocks', 'dries' => 280342800), 'setting'); + drupal_add_js(array('drupal' => 'rocks', 'dries' => 280342800), 'setting'); + drupal_add_js('misc/jquery.js'); + $javascript = drupal_add_js(); $this->assertEqual(280342800, $javascript['settings']['data'][2]['dries'], t('JavaScript setting is set correctly.')); $this->assertEqual('rocks', $javascript['settings']['data'][2]['drupal'], t('The other JavaScript setting is set correctly.')); } @@ -1256,6 +1260,7 @@ class JavaScriptTestCase extends DrupalWebTestCase { // Only the second of these two entries should appear in Drupal.settings. drupal_add_js(array('commonTestArray' => array('key' => 'commonTestOldValue')), 'setting'); drupal_add_js(array('commonTestArray' => array('key' => 'commonTestNewValue')), 'setting'); + drupal_add_js('misc/jquery.js'); $javascript = drupal_get_js('header'); $this->assertTrue(strpos($javascript, 'basePath') > 0, t('Rendered JavaScript header returns basePath setting.')); @@ -1283,7 +1288,7 @@ class JavaScriptTestCase extends DrupalWebTestCase { function testReset() { drupal_add_js('misc/collapse.js'); drupal_static_reset('drupal_add_js'); - $this->assertEqual(array(), drupal_add_js(), t('Resetting the JavaScript correctly empties the cache.')); + $this->assertEqual(drupal_js_settings_default(), drupal_add_js(), t('Resetting the JavaScript correctly empties the cache.')); } /** @@ -1291,10 +1296,11 @@ class JavaScriptTestCase extends DrupalWebTestCase { */ function testAddInline() { $inline = 'jQuery(function () { });'; - $javascript = drupal_add_js($inline, array('type' => 'inline', 'scope' => 'footer')); + drupal_add_js($inline, array('type' => 'inline', 'scope' => 'footer')); + drupal_add_js_page_defaults(); + $javascript = drupal_add_js(); $this->assertTrue(array_key_exists('misc/jquery.js', $javascript), t('jQuery is added when inline scripts are added.')); - $data = end($javascript); - $this->assertEqual($inline, $data['data'], t('Inline JavaScript is correctly added to the footer.')); + $this->assertEqual($inline, $javascript[0]['data'], t('Inline JavaScript is correctly added.')); } /** @@ -1338,8 +1344,12 @@ class JavaScriptTestCase extends DrupalWebTestCase { * Test adding a JavaScript file with a different weight. */ function testDifferentWeight() { - $javascript = drupal_add_js('misc/collapse.js', array('weight' => 2)); - $this->assertEqual($javascript['misc/collapse.js']['weight'], 2, t('Adding a JavaScript file with a different weight caches the given weight.')); + drupal_add_js('core/misc/collapse.js', array('weight' => 2)); + drupal_add_js('core/misc/tabledrag.js', array('weight' => 999)); + drupal_add_js_page_defaults(); + $javascript = drupal_add_js(); + $this->assertEqual($javascript['core/misc/collapse.js']['weight'], 2.001, t('Adding a JavaScript file with a different weight caches the given weight.')); + $this->assertEqual($javascript['core/misc/tabledrag.js']['weight'], 999.002, t('Adding a JavaScript file with a different weight caches the given weight.')); } /** @@ -1434,6 +1444,7 @@ class JavaScriptTestCase extends DrupalWebTestCase { // flag, then by weight (see drupal_sort_css_js()), so to test the effect of // weight, we need the other two options to be the same. drupal_add_js('misc/collapse.js', array('group' => JS_LIBRARY, 'every_page' => TRUE, 'weight' => -21)); + drupal_add_js_page_defaults(); $javascript = drupal_get_js(); $this->assertTrue(strpos($javascript, 'misc/collapse.js') < strpos($javascript, 'misc/jquery.js'), t('Rendering a JavaScript file above jQuery.')); } @@ -1546,6 +1557,61 @@ class JavaScriptTestCase extends DrupalWebTestCase { $query_string = variable_get('css_js_query_string', '0'); $this->assertRaw(drupal_get_path('module', 'node') . '/node.js?' . $query_string, t('Query string was appended correctly to js.')); } + + /** + * Tests JavaScript default settings and libraries. + * + * Note: Where this test says "no JavaScript", it means no Drupal and jQuery + * scripts. The html5shiv is expected to appear always. + */ + function testJavaScriptDefaults() { + // A regular page should not contain any JavaScript by default. + $this->drupalGet(''); + $this->assertNoRaw('Drupal.settings'); + $this->assertNoRaw('jquery'); + + // Repeat, with test helper. + $this->resetStaticVariables(); + $this->drupalRenderPage(); + $this->assertNoRaw('Drupal.settings'); + $this->assertNoRaw('jquery'); + + // When only settings are added in a request, no JavaScript should appear. + $this->resetStaticVariables(); + drupal_add_js(array('foo' => 'bar'), 'setting'); + $this->drupalRenderPage(); + $this->assertNoRaw('Drupal.settings'); + $this->assertNoRaw('jquery'); + + // When adding any other JavaScript, default libraries and settings should + // appear. + $this->resetStaticVariables(); + drupal_add_js('core/misc/ajax.js'); + $this->drupalRenderPage(); + $this->assertRaw('Drupal.settings'); + $this->assertRaw('jquery'); + } + + /** + * Sets the rendered JavaScript as browser content and logs it verbosely. + */ + function drupalRenderPage() { + // drupal_get_js() is called for each region/scope during page rendering. + // @see template_process_html() + $page = drupal_render_page(''); + $this->drupalSetContent($page); + $this->verbose($this->drupalGetContent()); + } + + /** + * Resets static variables to render a new page. + */ + function resetStaticVariables() { + drupal_static_reset('drupal_add_js'); + drupal_static_reset('drupal_add_css'); + drupal_static_reset('drupal_add_library'); + drupal_static_reset('drupal_get_library'); + } } /** diff --git a/modules/system/system.module b/modules/system/system.module index 2bbcd7f..ae204d5 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -2528,6 +2528,7 @@ function _system_rebuild_theme_data() { 'php' => DRUPAL_MINIMUM_PHP, 'stylesheets' => array(), 'scripts' => array(), + 'scripts_special' => array(), ); $sub_themes = array(); @@ -2564,6 +2565,7 @@ function _system_rebuild_theme_data() { $path = dirname($theme->uri); $theme->info['stylesheets'] = _system_info_add_path($theme->info['stylesheets'], $path); $theme->info['scripts'] = _system_info_add_path($theme->info['scripts'], $path); + $theme->info['scripts_special'] = _system_info_add_path($theme->info['scripts_special'], $path); // Give the screenshot proper path information. if (!empty($themes[$key]->info['screenshot'])) {