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