/** * Returns a {@link pv.Dom} operator for the given map. This is a convenience * factory method, equivalent to new pv.Dom(map). To apply the operator * and retrieve the root node, call {@link pv.Dom#root}; to retrieve all nodes * flattened, use {@link pv.Dom#nodes}. * * @see pv.Dom * @param map a map from which to construct a DOM. * @returns {pv.Dom} a DOM operator for the specified map. */ pv.dom = function(map) { return new pv.Dom(map); }; /** * Constructs a DOM operator for the specified map. This constructor should not * be invoked directly; use {@link pv.dom} instead. * * @class Represets a DOM operator for the specified map. This allows easy * transformation of a hierarchical JavaScript object (such as a JSON map) to a * W3C Document Object Model hierarchy. For more information on which attributes * and methods from the specification are supported, see {@link pv.Dom.Node}. * *
Leaves in the map are determined using an associated leaf function; * see {@link #leaf}. By default, leaves are any value whose type is not * "object", such as numbers or strings. * * @param map a map from which to construct a DOM. */ pv.Dom = function(map) { this.$map = map; }; /** @private The default leaf function. */ pv.Dom.prototype.$leaf = function(n) { return typeof n != "object"; }; /** * Sets or gets the leaf function for this DOM operator. The leaf function * identifies which values in the map are leaves, and which are internal nodes. * By default, objects are considered internal nodes, and primitives (such as * numbers and strings) are considered leaves. * * @param {function} f the new leaf function. * @returns the current leaf function, or this. */ pv.Dom.prototype.leaf = function(f) { if (arguments.length) { this.$leaf = f; return this; } return this.$leaf; }; /** * Applies the DOM operator, returning the root node. * * @returns {pv.Dom.Node} the root node. * @param {string} [nodeName] optional node name for the root. */ pv.Dom.prototype.root = function(nodeName) { var leaf = this.$leaf, root = recurse(this.$map); /** @private */ function recurse(map) { var n = new pv.Dom.Node(); for (var k in map) { var v = map[k]; n.appendChild(leaf(v) ? new pv.Dom.Node(v) : recurse(v)).nodeName = k; } return n; } root.nodeName = nodeName; return root; }; /** * Applies the DOM operator, returning the array of all nodes in preorder * traversal. * * @returns {array} the array of nodes in preorder traversal. */ pv.Dom.prototype.nodes = function() { return this.root().nodes(); }; /** * Constructs a DOM node for the specified value. Instances of this class are * not typically created directly; instead they are generated from a JavaScript * map using the {@link pv.Dom} operator. * * @class Represents a Node in the W3C Document Object Model. */ pv.Dom.Node = function(value) { this.nodeValue = value; this.childNodes = []; }; /** * The node name. When generated from a map, the node name corresponds to the * key at the given level in the map. Note that the root node has no associated * key, and thus has an undefined node name (and no parentNode). * * @type string * @field pv.Dom.Node.prototype.nodeName */ /** * The node value. When generated from a map, node value corresponds to the leaf * value for leaf nodes, and is undefined for internal nodes. * * @field pv.Dom.Node.prototype.nodeValue */ /** * The array of child nodes. This array is empty for leaf nodes. An easy way to * check if child nodes exist is to query firstChild. * * @type array * @field pv.Dom.Node.prototype.childNodes */ /** * The parent node, which is null for root nodes. * * @type pv.Dom.Node */ pv.Dom.Node.prototype.parentNode = null; /** * The first child, which is null for leaf nodes. * * @type pv.Dom.Node */ pv.Dom.Node.prototype.firstChild = null; /** * The last child, which is null for leaf nodes. * * @type pv.Dom.Node */ pv.Dom.Node.prototype.lastChild = null; /** * The previous sibling node, which is null for the first child. * * @type pv.Dom.Node */ pv.Dom.Node.prototype.previousSibling = null; /** * The next sibling node, which is null for the last child. * * @type pv.Dom.Node */ pv.Dom.Node.prototype.nextSibling = null; /** * Removes the specified child node from this node. * * @throws Error if the specified child is not a child of this node. * @returns {pv.Dom.Node} the removed child. */ pv.Dom.Node.prototype.removeChild = function(n) { var i = this.childNodes.indexOf(n); if (i == -1) throw new Error("child not found"); this.childNodes.splice(i, 1); if (n.previousSibling) n.previousSibling.nextSibling = n.nextSibling; else this.firstChild = n.nextSibling; if (n.nextSibling) n.nextSibling.previousSibling = n.previousSibling; else this.lastChild = n.previousSibling; delete n.nextSibling; delete n.previousSibling; delete n.parentNode; return n; }; /** * Appends the specified child node to this node. If the specified child is * already part of the DOM, the child is first removed before being added to * this node. * * @returns {pv.Dom.Node} the appended child. */ pv.Dom.Node.prototype.appendChild = function(n) { if (n.parentNode) n.parentNode.removeChild(n); n.parentNode = this; n.previousSibling = this.lastChild; if (this.lastChild) this.lastChild.nextSibling = n; else this.firstChild = n; this.lastChild = n; this.childNodes.push(n); return n; }; /** * Inserts the specified child n before the given reference child * r of this node. If r is null, this method is equivalent to * {@link #appendChild}. If n is already part of the DOM, it is first * removed before being inserted. * * @throws Error if r is non-null and not a child of this node. * @returns {pv.Dom.Node} the inserted child. */ pv.Dom.Node.prototype.insertBefore = function(n, r) { if (!r) return this.appendChild(n); var i = this.childNodes.indexOf(r); if (i == -1) throw new Error("child not found"); if (n.parentNode) n.parentNode.removeChild(n); n.parentNode = this; n.nextSibling = r; n.previousSibling = r.previousSibling; if (r.previousSibling) { r.previousSibling.nextSibling = n; } else { if (r == this.lastChild) this.lastChild = n; this.firstChild = n; } this.childNodes.splice(i, 0, n); return n; }; /** * Replaces the specified child r of this node with the node n. If * n is already part of the DOM, it is first removed before being added. * * @throws Error if r is not a child of this node. */ pv.Dom.Node.prototype.replaceChild = function(n, r) { var i = this.childNodes.indexOf(r); if (i == -1) throw new Error("child not found"); if (n.parentNode) n.parentNode.removeChild(n); n.parentNode = this; n.nextSibling = r.nextSibling; n.previousSibling = r.previousSibling; if (r.previousSibling) r.previousSibling.nextSibling = n; else this.firstChild = n; if (r.nextSibling) r.nextSibling.previousSibling = n; else this.lastChild = n; this.childNodes[i] = n; return r; }; /** * Visits each node in the tree in preorder traversal, applying the specified * function f. The arguments to the function are:
Note: during the sort operation, the comparator function should not rely * on the tree being well-formed; the values of previousSibling and * nextSibling for the nodes being compared are not defined during the * sort operation. * * @param {function} f a comparator function. * @returns this. */ pv.Dom.Node.prototype.sort = function(f) { if (this.firstChild) { this.childNodes.sort(f); var p = this.firstChild = this.childNodes[0], c; delete p.previousSibling; for (var i = 1; i < this.childNodes.length; i++) { p.sort(f); c = this.childNodes[i]; c.previousSibling = p; p = p.nextSibling = c; } this.lastChild = p; delete p.nextSibling; p.sort(f); } return this; }; /** * Reverses all sibling nodes. * * @returns this. */ pv.Dom.Node.prototype.reverse = function() { var childNodes = []; this.visitAfter(function(n) { while (n.lastChild) childNodes.push(n.removeChild(n.lastChild)); for (var c; c = childNodes.pop();) n.insertBefore(c, n.firstChild); }); return this; }; /** Returns all descendants of this node in preorder traversal. */ pv.Dom.Node.prototype.nodes = function() { var array = []; /** @private */ function flatten(node) { array.push(node); node.childNodes.forEach(flatten); } flatten(this, array); return array; }; /** * Toggles the child nodes of this node. If this node is not yet toggled, this * method removes all child nodes and appends them to a new toggled * array attribute on this node. Otherwise, if this node is toggled, this method * re-adds all toggled child nodes and deletes the toggled attribute. * *
This method has no effect if the node has no child nodes. * * @param {boolean} [recursive] whether the toggle should apply to descendants. */ pv.Dom.Node.prototype.toggle = function(recursive) { if (recursive) return this.toggled ? this.visitBefore(function(n) { if (n.toggled) n.toggle(); }) : this.visitAfter(function(n) { if (!n.toggled) n.toggle(); }); var n = this; if (n.toggled) { for (var c; c = n.toggled.pop();) n.appendChild(c); delete n.toggled; } else if (n.lastChild) { n.toggled = []; while (n.lastChild) n.toggled.push(n.removeChild(n.lastChild)); } }; /** * Given a flat array of values, returns a simple DOM with each value wrapped by * a node that is a child of the root node. * * @param {array} values. * @returns {array} nodes. */ pv.nodes = function(values) { var root = new pv.Dom.Node(); for (var i = 0; i < values.length; i++) { root.appendChild(new pv.Dom.Node(values[i])); } return root.nodes(); };