/** * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS-IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ function makeMagicDom(rootDomNode, contentWindow){ function nodeToString(node) { if (isNodeText(node)) return '"'+node.nodeValue+'"'; else return '<'+node.tagName+'>'; } var doc = rootDomNode.ownerDocument || rootDomNode.document; function childIndex(dnode) { var idx = 0; var n = dnode; while (n.previousSibling) { idx++; n = n.previousSibling; } return idx; } function ensureNormalized(dnode) { function mergePair(text1, text2) { var theParent = text1.parentNode; var newTextNode = mdom.doc.createTextNode(text1.nodeValue+""+text2.nodeValue); theParent.insertBefore(newTextNode, text1); theParent.removeChild(text1); theParent.removeChild(text2); return newTextNode; } var n = dnode; if (!isNodeText(n)) return; while (n.previousSibling && isNodeText(n.previousSibling)) { n = mergePair(n.previousSibling, n); } while (n.nextSibling && isNodeText(n.nextSibling)) { n = mergePair(n, n.nextSibling); } } function nextUniqueId() { // returns new unique identifier string; // not actually checked for uniqueness, but unique // wrt magicdom. // is document-unique to allow document.getElementById even // in theoretical case of multiple magicdoms per doc var doc = mdom.doc; var nextId = (getAssoc(doc, "nextId") || 1); setAssoc(doc, "nextId", nextId+1); return "magicdomid"+nextId; } var nodeProto = { parent: function() { return wrapDom(((! this.isRoot) && this.dom.parentNode) || null); }, index: function() { return childIndex(this.dom); }, equals: function (otherNode) { return otherNode && otherNode.dom && (this.dom == otherNode.dom); }, prev: function() { return wrapDom(this.dom.previousSibling || null); }, next: function() { return wrapDom(this.dom.nextSibling || null); }, remove: function() { if (! this.isRoot) { var dnode = this.dom; var prevSib = dnode.previousSibling; var nextSib = dnode.nextSibling; var normalizeNeeded = (prevSib && isNodeText(prevSib) && nextSib && isNodeText(nextSib)); var theParent = dnode.parentNode; theParent.removeChild(dnode); if (normalizeNeeded) { ensureNormalized(prevSib); } } }, addNext: function (newNode) { var dnode = this.dom; var nextSib = dnode.nextSibling; if (nextSib) { dnode.parentNode.insertBefore(newNode.dom, nextSib); } else { dnode.parentNode.appendChild(newNode.dom); } if (newNode.isText) ensureNormalized(newNode.dom); }, addPrev: function (newNode) { var dnode = this.dom; dnode.parentNode.insertBefore(newNode.dom, dnode); if (newNode.isText) ensureNormalized(newNode.dom); }, replaceWith: function (newNodes) { // var-args this.replaceWithArray(arguments); }, replaceWithArray: function (newNodes) { var addFunc; if (this.next()) { var next = this.next(); addFunc = function (n) { next.addPrev(n); }; } else { var parent = this.parent(); addFunc = function (n) { parent.appendChild(n); }; } // when using "this" functions, have to keep text // nodes from merging inappropriately var tempNode = mdom.newElement("span"); this.addNext(tempNode); this.remove(); forEach(newNodes, function (n) { addFunc(n); }); tempNode.remove(); }, getProp: function (propName) { return getAssoc(this.dom, propName); }, setProp: function (propName, value) { setAssoc(this.dom, propName, value); }, // not consistent between browsers in how line-breaks are handled innerText: function() { var dnode = this.dom; if ((typeof dnode.innerText) == "string") return dnode.innerText; if ((typeof dnode.textContent) == "string") return dnode.textContent; if ((typeof dnode.nodeValue) == "string") return dnode.nodeValue; return ""; }, depth: function() { try { // ZZZ var d = 0; var n = this; while (! n.isRoot) { d++; n = n.parent(); } return d; } catch (e) { parent.BAD_NODE = this.dom; throw e; } } }; var textNodeProto = extend(object(nodeProto), { isText: true, text: function() { return this.dom.nodeValue; }, eachChild: function() {}, childCount: function() { return 0; }, eachDescendant: function() {}, // precondition: 0 <= start < end <= length wrapRange: function(start, end, newNode) { var origText = this.text(); var text1 = null; if (start > 0) { text1 = mdom.newText(origText.substring(0, start)); } var text2 = mdom.newText(origText.substring(start, end)); var text3 = null; if (end < origText.length) { text3 = mdom.newText(origText.substring(end, origText.length)); } newNode.appendChild(text2); var nodesToUse = [] if (text1) nodesToUse.push(text1); nodesToUse.push(newNode); if (text3) nodesToUse.push(text3); this.replaceWithArray(nodesToUse); return [text1, newNode, text3]; } }); var elementNodeProto = extend(object(nodeProto), { isText: false, childCount: function() { return this.dom.childNodes.length; }, child: function (i) { return wrapDom(this.dom.childNodes.item(i)); }, firstChild: function() { return ((this.childCount() > 0) && this.child(0)) || null; }, lastChild: function() { return ((this.childCount() > 0) && this.child(this.childCount()-1)) || null; }, appendChild: function (newNode) { this.dom.appendChild(newNode.dom); if (newNode.isText) { ensureNormalized(newNode.dom); } }, prependChild: function (newNode) { if (this.childCount() > 0) { this.child(0).addPrev(newNode); } else { this.appendChild(newNode); } }, eachChild: function (func) { for(var i=0;i