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