Index: includes/ajax.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/ajax.inc,v retrieving revision 1.31 diff -u -p -r1.31 ajax.inc --- includes/ajax.inc 30 Apr 2010 08:07:54 -0000 1.31 +++ includes/ajax.inc 12 Aug 2010 11:11:48 -0000 @@ -193,12 +193,11 @@ * functions. */ function ajax_render($commands = array()) { - // Automatically extract any 'settings' added via drupal_add_js() and make - // them the first command. - $scripts = drupal_add_js(NULL, NULL); - if (!empty($scripts['settings'])) { - array_unshift($commands, ajax_command_settings(call_user_func_array('array_merge_recursive', $scripts['settings']['data']))); - } + // AJAX responses are returned as JSON and not themed via html.tpl.php. Thus, + // markup returned by drupal_get_js() is not contained, but the client needs + // to know the JavaScript settings and files required by the new content, so + // we call ajax_get_js() to get that. + $commands = array_merge(ajax_get_js(), $commands); // Allow modules to alter any AJAX response. drupal_alter('ajax_render', $commands); @@ -207,6 +206,113 @@ function ajax_render($commands = array() } /** + * Returns an AJAX commands array used by the client to load JavaScript code for the current page. + * + * This function is only invoked for AJAX requests and is the equivalent of + * drupal_get_js() for full page requests, with the following differences: + * - By default, all scopes are processed at once, as AJAX responses typically + * do not contain the same kind of 'header' and 'footer' scopes like full + * page responses. + * - Files are not aggregated, because we do not know, which source files have + * already been loaded as part of the base page. The client-side code will + * determine new files and only load those. + * - The files are returned as AJAX commands for use by ajax_render() and to be + * processed by client-side JavaScript code, rather than raw HTML SCRIPT tags. + * + * @param $scope + * (optional) The scope for which the JavaScript code should be returned. If + * not specified, returns the code for all scopes. + * @param $javascript + * (optional) An array with all JavaScript code. Defaults to the default + * JavaScript array for the given scope. + * + * @return + * An array of commands suitable for use with the ajax_render() function. + * + * @see drupal_get_js() + */ +function ajax_get_js($scope = NULL, $javascript = NULL) { + if (!isset($javascript)) { + $javascript = drupal_add_js(); + } + if (empty($javascript)) { + return array(); + } + + // Allow modules to alter the JavaScript. + drupal_alter('js', $javascript); + + // Filter out elements of the given scope. + if (isset($scope)) { + $items = array(); + foreach ($javascript as $item) { + if ($item['scope'] == $scope) { + $items[] = $item; + } + } + } + else { + $items = array_values($javascript); + } + + uasort($items, 'drupal_sort_weight'); + + // @see drupal_get_js() + $default_query_string = variable_get('css_js_query_string', '0'); + $js_version_string = variable_get('drupal_js_version_query_string', 'v='); + + foreach ($items as $item) { + switch ($item['type']) { + case 'setting': + // drupal_add_js() puts all settings into a single $item of type + // 'setting', so we can do a simple assignment here, rather than adding + // to an array. + $settings = $item['data']; + break; + + case 'inline': + // @todo Presently, inline JavaScript code is ignored and not returned + // to the client. Evaluate the use-cases of AJAX responses that depend + // on inline JavaScript code to determine how to best handle that. + break; + + case 'file': + // To force browsers to refresh stale files, determine a suitable + // query string to append to the file URL. + // Volatile files use a query string that changes on every request. + if (!$item['cache']) { + $query_string = REQUEST_TIME; + } + // Versioned files use their version information to control caching. + elseif (!empty($item['version'])) { + $query_string = $js_version_string . $item['version']; + } + // Non-volatile non-versioned files use a query string that only changes + // when caches are flushed. + else { + $query_string = $default_query_string; + } + $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?'; + $js_files[] = file_create_url($item['data']) . $query_string_separator . $query_string; + break; + + case 'external': + $js_files[] = $item['data']; + break; + } + } + + $commands = array(); + if (!empty($settings)) { + $commands[] = ajax_command_settings(call_user_func_array('array_merge_recursive', $settings)); + } + if (!empty($js_files)) { + $commands[] = ajax_command_scripts($js_files); + } + return $commands; +} + +/** * Get a form submitted via #ajax during an AJAX callback. * * This will load a form from the form cache used during AJAX operations. It @@ -955,3 +1061,26 @@ function ajax_command_restripe($selector ); } +/** + * Creates scripts command for the AJAX responder. + * + * This will add JavaScript files to the output. Files that have already been + * processed will not be processed again. + * + * This command is implemented by Drupal.ajax.prototype.commands.scripts() + * defined in misc/ajax.js. + * + * @param $files + * The $files array is a simple array of filenames that match, exactly, the + * scripts as they are added to the page or will be added to the page in the + * future. + * + * @return + * An array suitable for use with the ajax_render() function. + */ +function ajax_command_scripts($files) { + return array( + 'command' => 'scripts', + 'files' => $files, + ); +} Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.1204 diff -u -p -r1.1204 common.inc --- includes/common.inc 10 Aug 2010 01:00:42 -0000 1.1204 +++ includes/common.inc 12 Aug 2010 11:37:37 -0000 @@ -3827,6 +3827,7 @@ function drupal_get_js($scope = 'header' 'type' => 'text/javascript', ), ); + foreach ($items as $item) { $query_string = empty($item['version']) ? $default_query_string : $js_version_string . $item['version']; @@ -3852,12 +3853,13 @@ function drupal_get_js($scope = 'header' case 'file': $js_element = $element; + if ($item['defer']) { + $js_element['#attributes']['defer'] = 'defer'; + } + $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?'; + $uri = file_create_url($item['data']) . $query_string_separator . ($item['cache'] ? $query_string : REQUEST_TIME); 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['#attributes']['src'] = $uri; $processed[$index++] = theme('html_tag', array('element' => $js_element)); } else { @@ -3866,6 +3868,7 @@ function drupal_get_js($scope = 'header' $key = 'aggregate' . $index; $processed[$key] = ''; $files[$key][$item['data']] = $item; + $reported_files[$uri] = 1; } break; @@ -3885,7 +3888,7 @@ function drupal_get_js($scope = 'header' 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 + // Only include the file if it was written successfully. Errors are logged // using watchdog. if ($uri) { $preprocess_file = file_create_url($uri); @@ -3894,6 +3897,15 @@ function drupal_get_js($scope = 'header' $processed[$key] = theme('html_tag', array('element' => $js_element)); } } + + // Report the list of aggregated JavaScript files. Required to allow AJAX + // processing on the client-side to keep track of all files already loaded. + // Non-aggregated files can be enumerated directly on the page. + $js_element = $element; + $js_element['#value_prefix'] = $embed_prefix; + $js_element['#value'] = 'jQuery.extend(Drupal.ajaxFiles.scripts, ' . drupal_json_encode($reported_files) . ");"; + $js_element['#value_suffix'] = $embed_suffix; + $processed[] = theme('html_tag', array('element' => $js_element)); } // Keep the order of JS files consistent as some are preprocessed and others are not. Index: misc/ajax.js =================================================================== RCS file: /cvs/drupal/drupal/misc/ajax.js,v retrieving revision 1.18 diff -u -p -r1.18 ajax.js --- misc/ajax.js 25 Jun 2010 20:34:07 -0000 1.18 +++ misc/ajax.js 12 Aug 2010 11:46:52 -0000 @@ -16,6 +16,14 @@ Drupal.ajax = Drupal.ajax || {}; /** + * Holds a list of files known to be on the page so that AJAX requests can + * selectively load additionally required files later on. + * + * @see Drupal.ajax.prototype.commands.scripts() + */ +Drupal.ajaxFiles = Drupal.ajaxFiles || { scripts: {}, css: {} }; + +/** * Attaches the AJAX behavior to each AJAX form element. */ Drupal.behaviors.AJAX = { @@ -427,6 +435,36 @@ Drupal.ajax.prototype.commands = { }, /** + * Command to add scripts to the page. + * + * Keeps track of files already on the page and attempts to only load + * additionally required scripts. + */ + scripts: function (ajax, response, status) { + // Build a list of scripts already loaded. Aggregated files have already + // been registered in Drupal.ajaxFiles.scripts via drupal_get_js(). + // Non-aggregated files can simply be enumerated by script tags on the page. + $('script').each(function () { + Drupal.ajaxFiles.scripts[this.src] = this.src; + }); + + var html = ''; + for (var file in response.files) { + // @todo Files in this stack are based on their 'src' attribute and/or a + // previous AJAX request. Their URIs may contain query strings, domain + // names, and other additions that can conflict with this simple key + // check. Consider to store and use basenames? + if (!Drupal.ajaxFiles.scripts[file]) { + Drupal.ajaxFiles.scripts[file] = file; + html += ''; + } + } + if (html) { + $('html').prepend(html); + } + }, + + /** * Command to attach data using jQuery's data API. */ data: function (ajax, response, status) { Index: modules/field/tests/field.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/tests/field.test,v retrieving revision 1.37 diff -u -p -r1.37 field.test --- modules/field/tests/field.test 8 Aug 2010 02:18:53 -0000 1.37 +++ modules/field/tests/field.test 12 Aug 2010 10:53:40 -0000 @@ -1439,7 +1439,7 @@ class FieldFormTestCase extends FieldTes // Press 'add more' button through AJAX, and place the expected HTML result // as the tested content. $commands = $this->drupalPostAJAX(NULL, $edit, $this->field_name . '_add_more'); - $this->content = $commands[1]['data']; + $this->content = $commands[2]['data']; for ($delta = 0; $delta <= $delta_range; $delta++) { $this->assertFieldByName("$this->field_name[$langcode][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value"); Index: modules/poll/poll.test =================================================================== RCS file: /cvs/drupal/drupal/modules/poll/poll.test,v retrieving revision 1.37 diff -u -p -r1.37 poll.test --- modules/poll/poll.test 5 Aug 2010 23:53:38 -0000 1.37 +++ modules/poll/poll.test 12 Aug 2010 10:53:40 -0000 @@ -342,7 +342,7 @@ class PollJSAddChoice extends DrupalWebT // Press 'add choice' button through AJAX, and place the expected HTML result // as the tested content. $commands = $this->drupalPostAJAX(NULL, $edit, array('op' => t('More choices'))); - $this->content = $commands[1]['data']; + $this->content = $commands[2]['data']; $this->assertFieldByName('choice[chid:0][chtext]', $edit['choice[new:0][chtext]'], t('Field !i found', array('!i' => 0))); $this->assertFieldByName('choice[chid:1][chtext]', $edit['choice[new:1][chtext]'], t('Field !i found', array('!i' => 1))); Index: modules/simpletest/tests/ajax.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax.test,v retrieving revision 1.14 diff -u -p -r1.14 ajax.test --- modules/simpletest/tests/ajax.test 5 Aug 2010 23:53:38 -0000 1.14 +++ modules/simpletest/tests/ajax.test 12 Aug 2010 11:57:19 -0000 @@ -11,17 +11,25 @@ class AJAXTestCase extends DrupalWebTest } /** - * Returns the passed-in commands array without the initial settings command. + * Returns the passed-in commands array without the commands automatically prepended by ajax_render(). * * Depending on factors that may be irrelevant to a particular test, - * ajax_render() may prepend a settings command. This function allows the test - * to only have to concern itself with the commands that were passed to - * ajax_render(). + * ajax_render() may prepend some commands for adding JavaScript files and + * settings. This function allows the test to only have to concern itself with + * the commands that were passed to ajax_render(). + * + * @todo This function is named discardSettings() for legacy reasons, because + * at one time, ajax_render() only added a 'settings' command. For Drupal 8, + * or when a BC break of Drupal 7 tests is acceptable, rename this function + * to be more accurate. */ protected function discardSettings($commands) { if ($commands[0]['command'] == 'settings') { array_shift($commands); } + if ($commands[0]['command'] == 'scripts') { + array_shift($commands); + } return $commands; } } @@ -43,10 +51,17 @@ class AJAXFrameworkTestCase extends AJAX */ function testAJAXRender() { $result = $this->drupalGetAJAX('ajax-test/render'); - // Verify that JavaScript settings are contained (always first). - $this->assertIdentical($result[0]['command'], 'settings', t('drupal_add_js() settings are contained first.')); - // Verify that basePath is contained in JavaScript settings. - $this->assertEqual($result[0]['settings']['basePath'], base_path(), t('Base path is contained in JavaScript settings.')); + + // Verify that there is a command to load settings added with + // drupal_add_js(). + $this->assertIdentical($result[0]['command'], 'settings', t('ajax_render() added a settings command to load settings added with drupal_add_js().')); + $this->assertIdentical($result[0]['settings']['basePath'], base_path(), t('The %setting setting is included.', array('%setting' => 'basePath'))); + + // Verify that there is a command to load script files added with + // drupal_add_js(). + $this->assertIdentical($result[1]['command'], 'scripts', t('ajax_render() added a scripts command to load files added with drupal_add_js().')); + $file = file_create_url('misc/drupal.js') . '?' . variable_get('css_js_query_string', '0'); + $this->assertTrue(in_array($file, $result[1]['files']), t('The %file file is included.', array('%file' => 'misc/drupal.js'))); } /** @@ -84,11 +99,11 @@ class AJAXCommandsTestCase extends AJAXT function testAJAXRender() { $commands = array(); $commands[] = ajax_command_settings(array('foo' => 42)); - $result = $this->drupalGetAJAX('ajax-test/render', array('query' => array('commands' => $commands))); - // Verify that JavaScript settings are contained (always first). - $this->assertIdentical($result[0]['command'], 'settings', t('drupal_add_js() settings are contained first.')); + // discardSettings() discards only what is automatically added by + // ajax_render(), not the one added by the ajax-test/render callback. + $result = $this->discardSettings($this->drupalGetAJAX('ajax-test/render', array('query' => array('commands' => $commands)))); // Verify that the custom setting is contained. - $this->assertEqual($result[1]['settings']['foo'], 42, t('Custom setting is output.')); + $this->assertEqual($result[0]['settings']['foo'], 42, t('Custom setting is output.')); } /** Index: modules/simpletest/tests/ajax_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax_test.module,v retrieving revision 1.3 diff -u -p -r1.3 ajax_test.module --- modules/simpletest/tests/ajax_test.module 13 Mar 2010 06:55:50 -0000 1.3 +++ modules/simpletest/tests/ajax_test.module 12 Aug 2010 10:53:40 -0000 @@ -31,7 +31,8 @@ function ajax_test_menu() { * Menu callback; Returns $_GET['commands'] suitable for use by ajax_deliver(). * * Additionally ensures that ajax_render() incorporates JavaScript settings - * by invoking drupal_add_js() with a dummy setting. + * and files by invoking drupal_add_js() with a dummy setting, which causes + * drupal_add_js() to also automatically add "misc/drupal.js". */ function ajax_test_render() { // Prepare AJAX commands.