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 });