diff --git includes/ajax.inc includes/ajax.inc index 74b4e73..ba29a34 100644 --- includes/ajax.inc +++ includes/ajax.inc @@ -193,11 +193,39 @@ * functions. */ function ajax_render($commands = array()) { - // Automatically extract any 'settings' added via drupal_add_js() and make - // them the first command. + // Automatically extract any scripts and settings that have been added via + // drupal_add_js() and create commands to pass that to the client. $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']))); + $default_query_string = variable_get('css_js_query_string', '0'); + $js_version_string = variable_get('drupal_js_version_query_string', 'v='); + + foreach ($scripts as $item) { + $query_string = empty($item['version']) ? $default_query_string : $js_version_string . $item['version']; + switch ($item['type']) { + case 'setting': + $settings = $item['data']; + break; + + case 'inline': + // Presently inline data is ignored. + break; + + case 'file': + $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?'; + $js_files[] = file_create_url($item['data']) . $query_string_separator . ($item['cache'] ? $query_string : REQUEST_TIME); + break; + + case 'external': + $js_files[] = $item['data']; + break; + } + } + + if (!empty($settings)) { + array_unshift($commands, ajax_command_settings(call_user_func_array('array_merge_recursive', $settings))); + } + if (!empty($js_files)) { + array_unshift($commands, ajax_command_scripts($js_files)); } // Allow modules to alter any AJAX response. @@ -955,3 +983,27 @@ 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 adde 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, + ); +} + diff --git includes/common.inc includes/common.inc index 1066bdf..397258f 100644 --- includes/common.inc +++ includes/common.inc @@ -2108,7 +2108,7 @@ function url($path = NULL, array $options = array()) { * @param $path * The internal path or external URL being linked to, such as "node/34" or * "http://example.com/foo". - * @return + * @return * Boolean TRUE or FALSE, where TRUE indicates an external path. */ function url_is_external($path) { @@ -3760,6 +3760,7 @@ function drupal_get_js($scope = 'header', $javascript = NULL) { 'type' => 'text/javascript', ), ); + foreach ($items as $item) { $query_string = empty($item['version']) ? $default_query_string : $js_version_string . $item['version']; @@ -3785,11 +3786,11 @@ function drupal_get_js($scope = 'header', $javascript = NULL) { case 'file': $js_element = $element; + if ($item['defer']) { + $js_element['#attributes']['defer'] = 'defer'; + } + $query_string_separator = (strpos($item['data'], '?') !== 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); $processed[$index++] = theme('html_tag', array('element' => $js_element)); } @@ -3799,6 +3800,8 @@ function drupal_get_js($scope = 'header', $javascript = NULL) { $key = 'aggregate' . $index; $processed[$key] = ''; $files[$key][$item['data']] = $item; + $reported_files[] = file_create_url($item['data']) . $query_string_separator . ($item['cache'] ? $query_string : REQUEST_TIME); + } break; @@ -3827,6 +3830,13 @@ function drupal_get_js($scope = 'header', $javascript = NULL) { $processed[$key] = theme('html_tag', array('element' => $js_element)); } } + + // Report on the page the list of aggregated javascript files. + $js_element = $element; + $js_element['#value_prefix'] = $embed_prefix; + $js_element['#value'] = 'jQuery.extend(Drupal.ajaxFiles.scripts, ' . drupal_json_encode(drupal_map_assoc($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. @@ -6654,7 +6664,7 @@ function entity_form_field_validate($entity_type, $form, &$form_state) { * For some entity forms (e.g., forms with complex non-field data and forms that * simultaneously edit multiple entities), this behavior may be inappropriate, * so the #builder_function for such forms needs to implement the required - * functionality instead of calling this function. + * functionality instead of calling this function. */ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_state) { $info = entity_get_info($entity_type); diff --git misc/ajax.js misc/ajax.js index 553d2cc..cfd1010 100644 --- misc/ajax.js +++ misc/ajax.js @@ -13,7 +13,13 @@ * to provide AJAX capabilities. */ -Drupal.ajax = Drupal.ajax || {}; +Drupal.ajax = Drupal.ajax || { }; + +/** + * Hold a list of files known to be on the page so that AJAX requests can + * safely add more. + */ +Drupal.ajaxFiles = Drupal.ajaxFiles|| { scripts: {}, css: {} }; /** * Attaches the AJAX behavior to each AJAX form element. @@ -427,6 +433,31 @@ Drupal.ajax.prototype.commands = { }, /** + * Command to add scripts to the page. + * + * We keep a record of what CSS files are already on the page and attempt + * to avoid adding more. + */ + scripts: function (ajax, response, status) { + // Build a list of scripts already loaded. + var scripts = {}; + $('script').each(function () { + Drupal.ajaxFiles.scripts[this.src] = this.src; + }); + + var html = ''; + for (var i in response.files) { + if (!Drupal.ajaxFiles.scripts[response.files[i]]) { + Drupal.ajaxFiles.scripts[response.files[i]] = response.files[i]; + html += ''; + } + } + if (html) { + $('html').prepend(html); + } + }, + + /** * Command to attach data using jQuery's data API. */ data: function (ajax, response, status) { diff --git modules/field/tests/field.test modules/field/tests/field.test index 36d6284..99dd215 100644 --- modules/field/tests/field.test +++ modules/field/tests/field.test @@ -1439,7 +1439,7 @@ class FieldFormTestCase extends FieldTestCase { // 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"); diff --git modules/poll/poll.test modules/poll/poll.test index 0ca7aa8..9a10dab 100644 --- modules/poll/poll.test +++ modules/poll/poll.test @@ -342,7 +342,7 @@ class PollJSAddChoice extends DrupalWebTestCase { // 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))); diff --git modules/simpletest/tests/ajax.test modules/simpletest/tests/ajax.test index 2e10b80..82fbf4e 100644 --- modules/simpletest/tests/ajax.test +++ modules/simpletest/tests/ajax.test @@ -44,9 +44,9 @@ class AJAXFrameworkTestCase extends AJAXTestCase { 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.')); + $this->assertIdentical($result[1]['command'], 'settings', t('drupal_add_js() settings are contained right after scripts.')); // Verify that basePath is contained in JavaScript settings. - $this->assertEqual($result[0]['settings']['basePath'], base_path(), t('Base path is contained in JavaScript settings.')); + $this->assertEqual($result[1]['settings']['basePath'], base_path(), t('Base path is contained in JavaScript settings.')); } /** @@ -85,10 +85,10 @@ class AJAXCommandsTestCase extends AJAXTestCase { $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.')); + // Verify that JavaScript settings are contained (right after scripts). + $this->assertIdentical($result[1]['command'], 'settings', t('drupal_add_js() settings are contained right after scripts.')); // Verify that the custom setting is contained. - $this->assertEqual($result[1]['settings']['foo'], 42, t('Custom setting is output.')); + $this->assertEqual($result[2]['settings']['foo'], 42, t('Custom setting is output.')); } /** @@ -286,7 +286,7 @@ class AJAXMultiFormTestCase extends AJAXTestCase { $button = $this->xpath($field_xpath . $button_xpath_suffix); $button_id = (string) $button[0]['id']; $commands = $this->drupalPostAJAX(NULL, array(), array($button_name => $button_value), 'system/ajax', array(), array(), $form_id, $settings['ajax'][$button_id]); - $settings = array_merge_recursive($settings, $commands[0]['settings']); + $settings = array_merge_recursive($settings, $commands[1]['settings']); $this->assert(count($this->xpath($field_xpath . $field_items_xpath_suffix)) == $i+2, t('Found the correct number of field items after an AJAX submission.')); $this->assertFieldByXPath($field_xpath . $button_xpath_suffix, NULL, t('Found the "add more" button after an AJAX submission.')); $this->assertNoDuplicateIds(t('Updated page contains unique IDs'), 'Other');