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