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
This commit is contained in:
Sam Minnee 2008-11-12 04:31:33 +00:00
parent a6ea78a230
commit 73a075a491
9 changed files with 296 additions and 18 deletions

View File

@ -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 {
}
?>
?>

View File

@ -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

View File

@ -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')) {

View File

@ -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

View File

@ -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();
}
/**

View File

@ -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",

View File

@ -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");
}

View File

@ -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");

View File

@ -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<cssIncludes.length;i++) {
$.requireCss(cssIncludes[i]);
}
}
// JavaScript
if(xml.getResponseHeader('X-Include-JS')) {
var jsIncludes = xml.getResponseHeader('X-Include-JS').split(',');
var newIncludes = [];
for(i=0;i<jsIncludes.length;i++) {
if(!$.isJsLoaded(jsIncludes[i])) {
newIncludes.push(jsIncludes[i]);
}
}
}
// We make an array of the includes that are actually new, and attach the callback to the last one
// They are placed in a queue and will be included in order. This means that the callback will
// be able to execute script in the new includes (such as a livequery update)
if(newIncludes.length > 0) {
for(i=0;i<jsIncludes.length;i++) {
$.requireJs(jsIncludes[i], (i == jsIncludes.length-1) ? function() { _ondemandComplete(xml, status); } : null);
}
// If there aren't any new includes, then we can just call the callbacks ourselves
} else {
_ondemandComplete(xml, status);
}
}
_originalAjax(s);
}
})(jQuery);
/**
* This is the on-demand handler used by our patched version of prototype.
* once we get rid of all uses of prototype, we can remove this
*/
function prototypeOnDemandHandler(xml, callback) {
var i;
var newIncludes = [];
// CSS
if(xml.getResponseHeader('X-Include-CSS')) {
var cssIncludes = xml.getResponseHeader('X-Include-CSS').split(',');
for(i=0;i<cssIncludes.length;i++) {
jQuery.requireCss(cssIncludes[i]);
}
}
// JavaScript
if(xml.getResponseHeader('X-Include-JS')) {
var jsIncludes = xml.getResponseHeader('X-Include-JS').split(',');
for(i=0;i<jsIncludes.length;i++) {
if(!jQuery.isJsLoaded(jsIncludes[i])) {
newIncludes.push(jsIncludes[i]);
}
}
}
// We make an array of the includes that are actually new, and attach the callback to the last one
// They are placed in a queue and will be included in order. This means that the callback will
// be able to execute script in the new includes (such as a livequery update)
if(newIncludes.length > 0) {
for(i=0;i<jsIncludes.length;i++) {
jQuery.requireJs(jsIncludes[i], (i == jsIncludes.length-1) ? callback : null);
}
// If there aren't any new includes, then we can just call the callbacks ourselves
} else {
callback();
}
}