dojo.provide("dojox.dtl.dom"); dojo.require("dojox.dtl._base"); dojo.require("dojox.dtl.Context"); (function(){ var dd = dojox.dtl; dd.BOOLS = {checked: 1, disabled: 1, readonly: 1}; dd.TOKEN_CHANGE = -11; dd.TOKEN_ATTR = -12; dd.TOKEN_CUSTOM = -13; dd.TOKEN_NODE = 1; var ddt = dd.text; var ddh = dd.dom = { _attributes: {}, _uppers: {}, _re4: /^function anonymous\(\)\s*{\s*(.*)\s*}$/, _reTrim: /(?:^[\n\s]*(\{%)?\s*|\s*(%\})?[\n\s]*$)/g, _reSplit: /\s*%\}[\n\s]*\{%\s*/g, 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"); } if(dojo.isIE){ text = text.replace(/\b(checked|disabled|readonly|style)="/g, 't$1="'); } text = text.replace(/\bstyle="/g, 'tstyle="'); var match; var table = dojo.isWebKit; var pairs = [ // Format: [enable, parent, allowed children (first for nesting), nestings] [true, "select", "option"], [table, "tr", "td|th"], [table, "thead", "tr", "th"], [table, "tbody", "tr", "td"], [table, "table", "tbody|thead|tr", "tr", "td"] ]; var replacements = []; // 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] + "(?:.|\n)*?>((?:.|\n)+?)", "ig"); tagLoop: while(match = selectRe.exec(text)){ // Do it like this to make sure we don't double-wrap var inners = pair[2].split("|"); var innerRe = []; for(var j = 0, inner; inner = inners[j]; j++){ innerRe.push("<" + inner + "(?:.|\n)*?>(?:.|\n)*?"); } var tags = []; var tokens = dojox.string.tokenize(match[1], new RegExp("(" + innerRe.join("|") + ")", "ig"), function(data){ var tag = /<(\w+)/.exec(data)[1]; if(!tags[tag]){ tags[tag] = true; tags.push(tag); } return {data: data}; }); if(tags.length){ var tag = (tags.length == 1) ? tags[0] : pair[2].split("|")[0]; var replace = []; for(var j = 0, jl = tokens.length; j < jl; j++) { var token = tokens[j]; if(dojo.isObject(token)){ replace.push(token.data); }else{ var stripped = token.replace(this._reTrim, ""); if(!stripped){ continue; } token = stripped.split(this._reSplit); for(var k = 0, kl = token.length; k < kl; k++){ var replacement = ""; for(var p = 2, pl = pair.length; p < pl; p++){ if(p == 2){ replacement += "<" + tag + ' dtlinstruction="{% ' + token[k].replace('"', '\\"') + ' %}">'; }else if(tag == pair[p]) { continue; }else{ replacement += "<" + pair[p] + ">"; } } replacement += "DTL"; for(var p = pair.length - 1; p > 1; p--){ if(p == 2){ replacement += ""; }else if(tag == pair[p]) { continue; }else{ replacement += ""; } } replace.push("\xFF" + replacements.length); replacements.push(replacement); } } } text = text.replace(match[1], replace.join("")); } } } } for(var i=replacements.length; i--;){ text = text.replace("\xFF" + i, replacements[i]); } var re = /\b([a-zA-Z_:][a-zA-Z0-9_\-\.:]*)=['"]/g; while(match = re.exec(text)){ var lower = match[1].toLowerCase(); if(lower == "dtlinstruction"){ continue; } if(lower != match[1]){ this._uppers[lower] = match[1]; } this._attributes[lower] = 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; }}, new dd.Token(dd.TOKEN_ATTR, tag[0] + " " + 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 clear = false; 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); } }else if(key == "tstyle"){ clear = key; // Placeholder because we can't use style key = "style"; }else if(dd.BOOLS[key.slice(1)] && dojo.trim(value)){ key = key.slice(1); }else if(this._uppers[key] && dojo.trim(value)){ clear = this._uppers[key]; // Replaced by lowercase } } if(clear){ // Clear out values that are different than will // be used in plugins node.setAttribute(clear, ""); node.removeAttribute(clear); } 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 (some ref plugins) tokens.push([dd.TOKEN_ATTR, node, key, value]); } for(i = 0, child; child = children[i]; i++){ if(child.nodeType == 1){ var instruction = child.getAttribute("dtlinstruction"); if(instruction){ child.parentNode.removeChild(child); child = { nodeType: 8, data: instruction }; } } 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.DomTemplate = dojo.extend(function(/*String|DOMNode|dojo._Url*/ obj){ // summary: Use this object for DOM 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._DomParser(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.DomBuffer(); }, 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.DomBuffer = 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(parent && node.parentNode && node.parentNode === 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); } if(key == "style"){ //console.log(value); this._parent.style.cssText = value; }else{ dojo.attr(this._parent, key, value); //console.log(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._parent = node; this.onSetParent && this.onSetParent(node, up, root); 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._DomNode = 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._DomNodeList = dojo.extend(function(/*Node[]*/ nodes){ // summary: A list of any DOM-specific node objects // 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.DomTemplate.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.DomTemplate.prototype.getBuffer(); nodelist.unshift(new dd.ChangeNode(div)); nodelist.unshift(new dd._DomNode(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._DomNodeList(); var cloned = []; for(var i = 0; i < contents.length; i++){ var clone = contents[i].clone(buffer); if(clone instanceof dd.ChangeNode || clone instanceof dd._DomNode){ var item = clone.contents._clone; if(item){ clone.contents = item; }else if(parent != clone.contents && clone instanceof dd._DomNode){ 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){ var i = this.contents.length - 1; if(this.contents[i] instanceof dd._DomTextNode && this.contents[i].isEmpty()){ this.contents.pop(); }else{ break; } } return this; } }); dd._DomVarNode = 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._DomNodeList(); injected.push(new dd.ChangeNode(buffer.getParent())); injected.push(new dd._DomNode(root)); injected.push(str); injected.push(new dd.ChangeNode(buffer.getParent())); this._rendered = true; return injected.render(context, buffer); case "node": this._rendered = true; if(this._node && this._node != str && this._node.parentNode && this._node.parentNode === buffer.getParent()){ this._node.parentNode.removeChild(this._node); } 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; default: 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": if(this._node.parentNode === buffer.getParent()){ return buffer.remove(this._node); } return buffer; case "html": for(var i=0, l=this._html.length; i