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