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 range.optimizeBookmark(); 35 36 var startNode = range.startContainer; 37 var endNode = range.endContainer; 38 39 var startOffset = range.startOffset; 40 var endOffset = range.endOffset; 41 42 var removeStartNode; 43 var removeEndNode; 44 45 // For text containers, we must simply split the node and point to the 46 // second part. The removal will be handled by the rest of the code . 47 if ( endNode.type == CKEDITOR.NODE_TEXT ) 48 endNode = endNode.split( endOffset ); 49 else 50 { 51 // If the end container has children and the offset is pointing 52 // to a child, then we should start from it. 53 if ( endNode.getChildCount() > 0 ) 54 { 55 // If the offset points after the last node. 56 if ( endOffset >= endNode.getChildCount() ) 57 { 58 // Let's create a temporary node and mark it for removal. 59 endNode = endNode.append( range.document.createText( '' ) ); 60 removeEndNode = true; 61 } 62 else 63 endNode = endNode.getChild( endOffset ); 64 } 65 } 66 67 // For text containers, we must simply split the node. The removal will 68 // be handled by the rest of the code . 69 if ( startNode.type == CKEDITOR.NODE_TEXT ) 70 { 71 startNode.split( startOffset ); 72 73 // In cases the end node is the same as the start node, the above 74 // splitting will also split the end, so me must move the end to 75 // the second part of the split. 76 if ( startNode.equals( endNode ) ) 77 endNode = startNode.getNext(); 78 } 79 else 80 { 81 // If the start container has children and the offset is pointing 82 // to a child, then we should start from its previous sibling. 83 84 // If the offset points to the first node, we don't have a 85 // sibling, so let's use the first one, but mark it for removal. 86 if ( !startOffset ) 87 { 88 // Let's create a temporary node and mark it for removal. 89 startNode = startNode.getFirst().insertBeforeMe( range.document.createText( '' ) ); 90 removeStartNode = true; 91 } 92 else if ( startOffset >= startNode.getChildCount() ) 93 { 94 // Let's create a temporary node and mark it for removal. 95 startNode = startNode.append( range.document.createText( '' ) ); 96 removeStartNode = true; 97 } 98 else 99 startNode = startNode.getChild( startOffset ).getPrevious(); 100 } 101 102 // Get the parent nodes tree for the start and end boundaries. 103 var startParents = startNode.getParents(); 104 var endParents = endNode.getParents(); 105 106 // Compare them, to find the top most siblings. 107 var i, topStart, topEnd; 108 109 for ( i = 0 ; i < startParents.length ; i++ ) 110 { 111 topStart = startParents[ i ]; 112 topEnd = endParents[ i ]; 113 114 // The compared nodes will match until we find the top most 115 // siblings (different nodes that have the same parent). 116 // "i" will hold the index in the parents array for the top 117 // most element. 118 if ( !topStart.equals( topEnd ) ) 119 break; 120 } 121 122 var clone = docFrag, levelStartNode, levelClone, currentNode, currentSibling; 123 124 // Remove all successive sibling nodes for every node in the 125 // startParents tree. 126 for ( var j = i ; j < startParents.length ; j++ ) 127 { 128 levelStartNode = startParents[j]; 129 130 // For Extract and Clone, we must clone this level. 131 if ( clone && !levelStartNode.equals( startNode ) ) // action = 0 = Delete 132 levelClone = clone.append( levelStartNode.clone() ); 133 134 currentNode = levelStartNode.getNext(); 135 136 while( currentNode ) 137 { 138 // Stop processing when the current node matches a node in the 139 // endParents tree or if it is the endNode. 140 if ( currentNode.equals( endParents[ j ] ) || currentNode.equals( endNode ) ) 141 break; 142 143 // Cache the next sibling. 144 currentSibling = currentNode.getNext(); 145 146 // If cloning, just clone it. 147 if ( action == 2 ) // 2 = Clone 148 clone.append( currentNode.clone( true ) ); 149 else 150 { 151 // Both Delete and Extract will remove the node. 152 currentNode.remove(); 153 154 // When Extracting, move the removed node to the docFrag. 155 if ( action == 1 ) // 1 = Extract 156 clone.append( currentNode ); 157 } 158 159 currentNode = currentSibling; 160 } 161 162 if ( clone ) 163 clone = levelClone; 164 } 165 166 clone = docFrag; 167 168 // Remove all previous sibling nodes for every node in the 169 // endParents tree. 170 for ( var k = i ; k < endParents.length ; k++ ) 171 { 172 levelStartNode = endParents[ k ]; 173 174 // For Extract and Clone, we must clone this level. 175 if ( action > 0 && !levelStartNode.equals( endNode ) ) // action = 0 = Delete 176 levelClone = clone.append( levelStartNode.clone() ); 177 178 // The processing of siblings may have already been done by the parent. 179 if ( !startParents[ k ] || levelStartNode.$.parentNode != startParents[ k ].$.parentNode ) 180 { 181 currentNode = levelStartNode.getPrevious(); 182 183 while( currentNode ) 184 { 185 // Stop processing when the current node matches a node in the 186 // startParents tree or if it is the startNode. 187 if ( currentNode.equals( startParents[ k ] ) || currentNode.equals( startNode ) ) 188 break; 189 190 // Cache the next sibling. 191 currentSibling = currentNode.getPrevious(); 192 193 // If cloning, just clone it. 194 if ( action == 2 ) // 2 = Clone 195 clone.$.insertBefore( currentNode.$.cloneNode( true ), clone.$.firstChild ) ; 196 else 197 { 198 // Both Delete and Extract will remove the node. 199 currentNode.remove(); 200 201 // When Extracting, mode the removed node to the docFrag. 202 if ( action == 1 ) // 1 = Extract 203 clone.$.insertBefore( currentNode.$, clone.$.firstChild ); 204 } 205 206 currentNode = currentSibling; 207 } 208 } 209 210 if ( clone ) 211 clone = levelClone; 212 } 213 214 if ( action == 2 ) // 2 = Clone. 215 { 216 // No changes in the DOM should be done, so fix the split text (if any). 217 218 var startTextNode = range.startContainer; 219 if ( startTextNode.type == CKEDITOR.NODE_TEXT ) 220 { 221 startTextNode.$.data += startTextNode.$.nextSibling.data; 222 startTextNode.$.parentNode.removeChild( startTextNode.$.nextSibling ); 223 } 224 225 var endTextNode = range.endContainer; 226 if ( endTextNode.type == CKEDITOR.NODE_TEXT && endTextNode.$.nextSibling ) 227 { 228 endTextNode.$.data += endTextNode.$.nextSibling.data; 229 endTextNode.$.parentNode.removeChild( endTextNode.$.nextSibling ); 230 } 231 } 232 else 233 { 234 // Collapse the range. 235 236 // If a node has been partially selected, collapse the range between 237 // topStart and topEnd. Otherwise, simply collapse it to the start. (W3C specs). 238 if ( topStart && topEnd && ( startNode.$.parentNode != topStart.$.parentNode || endNode.$.parentNode != topEnd.$.parentNode ) ) 239 { 240 var endIndex = topEnd.getIndex(); 241 242 // If the start node is to be removed, we must correct the 243 // index to reflect the removal. 244 if ( removeStartNode && topEnd.$.parentNode == startNode.$.parentNode ) 245 endIndex--; 246 247 range.setStart( topEnd.getParent(), endIndex ); 248 } 249 250 // Collapse it to the start. 251 range.collapse( true ); 252 } 253 254 // Cleanup any marked node. 255 if( removeStartNode ) 256 startNode.remove(); 257 258 if( removeEndNode && endNode.$.parentNode ) 259 endNode.remove(); 260 }; 261 262 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 }; 263 264 // Creates the appropriate node evaluator for the dom walker used inside 265 // check(Start|End)OfBlock. 266 function getCheckStartEndBlockEvalFunction( isStart ) 267 { 268 var hadBr = false, bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ); 269 return function( node ) 270 { 271 // First ignore bookmark nodes. 272 if ( bookmarkEvaluator( node ) ) 273 return true; 274 275 if ( node.type == CKEDITOR.NODE_TEXT ) 276 { 277 // If there's any visible text, then we're not at the start. 278 if ( CKEDITOR.tools.trim( node.getText() ).length ) 279 return false; 280 } 281 else 282 { 283 // If there are non-empty inline elements (e.g. <img />), then we're not 284 // at the start. 285 if ( !inlineChildReqElements[ node.getName() ] ) 286 { 287 // If we're working at the end-of-block, forgive the first <br /> in non-IE 288 // browsers. 289 if ( !isStart && !CKEDITOR.env.ie && node.getName() == 'br' && !hadBr ) 290 hadBr = true; 291 else 292 return false; 293 } 294 } 295 return true; 296 }; 297 } 298 299 // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any 300 // text node and non-empty elements unless it's being bookmark text. 301 function elementBoundaryEval( node ) 302 { 303 // Reject any text node unless it's being bookmark. 304 return node.type != CKEDITOR.NODE_TEXT 305 && node.getName() in CKEDITOR.dtd.$removeEmpty 306 || node.getParent().hasAttribute( '_fck_bookmark' ); 307 } 308 309 CKEDITOR.dom.range.prototype = 310 { 311 clone : function() 312 { 313 var clone = new CKEDITOR.dom.range( this.document ); 314 315 clone.startContainer = this.startContainer; 316 clone.startOffset = this.startOffset; 317 clone.endContainer = this.endContainer; 318 clone.endOffset = this.endOffset; 319 clone.collapsed = this.collapsed; 320 321 return clone; 322 }, 323 324 collapse : function( toStart ) 325 { 326 if ( toStart ) 327 { 328 this.endContainer = this.startContainer; 329 this.endOffset = this.startOffset; 330 } 331 else 332 { 333 this.startContainer = this.endContainer; 334 this.startOffset = this.endOffset; 335 } 336 337 this.collapsed = true; 338 }, 339 340 // The selection may be lost when cloning (due to the splitText() call). 341 cloneContents : function() 342 { 343 var docFrag = new CKEDITOR.dom.documentFragment( this.document ); 344 345 if ( !this.collapsed ) 346 execContentsAction( this, 2, docFrag ); 347 348 return docFrag; 349 }, 350 351 deleteContents : function() 352 { 353 if ( this.collapsed ) 354 return; 355 356 execContentsAction( this, 0 ); 357 }, 358 359 extractContents : function() 360 { 361 var docFrag = new CKEDITOR.dom.documentFragment( this.document ); 362 363 if ( !this.collapsed ) 364 execContentsAction( this, 1, docFrag ); 365 366 return docFrag; 367 }, 368 369 /** 370 * Creates a bookmark object, which can be later used to restore the 371 * range by using the moveToBookmark function. 372 * This is an "intrusive" way to create a bookmark. It includes <span> tags 373 * in the range boundaries. The advantage of it is that it is possible to 374 * handle DOM mutations when moving back to the bookmark. 375 * Attention: the inclusion of nodes in the DOM is a design choice and 376 * should not be changed as there are other points in the code that may be 377 * using those nodes to perform operations. See GetBookmarkNode. 378 * @param {Boolean} [serializable] Indicates that the bookmark nodes 379 * must contain ids, which can be used to restore the range even 380 * when these nodes suffer mutations (like a clonation or innerHTML 381 * change). 382 * @returns {Object} And object representing a bookmark. 383 */ 384 createBookmark : function( serializable ) 385 { 386 var startNode, endNode; 387 var baseId; 388 var clone; 389 390 startNode = this.document.createElement( 'span' ); 391 startNode.setAttribute( '_fck_bookmark', 1 ); 392 startNode.setStyle( 'display', 'none' ); 393 394 // For IE, it must have something inside, otherwise it may be 395 // removed during DOM operations. 396 startNode.setHtml( ' ' ); 397 398 if ( serializable ) 399 { 400 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber(); 401 startNode.setAttribute( 'id', baseId + 'S' ); 402 } 403 404 // If collapsed, the endNode will not be created. 405 if ( !this.collapsed ) 406 { 407 endNode = startNode.clone(); 408 endNode.setHtml( ' ' ); 409 410 if ( serializable ) 411 endNode.setAttribute( 'id', baseId + 'E' ); 412 413 clone = this.clone(); 414 clone.collapse(); 415 clone.insertNode( endNode ); 416 } 417 418 clone = this.clone(); 419 clone.collapse( true ); 420 clone.insertNode( startNode ); 421 422 // Update the range position. 423 if ( endNode ) 424 { 425 this.setStartAfter( startNode ); 426 this.setEndBefore( endNode ); 427 } 428 else 429 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END ); 430 431 return { 432 startNode : serializable ? baseId + 'S' : startNode, 433 endNode : serializable ? baseId + 'E' : endNode, 434 serializable : serializable 435 }; 436 }, 437 438 /** 439 * Creates a "non intrusive" and "mutation sensible" bookmark. This 440 * kind of bookmark should be used only when the DOM is supposed to 441 * remain stable after its creation. 442 * @param {Boolean} [normalized] Indicates that the bookmark must 443 * normalized. When normalized, the successive text nodes are 444 * considered a single node. To sucessful load a normalized 445 * bookmark, the DOM tree must be also normalized before calling 446 * moveToBookmark. 447 * @returns {Object} An object representing the bookmark. 448 */ 449 createBookmark2 : function( normalized ) 450 { 451 var startContainer = this.startContainer, 452 endContainer = this.endContainer; 453 454 var startOffset = this.startOffset, 455 endOffset = this.endOffset; 456 457 var child, previous; 458 459 // If there is no range then get out of here. 460 // It happens on initial load in Safari #962 and if the editor it's 461 // hidden also in Firefox 462 if ( !startContainer || !endContainer ) 463 return { start : 0, end : 0 }; 464 465 if ( normalized ) 466 { 467 // Find out if the start is pointing to a text node that will 468 // be normalized. 469 if ( startContainer.type == CKEDITOR.NODE_ELEMENT ) 470 { 471 child = startContainer.getChild( startOffset ); 472 473 // In this case, move the start information to that text 474 // node. 475 if ( child && child.type == CKEDITOR.NODE_TEXT 476 && startOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT ) 477 { 478 startContainer = child; 479 startOffset = 0; 480 } 481 } 482 483 // Normalize the start. 484 while ( startContainer.type == CKEDITOR.NODE_TEXT 485 && ( previous = startContainer.getPrevious() ) 486 && previous.type == CKEDITOR.NODE_TEXT ) 487 { 488 startContainer = previous; 489 startOffset += previous.getLength(); 490 } 491 492 // Process the end only if not normalized. 493 if ( !this.isCollapsed ) 494 { 495 // Find out if the start is pointing to a text node that 496 // will be normalized. 497 if ( endContainer.type == CKEDITOR.NODE_ELEMENT ) 498 { 499 child = endContainer.getChild( endOffset ); 500 501 // In this case, move the start information to that 502 // text node. 503 if ( child && child.type == CKEDITOR.NODE_TEXT 504 && endOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT ) 505 { 506 endContainer = child; 507 endOffset = 0; 508 } 509 } 510 511 // Normalize the end. 512 while ( endContainer.type == CKEDITOR.NODE_TEXT 513 && ( previous = endContainer.getPrevious() ) 514 && previous.type == CKEDITOR.NODE_TEXT ) 515 { 516 endContainer = previous; 517 endOffset += previous.getLength(); 518 } 519 } 520 } 521 522 return { 523 start : startContainer.getAddress( normalized ), 524 end : this.isCollapsed ? null : endContainer.getAddress( normalized ), 525 startOffset : startOffset, 526 endOffset : endOffset, 527 normalized : normalized, 528 is2 : true // It's a createBookmark2 bookmark. 529 }; 530 }, 531 532 moveToBookmark : function( bookmark ) 533 { 534 if ( bookmark.is2 ) // Created with createBookmark2(). 535 { 536 // Get the start information. 537 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ), 538 startOffset = bookmark.startOffset; 539 540 // Get the end information. 541 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ), 542 endOffset = bookmark.endOffset; 543 544 // Set the start boundary. 545 this.setStart( startContainer, startOffset ); 546 547 // Set the end boundary. If not available, collapse it. 548 if ( endContainer ) 549 this.setEnd( endContainer, endOffset ); 550 else 551 this.collapse( true ); 552 } 553 else // Created with createBookmark(). 554 { 555 var serializable = bookmark.serializable, 556 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode, 557 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode; 558 559 // Set the range start at the bookmark start node position. 560 this.setStartBefore( startNode ); 561 562 // Remove it, because it may interfere in the setEndBefore call. 563 startNode.remove(); 564 565 // Set the range end at the bookmark end node position, or simply 566 // collapse it if it is not available. 567 if ( endNode ) 568 { 569 this.setEndBefore( endNode ); 570 endNode.remove(); 571 } 572 else 573 this.collapse( true ); 574 } 575 }, 576 577 getBoundaryNodes : function() 578 { 579 var startNode = this.startContainer, 580 endNode = this.endContainer, 581 startOffset = this.startOffset, 582 endOffset = this.endOffset, 583 childCount; 584 585 if ( startNode.type == CKEDITOR.NODE_ELEMENT ) 586 { 587 childCount = startNode.getChildCount(); 588 if ( childCount > startOffset ) 589 startNode = startNode.getChild( startOffset ); 590 else if ( childCount < 1 ) 591 startNode = startNode.getPreviousSourceNode(); 592 else // startOffset > childCount but childCount is not 0 593 { 594 // Try to take the node just after the current position. 595 startNode = startNode.$; 596 while ( startNode.lastChild ) 597 startNode = startNode.lastChild; 598 startNode = new CKEDITOR.dom.node( startNode ); 599 600 // Normally we should take the next node in DFS order. But it 601 // is also possible that we've already reached the end of 602 // document. 603 startNode = startNode.getNextSourceNode() || startNode; 604 } 605 } 606 if ( endNode.type == CKEDITOR.NODE_ELEMENT ) 607 { 608 childCount = endNode.getChildCount(); 609 if ( childCount > endOffset ) 610 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true ); 611 else if ( childCount < 1 ) 612 endNode = endNode.getPreviousSourceNode(); 613 else // endOffset > childCount but childCount is not 0 614 { 615 // Try to take the node just before the current position. 616 endNode = endNode.$; 617 while ( endNode.lastChild ) 618 endNode = endNode.lastChild; 619 endNode = new CKEDITOR.dom.node( endNode ); 620 } 621 } 622 623 // Sometimes the endNode will come right before startNode for collapsed 624 // ranges. Fix it. (#3780) 625 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING ) 626 startNode = endNode; 627 628 return { startNode : startNode, endNode : endNode }; 629 }, 630 631 /** 632 * Find the node which fully contains the range. 633 * @param includeSelf 634 * @param {Boolean} ignoreTextNode Whether ignore CKEDITOR.NODE_TEXT type. 635 */ 636 getCommonAncestor : function( includeSelf , ignoreTextNode ) 637 { 638 var start = this.startContainer, 639 end = this.endContainer, 640 ancestor; 641 642 if ( start.equals( end ) ) 643 { 644 if ( includeSelf 645 && start.type == CKEDITOR.NODE_ELEMENT 646 && this.startOffset == this.endOffset - 1 ) 647 ancestor = start.getChild( this.startOffset ); 648 else 649 ancestor = start; 650 } 651 else 652 ancestor = start.getCommonAncestor( end ); 653 654 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor; 655 }, 656 657 /** 658 * Transforms the startContainer and endContainer properties from text 659 * nodes to element nodes, whenever possible. This is actually possible 660 * if either of the boundary containers point to a text node, and its 661 * offset is set to zero, or after the last char in the node. 662 */ 663 optimize : function() 664 { 665 var container = this.startContainer; 666 var offset = this.startOffset; 667 668 if ( container.type != CKEDITOR.NODE_ELEMENT ) 669 { 670 if ( !offset ) 671 this.setStartBefore( container ); 672 else if ( offset >= container.getLength() ) 673 this.setStartAfter( container ); 674 } 675 676 container = this.endContainer; 677 offset = this.endOffset; 678 679 if ( container.type != CKEDITOR.NODE_ELEMENT ) 680 { 681 if ( !offset ) 682 this.setEndBefore( container ); 683 else if ( offset >= container.getLength() ) 684 this.setEndAfter( container ); 685 } 686 }, 687 688 /** 689 * Move the range out of bookmark nodes if they're been the container. 690 */ 691 optimizeBookmark: function() 692 { 693 var startNode = this.startContainer, 694 endNode = this.endContainer; 695 696 if ( startNode.is && startNode.is( 'span' ) 697 && startNode.hasAttribute( '_fck_bookmark' ) ) 698 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START ); 699 if ( endNode && endNode.is && endNode.is( 'span' ) 700 && endNode.hasAttribute( '_fck_bookmark' ) ) 701 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END ); 702 }, 703 704 trim : function( ignoreStart, ignoreEnd ) 705 { 706 var startContainer = this.startContainer, 707 startOffset = this.startOffset, 708 collapsed = this.collapsed; 709 if ( ( !ignoreStart || collapsed ) 710 && startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) 711 { 712 // If the offset is zero, we just insert the new node before 713 // the start. 714 if ( !startOffset ) 715 { 716 startOffset = startContainer.getIndex(); 717 startContainer = startContainer.getParent(); 718 } 719 // If the offset is at the end, we'll insert it after the text 720 // node. 721 else if ( startOffset >= startContainer.getLength() ) 722 { 723 startOffset = startContainer.getIndex() + 1; 724 startContainer = startContainer.getParent(); 725 } 726 // In other case, we split the text node and insert the new 727 // node at the split point. 728 else 729 { 730 var nextText = startContainer.split( startOffset ); 731 732 startOffset = startContainer.getIndex() + 1; 733 startContainer = startContainer.getParent(); 734 // Check if it is necessary to update the end boundary. 735 if ( !collapsed && this.startContainer.equals( this.endContainer ) ) 736 this.setEnd( nextText, this.endOffset - this.startOffset ); 737 } 738 739 this.setStart( startContainer, startOffset ); 740 741 if ( collapsed ) 742 this.collapse( true ); 743 } 744 745 var endContainer = this.endContainer; 746 var endOffset = this.endOffset; 747 748 if ( !( ignoreEnd || collapsed ) 749 && endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) 750 { 751 // If the offset is zero, we just insert the new node before 752 // the start. 753 if ( !endOffset ) 754 { 755 endOffset = endContainer.getIndex(); 756 endContainer = endContainer.getParent(); 757 } 758 // If the offset is at the end, we'll insert it after the text 759 // node. 760 else if ( endOffset >= endContainer.getLength() ) 761 { 762 endOffset = endContainer.getIndex() + 1; 763 endContainer = endContainer.getParent(); 764 } 765 // In other case, we split the text node and insert the new 766 // node at the split point. 767 else 768 { 769 endContainer.split( endOffset ); 770 771 endOffset = endContainer.getIndex() + 1; 772 endContainer = endContainer.getParent(); 773 } 774 775 this.setEnd( endContainer, endOffset ); 776 } 777 }, 778 779 enlarge : function( unit ) 780 { 781 switch ( unit ) 782 { 783 case CKEDITOR.ENLARGE_ELEMENT : 784 785 if ( this.collapsed ) 786 return; 787 788 // Get the common ancestor. 789 var commonAncestor = this.getCommonAncestor(); 790 791 var body = this.document.getBody(); 792 793 // For each boundary 794 // a. Depending on its position, find out the first node to be checked (a sibling) or, if not available, to be enlarge. 795 // 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. 796 797 var startTop, endTop; 798 799 var enlargeable, sibling, commonReached; 800 801 // Indicates that the node can be added only if whitespace 802 // is available before it. 803 var needsWhiteSpace = false; 804 var isWhiteSpace; 805 var siblingText; 806 807 // Process the start boundary. 808 809 var container = this.startContainer; 810 var offset = this.startOffset; 811 812 if ( container.type == CKEDITOR.NODE_TEXT ) 813 { 814 if ( offset ) 815 { 816 // Check if there is any non-space text before the 817 // offset. Otherwise, container is null. 818 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container; 819 820 // If we found only whitespace in the node, it 821 // means that we'll need more whitespace to be able 822 // to expand. For example, <i> can be expanded in 823 // "A <i> [B]</i>", but not in "A<i> [B]</i>". 824 needsWhiteSpace = !!container; 825 } 826 827 if ( container ) 828 { 829 if ( !( sibling = container.getPrevious() ) ) 830 enlargeable = container.getParent(); 831 } 832 } 833 else 834 { 835 // If we have offset, get the node preceeding it as the 836 // first sibling to be checked. 837 if ( offset ) 838 sibling = container.getChild( offset - 1 ) || container.getLast(); 839 840 // If there is no sibling, mark the container to be 841 // enlarged. 842 if ( !sibling ) 843 enlargeable = container; 844 } 845 846 while ( enlargeable || sibling ) 847 { 848 if ( enlargeable && !sibling ) 849 { 850 // If we reached the common ancestor, mark the flag 851 // for it. 852 if ( !commonReached && enlargeable.equals( commonAncestor ) ) 853 commonReached = true; 854 855 if ( !body.contains( enlargeable ) ) 856 break; 857 858 // If we don't need space or this element breaks 859 // the line, then enlarge it. 860 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) 861 { 862 needsWhiteSpace = false; 863 864 // If the common ancestor has been reached, 865 // we'll not enlarge it immediately, but just 866 // mark it to be enlarged later if the end 867 // boundary also enlarges it. 868 if ( commonReached ) 869 startTop = enlargeable; 870 else 871 this.setStartBefore( enlargeable ); 872 } 873 874 sibling = enlargeable.getPrevious(); 875 } 876 877 // Check all sibling nodes preceeding the enlargeable 878 // node. The node wil lbe enlarged only if none of them 879 // blocks it. 880 while ( sibling ) 881 { 882 // This flag indicates that this node has 883 // whitespaces at the end. 884 isWhiteSpace = false; 885 886 if ( sibling.type == CKEDITOR.NODE_TEXT ) 887 { 888 siblingText = sibling.getText(); 889 890 if ( /[^\s\ufeff]/.test( siblingText ) ) 891 sibling = null; 892 893 isWhiteSpace = /[\s\ufeff]$/.test( siblingText ); 894 } 895 else 896 { 897 // If this is a visible element. 898 // We need to check for the bookmark attribute because IE insists on 899 // rendering the display:none nodes we use for bookmarks. (#3363) 900 if ( sibling.$.offsetWidth > 0 && !sibling.getAttribute( '_fck_bookmark' ) ) 901 { 902 // We'll accept it only if we need 903 // whitespace, and this is an inline 904 // element with whitespace only. 905 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) 906 { 907 // It must contains spaces and inline elements only. 908 909 siblingText = sibling.getText(); 910 911 if ( !(/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF) 912 sibling = null; 913 else 914 { 915 var allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' ); 916 for ( var i = 0, child ; child = allChildren[ i++ ] ; ) 917 { 918 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) 919 { 920 sibling = null; 921 break; 922 } 923 } 924 } 925 926 if ( sibling ) 927 isWhiteSpace = !!siblingText.length; 928 } 929 else 930 sibling = null; 931 } 932 } 933 934 // A node with whitespaces has been found. 935 if ( isWhiteSpace ) 936 { 937 // Enlarge the last enlargeable node, if we 938 // were waiting for spaces. 939 if ( needsWhiteSpace ) 940 { 941 if ( commonReached ) 942 startTop = enlargeable; 943 else if ( enlargeable ) 944 this.setStartBefore( enlargeable ); 945 } 946 else 947 needsWhiteSpace = true; 948 } 949 950 if ( sibling ) 951 { 952 var next = sibling.getPrevious(); 953 954 if ( !enlargeable && !next ) 955 { 956 // Set the sibling as enlargeable, so it's 957 // parent will be get later outside this while. 958 enlargeable = sibling; 959 sibling = null; 960 break; 961 } 962 963 sibling = next; 964 } 965 else 966 { 967 // If sibling has been set to null, then we 968 // need to stop enlarging. 969 enlargeable = null; 970 } 971 } 972 973 if ( enlargeable ) 974 enlargeable = enlargeable.getParent(); 975 } 976 977 // Process the end boundary. This is basically the same 978 // code used for the start boundary, with small changes to 979 // make it work in the oposite side (to the right). This 980 // makes it difficult to reuse the code here. So, fixes to 981 // the above code are likely to be replicated here. 982 983 container = this.endContainer; 984 offset = this.endOffset; 985 986 // Reset the common variables. 987 enlargeable = sibling = null; 988 commonReached = needsWhiteSpace = false; 989 990 if ( container.type == CKEDITOR.NODE_TEXT ) 991 { 992 // Check if there is any non-space text after the 993 // offset. Otherwise, container is null. 994 container = !CKEDITOR.tools.trim( container.substring( offset ) ).length && container; 995 996 // If we found only whitespace in the node, it 997 // means that we'll need more whitespace to be able 998 // to expand. For example, <i> can be expanded in 999 // "A <i> [B]</i>", but not in "A<i> [B]</i>". 1000 needsWhiteSpace = !( container && container.getLength() ); 1001 1002 if ( container ) 1003 { 1004 if ( !( sibling = container.getNext() ) ) 1005 enlargeable = container.getParent(); 1006 } 1007 } 1008 else 1009 { 1010 // Get the node right after the boudary to be checked 1011 // first. 1012 sibling = container.getChild( offset ); 1013 1014 if ( !sibling ) 1015 enlargeable = container; 1016 } 1017 1018 while ( enlargeable || sibling ) 1019 { 1020 if ( enlargeable && !sibling ) 1021 { 1022 if ( !commonReached && enlargeable.equals( commonAncestor ) ) 1023 commonReached = true; 1024 1025 if ( !body.contains( enlargeable ) ) 1026 break; 1027 1028 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) 1029 { 1030 needsWhiteSpace = false; 1031 1032 if ( commonReached ) 1033 endTop = enlargeable; 1034 else if ( enlargeable ) 1035 this.setEndAfter( enlargeable ); 1036 } 1037 1038 sibling = enlargeable.getNext(); 1039 } 1040 1041 while ( sibling ) 1042 { 1043 isWhiteSpace = false; 1044 1045 if ( sibling.type == CKEDITOR.NODE_TEXT ) 1046 { 1047 siblingText = sibling.getText(); 1048 1049 if ( /[^\s\ufeff]/.test( siblingText ) ) 1050 sibling = null; 1051 1052 isWhiteSpace = /^[\s\ufeff]/.test( siblingText ); 1053 } 1054 else 1055 { 1056 // If this is a visible element. 1057 // We need to check for the bookmark attribute because IE insists on 1058 // rendering the display:none nodes we use for bookmarks. (#3363) 1059 if ( sibling.$.offsetWidth > 0 && !sibling.getAttribute( '_fck_bookmark' ) ) 1060 { 1061 // We'll accept it only if we need 1062 // whitespace, and this is an inline 1063 // element with whitespace only. 1064 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) 1065 { 1066 // It must contains spaces and inline elements only. 1067 1068 siblingText = sibling.getText(); 1069 1070 if ( !(/[^\s\ufeff]/).test( siblingText ) ) 1071 sibling = null; 1072 else 1073 { 1074 allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' ); 1075 for ( i = 0 ; child = allChildren[ i++ ] ; ) 1076 { 1077 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) 1078 { 1079 sibling = null; 1080 break; 1081 } 1082 } 1083 } 1084 1085 if ( sibling ) 1086 isWhiteSpace = !!siblingText.length; 1087 } 1088 else 1089 sibling = null; 1090 } 1091 } 1092 1093 if ( isWhiteSpace ) 1094 { 1095 if ( needsWhiteSpace ) 1096 { 1097 if ( commonReached ) 1098 endTop = enlargeable; 1099 else 1100 this.setEndAfter( enlargeable ); 1101 } 1102 } 1103 1104 if ( sibling ) 1105 { 1106 next = sibling.getNext(); 1107 1108 if ( !enlargeable && !next ) 1109 { 1110 enlargeable = sibling; 1111 sibling = null; 1112 break; 1113 } 1114 1115 sibling = next; 1116 } 1117 else 1118 { 1119 // If sibling has been set to null, then we 1120 // need to stop enlarging. 1121 enlargeable = null; 1122 } 1123 } 1124 1125 if ( enlargeable ) 1126 enlargeable = enlargeable.getParent(); 1127 } 1128 1129 // If the common ancestor can be enlarged by both boundaries, then include it also. 1130 if ( startTop && endTop ) 1131 { 1132 commonAncestor = startTop.contains( endTop ) ? endTop : startTop; 1133 1134 this.setStartBefore( commonAncestor ); 1135 this.setEndAfter( commonAncestor ); 1136 } 1137 break; 1138 1139 case CKEDITOR.ENLARGE_BLOCK_CONTENTS: 1140 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS: 1141 1142 // Enlarging the start boundary. 1143 var walkerRange = new CKEDITOR.dom.range( this.document ); 1144 1145 body = this.document.getBody(); 1146 1147 walkerRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START ); 1148 walkerRange.setEnd( this.startContainer, this.startOffset ); 1149 1150 var walker = new CKEDITOR.dom.walker( walkerRange ), 1151 blockBoundary, // The node on which the enlarging should stop. 1152 tailBr, // 1153 defaultGuard = CKEDITOR.dom.walker.blockBoundary( 1154 ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br : 1 } : null ), 1155 // Record the encountered 'blockBoundary' for later use. 1156 boundaryGuard = function( node ) 1157 { 1158 var retval = defaultGuard( node ); 1159 if ( !retval ) 1160 blockBoundary = node; 1161 return retval; 1162 }, 1163 // Record the encounted 'tailBr' for later use. 1164 tailBrGuard = function( node ) 1165 { 1166 var retval = boundaryGuard( node ); 1167 if ( !retval && node.is && node.is( 'br' ) ) 1168 tailBr = node; 1169 return retval; 1170 }; 1171 1172 walker.guard = boundaryGuard; 1173 1174 1175 if ( ( enlargeable = walker.lastBackward() ) ) 1176 { 1177 // It's the body which stop the enlaring if no block boundary found. 1178 blockBoundary = blockBoundary || body; 1179 1180 // Start the range at different position by comparing 1181 // the document position of it with 'enlargeable' node. 1182 this.setStartAt( 1183 blockBoundary, 1184 blockBoundary.contains( enlargeable ) ? 1185 CKEDITOR.POSITION_AFTER_START : 1186 CKEDITOR.POSITION_AFTER_END ); 1187 } 1188 1189 // Enlarging the end boundary. 1190 walkerRange = this.clone(); 1191 walkerRange.collapse(); 1192 walkerRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END ); 1193 walker = new CKEDITOR.dom.walker( walkerRange ); 1194 1195 // tailBrGuard only used for on range end. 1196 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? 1197 tailBrGuard : boundaryGuard; 1198 blockBoundary = null; 1199 // End the range right before the block boundary node. 1200 1201 if ( ( enlargeable = walker.lastForward() ) ) 1202 { 1203 // It's the body which stop the enlaring if no block boundary found. 1204 blockBoundary = blockBoundary || body; 1205 1206 // Start the range at different position by comparing 1207 // the document position of it with 'enlargeable' node. 1208 this.setEndAt( 1209 blockBoundary, 1210 blockBoundary.contains( enlargeable ) ? 1211 CKEDITOR.POSITION_BEFORE_END : 1212 CKEDITOR.POSITION_BEFORE_START ); 1213 } 1214 // We must include the <br> at the end of range if there's 1215 // one and we're expanding list item contents 1216 if ( tailBr ) 1217 this.setEndAfter( tailBr ); 1218 } 1219 }, 1220 1221 /** 1222 * Inserts a node at the start of the range. The range will be expanded 1223 * the contain the node. 1224 */ 1225 insertNode : function( node ) 1226 { 1227 this.optimizeBookmark(); 1228 this.trim( false, true ); 1229 1230 var startContainer = this.startContainer; 1231 var startOffset = this.startOffset; 1232 1233 var nextNode = startContainer.getChild( startOffset ); 1234 1235 if ( nextNode ) 1236 node.insertBefore( nextNode ); 1237 else 1238 startContainer.append( node ); 1239 1240 // Check if we need to update the end boundary. 1241 if ( node.getParent().equals( this.endContainer ) ) 1242 this.endOffset++; 1243 1244 // Expand the range to embrace the new node. 1245 this.setStartBefore( node ); 1246 }, 1247 1248 moveToPosition : function( node, position ) 1249 { 1250 this.setStartAt( node, position ); 1251 this.collapse( true ); 1252 }, 1253 1254 selectNodeContents : function( node ) 1255 { 1256 this.setStart( node, 0 ); 1257 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() ); 1258 }, 1259 1260 /** 1261 * Sets the start position of a Range. 1262 * @param {CKEDITOR.dom.node} startNode The node to start the range. 1263 * @param {Number} startOffset An integer greater than or equal to zero 1264 * representing the offset for the start of the range from the start 1265 * of startNode. 1266 */ 1267 setStart : function( startNode, startOffset ) 1268 { 1269 // W3C requires a check for the new position. If it is after the end 1270 // boundary, the range should be collapsed to the new start. It seams 1271 // we will not need this check for our use of this class so we can 1272 // ignore it for now. 1273 1274 this.startContainer = startNode; 1275 this.startOffset = startOffset; 1276 1277 if ( !this.endContainer ) 1278 { 1279 this.endContainer = startNode; 1280 this.endOffset = startOffset; 1281 } 1282 1283 updateCollapsed( this ); 1284 }, 1285 1286 /** 1287 * Sets the end position of a Range. 1288 * @param {CKEDITOR.dom.node} endNode The node to end the range. 1289 * @param {Number} endOffset An integer greater than or equal to zero 1290 * representing the offset for the end of the range from the start 1291 * of endNode. 1292 */ 1293 setEnd : function( endNode, endOffset ) 1294 { 1295 // W3C requires a check for the new position. If it is before the start 1296 // boundary, the range should be collapsed to the new end. It seams we 1297 // will not need this check for our use of this class so we can ignore 1298 // it for now. 1299 1300 this.endContainer = endNode; 1301 this.endOffset = endOffset; 1302 1303 if ( !this.startContainer ) 1304 { 1305 this.startContainer = endNode; 1306 this.startOffset = endOffset; 1307 } 1308 1309 updateCollapsed( this ); 1310 }, 1311 1312 setStartAfter : function( node ) 1313 { 1314 this.setStart( node.getParent(), node.getIndex() + 1 ); 1315 }, 1316 1317 setStartBefore : function( node ) 1318 { 1319 this.setStart( node.getParent(), node.getIndex() ); 1320 }, 1321 1322 setEndAfter : function( node ) 1323 { 1324 this.setEnd( node.getParent(), node.getIndex() + 1 ); 1325 }, 1326 1327 setEndBefore : function( node ) 1328 { 1329 this.setEnd( node.getParent(), node.getIndex() ); 1330 }, 1331 1332 setStartAt : function( node, position ) 1333 { 1334 switch( position ) 1335 { 1336 case CKEDITOR.POSITION_AFTER_START : 1337 this.setStart( node, 0 ); 1338 break; 1339 1340 case CKEDITOR.POSITION_BEFORE_END : 1341 if ( node.type == CKEDITOR.NODE_TEXT ) 1342 this.setStart( node, node.getLength() ); 1343 else 1344 this.setStart( node, node.getChildCount() ); 1345 break; 1346 1347 case CKEDITOR.POSITION_BEFORE_START : 1348 this.setStartBefore( node ); 1349 break; 1350 1351 case CKEDITOR.POSITION_AFTER_END : 1352 this.setStartAfter( node ); 1353 } 1354 1355 updateCollapsed( this ); 1356 }, 1357 1358 setEndAt : function( node, position ) 1359 { 1360 switch( position ) 1361 { 1362 case CKEDITOR.POSITION_AFTER_START : 1363 this.setEnd( node, 0 ); 1364 break; 1365 1366 case CKEDITOR.POSITION_BEFORE_END : 1367 if ( node.type == CKEDITOR.NODE_TEXT ) 1368 this.setEnd( node, node.getLength() ); 1369 else 1370 this.setEnd( node, node.getChildCount() ); 1371 break; 1372 1373 case CKEDITOR.POSITION_BEFORE_START : 1374 this.setEndBefore( node ); 1375 break; 1376 1377 case CKEDITOR.POSITION_AFTER_END : 1378 this.setEndAfter( node ); 1379 } 1380 1381 updateCollapsed( this ); 1382 }, 1383 1384 fixBlock : function( isStart, blockTag ) 1385 { 1386 var bookmark = this.createBookmark(), 1387 fixedBlock = this.document.createElement( blockTag ); 1388 1389 this.collapse( isStart ); 1390 1391 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS ); 1392 1393 this.extractContents().appendTo( fixedBlock ); 1394 fixedBlock.trim(); 1395 1396 if ( !CKEDITOR.env.ie ) 1397 fixedBlock.appendBogus(); 1398 1399 this.insertNode( fixedBlock ); 1400 1401 this.moveToBookmark( bookmark ); 1402 1403 return fixedBlock; 1404 }, 1405 1406 splitBlock : function( blockTag ) 1407 { 1408 var startPath = new CKEDITOR.dom.elementPath( this.startContainer ), 1409 endPath = new CKEDITOR.dom.elementPath( this.endContainer ); 1410 1411 var startBlockLimit = startPath.blockLimit, 1412 endBlockLimit = endPath.blockLimit; 1413 1414 var startBlock = startPath.block, 1415 endBlock = endPath.block; 1416 1417 var elementPath = null; 1418 // Do nothing if the boundaries are in different block limits. 1419 if ( !startBlockLimit.equals( endBlockLimit ) ) 1420 return null; 1421 1422 // Get or fix current blocks. 1423 if ( blockTag != 'br' ) 1424 { 1425 if ( !startBlock ) 1426 { 1427 startBlock = this.fixBlock( true, blockTag ); 1428 endBlock = new CKEDITOR.dom.elementPath( this.endContainer ).block; 1429 } 1430 1431 if ( !endBlock ) 1432 endBlock = this.fixBlock( false, blockTag ); 1433 } 1434 1435 // Get the range position. 1436 var isStartOfBlock = startBlock && this.checkStartOfBlock(), 1437 isEndOfBlock = endBlock && this.checkEndOfBlock(); 1438 1439 // Delete the current contents. 1440 // TODO: Why is 2.x doing CheckIsEmpty()? 1441 this.deleteContents(); 1442 1443 if ( startBlock && startBlock.equals( endBlock ) ) 1444 { 1445 if ( isEndOfBlock ) 1446 { 1447 elementPath = new CKEDITOR.dom.elementPath( this.startContainer ); 1448 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END ); 1449 endBlock = null; 1450 } 1451 else if ( isStartOfBlock ) 1452 { 1453 elementPath = new CKEDITOR.dom.elementPath( this.startContainer ); 1454 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START ); 1455 startBlock = null; 1456 } 1457 else 1458 { 1459 // Extract the contents of the block from the selection point to the end 1460 // of its contents. 1461 this.setEndAt( startBlock, CKEDITOR.POSITION_BEFORE_END ); 1462 var documentFragment = this.extractContents(); 1463 1464 // Duplicate the block element after it. 1465 endBlock = startBlock.clone( false ); 1466 1467 // Place the extracted contents into the duplicated block. 1468 documentFragment.appendTo( endBlock ); 1469 endBlock.insertAfter( startBlock ); 1470 this.moveToPosition( startBlock, CKEDITOR.POSITION_AFTER_END ); 1471 1472 // In Gecko, the last child node must be a bogus <br>. 1473 // Note: bogus <br> added under <ul> or <ol> would cause 1474 // lists to be incorrectly rendered. 1475 if ( !CKEDITOR.env.ie && !startBlock.is( 'ul', 'ol') ) 1476 startBlock.appendBogus() ; 1477 } 1478 } 1479 1480 return { 1481 previousBlock : startBlock, 1482 nextBlock : endBlock, 1483 wasStartOfBlock : isStartOfBlock, 1484 wasEndOfBlock : isEndOfBlock, 1485 elementPath : elementPath 1486 }; 1487 }, 1488 1489 /** 1490 * Check whether current range is on the inner edge of the specified element. 1491 * @param {Number} checkType ( CKEDITOR.START | CKEDITOR.END ) The checking side. 1492 * @param {CKEDITOR.dom.element} element The target element to check. 1493 */ 1494 checkBoundaryOfElement : function( element, checkType ) 1495 { 1496 var walkerRange = this.clone(); 1497 // Expand the range to element boundary. 1498 walkerRange[ checkType == CKEDITOR.START ? 1499 'setStartAt' : 'setEndAt' ] 1500 ( element, checkType == CKEDITOR.START ? 1501 CKEDITOR.POSITION_AFTER_START 1502 : CKEDITOR.POSITION_BEFORE_END ); 1503 1504 var walker = new CKEDITOR.dom.walker( walkerRange ), 1505 retval = false; 1506 walker.evaluator = elementBoundaryEval; 1507 return walker[ checkType == CKEDITOR.START ? 1508 'checkBackward' : 'checkForward' ](); 1509 }, 1510 // Calls to this function may produce changes to the DOM. The range may 1511 // be updated to reflect such changes. 1512 checkStartOfBlock : function() 1513 { 1514 var startContainer = this.startContainer, 1515 startOffset = this.startOffset; 1516 1517 // If the starting node is a text node, and non-empty before the offset, 1518 // then we're surely not at the start of block. 1519 if ( startOffset && startContainer.type == CKEDITOR.NODE_TEXT ) 1520 { 1521 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) ); 1522 if ( textBefore.length ) 1523 return false; 1524 } 1525 1526 // Antecipate the trim() call here, so the walker will not make 1527 // changes to the DOM, which would not get reflected into this 1528 // range otherwise. 1529 this.trim(); 1530 1531 // We need to grab the block element holding the start boundary, so 1532 // let's use an element path for it. 1533 var path = new CKEDITOR.dom.elementPath( this.startContainer ); 1534 1535 // Creates a range starting at the block start until the range start. 1536 var walkerRange = this.clone(); 1537 walkerRange.collapse( true ); 1538 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START ); 1539 1540 var walker = new CKEDITOR.dom.walker( walkerRange ); 1541 walker.evaluator = getCheckStartEndBlockEvalFunction( true ); 1542 1543 return walker.checkBackward(); 1544 }, 1545 1546 checkEndOfBlock : function() 1547 { 1548 var endContainer = this.endContainer, 1549 endOffset = this.endOffset; 1550 1551 // If the ending node is a text node, and non-empty after the offset, 1552 // then we're surely not at the end of block. 1553 if ( endContainer.type == CKEDITOR.NODE_TEXT ) 1554 { 1555 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) ); 1556 if ( textAfter.length ) 1557 return false; 1558 } 1559 1560 // Antecipate the trim() call here, so the walker will not make 1561 // changes to the DOM, which would not get reflected into this 1562 // range otherwise. 1563 this.trim(); 1564 1565 // We need to grab the block element holding the start boundary, so 1566 // let's use an element path for it. 1567 var path = new CKEDITOR.dom.elementPath( this.endContainer ); 1568 1569 // Creates a range starting at the block start until the range start. 1570 var walkerRange = this.clone(); 1571 walkerRange.collapse( false ); 1572 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END ); 1573 1574 var walker = new CKEDITOR.dom.walker( walkerRange ); 1575 walker.evaluator = getCheckStartEndBlockEvalFunction( false ); 1576 1577 return walker.checkForward(); 1578 }, 1579 1580 /** 1581 * Moves the range boundaries to the first editing point inside an 1582 * element. For example, in an element tree like 1583 * "<p><b><i></i></b> Text</p>", the start editing point is 1584 * "<p><b><i>^</i></b> Text</p>" (inside <i>). 1585 * @param {CKEDITOR.dom.element} targetElement The element into which 1586 * look for the editing spot. 1587 */ 1588 moveToElementEditStart : function( targetElement ) 1589 { 1590 var editableElement; 1591 1592 while ( targetElement && targetElement.type == CKEDITOR.NODE_ELEMENT ) 1593 { 1594 if ( targetElement.isEditable() ) 1595 editableElement = targetElement; 1596 else if ( editableElement ) 1597 break ; // If we already found an editable element, stop the loop. 1598 1599 targetElement = targetElement.getFirst(); 1600 } 1601 1602 if ( editableElement ) 1603 this.moveToPosition( editableElement, CKEDITOR.POSITION_AFTER_START ); 1604 }, 1605 1606 getTouchedStartNode : function() 1607 { 1608 var container = this.startContainer ; 1609 1610 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT ) 1611 return container ; 1612 1613 return container.getChild( this.startOffset ) || container ; 1614 }, 1615 1616 getTouchedEndNode : function() 1617 { 1618 var container = this.endContainer ; 1619 1620 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT ) 1621 return container ; 1622 1623 return container.getChild( this.endOffset - 1 ) || container ; 1624 } 1625 }; 1626 })(); 1627 1628 CKEDITOR.POSITION_AFTER_START = 1; // <element>^contents</element> "^text" 1629 CKEDITOR.POSITION_BEFORE_END = 2; // <element>contents^</element> "text^" 1630 CKEDITOR.POSITION_BEFORE_START = 3; // ^<element>contents</element> ^"text" 1631 CKEDITOR.POSITION_AFTER_END = 4; // <element>contents</element>^ "text" 1632 1633 CKEDITOR.ENLARGE_ELEMENT = 1; 1634 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2; 1635 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3; 1636 1637 /** 1638 * Check boundary types. 1639 * @see CKEDITOR.dom.range::checkBoundaryOfElement 1640 */ 1641 CKEDITOR.START = 1; 1642 CKEDITOR.END = 2; 1643 CKEDITOR.STARTEND = 3; 1644