      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