1 /**
  2  * Returns a {@link pv.Dom} operator for the given map. This is a convenience
  3  * factory method, equivalent to <tt>new pv.Dom(map)</tt>. To apply the operator
  4  * and retrieve the root node, call {@link pv.Dom#root}; to retrieve all nodes
  5  * flattened, use {@link pv.Dom#nodes}.
  6  *
  7  * @see pv.Dom
  8  * @param map a map from which to construct a DOM.
  9  * @returns {pv.Dom} a DOM operator for the specified map.
 10  */
 11 pv.dom = function(map) {
 12   return new pv.Dom(map);
 13 };
 14 
 15 /**
 16  * Constructs a DOM operator for the specified map. This constructor should not
 17  * be invoked directly; use {@link pv.dom} instead.
 18  *
 19  * @class Represets a DOM operator for the specified map. This allows easy
 20  * transformation of a hierarchical JavaScript object (such as a JSON map) to a
 21  * W3C Document Object Model hierarchy. For more information on which attributes
 22  * and methods from the specification are supported, see {@link pv.Dom.Node}.
 23  *
 24  * <p>Leaves in the map are determined using an associated <i>leaf</i> function;
 25  * see {@link #leaf}. By default, leaves are any value whose type is not
 26  * "object", such as numbers or strings.
 27  *
 28  * @param map a map from which to construct a DOM.
 29  */
 30 pv.Dom = function(map) {
 31   this.$map = map;
 32 };
 33 
 34 /** @private The default leaf function. */
 35 pv.Dom.prototype.$leaf = function(n) {
 36   return typeof n != "object";
 37 };
 38 
 39 /**
 40  * Sets or gets the leaf function for this DOM operator. The leaf function
 41  * identifies which values in the map are leaves, and which are internal nodes.
 42  * By default, objects are considered internal nodes, and primitives (such as
 43  * numbers and strings) are considered leaves.
 44  *
 45  * @param {function} f the new leaf function.
 46  * @returns the current leaf function, or <tt>this</tt>.
 47  */
 48 pv.Dom.prototype.leaf = function(f) {
 49   if (arguments.length) {
 50     this.$leaf = f;
 51     return this;
 52   }
 53   return this.$leaf;
 54 };
 55 
 56 /**
 57  * Applies the DOM operator, returning the root node.
 58  *
 59  * @returns {pv.Dom.Node} the root node.
 60  * @param {string} [nodeName] optional node name for the root.
 61  */
 62 pv.Dom.prototype.root = function(nodeName) {
 63   var leaf = this.$leaf, root = recurse(this.$map);
 64 
 65   /** @private */
 66   function recurse(map) {
 67     var n = new pv.Dom.Node();
 68     for (var k in map) {
 69       var v = map[k];
 70       n.appendChild(leaf(v) ? new pv.Dom.Node(v) : recurse(v)).nodeName = k;
 71     }
 72     return n;
 73   }
 74 
 75   root.nodeName = nodeName;
 76   return root;
 77 };
 78 
 79 /**
 80  * Applies the DOM operator, returning the array of all nodes in preorder
 81  * traversal.
 82  *
 83  * @returns {array} the array of nodes in preorder traversal.
 84  */
 85 pv.Dom.prototype.nodes = function() {
 86   return this.root().nodes();
 87 };
 88 
 89 /**
 90  * Constructs a DOM node for the specified value. Instances of this class are
 91  * not typically created directly; instead they are generated from a JavaScript
 92  * map using the {@link pv.Dom} operator.
 93  *
 94  * @class Represents a <tt>Node</tt> in the W3C Document Object Model.
 95  */
 96 pv.Dom.Node = function(value) {
 97   this.nodeValue = value;
 98   this.childNodes = [];
 99 };
