You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
379 lines
13 KiB
379 lines
13 KiB
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 `<title>` 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 == "number"){ |
|
cont = cont.toString(); |
|
} |
|
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; |
|
} |
|
if(typeof cont == 'number'){ |
|
cont = cont.toString(); |
|
} |
|
// 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#" + self.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 (typeof cont == 'number'){ |
|
cont = cont.toString(); |
|
} |
|
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; |
|
});
|
|
|