? 561858-ajax-lazy-load.patch ? drupal.ajax-lazy.106.patch ? drupal.ajax_lazy_load_561858_104.patch ? export.inc.txt ? files ? includes/export.inc ? modules/color/help ? modules/help/help ? modules/help/help-popup.css ? modules/help/help-popup.tpl.php ? modules/help/help.js ? sites/all/files ? sites/all/modules/examples ? sites/all/modules/examples-7.x-1.x-dev.tar.gz ? sites/all/modules/lazy_load_demo ? sites/all/modules/lazy_load_demo.tgz ? sites/all/modules/lazy_load_demo_108.tar_.gz ? sites/all/modules/test ? sites/all/themes/test ? sites/all/themes/thegreenhouse ? sites/all/themes/thegreenhouse.zip ? sites/all/themes/zen ? sites/all/themes/zen-6.x-1.x-dev.tar.gz ? sites/all/themes/zenlike ? sites/all/themes/zenlike.zip ? sites/all/themes/zent ? sites/all/themes/zent.print.css ? sites/default/files ? sites/default/private ? sites/default/settings.php 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 20 Aug 2010 19:10:15 -0000 @@ -193,12 +193,12 @@ * 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); + $commands = array_merge(ajax_get_css(), $commands); // Allow modules to alter any AJAX response. drupal_alter('ajax_render', $commands); @@ -207,6 +207,246 @@ function ajax_render($commands = array() } /** + * Return javascript files on the current page for use with with AJAX. + * + * This function is invoked during normal HTML page loads to tell the client + * what javascript files are in use on the page. This function is also invoked + * during AJAX operations to tell the client what javascript files may + * need to be added to the page. + * + * This function is the equivalent of drupal_get_js(), 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. + * - Query_strings are not used here as they can cause files to be loaded via + * ajax more than once if they change. We never want this, + * even if the file has changed. + * + * @param $type + * Either 'command' to get a command to send to the browser, or 'files' to + * get a list of javascript files. + * @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($type = 'command', $scope = NULL, $javascript = NULL) { + if (!isset($javascript)) { + // Get the static directly so that the 'alter' if used below will + // be permanent. + $javascript = &drupal_static('drupal_add_js', array()); + } + if (empty($javascript)) { + return array(); + } + + if ($type == 'command') { + // 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'); + + 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': + $file = file_create_url($item['data']); + $js_files[$file] = $file; + break; + + case 'external': + $js_files[$item['data']] = $item['data']; + break; + } + } + + switch ($type) { + case 'command': + $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; + + case 'files': + default: + return $js_files; + } +} + +/** + * Return a list of all CSS files used in the current page load for use with AJAX. + * + * This function is invoked during normal HTML page loads to tell the client + * what javascript files are in use on the page. This function is also invoked + * during AJAX operations to tell the client what javascript files may + * need to be added to the page. + * + * This function is equivalent to drupal_get_css() with the following differences: + * + * - Files are not aggregated. + * - Query strings are ignored. + * + * @param $type + * Either 'command' to get a command to send to the browser, or 'files' to + * get a list of css files. + * @param $css + * (optional) An array of CSS files. If no array is provided, the default + * stylesheets array is used instead. + * + * @return + * A string of XHTML CSS tags. + */ +function ajax_get_css($type = 'command', $css = NULL) { + if (!isset($css)) { + // Get the static directly so that the 'alter' if used below will + // be permanent. + $css = &drupal_static('drupal_add_css', array()); + } + + if ($type == 'command') { + // Allow modules and themes to alter the CSS items. + drupal_alter('css', $css); + } + + // Sort CSS items according to their weights. + uasort($css, 'drupal_sort_weight'); + + // Remove the overridden CSS files. Later CSS files override former ones. + $previous_item = array(); + foreach ($css as $key => $item) { + // Once the weight gets to 100, we're in theme CSS. We must stop here, + // because theme CSS files will not be added via AJAX. + if ($item['weight'] >= 100) { + break; + } + + if ($item['type'] == 'file') { + // If defined, force a unique basename for this file. + $basename = isset($item['basename']) ? $item['basename'] : basename($item['data']); + if (isset($previous_item[$basename])) { + // Remove the previous item that shared the same base name. + unset($css[$previous_item[$basename]]); + } + $previous_item[$basename] = $key; + } + } + + $css_files = array(); + foreach ($css as $name => $file) { + if ($file['type'] == 'file' || $file['type'] == 'external') { + $css_files[$name] = array( + 'href' => $file['data'], + 'media' => $file['media'], + 'browsers' => $file['browsers'], + ); + } + } + + switch ($type) { + case 'command': + $commands = array(); + if (!empty($css_files)) { + $commands[] = ajax_command_css_files($css_files); + } + return $commands; + + case 'files': + default: + return $css_files; + } + +} + +/** + * Add any last additional info to the footer needed for AJAX operations. + * + * If javascript is enabled on the page, we need to provide a list of all + * CSS and javascript files that we know about on the page so that they will + * not be included again in ajax operations. + * + * @return + * A string that is to be appended in the footer of the page containing + * javascript to inform the browser which .js and .css files are known + * to be in use. + */ +function ajax_get_footer() { + $output = ''; + $js_files = ajax_get_js('files'); + $css_files = array(); + + // Only bother doing this if there are javascript files. + if ($js_files) { + // For inline Javascript to validate as XHTML, all Javascript containing + // XHTML needs to be wrapped in CDATA. To make that backwards compatible + // with HTML 4, we need to comment out the CDATA-tag. + $loaded = array('ajaxFiles' => array('scripts' => $js_files)); + + $css_files = ajax_get_css('files'); + if ($css_files) { + $loaded['ajaxFiles']['css'] = $css_files; + } + + + $element = array( + '#tag' => 'script', + '#value' => '', + '#value_prefix' => "\n\n", + '#value' => 'jQuery.extend(Drupal.settings, ' . drupal_json_encode($loaded) . ");", + '#attributes' => array( + 'type' => 'text/javascript', + ), + ); + + $output .= theme('html_tag', array('element' => $element)); + } + + return $output; +} + +/** * 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 +1195,50 @@ 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, + ); +} + +/** + * Creates css command for the AJAX responder. + * + * This will add CSS files to the output. Files that have already been + * processed will not be processed again. + * + * This command is implemented by Drupal.ajax.prototype.commands.css() + * defined in misc/ajax.js. + * + * @param $files + * The $files array is a simple array of filenames that match, exactly, the + * css files 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_css_files($files) { + return array( + 'command' => 'css_files', + 'files' => $files, + ); +} Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.1209 diff -u -p -r1.1209 common.inc --- includes/common.inc 20 Aug 2010 01:17:51 -0000 1.1209 +++ includes/common.inc 20 Aug 2010 19:10:20 -0000 @@ -2816,7 +2816,7 @@ function drupal_add_css($data = NULL, $o */ function drupal_get_css($css = NULL) { if (!isset($css)) { - $css = drupal_add_css(); + $css = &drupal_static('drupal_add_css', array()); } // Allow modules and themes to alter the CSS items. @@ -3788,7 +3788,9 @@ function drupal_js_defaults($data = NULL */ function drupal_get_js($scope = 'header', $javascript = NULL) { if (!isset($javascript)) { - $javascript = drupal_add_js(); + // Get the static directly so that the 'alter' if used below will + // be permanent. + $javascript = &drupal_static('drupal_add_js', array()); } if (empty($javascript)) { return ''; @@ -3842,6 +3844,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']; @@ -3867,12 +3870,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 { @@ -3881,6 +3885,7 @@ function drupal_get_js($scope = 'header' $key = 'aggregate' . $index; $processed[$key] = ''; $files[$key][$item['data']] = $item; + $reported_files[$uri] = 1; } break; @@ -3900,7 +3905,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); @@ -5977,7 +5982,7 @@ function drupal_write_record($table, &$r } if (!property_exists($object, $field)) { - // Skip fields that are not provided, default values are already known + // Skip fields that are not provided, default values are already known // by the database. continue; } Index: includes/theme.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/theme.inc,v retrieving revision 1.605 diff -u -p -r1.605 theme.inc --- includes/theme.inc 8 Aug 2010 19:35:48 -0000 1.605 +++ includes/theme.inc 20 Aug 2010 19:10:22 -0000 @@ -1999,7 +1999,7 @@ function theme_indentation($variables) { /** * Returns HTML output for a single table cell for theme_table(). - * + * * @param $cell * Array of cell information, or string to display in cell. * @param bool $header @@ -2289,6 +2289,7 @@ function template_process_html(&$variabl // Place the rendered HTML for the page body into a top level variable. $variables['page'] = $variables['page']['#children']; $variables['page_bottom'] .= drupal_get_js('footer'); + $variables['page_bottom'] .= ajax_get_footer(); $variables['head'] = drupal_get_html_head(); $variables['css'] = drupal_add_css(); 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 20 Aug 2010 19:10:23 -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,69 @@ 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[Drupal.getPath(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 add CSS files to the page. + * + * Keeps track of files already on the page. Additionally, if a previous + * css_files command (During AJAX) added CSS to the page, this will be + * removed before new files are added, in order to keep IE from exploding + * with file after file. + */ + css_files: function(ajax, response, status) { + // Build a list of css files already loaded: + $('link:not(.drupal-temporary-css)').each(function () { + if ($(this).attr('type') == 'text/css') { + var link = Drupal.getPath($(this).attr('href')); + if (link) { + Drupal.ajaxFiles.css[link] = $(this).attr('href'); + } + } + }); + + var html = ''; + for (var i in response.files) { + if (!Drupal.ajaxFiles.css[response.files[i].href]) { + html += ''; + } + } + + if (html) { + $('link.drupal-temporary-css').remove(); + $('body').append($(html)); + } + }, + + /** * Command to attach data using jQuery's data API. */ data: function (ajax, response, status) { @@ -447,4 +518,14 @@ Drupal.ajax.prototype.commands = { } }; +/** + * Only on the beginning of the page, not using a behavior, add files that + * may have been specified in the footer of the page to our internal array. + */ + +$(function () { + if (Drupal.settings.ajaxFiles && Drupal.settings.ajaxFiles.scripts) { + $.extend(Drupal.ajaxFiles.scripts, Drupal.settings.ajaxFiles.scripts); + } +}); })(jQuery); Index: misc/drupal.js =================================================================== RCS file: /cvs/drupal/drupal/misc/drupal.js,v retrieving revision 1.69 diff -u -p -r1.69 drupal.js --- misc/drupal.js 28 Jul 2010 01:38:28 -0000 1.69 +++ misc/drupal.js 20 Aug 2010 19:10:23 -0000 @@ -325,6 +325,22 @@ Drupal.ajaxError = function (xmlhttp, ur return message; }; +/** + * Get the actual path of a link, dropping items after the ? + */ +Drupal.getPath = function (link) { + if (!link) { + return; + } + + var index = link.indexOf('?'); + if (index != -1) { + link = link.substr(0, index); + } + + return link; +} + // Class indicating that JS is enabled; used for styling purpose. $('html').addClass('js'); Index: modules/field/tests/field.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/tests/field.test,v retrieving revision 1.39 diff -u -p -r1.39 field.test --- modules/field/tests/field.test 18 Aug 2010 00:44:52 -0000 1.39 +++ modules/field/tests/field.test 20 Aug 2010 19:10:26 -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 20 Aug 2010 19:10:27 -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 20 Aug 2010 19:10:27 -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 20 Aug 2010 19:10:27 -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.