100 
101 /**
102  * The node name. When generated from a map, the node name corresponds to the
103  * key at the given level in the map. Note that the root node has no associated
104  * key, and thus has an undefined node name (and no <tt>parentNode</tt>).
105  *
106  * @type string
107  * @field pv.Dom.Node.prototype.nodeName
108  */
109 
110 /**
111  * The node value. When generated from a map, node value corresponds to the leaf
112  * value for leaf nodes, and is undefined for internal nodes.
113  *
114  * @field pv.Dom.Node.prototype.nodeValue
115  */
116 
117 /**
118  * The array of child nodes. This array is empty for leaf nodes. An easy way to
119  * check if child nodes exist is to query <tt>firstChild</tt>.
120  *
121  * @type array
122  * @field pv.Dom.Node.prototype.childNodes
123  */
124 
125 /**
126  * The parent node, which is null for root nodes.
127  *
128  * @type pv.Dom.Node
129  */
130 pv.Dom.Node.prototype.parentNode = null;
131 
132 /**
133  * The first child, which is null for leaf nodes.
134  *
135  * @type pv.Dom.Node
136  */
137 pv.Dom.Node.prototype.firstChild = null;
138 
139 /**
140  * The last child, which is null for leaf nodes.
141  *
142  * @type pv.Dom.Node
143  */
144 pv.Dom.Node.prototype.lastChild = null;
145 
146 /**
147  * The previous sibling node, which is null for the first child.
148  *
149  * @type pv.Dom.Node
150  */
151 pv.Dom.Node.prototype.previousSibling = null;
152 
153 /**
154  * The next sibling node, which is null for the last child.
155  *
156  * @type pv.Dom.Node
157  */
158 pv.Dom.Node.prototype.nextSibling = null;
159 
160 /**
161  * Removes the specified child node from this node.
162  *
163  * @throws Error if the specified child is not a child of this node.
164  * @returns {pv.Dom.Node} the removed child.
165  */
166 pv.Dom.Node.prototype.removeChild = function(n) {
167   var i = this.childNodes.indexOf(n);
168   if (i == -1) throw new Error("child not found");
169   this.childNodes.splice(i, 1);
170   if (n.previousSibling) n.previousSibling.nextSibling = n.nextSibling;
171   else this.firstChild = n.nextSibling;
172   if (n.nextSibling) n.nextSibling.previousSibling = n.previousSibling;
173   else this.lastChild = n.previousSibling;
174   delete n.nextSibling;
175   delete n.previousSibling;
176   delete n.parentNode;
177   return n;
178 };
179 
180 /**
181  * Appends the specified child node to this node. If the specified child is
182  * already part of the DOM, the child is first removed before being added to
183  * this node.
184  *
185  * @returns {pv.Dom.Node} the appended child.
186  */
187 pv.Dom.Node.prototype.appendChild = function(n) {
188   if (n.parentNode) n.parentNode.removeChild(n);
189   n.parentNode = this;
190   n.previousSibling = this.lastChild;
191   if (this.lastChild) this.lastChild.nextSibling = n;
192   else this.firstChild = n;
193   this.lastChild = n;
194   this.childNodes.push(n);
195   return n;
196 };
197 
198 /**
199  * Inserts the specified child <i>n</i> before the given reference child
200  * <i>r</i> of this node. If <i>r</i> is null, this method is equivalent to
201  * {@link #appendChild}. If <i>n</i> is already part of the DOM, it is first
202  * removed before being inserted.
203  *
204  * @throws Error if <i>r</i> is non-null and not a child of this node.
205  * @returns {pv.Dom.Node} the inserted child.
206  */
207 pv.Dom.Node.prototype.insertBefore = function(n, r) {
208   if (!r) return this.appendChild(n);
209   var i = this.childNodes.indexOf(r);
210   if (i == -1) throw new Error("child not found");
211   if (n.parentNode) n.parentNode.removeChild(n);
212   n.parentNode = this;
213   n.nextSibling = r;
214   n.previousSibling = r.previousSibling;
215   if (r.previousSibling) {
216     r.previousSibling.nextSibling = n;
217   } else {
218     if (r == this.lastChild) this.lastChild = n;
219     this.firstChild = n;
220   }
221   this.childNodes.splice(i, 0, n);
222   return n;
223 };
224 
225 /**
226  * Replaces the specified child <i>r</i> of this node with the node <i>n</i>. If
227  * <i>n</i> is already part of the DOM, it is first removed before being added.
228  *
229  * @throws Error if <i>r</i> is not a child of this node.
230  */
231 pv.Dom.Node.prototype.replaceChild = function(n, r) {
232   var i = this.childNodes.indexOf(r);
233   if (i == -1) throw new Error("child not found");
234   if (n.parentNode) n.parentNode.removeChild(n);
235   n.parentNode = this;
236   n.nextSibling = r.nextSibling;
237   n.previousSibling = r.previousSibling;
238   if (r.previousSibling) r.previousSibling.nextSibling = n;
239   else this.firstChild = n;
240   if (r.nextSibling) r.nextSibling.previousSibling = n;
241   else this.lastChild = n;
242   this.childNodes[i] = n;
243   return r;
244 };
245 
246 /**
247  * Visits each node in the tree in preorder traversal, applying the specified
248  * function <i>f</i>. The arguments to the function are:<ol>
249  *
250  * <li>The current node.
251  * <li>The current depth, starting at 0 for the root node.</ol>
252  *
253  * @param {function} f a function to apply to each node.
254  */
255 pv.Dom.Node.prototype.visitBefore = function(f) {
256   function visit(n, i) {
257     f(n, i);
258     for (var c = n.firstChild; c; c = c.nextSibling) {
259       visit(c, i + 1);
260     }
261   }
262   visit(this, 0);
263 };
264 
265 /**
266  * Visits each node in the tree in postorder traversal, applying the specified
267  * function <i>f</i>. The arguments to the function are:<ol>
268  *
269  * <li>The current node.
270  * <li>The current depth, starting at 0 for the root node.</ol>
271  *
272  * @param {function} f a function to apply to each node.
273  */
274 pv.Dom.Node.prototype.visitAfter = function(f) {
275   function visit(n, i) {
276     for (var c = n.firstChild; c; c = c.nextSibling) {
277       visit(c, i + 1);
278     }
279     f(n, i);
280   }
281   visit(this, 0);
282 };
283 
284 /**
285  * Sorts child nodes of this node, and all descendent nodes recursively, using
286  * the specified comparator function <tt>f</tt>. The comparator function is
287  * passed two nodes to compare.
288  *
289  * <p>Note: during the sort operation, the comparator function should not rely
290  * on the tree being well-formed; the values of <tt>previousSibling</tt> and
291  * <tt>nextSibling</tt> for the nodes being compared are not defined during the
292  * sort operation.
293  *
294  * @param {function} f a comparator function.
295  * @returns this.
296  */
297 pv.Dom.Node.prototype.sort = function(f) {
298   if (this.firstChild) {
299     this.childNodes.sort(f);
300     var p = this.firstChild = this.childNodes[0], c;
301     delete p.previousSibling;
302     for (var i = 1; i < this.childNodes.length; i++) {
303       p.sort(f);
304       c = this.childNodes[i];
305       c.previousSibling = p;
306       p = p.nextSibling = c;
307     }
308     this.lastChild = p;
309     delete p.nextSibling;
310     p.sort(f);
311   }
312   return this;
313 };
314 
315 /**
316  * Reverses all sibling nodes.
317  *
318  * @returns this.
319  */
320 pv.Dom.Node.prototype.reverse = function() {
321   var childNodes = [];
322   this.visitAfter(function(n) {
323       while (n.lastChild) childNodes.push(n.removeChild(n.lastChild));
324       for (var c; c = childNodes.pop();) n.insertBefore(c, n.firstChild);
325     });
326   return this;
327 };
328 
329 /** Returns all descendants of this node in preorder traversal. */
330 pv.Dom.Node.prototype.nodes = function() {
331   var array = [];
332 
333   /** @private */
334   function flatten(node) {
335     array.push(node);
336     node.childNodes.forEach(flatten);
337   }
338 
339   flatten(this, array);
340   return array;
341 };
342 
343 /**
344  * Toggles the child nodes of this node. If this node is not yet toggled, this
345  * method removes all child nodes and appends them to a new <tt>toggled</tt>
346  * array attribute on this node. Otherwise, if this node is toggled, this method
347  * re-adds all toggled child nodes and deletes the <tt>toggled</tt> attribute.
348  *
349  * <p>This method has no effect if the node has no child nodes.
350  *
351  * @param {boolean} [recursive] whether the toggle should apply to descendants.
352  */
353 pv.Dom.Node.prototype.toggle = function(recursive) {
354   if (recursive) return this.toggled
355       ? this.visitBefore(function(n) { if (n.toggled) n.toggle(); })
356       : this.visitAfter(function(n) { if (!n.toggled) n.toggle(); });
357   var n = this;
358   if (n.toggled) {
359     for (var c; c = n.toggled.pop();) n.appendChild(c);
360     delete n.toggled;
361   } else if (n.lastChild) {
362     n.toggled = [];
363     while (n.lastChild) n.toggled.push(n.removeChild(n.lastChild));
364   }
365 };
366 
367 /**
368  * Given a flat array of values, returns a simple DOM with each value wrapped by
369  * a node that is a child of the root node.
370  *
371  * @param {array} values.
372  * @returns {array} nodes.
373  */
374 pv.nodes = function(values) {
375   var root = new pv.Dom.Node();
376   for (var i = 0; i < values.length; i++) {
377     root.appendChild(new pv.Dom.Node(values[i]));
378   }
379   return root.nodes();
380 };
381