html.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. define(["./_base/kernel", "./_base/lang", "./_base/array", "./_base/declare", "./dom", "./dom-construct", "./parser"],
  2. function(kernel, lang, darray, declare, dom, domConstruct, parser){
  3. // module:
  4. // dojo/html
  5. // the parser might be needed..
  6. // idCounter is incremented with each instantiation to allow assignment of a unique id for tracking, logging purposes
  7. var idCounter = 0;
  8. var html = {
  9. // summary:
  10. // TODOC
  11. _secureForInnerHtml: function(/*String*/ cont){
  12. // summary:
  13. // removes !DOCTYPE and title elements from the html string.
  14. //
  15. // khtml is picky about dom faults, you can't attach a style or `<title>` node as child of body
  16. // must go into head, so we need to cut out those tags
  17. // cont:
  18. // An html string for insertion into the dom
  19. //
  20. return cont.replace(/(?:\s*<!DOCTYPE\s[^>]+>|<title[^>]*>[\s\S]*?<\/title>)/ig, ""); // String
  21. },
  22. // Deprecated, should use dojo/dom-constuct.empty() directly, remove in 2.0.
  23. _emptyNode: domConstruct.empty,
  24. _setNodeContent: function(/*DomNode*/ node, /*String|DomNode|NodeList*/ cont){
  25. // summary:
  26. // inserts the given content into the given node
  27. // node:
  28. // the parent element
  29. // content:
  30. // the content to be set on the parent element.
  31. // This can be an html string, a node reference or a NodeList, dojo/NodeList, Array or other enumerable list of nodes
  32. // always empty
  33. domConstruct.empty(node);
  34. if(cont){
  35. if(typeof cont == "string"){
  36. cont = domConstruct.toDom(cont, node.ownerDocument);
  37. }
  38. if(!cont.nodeType && lang.isArrayLike(cont)){
  39. // handle as enumerable, but it may shrink as we enumerate it
  40. for(var startlen=cont.length, i=0; i<cont.length; i=startlen==cont.length ? i+1 : 0){
  41. domConstruct.place( cont[i], node, "last");
  42. }
  43. }else{
  44. // pass nodes, documentFragments and unknowns through to dojo.place
  45. domConstruct.place(cont, node, "last");
  46. }
  47. }
  48. // return DomNode
  49. return node;
  50. },
  51. // we wrap up the content-setting operation in a object
  52. _ContentSetter: declare("dojo.html._ContentSetter", null, {
  53. // node: DomNode|String
  54. // An node which will be the parent element that we set content into
  55. node: "",
  56. // content: String|DomNode|DomNode[]
  57. // The content to be placed in the node. Can be an HTML string, a node reference, or a enumerable list of nodes
  58. content: "",
  59. // id: String?
  60. // Usually only used internally, and auto-generated with each instance
  61. id: "",
  62. // cleanContent: Boolean
  63. // Should the content be treated as a full html document,
  64. // and the real content stripped of <html>, <body> wrapper before injection
  65. cleanContent: false,
  66. // extractContent: Boolean
  67. // Should the content be treated as a full html document,
  68. // and the real content stripped of `<html> <body>` wrapper before injection
  69. extractContent: false,
  70. // parseContent: Boolean
  71. // Should the node by passed to the parser after the new content is set
  72. parseContent: false,
  73. // parserScope: String
  74. // Flag passed to parser. Root for attribute names to search for. If scopeName is dojo,
  75. // will search for data-dojo-type (or dojoType). For backwards compatibility
  76. // reasons defaults to dojo._scopeName (which is "dojo" except when
  77. // multi-version support is used, when it will be something like dojo16, dojo20, etc.)
  78. parserScope: kernel._scopeName,
  79. // startup: Boolean
  80. // Start the child widgets after parsing them. Only obeyed if parseContent is true.
  81. startup: true,
  82. // lifecycle methods
  83. constructor: function(/*Object*/ params, /*String|DomNode*/ node){
  84. // summary:
  85. // Provides a configurable, extensible object to wrap the setting on content on a node
  86. // call the set() method to actually set the content..
  87. // the original params are mixed directly into the instance "this"
  88. lang.mixin(this, params || {});
  89. // give precedence to params.node vs. the node argument
  90. // and ensure its a node, not an id string
  91. node = this.node = dom.byId( this.node || node );
  92. if(!this.id){
  93. this.id = [
  94. "Setter",
  95. (node) ? node.id || node.tagName : "",
  96. idCounter++
  97. ].join("_");
  98. }
  99. },
  100. set: function(/* String|DomNode|NodeList? */ cont, /*Object?*/ params){
  101. // summary:
  102. // front-end to the set-content sequence
  103. // cont:
  104. // An html string, node or enumerable list of nodes for insertion into the dom
  105. // If not provided, the object's content property will be used
  106. if(undefined !== cont){
  107. this.content = cont;
  108. }
  109. // in the re-use scenario, set needs to be able to mixin new configuration
  110. if(params){
  111. this._mixin(params);
  112. }
  113. this.onBegin();
  114. this.setContent();
  115. var ret = this.onEnd();
  116. if(ret && ret.then){
  117. // Make dojox/html/_ContentSetter.set() return a Promise that resolves when load and parse complete.
  118. return ret;
  119. }else{
  120. // Vanilla dojo/html._ContentSetter.set() returns a DOMNode for back compat. For 2.0, switch it to
  121. // return a Deferred like above.
  122. return this.node;
  123. }
  124. },
  125. setContent: function(){
  126. // summary:
  127. // sets the content on the node
  128. var node = this.node;
  129. if(!node){
  130. // can't proceed
  131. throw new Error(this.declaredClass + ": setContent given no node");
  132. }
  133. try{
  134. node = html._setNodeContent(node, this.content);
  135. }catch(e){
  136. // check if a domfault occurs when we are appending this.errorMessage
  137. // like for instance if domNode is a UL and we try append a DIV
  138. // FIXME: need to allow the user to provide a content error message string
  139. var errMess = this.onContentError(e);
  140. try{
  141. node.innerHTML = errMess;
  142. }catch(e){
  143. console.error('Fatal ' + this.declaredClass + '.setContent could not change content due to '+e.message, e);
  144. }
  145. }
  146. // always put back the node for the next method
  147. this.node = node; // DomNode
  148. },
  149. empty: function(){
  150. // summary:
  151. // cleanly empty out existing content
  152. // If there is a parse in progress, cancel it.
  153. if(this.parseDeferred){
  154. if(!this.parseDeferred.isResolved()){
  155. this.parseDeferred.cancel();
  156. }
  157. delete this.parseDeferred;
  158. }
  159. // destroy any widgets from a previous run
  160. // NOTE: if you don't want this you'll need to empty
  161. // the parseResults array property yourself to avoid bad things happening
  162. if(this.parseResults && this.parseResults.length){
  163. darray.forEach(this.parseResults, function(w){
  164. if(w.destroy){
  165. w.destroy();
  166. }
  167. });
  168. delete this.parseResults;
  169. }
  170. // this is fast, but if you know its already empty or safe, you could
  171. // override empty to skip this step
  172. domConstruct.empty(this.node);
  173. },
  174. onBegin: function(){
  175. // summary:
  176. // Called after instantiation, but before set();
  177. // It allows modification of any of the object properties -
  178. // including the node and content provided - before the set operation actually takes place
  179. // This default implementation checks for cleanContent and extractContent flags to
  180. // optionally pre-process html string content
  181. var cont = this.content;
  182. if(lang.isString(cont)){
  183. if(this.cleanContent){
  184. cont = html._secureForInnerHtml(cont);
  185. }
  186. if(this.extractContent){
  187. var match = cont.match(/<body[^>]*>\s*([\s\S]+)\s*<\/body>/im);
  188. if(match){ cont = match[1]; }
  189. }
  190. }
  191. // clean out the node and any cruft associated with it - like widgets
  192. this.empty();
  193. this.content = cont;
  194. return this.node; // DomNode
  195. },
  196. onEnd: function(){
  197. // summary:
  198. // Called after set(), when the new content has been pushed into the node
  199. // It provides an opportunity for post-processing before handing back the node to the caller
  200. // This default implementation checks a parseContent flag to optionally run the dojo parser over the new content
  201. if(this.parseContent){
  202. // populates this.parseResults and this.parseDeferred if you need those..
  203. this._parse();
  204. }
  205. return this.node; // DomNode
  206. // TODO: for 2.0 return a Promise indicating that the parse completed.
  207. },
  208. tearDown: function(){
  209. // summary:
  210. // manually reset the Setter instance if its being re-used for example for another set()
  211. // description:
  212. // tearDown() is not called automatically.
  213. // In normal use, the Setter instance properties are simply allowed to fall out of scope
  214. // but the tearDown method can be called to explicitly reset this instance.
  215. delete this.parseResults;
  216. delete this.parseDeferred;
  217. delete this.node;
  218. delete this.content;
  219. },
  220. onContentError: function(err){
  221. return "Error occurred setting content: " + err;
  222. },
  223. onExecError: function(err){
  224. return "Error occurred executing scripts: " + err;
  225. },
  226. _mixin: function(params){
  227. // mix properties/methods into the instance
  228. // TODO: the intention with tearDown is to put the Setter's state
  229. // back to that of the original constructor (vs. deleting/resetting everything regardless of ctor params)
  230. // so we could do something here to move the original properties aside for later restoration
  231. var empty = {}, key;
  232. for(key in params){
  233. if(key in empty){ continue; }
  234. // TODO: here's our opportunity to mask the properties we don't consider configurable/overridable
  235. // .. but history shows we'll almost always guess wrong
  236. this[key] = params[key];
  237. }
  238. },
  239. _parse: function(){
  240. // summary:
  241. // runs the dojo parser over the node contents, storing any results in this.parseResults
  242. // and the parse promise in this.parseDeferred
  243. // Any errors resulting from parsing are passed to _onError for handling
  244. var rootNode = this.node;
  245. try{
  246. // store the results (widgets, whatever) for potential retrieval
  247. var inherited = {};
  248. darray.forEach(["dir", "lang", "textDir"], function(name){
  249. if(this[name]){
  250. inherited[name] = this[name];
  251. }
  252. }, this);
  253. var self = this;
  254. this.parseDeferred = parser.parse({
  255. rootNode: rootNode,
  256. noStart: !this.startup,
  257. inherited: inherited,
  258. scope: this.parserScope
  259. }).then(function(results){
  260. return self.parseResults = results;
  261. }, function(e){
  262. self._onError('Content', e, "Error parsing in _ContentSetter#" + this.id);
  263. });
  264. }catch(e){
  265. this._onError('Content', e, "Error parsing in _ContentSetter#" + this.id);
  266. }
  267. },
  268. _onError: function(type, err, consoleText){
  269. // summary:
  270. // shows user the string that is returned by on[type]Error
  271. // override/implement on[type]Error and return your own string to customize
  272. var errText = this['on' + type + 'Error'].call(this, err);
  273. if(consoleText){
  274. console.error(consoleText, err);
  275. }else if(errText){ // a empty string won't change current content
  276. html._setNodeContent(this.node, errText, true);
  277. }
  278. }
  279. }), // end declare()
  280. set: function(/*DomNode*/ node, /*String|DomNode|NodeList*/ cont, /*Object?*/ params){
  281. // summary:
  282. // inserts (replaces) the given content into the given node. dojo/dom-construct.place(cont, node, "only")
  283. // may be a better choice for simple HTML insertion.
  284. // description:
  285. // Unless you need to use the params capabilities of this method, you should use
  286. // dojo/dom-construct.place(cont, node, "only"). dojo/dom-construct..place() has more robust support for injecting
  287. // an HTML string into the DOM, but it only handles inserting an HTML string as DOM
  288. // elements, or inserting a DOM node. dojo/dom-construct..place does not handle NodeList insertions
  289. // dojo/dom-construct.place(cont, node, "only"). dojo/dom-construct.place() has more robust support for injecting
  290. // an HTML string into the DOM, but it only handles inserting an HTML string as DOM
  291. // elements, or inserting a DOM node. dojo/dom-construct.place does not handle NodeList insertions
  292. // or the other capabilities as defined by the params object for this method.
  293. // node:
  294. // the parent element that will receive the content
  295. // cont:
  296. // the content to be set on the parent element.
  297. // This can be an html string, a node reference or a NodeList, dojo/NodeList, Array or other enumerable list of nodes
  298. // params:
  299. // Optional flags/properties to configure the content-setting. See dojo/html/_ContentSetter
  300. // example:
  301. // A safe string/node/nodelist content replacement/injection with hooks for extension
  302. // Example Usage:
  303. // | html.set(node, "some string");
  304. // | html.set(node, contentNode, {options});
  305. // | html.set(node, myNode.childNodes, {options});
  306. if(undefined == cont){
  307. console.warn("dojo.html.set: no cont argument provided, using empty string");
  308. cont = "";
  309. }
  310. if(!params){
  311. // simple and fast
  312. return html._setNodeContent(node, cont, true);
  313. }else{
  314. // more options but slower
  315. // note the arguments are reversed in order, to match the convention for instantiation via the parser
  316. var op = new html._ContentSetter(lang.mixin(
  317. params,
  318. { content: cont, node: node }
  319. ));
  320. return op.set();
  321. }
  322. }
  323. };
  324. lang.setObject("dojo.html", html);
  325. return html;
  326. });