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 // Check every node between the block boundary and the startNode or endNode. 263 var getCheckStartEndBlockFunction = function( isStart ) 264 { 265 return function( evt ) 266 { 267 // Don't check the block boundary itself. 268 if ( this.stopped() || !evt.data.node ) 269 return; 270 271 var node = evt.data.node, 272 hadBr = false; 273 if ( node.type == CKEDITOR.NODE_ELEMENT ) 274 { 275 // If there are non-empty inline elements (e.g. <img />), then we're not 276 // at the start. 277 if ( !inlineChildReqElements[ node.getName() ] ) 278 { 279 // If we're working at the end-of-block, forgive the first <br />. 280 if ( !isStart && node.getName() == 'br' && !hadBr ) 281 hadBr = true; 282 else 283 { 284 this.checkFailed = true; 285 this.stop(); 286 } 287 } 288 } 289 else if ( node.type == CKEDITOR.NODE_TEXT ) 290 { 291 // If there's any visible text, then we're not at the start. 292 var visibleText = CKEDITOR.tools.trim( node.getText() ); 293 if ( visibleText.length > 0 ) 294 { 295 this.checkFailed = true; 296 this.stop(); 297 } 298 } 299 }; 300 }; 301 302 303 CKEDITOR.dom.range.prototype = 304 { 305 clone : function() 306 { 307 var clone = new CKEDITOR.dom.range( this.document ); 308 309 clone.startContainer = this.startContainer; 310 clone.startOffset = this.startOffset; 311 clone.endContainer = this.endContainer; 312 clone.endOffset = this.endOffset; 313 clone.collapsed = this.collapsed; 314 315 return clone; 316 }, 317 318 collapse : function( toStart ) 319 { 320 if ( toStart ) 321 { 322 this.endContainer = this.startContainer; 323 this.endOffset = this.startOffset; 324 } 325 else 326 { 327 this.startContainer = this.endContainer; 328 this.startOffset = this.endOffset; 329 } 330 331 this.collapsed = true; 332 }, 333 334 // The selection may be lost when cloning (due to the splitText() call). 335 cloneContents : function() 336 { 337 var docFrag = new CKEDITOR.dom.documentFragment( this.document ); 338 339 if ( !this.collapsed ) 340 execContentsAction( this, 2, docFrag ); 341 342 return docFrag; 343 }, 344 345 deleteContents : function() 346 { 347 if ( this.collapsed ) 348 return; 349 350 execContentsAction( this, 0 ); 351 }, 352 353 extractContents : function() 354 { 355 var docFrag = new CKEDITOR.dom.documentFragment( this.document ); 356 357 if ( !this.collapsed ) 358 execContentsAction( this, 1, docFrag ); 359 360 return docFrag; 361 }, 362 363 /** 364 * Creates a bookmark object, which can be later used to restore the 365 * range by using the moveToBookmark function. 366 * This is an "intrusive" way to create a bookmark. It includes <span> tags 367 * in the range boundaries. The advantage of it is that it is possible to 368 * handle DOM mutations when moving back to the bookmark. 369 * Attention: the inclusion of nodes in the DOM is a design choice and 370 * should not be changed as there are other points in the code that may be 371 * using those nodes to perform operations. See GetBookmarkNode. 372 * @param {Boolean} [serializable] Indicates that the bookmark nodes 373 * must contain ids, which can be used to restore the range even 374 * when these nodes suffer mutations (like a clonation or innerHTML 375 * change). 376 * @returns {Object} And object representing a bookmark. 377 */ 378 createBookmark : function( serializable ) 379 { 380 var startNode, endNode; 381 var baseId; 382 var clone; 383 384 startNode = this.document.createElement( 'span' ); 385 startNode.setAttribute( '_fck_bookmark', 1 ); 386 startNode.setStyle( 'display', 'none' ); 387 388 // For IE, it must have something inside, otherwise it may be 389 // removed during DOM operations. 390 startNode.setHtml( ' ' ); 391 392 if ( serializable ) 393 { 394 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber(); 395 startNode.setAttribute( 'id', baseId + 'S' ); 396 } 397 398 // If collapsed, the endNode will not be created. 399 if ( !this.collapsed ) 400 { 401 endNode = startNode.clone(); 402 endNode.setHtml( ' ' ); 403 404 if ( serializable ) 405 endNode.setAttribute( 'id', baseId + 'E' ); 406 407 clone = this.clone(); 408 clone.collapse(); 409 clone.insertNode( endNode ); 410 } 411 412 clone = this.clone(); 413 clone.collapse( true ); 414 clone.insertNode( startNode ); 415 416 // Update the range position. 417 if ( endNode ) 418 { 419 this.setStartAfter( startNode ); 420 this.setEndBefore( endNode ); 421 } 422 else 423 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END ); 424 425 return { 426 startNode : serializable ? baseId + 'S' : startNode, 427 endNode : serializable ? baseId + 'E' : endNode, 428 serializable : serializable 429 }; 430 }, 431 432 /** 433 * Creates a "non intrusive" and "mutation sensible" bookmark. This 434 * kind of bookmark should be used only when the DOM is supposed to 435 * remain stable after its creation. 436 * @param {Boolean} [normalized] Indicates that the bookmark must 437 * normalized. When normalized, the successive text nodes are 438 * considered a single node. To sucessful load a normalized 439 * bookmark, the DOM tree must be also normalized before calling 440 * moveToBookmark. 441 * @returns {Object} An object representing the bookmark. 442 */ 443 createBookmark2 : function( normalized ) 444 { 445 var startContainer = this.startContainer, 446 endContainer = this.endContainer; 447 448 var startOffset = this.startOffset, 449 endOffset = this.endOffset; 450 451 var child, previous; 452 453 // If there is no range then get out of here. 454 // It happens on initial load in Safari #962 and if the editor it's 455 // hidden also in Firefox 456 if ( !startContainer || !endContainer ) 457 return { start : 0, end : 0 }; 458 459 if ( normalized ) 460 { 461 // Find out if the start is pointing to a text node that will 462 // be normalized. 463 if ( startContainer.type == CKEDITOR.NODE_ELEMENT ) 464 { 465 var child = startContainer.getChild( startOffset ); 466 467 // In this case, move the start information to that text 468 // node. 469 if ( child && child.type == CKEDITOR.NODE_TEXT 470 && startOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT ) 471 { 472 startContainer = child; 473 startOffset = 0; 474 } 475 } 476 477 // Normalize the start. 478 while ( startContainer.type == CKEDITOR.NODE_TEXT 479 && ( previous = startContainer.getPrevious() ) 480 && previous.type == CKEDITOR.NODE_TEXT ) 481 { 482 startContainer = previous; 483 startOffset += previous.getLength(); 484 } 485 486 // Process the end only if not normalized. 487 if ( !this.isCollapsed ) 488 { 489 // Find out if the start is pointing to a text node that 490 // will be normalized. 491 if ( endContainer.type == CKEDITOR.NODE_ELEMENT ) 492 { 493 child = endContainer.getChild( endOffset ); 494 495 // In this case, move the start information to that 496 // text node. 497 if ( child && child.type == CKEDITOR.NODE_TEXT 498 && endOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT ) 499 { 500 endContainer = child; 501 endOffset = 0; 502 } 503 } 504 505 // Normalize the end. 506 while ( endContainer.type == CKEDITOR.NODE_TEXT 507 && ( previous = endContainer.getPrevious() ) 508 && previous.type == CKEDITOR.NODE_TEXT ) 509 { 510 endContainer = previous; 511 endOffset += previous.getLength(); 512 } 513 } 514 } 515 516 return { 517 start : startContainer.getAddress( normalized ), 518 end : this.isCollapsed ? null : endContainer.getAddress( normalized ), 519 startOffset : startOffset, 520 endOffset : endOffset, 521 normalized : normalized, 522 is2 : true // It's a createBookmark2 bookmark. 523 }; 524 }, 525 526 moveToBookmark : function( bookmark ) 527 { 528 if ( bookmark.is2 ) // Created with createBookmark2(). 529 { 530 // Get the start information. 531 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ), 532 startOffset = bookmark.startOffset; 533 534 // Get the end information. 535 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ), 536 endOffset = bookmark.endOffset; 537 538 // Set the start boundary. 539 this.setStart( startContainer, startOffset ); 540 541 // Set the end boundary. If not available, collapse it. 542 if ( endContainer ) 543 this.setEnd( endContainer, endOffset ); 544 else 545 this.collapse( true ); 546 } 547 else // Created with createBookmark(). 548 { 549 var serializable = bookmark.serializable, 550 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode, 551 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode; 552 553 // Set the range start at the bookmark start node position. 554 this.setStartBefore( startNode ); 555 556 // Remove it, because it may interfere in the setEndBefore call. 557 startNode.remove(); 558 559 // Set the range end at the bookmark end node position, or simply 560 // collapse it if it is not available. 561 if ( endNode ) 562 { 563 this.setEndBefore( endNode ); 564 endNode.remove(); 565 } 566 else 567 this.collapse( true ); 568 } 569 }, 570 571 getBoundaryNodes : function() 572 { 573 var startNode = this.startContainer, 574 endNode = this.endContainer, 575 startOffset = this.startOffset, 576 endOffset = this.endOffset, 577 childCount; 578 579 if ( startNode.type == CKEDITOR.NODE_ELEMENT ) 580 { 581 childCount = startNode.getChildCount(); 582 if ( childCount > startOffset ) 583 startNode = startNode.getChild( startOffset ); 584 else if ( childCount < 1 ) 585 startNode = startNode.getPreviousSourceNode(); 586 else // startOffset > childCount but childCount is not 0 587 { 588 // Try to take the node just after the current position. 589 startNode = startNode.$; 590 while ( startNode.lastChild ) 591 startNode = startNode.lastChild; 592 startNode = new CKEDITOR.dom.node( startNode ); 593 594 // Normally we should take the next node in DFS order. But it 595 // is also possible that we've already reached the end of 596 // document. 597 startNode = startNode.getNextSourceNode() || startNode; 598 } 599 } 600 if ( endNode.type == CKEDITOR.NODE_ELEMENT ) 601 { 602 childCount = endNode.getChildCount(); 603 if ( childCount > endOffset ) 604 endNode = endNode.getChild( endOffset ).getPreviousSourceNode(); 605 else if ( childCount < 1 ) 606 endNode = endNode.getPreviousSourceNode(); 607 else // endOffset > childCount but childCount is not 0 608 { 609 // Try to take the node just before the current position. 610 endNode = endNode.$; 611 while ( endNode.lastChild ) 612 endNode = endNode.lastChild; 613 endNode = new CKEDITOR.dom.node( endNode ); 614 } 615 } 616 617 return { startNode : startNode, endNode : endNode }; 618 }, 619 620 getCommonAncestor : function( includeSelf ) 621 { 622 var start = this.startContainer; 623 var end = this.endContainer; 624 625 if ( start.equals( end ) ) 626 { 627 if ( includeSelf && start.type == CKEDITOR.NODE_ELEMENT && this.startOffset == this.endOffset - 1 ) 628 return start.getChild( this.startOffset ); 629 return start; 630 } 631 632 return start.getCommonAncestor( end ); 633 }, 634 635 /** 636 * Transforms the startContainer and endContainer properties from text 637 * nodes to element nodes, whenever possible. This is actually possible 638 * if either of the boundary containers point to a text node, and its 639 * offset is set to zero, or after the last char in the node. 640 */ 641 optimize : function() 642 { 643 var container = this.startContainer; 644 var offset = this.startOffset; 645 646 if ( container.type != CKEDITOR.NODE_ELEMENT ) 647 { 648 if ( !offset ) 649 this.setStartBefore( container ); 650 else if ( offset >= container.getLength() ) 651 this.setStartAfter( container ); 652 } 653 654 container = this.endContainer; 655 offset = this.endOffset; 656 657 if ( container.type != CKEDITOR.NODE_ELEMENT ) 658 { 659 if ( !offset ) 660 this.setEndBefore( container ); 661 else if ( offset >= container.getLength() ) 662 this.setEndAfter( container ); 663 } 664 }, 665 666 trim : function( ignoreStart, ignoreEnd ) 667 { 668 var startContainer = this.startContainer; 669 var startOffset = this.startOffset; 670 671 var endContainer = this.endContainer; 672 var endOffset = this.endOffset; 673 674 if ( !ignoreStart && startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) 675 { 676 // If the offset is zero, we just insert the new node before 677 // the start. 678 if ( !startOffset ) 679 { 680 startOffset = startContainer.getIndex(); 681 startContainer = startContainer.getParent(); 682 } 683 // If the offset is at the end, we'll insert it after the text 684 // node. 685 else if ( startOffset >= startContainer.getLength() ) 686 { 687 startOffset = startContainer.getIndex() + 1; 688 startContainer = startContainer.getParent(); 689 } 690 // In other case, we split the text node and insert the new 691 // node at the split point. 692 else 693 { 694 var nextText = startContainer.split( startOffset ); 695 696 startOffset = startContainer.getIndex() + 1; 697 startContainer = startContainer.getParent(); 698 699 // Check if it is necessary to update the end boundary. 700 if ( this.collapsed ) 701 this.setEnd( startContainer, startOffset ); 702 else if ( this.startContainer.equals( this.endContainer ) ) 703 this.setEnd( nextText, this.endOffset - this.startOffset ); 704 } 705 706 this.setStart( startContainer, startOffset ); 707 } 708 709 if ( !ignoreEnd && endContainer && !this.collapsed && endContainer.type == CKEDITOR.NODE_TEXT ) 710 { 711 // If the offset is zero, we just insert the new node before 712 // the start. 713 if ( !endOffset ) 714 { 715 endOffset = endContainer.getIndex(); 716 endContainer = endContainer.getParent(); 717 } 718 // If the offset is at the end, we'll insert it after the text 719 // node. 720 else if ( endOffset >= endContainer.getLength() ) 721 { 722 endOffset = endContainer.getIndex() + 1; 723 endContainer = endContainer.getParent(); 724 } 725 // In other case, we split the text node and insert the new 726 // node at the split point. 727 else 728 { 729 endContainer.split( endOffset ); 730 731 endOffset = endContainer.getIndex() + 1; 732 endContainer = endContainer.getParent(); 733 } 734 735 this.setEnd( endContainer, endOffset ); 736 } 737 }, 738 739 enlarge : function( unit ) 740 { 741 switch ( unit ) 742 { 743 case CKEDITOR.ENLARGE_ELEMENT : 744 745 if ( this.collapsed ) 746 return; 747 748 // Get the common ancestor. 749 var commonAncestor = this.getCommonAncestor(); 750 751 var body = this.document.getBody(); 752 753 // For each boundary 754 // a. Depending on its position, find out the first node to be checked (a sibling) or, if not available, to be enlarge. 755 // 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. 756 757 var startTop, endTop; 758 759 var enlargeable, sibling, commonReached; 760 761 // Indicates that the node can be added only if whitespace 762 // is available before it. 763 var needsWhiteSpace = false; 764 var isWhiteSpace; 765 var siblingText; 766 767 // Process the start boundary. 768 769 var container = this.startContainer; 770 var offset = this.startOffset; 771 772 if ( container.type == CKEDITOR.NODE_TEXT ) 773 { 774 if ( offset ) 775 { 776 // Check if there is any non-space text before the 777 // offset. Otherwise, container is null. 778 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container; 779 780 // If we found only whitespace in the node, it 781 // means that we'll need more whitespace to be able 782 // to expand. For example, <i> can be expanded in 783 // "A <i> [B]</i>", but not in "A<i> [B]</i>". 784 needsWhiteSpace = !!container; 785 } 786 787 if ( container ) 788 { 789 if ( !( sibling = container.getPrevious() ) ) 790 enlargeable = container.getParent(); 791 } 792 } 793 else 794 { 795 // If we have offset, get the node preceeding it as the 796 // first sibling to be checked. 797 if ( offset ) 798 sibling = container.getChild( offset - 1 ) || container.getLast(); 799 800 // If there is no sibling, mark the container to be 801 // enlarged. 802 if ( !sibling ) 803 enlargeable = container; 804 } 805 806 while ( enlargeable || sibling ) 807 { 808 if ( enlargeable && !sibling ) 809 { 810 // If we reached the common ancestor, mark the flag 811 // for it. 812 if ( !commonReached && enlargeable.equals( commonAncestor ) ) 813 commonReached = true; 814 815 if ( !body.contains( enlargeable ) ) 816 break; 817 818 // If we don't need space or this element breaks 819 // the line, then enlarge it. 820 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) 821 { 822 needsWhiteSpace = false; 823 824 // If the common ancestor has been reached, 825 // we'll not enlarge it immediately, but just 826 // mark it to be enlarged later if the end 827 // boundary also enlarges it. 828 if ( commonReached ) 829 startTop = enlargeable; 830 else 831 this.setStartBefore( enlargeable ); 832 } 833 834 sibling = enlargeable.getPrevious(); 835 } 836 837 // Check all sibling nodes preceeding the enlargeable 838 // node. The node wil lbe enlarged only if none of them 839 // blocks it. 840 while ( sibling ) 841 { 842 // This flag indicates that this node has 843 // whitespaces at the end. 844 isWhiteSpace = false; 845 846 if ( sibling.type == CKEDITOR.NODE_TEXT ) 847 { 848 siblingText = sibling.getText(); 849 850 if ( /[^\s\ufeff]/.test( siblingText ) ) 851 sibling = null; 852 853 isWhiteSpace = /[\s\ufeff]$/.test( siblingText ); 854 } 855 else 856 { 857 // If this is a visible element. 858 if ( sibling.$.offsetWidth > 0 ) 859 { 860 // We'll accept it only if we need 861 // whitespace, and this is an inline 862 // element with whitespace only. 863 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) 864 { 865 // It must contains spaces and inline elements only. 866 867 siblingText = sibling.getText(); 868 869 if ( !(/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF) 870 sibling = null; 871 else 872 { 873 var allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' ); 874 for ( var i = 0, child ; child = allChildren[ i++ ] ; ) 875 { 876 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) 877 { 878 sibling = null; 879 break; 880 } 881 } 882 } 883 884 if ( sibling ) 885 isWhiteSpace = !!siblingText.length; 886 } 887 else 888 sibling = null; 889 } 890 } 891 892 // A node with whitespaces has been found. 893 if ( isWhiteSpace ) 894 { 895 // Enlarge the last enlargeable node, if we 896 // were waiting for spaces. 897 if ( needsWhiteSpace ) 898 { 899 if ( commonReached ) 900 startTop = enlargeable; 901 else if ( enlargeable ) 902 this.setStartBefore( enlargeable ); 903 } 904 else 905 needsWhiteSpace = true; 906 } 907 908 if ( sibling ) 909 { 910 var next = sibling.getPrevious(); 911 912 if ( !enlargeable && !next ) 913 { 914 // Set the sibling as enlargeable, so it's 915 // parent will be get later outside this while. 916 enlargeable = sibling; 917 sibling = null; 918 break; 919 } 920 921 sibling = next; 922 } 923 else 924 { 925 // If sibling has been set to null, then we 926 // need to stop enlarging. 927 enlargeable = null; 928 } 929 } 930 931 if ( enlargeable ) 932 enlargeable = enlargeable.getParent(); 933 } 934 935 // Process the end boundary. This is basically the same 936 // code used for the start boundary, with small changes to 937 // make it work in the oposite side (to the right). This 938 // makes it difficult to reuse the code here. So, fixes to 939 // the above code are likely to be replicated here. 940 941 container = this.endContainer; 942 offset = this.endOffset; 943 944 // Reset the common variables. 945 enlargeable = sibling = null; 946 commonReached = needsWhiteSpace = false; 947 948 if ( container.type == CKEDITOR.NODE_TEXT ) 949 { 950 // Check if there is any non-space text after the 951 // offset. Otherwise, container is null. 952 container = !CKEDITOR.tools.trim( container.substring( offset ) ).length && container; 953 954 // If we found only whitespace in the node, it 955 // means that we'll need more whitespace to be able 956 // to expand. For example, <i> can be expanded in 957 // "A <i> [B]</i>", but not in "A<i> [B]</i>". 958 needsWhiteSpace = !( container && container.getLength() ); 959 960 if ( container ) 961 { 962 if ( !( sibling = container.getNext() ) ) 963 enlargeable = container.getParent(); 964 } 965 } 966 else 967 { 968 // Get the node right after the boudary to be checked 969 // first. 970 sibling = container.getChild( offset ); 971 972 if ( !sibling ) 973 enlargeable = container; 974 } 975 976 while ( enlargeable || sibling ) 977 { 978 if ( enlargeable && !sibling ) 979 { 980 if ( !commonReached && enlargeable.equals( commonAncestor ) ) 981 commonReached = true; 982 983 if ( !body.contains( enlargeable ) ) 984 break; 985 986 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) 987 { 988 needsWhiteSpace = false; 989 990 if ( commonReached ) 991 endTop = enlargeable; 992 else if ( enlargeable ) 993 this.setEndAfter( enlargeable ); 994 } 995 996 sibling = enlargeable.getNext(); 997 } 998 999 while ( sibling ) 1000 { 1001 isWhiteSpace = false; 1002 1003 if ( sibling.type == CKEDITOR.NODE_TEXT ) 1004 { 1005 siblingText = sibling.getText(); 1006 1007 if ( /[^\s\ufeff]/.test( siblingText ) ) 1008 sibling = null; 1009 1010 isWhiteSpace = /^[\s\ufeff]/.test( siblingText ); 1011 } 1012 else 1013 { 1014 // If this is a visible element. 1015 if ( sibling.$.offsetWidth > 0 ) 1016 { 1017 // We'll accept it only if we need 1018 // whitespace, and this is an inline 1019 // element with whitespace only. 1020 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) 1021 { 1022 // It must contains spaces and inline elements only. 1023 1024 siblingText = sibling.getText(); 1025 1026 if ( !(/[^\s\ufeff]/).test( siblingText ) ) 1027 sibling = null; 1028 else 1029 { 1030 allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' ); 1031 for ( i = 0 ; child = allChildren[ i++ ] ; ) 1032 { 1033 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) 1034 { 1035 sibling = null; 1036 break; 1037 } 1038 } 1039 } 1040 1041 if ( sibling ) 1042 isWhiteSpace = !!siblingText.length; 1043 } 1044 else 1045 sibling = null; 1046 } 1047 } 1048 1049 if ( isWhiteSpace ) 1050 { 1051 if ( needsWhiteSpace ) 1052 { 1053 if ( commonReached ) 1054 endTop = enlargeable; 1055 else 1056 this.setEndAfter( enlargeable ); 1057 } 1058 } 1059 1060 if ( sibling ) 1061 { 1062 next = sibling.getNext(); 1063 1064 if ( !enlargeable && !next ) 1065 { 1066 enlargeable = sibling; 1067 sibling = null; 1068 break; 1069 } 1070 1071 sibling = next; 1072 } 1073 else 1074 { 1075 // If sibling has been set to null, then we 1076 // need to stop enlarging. 1077 enlargeable = null; 1078 } 1079 } 1080 1081 if ( enlargeable ) 1082 enlargeable = enlargeable.getParent(); 1083 } 1084 1085 // If the common ancestor can be enlarged by both boundaries, then include it also. 1086 if ( startTop && endTop ) 1087 { 1088 commonAncestor = startTop.contains( endTop ) ? endTop : startTop; 1089 1090 this.setStartBefore( commonAncestor ); 1091 this.setEndAfter( commonAncestor ); 1092 } 1093 break; 1094 1095 case CKEDITOR.ENLARGE_BLOCK_CONTENTS: 1096 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS: 1097 // DFS backward to get the block/list item boundary at or before the start. 1098 1099 // Get the boundaries nodes. 1100 var startNode = this.getTouchedStartNode(), 1101 endNode = this.getTouchedEndNode(); 1102 1103 if ( startNode.type == CKEDITOR.NODE_ELEMENT && startNode.isBlockBoundary() ) 1104 { 1105 this.setStartAt( startNode, 1106 CKEDITOR.dtd.$empty[ startNode.getName() ] ? 1107 CKEDITOR.POSITION_AFTER_END : 1108 CKEDITOR.POSITION_AFTER_START ); 1109 } 1110 else 1111 { 1112 // Get the function used to check the enlaarging limits. 1113 var guardFunction = ( unit == CKEDITOR.ENLARGE_BLOCK_CONTENTS ? 1114 CKEDITOR.dom.domWalker.blockBoundary() : 1115 CKEDITOR.dom.domWalker.listItemBoundary() ); 1116 1117 // Create the DOM walker, which will traverse the DOM. 1118 var walker = new CKEDITOR.dom.domWalker( startNode ); 1119 1120 // Go walk in reverse sense. 1121 var data = walker.reverse( guardFunction ); 1122 1123 var boundaryEvent = data.events.shift(); 1124 1125 this.setStartBefore( boundaryEvent.from ); 1126 } 1127 1128 if ( endNode.type == CKEDITOR.NODE_ELEMENT && endNode.isBlockBoundary() ) 1129 { 1130 this.setEndAt( endNode, 1131 CKEDITOR.dtd.$empty[ endNode.getName() ] ? 1132 CKEDITOR.POSITION_BEFORE_START : 1133 CKEDITOR.POSITION_BEFORE_END ); 1134 } 1135 else 1136 { 1137 // DFS forward to get the block/list item boundary at or before the end. 1138 walker.setNode( endNode ); 1139 data = walker.forward( guardFunction ); 1140 boundaryEvent = data.events.shift(); 1141 1142 this.setEndAfter( boundaryEvent.from ); 1143 } 1144 } 1145 }, 1146 1147 /** 1148 * Inserts a node at the start of the range. The range will be expanded 1149 * the contain the node. 1150 */ 1151 insertNode : function( node ) 1152 { 1153 this.trim( false, true ); 1154 1155 var startContainer = this.startContainer; 1156 var startOffset = this.startOffset; 1157 1158 var nextNode = startContainer.getChild( startOffset ); 1159 1160 if ( nextNode ) 1161 node.insertBefore( nextNode ); 1162 else 1163 startContainer.append( node ); 1164 1165 // Check if we need to update the end boundary. 1166 if ( node.getParent().equals( this.endContainer ) ) 1167 this.endOffset++; 1168 1169 // Expand the range to embrace the new node. 1170 this.setStartBefore( node ); 1171 }, 1172 1173 moveToPosition : function( node, position ) 1174 { 1175 this.setStartAt( node, position ); 1176 this.collapse( true ); 1177 }, 1178 1179 selectNodeContents : function( node ) 1180 { 1181 this.setStart( node, 0 ); 1182 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() ); 1183 }, 1184 1185 /** 1186 * Sets the start position of a Range. 1187 * @param {CKEDITOR.dom.node} startNode The node to start the range. 1188 * @param {Number} startOffset An integer greater than or equal to zero 1189 * representing the offset for the start of the range from the start 1190 * of startNode. 1191 */ 1192 setStart : function( startNode, startOffset ) 1193 { 1194 // W3C requires a check for the new position. If it is after the end 1195 // boundary, the range should be collapsed to the new start. It seams 1196 // we will not need this check for our use of this class so we can 1197 // ignore it for now. 1198 1199 this.startContainer = startNode; 1200 this.startOffset = startOffset; 1201 1202 if ( !this.endContainer ) 1203 { 1204 this.endContainer = startNode; 1205 this.endOffset = startOffset; 1206 } 1207 1208 updateCollapsed( this ); 1209 }, 1210 1211 /** 1212 * Sets the end position of a Range. 1213 * @param {CKEDITOR.dom.node} endNode The node to end the range. 1214 * @param {Number} endOffset An integer greater than or equal to zero 1215 * representing the offset for the end of the range from the start 1216 * of endNode. 1217 */ 1218 setEnd : function( endNode, endOffset ) 1219 { 1220 // W3C requires a check for the new position. If it is before the start 1221 // boundary, the range should be collapsed to the new end. It seams we 1222 // will not need this check for our use of this class so we can ignore 1223 // it for now. 1224 1225 this.endContainer = endNode; 1226 this.endOffset = endOffset; 1227 1228 if ( !this.startContainer ) 1229 { 1230 this.startContainer = endNode; 1231 this.startOffset = endOffset; 1232 } 1233 1234 updateCollapsed( this ); 1235 }, 1236 1237 setStartAfter : function( node ) 1238 { 1239 this.setStart( node.getParent(), node.getIndex() + 1 ); 1240 }, 1241 1242 setStartBefore : function( node ) 1243 { 1244 this.setStart( node.getParent(), node.getIndex() ); 1245 }, 1246 1247 setEndAfter : function( node ) 1248 { 1249 this.setEnd( node.getParent(), node.getIndex() + 1 ); 1250 }, 1251 1252 setEndBefore : function( node ) 1253 { 1254 this.setEnd( node.getParent(), node.getIndex() ); 1255 }, 1256 1257 setStartAt : function( node, position ) 1258 { 1259 switch( position ) 1260 { 1261 case CKEDITOR.POSITION_AFTER_START : 1262 this.setStart( node, 0 ); 1263 break; 1264 1265 case CKEDITOR.POSITION_BEFORE_END : 1266 if ( node.type == CKEDITOR.NODE_TEXT ) 1267 this.setStart( node, node.getLength() ); 1268 else 1269 this.setStart( node, node.getChildCount() ); 1270 break; 1271 1272 case CKEDITOR.POSITION_BEFORE_START : 1273 this.setStartBefore( node ); 1274 break; 1275 1276 case CKEDITOR.POSITION_AFTER_END : 1277 this.setStartAfter( node ); 1278 } 1279 1280 updateCollapsed( this ); 1281 }, 1282 1283 setEndAt : function( node, position ) 1284 { 1285 switch( position ) 1286 { 1287 case CKEDITOR.POSITION_AFTER_START : 1288 this.setEnd( node, 0 ); 1289 break; 1290 1291 case CKEDITOR.POSITION_BEFORE_END : 1292 if ( node.type == CKEDITOR.NODE_TEXT ) 1293 this.setEnd( node, node.getLength() ); 1294 else 1295 this.setEnd( node, node.getChildCount() ); 1296 break; 1297 1298 case CKEDITOR.POSITION_BEFORE_START : 1299 this.setEndBefore( node ); 1300 break; 1301 1302 case CKEDITOR.POSITION_AFTER_END : 1303 this.setEndAfter( node ); 1304 } 1305 1306 updateCollapsed( this ); 1307 }, 1308 1309 fixBlock : function( isStart, blockTag ) 1310 { 1311 var bookmark = this.createBookmark(), 1312 fixedBlock = this.document.createElement( blockTag ); 1313 1314 this.collapse( isStart ); 1315 1316 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS ); 1317 1318 this.extractContents().appendTo( fixedBlock ); 1319 fixedBlock.trim(); 1320 1321 if ( !CKEDITOR.env.ie ) 1322 fixedBlock.appendBogus(); 1323 1324 this.insertNode( fixedBlock ); 1325 1326 this.moveToBookmark( bookmark ); 1327 1328 return fixedBlock; 1329 }, 1330 1331 splitBlock : function( blockTag ) 1332 { 1333 var startPath = new CKEDITOR.dom.elementPath( this.startContainer ), 1334 endPath = new CKEDITOR.dom.elementPath( this.endContainer ); 1335 1336 var startBlockLimit = startPath.blockLimit, 1337 endBlockLimit = endPath.blockLimit; 1338 1339 var startBlock = startPath.block, 1340 endBlock = endPath.block; 1341 1342 var elementPath = null; 1343 1344 // Do nothing if the boundaries are in different block limits. 1345 if ( !startBlockLimit.equals( endBlockLimit ) ) 1346 return null; 1347 1348 // Get or fix current blocks. 1349 if ( blockTag != 'br' ) 1350 { 1351 if ( !startBlock ) 1352 { 1353 startBlock = this.fixBlock( true, blockTag ); 1354 endBlock = new CKEDITOR.dom.elementPath( this.endContainer ).block; 1355 } 1356 1357 if ( !endBlock ) 1358 endBlock = this.fixBlock( false, blockTag ); 1359 } 1360 1361 // Get the range position. 1362 var isStartOfBlock = startBlock && this.checkStartOfBlock(), 1363 isEndOfBlock = endBlock && this.checkEndOfBlock(); 1364 1365 // Delete the current contents. 1366 // TODO: Why is 2.x doing CheckIsEmpty()? 1367 this.deleteContents(); 1368 1369 if ( startBlock && startBlock.equals( endBlock ) ) 1370 { 1371 if ( isEndOfBlock ) 1372 { 1373 elementPath = new CKEDITOR.dom.elementPath( this.startContainer ); 1374 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END ); 1375 endBlock = null; 1376 } 1377 else if ( isStartOfBlock ) 1378 { 1379 elementPath = new CKEDITOR.dom.elementPath( this.startContainer ); 1380 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START ); 1381 startBlock = null; 1382 } 1383 else 1384 { 1385 // Extract the contents of the block from the selection point to the end 1386 // of its contents. 1387 this.setEndAt( startBlock, CKEDITOR.POSITION_BEFORE_END ); 1388 var documentFragment = this.extractContents(); 1389 1390 // Duplicate the block element after it. 1391 endBlock = startBlock.clone( false ); 1392 1393 // Place the extracted contents into the duplicated block. 1394 documentFragment.appendTo( endBlock ); 1395 endBlock.insertAfter( startBlock ); 1396 this.moveToPosition( startBlock, CKEDITOR.POSITION_AFTER_END ); 1397 1398 // In Gecko, the last child node must be a bogus <br>. 1399 // Note: bogus <br> added under <ul> or <ol> would cause 1400 // lists to be incorrectly rendered. 1401 if ( !CKEDITOR.env.ie && !startBlock.is( 'ul', 'ol') ) 1402 startBlock.appendBogus() ; 1403 } 1404 } 1405 1406 return { 1407 previousBlock : startBlock, 1408 nextBlock : endBlock, 1409 wasStartOfBlock : isStartOfBlock, 1410 wasEndOfBlock : isEndOfBlock, 1411 elementPath : elementPath 1412 }; 1413 }, 1414 1415 checkStartOfBlock : function() 1416 { 1417 var startContainer = this.startContainer, 1418 startOffset = this.startOffset; 1419 1420 // If the starting node is a text node, and non-empty before the offset, 1421 // then we're surely not at the start of block. 1422 if ( startContainer.type == CKEDITOR.NODE_TEXT ) 1423 { 1424 var textBefore = CKEDITOR.tools.ltrim( startContainer.getText().substr( 0, startOffset ) ); 1425 if ( textBefore.length > 0 ) 1426 return false; 1427 } 1428 1429 var startNode = this.getBoundaryNodes().startNode, 1430 walker = new CKEDITOR.dom.domWalker( startNode ); 1431 1432 // DFS backwards until the block boundary, with the checker function. 1433 walker.on( 'step', getCheckStartEndBlockFunction( true ), null, null, 20 ); 1434 walker.reverse( CKEDITOR.dom.domWalker.blockBoundary() ); 1435 1436 return !walker.checkFailed; 1437 }, 1438 1439 checkEndOfBlock : function() 1440 { 1441 var endContainer = this.endContainer, 1442 endOffset = this.endOffset; 1443 1444 // If the ending node is a text node, and non-empty after the offset, 1445 // then we're surely not at the end of block. 1446 if ( endContainer.type == CKEDITOR.NODE_TEXT ) 1447 { 1448 var textAfter = CKEDITOR.tools.rtrim( endContainer.getText().substr( endOffset ) ); 1449 if ( textAfter.length > 0 ) 1450 return false; 1451 } 1452 1453 var endNode = this.getBoundaryNodes().endNode, 1454 walker = new CKEDITOR.dom.domWalker( endNode ); 1455 1456 // DFS forward until the block boundary, with the checker function. 1457 walker.on( 'step', getCheckStartEndBlockFunction( false ), null, null, 20 ); 1458 walker.forward( CKEDITOR.dom.domWalker.blockBoundary() ); 1459 1460 return !walker.checkFailed; 1461 }, 1462 1463 /** 1464 * Moves the range boundaries to the first editing point inside an 1465 * element. For example, in an element tree like 1466 * "<p><b><i></i></b> Text</p>", the start editing point is 1467 * "<p><b><i>^</i></b> Text</p>" (inside <i>). 1468 * @param {CKEDITOR.dom.element} targetElement The element into which 1469 * look for the editing spot. 1470 */ 1471 moveToElementEditStart : function( targetElement ) 1472 { 1473 var editableElement; 1474 1475 while ( targetElement && targetElement.type == CKEDITOR.NODE_ELEMENT ) 1476 { 1477 if ( targetElement.isEditable() ) 1478 editableElement = targetElement; 1479 else if ( editableElement ) 1480 break ; // If we already found an editable element, stop the loop. 1481 1482 targetElement = targetElement.getFirst(); 1483 } 1484 1485 if ( editableElement ) 1486 this.moveToPosition( editableElement, CKEDITOR.POSITION_AFTER_START ); 1487 }, 1488 1489 getTouchedStartNode : function() 1490 { 1491 var container = this.startContainer ; 1492 1493 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT ) 1494 return container ; 1495 1496 return container.getChild( this.startOffset ) || container ; 1497 }, 1498 1499 getTouchedEndNode : function() 1500 { 1501 var container = this.endContainer ; 1502 1503 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT ) 1504 return container ; 1505 1506 return container.getChild[ this.endOffset - 1 ] || container ; 1507 } 1508 }; 1509 })(); 1510 1511 CKEDITOR.POSITION_AFTER_START = 1; // <element>^contents</element> "^text" 1512 CKEDITOR.POSITION_BEFORE_END = 2; // <element>contents^</element> "text^" 1513 CKEDITOR.POSITION_BEFORE_START = 3; // ^<element>contents</element> ^"text" 1514 CKEDITOR.POSITION_AFTER_END = 4; // <element>contents</element>^ "text" 1515 1516 CKEDITOR.ENLARGE_ELEMENT = 1; 1517 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2; 1518 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3; 1519