dojo.provide("dojo.html"); // the parser might be needed.. dojo.require("dojo.parser"); (function(){ // private scope, sort of a namespace // idCounter is incremented with each instantiation to allow asignment of a unique id for tracking, logging purposes var idCounter = 0; dojo.html._secureForInnerHtml = function(/*String*/ cont){ // summary: // removes !DOCTYPE and title elements from the html string. // // khtml is picky about dom faults, you can't attach a style or node as child of body // must go into head, so we need to cut out those tags // cont: // An html string for insertion into the dom // return cont.replace(/(?:\s*<!DOCTYPE\s[^>]+>|<title[^>]*>[\s\S]*?<\/title>)/ig, ""); // String }; dojo.html._emptyNode = function(/* DomNode */ node){ // summary: // removes all child nodes from the given node // node: // the parent element while(node.firstChild){ dojo._destroyElement(node.firstChild); } }; dojo.html._setNodeContent = function(/* DomNode */ node, /* String|DomNode|NodeList */ cont, /* Boolean? */ shouldEmptyFirst){ // summary: // inserts the given content into the given node // overlaps similiar functionality in dijit.layout.ContentPane._setContent // node: // the parent element // content: // the content to be set on the parent element. // This can be an html string, a node reference or a NodeList, dojo.NodeList, Array or other enumerable list of nodes // shouldEmptyFirst // if shouldEmptyFirst is true, the node will first be emptied of all content before the new content is inserted // defaults to false if(shouldEmptyFirst){ dojo.html._emptyNode(node); } if(typeof cont == "string"){ // there's some hoops to jump through before we can set innerHTML on the would-be parent element. // rationale for this block: // if node is a table derivate tag, some browsers dont allow innerHTML on those // TODO: <select>, <dl>? what other elements will give surprises if you naively set innerHTML? var pre = '', post = '', walk = 0, name = node.nodeName.toLowerCase(); switch(name){ case 'tr': pre = '<tr>'; post = '</tr>'; walk += 1;//fallthrough case 'tbody': case 'thead':// children of THEAD is of same type as TBODY pre = '<tbody>' + pre; post += '</tbody>'; walk += 1;// falltrough case 'table': pre = '<table>' + pre; post += '</table>'; walk += 1; break; } if(walk){ var n = node.ownerDocument.createElement('div'); n.innerHTML = pre + cont + post; do{ n = n.firstChild; }while(--walk); // now we can safely add the child nodes... dojo.forEach(n.childNodes, function(n){ node.appendChild(n.cloneNode(true)); }); }else{ // innerHTML the content as-is into the node (element) // should we ever support setting content on non-element node types? // e.g. text nodes, comments, etc.? node.innerHTML = cont; } }else{ // DomNode or NodeList if(cont.nodeType){ // domNode (htmlNode 1 or textNode 3) node.appendChild(cont); }else{// nodelist or array such as dojo.Nodelist dojo.forEach(cont, function(n){ node.appendChild(n.cloneNode(true)); }); } } // return DomNode return node; }; // we wrap up the content-setting operation in a object dojo.declare("dojo.html._ContentSetter", null, { // node: DomNode|String // An node which will be the parent element that we set content into node: "", // content: String|DomNode|DomNode[] // The content to be placed in the node. Can be an HTML string, a node reference, or a enumerable list of nodes content: "", // id: String? // Usually only used internally, and auto-generated with each instance id: "", // cleanContent: Boolean // Should the content be treated as a full html document, // and the real content stripped of <html>, <body> wrapper before injection cleanContent: false, // extractContent: Boolean // Should the content be treated as a full html document, and the real content stripped of <html>, <body> wrapper before injection extractContent: false, // parseContent: Boolean // Should the node by passed to the parser after the new content is set parseContent: false, // lifecyle methods constructor: function(/* Object */params, /* String|DomNode */node){ // summary: // Provides a configurable, extensible object to wrap the setting on content on a node // call the set() method to actually set the content.. // the original params are mixed directly into the instance "this" dojo.mixin(this, params || {}); // give precedence to params.node vs. the node argument // and ensure its a node, not an id string node = this.node = dojo.byId( this.node || node ); if(!this.id){ this.id = [ "Setter", (node) ? node.id || node.tagName : "", idCounter++ ].join("_"); } if(! (this.node || node)){ new Error(this.declaredClass + ": no node provided to " + this.id); } }, set: function(/* String|DomNode|NodeList? */ cont, /* Object? */ params){ // summary: // front-end to the set-content sequence // cont: // An html string, node or enumerable list of nodes for insertion into the dom // If not provided, the object's content property will be used if(undefined !== cont){ this.content = cont; } // in the re-use scenario, set needs to be able to mixin new configuration if(params){ this._mixin(params); } this.onBegin(); this.setContent(); this.onEnd(); return this.node; }, setContent: function(){ // summary: // sets the content on the node var node = this.node; if(!node) { console.error("setContent given no node"); } try{ node = dojo.html._setNodeContent(node, this.content); }catch(e){ // check if a domfault occurs when we are appending this.errorMessage // like for instance if domNode is a UL and we try append a DIV // FIXME: need to allow the user to provide a content error message string var errMess = this.onContentError(e); try{ node.innerHTML = errMess; }catch(e){ console.error('Fatal ' + this.declaredClass + '.setContent could not change content due to '+e.message, e); } } // always put back the node for the next method this.node = node; // DomNode }, empty: function() { // summary // cleanly empty out existing content // destroy any widgets from a previous run // NOTE: if you dont want this you'll need to empty // the parseResults array property yourself to avoid bad things happenning if(this.parseResults && this.parseResults.length) { dojo.forEach(this.parseResults, function(w) { if(w.destroy){ w.destroy(); } }); delete this.parseResults; } // this is fast, but if you know its already empty or safe, you could // override empty to skip this step dojo.html._emptyNode(this.node); }, onBegin: function(){ // summary // Called after instantiation, but before set(); // It allows modification of any of the object properties // - including the node and content provided - before the set operation actually takes place // This default implementation checks for cleanContent and extractContent flags to // optionally pre-process html string content var cont = this.content; if(dojo.isString(cont)){ if(this.cleanContent){ cont = dojo.html._secureForInnerHtml(cont); } if(this.extractContent){ var match = cont.match(/<body[^>]*>\s*([\s\S]+)\s*<\/body>/im); if(match){ cont = match[1]; } } } // clean out the node and any cruft associated with it - like widgets this.empty(); this.content = cont; return this.node; /* DomNode */ }, onEnd: function(){ // summary // Called after set(), when the new content has been pushed into the node // It provides an opportunity for post-processing before handing back the node to the caller // This default implementation checks a parseContent flag to optionally run the dojo parser over the new content if(this.parseContent){ // populates this.parseResults if you need those.. this._parse(); } return this.node; /* DomNode */ }, tearDown: function(){ // summary // manually reset the Setter instance if its being re-used for example for another set() // description // tearDown() is not called automatically. // In normal use, the Setter instance properties are simply allowed to fall out of scope // but the tearDown method can be called to explicitly reset this instance. delete this.parseResults; delete this.node; delete this.content; }, onContentError: function(err){ return "Error occured setting content: " + err; }, _mixin: function(params){ // mix properties/methods into the instance // TODO: the intention with tearDown is to put the Setter's state // back to that of the original constructor (vs. deleting/resetting everything regardless of ctor params) // so we could do something here to move the original properties aside for later restoration var empty = {}, key; for(key in params){ if(key in empty){ continue; } // TODO: here's our opportunity to mask the properties we dont consider configurable/overridable // .. but history shows we'll almost always guess wrong this[key] = params[key]; } }, _parse: function(){ // summary: // runs the dojo parser over the node contents, storing any results in this.parseResults // Any errors resulting from parsing are passed to _onError for handling var rootNode = this.node; try{ // store the results (widgets, whatever) for potential retrieval this.parseResults = dojo.parser.parse(rootNode, true); }catch(e){ this._onError('Content', e, "Error parsing in _ContentSetter#"+this.id); } }, _onError: function(type, err, consoleText){ // summary: // shows user the string that is returned by on[type]Error // overide/implement on[type]Error and return your own string to customize var errText = this['on' + type + 'Error'].call(this, err); if(consoleText){ console.error(consoleText, err); }else if(errText){ // a empty string won't change current content dojo.html._setNodeContent(this.node, errText, true); } } }); // end dojo.declare() dojo.html.set = function(/* DomNode */ node, /* String|DomNode|NodeList */ cont, /* Object? */ params){ // summary: // inserts (replaces) the given content into the given node // node: // the parent element that will receive the content // cont: // the content to be set on the parent element. // This can be an html string, a node reference or a NodeList, dojo.NodeList, Array or other enumerable list of nodes // params: // Optional flags/properties to configure the content-setting. See dojo.html._ContentSetter // example: // A safe string/node/nodelist content replacement/injection with hooks for extension // Example Usage: // dojo.html.set(node, "some string"); // dojo.html.set(node, contentNode, {options}); // dojo.html.set(node, myNode.childNodes, {options}); if(undefined == cont){ console.warn("dojo.html.set: no cont argument provided, using empty string"); cont = ""; } if(!params){ // simple and fast return dojo.html._setNodeContent(node, cont, true); }else{ // more options but slower // note the arguments are reversed in order, to match the convention for instantiation via the parser var op = new dojo.html._ContentSetter(dojo.mixin( params, { content: cont, node: node } )); return op.set(); } }; })();