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