dojo.provide("dojox.dtl.html"); dojo.require("dojox.dtl._base"); dojo.require("dojox.dtl.Context"); (function(){ var dd = dojox.dtl; dd.TOKEN_CHANGE = -11; dd.TOKEN_ATTR = -12; dd.TOKEN_CUSTOM = -13; dd.TOKEN_NODE = 1; var ddt = dd.text; var ddh = dd.html = { _attributes: {}, _re4: /^function anonymous\(\)\s*{\s*(.*)\s*}$/, getTemplate: function(text){ if(typeof this._commentable == "undefined"){ // Check to see if the browser can handle comments this._commentable = false; var div = document.createElement("div"); div.innerHTML = ""; if(div.childNodes.length && div.childNodes[0].nodeType == 8 && div.childNodes[0].data == "comment"){ this._commentable = true; } } if(!this._commentable){ // Strip comments text = text.replace(//g, "$1"); } var match; var pairs = [ [true, "select", "option"], [dojo.isSafari, "tr", "th"], [dojo.isSafari, "tr", "td"], [dojo.isSafari, "thead", "tr", "th"], [dojo.isSafari, "tbody", "tr", "td"] ]; // Some tags can't contain text. So we wrap the text in tags that they can have. for(var i = 0, pair; pair = pairs[i]; i++){ if(!pair[0]){ continue; } if(text.indexOf("<" + pair[1]) != -1){ var selectRe = new RegExp("<" + pair[1] + "[\\s\\S]*?>([\\s\\S]+?)", "ig"); while(match = selectRe.exec(text)){ // Do it like this to make sure we don't double-wrap var found = false; var tokens = dojox.string.tokenize(match[1], new RegExp("(<" + pair[2] + "[\\s\\S]*?>[\\s\\S]*?)", "ig"), function(child){ found = true; return {data: child}; }); if(found){ var replace = []; for(var j = 0; j < tokens.length; j++) { if(dojo.isObject(tokens[j])){ replace.push(tokens[j].data); }else{ var close = pair[pair.length - 1]; var k, replacement = ""; for(k = 2; k < pair.length - 1; k++){ replacement += "<" + pair[k] + ">"; } replacement += "<" + close + ' iscomment="true">' + dojo.trim(tokens[j]) + ""; for(k = 2; k < pair.length - 1; k++){ replacement += ""; } replace.push(replacement); } } text = text.replace(match[1], replace.join("")); } } } } var re = /\b([a-zA-Z]+)=['"]/g; while(match = re.exec(text)){ this._attributes[match[1].toLowerCase()] = true; } var div = document.createElement("div"); div.innerHTML = text; var output = {nodes: []}; while(div.childNodes.length){ output.nodes.push(div.removeChild(div.childNodes[0])) } return output; }, tokenize: function(/*Node*/ nodes){ var tokens = []; for(var i = 0, node; node = nodes[i++];){ if(node.nodeType != 1){ this.__tokenize(node, tokens); }else{ this._tokenize(node, tokens); } } return tokens; }, _swallowed: [], _tokenize: function(/*Node*/ node, /*Array*/ tokens){ var first = false; var swallowed = this._swallowed; var i, j, tag, child; if(!tokens.first){ // Try to efficiently associate tags that use an attribute to // remove the node from DOM (eg dojoType) so that we can efficiently // locate them later in the tokenizing. first = tokens.first = true; var tags = dd.register.getAttributeTags(); for(i = 0; tag = tags[i]; i++){ try{ (tag[2])({ swallowNode: function(){ throw 1; }}, new dd.Token(dd.TOKEN_ATTR, "")); }catch(e){ swallowed.push(tag); } } } for(i = 0; tag = swallowed[i]; i++){ var text = node.getAttribute(tag[0]); if(text){ var swallowed = false; var custom = (tag[2])({ swallowNode: function(){ swallowed = true; return node; }}, text); if(swallowed){ if(node.parentNode && node.parentNode.removeChild){ node.parentNode.removeChild(node); } tokens.push([dd.TOKEN_CUSTOM, custom]); return; } } } var children = []; if(dojo.isIE && node.tagName == "SCRIPT"){ children.push({ nodeType: 3, data: node.text }); node.text = ""; }else{ for(i = 0; child = node.childNodes[i]; i++){ children.push(child); } } tokens.push([dd.TOKEN_NODE, node]); var change = false; if(children.length){ // Only do a change request if we need to tokens.push([dd.TOKEN_CHANGE, node]); change = true; } for(var key in this._attributes){ var value = ""; if(key == "class"){ value = node.className || value; }else if(key == "for"){ value = node.htmlFor || value; }else if(key == "value" && node.value == node.innerHTML){ // Sometimes .value is set the same as the contents of the item (button) continue; }else if(node.getAttribute){ value = node.getAttribute(key, 2) || value; if(key == "href" || key == "src"){ if(dojo.isIE){ var hash = location.href.lastIndexOf(location.hash); var href = location.href.substring(0, hash).split("/"); href.pop(); href = href.join("/") + "/"; if(value.indexOf(href) == 0){ value = value.replace(href, ""); } value = decodeURIComponent(value); } if(value.indexOf("{%") != -1 || value.indexOf("{{") != -1){ node.setAttribute(key, ""); } } } if(typeof value == "function"){ value = value.toString().replace(this._re4, "$1"); } if(!change){ // Only do a change request if we need to tokens.push([dd.TOKEN_CHANGE, node]); change = true; } // We'll have to resolve attributes during parsing tokens.push([dd.TOKEN_ATTR, node, key, value]); } for(i = 0, child; child = children[i]; i++){ if(child.nodeType == 1 && child.getAttribute("iscomment")){ child.parentNode.removeChild(child); child = { nodeType: 8, data: child.innerHTML }; } this.__tokenize(child, tokens); } if(!first && node.parentNode && node.parentNode.tagName){ if(change){ tokens.push([dd.TOKEN_CHANGE, node, true]); } tokens.push([dd.TOKEN_CHANGE, node.parentNode]); node.parentNode.removeChild(node); }else{ // If this node is parentless, it's a base node, so we have to "up" change to itself // and note that it's a top-level to watch for errors tokens.push([dd.TOKEN_CHANGE, node, true, true]); } }, __tokenize: function(child, tokens){ var data = child.data; switch(child.nodeType){ case 1: this._tokenize(child, tokens); return; case 3: if(data.match(/[^\s\n]/) && (data.indexOf("{{") != -1 || data.indexOf("{%") != -1)){ var texts = ddt.tokenize(data); for(var j = 0, text; text = texts[j]; j++){ if(typeof text == "string"){ tokens.push([dd.TOKEN_TEXT, text]); }else{ tokens.push(text); } } }else{ tokens.push([child.nodeType, child]); } if(child.parentNode) child.parentNode.removeChild(child); return; case 8: if(data.indexOf("{%") == 0){ var text = dojo.trim(data.slice(2, -2)); if(text.substr(0, 5) == "load "){ var parts = dojo.trim(text).split(/\s+/g); for(var i = 1, part; part = parts[i]; i++){ dojo["require"](part); } } tokens.push([dd.TOKEN_BLOCK, text]); } if(data.indexOf("{{") == 0){ tokens.push([dd.TOKEN_VAR, dojo.trim(data.slice(2, -2))]); } if(child.parentNode) child.parentNode.removeChild(child); return; } } }; dd.HtmlTemplate = dojo.extend(function(/*String|DOMNode|dojo._Url*/ obj){ // summary: Use this object for HTML templating if(!obj.nodes){ var node = dojo.byId(obj); if(node && node.nodeType == 1){ dojo.forEach(["class", "src", "href", "name", "value"], function(item){ ddh._attributes[item] = true; }); obj = { nodes: [node] }; }else{ if(typeof obj == "object"){ obj = ddt.getTemplateString(obj); } obj = ddh.getTemplate(obj); } } var tokens = ddh.tokenize(obj.nodes); if(dd.tests){ this.tokens = tokens.slice(0); } var parser = new dd._HtmlParser(tokens); this.nodelist = parser.parse(); }, { _count: 0, _re: /\bdojo:([a-zA-Z0-9_]+)\b/g, setClass: function(str){ this.getRootNode().className = str; }, getRootNode: function(){ return this.buffer.rootNode; }, getBuffer: function(){ return new dd.HtmlBuffer(); }, render: function(context, buffer){ buffer = this.buffer = buffer || this.getBuffer(); this.rootNode = null; var output = this.nodelist.render(context || new dd.Context({}), buffer); for(var i = 0, node; node = buffer._cache[i]; i++){ if(node._cache){ node._cache.length = 0; } } return output; }, unrender: function(context, buffer){ return this.nodelist.unrender(context, buffer); } }); dd.HtmlBuffer = dojo.extend(function(/*Node*/ parent){ // summary: Allows the manipulation of DOM // description: // Use this to append a child, change the parent, or // change the attribute of the current node. this._parent = parent; this._cache = []; }, { concat: function(/*DOMNode*/ node){ var parent = this._parent; if(node.parentNode && node.parentNode.tagName && parent && !parent._dirty){ return this; } if(node.nodeType == 1 && !this.rootNode){ this.rootNode = node || true; return this; } if(!parent){ if(node.nodeType == 3 && dojo.trim(node.data)){ throw new Error("Text should not exist outside of the root node in template"); } return this; } if(this._closed){ if(node.nodeType == 3 && !dojo.trim(node.data)){ return this; }else{ throw new Error("Content should not exist outside of the root node in template"); } } if(parent._dirty){ if(node._drawn && node.parentNode == parent){ var caches = parent._cache; if(caches){ for(var i = 0, cache; cache = caches[i]; i++){ this.onAddNode && this.onAddNode(cache); parent.insertBefore(cache, node); this.onAddNodeComplete && this.onAddNodeComplete(cache); } caches.length = 0; } } parent._dirty = false; } if(!parent._cache){ parent._cache = []; this._cache.push(parent); } parent._dirty = true; parent._cache.push(node); return this; }, remove: function(obj){ if(typeof obj == "string"){ if(this._parent){ this._parent.removeAttribute(obj); } }else{ if(obj.nodeType == 1 && !this.getRootNode() && !this._removed){ this._removed = true; return this; } if(obj.parentNode){ this.onRemoveNode && this.onRemoveNode(obj); if(obj.parentNode){ obj.parentNode.removeChild(obj); } } } return this; }, setAttribute: function(key, value){ var old = dojo.attr(this._parent, key); if(this.onChangeAttribute && old != value){ this.onChangeAttribute(this._parent, key, old, value); } dojo.attr(this._parent, key, value); return this; }, addEvent: function(context, type, fn, /*Array|Function*/ args){ if(!context.getThis()){ throw new Error("You must use Context.setObject(instance)"); } this.onAddEvent && this.onAddEvent(this.getParent(), type, fn); var resolved = fn; if(dojo.isArray(args)){ resolved = function(e){ this[fn].apply(this, [e].concat(args)); } } return dojo.connect(this.getParent(), type, context.getThis(), resolved); }, setParent: function(node, /*Boolean?*/ up, /*Boolean?*/ root){ if(!this._parent) this._parent = this._first = node; if(up && root && node === this._first){ this._closed = true; } if(up){ var parent = this._parent; var script = ""; var ie = dojo.isIE && parent.tagName == "SCRIPT"; if(ie){ parent.text = ""; } if(parent._dirty){ var caches = parent._cache; var select = (parent.tagName == "SELECT" && !parent.options.length); for(var i = 0, cache; cache = caches[i]; i++){ if(cache !== parent){ this.onAddNode && this.onAddNode(cache); if(ie){ script += cache.data; }else{ parent.appendChild(cache); if(select && cache.defaultSelected && i){ select = i; } } this.onAddNodeComplete && this.onAddNodeComplete(cache); } } if(select){ parent.options.selectedIndex = (typeof select == "number") ? select : 0; } caches.length = 0; parent._dirty = false; } if(ie){ parent.text = script; } } this.onSetParent && this.onSetParent(node, up); this._parent = node; return this; }, getParent: function(){ return this._parent; }, getRootNode: function(){ return this.rootNode; } /*===== , onSetParent: function(node, up){ // summary: Stub called when setParent is used. }, onAddNode: function(node){ // summary: Stub called before new nodes are added }, onAddNodeComplete: function(node){ // summary: Stub called after new nodes are added }, onRemoveNode: function(node){ // summary: Stub called when nodes are removed }, onChangeAttribute: function(node, attribute, old, updated){ // summary: Stub called when an attribute is changed }, onChangeData: function(node, old, updated){ // summary: Stub called when a data in a node is changed }, onClone: function(from, to){ // summary: Stub called when a node is duplicated // from: DOMNode // to: DOMNode }, onAddEvent: function(node, type, description){ // summary: Stub to call when you're adding an event // node: DOMNode // type: String // description: String } =====*/ }); dd._HtmlNode = dojo.extend(function(node){ // summary: Places a node into DOM this.contents = node; }, { render: function(context, buffer){ this._rendered = true; return buffer.concat(this.contents); }, unrender: function(context, buffer){ if(!this._rendered){ return buffer; } this._rendered = false; return buffer.remove(this.contents); }, clone: function(buffer){ return new this.constructor(this.contents); } }); dd._HtmlNodeList = dojo.extend(function(/*Node[]*/ nodes){ // summary: A list of any HTML-specific node object // description: // Any object that's used in the constructor or added // through the push function much implement the // render, unrender, and clone functions. this.contents = nodes || []; }, { push: function(node){ this.contents.push(node); }, unshift: function(node){ this.contents.unshift(node); }, render: function(context, buffer, /*Node*/ instance){ buffer = buffer || dd.HtmlTemplate.prototype.getBuffer(); if(instance){ var parent = buffer.getParent(); } for(var i = 0; i < this.contents.length; i++){ buffer = this.contents[i].render(context, buffer); if(!buffer) throw new Error("Template node render functions must return their buffer"); } if(parent){ buffer.setParent(parent); } return buffer; }, dummyRender: function(context, buffer, asNode){ // summary: A really expensive way of checking to see how a rendering will look. // Used in the ifchanged tag var div = document.createElement("div"); var parent = buffer.getParent(); var old = parent._clone; // Tell the clone system to attach itself to our new div parent._clone = div; var nodelist = this.clone(buffer, div); if(old){ // Restore state if there was a previous clone parent._clone = old; }else{ // Remove if there was no clone parent._clone = null; } buffer = dd.HtmlTemplate.prototype.getBuffer(); nodelist.unshift(new dd.ChangeNode(div)); nodelist.unshift(new dd._HtmlNode(div)); nodelist.push(new dd.ChangeNode(div, true)); nodelist.render(context, buffer); if(asNode){ return buffer.getRootNode(); } var html = div.innerHTML; return (dojo.isIE) ? html.replace(/\s*_(dirty|clone)="[^"]*"/g, "") : html; }, unrender: function(context, buffer, instance){ if(instance){ var parent = buffer.getParent(); } for(var i = 0; i < this.contents.length; i++){ buffer = this.contents[i].unrender(context, buffer); if(!buffer) throw new Error("Template node render functions must return their buffer"); } if(parent){ buffer.setParent(parent); } return buffer; }, clone: function(buffer){ // summary: // Used to create an identical copy of a NodeList, useful for things like the for tag. var parent = buffer.getParent(); var contents = this.contents; var nodelist = new dd._HtmlNodeList(); var cloned = []; for(var i = 0; i < contents.length; i++){ var clone = contents[i].clone(buffer); if(clone instanceof dd.ChangeNode || clone instanceof dd._HtmlNode){ var item = clone.contents._clone; if(item){ clone.contents = item; }else if(parent != clone.contents && clone instanceof dd._HtmlNode){ var node = clone.contents; clone.contents = clone.contents.cloneNode(false); buffer.onClone && buffer.onClone(node, clone.contents); cloned.push(node); node._clone = clone.contents; } } nodelist.push(clone); } for(var i = 0, clone; clone = cloned[i]; i++){ clone._clone = null; } return nodelist; }, rtrim: function(){ while(1){ i = this.contents.length - 1; if(this.contents[i] instanceof dd._HtmlTextNode && this.contents[i].isEmpty()){ this.contents.pop(); }else{ break; } } return this; } }); dd._HtmlVarNode = dojo.extend(function(str){ // summary: A node to be processed as a variable // description: // Will render an object that supports the render function // and the getRootNode function this.contents = new dd._Filter(str); }, { render: function(context, buffer){ var str = this.contents.resolve(context); // What type of rendering? var type = "text"; if(str){ if(str.render && str.getRootNode){ type = "injection"; }else if(str.safe){ if(str.nodeType){ type = "node"; }else if(str.toString){ str = str.toString(); type = "html"; } } } // Has the typed changed? if(this._type && type != this._type){ this.unrender(context, buffer); } this._type = type; // Now render switch(type){ case "text": this._rendered = true; this._txt = this._txt || document.createTextNode(str); if(this._txt.data != str){ var old = this._txt.data; this._txt.data = str; buffer.onChangeData && buffer.onChangeData(this._txt, old, this._txt.data); } return buffer.concat(this._txt); case "injection": var root = str.getRootNode(); if(this._rendered && root != this._root){ buffer = this.unrender(context, buffer); } this._root = root; var injected = this._injected = new dd._HtmlNodeList(); injected.push(new dd.ChangeNode(buffer.getParent())); injected.push(new dd._HtmlNode(root)); injected.push(str); injected.push(new dd.ChangeNode(buffer.getParent())); this._rendered = true; return injected.render(context, buffer); case "node": this._rendered = true; this._node = str; return buffer.concat(str); case "html": if(this._rendered && this._src != str){ buffer = this.unrender(context, buffer); } this._src = str; // This can get reset in the above tag if(!this._rendered){ this._rendered = true; this._html = this._html || []; var div = (this._div = this._div || document.createElement("div")); div.innerHTML = str; var children = div.childNodes; while(children.length){ var removed = div.removeChild(children[0]); this._html.push(removed); buffer = buffer.concat(removed); } } return buffer; defaul: return buffer; } }, unrender: function(context, buffer){ if(!this._rendered){ return buffer; } this._rendered = false; // Unrender injected nodes switch(this._type){ case "text": return buffer.remove(this._txt); case "injection": return this._injection.unrender(context, buffer); case "node": return buffer.remove(this._node); case "html": for(var i=0, l=this._html.length; i