1 /**
  2  * Encapsulates browser incompatibility problem and provides rich set of DOM manipulation API.
  3  *
  4  * RichDom provides basic CRUD + Advanced DOM manipulation API, various query methods and caret/selection management API
  5  */
  6 xq.RichDom = Class.create({
  7 	/**
  8 	 * Initialize RichDom. Target window and root element should be set after initialization. See setWin and setRoot.
  9 	 *
 10      * @constructor
 11 	 */
 12 	initialize: function() {
 13 		/**
 14 		 * {xq.DomTree} instance of DomTree
 15 		 */
 16 		this.tree = new xq.DomTree();
 17 		
 18 		this._lastMarkerId = 0;
 19 	},
 20 	
 21 	
 22 	
 23 	/**
 24 	 * @param {Window} win Browser's window object
 25 	 */
 26 	setWin: function(win) {
 27 		if(!win) throw "[win] is null";
 28 		this.win = win;
 29 	},
 30 	
 31 	/**
 32 	 * @param {Element} root Root element
 33 	 */
 34 	setRoot: function(root) {
 35 		if(!root) throw "[root] is null";
 36 		if(this.win && (root.ownerDocument != this.win.document)) throw "root.ownerDocument != this.win.document";
 37 		this.root = root;
 38 		this.doc = this.root.ownerDocument;
 39 	},
 40 	
 41 	/**
 42 	 * @returns Browser's window object.
 43 	 */
 44 	getWin: function() {return this.win},
 45 	
 46 	/**
 47 	 * @returns Document object of root element.
 48 	 */
 49 	getDoc: function() {return this.doc},
 50 	
 51 	/**
 52 	 * @returns Root element.
 53 	 */
 54 	getRoot: function() {return this.root},
 55 	
 56 	
 57 	
 58 	/////////////////////////////////////////////
 59 	// CRUDs
 60 	
 61 	clearRoot: function() {
 62 		this.root.innerHTML = "";
 63 		this.root.appendChild(this.makeEmptyParagraph());
 64 	},
 65 	
 66 	/**
 67 	 * Removes place holders and empty text nodes of given element.
 68 	 *
 69 	 * @param {Element} element target element
 70 	 */
 71 	removePlaceHoldersAndEmptyNodes: function(element) {
 72 		var children = element.childNodes;
 73 		if(!children) return;
 74 		var stopAt = this.getBottommostLastChild(element);
 75 		if(!stopAt) return;
 76 		stopAt = this.tree.walkForward(stopAt);
 77 		
 78 		while(true) {
 79 			if(!element || element == stopAt) break;
 80 			
 81 			if(
 82 				this.isPlaceHolder(element) ||
 83 				(element.nodeType == 3 && element.nodeValue == "") ||
 84 				(!this.getNextSibling(element) && element.nodeType == 3 && element.nodeValue.strip() == "")
 85 			) {
 86 				var deleteTarget = element;
 87 				element = this.tree.walkForward(element);
 88 				
 89 				this.deleteNode(deleteTarget);
 90 			} else {
 91 				element = this.tree.walkForward(element);
 92 			}
 93 		}
 94 	},
 95 	
 96 	/**
 97 	 * Sets multiple attributes into element at once
 98 	 *
 99 	 * @param {Element} element target element
100 	 * @param {Object} map key-value pairs
101 	 */
102 	setAttributes: function(element, map) {
103 		for(key in map) element.setAttribute(key, map[key]);
104 	},
105 
106 	/**
107 	 * Creates textnode by given node value.
108 	 *
109 	 * @param {String} value value of textnode
110 	 * @returns {Node} Created text node
111 	 */	
112 	createTextNode: function(value) {return this.doc.createTextNode(value);},
113 
114 	/**
115 	 * Creates empty element by given tag name.
116 	 *
117 	 * @param {String} tagName name of tag
118 	 * @returns {Element} Created element
119 	 */	
120 	createElement: function(tagName) {return this.doc.createElement(tagName);},
121 
122 	/**
123 	 * Creates element from HTML string
124 	 * 
125 	 * @param {String} html HTML string
126 	 * @returns {Element} Created element
127 	 */
128 	createElementFromHtml: function(html) {
129 		var node = this.createElement("div");
130 		node.innerHTML = html;
131 		if(node.childNodes.length != 1) {
132 			throw "Illegal HTML fragment";
133 		}
134 		return this.getFirstChild(node);
135 	},
136 	
137 	/**
138 	 * Deletes node from DOM tree.
139 	 *
140 	 * @param {Node} node Target node which should be deleted
141 	 * @param {boolean} deleteEmptyParentsRecursively Recursively delete empty parent elements
142 	 * @param {boolean} correctEmptyParent Call #correctEmptyElement on empty parent element after deletion
143 	 */	
144 	deleteNode: function(node, deleteEmptyParentsRecursively, correctEmptyParent) {
145 		if(!node || !node.parentNode) return;
146 		
147 		var parent = node.parentNode;
148 		parent.removeChild(node);
149 		
150 		if(deleteEmptyParentsRecursively) {
151 			while(!parent.hasChildNodes()) {
152 				node = parent;
153 				parent = node.parentNode;
154 				if(!parent || this.getRoot() == node) break;
155 				parent.removeChild(node);
156 			}
157 		}
158 		
159 		if(correctEmptyParent && this.isEmptyBlock(parent)) {
160 			parent.innerHTML = "";
161 			this.correctEmptyElement(parent);
162 		}
163 	},
164 
165 	/**
166 	 * Inserts given node into current caret position
167 	 *
168 	 * @param {Node} node Target node
169 	 * @returns {Node} Inserted node. It could be different with given node.
170 	 */
171 	insertNode: function(node) {throw "Not implemented"},
172 
173 	/**
174 	 * Inserts given html into current caret position
175 	 *
176 	 * @param {String} html HTML string
177 	 * @returns {Node} Inserted node. It could be different with given node.
178 	 */
179 	insertHtml: function(html) {
180 		return this.insertNode(this.createElementFromHtml(html));
181 	},
182 	
183 	/**
184 	 * Creates textnode from given text and inserts it into current caret position
185 	 *
186 	 * @param {String} text Value of textnode
187 	 * @returns {Node} Inserted node
188 	 */
189 	insertText: function(text) {
190 		this.insertNode(this.createTextNode(text));
191 	},
192 	
193 	/**
194 	 * Places given node nearby target.
195 	 *
196 	 * @param {Node} node Node to be inserted.
197 	 * @param {Node} target Target node.
198 	 * @param {String} where Possible values: "before", "start", "end", "after"
199 	 * @param {boolean} performValidation Validate node if needed. For example when P placed into UL, its tag name automatically replaced with LI
200 	 *
201 	 * @returns {Node} Inserted node. It could be different with given node.
202 	 */
203 	insertNodeAt: function(node, target, where, performValidation) {
204 		if(
205 			["HTML", "HEAD"].include(target.nodeName) ||
206 			["BODY"].include(target.nodeName) && ["before", "after"].include(where)
207 		) throw "Illegal argument. Cannot move node[" + node.nodeName + "] to '" + where + "' of target[" + target.nodeName + "]"
208 		
209 		var object;
210 		var message;
211 		var secondParam;
212 		
213 		switch(where.toLowerCase()) {
214 			case "before":
215 				object = target.parentNode;
216 				message = 'insertBefore';
217 				secondParam = target;
218 				break
219 			case "start":
220 				if(target.firstChild) {
221 					object = target;
222 					message = 'insertBefore';
223 					secondParam = target.firstChild;
224 				} else {
225 					object = target;
226 					message = 'appendChild';
227 				}
228 				break
229 			case "end":
230 				object = target;
231 				message = 'appendChild';
232 				break
233 			case "after":
234 				if(target.nextSibling) {
235 					object = target.parentNode;
236 					message = 'insertBefore';
237 					secondParam = target.nextSibling;
238 				} else {
239 					object = target.parentNode;
240 					message = 'appendChild';
241 				}
242 				break
243 		}
244 
245 		if(performValidation && this.tree.isListContainer(object) && node.nodeName != "LI") {
246 			var li = this.createElement("LI");
247 			li.appendChild(node);
248 			node = li;
249 			object[message](node, secondParam);		
250 		} else if(performValidation && !this.tree.isListContainer(object) && node.nodeName == "LI") {
251 			this.wrapAllInlineOrTextNodesAs("P", node, true);
252 			var div = this.createElement("DIV");
253 			this.moveChildNodes(node, div);
254 			this.deleteNode(node);
255 			object[message](div, secondParam);
256 			node = this.unwrapElement(div, true);
257 		} else {
258 			object[message](node, secondParam);
259 		}
260 		
261 		return node;
262 	},
263 
264 	/**
265 	 * Creates textnode from given text and places given node nearby target.
266 	 *
267 	 * @param {String} text Text to be inserted.
268 	 * @param {Node} target Target node.
269 	 * @param {String} where Possible values: "before", "start", "end", "after"
270 	 *
271 	 * @returns {Node} Inserted node.
272 	 */
273 	insertTextAt: function(text, target, where) {
274 		return this.insertNodeAt(this.createTextNode(text), target, where);
275 	},
276 
277 	/**
278 	 * Creates element from given HTML string and places given it nearby target.
279 	 *
280 	 * @param {String} html HTML to be inserted.
281 	 * @param {Node} target Target node.
282 	 * @param {String} where Possible values: "before", "start", "end", "after"
283 	 *
284 	 * @returns {Node} Inserted node.
285 	 */
286 	insertHtmlAt: function(html, target, where) {
287 		return this.insertNodeAt(this.createElementFromHtml(html), target, where);
288 	},
289 
290 	/**
291 	 * Replaces element's tag by removing current element and creating new element by given tag name.
292 	 *
293 	 * @param {String} tag New tag name
294 	 * @param {Element} element Target element
295 	 *
296 	 * @returns {Element} Replaced element
297 	 */	
298 	replaceTag: function(tag, element) {
299 		if(element.nodeName == tag) return null;
300 		if(this.tree.isTableCell(element)) return null;
301 		
302 		var newElement = this.createElement(tag);
303 		this.moveChildNodes(element, newElement);
304 		this.copyAttributes(element, newElement, true);
305 		element.parentNode.replaceChild(newElement, element);
306 		
307 		if(!newElement.hasChildNodes()) this.correctEmptyElement(newElement);
308 		
309 		return newElement;
310 	},
311 
312 	/**
313 	 * Unwraps unnecessary paragraph.
314 	 *
315 	 * Unnecessary paragraph is P which is the only child of given container element.
316 	 * For example, P which is contained by LI and is the only child is the unnecessary paragraph.
317 	 * But if given container element is a block-only-container(BLOCKQUOTE, BODY), this method does nothing.
318 	 *
319 	 * @param {Element} element Container element
320 	 * @returns {boolean} True if unwrap performed.
321 	 */
322 	unwrapUnnecessaryParagraph: function(element) {
323 		if(!element) return false;
324 		
325 		if(!this.tree.isBlockOnlyContainer(element) && element.childNodes.length == 1 && element.firstChild.nodeName == "P" && !this.hasImportantAttributes(element.firstChild)) {
326 			var p = element.firstChild;
327 			this.moveChildNodes(p, element);
328 			this.deleteNode(p);
329 			return true;
330 		}
331 		return false;
332 	},
333 	
334 	/**
335 	 * Unwraps element by extracting all children out and removing the element.
336 	 *
337 	 * @param {Element} element Target element
338 	 * @param {boolean} wrapInlineAndTextNodes Wrap all inline and text nodes with P before unwrap
339 	 * @returns {Node} First child of unwrapped element
340 	 */
341 	unwrapElement: function(element, wrapInlineAndTextNodes) {
342 		if(wrapInlineAndTextNodes) this.wrapAllInlineOrTextNodesAs("P", element);
343 		
344 		var nodeToReturn = element.firstChild;
345 		
346 		while(element.firstChild) this.insertNodeAt(element.firstChild, element, "before");
347 		this.deleteNode(element);
348 		
349 		return nodeToReturn;
350 	},
351 	
352 	/**
353 	 * Wraps element by given tag
354 	 *
355 	 * @param {String} tag tag name
356 	 * @param {Element} element target element to wrap
357 	 * @returns {Element} wrapper
358 	 */
359 	wrapElement: function(tag, element) {
360 		var wrapper = this.insertNodeAt(this.createElement(tag), element, "before");
361 		wrapper.appendChild(element);
362 		return wrapper;
363 	},
364 	
365 	/**
366 	 * Tests #smartWrap with given criteria but doesn't change anything
367 	 */
368 	testSmartWrap: function(endElement, criteria) {
369 		return this.smartWrap(endElement, null, criteria, true);
370 	},
371 	
372 	/**
373 	 * Create inline element with given tag name and wraps nodes nearby endElement by given criteria
374 	 *
375 	 * @param {Element} endElement Boundary(end point, exclusive) of wrapper.
376 	 * @param {String} tag Tag name of wrapper.
377 	 * @param {Object} function which returns text index of start boundary.
378 	 * @param {boolean} testOnly just test boundary and do not perform actual wrapping.
379 	 *
380 	 * @returns {Element} wrapper
381 	 */
382 	smartWrap: function(endElement, tag, criteria, testOnly) {
383 		var block = this.getParentBlockElementOf(endElement);
384 
385 		tag = tag || "SPAN";
386 		criteria = criteria || function(text) {return -1};
387 		
388 		// check for empty wrapper
389 		if(!testOnly && (!endElement.previousSibling || this.isEmptyBlock(block))) {
390 			var wrapper = this.insertNodeAt(this.createElement(tag), endElement, "before");
391 			return wrapper;
392 		}
393 		
394 		// collect all textnodes
395 		var textNodes = this.tree.collectForward(block, function(node) {return node == endElement}, function(node) {return node.nodeType == 3});
396 		
397 		// find textnode and break-point
398 		var nodeIndex = 0;
399 		var nodeValues = textNodes.pluck("nodeValue");
400 		var textToWrap = nodeValues.join("");
401 		var textIndex = criteria(textToWrap)
402 		var breakPoint = textIndex;
403 		
404 		if(breakPoint == -1) {
405 			breakPoint = 0;
406 		} else {
407 			textToWrap = textToWrap.substring(breakPoint);
408 		}
409 		
410 		for(var i = 0; i < textNodes.length; i++) {
411 			if(breakPoint > nodeValues[i].length) {
412 				breakPoint -= nodeValues[i].length;
413 			} else {
414 				nodeIndex = i;
415 				break;
416 			}
417 		}
418 		
419 		if(testOnly) return {text:textToWrap, textIndex:textIndex, nodeIndex:nodeIndex, breakPoint:breakPoint};
420 		
421 		// break textnode if necessary 
422 		if(breakPoint != 0) {
423 			var splitted = textNodes[nodeIndex].splitText(breakPoint);
424 			nodeIndex++;
425 			textNodes.splice(nodeIndex, 0, splitted);
426 		}
427 		var startElement = textNodes[nodeIndex] || block.firstChild;
428 		
429 		// split inline elements up to parent block if necessary
430 		var family = this.tree.findCommonAncestorAndImmediateChildrenOf(startElement, endElement);
431 		var ca = family.parent;
432 		if(ca) {
433 			if(startElement.parentNode != ca) startElement = this.splitElementUpto(startElement, ca, true);
434 			if(endElement.parentNode != ca) endElement = this.splitElementUpto(endElement, ca, true);
435 			
436 			var prevStart = startElement.previousSibling;
437 			var nextEnd = endElement.nextSibling;
438 			
439 			// remove empty inline elements
440 			if(prevStart && prevStart.nodeType == 1 && this.isEmptyBlock(prevStart)) this.deleteNode(prevStart);
441 			if(nextEnd && nextEnd.nodeType == 1 && this.isEmptyBlock(nextEnd)) this.deleteNode(nextEnd);
442 			
443 			// wrap
444 			var wrapper = this.insertNodeAt(this.createElement(tag), startElement, "before");
445 			while(wrapper.nextSibling != endElement) wrapper.appendChild(wrapper.nextSibling);
446 			return wrapper;
447 		} else {
448 			// wrap
449 			var wrapper = this.insertNodeAt(this.createElement(tag), endElement, "before");
450 			return wrapper;
451 		}
452 	},
453 	
454 	/**
455 	 * Wraps all adjust inline elements and text nodes into block element.
456 	 *
457 	 * TODO: empty element should return empty array when it is not forced and (at least) single item array when forced
458 	 *
459 	 * @param {String} tag Tag name of wrapper
460 	 * @param {Element} element Target element
461 	 * @param {boolean} force Force wrapping. If it is set to false, this method do not makes unnecessary wrapper.
462 	 *
463 	 * @returns {Array} Array of wrappers. If nothing performed it returns empty array
464 	 */
465 	wrapAllInlineOrTextNodesAs: function(tag, element, force) {
466 		var wrappers = [];
467 		
468 		if(!force && !this.tree.hasMixedContents(element)) return wrappers;
469 		
470 		var node = element.firstChild;
471 		while(node) {
472 			if(this.tree.isTextOrInlineNode(node)) {
473 				var wrapper = this.wrapInlineOrTextNodesAs(tag, node);
474 				wrappers.push(wrapper);
475 				node = wrapper.nextSibling;
476 			} else {
477 				node = node.nextSibling;
478 			}
479 		}
480 
481 		return wrappers;
482 	},
483 
484 	/**
485 	 * Wraps node and its adjust next siblings into an element
486 	 */
487 	wrapInlineOrTextNodesAs: function(tag, node) {
488 		var wrapper = this.createElement(tag);
489 		var from = node;
490 
491 		from.parentNode.replaceChild(wrapper, from);
492 		wrapper.appendChild(from);
493 
494 		// move nodes into wrapper
495 		while(wrapper.nextSibling && this.tree.isTextOrInlineNode(wrapper.nextSibling)) wrapper.appendChild(wrapper.nextSibling);
496 
497 		return wrapper;
498 	},
499 	
500 	/**
501 	 * Turns block element into list item
502 	 *
503 	 * @param {Element} element Target element
504 	 * @param {String} type One of "UL", "OL", "CODE". "CODE" is same with "OL" but it gives "OL" a class name "code"
505 	 *
506 	 * @return {Element} LI element
507 	 */
508 	turnElementIntoListItem: function(element, type) {
509 		type = type.toUpperCase();
510 		
511 		var container = this.createElement(type == "UL" ? "UL" : "OL");
512 		if(type == "CODE") container.className = "code";
513 		
514 		if(this.tree.isTableCell(element)) {
515 			var p = this.wrapAllInlineOrTextNodesAs("P", element, true)[0];
516 			container = this.insertNodeAt(container, element, "start");
517 			var li = this.insertNodeAt(this.createElement("LI"), container, "start");
518 			li.appendChild(p);
519 		} else {
520 			container = this.insertNodeAt(container, element, "after");
521 			var li = this.insertNodeAt(this.createElement("LI"), container, "start");
522 			li.appendChild(element);
523 		}
524 		
525 		this.unwrapUnnecessaryParagraph(li);
526 		this.mergeAdjustLists(container);
527 		
528 		return li;
529 	},
530 	
531 	/**
532 	 * Extracts given element out from its parent element.
533 	 * 
534 	 * @param {Element} element Target element
535 	 */
536 	extractOutElementFromParent: function(element) {
537 		if(element == this.root || this.root == element.parentNode || !element.offsetParent) return null;
538 		
539 		if(element.nodeName == "LI") {
540 			this.wrapAllInlineOrTextNodesAs("P", element, true);
541 			element = element.firstChild;
542 		}
543 
544 		var container = element.parentNode;
545 		var nodeToReturn = null;
546 		
547 		if(container.nodeName == "LI" && container.parentNode.parentNode.nodeName == "LI") {
548 			// nested list item
549 			if(element.previousSibling) {
550 				this.splitContainerOf(element, true);
551 				this.correctEmptyElement(element);
552 			}
553 			
554 			this.outdentListItem(element);
555 			nodeToReturn = element;
556 		} else if(container.nodeName == "LI") {
557 			// not-nested list item
558 			
559 			if(this.tree.isListContainer(element.nextSibling)) {
560 				// 1. split listContainer
561 				var listContainer = container.parentNode;
562 				this.splitContainerOf(container, true);
563 				this.correctEmptyElement(element);
564 				
565 				// 2. extract out LI's children
566 				nodeToReturn = container.firstChild;
567 				while(container.firstChild) {
568 					this.insertNodeAt(container.firstChild, listContainer, "before");
569 				}
570 				
571 				// 3. remove listContainer and merge adjust lists
572 				var prevContainer = listContainer.previousSibling;
573 				this.deleteNode(listContainer);
574 				if(prevContainer && this.tree.isListContainer(prevContainer)) this.mergeAdjustLists(prevContainer);
575 			} else {
576 				// 1. split LI
577 				this.splitContainerOf(element, true);
578 				this.correctEmptyElement(element);
579 				
580 				// 2. split list container
581 				var listContainer = this.splitContainerOf(container);
582 				
583 				// 3. extract out
584 				this.insertNodeAt(element, listContainer.parentNode, "before");
585 				this.deleteNode(listContainer.parentNode);
586 				
587 				nodeToReturn = element;
588 			}
589 		} else if(this.tree.isTableCell(container) || this.tree.isTableCell(element)) {
590 			// do nothing
591 		} else {
592 			// normal block
593 			this.splitContainerOf(element, true);
594 			this.correctEmptyElement(element);
595 			nodeToReturn = this.insertNodeAt(element, container, "before");
596 			
597 			this.deleteNode(container);
598 		}
599 		
600 		return nodeToReturn;
601 	},
602 	
603 	/**
604 	 * Insert new block above or below given element.
605 	 *
606 	 * @param {Element} block Target block
607 	 * @param {boolean} before Insert new block above(before) target block
608 	 * @param {String} forceTag New block's tag name. If omitted, target block's tag name will be used.
609 	 *
610 	 * @returns {Element} Inserted block
611 	 */
612 	insertNewBlockAround: function(block, before, forceTag) {
613 		var isListItem = block.nodeName == "LI" || block.parentNode.nodeName == "LI";
614 		
615 		this.removeTrailingWhitespace(block);
616 		if(this.isFirstLiWithNestedList(block) && !forceTag && before) {
617 			var li = this.getParentElementOf(block, ["LI"]);
618 			var newBlock = this._insertNewBlockAround(li, before);
619 			return newBlock;
620 		} else if(isListItem && !forceTag) {
621 			var li = this.getParentElementOf(block, ["LI"]);
622 			var newBlock = this._insertNewBlockAround(block, before);
623 			if(li != block) newBlock = this.splitContainerOf(newBlock, false, "prev");
624 			return newBlock;
625 		} else if(this.tree.isBlockContainer(block)) {
626 			this.wrapAllInlineOrTextNodesAs("P", block, true);
627 			return this._insertNewBlockAround(block.firstChild, before, forceTag);
628 		} else {
629 			return this._insertNewBlockAround(block, before, this.tree.isHeading(block) ? "P" : forceTag);
630 		}
631 	},
632 	
633 	/**
634 	 * @private
635 	 *
636 	 * TODO: Rename
637 	 */
638 	_insertNewBlockAround: function(element, before, tagName) {
639 		var newElement = this.createElement(tagName || element.nodeName);
640 		this.copyAttributes(element, newElement, false);
641 		this.correctEmptyElement(newElement);
642 		newElement = this.insertNodeAt(newElement, element, before ? "before" : "after");
643 		return newElement;
644 	},
645 	
646 	/**
647 	 * Wrap or replace element with given tag name.
648 	 *
649 	 * @param {String} tag Tag name
650 	 * @param {Element} element Target element
651 	 *
652 	 * @return {Element} wrapper element or replaced element.
653 	 */
654 	applyTagIntoElement: function(tag, element) {
655 		if(this.tree.isBlockOnlyContainer(tag)) {
656 			return this.wrapBlock(tag, element);
657 		} else if(this.tree.isBlockContainer(element)) {
658 			var wrapper = this.createElement(tag);
659 			this.moveChildNodes(element, wrapper);
660 			return this.insertNodeAt(wrapper, element, "start");
661 		} else {
662 			if(this.tree.isBlockContainer(tag) && this.hasImportantAttributes(element)) {
663 				return this.wrapBlock(tag, element);
664 			} else {
665 				return this.replaceTag(tag, element);
666 			}
667 		}
668 		
669 		throw "IllegalArgumentException - [" + tag + ", " + element + "]";
670 	},
671 	
672 	/**
673 	 * Wrap or replace elements with given tag name.
674 	 *
675 	 * @param {String} tag Tag name
676 	 * @param {Element} from Start boundary (inclusive)
677 	 * @param {Element} to End boundary (inclusive)
678 	 *
679 	 * @returns {Array} Array of wrappers or replaced elements
680 	 */
681 	applyTagIntoElements: function(tagName, from, to) {
682 		var applied = [];
683 		
684 		if(this.tree.isBlockContainer(tagName)) {
685 			var family = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to);
686 			var node = family.left;
687 			var wrapper = this.insertNodeAt(this.createElement(tagName), node, "before");
688 			
689 			var coveringWholeList =
690 				family.parent.nodeName == "LI" &&
691 				family.parent.parentNode.childNodes.length == 1 &&
692 				!family.left.previousSilbing &&
693 				!family.right.nextSibling;
694 				
695 			if(coveringWholeList) {
696 				var ul = node.parentNode.parentNode;
697 				this.insertNodeAt(wrapper, ul, "before");
698 				wrapper.appendChild(ul);
699 			} else {
700 				while(node != family.right) {
701 					next = node.nextSibling;
702 					wrapper.appendChild(node);
703 					node = next;
704 				}
705 				wrapper.appendChild(family.right);
706 			}
707 			applied.push(wrapper);
708 		} else {
709 			// is normal tagName
710 			var elements = this.getBlockElementsBetween(from, to);
711 			for(var i = 0; i < elements.length; i++) {
712 				if(this.tree.isBlockContainer(elements[i])) {
713 					applied.push(this.wrapAllInlineOrTextNodesAs(tagName, elements[i], true));
714 				} else {
715 					applied.push(this.replaceTag(tagName, elements[i]));
716 				}
717 			}
718 		}
719 		return applied.flatten();
720 	},
721 	
722 	/**
723 	 * Moves block up or down
724 	 *
725 	 * @param {Element} block Target block
726 	 * @param {boolean} up Move up if true
727 	 * 
728 	 * @returns {Element} Moved block. It could be different with given block.
729 	 */
730 	moveBlock: function(block, up) {
731 		// if block is table cell or contained by table cell, select its row as mover
732 		block = this.getParentElementOf(block, ["TR"]) || block;
733 		
734 		// if block is only child, select its parent as mover
735 		while(block.nodeName != "TR" && block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) {
736 			block = block.parentNode;
737 		}
738 		
739 		// find target and where
740 		var target, where;
741 		if (up) {
742 			target = block.previousSibling;
743 			
744 			if(target) {
745 				var singleNodeLi = target.nodeName == 'LI' && ((target.childNodes.length == 1 && this.tree.isBlock(target.firstChild)) || !this.tree.hasBlocks(target));
746 				var table = ['TABLE', 'TR'].include(target.nodeName);
747 
748 				where = this.tree.isBlockContainer(target) && !singleNodeLi && !table ? "end" : "before";
749 			} else if(block.parentNode != this.getRoot()) {
750 				target = block.parentNode;
751 				where = "before";
752 			}
753 		} else {
754 			target = block.nextSibling;
755 			
756 			if(target) {
757 				var singleNodeLi = target.nodeName == 'LI' && ((target.childNodes.length == 1 && this.tree.isBlock(target.firstChild)) || !this.tree.hasBlocks(target));
758 				var table = ['TABLE', 'TR'].include(target.nodeName);
759 				
760 				where = this.tree.isBlockContainer(target) && !singleNodeLi && !table ? "start" : "after";
761 			} else if(block.parentNode != this.getRoot()) {
762 				target = block.parentNode;
763 				where = "after";
764 			}
765 		}
766 		
767 		
768 		// no way to go?
769 		if(!target) return null;
770 		if(["TBODY", "THEAD"].include(target.nodeName)) return null;
771 		
772 		// normalize
773 		this.wrapAllInlineOrTextNodesAs("P", target, true);
774 		
775 		// make placeholder if needed
776 		if(this.isFirstLiWithNestedList(block)) {
777 			this.insertNewBlockAround(block, false, "P");
778 		}
779 		
780 		// perform move
781 		var parent = block.parentNode;
782 		var moved = this.insertNodeAt(block, target, where, true);
783 		
784 		// cleanup
785 		if(!parent.hasChildNodes()) this.deleteNode(parent, true);
786 		this.unwrapUnnecessaryParagraph(moved);
787 		this.unwrapUnnecessaryParagraph(target);
788 
789 		// remove placeholder
790 		if(up) {
791 			if(moved.previousSibling && this.isEmptyBlock(moved.previousSibling) && !moved.previousSibling.previousSibling && moved.parentNode.nodeName == "LI" && this.tree.isListContainer(moved.nextSibling)) {
792 				this.deleteNode(moved.previousSibling);
793 			}
794 		} else {
795 			if(moved.nextSibling && this.isEmptyBlock(moved.nextSibling) && !moved.previousSibling && moved.parentNode.nodeName == "LI" && this.tree.isListContainer(moved.nextSibling.nextSibling)) {
796 				this.deleteNode(moved.nextSibling);
797 			}
798 		}
799 		
800 		return moved;
801 	},
802 	
803 	/**
804 	 * Remove given block
805 	 *
806 	 * @param {Element} block Target block
807 	 * @returns {Element} Nearest block of remove element
808 	 */
809 	removeBlock: function(block) {
810 		var blockToMove;
811 
812 		// if block is only child, select its parent as mover
813 		while(block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) {
814 			block = block.parentNode;
815 		}
816 		
817 		var finder = function(node) {return this.tree.isBlock(node) && !this.tree.isAtomic(node) && !this.tree.isDescendantOf(block, node) && !this.tree.hasBlocks(node);}.bind(this);
818 		var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this);
819 		
820 		if(this.isFirstLiWithNestedList(block)) {
821 			blockToMove = this.outdentListItem(block.nextSibling.firstChild);
822 			this.deleteNode(blockToMove.previousSibling, true);
823 		} else if(this.tree.isTableCell(block)) {
824 			var rtable = new xq.RichTable(this, this.getParentElementOf(block, ["TABLE"]));
825 			blockToMove = rtable.getBelowCellOf(block);
826 			
827 			// should not delete row when there's thead and the row is the only child of tbody
828 			if(
829 				block.parentNode.parentNode.nodeName == "TBODY" &&
830 				rtable.hasHeadingAtTop() &&
831 				rtable.getDom().tBodies[0].rows.length == 1) return blockToMove;
832 			
833 			blockToMove = blockToMove ||
834 				this.tree.findForward(block, finder, exitCondition) ||
835 				this.tree.findBackward(block, finder, exitCondition);
836 			
837 			this.deleteNode(block.parentNode, true);
838 		} else {
839 			blockToMove = blockToMove ||
840 				this.tree.findForward(block, finder, exitCondition) ||
841 				this.tree.findBackward(block, finder, exitCondition);
842 			
843 			if(!blockToMove) blockToMove = this.insertNodeAt(this.makeEmptyParagraph(), block, "after");
844 			
845 			this.deleteNode(block, true);
846 		}
847 		if(!this.getRoot().hasChildNodes()) {
848 			blockToMove = this.createElement("P");
849 			this.getRoot().appendChild(blockToMove);
850 			this.correctEmptyElement(blockToMove);
851 		}
852 		
853 		return blockToMove;
854 	},
855 	
856 	/**
857 	 * Removes trailing whitespaces of given block
858 	 *
859 	 * @param {Element} block Target block
860 	 */
861 	removeTrailingWhitespace: function(block) {throw "Not implemented"},
862 	
863 	/**
864 	 * Extract given list item out and change its container's tag
865 	 *
866 	 * @param {Element} element LI or P which is a child of LI
867 	 * @param {String} type "OL", "UL", or "CODE"
868 	 *
869 	 * @returns {Element} changed element
870 	 */
871 	changeListTypeTo: function(element, type) {
872 		type = type.toUpperCase();
873 		
874 		var li = this.getParentElementOf(element, ["LI"]);
875 		if(!li) throw "IllegalArgumentException";
876 		
877 		var container = li.parentNode;
878 
879 		this.splitContainerOf(li);
880 		
881 		var newContainer = this.insertNodeAt(this.createElement(type == "UL" ? "UL" : "OL"), container, "before");
882 		if(type == "CODE") newContainer.className = "code";
883 		
884 		this.insertNodeAt(li, newContainer, "start");
885 		this.deleteNode(container);
886 		
887 		this.mergeAdjustLists(newContainer);
888 		
889 		return element;
890 	},
891 	
892 	/**
893 	 * Split container of element into (maxium) three pieces.
894 	 */
895 	splitContainerOf: function(element, preserveElementItself, dir) {
896 		if([element, element.parentNode].include(this.getRoot())) return element;
897 
898 		var container = element.parentNode;
899 		if(element.previousSibling && (!dir || dir.toLowerCase() == "prev")) {
900 			var prev = this.createElement(container.nodeName);
901 			this.copyAttributes(container, prev);
902 			while(container.firstChild != element) {
903 				prev.appendChild(container.firstChild);
904 			}
905 			this.insertNodeAt(prev, container, "before");
906 			this.unwrapUnnecessaryParagraph(prev);
907 		}
908 		
909 		if(element.nextSibling && (!dir || dir.toLowerCase() == "next")) {
910 			var next = this.createElement(container.nodeName);
911 			this.copyAttributes(container, next);
912 			while(container.lastChild != element) {
913 				this.insertNodeAt(container.lastChild, next, "start");
914 			}
915 			this.insertNodeAt(next, container, "after");
916 			this.unwrapUnnecessaryParagraph(next);
917 		}
918 		
919 		if(!preserveElementItself) element = this.unwrapUnnecessaryParagraph(container) ? container : element;
920 		return element;
921 	},
922 
923 	/**
924 	 * TODO: Add specs
925 	 */
926 	splitParentElement: function(seperator) {
927 		var parent = seperator.parentNode;
928 		if(["HTML", "HEAD", "BODY"].include(parent.nodeName)) throw "Illegal argument. Cannot seperate element[" + parent.nodeName + "]";
929 
930 		var previousSibling = seperator.previousSibling;
931 		var nextSibling = seperator.nextSibling;
932 		
933 		var newElement = this.insertNodeAt(this.createElement(parent.nodeName), parent, "after");
934 		
935 		var next;
936 		while(next = seperator.nextSibling) newElement.appendChild(next);
937 		
938 		this.insertNodeAt(seperator, newElement, "start");
939 		this.copyAttributes(parent, newElement);
940 		
941 		return newElement;
942 	},
943 	
944 	/**
945 	 * TODO: Add specs
946 	 */
947 	splitElementUpto: function(seperator, element, excludeElement) {
948 		while(seperator.previousSibling != element) {
949 			if(excludeElement && seperator.parentNode == element) break;
950 			seperator = this.splitParentElement(seperator);
951 		}
952 		return seperator;
953 	},
954 	
955 	/**
956 	 * Merges two adjust elements
957 	 *
958 	 * @param {Element} element base element
959 	 * @param {boolean} withNext merge base element with next sibling
960 	 * @param {boolean} skip skip merge steps
961 	 */
962 	mergeElement: function(element, withNext, skip) {
963 		this.wrapAllInlineOrTextNodesAs("P", element.parentNode, true);
964 		
965 		// find two block
966 		if(withNext) {
967 			var prev = element;
968 			var next = this.tree.findForward(
969 				element,
970 				function(node) {return this.tree.isBlock(node) && !this.tree.isListContainer(node) && node != element.parentNode}.bind(this)
971 			);
972 		} else {
973 			var next = element;
974 			var prev = this.tree.findBackward(
975 				element,
976 				function(node) {return this.tree.isBlock(node) && !this.tree.isListContainer(node) && node != element.parentNode}.bind(this)
977 			);
978 		}
979 		
980 		// normalize next block
981 		if(next && this.tree.isDescendantOf(this.getRoot(), next)) {
982 			var nextContainer = next.parentNode;
983 			if(this.tree.isBlockContainer(next)) {
984 				nextContainer = next;
985 				this.wrapAllInlineOrTextNodesAs("P", nextContainer, true);
986 				next = nextContainer.firstChild;
987 			}
988 		} else {
989 			next = null;
990 		}
991 		
992 		// normalize prev block
993 		if(prev && this.tree.isDescendantOf(this.getRoot(), prev)) {
994 			var prevContainer = prev.parentNode;
995 			if(this.tree.isBlockContainer(prev)) {
996 				prevContainer = prev;
997 				this.wrapAllInlineOrTextNodesAs("P", prevContainer, true);
998 				prev = prevContainer.lastChild;
999 			}
1000 		} else {
1001 			prev = null;
1002 		}
1003 		
1004 		try {
1005 			var containersAreTableCell =
1006 				prevContainer && (this.tree.isTableCell(prevContainer) || ['TR', 'THEAD', 'TBODY'].include(prevContainer.nodeName)) &&
1007 				nextContainer && (this.tree.isTableCell(nextContainer) || ['TR', 'THEAD', 'TBODY'].include(nextContainer.nodeName));
1008 			
1009 			if(containersAreTableCell && prevContainer != nextContainer) return null;
1010 			
1011 			// if next has margin, perform outdent
1012 			if((!skip || !prev) && next && this.outdentElement(next)) return element;
1013 
1014 			// nextContainer is first li and next of it is list container
1015 			if(nextContainer && nextContainer.nodeName == 'LI' && this.tree.isListContainer(next.nextSibling)) {
1016 				this.extractOutElementFromParent(nextContainer);
1017 				return prev;
1018 			}
1019 			
1020 			// merge two list containers
1021 			if(nextContainer && nextContainer.nodeName == 'LI' && this.tree.isListContainer(nextContainer.parentNode.previousSibling)) {
1022 				this.mergeAdjustLists(nextContainer.parentNode.previousSibling, true, "next");
1023 				return prev;
1024 			}
1025 
1026 			if(next && !containersAreTableCell && prevContainer && prevContainer.nodeName == 'LI' && nextContainer && nextContainer.nodeName == 'LI' && prevContainer.parentNode.nextSibling == nextContainer.parentNode) {
1027 				var nextContainerContainer = nextContainer.parentNode;
1028 				this.moveChildNodes(nextContainer.parentNode, prevContainer.parentNode);
1029 				this.deleteNode(nextContainerContainer);
1030 				return prev;
1031 			}
1032 			
1033 			// merge two containers
1034 			if(next && !containersAreTableCell && prevContainer && prevContainer.nextSibling == nextContainer && ((skip && prevContainer.nodeName != "LI") || (!skip && prevContainer.nodeName == "LI"))) {
1035 				this.moveChildNodes(nextContainer, prevContainer);
1036 				return prev;
1037 			}
1038 
1039 			// unwrap container
1040 			if(nextContainer && nextContainer.nodeName != "LI" && !this.getParentElementOf(nextContainer, ["TABLE"]) && !this.tree.isListContainer(nextContainer) && nextContainer != this.getRoot() && !next.previousSibling) {
1041 				return this.unwrapElement(nextContainer, true);
1042 			}
1043 			
1044 			// delete table
1045 			if(withNext && nextContainer && nextContainer.nodeName == "TABLE") {
1046 				this.deleteNode(nextContainer, true);
1047 				return prev;
1048 			} else if(!withNext && prevContainer && this.tree.isTableCell(prevContainer) && !this.tree.isTableCell(nextContainer)) {
1049 				this.deleteNode(this.getParentElementOf(prevContainer, ["TABLE"]), true);
1050 				return next;
1051 			}
1052 			
1053 			// if prev is same with next, do nothing
1054 			if(prev == next) return null;
1055 
1056 			// if there is a null block, do nothing
1057 			if(!prev || !next || !prevContainer || !nextContainer) return null;
1058 			
1059 			// if two blocks are not in the same table cell, do nothing
1060 			if(this.getParentElementOf(prev, ["TD", "TH"]) != this.getParentElementOf(next, ["TD", "TH"])) return null;
1061 			
1062 			var prevIsEmpty = false;
1063 			
1064 			// cleanup empty block before merge
1065 
1066 			// 1. cleanup prev node which ends with marker +  
1067 			if(
1068 				xq.Browser.isTrident &&
1069 				prev.childNodes.length >= 2 &&
1070 				this.isMarker(prev.lastChild.previousSibling) &&
1071 				prev.lastChild.nodeType == 3 &&
1072 				prev.lastChild.nodeValue.length == 1 &&
1073 				prev.lastChild.nodeValue.charCodeAt(0) == 160
1074 			) {
1075 				this.deleteNode(prev.lastChild);
1076 			}
1077 
1078 			// 2. cleanup prev node (if prev is empty, then replace prev's tag with next's)
1079 			this.removePlaceHoldersAndEmptyNodes(prev);
1080 			if(this.isEmptyBlock(prev)) {
1081 				// replace atomic block with normal block so that following code don't need to care about atomic block
1082 				if(this.tree.isAtomic(prev)) prev = this.replaceTag("P", prev);
1083 				
1084 				prev = this.replaceTag(next.nodeName, prev) || prev;
1085 				prev.innerHTML = "";
1086 			} else if(prev.firstChild == prev.lastChild && this.isMarker(prev.firstChild)) {
1087 				prev = this.replaceTag(next.nodeName, prev) || prev;
1088 			}
1089 			
1090 			// 3. cleanup next node
1091 			if(this.isEmptyBlock(next)) {
1092 				// replace atomic block with normal block so that following code don't need to care about atomic block
1093 				if(this.tree.isAtomic(next)) next = this.replaceTag("P", next);
1094 				
1095 				next.innerHTML = "";
1096 			}
1097 			
1098 			// perform merge
1099 			this.moveChildNodes(next, prev);
1100 			this.deleteNode(next);
1101 			return prev;
1102 		} finally {
1103 			// cleanup
1104 			if(prevContainer && this.isEmptyBlock(prevContainer)) this.deleteNode(prevContainer, true);
1105 			if(nextContainer && this.isEmptyBlock(nextContainer)) this.deleteNode(nextContainer, true);
1106 			
1107 			if(prevContainer) this.unwrapUnnecessaryParagraph(prevContainer);
1108 			if(nextContainer) this.unwrapUnnecessaryParagraph(nextContainer);
1109 		}
1110 	},
1111 	
1112 	/**
1113 	 * Merges adjust list containers which has same tag name
1114 	 *
1115 	 * @param {Element} container target list container
1116 	 * @param {boolean} force force adjust list container even if they have different list type
1117 	 * @param {String} dir Specify merge direction: PREV or NEXT. If not supplied it will be merged with both direction.
1118 	 */
1119 	mergeAdjustLists: function(container, force, dir) {
1120 		var prev = container.previousSibling;
1121 		var isPrevSame = prev && (prev.nodeName == container.nodeName && prev.className == container.className);
1122 		if((!dir || dir.toLowerCase() == 'prev') && (isPrevSame || (force && this.tree.isListContainer(prev)))) {
1123 			while(prev.lastChild) {
1124 				this.insertNodeAt(prev.lastChild, container, "start");
1125 			}
1126 			this.deleteNode(prev);
1127 		}
1128 		
1129 		var next = container.nextSibling;
1130 		var isNextSame = next && (next.nodeName == container.nodeName && next.className == container.className);
1131 		if((!dir || dir.toLowerCase() == 'next') && (isNextSame || (force && this.tree.isListContainer(next)))) {
1132 			while(next.firstChild) {
1133 				this.insertNodeAt(next.firstChild, container, "end");
1134 			}
1135 			this.deleteNode(next);
1136 		}
1137 	},
1138 	
1139 	/**
1140 	 * Moves child nodes from one element into another.
1141 	 *
1142 	 * @param {Elemet} from source element
1143 	 * @param {Elemet} to target element
1144 	 */
1145 	moveChildNodes: function(from, to) {
1146 		if(this.tree.isDescendantOf(from, to) || ["HTML", "HEAD"].include(to.nodeName))
1147 			throw "Illegal argument. Cannot move children of element[" + from.nodeName + "] to element[" + to.nodeName + "]";
1148 		
1149 		if(from == to) return;
1150 		
1151 		while(from.firstChild) to.appendChild(from.firstChild);
1152 	},
1153 	
1154 	/**
1155 	 * Copies attributes from one element into another.
1156 	 *
1157 	 * @param {Element} from source element
1158 	 * @param {Element} to target element
1159 	 * @param {boolean} copyId copy ID attribute of source element
1160 	 */
1161 	copyAttributes: function(from, to, copyId) {
1162 		// IE overrides this
1163 		
1164 		var attrs = from.attributes;
1165 		if(!attrs) return;
1166 		
1167 		for(var i = 0; i < attrs.length; i++) {
1168 			if(attrs[i].nodeName == "class" && attrs[i].nodeValue) {
1169 				to.className = attrs[i].nodeValue;
1170 			} else if((copyId || !["id"].include(attrs[i].nodeName)) && attrs[i].nodeValue) {
1171 				to.setAttribute(attrs[i].nodeName, attrs[i].nodeValue);
1172 			}
1173 		}
1174 	},
1175 
1176 	_indentElements: function(node, blocks, affect) {
1177 		for (var i=0; i < affect.length; i++) {
1178 			if (affect[i] == node || this.tree.isDescendantOf(affect[i], node))
1179 				return;
1180 		}
1181 		leaves = this.tree.getLeavesAtEdge(node);
1182 		
1183 		if (blocks.include(leaves[0])) {
1184 			var affected = this.indentElement(node, true);
1185 			if (affected) {
1186 				affect.push(affected);
1187 				return;
1188 			}
1189 		}
1190 		
1191 		if (blocks.include(node)) {
1192 			var affected = this.indentElement(node, true);
1193 			if (affected) {
1194 				affect.push(affected);
1195 				return;
1196 			}
1197 		}
1198 
1199 		var children=$A(node.childNodes);
1200 		for (var i=0; i < children.length; i++)
1201 			this._indentElements(children[i], blocks, affect);
1202 		return;
1203 	},
1204 
1205 	indentElements: function(from, to) {
1206 		var blocks = this.getBlockElementsBetween(from, to);
1207 		var top = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to);
1208 		
1209 		var affect = [];
1210 		
1211 		leaves = this.tree.getLeavesAtEdge(top.parent);
1212 		if (blocks.include(leaves[0])) {
1213 			var affected = this.indentElement(top.parent);
1214 			if (affected)
1215 				return [affected];
1216 		}
1217 		
1218 		var children = $A(top.parent.childNodes);
1219 		for (var i=0; i < children.length; i++) {
1220 			this._indentElements(children[i], blocks, affect);
1221 		}
1222 		
1223 		affect = affect.flatten()
1224 		return affect.length > 0 ? affect : blocks;
1225 	},
1226 	
1227 	outdentElementsCode: function(node) {
1228 		if (node.tagName == 'LI')
1229 			node = node.parentNode;
1230 		if (node.tagName == 'OL' && node.className == 'code')
1231 			return true;
1232 		return false;
1233 	},
1234 	
1235 	_outdentElements: function(node, blocks, affect) {
1236 		for (var i=0; i < affect.length; i++) {
1237 			if (affect[i] == node || this.tree.isDescendantOf(affect[i], node))
1238 				return;
1239 		}
1240 		leaves = this.tree.getLeavesAtEdge(node);
1241 		
1242 		if (blocks.include(leaves[0]) && !this.outdentElementsCode(leaves[0])) {
1243 			var affected = this.outdentElement(node, true);
1244 			if (affected) {
1245 				affect.push(affected);
1246 				return;
1247 			}
1248 		}
1249 		
1250 		if (blocks.include(node)) {
1251 			var children = $A(node.parentNode.childNodes);
1252 			var isCode = this.outdentElementsCode(node);
1253 			var affected = this.outdentElement(node, true, isCode);
1254 			if (affected) {
1255 				if (children.include(affected) && this.tree.isListContainer(node.parentNode) && !isCode) {
1256 					for (var i=0; i < children.length; i++) {
1257 						if (blocks.include(children[i]) && !affect.include(children[i]))
1258 							affect.push(children[i]);
1259 					}
1260 				}else
1261 					affect.push(affected);
1262 				return;
1263 			}
1264 		}
1265 
1266 		var children=$A(node.childNodes);
1267 		for (var i=0; i < children.length; i++)
1268 			this._outdentElements(children[i], blocks, affect);
1269 		return;
1270 	},
1271 
1272 	outdentElements: function(from, to) {
1273 		var start, end;
1274 		
1275 		if (from.parentNode.tagName == 'LI') start=from.parentNode;
1276 		if (to.parentNode.tagName == 'LI') end=to.parentNode;
1277 		
1278 		var blocks = this.getBlockElementsBetween(from, to);
1279 		var top = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to);
1280 		
1281 		var affect = [];
1282 		
1283 		leaves = this.tree.getLeavesAtEdge(top.parent);
1284 		if (blocks.include(leaves[0]) && !this.outdentElementsCode(top.parent)) {
1285 			var affected = this.outdentElement(top.parent);
1286 			if (affected)
1287 				return [affected];
1288 		}
1289 		
1290 		var children = $A(top.parent.childNodes);
1291 		for (var i=0; i < children.length; i++) {
1292 			this._outdentElements(children[i], blocks, affect);
1293 		}
1294 
1295 		if (from.offsetParent && to.offsetParent) {
1296 			start = from;
1297 			end = to;
1298 		}else if (blocks.first().offsetParent && blocks.last().offsetParent) {
1299 			start = blocks.first();
1300 			end = blocks.last();
1301 		}
1302 		
1303 		affect = affect.flatten()
1304 		if (!start || !start.offsetParent)
1305 			start = affect.first();
1306 		if (!end || !end.offsetParent)
1307 			end = affect.last();
1308 		
1309 		return this.getBlockElementsBetween(start, end);
1310 	},
1311 	
1312 	/**
1313 	 * Performs indent by increasing element's margin-left
1314 	 */	
1315 	indentElement: function(element, noParent, forceMargin) {
1316 		if(
1317 			!forceMargin &&
1318 			(element.nodeName == "LI" || (!this.tree.isListContainer(element) && !element.previousSibling && element.parentNode.nodeName == "LI"))
1319 		) return this.indentListItem(element, noParent);
1320 		
1321 		var root = this.getRoot();
1322 		if(!element || element == root) return null;
1323 		
1324 		if (element.parentNode != root && !element.previousSibling && !noParent) element=element.parentNode;
1325 		
1326 		var margin = element.style.marginLeft;
1327 		var cssValue = margin ? this._getCssValue(margin, "px") : {value:0, unit:"em"};
1328 		
1329 		cssValue.value += 2;
1330 		element.style.marginLeft = cssValue.value + cssValue.unit;
1331 		
1332 		return element;
1333 	},
1334 	
1335 	/**
1336 	 * Performs outdent by decreasing element's margin-left
1337 	 */	
1338 	outdentElement: function(element, noParent, forceMargin) {
1339 		if(!forceMargin && element.nodeName == "LI") return this.outdentListItem(element, noParent);
1340 		
1341 		var root = this.getRoot();
1342 		if(!element || element == root) return null;
1343 		
1344 		var margin = element.style.marginLeft;
1345 		
1346 		var cssValue = margin ? this._getCssValue(margin, "px") : {value:0, unit:"em"};
1347 		if(cssValue.value == 0) {
1348 			return element.previousSibling || forceMargin ?
1349 				null :
1350 				this.outdentElement(element.parentNode, noParent);
1351 		}
1352 		
1353 		cssValue.value -= 2;
1354 		element.style.marginLeft = cssValue.value <= 0 ? "" : cssValue.value + cssValue.unit;
1355 		if(element.style.cssText == "") element.removeAttribute("style");
1356 		
1357 		return element;
1358 	},
1359 	
1360 	/**
1361 	 * Performs indent for list item
1362 	 */
1363 	indentListItem: function(element, treatListAsNormalBlock) {
1364 		var li = this.getParentElementOf(element, ["LI"]);
1365 		var container = li.parentNode;
1366 		var prev = li.previousSibling;
1367 		if(!li.previousSibling) return this.indentElement(container);
1368 		
1369 		if(li.parentNode.nodeName == "OL" && li.parentNode.className == "code") return this.indentElement(li, treatListAsNormalBlock, true);
1370 		
1371 		if(!prev.lastChild) prev.appendChild(this.makePlaceHolder());
1372 		
1373 		var targetContainer = 
1374 			this.tree.isListContainer(prev.lastChild) ?
1375 			// if there's existing list container, select it as target container
1376 			prev.lastChild :
1377 			// if there's nothing, create new one
1378 			this.insertNodeAt(this.createElement(container.nodeName), prev, "end");
1379 		
1380 		this.wrapAllInlineOrTextNodesAs("P", prev, true);
1381 		
1382 		// perform move
1383 		targetContainer.appendChild(li);
1384 		
1385 		// flatten nested list
1386 		if(!treatListAsNormalBlock && li.lastChild && this.tree.isListContainer(li.lastChild)) {
1387 			var childrenContainer = li.lastChild;
1388 			var child;
1389 			while(child = childrenContainer.lastChild) {
1390 				this.insertNodeAt(child, li, "after");
1391 			}
1392 			this.deleteNode(childrenContainer);
1393 		}
1394 		
1395 		this.unwrapUnnecessaryParagraph(li);
1396 		
1397 		return li;
1398 	},
1399 	
1400 	/**
1401 	 * Performs outdent for list item
1402 	 *
1403 	 * @return {Element} outdented list item or null if no outdent performed
1404 	 */
1405 	outdentListItem: function(element, treatListAsNormalBlock) {
1406 		var li = this.getParentElementOf(element, ["LI"]);
1407 		var container = li.parentNode;
1408 
1409 		if(!li.previousSibling) {
1410 			var performed = this.outdentElement(container);
1411 			if(performed) return performed;
1412 		}
1413 
1414 		if(li.parentNode.nodeName == "OL" && li.parentNode.className == "code") return this.outdentElement(li, treatListAsNormalBlock, true);
1415 		
1416 		var parentLi = container.parentNode;
1417 		if(parentLi.nodeName != "LI") return null;
1418 		
1419 		if(treatListAsNormalBlock) {
1420 			while(container.lastChild != li) {
1421 				this.insertNodeAt(container.lastChild, parentLi, "after");
1422 			}
1423 		} else {
1424 			// make next siblings as children
1425 			if(li.nextSibling) {
1426 				var targetContainer =
1427 					li.lastChild && this.tree.isListContainer(li.lastChild) ?
1428 						// if there's existing list container, select it as target container
1429 						li.lastChild :
1430 						// if there's nothing, create new one
1431 						this.insertNodeAt(this.createElement(container.nodeName), li, "end");
1432 				
1433 				this.copyAttributes(container, targetContainer);
1434 				
1435 				var sibling;
1436 				while(sibling = li.nextSibling) {
1437 					targetContainer.appendChild(sibling);
1438 				}
1439 			}
1440 		}
1441 		
1442 		// move current LI into parent LI's next sibling
1443 		li = this.insertNodeAt(li, parentLi, "after");
1444 		
1445 		// remove empty container
1446 		if(container.childNodes.length == 0) this.deleteNode(container);
1447 		
1448 		if(li.firstChild && this.tree.isListContainer(li.firstChild)) {
1449 			this.insertNodeAt(this.makePlaceHolder(), li, "start");
1450 		}
1451 		
1452 		this.wrapAllInlineOrTextNodesAs("P", li);
1453 		this.unwrapUnnecessaryParagraph(parentLi);
1454 		
1455 		return li;
1456 	},
1457 	
1458 	/**
1459 	 * Performs justification
1460 	 *
1461 	 * @param {Element} block target element
1462 	 * @param {String} dir one of "LEFT", "CENTER", "RIGHT", "BOTH"
1463 	 */
1464 	justifyBlock: function(block, dir) {
1465 		// if block is only child, select its parent as mover
1466 		while(block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) {
1467 			block = block.parentNode;
1468 		}
1469 		
1470 		var styleValue = dir.toLowerCase() == "both" ? "justify" : dir;
1471 		if(styleValue == "left") {
1472 			block.style.textAlign = "";
1473 			if(block.style.cssText == "") block.removeAttribute("style");
1474 		} else {
1475 			block.style.textAlign = styleValue;
1476 		}
1477 		return block;
1478 	},
1479 	
1480 	justifyBlocks: function(blocks, dir) {
1481 		blocks.each(function(block) {
1482 			this.justifyBlock(block, dir);
1483 		}.bind(this));
1484 		
1485 		return blocks;
1486 	},
1487 	
1488 	/**
1489      * Turn given element into list. If the element is a list already, it will be reversed into normal element.
1490 	 *
1491 	 * @param {Element} element target element
1492 	 * @param {String} type one of "UL", "OL"
1493 	 * @returns {Element} affected element
1494 	 */
1495 	applyList: function(element, type) {
1496 		type = type.toUpperCase();
1497 		var containerTag = type == "UL" ? "UL" : "OL";
1498 		
1499 		if(element.nodeName == "LI" || (element.parentNode.nodeName == "LI" && !element.previousSibling)) {
1500 			var element = this.getParentElementOf(element, ["LI"]);
1501 			var container = element.parentNode;
1502 			if(container.nodeName == containerTag) {
1503 				return this.extractOutElementFromParent(element);
1504 			} else {
1505 				return this.changeListTypeTo(element, type);
1506 			}
1507 		} else {
1508 			return this.turnElementIntoListItem(element, type);
1509 		}
1510 	},
1511 	
1512 	applyLists: function(from, to, type) {
1513 		type = type.toUpperCase();
1514 		var containerTag = type == "UL" ? "UL" : "OL";
1515 		var blocks = this.getBlockElementsBetween(from, to);
1516 		
1517 		// LIs or Non-containing blocks
1518 		var whole = blocks.findAll(function(e) {
1519 			return e.nodeName == "LI" || !this.tree.isBlockContainer(e);
1520 		}.bind(this));
1521 		
1522 		// LIs
1523 		var listItems = whole.findAll(function(e) {return e.nodeName == "LI"}.bind(this));
1524 		
1525 		// Non-containing blocks which is not a descendant of any LIs selected above(listItems).
1526 		var normalBlocks = whole.findAll(function(e) {
1527 			return e.nodeName != "LI" &&
1528 				!(e.parentNode.nodeName == "LI" && !e.previousSibling && !e.nextSibling) &&
1529 				!this.tree.isDescendantOf(listItems, e)
1530 		}.bind(this));
1531 		
1532 		var diffListItems = listItems.findAll(function(e) {
1533 			return e.parentNode.nodeName != containerTag;
1534 		}.bind(this));
1535 		
1536 		// Conditions needed to determine mode
1537 		var hasNormalBlocks = normalBlocks.length > 0;
1538 		var hasDifferentListStyle = diffListItems.length > 0;
1539 		
1540 		var blockToHandle = null;
1541 		
1542 		if(hasNormalBlocks) {
1543 			blockToHandle = normalBlocks;
1544 		} else if(hasDifferentListStyle) {
1545 			blockToHandle = diffListItems;
1546 		} else {
1547 			blockToHandle = listItems;
1548 		}
1549 		
1550 		// perform operation
1551 		for(var i = 0; i < blockToHandle.length; i++) {
1552 			var block = blockToHandle[i];
1553 			
1554 			// preserve original index to restore selection
1555 			var originalIndex = blocks.indexOf(block);
1556 			blocks[originalIndex] = this.applyList(block, type);
1557 		}
1558 		
1559 		return blocks;
1560 	},
1561 
1562 	/**
1563 	 * Insert place-holder for given empty element. Empty element does not displayed and causes many editing problems.
1564 	 *
1565 	 * @param {Element} element empty element
1566 	 */
1567 	correctEmptyElement: function(element) {throw "Not implemented"},
1568 
1569 	/**
1570 	 * Corrects current block-only-container to do not take any non-block element or node.
1571 	 */
1572 	correctParagraph: function() {throw "Not implemented"},
1573 	
1574 	/**
1575 	 * Makes place-holder for empty element.
1576 	 *
1577 	 * @returns {Node} Platform specific place holder
1578 	 */
1579 	makePlaceHolder: function() {throw "Not implemented"},
1580 	
1581 	/**
1582 	 * Makes place-holder string.
1583 	 *
1584 	 * @returns {String} Platform specific place holder string
1585 	 */
1586 	makePlaceHolderString: function() {throw "Not implemented"},
1587 	
1588 	/**
1589 	 * Makes empty paragraph which contains only one place-holder
1590 	 */
1591 	makeEmptyParagraph: function() {throw "Not implemented"},
1592 
1593 	/**
1594 	 * Applies background color to selected area
1595 	 *
1596 	 * @param {Object} color valid CSS color value
1597 	 */
1598 	applyBackgroundColor: function(color) {throw "Not implemented";},
1599 
1600 	/**
1601 	 * Applies foreground color to selected area
1602 	 *
1603 	 * @param {Object} color valid CSS color value
1604 	 */
1605 	applyForegroundColor: function(color) {
1606 		this.execCommand("forecolor", color);
1607 	},
1608 	
1609 	execCommand: function(commandId, param) {throw "Not implemented";},
1610 	
1611 	applyRemoveFormat: function() {throw "Not implemented";},
1612 	applyEmphasis: function() {throw "Not implemented";},
1613 	applyStrongEmphasis: function() {throw "Not implemented";},
1614 	applyStrike: function() {throw "Not implemented";},
1615 	applyUnderline: function() {throw "Not implemented";},
1616 	applySuperscription: function() {
1617 		this.execCommand("superscript");
1618 	},
1619 	applySubscription: function() {
1620 		this.execCommand("subscript");
1621 	},
1622 	indentBlock: function(element, treatListAsNormalBlock) {
1623 		return (!element.previousSibling && element.parentNode.nodeName == "LI") ?
1624 			this.indentListItem(element, treatListAsNormalBlock) :
1625 			this.indentElement(element);
1626 	},
1627 	outdentBlock: function(element, treatListAsNormalBlock) {
1628 		while(true) {
1629 			if(!element.previousSibling && element.parentNode.nodeName == "LI") {
1630 				element = this.outdentListItem(element, treatListAsNormalBlock);
1631 				return element;
1632 			} else {
1633 				var performed = this.outdentElement(element);
1634 				if(performed) return performed;
1635 				
1636 				// first-child can outdent container
1637 				if(!element.previousSibling) {
1638 					element = element.parentNode;
1639 				} else {
1640 					break;
1641 				}
1642 			}
1643 		}
1644 		
1645 		return null;
1646 	},
1647 	wrapBlock: function(tag, start, end) {
1648 		if(!this.tree._blockTags.include(tag)) throw "Unsuppored block container: [" + tag + "]";
1649 		if(!start) start = this.getCurrentBlockElement();
1650 		if(!end) end = start;
1651 		
1652 		// Check if the selection captures valid fragement
1653 		var validFragment = false;
1654 		
1655 		if(start == end) {
1656 			// are they same block?
1657 			validFragment = true;
1658 		} else if(start.parentNode == end.parentNode && !start.previousSibling && !end.nextSibling) {
1659 			// are they covering whole parent?
1660 			validFragment = true;
1661 			start = end = start.parentNode;
1662 		} else {
1663 			// are they siblings of non-LI blocks?
1664 			validFragment =
1665 				(start.parentNode == end.parentNode) &&
1666 				(start.nodeName != "LI");
1667 		}
1668 		
1669 		if(!validFragment) return null;
1670 		
1671 		var wrapper = this.createElement(tag);
1672 		
1673 		if(start == end) {
1674 			// They are same.
1675 			if(this.tree.isBlockContainer(start) && !this.tree.isListContainer(start)) {
1676 				// It's a block container. Wrap its contents.
1677 				if(this.tree.isBlockOnlyContainer(wrapper)) {
1678 					this.correctEmptyElement(start);
1679 					this.wrapAllInlineOrTextNodesAs("P", start, true);
1680 				}
1681 				this.moveChildNodes(start, wrapper);
1682 				start.appendChild(wrapper);
1683 			} else {
1684 				// It's not a block container. Wrap itself.
1685 				wrapper = this.insertNodeAt(wrapper, start, "after");
1686 				wrapper.appendChild(start);
1687 			}
1688 			
1689 			this.correctEmptyElement(wrapper);
1690 		} else {
1691 			// They are siblings. Wrap'em all.
1692 			wrapper = this.insertNodeAt(wrapper, start, "before");
1693 			var node = start;
1694 			
1695 			while(node != end) {
1696 				next = node.nextSibling;
1697 				wrapper.appendChild(node);
1698 				node = next;
1699 			}
1700 			wrapper.appendChild(node);
1701 		}
1702 		
1703 		return wrapper;
1704 	},
1705 
1706 
1707 	
1708 	/////////////////////////////////////////////
1709 	// Focus/Caret/Selection
1710 	
1711 	/**
1712 	 * Gives focus to root element's window
1713 	 */
1714 	focus: function() {throw "Not implemented";},
1715 
1716 	/**
1717 	 * Returns selection object
1718 	 */
1719 	sel: function() {throw "Not implemented";},
1720 	
1721 	/**
1722 	 * Returns range object
1723 	 */
1724 	rng: function() {throw "Not implemented";},
1725 	
1726 	/**
1727 	 * Returns true if DOM has selection
1728 	 */
1729 	hasSelection: function() {throw "Not implemented";},
1730 
1731 	/**
1732 	 * Returns true if root element's window has selection
1733 	 */
1734 	hasFocus: function() {
1735 		var cur = this.getCurrentElement();
1736 		return (cur && cur.ownerDocument == this.getDoc());
1737 	},
1738 	
1739 	/**
1740 	 * Adjust scrollbar to make the element visible in current viewport.
1741 	 *
1742 	 * @param {Element} element Target element
1743 	 * @param {boolean} toTop Align element to top of the viewport
1744 	 * @param {boolean} moveCaret Move caret to the element
1745 	 */
1746 	scrollIntoView: function(element, toTop, moveCaret) {
1747 		element.scrollIntoView(toTop);
1748 		if(moveCaret) this.placeCaretAtStartOf(element);
1749 	},
1750 	
1751 	/**
1752 	 * Select all document
1753 	 */
1754 	selectAll: function() {
1755 		return this.execCommand('selectall');
1756 	},
1757 	
1758 	/**
1759 	 * Select specified element.
1760 	 *
1761 	 * @param {Element} element element to select
1762 	 * @param {boolean} entireElement true to select entire element, false to select inner content of element 
1763 	 */
1764 	selectElement: function(node, entireElement) {throw "Not implemented"},
1765 	
1766 	/**
1767 	 * Select all elements between two blocks(inclusive).
1768 	 *
1769 	 * @param {Element} start start of selection
1770 	 * @param {Element} end end of selection
1771 	 */
1772 	selectBlocksBetween: function(start, end) {throw "Not implemented"},
1773 	
1774 	/**
1775 	 * Delete selected area
1776 	 */
1777 	deleteSelection: function() {throw "Not implemented"},
1778 	
1779 	/**
1780 	 * Collapses current selection.
1781 	 *
1782 	 * @param {boolean} toStart true to move caret to start of selected area.
1783 	 */
1784 	collapseSelection: function(toStart) {throw "Not implemented"},
1785 	
1786 	/**
1787 	 * Returns selected area as HTML string
1788 	 */
1789 	getSelectionAsHtml: function() {throw "Not implemented"},
1790 	
1791 	/**
1792 	 * Returns selected area as text string
1793 	 */
1794 	getSelectionAsText: function() {throw "Not implemented"},
1795 	
1796 	/**
1797 	 * Places caret at start of the element
1798 	 *
1799 	 * @param {Element} element Target element
1800 	 */
1801 	placeCaretAtStartOf: function(element) {throw "Not implemented"},
1802 	
1803 	/**
1804 	 * Checks if the node is empty-text-node or not
1805 	 */
1806 	isEmptyTextNode: function(node) {
1807 		return node.nodeType == 3 && node.nodeValue.length == 0;
1808 	},
1809 	
1810 	/**
1811 	 * Checks if the caret is place in empty block element
1812 	 */
1813 	isCaretAtEmptyBlock: function() {
1814 		return this.isEmptyBlock(this.getCurrentBlockElement());
1815 	},
1816 	
1817 	/**
1818 	 * Checks if the caret is place at start of the block
1819 	 */
1820 	isCaretAtBlockStart: function() {throw "Not implemented"},
1821 
1822 	/**
1823 	 * Checks if the caret is place at end of the block
1824 	 */
1825 	isCaretAtBlockEnd: function() {throw "Not implemented"},
1826 	
1827 	/**
1828 	 * Saves current selection info
1829 	 *
1830 	 * @returns {Object} Bookmark for selection
1831 	 */
1832 	saveSelection: function() {throw "Not implemented"},
1833 	
1834 	/**
1835 	 * Restores current selection info
1836 	 *
1837 	 * @param {Object} bookmark Bookmark
1838 	 */
1839 	restoreSelection: function(bookmark) {throw "Not implemented"},
1840 	
1841 	/**
1842 	 * Create marker
1843 	 */
1844 	createMarker: function() {
1845 		var marker = this.createElement("SPAN");
1846 		marker.id = "xquared_marker_" + (this._lastMarkerId++);
1847 		marker.className = "xquared_marker";
1848 		return marker;
1849 	},
1850 
1851 	/**
1852 	 * Create and insert marker into current caret position.
1853 	 * Marker is an inline element which has no child nodes. It can be used with many purposes.
1854 	 * For example, You can push marker to mark current caret position.
1855 	 *
1856 	 * @returns {Element} marker
1857 	 */
1858 	pushMarker: function() {
1859 		var marker = this.createMarker();
1860 		return this.insertNode(marker);
1861 	},
1862 	
1863 	/**
1864 	 * Removes last marker
1865 	 *
1866 	 * @params {boolean} moveCaret move caret into marker before delete.
1867 	 */
1868 	popMarker: function(moveCaret) {
1869 		var id = "xquared_marker_" + (--this._lastMarkerId);
1870 		var marker = this.$(id);
1871 		if(!marker) return;
1872 		
1873 		if(moveCaret) {
1874 			this.selectElement(marker, true);
1875 			this.collapseSelection(false);
1876 		}
1877 		
1878 		this.deleteNode(marker);
1879 	},
1880 	
1881 	
1882 	
1883 	/////////////////////////////////////////////
1884 	// Query methods
1885 	
1886 	isMarker: function(node) {
1887 		return (node.nodeType == 1 && node.nodeName == "SPAN" && node.className == "xquared_marker");
1888 	},
1889 	
1890 	isFirstBlockOfBody: function(block) {
1891 		var root = this.getRoot();
1892 		var found = this.tree.findBackward(
1893 			block,
1894 			function(node) {return (node == root) || node.previousSibling;}.bind(this)
1895 		);
1896 		
1897 		return found == root;
1898 	},
1899 	
1900 	/**
1901 	 * Returns outer HTML of given element
1902 	 */
1903 	getOuterHTML: function(element) {throw "Not implemented"},
1904 	
1905 	/**
1906 	 * Returns inner text of given element
1907 	 * 
1908 	 * @param {Element} element Target element
1909 	 * @returns {String} Text string
1910 	 */
1911 	getInnerText: function(element) {
1912 		return element.innerHTML.stripTags();
1913 	},
1914 
1915 	/**
1916 	 * Checks if given node is place holder or not.
1917 	 * 
1918 	 * @param {Node} node DOM node
1919 	 */
1920 	isPlaceHolder: function(node) {throw "Not implemented"},
1921 	
1922 	/**
1923 	 * Checks if given block is the first LI whose next sibling is a nested list.
1924 	 *
1925 	 * @param {Element} block Target block
1926 	 */
1927 	isFirstLiWithNestedList: function(block) {
1928 		return !block.previousSibling &&
1929 			block.parentNode.nodeName == "LI" &&
1930 			this.tree.isListContainer(block.nextSibling);
1931 	},
1932 	
1933 	/**
1934 	 * Search all links within given element
1935 	 *
1936 	 * @param {Element} [element] Container element. If not given, the root element will be used.
1937 	 * @param {Array} [found] if passed, links will be appended into this array.
1938 	 * @returns {Array} Array of anchors. It returns empty array if there's no links.
1939 	 */
1940 	searchAnchors: function(element, found) {
1941 		if(!element) element = this.getRoot();
1942 		if(!found) found = [];
1943 
1944 		var anchors = element.getElementsByTagName("A");
1945 		for(var i = 0; i < anchors.length; i++) {
1946 			found.push(anchors[i]);
1947 		}
1948 
1949 		return found;
1950 	},
1951 	
1952 	/**
1953 	 * Search all headings within given element
1954 	 *
1955 	 * @param {Element} [element] Container element. If not given, the root element will be used.
1956 	 * @param {Array} [found] if passed, headings will be appended into this array.
1957 	 * @returns {Array} Array of headings. It returns empty array if there's no headings.
1958 	 */
1959 	searchHeadings: function(element, found) {
1960 		if(!element) element = this.getRoot();
1961 		if(!found) found = [];
1962 
1963 		var regexp = /^h[1-6]/ig;
1964 
1965 		if (!element.childNodes) return [];
1966 		$A(element.childNodes).each(function(child) {
1967 			var isContainer = child && this.tree._blockContainerTags.include(child.nodeName);
1968 			var isHeading = child && child.nodeName.match(regexp);
1969 
1970 			if (isContainer) {
1971 				this.searchHeadings(child, found);
1972 			} else if (isHeading) {
1973 				found.push(child);
1974 			}
1975 		}.bind(this));
1976 
1977 		return found;
1978 	},
1979 	
1980 	/**
1981 	 * Collect structure and style informations of given element.
1982 	 *
1983 	 * @param {Element} element target element
1984 	 * @returns {Object} object that contains information: {em: true, strong: false, block: "p", list: "ol", ...}
1985 	 */
1986 	collectStructureAndStyle: function(element) {
1987 		if(!element || element.nodeName == "#document") return {};
1988 
1989 		var block = this.getParentBlockElementOf(element);
1990 		var parents = this.tree.collectParentsOf(element, true, function(node) {return block.parentNode == node});
1991 		var blockName = block.nodeName;
1992 
1993 		var info = {};
1994 		
1995 		var doc = this.getDoc();
1996 		var em = doc.queryCommandState("Italic");
1997 		var strong = doc.queryCommandState("Bold");
1998 		var strike = doc.queryCommandState("Strikethrough");
1999 		var underline = doc.queryCommandState("Underline") && !this.getParentElementOf(element, ["A"]);
2000 		var superscription = doc.queryCommandState("superscript");
2001 		var subscription = doc.queryCommandState("subscript");
2002 		
2003 		// if block is only child, select its parent
2004 		while(block.parentNode && block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) {
2005 			block = block.parentNode;
2006 		}
2007 
2008 		var list = false;
2009 		if(block.nodeName == "LI") {
2010 			var parent = block.parentNode;
2011 			var isCode = parent.nodeName == "OL" && parent.className == "code";
2012 			list = isCode ? "CODE" : parent.nodeName;
2013 		}
2014 		
2015 		var justification = block.style.textAlign || "left";
2016 		
2017 		return {
2018 			block:blockName,
2019 			em: em,
2020 			strong: strong,
2021 			strike: strike,
2022 			underline: underline,
2023 			superscription: superscription,
2024 			subscription: subscription,
2025 			list: list,
2026 			justification: justification
2027 		};
2028 	},
2029 	
2030 	/**
2031 	 * Find elements by CSS selector.
2032 	 *
2033 	 * WARNING: Use this method carefully since prototype.js doesn't work well with designMode DOM.
2034 	 */
2035 	findBySelector: function(selector) {
2036 		return Element.getElementsBySelector(this.root, selector);
2037 	},
2038 	
2039 	/**
2040 	 * Find elements by attribute.
2041 	 * 
2042 	 * This method will be deprecated when findBySelector get stabilized.
2043 	 */
2044 	findByAttribute: function(name, value) {
2045 		var nodes = [];
2046 		this._findByAttribute(nodes, this.root, name, value);
2047 		return nodes;
2048 	},
2049 	
2050 	/** @private */
2051 	_findByAttribute: function(nodes, element, name, value) {
2052 		if(element.getAttribute(name) == value) nodes.push(element);
2053 		if(!element.hasChildNodes()) return;
2054 		
2055 		var children = element.childNodes;
2056 		for(var i = 0; i < children.length; i++) {
2057 			if(children[i].nodeType == 1) this._findByAttribute(nodes, children[i], name, value);
2058 		}
2059 	},
2060 	
2061 	/**
2062 	 * Checks if the element has one or more important attributes: id, class, style
2063 	 *
2064 	 * @param {Element} element Target element
2065 	 */
2066 	hasImportantAttributes: function(element) {throw "Not implemented"},
2067 	
2068 	/**
2069 	 * Checks if the element is empty or not. Place-holder is not counted as a child.
2070 	 *
2071 	 * @param {Element} element Target element
2072 	 */
2073 	isEmptyBlock: function(element) {throw "Not implemented"},
2074 	
2075 	/**
2076 	 * Returns element that contains caret.
2077 	 */
2078 	getCurrentElement: function() {throw "Not implemented"},
2079 	
2080 	/**
2081 	 * Returns block element that contains caret.
2082 	 */
2083 	getCurrentBlockElement: function() {
2084 		var cur = this.getCurrentElement();
2085 		if(!cur) return null;
2086 		
2087 		var block = this.getParentBlockElementOf(cur);
2088 		if(!block) return null;
2089 		
2090 		return (block.nodeName == "BODY") ? null : block;
2091 	},
2092 	
2093 	/**
2094 	 * Returns parent block element of parameter.
2095 	 * If the parameter itself is a block, it will be returned.
2096 	 *
2097 	 * @param {Element} element Target element
2098 	 *
2099 	 * @returns {Element} Element or null
2100 	 */
2101 	getParentBlockElementOf: function(element) {
2102 		while(element) {
2103 			if(this.tree._blockTags.include(element.nodeName)) return element;
2104 			element = element.parentNode;
2105 		}
2106 		return null;
2107 	},
2108 	
2109 	/**
2110 	 * Returns parent element of parameter which has one of given tag name.
2111 	 * If the parameter itself has the same tag name, it will be returned.
2112 	 *
2113 	 * @param {Element} element Target element
2114 	 * @param {Array} tagNames Array of string which contains tag names
2115 	 *
2116 	 * @returns {Element} Element or null
2117 	 */
2118 	getParentElementOf: function(element, tagNames) {
2119 		while(element) {
2120 			if(tagNames.include(element.nodeName)) return element;
2121 			element = element.parentNode;
2122 		}
2123 		return null;
2124 	},
2125 	
2126 	/**
2127 	 * Collects all block elements between two elements
2128 	 *
2129 	 * @param {Element} from Start element(inclusive)
2130 	 * @param {Element} to End element(inclusive)
2131 	 */
2132 	getBlockElementsBetween: function(from, to) {
2133 		return this.tree.collectNodesBetween(from, to, function(node) {
2134 			return node.nodeType == 1 && this.tree.isBlock(node);
2135 		}.bind(this));
2136 	},
2137 	
2138 	/**
2139 	 * Returns block element that contains selection start.
2140 	 *
2141 	 * This method will return exactly same result with getCurrentBlockElement method
2142 	 * when there's no selection.
2143 	 */
2144 	getBlockElementAtSelectionStart: function() {throw "Not implemented"},
2145 	
2146 	/**
2147 	 * Returns block element that contains selection end.
2148 	 *
2149 	 * This method will return exactly same result with getCurrentBlockElement method
2150 	 * when there's no selection.
2151 	 */
2152 	getBlockElementAtSelectionEnd: function() {throw "Not implemented"},
2153 	
2154 	/**
2155 	 * Returns blocks at each edge of selection(start and end).
2156 	 *
2157 	 * TODO: implement ignoreEmptyEdges for FF
2158 	 *
2159 	 * @param {boolean} naturalOrder Mak the start element always comes before the end element
2160 	 * @param {boolean} ignoreEmptyEdges Prevent some browser(Gecko) from selecting one more block than expected
2161 	 */
2162 	getBlockElementsAtSelectionEdge: function(naturalOrder, ignoreEmptyEdges) {throw "Not implemented"},
2163 	
2164 	/**
2165 	 * Returns array of selected block elements
2166 	 */
2167 	getSelectedBlockElements: function() {
2168 		var selectionEdges = this.getBlockElementsAtSelectionEdge(true, true);
2169 		var start = selectionEdges[0];
2170 		var end = selectionEdges[1];
2171 		
2172 		return this.tree.collectNodesBetween(start, end, function(node) {
2173 			return node.nodeType == 1 && this.tree.isBlock(node);
2174 		}.bind(this));
2175 	},
2176 	
2177 	/**
2178 	 * Get element by ID
2179 	 *
2180 	 * @param {String} id Element's ID
2181 	 * @returns {Element} element or null
2182 	 */
2183 	getElementById: function(id) {return this.doc.getElementById(id)},
2184 	
2185 	/**
2186 	 * Shortcut for #getElementById
2187 	 */
2188 	$: function(id) {return this.getElementById(id)},
2189 	
2190 	/**
2191 	  * Returns first "valid" child of given element. It ignores empty textnodes.
2192 	  *
2193 	  * @param {Element} element Target element
2194 	  * @returns {Node} first child node or null
2195 	  */
2196 	getFirstChild: function(element) {
2197 		if(!element) return null;
2198 		
2199 		var nodes = $A(element.childNodes);
2200 		for(var i = 0; i < nodes.length; i++) {
2201 			if(!this.isEmptyTextNode(nodes[i])) return nodes[i];
2202 		}
2203 		return null;
2204 	},
2205 	
2206 	/**
2207 	  * Returns last "valid" child of given element. It ignores empty textnodes and place-holders.
2208 	  *
2209 	  * @param {Element} element Target element
2210 	  * @returns {Node} last child node or null
2211 	  */
2212 	getLastChild: function(element) {throw "Not implemented"},
2213 
2214 	getNextSibling: function(node) {
2215 		while(node = node.nextSibling) {
2216 			if(node.nodeType != 3 || node.nodeValue.strip() != "") break;
2217 		}
2218 		return node;
2219 	},
2220 
2221 	getBottommostFirstChild: function(node) {
2222 		while(node.firstChild && node.nodeType == 1) node = node.firstChild;
2223 		return node;
2224 	},
2225 	
2226 	getBottommostLastChild: function(node) {
2227 		while(node.lastChild && node.nodeType == 1) node = node.lastChild;
2228 		return node;
2229 	},
2230 
2231 	/** @private */
2232 	_getCssValue: function(str, defaultUnit) {
2233 		if(!str || str.length == 0) return {value:0, unit:defaultUnit};
2234 		
2235 		var tokens = str.match(/(\d+)(.*)/);
2236 		return {
2237 			value:parseInt(tokens[1]),
2238 			unit:tokens[2] || defaultUnit
2239 		};
2240 	}
2241 });
2242 
2243 /**
2244  * Creates and returns instance of browser specific implementation.
2245  */
2246 xq.RichDom.createInstance = function() {
2247 	if(xq.Browser.isTrident) {
2248 		return new xq.RichDomTrident();
2249 	} else if(xq.Browser.isWebkit) {
2250 		return new xq.RichDomWebkit();
2251 	} else {
2252 		return new xq.RichDomGecko();
2253 	}
2254 }
2255