define(["./_base/kernel", "./_base/lang", "./_base/array", "./_base/declare", "./dom", "./dom-construct", "./parser"], function(kernel, lang, darray, declare, dom, domConstruct, parser){ // module: // dojo/html // the parser might be needed.. // idCounter is incremented with each instantiation to allow assignment of a unique id for tracking, logging purposes var idCounter = 0; var html = { // summary: // TODOC _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 }, // Deprecated, should use dojo/dom-constuct.empty() directly, remove in 2.0. _emptyNode: domConstruct.empty, _setNodeContent: function(/*DomNode*/ node, /*String|DomNode|NodeList*/ cont){ // summary: // inserts the given content into the given node // 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 // always empty domConstruct.empty(node); if(cont){ if(typeof cont == "string"){ cont = domConstruct.toDom(cont, node.ownerDocument); } if(!cont.nodeType && lang.isArrayLike(cont)){ // handle as enumerable, but it may shrink as we enumerate it for(var startlen=cont.length, i=0; i<cont.length; i=startlen==cont.length ? i+1 : 0){ domConstruct.place( cont[i], node, "last"); } }else{ // pass nodes, documentFragments and unknowns through to dojo.place domConstruct.place(cont, node, "last"); } } // return DomNode return node; }, // we wrap up the content-setting operation in a object _ContentSetter: 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, // parserScope: String // Flag passed to parser. Root for attribute names to search for. If scopeName is dojo, // will search for data-dojo-type (or dojoType). For backwards compatibility // reasons defaults to dojo._scopeName (which is "dojo" except when // multi-version support is used, when it will be something like dojo16, dojo20, etc.) parserScope: kernel._scopeName, // startup: Boolean // Start the child widgets after parsing them. Only obeyed if parseContent is true. startup: true, // lifecycle 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" lang.mixin(this, params || {}); // give precedence to params.node vs. the node argument // and ensure its a node, not an id string node = this.node = dom.byId( this.node || node ); if(!this.id){ this.id = [ "Setter", (node) ? node.id || node.tagName : "", idCounter++ ].join("_"); } }, 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(); var ret = this.onEnd(); if(ret && ret.then){ // Make dojox/html/_ContentSetter.set() return a Promise that resolves when load and parse complete. return ret; }else{ // Vanilla dojo/html._ContentSetter.set() returns a DOMNode for back compat. For 2.0, switch it to // return a Deferred like above. return this.node; } }, setContent: function(){ // summary: // sets the content on the node var node = this.node; if(!node){ // can't proceed throw new Error(this.declaredClass + ": setContent given no node"); } try{ node = 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 // If there is a parse in progress, cancel it. if(this.parseDeferred){ if(!this.parseDeferred.isResolved()){ this.parseDeferred.cancel(); } delete this.parseDeferred; } // destroy any widgets from a previous run // NOTE: if you don't want this you'll need to empty // the parseResults array property yourself to avoid bad things happening if(this.parseResults && this.parseResults.length){ darray.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 domConstruct.empty(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(lang.isString(cont)){ if(this.cleanContent){ cont = 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 and this.parseDeferred if you need those.. this._parse(); } return this.node; // DomNode // TODO: for 2.0 return a Promise indicating that the parse completed. }, 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.parseDeferred; delete this.node; delete this.content; }, onContentError: function(err){ return "Error occurred setting content: " + err; }, onExecError: function(err){ return "Error occurred executing scripts: " + 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 don't 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 // and the parse promise in this.parseDeferred // Any errors resulting from parsing are passed to _onError for handling var rootNode = this.node; try{ // store the results (widgets, whatever) for potential retrieval var inherited = {}; darray.forEach(["dir", "lang", "textDir"], function(name){ if(this[name]){ inherited[name] = this[name]; } }, this); var self = this; this.parseDeferred = parser.parse({ rootNode: rootNode, noStart: !this.startup, inherited: inherited, scope: this.parserScope }).then(function(results){ return self.parseResults = results; }, function(e){ self._onError('Content', e, "Error parsing in _ContentSetter#" + this.id); }); }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 // override/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 html._setNodeContent(this.node, errText, true); } } }), // end declare() set: function(/*DomNode*/ node, /*String|DomNode|NodeList*/ cont, /*Object?*/ params){ // summary: // inserts (replaces) the given content into the given node. dojo/dom-construct.place(cont, node, "only") // may be a better choice for simple HTML insertion. // description: // Unless you need to use the params capabilities of this method, you should use // dojo/dom-construct.place(cont, node, "only"). dojo/dom-construct..place() has more robust support for injecting // an HTML string into the DOM, but it only handles inserting an HTML string as DOM // elements, or inserting a DOM node. dojo/dom-construct..place does not handle NodeList insertions // dojo/dom-construct.place(cont, node, "only"). dojo/dom-construct.place() has more robust support for injecting // an HTML string into the DOM, but it only handles inserting an HTML string as DOM // elements, or inserting a DOM node. dojo/dom-construct.place does not handle NodeList insertions // or the other capabilities as defined by the params object for this method. // 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: // | html.set(node, "some string"); // | html.set(node, contentNode, {options}); // | 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 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 html._ContentSetter(lang.mixin( params, { content: cont, node: node } )); return op.set(); } } }; lang.setObject("dojo.html", html); return html; });