1 /* 2 Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved. 3 For licensing, see LICENSE.html or http://ckeditor.com/license 4 */ 5 6 CKEDITOR.dom.range = function( document ) 7 { 8 this.startContainer = null; 9 this.startOffset = null; 10 this.endContainer = null; 11 this.endOffset = null; 12 this.collapsed = true; 13 14 this.document = document; 15 }; 16 17 (function() 18 { 19 // Updates the "collapsed" property for the given range object. 20 var updateCollapsed = function( range ) 21 { 22 range.collapsed = ( 23 range.startContainer && 24 range.endContainer && 25 range.startContainer.equals( range.endContainer ) && 26 range.startOffset == range.endOffset ); 27 }; 28 29 // This is a shared function used to delete, extract and clone the range 30 // contents. 31 // V2 32 var execContentsAction = function( range, action, docFrag ) 33 { 34 var startNode = range.startContainer; 35 var endNode = range.endContainer; 36 37 var startOffset = range.startOffset; 38 var endOffset = range.endOffset; 39 40 var removeStartNode; 41 var removeEndNode; 42 43 // For text containers, we must simply split the node and point to the 44 // second part. The removal will be handled by the rest of the code . 45 if ( endNode.type == CKEDITOR.NODE_TEXT ) 46 endNode = endNode.split( endOffset ); 47 else 48 { 49 // If the end container has children and the offset is pointing 50 // to a child, then we should start from it. 51 if ( endNode.getChildCount() > 0 ) 52 { 53 // If the offset points after the last node. 54 if ( endOffset >= endNode.getChildCount() ) 55 { 56 // Let's create a temporary node and mark it for removal. 57 endNode = endNode.append( range.document.createText( '' ) ); 58 removeEndNode = true; 59 } 60 else 61 endNode = endNode.getChild( endOffset ); 62 } 63 } 64 65 // For text containers, we must simply split the node. The removal will 66 // be handled by the rest of the code . 67 if ( startNode.type == CKEDITOR.NODE_TEXT ) 68 { 69 startNode.split( startOffset ); 70 71 // In cases the end node is the same as the start node, the above 72 // splitting will also split the end, so me must move the end to 73 // the second part of the split. 74 if ( startNode.equals( endNode ) ) 75 endNode = startNode.getNext(); 76 } 77 else 78 { 79 // If the start container has children and the offset is pointing 80 // to a child, then we should start from its previous sibling. 81 82 // If the offset points to the first node, we don't have a 83 // sibling, so let's use the first one, but mark it for removal. 84 if ( !startOffset ) 85 { 86 // Let's create a temporary node and mark it for removal. 87 startNode = startNode.getFirst().insertBeforeMe( range.document.createText( '' ) ); 88 removeStartNode = true; 89 } 90 else if ( startOffset >= startNode.getChildCount() ) 91 { 92 // Let's create a temporary node and mark it for removal. 93 startNode = startNode.append( range.document.createText( '' ) ); 94 removeStartNode = true; 95 } 96 else 97 startNode = startNode.getChild( startOffset ).getPrevious(); 98 } 99 100 // Get the parent nodes tree for the start and end boundaries. 101 var startParents = startNode.getParents(); 102 var endParents = endNode.getParents(); 103 104 // Compare them, to find the top most siblings. 105 var i, topStart, topEnd; 106 107 for ( i = 0 ; i < startParents.length ; i++ ) 108 { 109 topStart = startParents[ i ]; 110 topEnd = endParents[ i ]; 111 112 // The compared nodes will match until we find the top most 113 // siblings (different nodes that have the same parent). 114 // "i" will hold the index in the parents array for the top 115 // most element. 116 if ( !topStart.equals( topEnd ) ) 117 break; 118 } 119 120 var clone = docFrag, levelStartNode, levelClone, currentNode, currentSibling; 121 122 // Remove all successive sibling nodes for every node in the 123 // startParents tree. 124 for ( var j = i ; j < startParents.length ; j++ ) 125 { 126 levelStartNode = startParents[j]; 127 128 // For Extract and Clone, we must clone this level. 129 if ( clone && !levelStartNode.equals( startNode ) ) // action = 0 = Delete 130 levelClone = clone.append( levelStartNode.clone() ); 131 132 currentNode = levelStartNode.getNext(); 133 134 while( currentNode ) 135 { 136 // Stop processing when the current node matches a node in the 137 // endParents tree or if it is the endNode. 138 if ( currentNode.equals( endParents[ j ] ) || currentNode.equals( endNode ) ) 139 break; 140 141 // Cache the next sibling. 142 currentSibling = currentNode.getNext(); 143 144 // If cloning, just clone it. 145 if ( action == 2 ) // 2 = Clone 146 clone.append( currentNode.clone( true ) ); 147 else 148 { 149 // Both Delete and Extract will remove the node. 150 currentNode.remove(); 151 152 // When Extracting, move the removed node to the docFrag. 153 if ( action == 1 ) // 1 = Extract 154 clone.append( currentNode ); 155 } 156 157 currentNode = currentSibling; 158 } 159 160 if ( clone ) 161 clone = levelClone; 162 } 163 164 clone = docFrag; 165 166 // Remove all previous sibling nodes for every node in the 167 // endParents tree. 168 for ( var k = i ; k < endParents.length ; k++ ) 169 { 170 levelStartNode = endParents[ k ]; 171 172 // For Extract and Clone, we must clone this level. 173 if ( action > 0 && !levelStartNode.equals( endNode ) ) // action = 0 = Delete 174 levelClone = clone.append( levelStartNode.clone() ); 175 176 // The processing of siblings may have already been done by the parent. 177 if ( !startParents[ k ] || levelStartNode.$.parentNode != startParents[ k ].$.parentNode ) 178 { 179 currentNode = levelStartNode.getPrevious(); 180 181 while( currentNode ) 182 { 183 // Stop processing when the current node matches a node in the 184 // startParents tree or if it is the startNode. 185 if ( currentNode.equals( startParents[ k ] ) || currentNode.equals( startNode ) ) 186 break; 187 188 // Cache the next sibling. 189 currentSibling = currentNode.getPrevious(); 190 191 // If cloning, just clone it. 192 if ( action == 2 ) // 2 = Clone 193 clone.$.insertBefore( currentNode.$.cloneNode( true ), clone.$.firstChild ) ; 194 else 195 { 196 // Both Delete and Extract will remove the node. 197 currentNode.remove(); 198 199 // When Extracting, mode the removed node to the docFrag. 200 if ( action == 1 ) // 1 = Extract 201 clone.$.insertBefore( currentNode.$, clone.$.firstChild ); 202 } 203 204 currentNode = currentSibling; 205 } 206 } 207 208 if ( clone ) 209 clone = levelClone; 210 } 211 212 if ( action == 2 ) // 2 = Clone. 213 { 214 // No changes in the DOM should be done, so fix the split text (if any). 215 216 var startTextNode = range.startContainer; 217 if ( startTextNode.type == CKEDITOR.NODE_TEXT ) 218 { 219 startTextNode.$.data += startTextNode.$.nextSibling.data; 220 startTextNode.$.parentNode.removeChild( startTextNode.$.nextSibling ); 221 } 222 223 var endTextNode = range.endContainer; 224 if ( endTextNode.type == CKEDITOR.NODE_TEXT && endTextNode.$.nextSibling ) 225 { 226 endTextNode.$.data += endTextNode.$.nextSibling.data; 227 endTextNode.$.parentNode.removeChild( endTextNode.$.nextSibling ); 228 } 229 } 230 else 231 { 232 // Collapse the range. 233 234 // If a node has been partially selected, collapse the range between 235 // topStart and topEnd. Otherwise, simply collapse it to the start. (W3C specs). 236 if ( topStart && topEnd && ( startNode.$.parentNode != topStart.$.parentNode || endNode.$.parentNode != topEnd.$.parentNode ) ) 237 { 238 var endIndex = topEnd.getIndex(); 239 240 // If the start node is to be removed, we must correct the 241 // index to reflect the removal. 242 if ( removeStartNode && topEnd.$.parentNode == startNode.$.parentNode ) 243 endIndex--; 244 245 range.setStart( topEnd.getParent(), endIndex ); 246 } 247 248 // Collapse it to the start. 249 range.collapse( true ); 250 } 251 252 // Cleanup any marked node. 253 if( removeStartNode ) 254 startNode.remove(); 255 256 if( removeEndNode && endNode.$.parentNode ) 257 endNode.remove(); 258 }; 259 260 var inlineChildReqElements = { abbr:1,acronym:1,b:1,bdo:1,big:1,cite:1,code:1,del:1,dfn:1,em:1,font:1,i:1,ins:1,label:1,kbd:1,q:1,samp:1,small:1,span:1,strike:1,strong:1,sub:1,sup:1,tt:1,u:1,'var':1 }; 261 262 var getBoundaryNodes = function() 263 { 264 var startNode = this.startContainer, 265 endNode = this.endContainer, 266 startOffset = this.startOffset, 267 endOffset = this.endOffset, 268 childCount; 269 270 if ( startNode.type == CKEDITOR.NODE_ELEMENT ) 271 { 272 childCount = startNode.getChildCount(); 273 if ( childCount > startOffset ) 274 startNode = startNode.getChild( startOffset ); 275 else if ( childCount < 1 ) 276 startNode = startNode.getPreviousSourceNode(); 277 else // startOffset > childCount but childCount is not 0 278 { 279 // Try to take the node just after the current position. 280 startNode = startNode.$; 281 while ( startNode.lastChild ) 282 startNode = startNode.lastChild; 283 startNode = new CKEDITOR.dom.node( startNode ); 284 285 // Normally we should take the next node in DFS order. But it 286 // is also possible that we've already reached the end of 287 // document. 288 startNode = startNode.getNextSourceNode() || startNode; 289 } 290 } 291 if ( endNode.type == CKEDITOR.NODE_ELEMENT ) 292 { 293 childCount = endNode.getChildCount(); 294 if ( childCount > endOffset ) 295 endNode = endNode.getChild( endOffset ).getPreviousSourceNode(); 296 else if ( childCount < 1 ) 297 endNode = endNode.getPreviousSourceNode(); 298 else // endOffset > childCount but childCount is not 0 299 { 300 // Try to take the node just before the current position. 301 endNode = endNode.$; 302 while ( endNode.lastChild ) 303 endNode = endNode.lastChild; 304 endNode = new CKEDITOR.dom.node( endNode ); 305 } 306 } 307 308 return { startNode : startNode, endNode : endNode }; 309 }; 310 311 // Check every node between the block boundary and the startNode or endNode. 312 var getCheckStartEndBlockFunction = function( isStart ) 313 { 314 return function( evt ) 315 { 316 // Don't check the block boundary itself. 317 if ( this.stopped() || !evt.data.node ) 318 return; 319 320 var node = evt.data.node, 321 hadBr = false; 322 if ( node.type == CKEDITOR.NODE_ELEMENT ) 323 { 324 // If there are non-empty inline elements (e.g. <img />), then we're not 325 // at the start. 326 if ( !inlineChildReqElements[ node.getName() ] ) 327 { 328 // If we're working at the end-of-block, forgive the first <br />. 329 if ( !isStart && node.getName() == 'br' && !hadBr ) 330 hadBr = true; 331 else 332 { 333 this.checkFailed = true; 334 this.stop(); 335 } 336 } 337 } 338 else if ( node.type == CKEDITOR.NODE_TEXT ) 339 { 340 // If there's any visible text, then we're not at the start. 341 var visibleText = CKEDITOR.tools.trim( node.getText() ); 342 if ( visibleText.length > 0 ) 343 { 344 this.checkFailed = true; 345 this.stop(); 346 } 347 } 348 }; 349 }; 350 351 352 CKEDITOR.dom.range.prototype = 353 { 354 clone : function() 355 { 356 var clone = new CKEDITOR.dom.range( this.document ); 357 358 clone.startContainer = this.startContainer; 359 clone.startOffset = this.startOffset; 360 clone.endContainer = this.endContainer; 361 clone.endOffset = this.endOffset; 362 clone.collapsed = this.collapsed; 363 364 return clone; 365 }, 366 367 collapse : function( toStart ) 368 { 369 if ( toStart ) 370 { 371 this.endContainer = this.startContainer; 372 this.endOffset = this.startOffset; 373 } 374 else 375 { 376 this.startContainer = this.endContainer; 377 this.startOffset = this.endOffset; 378 } 379 380 this.collapsed = true; 381 }, 382 383 // The selection may be lost when cloning (due to the splitText() call). 384 cloneContents : function() 385 { 386 var docFrag = new CKEDITOR.dom.documentFragment( this.document ); 387 388 if ( !this.collapsed ) 389 execContentsAction( this, 2, docFrag ); 390 391 return docFrag; 392 }, 393 394 deleteContents : function() 395 { 396 if ( this.collapsed ) 397 return; 398 399 execContentsAction( this, 0 ); 400 }, 401 402 extractContents : function() 403 { 404 var docFrag = new CKEDITOR.dom.documentFragment( this.document ); 405 406 if ( !this.collapsed ) 407 execContentsAction( this, 1, docFrag ); 408 409 return docFrag; 410 }, 411 412 // This is an "intrusive" way to create a bookmark. It includes <span> tags 413 // in the range boundaries. The advantage of it is that it is possible to 414 // handle DOM mutations when moving back to the bookmark. 415 // Attention: the inclusion of nodes in the DOM is a design choice and 416 // should not be changed as there are other points in the code that may be 417 // using those nodes to perform operations. See GetBookmarkNode. 418 createBookmark : function() 419 { 420 var startNode, endNode; 421 var clone; 422 423 startNode = this.document.createElement( 'span' ); 424 startNode.setAttribute( '_fck_bookmark', 1 ); 425 startNode.setStyle( 'display', 'none' ); 426 427 // For IE, it must have something inside, otherwise it may be 428 // removed during DOM operations. 429 startNode.setHtml( ' ' ); 430 431 // If collapsed, the endNode will not be created. 432 if ( !this.collapsed ) 433 { 434 endNode = startNode.clone(); 435 endNode.setHtml( ' ' ); 436 437 clone = this.clone(); 438 clone.collapse(); 439 clone.insertNode( endNode ); 440 } 441 442 clone = this.clone(); 443 clone.collapse( true ); 444 clone.insertNode( startNode ); 445 446 // Update the range position. 447 if ( endNode ) 448 { 449 this.setStartAfter( startNode ); 450 this.setEndBefore( endNode ); 451 } 452 else 453 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END ); 454 455 return { 456 startNode : startNode, 457 endNode : endNode 458 }; 459 }, 460 461 moveToBookmark : function( bookmark ) 462 { 463 // Set the range start at the bookmark start node position. 464 this.setStartBefore( bookmark.startNode ); 465 466 // Remove it, because it may interfere in the setEndBefore call. 467 bookmark.startNode.remove(); 468 469 // Set the range end at the bookmark end node position, or simply 470 // collapse it if it is not available. 471 var endNode = bookmark.endNode; 472 if ( endNode ) 473 { 474 this.setEndBefore( endNode ); 475 endNode.remove(); 476 } 477 else 478 this.collapse( true ); 479 }, 480 481 getCommonAncestor : function( includeSelf ) 482 { 483 var start = this.startContainer; 484 var end = this.endContainer; 485 486 if ( start.equals( end ) ) 487 { 488 if ( includeSelf && start.type == CKEDITOR.NODE_ELEMENT && this.startOffset == this.endOffset - 1 ) 489 return start.getChild( this.startOffset ); 490 return start; 491 } 492 493 if ( end.type == CKEDITOR.NODE_ELEMENT && end.contains( start ) ) 494 return end; 495 496 if ( start.type != CKEDITOR.NODE_ELEMENT ) 497 start = start.getParent(); 498 499 do 500 { 501 if ( start.contains( end ) ) 502 return start; 503 } 504 while( ( start = start.getParent() ) ) 505 506 return null; 507 }, 508 509 /** 510 * Transforms the startContainer and endContainer properties from text 511 * nodes to element nodes, whenever possible. This is actually possible 512 * if either of the boundary containers point to a text node, and its 513 * offset is set to zero, or after the last char in the node. 514 */ 515 optimize : function() 516 { 517 var container = this.startContainer; 518 var offset = this.startOffset; 519 520 if ( container.type != CKEDITOR.NODE_ELEMENT ) 521 { 522 if ( !offset ) 523 this.setStartBefore( container ); 524 else if ( offset >= container.getLength() ) 525 this.setStartAfter( container ); 526 } 527 528 container = this.endContainer; 529 offset = this.endOffset; 530 531 if ( container.type != CKEDITOR.NODE_ELEMENT ) 532 { 533 if ( !offset ) 534 this.setEndBefore( container ); 535 else if ( offset >= container.getLength() ) 536 this.setEndAfter( container ); 537 } 538 }, 539 540 trim : function( ignoreStart, ignoreEnd ) 541 { 542 var startContainer = this.startContainer; 543 var startOffset = this.startOffset; 544 545 var endContainer = this.endContainer; 546 var endOffset = this.endOffset; 547 548 if ( !ignoreStart && startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) 549 { 550 // If the offset is zero, we just insert the new node before 551 // the start. 552 if ( !startOffset ) 553 { 554 startOffset = startContainer.getIndex(); 555 startContainer = startContainer.getParent(); 556 } 557 // If the offset is at the end, we'll insert it after the text 558 // node. 559 else if ( startOffset >= startContainer.getLength() ) 560 { 561 startOffset = startContainer.getIndex() + 1; 562 startContainer = startContainer.getParent(); 563 } 564 // In other case, we split the text node and insert the new 565 // node at the split point. 566 else 567 { 568 var nextText = startContainer.split( startOffset ); 569 570 startOffset = startContainer.getIndex() + 1; 571 startContainer = startContainer.getParent(); 572 573 // Check if it is necessary to update the end boundary. 574 if ( this.collapsed ) 575 this.setEnd( startContainer, startOffset ); 576 else if ( this.startContainer.equals( this.endContainer ) ) 577 this.setEnd( nextText, this.endOffset - this.startOffset ); 578 } 579 580 this.setStart( startContainer, startOffset ); 581 } 582 583 if ( !ignoreEnd && endContainer && !this.collapsed && endContainer.type == CKEDITOR.NODE_TEXT ) 584 { 585 // If the offset is zero, we just insert the new node before 586 // the start. 587 if ( !endOffset ) 588 { 589 endOffset = endContainer.getIndex(); 590 endContainer = endContainer.getParent(); 591 } 592 // If the offset is at the end, we'll insert it after the text 593 // node. 594 else if ( endOffset >= endContainer.getLength() ) 595 { 596 endOffset = endContainer.getIndex() + 1; 597 endContainer = endContainer.getParent(); 598 } 599 // In other case, we split the text node and insert the new 600 // node at the split point. 601 else 602 { 603 endContainer.split( endOffset ); 604 605 endOffset = endContainer.getIndex() + 1; 606 endContainer = endContainer.getParent(); 607 } 608 609 this.setEnd( endContainer, endOffset ); 610 } 611 }, 612 613 enlarge : function( unit ) 614 { 615 switch ( unit ) 616 { 617 case CKEDITOR.ENLARGE_ELEMENT : 618 619 if ( this.collapsed ) 620 return; 621 622 // Get the common ancestor. 623 var commonAncestor = this.getCommonAncestor(); 624 625 var body = this.document.getBody(); 626 627 // For each boundary 628 // a. Depending on its position, find out the first node to be checked (a sibling) or, if not available, to be enlarge. 629 // b. Go ahead checking siblings and enlarging the boundary as much as possible until the common ancestor is not reached. After reaching the common ancestor, just save the enlargeable node to be used later. 630 631 var startTop, endTop; 632 633 var enlargeable, sibling, commonReached; 634 635 // Indicates that the node can be added only if whitespace 636 // is available before it. 637 var needsWhiteSpace = false; 638 var isWhiteSpace; 639 var siblingText; 640 641 // Process the start boundary. 642 643 var container = this.startContainer; 644 var offset = this.startOffset; 645 646 if ( container.type == CKEDITOR.NODE_TEXT ) 647 { 648 if ( offset ) 649 { 650 // Check if there is any non-space text before the 651 // offset. Otherwise, container is null. 652 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container; 653 654 // If we found only whitespace in the node, it 655 // means that we'll need more whitespace to be able 656 // to expand. For example, <i> can be expanded in 657 // "A <i> [B]</i>", but not in "A<i> [B]</i>". 658 needsWhiteSpace = !!container; 659 } 660 661 if ( container ) 662 { 663 if ( !( sibling = container.getPrevious() ) ) 664 enlargeable = container.getParent(); 665 } 666 } 667 else 668 { 669 // If we have offset, get the node preceeding it as the 670 // first sibling to be checked. 671 if ( offset ) 672 sibling = container.getChild( offset - 1 ) || container.getLast(); 673 674 // If there is no sibling, mark the container to be 675 // enlarged. 676 if ( !sibling ) 677 enlargeable = container; 678 } 679 680 while ( enlargeable || sibling ) 681 { 682 if ( enlargeable && !sibling ) 683 { 684 // If we reached the common ancestor, mark the flag 685 // for it. 686 if ( !commonReached && enlargeable.equals( commonAncestor ) ) 687 commonReached = true; 688 689 if ( !body.contains( enlargeable ) ) 690 break; 691 692 // If we don't need space or this element breaks 693 // the line, then enlarge it. 694 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) 695 { 696 needsWhiteSpace = false; 697 698 // If the common ancestor has been reached, 699 // we'll not enlarge it immediately, but just 700 // mark it to be enlarged later if the end 701 // boundary also enlarges it. 702 if ( commonReached ) 703 startTop = enlargeable; 704 else 705 this.setStartBefore( enlargeable ); 706 } 707 708 sibling = enlargeable.getPrevious(); 709 } 710 711 // Check all sibling nodes preceeding the enlargeable 712 // node. The node wil lbe enlarged only if none of them 713 // blocks it. 714 while ( sibling ) 715 { 716 // This flag indicates that this node has 717 // whitespaces at the end. 718 isWhiteSpace = false; 719 720 if ( sibling.type == CKEDITOR.NODE_TEXT ) 721 { 722 siblingText = sibling.getText(); 723 724 if ( /[^\s\ufeff]/.test( siblingText ) ) 725 sibling = null; 726 727 isWhiteSpace = /[\s\ufeff]$/.test( siblingText ); 728 } 729 else 730 { 731 // If this is a visible element. 732 if ( sibling.$.offsetWidth > 0 ) 733 { 734 // We'll accept it only if we need 735 // whitespace, and this is an inline 736 // element with whitespace only. 737 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) 738 { 739 // It must contains spaces and inline elements only. 740 741 siblingText = sibling.getText(); 742 743 if ( !(/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF) 744 sibling = null; 745 else 746 { 747 var allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' ); 748 for ( var i = 0, child ; child = allChildren[ i++ ] ; ) 749 { 750 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) 751 { 752 sibling = null; 753 break; 754 } 755 } 756 } 757 758 if ( sibling ) 759 isWhiteSpace = !!siblingText.length; 760 } 761 else 762 sibling = null; 763 } 764 } 765 766 // A node with whitespaces has been found. 767 if ( isWhiteSpace ) 768 { 769 // Enlarge the last enlargeable node, if we 770 // were waiting for spaces. 771 if ( needsWhiteSpace ) 772 { 773 if ( commonReached ) 774 startTop = enlargeable; 775 else if ( enlargeable ) 776 this.setStartBefore( enlargeable ); 777 } 778 else 779 needsWhiteSpace = true; 780 } 781 782 if ( sibling ) 783 { 784 var next = sibling.getPrevious(); 785 786 if ( !enlargeable && !next ) 787 { 788 // Set the sibling as enlargeable, so it's 789 // parent will be get later outside this while. 790 enlargeable = sibling; 791 sibling = null; 792 break; 793 } 794 795 sibling = next; 796 } 797 else 798 { 799 // If sibling has been set to null, then we 800 // need to stop enlarging. 801 enlargeable = null; 802 } 803 } 804 805 if ( enlargeable ) 806 enlargeable = enlargeable.getParent(); 807 } 808 809 // Process the end boundary. This is basically the same 810 // code used for the start boundary, with small changes to 811 // make it work in the oposite side (to the right). This 812 // makes it difficult to reuse the code here. So, fixes to 813 // the above code are likely to be replicated here. 814 815 container = this.endContainer; 816 offset = this.endOffset; 817 818 // Reset the common variables. 819 enlargeable = sibling = null; 820 commonReached = needsWhiteSpace = false; 821 822 if ( container.type == CKEDITOR.NODE_TEXT ) 823 { 824 // Check if there is any non-space text after the 825 // offset. Otherwise, container is null. 826 container = !CKEDITOR.tools.trim( container.substring( offset ) ).length && container; 827 828 // If we found only whitespace in the node, it 829 // means that we'll need more whitespace to be able 830 // to expand. For example, <i> can be expanded in 831 // "A <i> [B]</i>", but not in "A<i> [B]</i>". 832 needsWhiteSpace = !( container && container.getLength() ); 833 834 if ( container ) 835 { 836 if ( !( sibling = container.getNext() ) ) 837 enlargeable = container.getParent(); 838 } 839 } 840 else 841 { 842 // Get the node right after the boudary to be checked 843 // first. 844 sibling = container.getChild( offset ); 845 846 if ( !sibling ) 847 enlargeable = container; 848 } 849 850 while ( enlargeable || sibling ) 851 { 852 if ( enlargeable && !sibling ) 853 { 854 if ( !commonReached && enlargeable.equals( commonAncestor ) ) 855 commonReached = true; 856 857 if ( !body.contains( enlargeable ) ) 858 break; 859 860 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) 861 { 862 needsWhiteSpace = false; 863 864 if ( commonReached ) 865 endTop = enlargeable; 866 else if ( enlargeable ) 867 this.setEndAfter( enlargeable ); 868 } 869 870 sibling = enlargeable.getNext(); 871 } 872 873 while ( sibling ) 874 { 875 isWhiteSpace = false; 876 877 if ( sibling.type == CKEDITOR.NODE_TEXT ) 878 { 879 siblingText = sibling.getText(); 880 881 if ( /[^\s\ufeff]/.test( siblingText ) ) 882 sibling = null; 883 884 isWhiteSpace = /^[\s\ufeff]/.test( siblingText ); 885 } 886 else 887 { 888 // If this is a visible element. 889 if ( sibling.$.offsetWidth > 0 ) 890 { 891 // We'll accept it only if we need 892 // whitespace, and this is an inline 893 // element with whitespace only. 894 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) 895 { 896 // It must contains spaces and inline elements only. 897 898 siblingText = sibling.getText(); 899 900 if ( !(/[^\s\ufeff]/).test( siblingText ) ) 901 sibling = null; 902 else 903 { 904 allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' ); 905 for ( i = 0 ; child = allChildren[ i++ ] ; ) 906 { 907 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) 908 { 909 sibling = null; 910 break; 911 } 912 } 913 } 914 915 if ( sibling ) 916 isWhiteSpace = !!siblingText.length; 917 } 918 else 919 sibling = null; 920 } 921 } 922 923 if ( isWhiteSpace ) 924 { 925 if ( needsWhiteSpace ) 926 { 927 if ( commonReached ) 928 endTop = enlargeable; 929 else 930 this.setEndAfter( enlargeable ); 931 } 932 } 933 934 if ( sibling ) 935 { 936 next = sibling.getNext(); 937 938 if ( !enlargeable && !next ) 939 { 940 enlargeable = sibling; 941 sibling = null; 942 break; 943 } 944 945 sibling = next; 946 } 947 else 948 { 949 // If sibling has been set to null, then we 950 // need to stop enlarging. 951 enlargeable = null; 952 } 953 } 954 955 if ( enlargeable ) 956 enlargeable = enlargeable.getParent(); 957 } 958 959 // If the common ancestor can be enlarged by both boundaries, then include it also. 960 if ( startTop && endTop ) 961 { 962 commonAncestor = startTop.contains( endTop ) ? endTop : startTop; 963 964 this.setStartBefore( commonAncestor ); 965 this.setEndAfter( commonAncestor ); 966 } 967 break; 968 969 case CKEDITOR.ENLARGE_BLOCK_CONTENTS: 970 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS: 971 // DFS backward to get the block/list item boundary at or before the start. 972 var boundaryNodes = getBoundaryNodes.apply( this ), 973 startNode = boundaryNodes.startNode, 974 endNode = boundaryNodes.endNode, 975 guardFunction = ( unit == CKEDITOR.ENLARGE_BLOCK_CONTENTS ? 976 CKEDITOR.dom.domWalker.blockBoundary() : 977 CKEDITOR.dom.domWalker.listItemBoundary() ), 978 walker = new CKEDITOR.dom.domWalker( startNode ), 979 data = walker.reverse( guardFunction ), 980 boundaryEvent = data.events.shift(); 981 982 this.setStartBefore( boundaryEvent.from ); 983 984 // DFS forward to get the block/list item boundary at or before the end. 985 walker.setNode( endNode ); 986 data = walker.forward( guardFunction ); 987 boundaryEvent = data.events.shift(); 988 989 this.setEndAfter( boundaryEvent.from ); 990 break; 991 992 default: 993 } 994 }, 995 996 /** 997 * Inserts a node at the start of the range. The range will be expanded 998 * the contain the node. 999 */ 1000 insertNode : function( node ) 1001 { 1002 this.trim( false, true ); 1003 1004 var startContainer = this.startContainer; 1005 var startOffset = this.startOffset; 1006 1007 var nextNode = startContainer.getChild( startOffset ); 1008 1009 if ( nextNode ) 1010 node.insertBefore( nextNode ); 1011 else 1012 startContainer.append( node ); 1013 1014 // Check if we need to update the end boundary. 1015 if ( node.getParent().equals( this.endContainer ) ) 1016 this.endOffset++; 1017 1018 // Expand the range to embrace the new node. 1019 this.setStartBefore( node ); 1020 }, 1021 1022 moveToPosition : function( node, position ) 1023 { 1024 this.setStartAt( node, position ); 1025 this.collapse( true ); 1026 }, 1027 1028 selectNodeContents : function( node ) 1029 { 1030 this.setStart( node, 0 ); 1031 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() ); 1032 }, 1033 1034 /** 1035 * Sets the start position of a Range. 1036 * @param {CKEDITOR.dom.node} startNode The node to start the range. 1037 * @param {Number} startOffset An integer greater than or equal to zero 1038 * representing the offset for the start of the range from the start 1039 * of startNode. 1040 */ 1041 setStart : function( startNode, startOffset ) 1042 { 1043 // W3C requires a check for the new position. If it is after the end 1044 // boundary, the range should be collapsed to the new start. It seams 1045 // we will not need this check for our use of this class so we can 1046 // ignore it for now. 1047 1048 this.startContainer = startNode; 1049 this.startOffset = startOffset; 1050 1051 if ( !this.endContainer ) 1052 { 1053 this.endContainer = startNode; 1054 this.endOffset = startOffset; 1055 } 1056 1057 updateCollapsed( this ); 1058 }, 1059 1060 /** 1061 * Sets the end position of a Range. 1062 * @param {CKEDITOR.dom.node} endNode The node to end the range. 1063 * @param {Number} endOffset An integer greater than or equal to zero 1064 * representing the offset for the end of the range from the start 1065 * of endNode. 1066 */ 1067 setEnd : function( endNode, endOffset ) 1068 { 1069 // W3C requires a check for the new position. If it is before the start 1070 // boundary, the range should be collapsed to the new end. It seams we 1071 // will not need this check for our use of this class so we can ignore 1072 // it for now. 1073 1074 this.endContainer = endNode; 1075 this.endOffset = endOffset; 1076 1077 if ( !this.startContainer ) 1078 { 1079 this.startContainer = endNode; 1080 this.startOffset = endOffset; 1081 } 1082 1083 updateCollapsed( this ); 1084 }, 1085 1086 setStartAfter : function( node ) 1087 { 1088 this.setStart( node.getParent(), node.getIndex() + 1 ); 1089 }, 1090 1091 setStartBefore : function( node ) 1092 { 1093 this.setStart( node.getParent(), node.getIndex() ); 1094 }, 1095 1096 setEndAfter : function( node ) 1097 { 1098 this.setEnd( node.getParent(), node.getIndex() + 1 ); 1099 }, 1100 1101 setEndBefore : function( node ) 1102 { 1103 this.setEnd( node.getParent(), node.getIndex() ); 1104 }, 1105 1106 setStartAt : function( node, position ) 1107 { 1108 switch( position ) 1109 { 1110 case CKEDITOR.POSITION_AFTER_START : 1111 this.setStart( node, 0 ); 1112 break; 1113 1114 case CKEDITOR.POSITION_BEFORE_END : 1115 if ( node.type == CKEDITOR.NODE_TEXT ) 1116 this.setStart( node, node.getLength() ); 1117 else 1118 this.setStart( node, node.getChildCount() ); 1119 break; 1120 1121 case CKEDITOR.POSITION_BEFORE_START : 1122 this.setStartBefore( node ); 1123 break; 1124 1125 case CKEDITOR.POSITION_AFTER_END : 1126 this.setStartAfter( node ); 1127 } 1128 1129 updateCollapsed( this ); 1130 }, 1131 1132 setEndAt : function( node, position ) 1133 { 1134 switch( position ) 1135 { 1136 case CKEDITOR.POSITION_AFTER_START : 1137 this.setEnd( node, 0 ); 1138 break; 1139 1140 case CKEDITOR.POSITION_BEFORE_END : 1141 if ( node.type == CKEDITOR.NODE_TEXT ) 1142 this.setEnd( node, node.getLength() ); 1143 else 1144 this.setEnd( node, node.getChildCount() ); 1145 break; 1146 1147 case CKEDITOR.POSITION_BEFORE_START : 1148 this.setEndBefore( node ); 1149 break; 1150 1151 case CKEDITOR.POSITION_AFTER_END : 1152 this.setEndAfter( node ); 1153 } 1154 1155 updateCollapsed( this ); 1156 }, 1157 1158 // TODO: The fixed block isn't trimmed, does not work for <pre>. 1159 // TODO: Does not add bogus <br> to empty fixed blocks. 1160 fixBlock : function( isStart, blockTag ) 1161 { 1162 var bookmark = this.createBookmark(), 1163 fixedBlock = new CKEDITOR.dom.element( blockTag, this.document ); 1164 this.collapse( isStart ); 1165 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS ); 1166 this.extractContents().appendTo( fixedBlock ); 1167 this.insertNode( fixedBlock ); 1168 this.moveToBookmark( bookmark ); 1169 return fixedBlock; 1170 }, 1171 1172 splitBlock : function( blockTag ) 1173 { 1174 var startPath = new CKEDITOR.dom.elementPath( this.startContainer ), 1175 endPath = new CKEDITOR.dom.elementPath( this.endContainer ), 1176 startBlockLimit = startPath.blockLimit, 1177 endBlockLimit = endPath.blockLimit, 1178 startBlock = startPath.block, 1179 endBlock = endPath.block, 1180 elementPath = null; 1181 1182 if ( !startBlockLimit.equals( endBlockLimit ) ) 1183 return null; 1184 1185 // Get or fix current blocks. 1186 if ( blockTag != 'br' ) 1187 { 1188 if ( !startBlock ) 1189 { 1190 startBlock = this.fixBlock( true, blockTag ); 1191 endBlock = new CKEDITOR.dom.elementPath( this.endContainer ); 1192 } 1193 1194 if ( !endBlock ) 1195 endBlock = this.fixBlock( false, blockTag ); 1196 } 1197 1198 // Get the range position. 1199 var isStartOfBlock = startBlock && this.checkStartOfBlock(), 1200 isEndOfBlock = endBlock && this.checkEndOfBlock(); 1201 1202 // Delete the current contents. 1203 // TODO: Why is 2.x doing CheckIsEmpty()? 1204 this.deleteContents(); 1205 1206 if ( startBlock && startBlock.equals( endBlock ) ) 1207 { 1208 if ( isEndOfBlock ) 1209 { 1210 elementPath = new CKEDITOR.dom.elementPath( this.startContainer ); 1211 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END ); 1212 endBlock = null; 1213 } 1214 else if ( isStartOfBlock ) 1215 { 1216 elementPath = new CKEDITOR.dom.elementPath( this.startContainer ); 1217 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START ); 1218 startBlock = null; 1219 } 1220 else 1221 { 1222 // Extract the contents of the block from the selection point to the end 1223 // of its contents. 1224 this.setEndAt( startBlock, CKEDITOR.POSITION_BEFORE_END ); 1225 var documentFragment = this.extractContents(); 1226 1227 // Duplicate the block element after it. 1228 endBlock = startBlock.clone( false ); 1229 endBlock.removeAttribute( 'id' ); 1230 1231 // Place the extracted contents into the duplicated block. 1232 documentFragment.appendTo( endBlock ); 1233 endBlock.insertAfter( startBlock ); 1234 this.moveToPosition( startBlock, CKEDITOR.POSITION_AFTER_END ); 1235 1236 // TODO: Append bogus br to startBlock for Gecko 1237 } 1238 } 1239 1240 return { 1241 previousBlock : startBlock, 1242 nextBlock : endBlock, 1243 wasStartOfBlock : isStartOfBlock, 1244 wasEndOfBlock : isEndOfBlock, 1245 elementPath : elementPath 1246 }; 1247 }, 1248 1249 checkStartOfBlock : function() 1250 { 1251 var startContainer = this.startContainer, 1252 startOffset = this.startOffset; 1253 1254 // If the starting node is a text node, and non-empty before the offset, 1255 // then we're surely not at the start of block. 1256 if ( startContainer.type == CKEDITOR.NODE_TEXT ) 1257 { 1258 var textBefore = CKEDITOR.tools.ltrim( startContainer.getText().substr( 0, startOffset ) ); 1259 if ( textBefore.length > 0 ) 1260 return false; 1261 } 1262 1263 var startNode = getBoundaryNodes.apply( this ).startNode, 1264 walker = new CKEDITOR.dom.domWalker( startNode ); 1265 1266 // DFS backwards until the block boundary, with the checker function. 1267 walker.on( 'step', getCheckStartEndBlockFunction( true ), null, null, 20 ); 1268 walker.reverse( CKEDITOR.dom.domWalker.blockBoundary() ); 1269 1270 return !walker.checkFailed; 1271 }, 1272 1273 checkEndOfBlock : function() 1274 { 1275 var endContainer = this.endContainer, 1276 endOffset = this.endOffset; 1277 1278 // If the ending node is a text node, and non-empty after the offset, 1279 // then we're surely not at the end of block. 1280 if ( endContainer.type == CKEDITOR.NODE_TEXT ) 1281 { 1282 var textAfter = CKEDITOR.tools.rtrim( endContainer.getText().substr( endOffset ) ); 1283 if ( textAfter.length > 0 ) 1284 return false; 1285 } 1286 1287 var endNode = getBoundaryNodes.apply( this ).endNode, 1288 walker = new CKEDITOR.dom.domWalker( endNode ); 1289 1290 // DFS forward until the block boundary, with the checker function. 1291 walker.on( 'step', getCheckStartEndBlockFunction( false ), null, null, 20 ); 1292 walker.forward( CKEDITOR.dom.domWalker.blockBoundary() ); 1293 1294 return !walker.checkFailed; 1295 } 1296 }; 1297 })(); 1298 1299 CKEDITOR.POSITION_AFTER_START = 1; // <element>^contents</element> "^text" 1300 CKEDITOR.POSITION_BEFORE_END = 2; // <element>contents^</element> "text^" 1301 CKEDITOR.POSITION_BEFORE_START = 3; // ^<element>contents</element> ^"text" 1302 CKEDITOR.POSITION_AFTER_END = 4; // <element>contents</element>^ "text" 1303 1304 CKEDITOR.ENLARGE_ELEMENT = 1; 1305 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2; 1306 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3; 1307