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