aboutsummaryrefslogblamecommitdiffstats
path: root/infrastructure/ace/www/magicdom.js
blob: 4bad3d4120ff5151fc6bbccb322a64c3ef91b98f (plain) (tree)




































































































































































































































































































                                                                                                 
/**
 * 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<this.childCount();i++) {
	var result = func(this.child(i), i);
	if (result) break;
      }
    },
    eachDescendant: function (func) {
      this.eachChild(function (n) {
	var result = func(n);
	if (! result) n.eachDescendant(func);
      });
    },
    dumpContents: function() {
      var mnode = this, dnode = this.dom;
      if (mnode.childCount() < 1) {
	mnode.remove();
      }
      else {
	var theParent = dnode.parentNode;
	var n;
	while ((n = dnode.firstChild)) {
	  dnode.removeChild(n);
	  theParent.insertBefore(n, dnode);
	  ensureNormalized(n);
	}
	mnode.remove();
      }
    },
    uniqueId: function() {
      // not actually guaranteed to be unique, e.g. if user copy-pastes
      // nodes with ids
      var dnode = this.dom;
      if (dnode.id) return dnode.id;
      dnode.id = nextUniqueId();
      return dnode.id;
    }
  });
  
  function wrapDom(dnode) {
    if (! dnode) return dnode;
    var mnode;
    if (isNodeText(dnode)) {
      mnode = object(textNodeProto);
    }
    else {
      mnode = object(elementNodeProto);
    }
    mnode.isRoot = (dnode == rootDomNode);
    mnode.dom = dnode;
    return mnode;
  }

  var mdom = {};
  mdom.root = wrapDom(rootDomNode);
  mdom.doc = doc;
  mdom.win = contentWindow;
  mdom.byId = function (id) {
    return wrapDom(mdom.doc.getElementById(id));
  }
  mdom.newText = function (txt) {
    return wrapDom(mdom.doc.createTextNode(txt));
  }
  mdom.newElement = function (tagName) {
    return wrapDom(mdom.doc.createElement(tagName));
  }
  mdom.wrapDom = wrapDom;

  return mdom;
}