From 73a075a491d93333c8d445643703cb607010fda9 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Wed, 12 Nov 2008 04:31:33 +0000 Subject: [PATCH] FEATURE #594: Added javascript-on-demand support git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@65688 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- core/Requirements.php | 37 ++++- core/control/HTTPResponse.php | 5 + core/model/SiteTree.php | 1 + forms/CheckboxSetField.php | 8 +- forms/HasManyComplexTableField.php | 6 + forms/HtmlEditorField.php | 12 +- forms/SelectionGroup.php | 2 +- forms/TreeDropdownField.php | 4 +- javascript/core/jquery.ondemand.js | 239 +++++++++++++++++++++++++++++ 9 files changed, 296 insertions(+), 18 deletions(-) create mode 100644 javascript/core/jquery.ondemand.js diff --git a/core/Requirements.php b/core/Requirements.php index accf5de9f..04b065df2 100644 --- a/core/Requirements.php +++ b/core/Requirements.php @@ -174,6 +174,10 @@ class Requirements { return self::backend()->includeInHTML($templateFile, $content); } + static function include_in_response(HTTPResponse $response) { + return self::backend()->include_in_response($response); + } + /** * Automatically includes the necessary lang-files from the module. * @@ -333,7 +337,7 @@ class Requirements_Backend { * * @var boolean */ - public $write_js_to_body = false; + public $write_js_to_body = true; /** * Register the given javascript file as required. @@ -510,10 +514,8 @@ class Requirements_Backend { $jsRequirements = ''; // Combine files - updates $this->javascript and $this->css - $this->process_combined_files(); - - // $this->process_i18n_javascript(); + $this->process_combined_files(); foreach(array_diff_key($this->javascript,$this->blocked) as $file => $dummy) { $path = self::path_for_file($file); @@ -575,6 +577,27 @@ class Requirements_Backend { return $content; } + + /** + * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the HTTP response + */ + function include_in_response(HTTPResponse $response) { + $this->process_combined_files(); + + foreach(array_diff_key($this->javascript,$this->blocked) as $file => $dummy) { + $path = $this->path_for_file($file); + if($path) $jsRequirements[] = $path; + } + + $response->addHeader('X-Include-JS', implode(',', $jsRequirements)); + + foreach(array_diff_key($this->css,$this->blocked) as $file => $params) { + $path = $this->path_for_file($file); + if($path) $cssRequirements[] = $path; + } + + $response->addHeader('X-Include-CSS', implode(',', $cssRequirements)); + } /** * Automatically includes the necessary lang-files from the module @@ -720,7 +743,7 @@ class Requirements_Backend { } } - function clear_combined_files() { + function clear_combined_files() { $this->combine_files = array(); } @@ -798,7 +821,7 @@ class Requirements_Backend { $fileContent = JSMin::minify($fileContent); } // write a header comment for each file for easier identification and debugging - $combinedData .= "/****** FILE: $file *****/\n" . $fileContent . "\n"; + $combinedData .= "/****** FILE: $file *****/\n" . $fileContent . "\n;\n"; } if(!file_exists(dirname($base . $combinedFile))) { Filesytem::makeFolder(dirname($base . $combinedFile)); @@ -856,4 +879,4 @@ class Requirements_Backend { } -?> \ No newline at end of file +?> diff --git a/core/control/HTTPResponse.php b/core/control/HTTPResponse.php index 35c6dc65d..eacdd0370 100644 --- a/core/control/HTTPResponse.php +++ b/core/control/HTTPResponse.php @@ -169,6 +169,11 @@ class HTTPResponse extends Object { * Send this HTTPReponse to the browser */ function output() { + // Attach appropriate X-Include-JavaScript and X-Include-CSS headers + if(Director::is_ajax()) { + Requirements::include_in_response($this); + } + if(in_array($this->statusCode, self::$redirect_codes) && headers_sent($file, $line)) { $url = $this->headers['Location']; echo diff --git a/core/model/SiteTree.php b/core/model/SiteTree.php index 9aeb466a9..bbe308190 100644 --- a/core/model/SiteTree.php +++ b/core/model/SiteTree.php @@ -1083,6 +1083,7 @@ class SiteTree extends DataObject { function getCMSFields() { require_once("forms/Form.php"); Requirements::javascript(CMS_DIR . "/javascript/SitetreeAccess.js"); + Requirements::javascript(SAPPHIRE_DIR . '/javascript/UpdateURL.js'); // Backlink report if($this->hasMethod('BackLinkTracking')) { diff --git a/forms/CheckboxSetField.php b/forms/CheckboxSetField.php index 78bb718be..728c9c1f1 100755 --- a/forms/CheckboxSetField.php +++ b/forms/CheckboxSetField.php @@ -12,18 +12,14 @@ class CheckboxSetField extends OptionsetField { protected $disabled = false; - function __construct($name, $title = "", $source = array(), $value = "", $form = null) { - parent::__construct($name, $title, $source, $value, $form); - - Requirements::css(SAPPHIRE_DIR . '/css/CheckboxSetField.css'); - } - /** * Object handles arrays and dosets being passed by reference. * * @todo Should use CheckboxField FieldHolder rather than constructing own markup. */ function Field() { + Requirements::css(SAPPHIRE_DIR . '/css/CheckboxSetField.css'); + $values = $this->value; // Get values from the join, if available diff --git a/forms/HasManyComplexTableField.php b/forms/HasManyComplexTableField.php index f41649eb3..0c8bc3cd6 100644 --- a/forms/HasManyComplexTableField.php +++ b/forms/HasManyComplexTableField.php @@ -32,8 +32,14 @@ class HasManyComplexTableField extends ComplexTableField { user_error("Can't figure out the data class of $controller", E_USER_WARNING); } + } + + function Field() { Requirements::javascript(SAPPHIRE_DIR . "/javascript/i18n.js"); Requirements::javascript(SAPPHIRE_DIR . "/javascript/HasManyFileField.js"); + Requirements::javascript(SAPPHIRE_DIR . '/javascript/RelationComplexTableField.js'); + Requirements::css(SAPPHIRE_DIR . '/css/HasManyFileField.css'); + return parent::Field(); } /** diff --git a/forms/HtmlEditorField.php b/forms/HtmlEditorField.php index b51a99556..12ae61589 100755 --- a/forms/HtmlEditorField.php +++ b/forms/HtmlEditorField.php @@ -15,7 +15,7 @@ class HtmlEditorField extends TextareaField { /** * Construct a new HtmlEditor field */ - function __construct($name, $title = "", $rows = 30, $cols = 20, $value = "", $form = null) { + function __construct($name, $title = "", $rows = 20, $cols = 20, $value = "", $form = null) { parent::__construct($name, $title, $rows, $cols, $value, $form); $this->extraClass = 'typography'; } @@ -27,6 +27,10 @@ class HtmlEditorField extends TextareaField { function Field() { Requirements::javascript(MCE_ROOT . "tiny_mce_src.js"); Requirements::javascript(THIRDPARTY_DIR . "/tiny_mce_improvements.js"); + Requirements::css('cms/css/TinyMCEImageEnhancement.css'); + Requirements::javascript('jsparty/SWFUpload/SWFUpload.js'); + Requirements::javascript('cms/javascript/Upload.js'); + Requirements::javascript('cms/javascript/TinyMCEImageEnhancement.js'); // Don't allow unclosed tags - they will break the whole application ;-) $cleanVal = $this->value; @@ -392,6 +396,9 @@ class HtmlEditorField_Toolbar extends RequestHandler { * @return Form */ function ImageForm() { + Requirements::javascript(THIRDPARTY_DIR . '/SWFUpload/SWFUpload.js'); + Requirements::javascript(CMS_DIR . '/javascript/Upload.js'); + $form = new Form( $this->controller, "{$this->name}/ImageForm", @@ -439,6 +446,9 @@ class HtmlEditorField_Toolbar extends RequestHandler { } function FlashForm() { + Requirements::javascript(THIRDPARTY_DIR . '/SWFUpload/SWFUpload.js'); + Requirements::javascript(CMS_DIR . '/javascript/Upload.js'); + $form = new Form( $this->controller, "{$this->name}/FlashForm", diff --git a/forms/SelectionGroup.php b/forms/SelectionGroup.php index d2e2f7bb8..d85886d5f 100755 --- a/forms/SelectionGroup.php +++ b/forms/SelectionGroup.php @@ -82,7 +82,7 @@ class SelectionGroup extends CompositeField { Requirements::javascript(THIRDPARTY_DIR . '/behaviour.js'); Requirements::javascript(THIRDPARTY_DIR . '/prototype_improvements.js'); Requirements::javascript(SAPPHIRE_DIR . '/javascript/SelectionGroup.js'); - + Requirements::css(SAPPHIRE_DIR . '/css/SelectionGroup.css'); return $this->renderWith("SelectionGroup"); } diff --git a/forms/TreeDropdownField.php b/forms/TreeDropdownField.php index 3a33ce0f3..eb53dbc0e 100755 --- a/forms/TreeDropdownField.php +++ b/forms/TreeDropdownField.php @@ -20,9 +20,6 @@ class TreeDropdownField extends FormField { $this->sourceObject = $sourceObject; $this->keyField = $keyField; $this->labelField = $labelField; - - Requirements::css(SAPPHIRE_DIR . '/css/TreeDropdownField.css'); - parent::__construct($name, $title); } @@ -37,6 +34,7 @@ class TreeDropdownField extends FormField { } function Field() { + Requirements::css(SAPPHIRE_DIR . '/css/TreeDropdownField.css'); Requirements::javascript(THIRDPARTY_DIR . "/tree/tree.js"); Requirements::css(THIRDPARTY_DIR . "/tree/tree.css"); Requirements::javascript(SAPPHIRE_DIR . "/javascript/TreeSelectorField.js"); diff --git a/javascript/core/jquery.ondemand.js b/javascript/core/jquery.ondemand.js new file mode 100644 index 000000000..05780d8f3 --- /dev/null +++ b/javascript/core/jquery.ondemand.js @@ -0,0 +1,239 @@ +/** + * On-demand JavaScript handler + * Based on http://plugins.jquery.com/files/issues/jquery.ondemand.js_.txt and modified to integrate with Sapphire + */ +(function($){ + + function isExternalScript(url){ + re = new RegExp('(http|https)://'); + return re.test(url); + }; + + $.extend({ + + requireConfig : { + routeJs : '', // empty default paths give more flexibility and user has + routeCss : '' // choice of using this config or full path in scriptUrl argument + }, // previously were useless for users which don't use '_js/' and '_css/' folders. (by PGA) + + queue : [], + pending : null, + loaded_list : null, // loaded files list - to protect against loading existed file again (by PGA) + + + // Added by SRM: Initialise the loaded_list with the scripts included on first load + initialiseJSLoadedList : function() { + if(this.loaded_list == null) { + $this = this; + $this.loaded_list = []; + $('script').each(function() { + if($(this).attr('src')) $this.loaded_list[ $(this).attr('src') ] = 1; + }); + } + }, + + isJsLoaded : function(scriptUrl) { + this.initialiseJSLoadedList(); + return this.loaded_list[scriptUrl] != undefined; + }, + + requireJs : function(scriptUrl, callback, opts, obj, scope) + { + + if(opts != undefined || opts == null){ + $.extend($.requireConfig, opts); + } + + var _request = { + url : scriptUrl, + callback : callback, + opts : opts, + obj : obj, + scope : scope + } + + if(this.pending) + { + this.queue.push(_request); + return; + } + + this.pending = _request; + + this.initialiseJSLoadedList(); + + if (this.loaded_list[this.pending.url] != undefined) { // if required file exists (by PGA) + this.requestComplete(); // => request complete + return; + } + + var _this = this; + var _url = (isExternalScript(scriptUrl)) ? scriptUrl : $.requireConfig.routeJs + scriptUrl; + var _head = document.getElementsByTagName('head')[0]; + var _scriptTag = document.createElement('script'); + + // Firefox, Opera + $(_scriptTag).bind('load', function(){ + _this.requestComplete(); + }); + + // IE + _scriptTag.onreadystatechange = function(){ + if(this.readyState === 'loaded' || this.readyState === 'complete'){ + _this.requestComplete(); + } + } + + _scriptTag.type = "text/javascript"; + _scriptTag.src = _url; + + _head.appendChild(_scriptTag); + }, + + requestComplete : function() + { + + if(this.pending.callback){ + if(this.pending.obj){ + if(this.pending.scope){ + this.pending.callback.call(this.pending.obj); + } else { + this.pending.callback.call(window, this.pending.obj); + } + } else { + this.pending.callback.call(); + } + } + + this.loaded_list[this.pending.url] = 1; // adding loaded file to loaded list (by PGA) + this.pending = null; + + if(this.queue.length > 0) + { + var request = this.queue.shift(); + this.requireJs(request.url, request.callback, request.opts, request.obj, request.scope); + } + }, + + requireCss : function(styleUrl){ + + if(document.createStyleSheet){ + document.createStyleSheet($.requireConfig.routeCss + styleUrl); + } + else { + + var styleTag = document.createElement('link'); + + $(styleTag).attr({ + href : $.requireConfig.routeCss + styleUrl, + type : 'text/css', + media : 'screen', + rel : 'stylesheet' + }).appendTo($('head').get(0)); + + } + + } + + }) + + /** + * Sapphire extensions + * Ajax requests are amended to look for X-Include-JS and X-Include-CSS headers + */ + _originalAjax = $.ajax; + $.ajax = function(s) { + var _complete = s.complete; + var _success = s.success; + var _dataType = s.dataType; + + // This replaces the usual ajax success & complete handlers. They are called after any on demand JS is loaded. + var _ondemandComplete = function(xml, status) { + if(status == 'success') { + data = jQuery.httpData(xml, _dataType); + if(_success) _success(data, status, xml); + } + if(_complete) _complete(xml, status); + } + + // We remove the success handler and take care of calling it outselves within _ondemandComplete + s.success = null; + s.complete = function(xml, status) { + var i; + // CSS + if(xml.getResponseHeader('X-Include-CSS')) { + var cssIncludes = xml.getResponseHeader('X-Include-CSS').split(','); + for(i=0;i 0) { + for(i=0;i 0) { + for(i=0;i