1 /** 2 * Provide various tree operations. 3 * 4 * TODO: Add specs 5 */ 6 xq.DomTree = xq.Class({ 7 initialize: function() { 8 xq.addToFinalizeQueue(this); 9 this._blockTags = ["DIV", "DD", "LI", "ADDRESS", "CAPTION", "DT", "H1", "H2", "H3", "H4", "H5", "H6", "HR", "P", "BODY", "BLOCKQUOTE", "PRE", "PARAM", "DL", "OL", "UL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"]; 10 this._blockContainerTags = ["DIV", "DD", "LI", "BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"]; 11 this._listContainerTags = ["OL", "UL", "DL"]; 12 this._tableCellTags = ["TH", "TD"]; 13 this._blockOnlyContainerTags = ["BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR"]; 14 this._atomicTags = ["IMG", "OBJECT", "BR", "HR"]; 15 }, 16 17 getBlockTags: function() { 18 return this._blockTags; 19 }, 20 21 /** 22 * Find common ancestor(parent) and his immediate children(left and right). 23 * 24 * A --- B -+- C -+- D -+- E 25 * | 26 * +- F -+- G 27 * 28 * For example: 29 * > findCommonAncestorAndImmediateChildrenOf("E", "G") 30 * 31 * will return 32 * 33 * > {parent:"B", left:"C", right:"F"} 34 */ 35 findCommonAncestorAndImmediateChildrenOf: function(left, right) { 36 if(left.parentNode == right.parentNode) { 37 return { 38 left:left, 39 right:right, 40 parent:left.parentNode 41 }; 42 } else { 43 var parentsOfLeft = this.collectParentsOf(left, true); 44 var parentsOfRight = this.collectParentsOf(right, true); 45 var ca = this.getCommonAncestor(parentsOfLeft, parentsOfRight); 46 47 var leftAncestor = parentsOfLeft.find(function(node) {return node.parentNode == ca}); 48 var rightAncestor = parentsOfRight.find(function(node) {return node.parentNode == ca}); 49 50 return { 51 left:leftAncestor, 52 right:rightAncestor, 53 parent:ca 54 }; 55 } 56 }, 57 58 /** 59 * Find leaves at edge. 60 * 61 * A --- B -+- C -+- D -+- E 62 * | 63 * +- F -+- G 64 * 65 * For example: 66 * > getLeavesAtEdge("A") 67 * 68 * will return 69 * 70 * > ["E", "G"] 71 */ 72 getLeavesAtEdge: function(element) { 73 if(!element.hasChildNodes()) return [null, null]; 74 75 var findLeft = function(el) { 76 for (var i = 0; i < el.childNodes.length; i++) { 77 if (el.childNodes[i].nodeType == 1 && this.isBlock(el.childNodes[i])) return findLeft(el.childNodes[i]); 78 } 79 return el; 80 }.bind(this); 81 82 var findRight=function(el) { 83 for (var i = el.childNodes.length; i--;) { 84 if (el.childNodes[i].nodeType == 1 && this.isBlock(el.childNodes[i])) return findRight(el.childNodes[i]); 85 } 86 return el; 87 }.bind(this); 88 89 var left = findLeft(element); 90 var right = findRight(element); 91 92 return [left == element ? null : left, right == element ? null : right]; 93 }, 94 95 getCommonAncestor: function(parents1, parents2) { 96 for(var i = 0; i < parents1.length; i++) { 97 for(var j = 0; j < parents2.length; j++) { 98 if(parents1[i] == parents2[j]) return parents1[i]; 99 } 100 } 101 }, 102 103 collectParentsOf: function(node, includeSelf, exitCondition) { 104 var parents = []; 105 if(includeSelf) parents.push(node); 106 107 while((node = node.parentNode) && (node.nodeName != "HTML") && !(typeof exitCondition == "function" && exitCondition(node))) parents.push(node); 108 return parents; 109 }, 110 111 isDescendantOf: function(parent, child) { 112 if(parent.length > 0) { 113 for(var i = 0; i < parent.length; i++) { 114 if(this.isDescendantOf(parent[i], child)) return true; 115 } 116 return false; 117 } 118 119 if(parent == child) return false; 120 121 while (child = child.parentNode) 122 if (child == parent) return true; 123 return false; 124 }, 125 126 /** 127 * Perform tree walking (foreward) 128 */ 129 walkForward: function(node) { 130 if(node.hasChildNodes()) return node.firstChild; 131 if(node.nextSibling) return node.nextSibling; 132 133 while(node = node.parentNode) { 134 if(node.nextSibling) return node.nextSibling; 135 } 136 137 return null; 138 }, 139 140 /** 141 * Perform tree walking (backward) 142 */ 143 walkBackward: function(node) { 144 if(node.previousSibling) { 145 node = node.previousSibling; 146 while(node.hasChildNodes()) {node = node.lastChild;} 147 return node; 148 } 149 150 return node.parentNode; 151 }, 152 153 /** 154 * Perform tree walking (to next siblings) 155 */ 156 walkNext: function(node) {return node.nextSibling}, 157 158 /** 159 * Perform tree walking (to next siblings) 160 */ 161 walkPrev: function(node) {return node.previousSibling}, 162 163 /** 164 * Returns true if target is followed by start 165 */ 166 checkTargetForward: function(start, target) { 167 return this._check(start, this.walkForward, target); 168 }, 169 170 /** 171 * Returns true if start is followed by target 172 */ 173 checkTargetBackward: function(start, target) { 174 return this._check(start, this.walkBackward, target); 175 }, 176 177 findForward: function(start, condition, exitCondition) { 178 return this._find(start, this.walkForward, condition, exitCondition); 179 }, 180 181 findBackward: function(start, condition, exitCondition) { 182 return this._find(start, this.walkBackward, condition, exitCondition); 183 }, 184 185 /** @private */ 186 _check: function(start, direction, target) { 187 if(start == target) return false; 188 189 while(start = direction(start)) { 190 if(start == target) return true; 191 } 192 return false; 193 }, 194 195 /** @private */ 196 _find: function(start, direction, condition, exitCondition) { 197 while(start = direction(start)) { 198 if(exitCondition && exitCondition(start)) return null; 199 if(condition(start)) return start; 200 } 201 return null; 202 }, 203 204 /** 205 * Walks Forward through DOM tree from start to end, and collects all nodes that matches with a filter. 206 * If no filter provided, it just collects all nodes. 207 * 208 * @param function filter a filter function 209 */ 210 collectNodesBetween: function(start, end, filter) { 211 if(start == end) return [start, end].findAll(filter || function() {return true}); 212 213 var nodes = this.collectForward(start, function(node) {return node == end}, filter); 214 if( 215 start != end && 216 typeof filter == "function" && 217 filter(end) 218 ) nodes.push(end); 219 220 return nodes; 221 }, 222 223 collectForward: function(start, exitCondition, filter) { 224 return this.collect(start, this.walkForward, exitCondition, filter); 225 }, 226 227 collectBackward: function(start, exitCondition, filter) { 228 return this.collect(start, this.walkBackward, exitCondition, filter); 229 }, 230 231 collectNext: function(start, exitCondition, filter) { 232 return this.collect(start, this.walkNext, exitCondition, filter); 233 }, 234 235 collectPrev: function(start, exitCondition, filter) { 236 return this.collect(start, this.walkPrev, exitCondition, filter); 237 }, 238 239 collect: function(start, next, exitCondition, filter) { 240 var nodes = [start]; 241 242 while(true) { 243 start = next(start); 244 if( 245 (start == null) || 246 (typeof exitCondition == "function" && exitCondition(start)) 247 ) break; 248 249 nodes.push(start); 250 } 251 252 return (typeof filter == "function") ? nodes.findAll(filter) : nodes; 253 }, 254 255 256 hasBlocks: function(element) { 257 var nodes = element.childNodes; 258 for(var i = 0; i < nodes.length; i++) { 259 if(this.isBlock(nodes[i])) return true; 260 } 261 return false; 262 }, 263 264 hasMixedContents: function(element) { 265 if(!this.isBlock(element)) return false; 266 if(!this.isBlockContainer(element)) return false; 267 268 var hasTextOrInline = false; 269 var hasBlock = false; 270 for(var i = 0; i < element.childNodes.length; i++) { 271 var node = element.childNodes[i]; 272 if(!hasTextOrInline && this.isTextOrInlineNode(node)) hasTextOrInline = true; 273 if(!hasBlock && this.isBlock(node)) hasBlock = true; 274 275 if(hasTextOrInline && hasBlock) break; 276 } 277 if(!hasTextOrInline || !hasBlock) return false; 278 279 return true; 280 }, 281 282 isBlockOnlyContainer: function(element) { 283 if(!element) return false; 284 return this._blockOnlyContainerTags.indexOf(typeof element == 'string' ? element : element.nodeName) != -1; 285 }, 286 287 isTableCell: function(element) { 288 if(!element) return false; 289 return this._tableCellTags.indexOf(typeof element == 'string' ? element : element.nodeName) != -1; 290 }, 291 292 isBlockContainer: function(element) { 293 if(!element) return false; 294 return this._blockContainerTags.indexOf(typeof element == 'string' ? element : element.nodeName) != -1; 295 }, 296 297 isHeading: function(element) { 298 if(!element) return false; 299 return (typeof element == 'string' ? element : element.nodeName).match(/H\d/); 300 }, 301 302 isBlock: function(element) { 303 if(!element) return false; 304 return this._blockTags.indexOf(typeof element == 'string' ? element : element.nodeName) != -1; 305 }, 306 307 isAtomic: function(element) { 308 if(!element) return false; 309 return this._atomicTags.indexOf(typeof element == 'string' ? element : element.nodeName) != -1; 310 }, 311 312 isListContainer: function(element) { 313 if(!element) return false; 314 return this._listContainerTags.indexOf(typeof element == 'string' ? element : element.nodeName) != -1; 315 }, 316 317 isTextOrInlineNode: function(node) { 318 return node && (node.nodeType == 3 || !this.isBlock(node)); 319 } 320 });