/* Copyright (c) 2004-2006, The Dojo Foundation All Rights Reserved. Licensed under the Academic Free License version 2.1 or above OR the modified BSD license. For more information on Dojo licensing, see: http://dojotoolkit.org/community/licensing.shtml */ dojo.provide("dojo.widget.ContentPane"); dojo.require("dojo.widget.*"); dojo.require("dojo.io.*"); dojo.require("dojo.widget.HtmlWidget"); dojo.require("dojo.string"); dojo.require("dojo.string.extras"); dojo.require("dojo.html.style"); dojo.widget.defineWidget( "dojo.widget.ContentPane", dojo.widget.HtmlWidget, function(){ // summary: // A widget that can be used as a standalone widget // or as a baseclass for other widgets // Handles replacement of document fragment using either external uri or javascript/java // generated markup or DomNode content, instanciating widgets within content and runs scripts. // Dont confuse it with an iframe, it only needs document fragments. // It's useful as a child of LayoutContainer, SplitContainer, or TabContainer. // But note that those classes can contain any widget as a child. // scriptScope: Function // reference holder to the inline scripts container, if scriptSeparation is true // bindArgs: String[] // Send in extra args to the dojo.io.bind call // per widgetImpl variables this._styleNodes = []; this._onLoadStack = []; this._onUnloadStack = []; this._callOnUnload = false; this._ioBindObj; // Note: // dont change this value externally this.scriptScope; // undefined for now // loading option // example: // bindArgs="preventCache:false;" overrides cacheContent this.bindArgs = {}; }, { isContainer: true, // loading options // adjustPaths: Boolean // adjust relative paths in markup to fit this page adjustPaths: true, // href: String // The href of the content that displays now // Set this at construction if you want to load externally, // changing href after creation doesnt have any effect, see setUrl href: "", // extractContent Boolean: Extract visible content from inside of .... extractContent: true, // parseContent Boolean: Construct all widgets that is in content parseContent: true, // cacheContent Boolean: Cache content retreived externally cacheContent: true, // preload: Boolean // Force load of data even if pane is hidden. // Note: // In order to delay download you need to initially hide the node it constructs from preload: false, // refreshOnShow: Boolean // Refresh (re-download) content when pane goes from hidden to shown refreshOnShow: false, // handler: String||Function // Generate pane content from a java function // The name of the java proxy function handler: "", // executeScripts: Boolean // Run scripts within content, extractContent has NO effect on this. // Note: // if true scripts in content will be evaled after content is innerHTML'ed executeScripts: false, // scriptSeparation: Boolean // Run scripts in a separate scope, unique for each ContentPane scriptSeparation: true, // loadingMessage: String // Message that shows while downloading loadingMessage: "Loading...", // isLoaded: Boolean // Tells loading status isLoaded: false, postCreate: function(args, frag, parentComp){ if (this.handler!==""){ this.setHandler(this.handler); } if(this.isShowing() || this.preload){ this.loadContents(); } }, show: function(){ // if refreshOnShow is true, reload the contents every time; otherwise, load only the first time if(this.refreshOnShow){ this.refresh(); }else{ this.loadContents(); } dojo.widget.ContentPane.superclass.show.call(this); }, refresh: function(){ // summary: // Force a refresh (re-download) of content, be sure to turn of cache this.isLoaded=false; this.loadContents(); }, loadContents: function() { // summary: // Download if isLoaded is false, else ignore if ( this.isLoaded ){ return; } if ( dojo.lang.isFunction(this.handler)) { this._runHandler(); } else if ( this.href != "" ) { this._downloadExternalContent(this.href, this.cacheContent && !this.refreshOnShow); } }, setUrl: function(/*String||dojo.uri.Uri*/ url) { // summary: // Reset the (external defined) content of this pane and replace with new url // Note: // It delays the download until widget is shown if preload is false this.href = url; this.isLoaded = false; if ( this.preload || this.isShowing() ){ this.loadContents(); } }, abort: function(){ // summary // Aborts a inflight download of content var bind = this._ioBindObj; if(!bind || !bind.abort){ return; } bind.abort(); delete this._ioBindObj; }, _downloadExternalContent: function(url, useCache) { this.abort(); this._handleDefaults(this.loadingMessage, "onDownloadStart"); var self = this; this._ioBindObj = dojo.io.bind( this._cacheSetting({ url: url, mimetype: "text/html", handler: function(type, data, xhr){ delete self._ioBindObj; // makes sure abort doesnt clear cache if(type=="load"){ self.onDownloadEnd.call(self, url, data); }else{ // XHR isnt a normal JS object, IE doesnt have prototype on XHR so we cant extend it or shallowCopy it var e = { responseText: xhr.responseText, status: xhr.status, statusText: xhr.statusText, responseHeaders: xhr.getAllResponseHeaders(), text: "Error loading '" + url + "' (" + xhr.status + " "+ xhr.statusText + ")" }; self._handleDefaults.call(self, e, "onDownloadError"); self.onLoad(); } } }, useCache) ); }, _cacheSetting: function(bindObj, useCache){ for(var x in this.bindArgs){ if(dojo.lang.isUndefined(bindObj[x])){ bindObj[x] = this.bindArgs[x]; } } if(dojo.lang.isUndefined(bindObj.useCache)){ bindObj.useCache = useCache; } if(dojo.lang.isUndefined(bindObj.preventCache)){ bindObj.preventCache = !useCache; } if(dojo.lang.isUndefined(bindObj.mimetype)){ bindObj.mimetype = "text/html"; } return bindObj; }, onLoad: function(e){ // summary: // Event hook, is called after everything is loaded and widgetified this._runStack("_onLoadStack"); this.isLoaded=true; }, onUnLoad: function(e){ // summary: // Deprecated, use onUnload (lowercased load) dojo.deprecated(this.widgetType+".onUnLoad, use .onUnload (lowercased load)", 0.5); }, onUnload: function(e){ // summary: // Event hook, is called before old content is cleared this._runStack("_onUnloadStack"); delete this.scriptScope; // FIXME: remove for 0.5 along with onUnLoad if(this.onUnLoad !== dojo.widget.ContentPane.prototype.onUnLoad){ this.onUnLoad.apply(this, arguments); } }, _runStack: function(stName){ var st = this[stName]; var err = ""; var scope = this.scriptScope || window; for(var i = 0;i < st.length; i++){ try{ st[i].call(scope); }catch(e){ err += "\n"+st[i]+" failed: "+e.description; } } this[stName] = []; if(err.length){ var name = (stName== "_onLoadStack") ? "addOnLoad" : "addOnUnLoad"; this._handleDefaults(name+" failure\n "+err, "onExecError", "debug"); } }, addOnLoad: function(obj, func){ // summary // Stores function refs and calls them one by one in the order they came in // when load event occurs. // obj: Function||Object? // holder object // func: Function // function that will be called this._pushOnStack(this._onLoadStack, obj, func); }, addOnUnload: function(obj, func){ // summary // Stores function refs and calls them one by one in the order they came in // when unload event occurs. // obj: Function||Object // holder object // func: Function // function that will be called this._pushOnStack(this._onUnloadStack, obj, func); }, addOnUnLoad: function(){ // summary: // Deprecated use addOnUnload (lower cased load) dojo.deprecated(this.widgetType + ".addOnUnLoad, use addOnUnload instead. (lowercased Load)", 0.5); this.addOnUnload.apply(this, arguments); }, _pushOnStack: function(stack, obj, func){ if(typeof func == 'undefined') { stack.push(obj); }else{ stack.push(function(){ obj[func](); }); } }, destroy: function(){ // make sure we call onUnload this.onUnload(); dojo.widget.ContentPane.superclass.destroy.call(this); }, onExecError: function(/*Object*/e){ // summary: // called when content script eval error or Java error occurs, preventDefault-able // default is to debug not alert as in 0.3.1 }, onContentError: function(/*Object*/e){ // summary: // called on DOM faults, require fault etc in content, preventDefault-able // default is to display errormessage inside pane }, onDownloadError: function(/*Object*/e){ // summary: // called when download error occurs, preventDefault-able // default is to display errormessage inside pane }, onDownloadStart: function(/*Object*/e){ // summary: // called before download starts, preventDefault-able // default is to display loadingMessage inside pane // by changing e.text in your event handler you can change loading message }, // onDownloadEnd: function(url, data){ // summary: // called when download is finished // // url String: url that downloaded data // data String: the markup that was downloaded data = this.splitAndFixPaths(data, url); this.setContent(data); }, // useful if user wants to prevent default behaviour ie: _setContent("Error...") _handleDefaults: function(e, handler, messType){ if(!handler){ handler = "onContentError"; } if(dojo.lang.isString(e)){ e = {text: e}; } if(!e.text){ e.text = e.toString(); } e.toString = function(){ return this.text; }; if(typeof e.returnValue != "boolean"){ e.returnValue = true; } if(typeof e.preventDefault != "function"){ e.preventDefault = function(){ this.returnValue = false; }; } // call our handler this[handler](e); if(e.returnValue){ switch(messType){ case true: // fallthrough, old compat case "alert": alert(e.toString()); break; case "debug": dojo.debug(e.toString()); break; default: // makes sure scripts can clean up after themselves, before we setContent if(this._callOnUnload){ this.onUnload(); } // makes sure we dont try to call onUnLoad again on this event, // ie onUnLoad before 'Loading...' but not before clearing 'Loading...' this._callOnUnload = false; // we might end up in a endless recursion here if domNode cant append content if(arguments.callee._loopStop){ dojo.debug(e.toString()); }else{ arguments.callee._loopStop = true; this._setContent(e.toString()); } } } arguments.callee._loopStop = false; }, // pathfixes, require calls, css stuff and neccesary content clean splitAndFixPaths: function(s, url){ // summary: // adjusts all relative paths in (hopefully) all cases, images, remote scripts, links etc. // splits up content in different pieces, scripts, title, style, link and whats left becomes .xml // s String: The markup in string // url (String||dojo.uri.Uri?) url that pulled in markup var titles = [], scripts = [],tmp = [];// init vars var match = [], requires = [], attr = [], styles = []; var str = '', path = '', fix = '', tagFix = '', tag = '', origPath = ''; if(!url) { url = "./"; } // point to this page if not set if(s){ // make sure we dont run regexes on empty content /************** ***********/ // khtml is picky about dom faults, you can't attach a <style> or <title> node as child of body // must go into head, so we need to cut out those tags var regex = /<title[^>]*>([\s\S]*?)<\/title>/i; while(match = regex.exec(s)){ titles.push(match[1]); s = s.substring(0, match.index) + s.substr(match.index + match[0].length); }; /************** adjust paths *****************/ if(this.adjustPaths){ // attributepaths one tag can have multiple paths example: // <input src="..." style="url(..)"/> or <a style="url(..)" href=".."> // strip out the tag and run fix on that. // this guarantees that we won't run replace on another tag's attribute + it was easier do var regexFindTag = /<[a-z][a-z0-9]*[^>]*\s(?:(?:src|href|style)=[^>])+[^>]*>/i; var regexFindAttr = /\s(src|href|style)=(['"]?)([\w()\[\]\/.,\\'"-:;#=&?\s@]+?)\2/i; // these are the supported protocols, all other is considered relative var regexProtocols = /^(?:[#]|(?:(?:https?|ftps?|file|javascript|mailto|news):))/; while(tag = regexFindTag.exec(s)){ str += s.substring(0, tag.index); s = s.substring((tag.index + tag[0].length), s.length); tag = tag[0]; // loop through attributes tagFix = ''; while(attr = regexFindAttr.exec(tag)){ path = ""; origPath = attr[3]; switch(attr[1].toLowerCase()){ case "src":// falltrough case "href": if(regexProtocols.exec(origPath)){ path = origPath; } else { path = (new dojo.uri.Uri(url, origPath).toString()); } break; case "style":// style path = dojo.html.fixPathsInCssText(origPath, url); break; default: path = origPath; } fix = " " + attr[1] + "=" + attr[2] + path + attr[2]; // slices up tag before next attribute check tagFix += tag.substring(0, attr.index) + fix; tag = tag.substring((attr.index + attr[0].length), tag.length); } str += tagFix + tag; //dojo.debug(tagFix + tag); } s = str+s; } /**************** cut out all <style> and <link rel="stylesheet" href=".."> **************/ regex = /(?:<(style)[^>]*>([\s\S]*?)<\/style>|<link ([^>]*rel=['"]?stylesheet['"]?[^>]*)>)/i; while(match = regex.exec(s)){ if(match[1] && match[1].toLowerCase() == "style"){ styles.push(dojo.html.fixPathsInCssText(match[2],url)); }else if(attr = match[3].match(/href=(['"]?)([^'">]*)\1/i)){ styles.push({path: attr[2]}); } s = s.substring(0, match.index) + s.substr(match.index + match[0].length); }; /***************** cut out all <script> tags, push them into scripts array ***************/ var regex = /<script([^>]*)>([\s\S]*?)<\/script>/i; var regexSrc = /src=(['"]?)([^"']*)\1/i; var regexDojoJs = /.*(\bdojo\b\.js(?:\.uncompressed\.js)?)$/; var regexInvalid = /(?:var )?\bdjConfig\b(?:[\s]*=[\s]*\{[^}]+\}|\.[\w]*[\s]*=[\s]*[^;\n]*)?;?|dojo\.hostenv\.writeIncludes\(\s*\);?/g; var regexRequires = /dojo\.(?:(?:require(?:After)?(?:If)?)|(?:widget\.(?:manager\.)?registerWidgetPackage)|(?:(?:hostenv\.)?setModulePrefix|registerModulePath)|defineNamespace)\((['"]).*?\1\)\s*;?/; while(match = regex.exec(s)){ if(this.executeScripts && match[1]){ if(attr = regexSrc.exec(match[1])){ // remove a dojo.js or dojo.js.uncompressed.js from remoteScripts // we declare all files named dojo.js as bad, regardless of path if(regexDojoJs.exec(attr[2])){ dojo.debug("Security note! inhibit:"+attr[2]+" from being loaded again."); }else{ scripts.push({path: attr[2]}); } } } if(match[2]){ // remove all invalid variables etc like djConfig and dojo.hostenv.writeIncludes() var sc = match[2].replace(regexInvalid, ""); if(!sc){ continue; } // cut out all dojo.require (...) calls, if we have execute // scripts false widgets dont get there require calls // takes out possible widgetpackage registration as well while(tmp = regexRequires.exec(sc)){ requires.push(tmp[0]); sc = sc.substring(0, tmp.index) + sc.substr(tmp.index + tmp[0].length); } if(this.executeScripts){ scripts.push(sc); } } s = s.substr(0, match.index) + s.substr(match.index + match[0].length); } /********* extract content *********/ if(this.extractContent){ match = s.match(/<body[^>]*>\s*([\s\S]+)\s*<\/body>/im); if(match) { s = match[1]; } } /*** replace scriptScope prefix in html Event handler * working order: find tags with scriptScope in a tag attribute * then replace all standalone scriptScope occurencies with reference to to this widget * valid onClick="scriptScope.func()" or onClick="scriptScope['func']();scriptScope.i++" * not valid onClick="var.scriptScope.ref" nor onClick="var['scriptScope'].ref" */ if(this.executeScripts && this.scriptSeparation){ var regex = /(<[a-zA-Z][a-zA-Z0-9]*\s[^>]*?\S=)((['"])[^>]*scriptScope[^>]*>)/; var regexAttr = /([\s'";:\(])scriptScope(.*)/; // we rely on that attribute begins ' or " str = ""; while(tag = regex.exec(s)){ tmp = ((tag[3]=="'") ? '"': "'");fix= ""; str += s.substring(0, tag.index) + tag[1]; while(attr = regexAttr.exec(tag[2])){ tag[2] = tag[2].substring(0, attr.index) + attr[1] + "dojo.widget.byId("+ tmp + this.widgetId + tmp + ").scriptScope" + attr[2]; } str += tag[2]; s = s.substr(tag.index + tag[0].length); } s = str + s; } } return {"xml": s, // Object "styles": styles, "titles": titles, "requires": requires, "scripts": scripts, "url": url}; }, _setContent: function(cont){ this.destroyChildren(); // remove old stylenodes from HEAD for(var i = 0; i < this._styleNodes.length; i++){ if(this._styleNodes[i] && this._styleNodes[i].parentNode){ this._styleNodes[i].parentNode.removeChild(this._styleNodes[i]); } } this._styleNodes = []; try{ var node = this.containerNode || this.domNode; while(node.firstChild){ dojo.html.destroyNode(node.firstChild); } if(typeof cont != "string"){ node.appendChild(cont); }else{ node.innerHTML = cont; } }catch(e){ e.text = "Couldn't load content:"+e.description; this._handleDefaults(e, "onContentError"); } }, setContent: function(data){ // summary: // Replaces old content with data content, include style classes from old content // data String||DomNode: new content, be it Document fragment or a DomNode chain // If data contains style tags, link rel=stylesheet it inserts those styles into DOM this.abort(); if(this._callOnUnload){ this.onUnload(); }// this tells a remote script clean up after itself this._callOnUnload = true; if(!data || dojo.html.isNode(data)){ // if we do a clean using setContent(""); or setContent(#node) bypass all parsing, extractContent etc this._setContent(data); this.onResized(); this.onLoad(); }else{ // need to run splitAndFixPaths? ie. manually setting content // adjustPaths is taken care of inside splitAndFixPaths if(typeof data.xml != "string"){ this.href = ""; // so we can refresh safely data = this.splitAndFixPaths(data); } this._setContent(data.xml); // insert styles from content (in same order they came in) for(var i = 0; i < data.styles.length; i++){ if(data.styles[i].path){ this._styleNodes.push(dojo.html.insertCssFile(data.styles[i].path, dojo.doc(), false, true)); }else{ this._styleNodes.push(dojo.html.insertCssText(data.styles[i])); } } if(this.parseContent){ for(var i = 0; i < data.requires.length; i++){ try{ eval(data.requires[i]); } catch(e){ e.text = "ContentPane: error in package loading calls, " + (e.description||e); this._handleDefaults(e, "onContentError", "debug"); } } } // need to allow async load, Xdomain uses it // is inline function because we cant send args to dojo.addOnLoad var _self = this; function asyncParse(){ if(_self.executeScripts){ _self._executeScripts(data.scripts); } if(_self.parseContent){ var node = _self.containerNode || _self.domNode; var parser = new dojo.xml.Parse(); var frag = parser.parseElement(node, null, true); // createSubComponents not createComponents because frag has already been created dojo.widget.getParser().createSubComponents(frag, _self); } _self.onResized(); _self.onLoad(); } // try as long as possible to make setContent sync call if(dojo.hostenv.isXDomain && data.requires.length){ dojo.addOnLoad(asyncParse); }else{ asyncParse(); } } }, setHandler: function(/*Function*/ handler) { // summary: // Generate pane content from given java function var fcn = dojo.lang.isFunction(handler) ? handler : window[handler]; if(!dojo.lang.isFunction(fcn)) { // FIXME: needs testing! somebody with java knowledge needs to try this this._handleDefaults("Unable to set handler, '" + handler + "' not a function.", "onExecError", true); return; } this.handler = function() { return fcn.apply(this, arguments); } }, _runHandler: function() { var ret = true; if(dojo.lang.isFunction(this.handler)) { this.handler(this, this.domNode); ret = false; } this.onLoad(); return ret; }, _executeScripts: function(scripts) { // loop through the scripts in the order they came in var self = this; var tmp = "", code = ""; for(var i = 0; i < scripts.length; i++){ if(scripts[i].path){ // remotescript dojo.io.bind(this._cacheSetting({ "url": scripts[i].path, "load": function(type, scriptStr){ dojo.lang.hitch(self, tmp = ";"+scriptStr); }, "error": function(type, error){ error.text = type + " downloading remote script"; self._handleDefaults.call(self, error, "onExecError", "debug"); }, "mimetype": "text/plain", "sync": true }, this.cacheContent)); code += tmp; }else{ code += scripts[i]; } } try{ if(this.scriptSeparation){ // initialize a new anonymous container for our script, dont make it part of this widgets scope chain // instead send in a variable that points to this widget, useful to connect events to onLoad, onUnload etc.. delete this.scriptScope; this.scriptScope = new (new Function('_container_', code+'; return this;'))(self); }else{ // exec in global, lose the _container_ feature var djg = dojo.global(); if(djg.execScript){ djg.execScript(code); }else{ var djd = dojo.doc(); var sc = djd.createElement("script"); sc.appendChild(djd.createTextNode(code)); (this.containerNode||this.domNode).appendChild(sc); } } }catch(e){ e.text = "Error running scripts from content:\n"+e.description; this._handleDefaults(e, "onExecError", "debug"); } } } );