1 /* 2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved. 3 For licensing, see LICENSE.html or http://ckeditor.com/license 4 */ 5 6 /** 7 * Creates a CKEDITOR.dom.range instance that can be used inside a specific 8 * DOM Document. 9 * @class Represents a delimited piece of content in a DOM Document. 10 * It is contiguous in the sense that it can be characterized as selecting all 11 * of the content between a pair of boundary-points.<br> 12 * <br> 13 * This class shares much of the W3C 14 * <a href="http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html">Document Object Model Range</a> 15 * ideas and features, adding several range manipulation tools to it, but it's 16 * not intended to be compatible with it. 17 * @param {CKEDITOR.dom.document} document The document into which the range 18 * features will be available. 19 * @example 20 * // Create a range for the entire contents of the editor document body. 21 * var range = new CKEDITOR.dom.range( editor.document ); 22 * range.selectNodeContents( editor.document.getBody() ); 23 * // Delete the contents. 24 * range.deleteContents(); 25 */ 26 CKEDITOR.dom.range = function( document ) 27 { 28 /** 29 * Node within which the range begins. 30 * @type {CKEDITOR.NODE_ELEMENT|CKEDITOR.NODE_TEXT} 31 * @example 32 * var range = new CKEDITOR.dom.range( editor.document ); 33 * range.selectNodeContents( editor.document.getBody() ); 34 * alert( range.startContainer.getName() ); // "body" 35 */ 36 this.startContainer = null; 37 38 /** 39 * Offset within the starting node of the range. 40 * @type {Number} 41 * @example 42 * var range = new CKEDITOR.dom.range( editor.document ); 43 * range.selectNodeContents( editor.document.getBody() ); 44 * alert( range.startOffset ); // "0" 45 */ 46 this.startOffset = null; 47 48 /** 49 * Node within which the range ends. 50 * @type {CKEDITOR.NODE_ELEMENT|CKEDITOR.NODE_TEXT} 51 * @example 52 * var range = new CKEDITOR.dom.range( editor.document ); 53 * range.selectNodeContents( editor.document.getBody() ); 54 * alert( range.endContainer.getName() ); // "body" 55 */ 56 this.endContainer = null; 57 58 /** 59 * Offset within the ending node of the range. 60 * @type {Number} 61 * @example 62 * var range = new CKEDITOR.dom.range( editor.document ); 63 * range.selectNodeContents( editor.document.getBody() ); 64 * alert( range.endOffset ); // == editor.document.getBody().getChildCount() 65 */ 66 this.endOffset = null; 67 68 /** 69 * Indicates that this is a collapsed range. A collapsed range has it's 70 * start and end boudaries at the very same point so nothing is contained 71 * in it. 72 * @example 73 * var range = new CKEDITOR.dom.range( editor.document ); 74 * range.selectNodeContents( editor.document.getBody() ); 75 * alert( range.collapsed ); // "false" 76 * range.collapse(); 77 * alert( range.collapsed ); // "true" 78 */ 79 this.collapsed = true; 80 81 /** 82 * The document within which the range can be used. 83 * @type {CKEDITOR.dom.document} 84 * @example 85 * // Selects the body contents of the range document. 86 * range.selectNodeContents( range.document.getBody() ); 87 */ 88 this.document = document; 89 }; 90 91 (function() 92 { 93 // Updates the "collapsed" property for the given range object. 94 var updateCollapsed = function( range ) 95 { 96 range.collapsed = ( 97 range.startContainer && 98 range.endContainer && 99 range.startContainer.equals( range.endContainer ) && 100 range.startOffset == range.endOffset ); 101 }; 102 103 // This is a shared function used to delete, extract and clone the range 104 // contents. 105 // V2 106 var execContentsAction = function( range, action, docFrag, mergeThen ) 107 { 108 range.optimizeBookmark(); 109 110 var startNode = range.startContainer; 111 var endNode = range.endContainer; 112 113 var startOffset = range.startOffset; 114 var endOffset = range.endOffset; 115 116 var removeStartNode; 117 var removeEndNode; 118 119 // For text containers, we must simply split the node and point to the 120 // second part. The removal will be handled by the rest of the code . 121 if ( endNode.type == CKEDITOR.NODE_TEXT ) 122 endNode = endNode.split( endOffset ); 123 else 124 { 125 // If the end container has children and the offset is pointing 126 // to a child, then we should start from it. 127 if ( endNode.getChildCount() > 0 ) 128 { 129 // If the offset points after the last node. 130 if ( endOffset >= endNode.getChildCount() ) 131 { 132 // Let's create a temporary node and mark it for removal. 133 endNode = endNode.append( range.document.createText( '' ) ); 134 removeEndNode = true; 135 } 136 else 137 endNode = endNode.getChild( endOffset ); 138 } 139 } 140 141 // For text containers, we must simply split the node. The removal will 142 // be handled by the rest of the code . 143 if ( startNode.type == CKEDITOR.NODE_TEXT ) 144 { 145 startNode.split( startOffset ); 146 147 // In cases the end node is the same as the start node, the above 148 // splitting will also split the end, so me must move the end to 149 // the second part of the split. 150 if ( startNode.equals( endNode ) ) 151 endNode = startNode.getNext(); 152 } 153 else 154 { 155 // If the start container has children and the offset is pointing 156 // to a child, then we should start from its previous sibling. 157 158 // If the offset points to the first node, we don't have a 159 // sibling, so let's use the first one, but mark it for removal. 160 if ( !startOffset ) 161 { 162 // Let's create a temporary node and mark it for removal. 163 startNode = startNode.getFirst().insertBeforeMe( range.document.createText( '' ) ); 164 removeStartNode = true; 165 } 166 else if ( startOffset >= startNode.getChildCount() ) 167 { 168 // Let's create a temporary node and mark it for removal. 169 startNode = startNode.append( range.document.createText( '' ) ); 170 removeStartNode = true; 171 } 172 else 173 startNode = startNode.getChild( startOffset ).getPrevious(); 174 } 175 176 // Get the parent nodes tree for the start and end boundaries. 177 var startParents = startNode.getParents(); 178 var endParents = endNode.getParents(); 179 180 // Compare them, to find the top most siblings. 181 var i, topStart, topEnd; 182 183 for ( i = 0 ; i < startParents.length ; i++ ) 184 { 185 topStart = startParents[ i ]; 186 topEnd = endParents[ i ]; 187 188 // The compared nodes will match until we find the top most 189 // siblings (different nodes that have the same parent). 190 // "i" will hold the index in the parents array for the top 191 // most element. 192 if ( !topStart.equals( topEnd ) ) 193 break; 194 } 195 196 var clone = docFrag, levelStartNode, levelClone, currentNode, currentSibling; 197 198 // Remove all successive sibling nodes for every node in the 199 // startParents tree. 200 for ( var j = i ; j < startParents.length ; j++ ) 201 { 202 levelStartNode = startParents[j]; 203 204 // For Extract and Clone, we must clone this level. 205 if ( clone && !levelStartNode.equals( startNode ) ) // action = 0 = Delete 206 levelClone = clone.append( levelStartNode.clone() ); 207 208 currentNode = levelStartNode.getNext(); 209 210 while ( currentNode ) 211 { 212 // Stop processing when the current node matches a node in the 213 // endParents tree or if it is the endNode. 214 if ( currentNode.equals( endParents[ j ] ) || currentNode.equals( endNode ) ) 215 break; 216 217 // Cache the next sibling. 218 currentSibling = currentNode.getNext(); 219 220 // If cloning, just clone it. 221 if ( action == 2 ) // 2 = Clone 222 clone.append( currentNode.clone( true ) ); 223 else 224 { 225 // Both Delete and Extract will remove the node. 226 currentNode.remove(); 227 228 // When Extracting, move the removed node to the docFrag. 229 if ( action == 1 ) // 1 = Extract 230 clone.append( currentNode ); 231 } 232 233 currentNode = currentSibling; 234 } 235 236 if ( clone ) 237 clone = levelClone; 238 } 239 240 clone = docFrag; 241 242 // Remove all previous sibling nodes for every node in the 243 // endParents tree. 244 for ( var k = i ; k < endParents.length ; k++ ) 245 { 246 levelStartNode = endParents[ k ]; 247 248 // For Extract and Clone, we must clone this level. 249 if ( action > 0 && !levelStartNode.equals( endNode ) ) // action = 0 = Delete 250 levelClone = clone.append( levelStartNode.clone() ); 251 252 // The processing of siblings may have already been done by the parent. 253 if ( !startParents[ k ] || levelStartNode.$.parentNode != startParents[ k ].$.parentNode ) 254 { 255 currentNode = levelStartNode.getPrevious(); 256 257 while ( currentNode ) 258 { 259 // Stop processing when the current node matches a node in the 260 // startParents tree or if it is the startNode. 261 if ( currentNode.equals( startParents[ k ] ) || currentNode.equals( startNode ) ) 262 break; 263 264 // Cache the next sibling. 265 currentSibling = currentNode.getPrevious(); 266 267 // If cloning, just clone it. 268 if ( action == 2 ) // 2 = Clone 269 clone.$.insertBefore( currentNode.$.cloneNode( true ), clone.$.firstChild ) ; 270 else 271 { 272 // Both Delete and Extract will remove the node. 273 currentNode.remove(); 274 275 // When Extracting, mode the removed node to the docFrag. 276 if ( action == 1 ) // 1 = Extract 277 clone.$.insertBefore( currentNode.$, clone.$.firstChild ); 278 } 279 280 currentNode = currentSibling; 281 } 282 } 283 284 if ( clone ) 285 clone = levelClone; 286 } 287 288 if ( action == 2 ) // 2 = Clone. 289 { 290 // No changes in the DOM should be done, so fix the split text (if any). 291 292 var startTextNode = range.startContainer; 293 if ( startTextNode.type == CKEDITOR.NODE_TEXT ) 294 { 295 startTextNode.$.data += startTextNode.$.nextSibling.data; 296 startTextNode.$.parentNode.removeChild( startTextNode.$.nextSibling ); 297 } 298 299 var endTextNode = range.endContainer; 300 if ( endTextNode.type == CKEDITOR.NODE_TEXT && endTextNode.$.nextSibling ) 301 { 302 endTextNode.$.data += endTextNode.$.nextSibling.data; 303 endTextNode.$.parentNode.removeChild( endTextNode.$.nextSibling ); 304 } 305 } 306 else 307 { 308 // Collapse the range. 309 310 // If a node has been partially selected, collapse the range between 311 // topStart and topEnd. Otherwise, simply collapse it to the start. (W3C specs). 312 if ( topStart && topEnd && ( startNode.$.parentNode != topStart.$.parentNode || endNode.$.parentNode != topEnd.$.parentNode ) ) 313 { 314 var endIndex = topEnd.getIndex(); 315 316 // If the start node is to be removed, we must correct the 317 // index to reflect the removal. 318 if ( removeStartNode && topEnd.$.parentNode == startNode.$.parentNode ) 319 endIndex--; 320 321 // Merge splitted parents. 322 if ( mergeThen && topStart.type == CKEDITOR.NODE_ELEMENT ) 323 { 324 var span = CKEDITOR.dom.element.createFromHtml( '<span ' + 325 'data-cke-bookmark="1" style="display:none"> </span>', range.document ); 326 span.insertAfter( topStart ); 327 topStart.mergeSiblings( false ); 328 range.moveToBookmark( { startNode : span } ); 329 } 330 else 331 range.setStart( topEnd.getParent(), endIndex ); 332 } 333 334 // Collapse it to the start. 335 range.collapse( true ); 336 } 337 338 // Cleanup any marked node. 339 if ( removeStartNode ) 340 startNode.remove(); 341 342 if ( removeEndNode && endNode.$.parentNode ) 343 endNode.remove(); 344 }; 345 346 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 }; 347 348 // Creates the appropriate node evaluator for the dom walker used inside 349 // check(Start|End)OfBlock. 350 function getCheckStartEndBlockEvalFunction() 351 { 352 var skipBogus = false, 353 whitespaces = CKEDITOR.dom.walker.whitespaces(), 354 bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ), 355 isBogus = CKEDITOR.dom.walker.bogus(); 356 357 return function( node ) 358 { 359 // First skip empty nodes. 360 if ( bookmarkEvaluator( node ) || whitespaces( node ) ) 361 return true; 362 363 // Skip the bogus node at the end of block. 364 if ( isBogus( node ) && 365 !skipBogus ) 366 { 367 skipBogus = true; 368 return true; 369 } 370 371 // If there's any visible text, then we're not at the start. 372 if ( node.type == CKEDITOR.NODE_TEXT && 373 ( node.hasAscendant( 'pre' ) || 374 CKEDITOR.tools.trim( node.getText() ).length ) ) 375 return false; 376 377 // If there are non-empty inline elements (e.g. <img />), then we're not 378 // at the start. 379 if ( node.type == CKEDITOR.NODE_ELEMENT && !inlineChildReqElements[ node.getName() ] ) 380 return false; 381 382 return true; 383 }; 384 } 385 386 387 var isBogus = CKEDITOR.dom.walker.bogus(); 388 // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any 389 // text node and non-empty elements unless it's being bookmark text. 390 function elementBoundaryEval( checkStart ) 391 { 392 var whitespaces = CKEDITOR.dom.walker.whitespaces(), 393 bookmark = CKEDITOR.dom.walker.bookmark( 1 ); 394 395 return function( node ) 396 { 397 // First skip empty nodes. 398 if ( bookmark( node ) || whitespaces( node ) ) 399 return true; 400 401 // Tolerant bogus br when checking at the end of block. 402 // Reject any text node unless it's being bookmark 403 // OR it's spaces. 404 // Reject any element unless it's being invisible empty. (#3883) 405 return !checkStart && isBogus( node ) || 406 node.type == CKEDITOR.NODE_ELEMENT && 407 node.getName() in CKEDITOR.dtd.$removeEmpty; 408 }; 409 } 410 411 var whitespaceEval = new CKEDITOR.dom.walker.whitespaces(), 412 bookmarkEval = new CKEDITOR.dom.walker.bookmark(), 413 nbspRegExp = /^[\t\r\n ]*(?: |\xa0)$/; 414 415 function nonWhitespaceOrBookmarkEval( node ) 416 { 417 // Whitespaces and bookmark nodes are to be ignored. 418 return !whitespaceEval( node ) && !bookmarkEval( node ); 419 } 420 421 CKEDITOR.dom.range.prototype = 422 { 423 clone : function() 424 { 425 var clone = new CKEDITOR.dom.range( this.document ); 426 427 clone.startContainer = this.startContainer; 428 clone.startOffset = this.startOffset; 429 clone.endContainer = this.endContainer; 430 clone.endOffset = this.endOffset; 431 clone.collapsed = this.collapsed; 432 433 return clone; 434 }, 435 436 collapse : function( toStart ) 437 { 438 if ( toStart ) 439 { 440 this.endContainer = this.startContainer; 441 this.endOffset = this.startOffset; 442 } 443 else 444 { 445 this.startContainer = this.endContainer; 446 this.startOffset = this.endOffset; 447 } 448 449 this.collapsed = true; 450 }, 451 452 /** 453 * The content nodes of the range are cloned and added to a document fragment, which is returned. 454 * <strong> Note: </strong> Text selection may lost after invoking this method. (caused by text node splitting). 455 */ 456 cloneContents : function() 457 { 458 var docFrag = new CKEDITOR.dom.documentFragment( this.document ); 459 460 if ( !this.collapsed ) 461 execContentsAction( this, 2, docFrag ); 462 463 return docFrag; 464 }, 465 466 /** 467 * Deletes the content nodes of the range permanently from the DOM tree. 468 * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection. 469 */ 470 deleteContents : function( mergeThen ) 471 { 472 if ( this.collapsed ) 473 return; 474 475 execContentsAction( this, 0, null, mergeThen ); 476 }, 477 478 /** 479 * The content nodes of the range are cloned and added to a document fragment, 480 * meanwhile they're removed permanently from the DOM tree. 481 * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection. 482 */ 483 extractContents : function( mergeThen ) 484 { 485 var docFrag = new CKEDITOR.dom.documentFragment( this.document ); 486 487 if ( !this.collapsed ) 488 execContentsAction( this, 1, docFrag, mergeThen ); 489 490 return docFrag; 491 }, 492 493 /** 494 * Creates a bookmark object, which can be later used to restore the 495 * range by using the moveToBookmark function. 496 * This is an "intrusive" way to create a bookmark. It includes <span> tags 497 * in the range boundaries. The advantage of it is that it is possible to 498 * handle DOM mutations when moving back to the bookmark. 499 * Attention: the inclusion of nodes in the DOM is a design choice and 500 * should not be changed as there are other points in the code that may be 501 * using those nodes to perform operations. See GetBookmarkNode. 502 * @param {Boolean} [serializable] Indicates that the bookmark nodes 503 * must contain ids, which can be used to restore the range even 504 * when these nodes suffer mutations (like a clonation or innerHTML 505 * change). 506 * @returns {Object} And object representing a bookmark. 507 */ 508 createBookmark : function( serializable ) 509 { 510 var startNode, endNode; 511 var baseId; 512 var clone; 513 var collapsed = this.collapsed; 514 515 startNode = this.document.createElement( 'span' ); 516 startNode.data( 'cke-bookmark', 1 ); 517 startNode.setStyle( 'display', 'none' ); 518 519 // For IE, it must have something inside, otherwise it may be 520 // removed during DOM operations. 521 startNode.setHtml( ' ' ); 522 523 if ( serializable ) 524 { 525 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber(); 526 startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) ); 527 } 528 529 // If collapsed, the endNode will not be created. 530 if ( !collapsed ) 531 { 532 endNode = startNode.clone(); 533 endNode.setHtml( ' ' ); 534 535 if ( serializable ) 536 endNode.setAttribute( 'id', baseId + 'E' ); 537 538 clone = this.clone(); 539 clone.collapse(); 540 clone.insertNode( endNode ); 541 } 542 543 clone = this.clone(); 544 clone.collapse( true ); 545 clone.insertNode( startNode ); 546 547 // Update the range position. 548 if ( endNode ) 549 { 550 this.setStartAfter( startNode ); 551 this.setEndBefore( endNode ); 552 } 553 else 554 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END ); 555 556 return { 557 startNode : serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode, 558 endNode : serializable ? baseId + 'E' : endNode, 559 serializable : serializable, 560 collapsed : collapsed 561 }; 562 }, 563 564 /** 565 * Creates a "non intrusive" and "mutation sensible" bookmark. This 566 * kind of bookmark should be used only when the DOM is supposed to 567 * remain stable after its creation. 568 * @param {Boolean} [normalized] Indicates that the bookmark must 569 * normalized. When normalized, the successive text nodes are 570 * considered a single node. To sucessful load a normalized 571 * bookmark, the DOM tree must be also normalized before calling 572 * moveToBookmark. 573 * @returns {Object} An object representing the bookmark. 574 */ 575 createBookmark2 : function( normalized ) 576 { 577 var startContainer = this.startContainer, 578 endContainer = this.endContainer; 579 580 var startOffset = this.startOffset, 581 endOffset = this.endOffset; 582 583 var collapsed = this.collapsed; 584 585 var child, previous; 586 587 // If there is no range then get out of here. 588 // It happens on initial load in Safari #962 and if the editor it's 589 // hidden also in Firefox 590 if ( !startContainer || !endContainer ) 591 return { start : 0, end : 0 }; 592 593 if ( normalized ) 594 { 595 // Find out if the start is pointing to a text node that will 596 // be normalized. 597 if ( startContainer.type == CKEDITOR.NODE_ELEMENT ) 598 { 599 child = startContainer.getChild( startOffset ); 600 601 // In this case, move the start information to that text 602 // node. 603 if ( child && child.type == CKEDITOR.NODE_TEXT 604 && startOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT ) 605 { 606 startContainer = child; 607 startOffset = 0; 608 } 609 610 // Get the normalized offset. 611 if ( child && child.type == CKEDITOR.NODE_ELEMENT ) 612 startOffset = child.getIndex( 1 ); 613 } 614 615 // Normalize the start. 616 while ( startContainer.type == CKEDITOR.NODE_TEXT 617 && ( previous = startContainer.getPrevious() ) 618 && previous.type == CKEDITOR.NODE_TEXT ) 619 { 620 startContainer = previous; 621 startOffset += previous.getLength(); 622 } 623 624 // Process the end only if not normalized. 625 if ( !collapsed ) 626 { 627 // Find out if the start is pointing to a text node that 628 // will be normalized. 629 if ( endContainer.type == CKEDITOR.NODE_ELEMENT ) 630 { 631 child = endContainer.getChild( endOffset ); 632 633 // In this case, move the start information to that 634 // text node. 635 if ( child && child.type == CKEDITOR.NODE_TEXT 636 && endOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT ) 637 { 638 endContainer = child; 639 endOffset = 0; 640 } 641 642 // Get the normalized offset. 643 if ( child && child.type == CKEDITOR.NODE_ELEMENT ) 644 endOffset = child.getIndex( 1 ); 645 } 646 647 // Normalize the end. 648 while ( endContainer.type == CKEDITOR.NODE_TEXT 649 && ( previous = endContainer.getPrevious() ) 650 && previous.type == CKEDITOR.NODE_TEXT ) 651 { 652 endContainer = previous; 653 endOffset += previous.getLength(); 654 } 655 } 656 } 657 658 return { 659 start : startContainer.getAddress( normalized ), 660 end : collapsed ? null : endContainer.getAddress( normalized ), 661 startOffset : startOffset, 662 endOffset : endOffset, 663 normalized : normalized, 664 collapsed : collapsed, 665 is2 : true // It's a createBookmark2 bookmark. 666 }; 667 }, 668 669 moveToBookmark : function( bookmark ) 670 { 671 if ( bookmark.is2 ) // Created with createBookmark2(). 672 { 673 // Get the start information. 674 var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ), 675 startOffset = bookmark.startOffset; 676 677 // Get the end information. 678 var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ), 679 endOffset = bookmark.endOffset; 680 681 // Set the start boundary. 682 this.setStart( startContainer, startOffset ); 683 684 // Set the end boundary. If not available, collapse it. 685 if ( endContainer ) 686 this.setEnd( endContainer, endOffset ); 687 else 688 this.collapse( true ); 689 } 690 else // Created with createBookmark(). 691 { 692 var serializable = bookmark.serializable, 693 startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode, 694 endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode; 695 696 // Set the range start at the bookmark start node position. 697 this.setStartBefore( startNode ); 698 699 // Remove it, because it may interfere in the setEndBefore call. 700 startNode.remove(); 701 702 // Set the range end at the bookmark end node position, or simply 703 // collapse it if it is not available. 704 if ( endNode ) 705 { 706 this.setEndBefore( endNode ); 707 endNode.remove(); 708 } 709 else 710 this.collapse( true ); 711 } 712 }, 713 714 getBoundaryNodes : function() 715 { 716 var startNode = this.startContainer, 717 endNode = this.endContainer, 718 startOffset = this.startOffset, 719 endOffset = this.endOffset, 720 childCount; 721 722 if ( startNode.type == CKEDITOR.NODE_ELEMENT ) 723 { 724 childCount = startNode.getChildCount(); 725 if ( childCount > startOffset ) 726 startNode = startNode.getChild( startOffset ); 727 else if ( childCount < 1 ) 728 startNode = startNode.getPreviousSourceNode(); 729 else // startOffset > childCount but childCount is not 0 730 { 731 // Try to take the node just after the current position. 732 startNode = startNode.$; 733 while ( startNode.lastChild ) 734 startNode = startNode.lastChild; 735 startNode = new CKEDITOR.dom.node( startNode ); 736 737 // Normally we should take the next node in DFS order. But it 738 // is also possible that we've already reached the end of 739 // document. 740 startNode = startNode.getNextSourceNode() || startNode; 741 } 742 } 743 if ( endNode.type == CKEDITOR.NODE_ELEMENT ) 744 { 745 childCount = endNode.getChildCount(); 746 if ( childCount > endOffset ) 747 endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true ); 748 else if ( childCount < 1 ) 749 endNode = endNode.getPreviousSourceNode(); 750 else // endOffset > childCount but childCount is not 0 751 { 752 // Try to take the node just before the current position. 753 endNode = endNode.$; 754 while ( endNode.lastChild ) 755 endNode = endNode.lastChild; 756 endNode = new CKEDITOR.dom.node( endNode ); 757 } 758 } 759 760 // Sometimes the endNode will come right before startNode for collapsed 761 // ranges. Fix it. (#3780) 762 if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING ) 763 startNode = endNode; 764 765 return { startNode : startNode, endNode : endNode }; 766 }, 767 768 /** 769 * Find the node which fully contains the range. 770 * @param includeSelf 771 * @param {Boolean} ignoreTextNode Whether ignore CKEDITOR.NODE_TEXT type. 772 */ 773 getCommonAncestor : function( includeSelf , ignoreTextNode ) 774 { 775 var start = this.startContainer, 776 end = this.endContainer, 777 ancestor; 778 779 if ( start.equals( end ) ) 780 { 781 if ( includeSelf 782 && start.type == CKEDITOR.NODE_ELEMENT 783 && this.startOffset == this.endOffset - 1 ) 784 ancestor = start.getChild( this.startOffset ); 785 else 786 ancestor = start; 787 } 788 else 789 ancestor = start.getCommonAncestor( end ); 790 791 return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor; 792 }, 793 794 /** 795 * Transforms the startContainer and endContainer properties from text 796 * nodes to element nodes, whenever possible. This is actually possible 797 * if either of the boundary containers point to a text node, and its 798 * offset is set to zero, or after the last char in the node. 799 */ 800 optimize : function() 801 { 802 var container = this.startContainer; 803 var offset = this.startOffset; 804 805 if ( container.type != CKEDITOR.NODE_ELEMENT ) 806 { 807 if ( !offset ) 808 this.setStartBefore( container ); 809 else if ( offset >= container.getLength() ) 810 this.setStartAfter( container ); 811 } 812 813 container = this.endContainer; 814 offset = this.endOffset; 815 816 if ( container.type != CKEDITOR.NODE_ELEMENT ) 817 { 818 if ( !offset ) 819 this.setEndBefore( container ); 820 else if ( offset >= container.getLength() ) 821 this.setEndAfter( container ); 822 } 823 }, 824 825 /** 826 * Move the range out of bookmark nodes if they'd been the container. 827 */ 828 optimizeBookmark: function() 829 { 830 var startNode = this.startContainer, 831 endNode = this.endContainer; 832 833 if ( startNode.is && startNode.is( 'span' ) 834 && startNode.data( 'cke-bookmark' ) ) 835 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START ); 836 if ( endNode && endNode.is && endNode.is( 'span' ) 837 && endNode.data( 'cke-bookmark' ) ) 838 this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END ); 839 }, 840 841 trim : function( ignoreStart, ignoreEnd ) 842 { 843 var startContainer = this.startContainer, 844 startOffset = this.startOffset, 845 collapsed = this.collapsed; 846 if ( ( !ignoreStart || collapsed ) 847 && startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) 848 { 849 // If the offset is zero, we just insert the new node before 850 // the start. 851 if ( !startOffset ) 852 { 853 startOffset = startContainer.getIndex(); 854 startContainer = startContainer.getParent(); 855 } 856 // If the offset is at the end, we'll insert it after the text 857 // node. 858 else if ( startOffset >= startContainer.getLength() ) 859 { 860 startOffset = startContainer.getIndex() + 1; 861 startContainer = startContainer.getParent(); 862 } 863 // In other case, we split the text node and insert the new 864 // node at the split point. 865 else 866 { 867 var nextText = startContainer.split( startOffset ); 868 869 startOffset = startContainer.getIndex() + 1; 870 startContainer = startContainer.getParent(); 871 872 // Check all necessity of updating the end boundary. 873 if ( this.startContainer.equals( this.endContainer ) ) 874 this.setEnd( nextText, this.endOffset - this.startOffset ); 875 else if ( startContainer.equals( this.endContainer ) ) 876 this.endOffset += 1; 877 } 878 879 this.setStart( startContainer, startOffset ); 880 881 if ( collapsed ) 882 { 883 this.collapse( true ); 884 return; 885 } 886 } 887 888 var endContainer = this.endContainer; 889 var endOffset = this.endOffset; 890 891 if ( !( ignoreEnd || collapsed ) 892 && endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) 893 { 894 // If the offset is zero, we just insert the new node before 895 // the start. 896 if ( !endOffset ) 897 { 898 endOffset = endContainer.getIndex(); 899 endContainer = endContainer.getParent(); 900 } 901 // If the offset is at the end, we'll insert it after the text 902 // node. 903 else if ( endOffset >= endContainer.getLength() ) 904 { 905 endOffset = endContainer.getIndex() + 1; 906 endContainer = endContainer.getParent(); 907 } 908 // In other case, we split the text node and insert the new 909 // node at the split point. 910 else 911 { 912 endContainer.split( endOffset ); 913 914 endOffset = endContainer.getIndex() + 1; 915 endContainer = endContainer.getParent(); 916 } 917 918 this.setEnd( endContainer, endOffset ); 919 } 920 }, 921 922 /** 923 * Expands the range so that partial units are completely contained. 924 * @param unit {Number} The unit type to expand with. 925 * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding. 926 */ 927 enlarge : function( unit, excludeBrs ) 928 { 929 switch ( unit ) 930 { 931 case CKEDITOR.ENLARGE_ELEMENT : 932 933 if ( this.collapsed ) 934 return; 935 936 // Get the common ancestor. 937 var commonAncestor = this.getCommonAncestor(); 938 939 var body = this.document.getBody(); 940 941 // For each boundary 942 // a. Depending on its position, find out the first node to be checked (a sibling) or, if not available, to be enlarge. 943 // 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. 944 945 var startTop, endTop; 946 947 var enlargeable, sibling, commonReached; 948 949 // Indicates that the node can be added only if whitespace 950 // is available before it. 951 var needsWhiteSpace = false; 952 var isWhiteSpace; 953 var siblingText; 954 955 // Process the start boundary. 956 957 var container = this.startContainer; 958 var offset = this.startOffset; 959 960 if ( container.type == CKEDITOR.NODE_TEXT ) 961 { 962 if ( offset ) 963 { 964 // Check if there is any non-space text before the 965 // offset. Otherwise, container is null. 966 container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container; 967 968 // If we found only whitespace in the node, it 969 // means that we'll need more whitespace to be able 970 // to expand. For example, <i> can be expanded in 971 // "A <i> [B]</i>", but not in "A<i> [B]</i>". 972 needsWhiteSpace = !!container; 973 } 974 975 if ( container ) 976 { 977 if ( !( sibling = container.getPrevious() ) ) 978 enlargeable = container.getParent(); 979 } 980 } 981 else 982 { 983 // If we have offset, get the node preceeding it as the 984 // first sibling to be checked. 985 if ( offset ) 986 sibling = container.getChild( offset - 1 ) || container.getLast(); 987 988 // If there is no sibling, mark the container to be 989 // enlarged. 990 if ( !sibling ) 991 enlargeable = container; 992 } 993 994 while ( enlargeable || sibling ) 995 { 996 if ( enlargeable && !sibling ) 997 { 998 // If we reached the common ancestor, mark the flag 999 // for it. 1000 if ( !commonReached && enlargeable.equals( commonAncestor ) ) 1001 commonReached = true; 1002 1003 if ( !body.contains( enlargeable ) ) 1004 break; 1005 1006 // If we don't need space or this element breaks 1007 // the line, then enlarge it. 1008 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) 1009 { 1010 needsWhiteSpace = false; 1011 1012 // If the common ancestor has been reached, 1013 // we'll not enlarge it immediately, but just 1014 // mark it to be enlarged later if the end 1015 // boundary also enlarges it. 1016 if ( commonReached ) 1017 startTop = enlargeable; 1018 else 1019 this.setStartBefore( enlargeable ); 1020 } 1021 1022 sibling = enlargeable.getPrevious(); 1023 } 1024 1025 // Check all sibling nodes preceeding the enlargeable 1026 // node. The node wil lbe enlarged only if none of them 1027 // blocks it. 1028 while ( sibling ) 1029 { 1030 // This flag indicates that this node has 1031 // whitespaces at the end. 1032 isWhiteSpace = false; 1033 1034 if ( sibling.type == CKEDITOR.NODE_COMMENT ) 1035 { 1036 sibling = sibling.getPrevious(); 1037 continue; 1038 } 1039 else if ( sibling.type == CKEDITOR.NODE_TEXT ) 1040 { 1041 siblingText = sibling.getText(); 1042 1043 if ( /[^\s\ufeff]/.test( siblingText ) ) 1044 sibling = null; 1045 1046 isWhiteSpace = /[\s\ufeff]$/.test( siblingText ); 1047 } 1048 else 1049 { 1050 // If this is a visible element. 1051 // We need to check for the bookmark attribute because IE insists on 1052 // rendering the display:none nodes we use for bookmarks. (#3363) 1053 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041) 1054 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) 1055 { 1056 // We'll accept it only if we need 1057 // whitespace, and this is an inline 1058 // element with whitespace only. 1059 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) 1060 { 1061 // It must contains spaces and inline elements only. 1062 1063 siblingText = sibling.getText(); 1064 1065 if ( (/[^\s\ufeff]/).test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF) 1066 sibling = null; 1067 else 1068 { 1069 var allChildren = sibling.$.getElementsByTagName( '*' ); 1070 for ( var i = 0, child ; child = allChildren[ i++ ] ; ) 1071 { 1072 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) 1073 { 1074 sibling = null; 1075 break; 1076 } 1077 } 1078 } 1079 1080 if ( sibling ) 1081 isWhiteSpace = !!siblingText.length; 1082 } 1083 else 1084 sibling = null; 1085 } 1086 } 1087 1088 // A node with whitespaces has been found. 1089 if ( isWhiteSpace ) 1090 { 1091 // Enlarge the last enlargeable node, if we 1092 // were waiting for spaces. 1093 if ( needsWhiteSpace ) 1094 { 1095 if ( commonReached ) 1096 startTop = enlargeable; 1097 else if ( enlargeable ) 1098 this.setStartBefore( enlargeable ); 1099 } 1100 else 1101 needsWhiteSpace = true; 1102 } 1103 1104 if ( sibling ) 1105 { 1106 var next = sibling.getPrevious(); 1107 1108 if ( !enlargeable && !next ) 1109 { 1110 // Set the sibling as enlargeable, so it's 1111 // parent will be get later outside this while. 1112 enlargeable = sibling; 1113 sibling = null; 1114 break; 1115 } 1116 1117 sibling = next; 1118 } 1119 else 1120 { 1121 // If sibling has been set to null, then we 1122 // need to stop enlarging. 1123 enlargeable = null; 1124 } 1125 } 1126 1127 if ( enlargeable ) 1128 enlargeable = enlargeable.getParent(); 1129 } 1130 1131 // Process the end boundary. This is basically the same 1132 // code used for the start boundary, with small changes to 1133 // make it work in the oposite side (to the right). This 1134 // makes it difficult to reuse the code here. So, fixes to 1135 // the above code are likely to be replicated here. 1136 1137 container = this.endContainer; 1138 offset = this.endOffset; 1139 1140 // Reset the common variables. 1141 enlargeable = sibling = null; 1142 commonReached = needsWhiteSpace = false; 1143 1144 if ( container.type == CKEDITOR.NODE_TEXT ) 1145 { 1146 // Check if there is any non-space text after the 1147 // offset. Otherwise, container is null. 1148 container = !CKEDITOR.tools.trim( container.substring( offset ) ).length && container; 1149 1150 // If we found only whitespace in the node, it 1151 // means that we'll need more whitespace to be able 1152 // to expand. For example, <i> can be expanded in 1153 // "A <i> [B]</i>", but not in "A<i> [B]</i>". 1154 needsWhiteSpace = !( container && container.getLength() ); 1155 1156 if ( container ) 1157 { 1158 if ( !( sibling = container.getNext() ) ) 1159 enlargeable = container.getParent(); 1160 } 1161 } 1162 else 1163 { 1164 // Get the node right after the boudary to be checked 1165 // first. 1166 sibling = container.getChild( offset ); 1167 1168 if ( !sibling ) 1169 enlargeable = container; 1170 } 1171 1172 while ( enlargeable || sibling ) 1173 { 1174 if ( enlargeable && !sibling ) 1175 { 1176 if ( !commonReached && enlargeable.equals( commonAncestor ) ) 1177 commonReached = true; 1178 1179 if ( !body.contains( enlargeable ) ) 1180 break; 1181 1182 if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) 1183 { 1184 needsWhiteSpace = false; 1185 1186 if ( commonReached ) 1187 endTop = enlargeable; 1188 else if ( enlargeable ) 1189 this.setEndAfter( enlargeable ); 1190 } 1191 1192 sibling = enlargeable.getNext(); 1193 } 1194 1195 while ( sibling ) 1196 { 1197 isWhiteSpace = false; 1198 1199 if ( sibling.type == CKEDITOR.NODE_TEXT ) 1200 { 1201 siblingText = sibling.getText(); 1202 1203 if ( /[^\s\ufeff]/.test( siblingText ) ) 1204 sibling = null; 1205 1206 isWhiteSpace = /^[\s\ufeff]/.test( siblingText ); 1207 } 1208 else if ( sibling.type == CKEDITOR.NODE_ELEMENT ) 1209 { 1210 // If this is a visible element. 1211 // We need to check for the bookmark attribute because IE insists on 1212 // rendering the display:none nodes we use for bookmarks. (#3363) 1213 // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041) 1214 if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) 1215 { 1216 // We'll accept it only if we need 1217 // whitespace, and this is an inline 1218 // element with whitespace only. 1219 if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) 1220 { 1221 // It must contains spaces and inline elements only. 1222 1223 siblingText = sibling.getText(); 1224 1225 if ( (/[^\s\ufeff]/).test( siblingText ) ) 1226 sibling = null; 1227 else 1228 { 1229 allChildren = sibling.$.getElementsByTagName( '*' ); 1230 for ( i = 0 ; child = allChildren[ i++ ] ; ) 1231 { 1232 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) 1233 { 1234 sibling = null; 1235 break; 1236 } 1237 } 1238 } 1239 1240 if ( sibling ) 1241 isWhiteSpace = !!siblingText.length; 1242 } 1243 else 1244 sibling = null; 1245 } 1246 } 1247 else 1248 isWhiteSpace = 1; 1249 1250 if ( isWhiteSpace ) 1251 { 1252 if ( needsWhiteSpace ) 1253 { 1254 if ( commonReached ) 1255 endTop = enlargeable; 1256 else 1257 this.setEndAfter( enlargeable ); 1258 } 1259 } 1260 1261 if ( sibling ) 1262 { 1263 next = sibling.getNext(); 1264 1265 if ( !enlargeable && !next ) 1266 { 1267 enlargeable = sibling; 1268 sibling = null; 1269 break; 1270 } 1271 1272 sibling = next; 1273 } 1274 else 1275 { 1276 // If sibling has been set to null, then we 1277 // need to stop enlarging. 1278 enlargeable = null; 1279 } 1280 } 1281 1282 if ( enlargeable ) 1283 enlargeable = enlargeable.getParent(); 1284 } 1285 1286 // If the common ancestor can be enlarged by both boundaries, then include it also. 1287 if ( startTop && endTop ) 1288 { 1289 commonAncestor = startTop.contains( endTop ) ? endTop : startTop; 1290 1291 this.setStartBefore( commonAncestor ); 1292 this.setEndAfter( commonAncestor ); 1293 } 1294 break; 1295 1296 case CKEDITOR.ENLARGE_BLOCK_CONTENTS: 1297 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS: 1298 1299 // Enlarging the start boundary. 1300 var walkerRange = new CKEDITOR.dom.range( this.document ); 1301 1302 body = this.document.getBody(); 1303 1304 walkerRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START ); 1305 walkerRange.setEnd( this.startContainer, this.startOffset ); 1306 1307 var walker = new CKEDITOR.dom.walker( walkerRange ), 1308 blockBoundary, // The node on which the enlarging should stop. 1309 tailBr, // In case BR as block boundary. 1310 notBlockBoundary = CKEDITOR.dom.walker.blockBoundary( 1311 ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br : 1 } : null ), 1312 // Record the encountered 'blockBoundary' for later use. 1313 boundaryGuard = function( node ) 1314 { 1315 var retval = notBlockBoundary( node ); 1316 if ( !retval ) 1317 blockBoundary = node; 1318 return retval; 1319 }, 1320 // Record the encounted 'tailBr' for later use. 1321 tailBrGuard = function( node ) 1322 { 1323 var retval = boundaryGuard( node ); 1324 if ( !retval && node.is && node.is( 'br' ) ) 1325 tailBr = node; 1326 return retval; 1327 }; 1328 1329 walker.guard = boundaryGuard; 1330 1331 enlargeable = walker.lastBackward(); 1332 1333 // It's the body which stop the enlarging if no block boundary found. 1334 blockBoundary = blockBoundary || body; 1335 1336 // Start the range either after the end of found block (<p>...</p>[text) 1337 // or at the start of block (<p>[text...), by comparing the document position 1338 // with 'enlargeable' node. 1339 this.setStartAt( 1340 blockBoundary, 1341 !blockBoundary.is( 'br' ) && 1342 ( !enlargeable && this.checkStartOfBlock() 1343 || enlargeable && blockBoundary.contains( enlargeable ) ) ? 1344 CKEDITOR.POSITION_AFTER_START : 1345 CKEDITOR.POSITION_AFTER_END ); 1346 1347 // Avoid enlarging the range further when end boundary spans right after the BR. (#7490) 1348 if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) 1349 { 1350 var theRange = this.clone(); 1351 walker = new CKEDITOR.dom.walker( theRange ); 1352 1353 var whitespaces = CKEDITOR.dom.walker.whitespaces(), 1354 bookmark = CKEDITOR.dom.walker.bookmark(); 1355 1356 walker.evaluator = function( node ) { return !whitespaces( node ) && !bookmark( node ); }; 1357 var previous = walker.previous(); 1358 if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) ) 1359 return; 1360 } 1361 1362 1363 // Enlarging the end boundary. 1364 walkerRange = this.clone(); 1365 walkerRange.collapse(); 1366 walkerRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END ); 1367 walker = new CKEDITOR.dom.walker( walkerRange ); 1368 1369 // tailBrGuard only used for on range end. 1370 walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? 1371 tailBrGuard : boundaryGuard; 1372 blockBoundary = null; 1373 // End the range right before the block boundary node. 1374 1375 enlargeable = walker.lastForward(); 1376 1377 // It's the body which stop the enlarging if no block boundary found. 1378 blockBoundary = blockBoundary || body; 1379 1380 // Close the range either before the found block start (text]<p>...</p>) or at the block end (...text]</p>) 1381 // by comparing the document position with 'enlargeable' node. 1382 this.setEndAt( 1383 blockBoundary, 1384 ( !enlargeable && this.checkEndOfBlock() 1385 || enlargeable && blockBoundary.contains( enlargeable ) ) ? 1386 CKEDITOR.POSITION_BEFORE_END : 1387 CKEDITOR.POSITION_BEFORE_START ); 1388 // We must include the <br> at the end of range if there's 1389 // one and we're expanding list item contents 1390 if ( tailBr ) 1391 this.setEndAfter( tailBr ); 1392 } 1393 }, 1394 1395 /** 1396 * Descrease the range to make sure that boundaries 1397 * always anchor beside text nodes or innermost element. 1398 * @param {Number} mode ( CKEDITOR.SHRINK_ELEMENT | CKEDITOR.SHRINK_TEXT ) The shrinking mode. 1399 * <dl> 1400 * <dt>CKEDITOR.SHRINK_ELEMENT</dt> 1401 * <dd>Shrink the range boundaries to the edge of the innermost element.</dd> 1402 * <dt>CKEDITOR.SHRINK_TEXT</dt> 1403 * <dd>Shrink the range boudaries to anchor by the side of enclosed text node, range remains if there's no text nodes on boundaries at all.</dd> 1404 * </dl> 1405 * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node. 1406 */ 1407 shrink : function( mode, selectContents ) 1408 { 1409 // Unable to shrink a collapsed range. 1410 if ( !this.collapsed ) 1411 { 1412 mode = mode || CKEDITOR.SHRINK_TEXT; 1413 1414 var walkerRange = this.clone(); 1415 1416 var startContainer = this.startContainer, 1417 endContainer = this.endContainer, 1418 startOffset = this.startOffset, 1419 endOffset = this.endOffset, 1420 collapsed = this.collapsed; 1421 1422 // Whether the start/end boundary is moveable. 1423 var moveStart = 1, 1424 moveEnd = 1; 1425 1426 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) 1427 { 1428 if ( !startOffset ) 1429 walkerRange.setStartBefore( startContainer ); 1430 else if ( startOffset >= startContainer.getLength( ) ) 1431 walkerRange.setStartAfter( startContainer ); 1432 else 1433 { 1434 // Enlarge the range properly to avoid walker making 1435 // DOM changes caused by triming the text nodes later. 1436 walkerRange.setStartBefore( startContainer ); 1437 moveStart = 0; 1438 } 1439 } 1440 1441 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) 1442 { 1443 if ( !endOffset ) 1444 walkerRange.setEndBefore( endContainer ); 1445 else if ( endOffset >= endContainer.getLength( ) ) 1446 walkerRange.setEndAfter( endContainer ); 1447 else 1448 { 1449 walkerRange.setEndAfter( endContainer ); 1450 moveEnd = 0; 1451 } 1452 } 1453 1454 var walker = new CKEDITOR.dom.walker( walkerRange ), 1455 isBookmark = CKEDITOR.dom.walker.bookmark(); 1456 1457 walker.evaluator = function( node ) 1458 { 1459 return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ? 1460 CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT ); 1461 }; 1462 1463 var currentElement; 1464 walker.guard = function( node, movingOut ) 1465 { 1466 if ( isBookmark( node ) ) 1467 return true; 1468 1469 // Stop when we're shrink in element mode while encountering a text node. 1470 if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT ) 1471 return false; 1472 1473 // Stop when we've already walked "through" an element. 1474 if ( movingOut && node.equals( currentElement ) ) 1475 return false; 1476 1477 if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT ) 1478 currentElement = node; 1479 1480 return true; 1481 }; 1482 1483 if ( moveStart ) 1484 { 1485 var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next'](); 1486 textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START ); 1487 } 1488 1489 if ( moveEnd ) 1490 { 1491 walker.reset(); 1492 var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous'](); 1493 textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END ); 1494 } 1495 1496 return !!( moveStart || moveEnd ); 1497 } 1498 }, 1499 1500 /** 1501 * Inserts a node at the start of the range. The range will be expanded 1502 * the contain the node. 1503 */ 1504 insertNode : function( node ) 1505 { 1506 this.optimizeBookmark(); 1507 this.trim( false, true ); 1508 1509 var startContainer = this.startContainer; 1510 var startOffset = this.startOffset; 1511 1512 var nextNode = startContainer.getChild( startOffset ); 1513 1514 if ( nextNode ) 1515 node.insertBefore( nextNode ); 1516 else 1517 startContainer.append( node ); 1518 1519 // Check if we need to update the end boundary. 1520 if ( node.getParent().equals( this.endContainer ) ) 1521 this.endOffset++; 1522 1523 // Expand the range to embrace the new node. 1524 this.setStartBefore( node ); 1525 }, 1526 1527 moveToPosition : function( node, position ) 1528 { 1529 this.setStartAt( node, position ); 1530 this.collapse( true ); 1531 }, 1532 1533 selectNodeContents : function( node ) 1534 { 1535 this.setStart( node, 0 ); 1536 this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() ); 1537 }, 1538 1539 /** 1540 * Sets the start position of a Range. 1541 * @param {CKEDITOR.dom.node} startNode The node to start the range. 1542 * @param {Number} startOffset An integer greater than or equal to zero 1543 * representing the offset for the start of the range from the start 1544 * of startNode. 1545 */ 1546 setStart : function( startNode, startOffset ) 1547 { 1548 // W3C requires a check for the new position. If it is after the end 1549 // boundary, the range should be collapsed to the new start. It seams 1550 // we will not need this check for our use of this class so we can 1551 // ignore it for now. 1552 1553 // Fixing invalid range start inside dtd empty elements. 1554 if( startNode.type == CKEDITOR.NODE_ELEMENT 1555 && CKEDITOR.dtd.$empty[ startNode.getName() ] ) 1556 startOffset = startNode.getIndex(), startNode = startNode.getParent(); 1557 1558 this.startContainer = startNode; 1559 this.startOffset = startOffset; 1560 1561 if ( !this.endContainer ) 1562 { 1563 this.endContainer = startNode; 1564 this.endOffset = startOffset; 1565 } 1566 1567 updateCollapsed( this ); 1568 }, 1569 1570 /** 1571 * Sets the end position of a Range. 1572 * @param {CKEDITOR.dom.node} endNode The node to end the range. 1573 * @param {Number} endOffset An integer greater than or equal to zero 1574 * representing the offset for the end of the range from the start 1575 * of endNode. 1576 */ 1577 setEnd : function( endNode, endOffset ) 1578 { 1579 // W3C requires a check for the new position. If it is before the start 1580 // boundary, the range should be collapsed to the new end. It seams we 1581 // will not need this check for our use of this class so we can ignore 1582 // it for now. 1583 1584 // Fixing invalid range end inside dtd empty elements. 1585 if( endNode.type == CKEDITOR.NODE_ELEMENT 1586 && CKEDITOR.dtd.$empty[ endNode.getName() ] ) 1587 endOffset = endNode.getIndex() + 1, endNode = endNode.getParent(); 1588 1589 this.endContainer = endNode; 1590 this.endOffset = endOffset; 1591 1592 if ( !this.startContainer ) 1593 { 1594 this.startContainer = endNode; 1595 this.startOffset = endOffset; 1596 } 1597 1598 updateCollapsed( this ); 1599 }, 1600 1601 setStartAfter : function( node ) 1602 { 1603 this.setStart( node.getParent(), node.getIndex() + 1 ); 1604 }, 1605 1606 setStartBefore : function( node ) 1607 { 1608 this.setStart( node.getParent(), node.getIndex() ); 1609 }, 1610 1611 setEndAfter : function( node ) 1612 { 1613 this.setEnd( node.getParent(), node.getIndex() + 1 ); 1614 }, 1615 1616 setEndBefore : function( node ) 1617 { 1618 this.setEnd( node.getParent(), node.getIndex() ); 1619 }, 1620 1621 setStartAt : function( node, position ) 1622 { 1623 switch( position ) 1624 { 1625 case CKEDITOR.POSITION_AFTER_START : 1626 this.setStart( node, 0 ); 1627 break; 1628 1629 case CKEDITOR.POSITION_BEFORE_END : 1630 if ( node.type == CKEDITOR.NODE_TEXT ) 1631 this.setStart( node, node.getLength() ); 1632 else 1633 this.setStart( node, node.getChildCount() ); 1634 break; 1635 1636 case CKEDITOR.POSITION_BEFORE_START : 1637 this.setStartBefore( node ); 1638 break; 1639 1640 case CKEDITOR.POSITION_AFTER_END : 1641 this.setStartAfter( node ); 1642 } 1643 1644 updateCollapsed( this ); 1645 }, 1646 1647 setEndAt : function( node, position ) 1648 { 1649 switch( position ) 1650 { 1651 case CKEDITOR.POSITION_AFTER_START : 1652 this.setEnd( node, 0 ); 1653 break; 1654 1655 case CKEDITOR.POSITION_BEFORE_END : 1656 if ( node.type == CKEDITOR.NODE_TEXT ) 1657 this.setEnd( node, node.getLength() ); 1658 else 1659 this.setEnd( node, node.getChildCount() ); 1660 break; 1661 1662 case CKEDITOR.POSITION_BEFORE_START : 1663 this.setEndBefore( node ); 1664 break; 1665 1666 case CKEDITOR.POSITION_AFTER_END : 1667 this.setEndAfter( node ); 1668 } 1669 1670 updateCollapsed( this ); 1671 }, 1672 1673 fixBlock : function( isStart, blockTag ) 1674 { 1675 var bookmark = this.createBookmark(), 1676 fixedBlock = this.document.createElement( blockTag ); 1677 1678 this.collapse( isStart ); 1679 1680 this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS ); 1681 1682 this.extractContents().appendTo( fixedBlock ); 1683 fixedBlock.trim(); 1684 1685 if ( !CKEDITOR.env.ie ) 1686 fixedBlock.appendBogus(); 1687 1688 this.insertNode( fixedBlock ); 1689 1690 this.moveToBookmark( bookmark ); 1691 1692 return fixedBlock; 1693 }, 1694 1695 splitBlock : function( blockTag ) 1696 { 1697 var startPath = new CKEDITOR.dom.elementPath( this.startContainer ), 1698 endPath = new CKEDITOR.dom.elementPath( this.endContainer ); 1699 1700 var startBlockLimit = startPath.blockLimit, 1701 endBlockLimit = endPath.blockLimit; 1702 1703 var startBlock = startPath.block, 1704 endBlock = endPath.block; 1705 1706 var elementPath = null; 1707 // Do nothing if the boundaries are in different block limits. 1708 if ( !startBlockLimit.equals( endBlockLimit ) ) 1709 return null; 1710 1711 // Get or fix current blocks. 1712 if ( blockTag != 'br' ) 1713 { 1714 if ( !startBlock ) 1715 { 1716 startBlock = this.fixBlock( true, blockTag ); 1717 endBlock = new CKEDITOR.dom.elementPath( this.endContainer ).block; 1718 } 1719 1720 if ( !endBlock ) 1721 endBlock = this.fixBlock( false, blockTag ); 1722 } 1723 1724 // Get the range position. 1725 var isStartOfBlock = startBlock && this.checkStartOfBlock(), 1726 isEndOfBlock = endBlock && this.checkEndOfBlock(); 1727 1728 // Delete the current contents. 1729 // TODO: Why is 2.x doing CheckIsEmpty()? 1730 this.deleteContents(); 1731 1732 if ( startBlock && startBlock.equals( endBlock ) ) 1733 { 1734 if ( isEndOfBlock ) 1735 { 1736 elementPath = new CKEDITOR.dom.elementPath( this.startContainer ); 1737 this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END ); 1738 endBlock = null; 1739 } 1740 else if ( isStartOfBlock ) 1741 { 1742 elementPath = new CKEDITOR.dom.elementPath( this.startContainer ); 1743 this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START ); 1744 startBlock = null; 1745 } 1746 else 1747 { 1748 endBlock = this.splitElement( startBlock ); 1749 1750 // In Gecko, the last child node must be a bogus <br>. 1751 // Note: bogus <br> added under <ul> or <ol> would cause 1752 // lists to be incorrectly rendered. 1753 if ( !CKEDITOR.env.ie && !startBlock.is( 'ul', 'ol') ) 1754 startBlock.appendBogus() ; 1755 } 1756 } 1757 1758 return { 1759 previousBlock : startBlock, 1760 nextBlock : endBlock, 1761 wasStartOfBlock : isStartOfBlock, 1762 wasEndOfBlock : isEndOfBlock, 1763 elementPath : elementPath 1764 }; 1765 }, 1766 1767 /** 1768 * Branch the specified element from the collapsed range position and 1769 * place the caret between the two result branches. 1770 * Note: The range must be collapsed and been enclosed by this element. 1771 * @param {CKEDITOR.dom.element} element 1772 * @return {CKEDITOR.dom.element} Root element of the new branch after the split. 1773 */ 1774 splitElement : function( toSplit ) 1775 { 1776 if ( !this.collapsed ) 1777 return null; 1778 1779 // Extract the contents of the block from the selection point to the end 1780 // of its contents. 1781 this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END ); 1782 var documentFragment = this.extractContents(); 1783 1784 // Duplicate the element after it. 1785 var clone = toSplit.clone( false ); 1786 1787 // Place the extracted contents into the duplicated element. 1788 documentFragment.appendTo( clone ); 1789 clone.insertAfter( toSplit ); 1790 this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END ); 1791 return clone; 1792 }, 1793 1794 /** 1795 * Check whether a range boundary is at the inner boundary of a given 1796 * element. 1797 * @param {CKEDITOR.dom.element} element The target element to check. 1798 * @param {Number} checkType The boundary to check for both the range 1799 * and the element. It can be CKEDITOR.START or CKEDITOR.END. 1800 * @returns {Boolean} "true" if the range boundary is at the inner 1801 * boundary of the element. 1802 */ 1803 checkBoundaryOfElement : function( element, checkType ) 1804 { 1805 var checkStart = ( checkType == CKEDITOR.START ); 1806 1807 // Create a copy of this range, so we can manipulate it for our checks. 1808 var walkerRange = this.clone(); 1809 1810 // Collapse the range at the proper size. 1811 walkerRange.collapse( checkStart ); 1812 1813 // Expand the range to element boundary. 1814 walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ] 1815 ( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END ); 1816 1817 // Create the walker, which will check if we have anything useful 1818 // in the range. 1819 var walker = new CKEDITOR.dom.walker( walkerRange ); 1820 walker.evaluator = elementBoundaryEval( checkStart ); 1821 1822 return walker[ checkStart ? 'checkBackward' : 'checkForward' ](); 1823 }, 1824 1825 // Calls to this function may produce changes to the DOM. The range may 1826 // be updated to reflect such changes. 1827 checkStartOfBlock : function() 1828 { 1829 var startContainer = this.startContainer, 1830 startOffset = this.startOffset; 1831 1832 // [IE] Special handling for range start in text with a leading NBSP, 1833 // we it to be isolated, for bogus check. 1834 if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT ) 1835 { 1836 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) ); 1837 if ( nbspRegExp.test( textBefore ) ) 1838 this.trim( 0, 1 ); 1839 } 1840 1841 // We need to grab the block element holding the start boundary, so 1842 // let's use an element path for it. 1843 var path = new CKEDITOR.dom.elementPath( this.startContainer ); 1844 1845 // Creates a range starting at the block start until the range start. 1846 var walkerRange = this.clone(); 1847 walkerRange.collapse( true ); 1848 walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START ); 1849 1850 var walker = new CKEDITOR.dom.walker( walkerRange ); 1851 walker.evaluator = getCheckStartEndBlockEvalFunction(); 1852 1853 return walker.checkBackward(); 1854 }, 1855 1856 checkEndOfBlock : function() 1857 { 1858 var endContainer = this.endContainer, 1859 endOffset = this.endOffset; 1860 1861 // [IE] Special handling for range end in text with a following NBSP, 1862 // we it to be isolated, for bogus check. 1863 if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT ) 1864 { 1865 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) ); 1866 if ( nbspRegExp.test( textAfter ) ) 1867 this.trim( 1, 0 ); 1868 } 1869 1870 // We need to grab the block element holding the start boundary, so 1871 // let's use an element path for it. 1872 var path = new CKEDITOR.dom.elementPath( this.endContainer ); 1873 1874 // Creates a range starting at the block start until the range start. 1875 var walkerRange = this.clone(); 1876 walkerRange.collapse( false ); 1877 walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END ); 1878 1879 var walker = new CKEDITOR.dom.walker( walkerRange ); 1880 walker.evaluator = getCheckStartEndBlockEvalFunction(); 1881 1882 return walker.checkForward(); 1883 }, 1884 1885 /** 1886 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start. 1887 * @param {Function} evaluator Function used as the walker's evaluator. 1888 * @param {Function} [guard] Function used as the walker's guard. 1889 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited, 1890 * default to the root editable if not defined. 1891 * 1892 * @return {CKEDITOR.dom.element|null} The returned node from the traversal. 1893 */ 1894 getPreviousNode : function( evaluator, guard, boundary ) { 1895 1896 var walkerRange = this.clone(); 1897 walkerRange.collapse( 1 ); 1898 walkerRange.setStartAt( boundary || this.document.getBody(), CKEDITOR.POSITION_AFTER_START ); 1899 1900 var walker = new CKEDITOR.dom.walker( walkerRange ); 1901 walker.evaluator = evaluator; 1902 walker.guard = guard; 1903 return walker.previous(); 1904 }, 1905 1906 /** 1907 * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start. 1908 * @param {Function} evaluator Function used as the walker's evaluator. 1909 * @param {Function} [guard] Function used as the walker's guard. 1910 * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited, 1911 * default to the root editable if not defined. 1912 * 1913 * @return {CKEDITOR.dom.element|null} The returned node from the traversal. 1914 */ 1915 getNextNode: function( evaluator, guard, boundary ) 1916 { 1917 var walkerRange = this.clone(); 1918 walkerRange.collapse(); 1919 walkerRange.setEndAt( boundary || this.document.getBody(), CKEDITOR.POSITION_BEFORE_END ); 1920 1921 var walker = new CKEDITOR.dom.walker( walkerRange ); 1922 walker.evaluator = evaluator; 1923 walker.guard = guard; 1924 return walker.next(); 1925 }, 1926 1927 checkReadOnly : ( function() 1928 { 1929 function checkNodesEditable( node, anotherEnd ) 1930 { 1931 while( node ) 1932 { 1933 if ( node.type == CKEDITOR.NODE_ELEMENT ) 1934 { 1935 if ( node.getAttribute( 'contentEditable' ) == 'false' 1936 && !node.data( 'cke-editable' ) ) 1937 { 1938 return 0; 1939 } 1940 // Range enclosed entirely in an editable element. 1941 else if ( node.is( 'html' ) 1942 || node.getAttribute( 'contentEditable' ) == 'true' 1943 && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) ) 1944 { 1945 break; 1946 } 1947 } 1948 node = node.getParent(); 1949 } 1950 1951 return 1; 1952 } 1953 1954 return function() 1955 { 1956 var startNode = this.startContainer, 1957 endNode = this.endContainer; 1958 1959 // Check if elements path at both boundaries are editable. 1960 return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) ); 1961 }; 1962 })(), 1963 1964 /** 1965 * Moves the range boundaries to the first/end editing point inside an 1966 * element. For example, in an element tree like 1967 * "<p><b><i></i></b> Text</p>", the start editing point is 1968 * "<p><b><i>^</i></b> Text</p>" (inside <i>). 1969 * @param {CKEDITOR.dom.element} el The element into which look for the 1970 * editing spot. 1971 * @param {Boolean} isMoveToEnd Whether move to the end editable position. 1972 */ 1973 moveToElementEditablePosition : function( el, isMoveToEnd ) 1974 { 1975 function nextDFS( node, childOnly ) 1976 { 1977 var next; 1978 1979 if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) ) 1980 next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( nonWhitespaceOrBookmarkEval ); 1981 1982 if ( !childOnly && !next ) 1983 next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( nonWhitespaceOrBookmarkEval ); 1984 1985 return next; 1986 } 1987 1988 // Handle non-editable element e.g. HR. 1989 if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) ) 1990 { 1991 this.moveToPosition( el, isMoveToEnd ? 1992 CKEDITOR.POSITION_AFTER_END : 1993 CKEDITOR.POSITION_BEFORE_START ); 1994 return true; 1995 } 1996 1997 var found = 0; 1998 1999 while ( el ) 2000 { 2001 // Stop immediately if we've found a text node. 2002 if ( el.type == CKEDITOR.NODE_TEXT ) 2003 { 2004 // Put cursor before block filler. 2005 if ( isMoveToEnd && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) ) 2006 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START ); 2007 else 2008 this.moveToPosition( el, isMoveToEnd ? 2009 CKEDITOR.POSITION_AFTER_END : 2010 CKEDITOR.POSITION_BEFORE_START ); 2011 found = 1; 2012 break; 2013 } 2014 2015 // If an editable element is found, move inside it, but not stop the searching. 2016 if ( el.type == CKEDITOR.NODE_ELEMENT ) 2017 { 2018 if ( el.isEditable() ) 2019 { 2020 this.moveToPosition( el, isMoveToEnd ? 2021 CKEDITOR.POSITION_BEFORE_END : 2022 CKEDITOR.POSITION_AFTER_START ); 2023 found = 1; 2024 } 2025 // Put cursor before padding block br. 2026 else if ( isMoveToEnd && el.is( 'br' ) && this.checkEndOfBlock() ) 2027 this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START ); 2028 } 2029 2030 el = nextDFS( el, found ); 2031 } 2032 2033 return !!found; 2034 }, 2035 2036 /** 2037 *@see {CKEDITOR.dom.range.moveToElementEditablePosition} 2038 */ 2039 moveToElementEditStart : function( target ) 2040 { 2041 return this.moveToElementEditablePosition( target ); 2042 }, 2043 2044 /** 2045 *@see {CKEDITOR.dom.range.moveToElementEditablePosition} 2046 */ 2047 moveToElementEditEnd : function( target ) 2048 { 2049 return this.moveToElementEditablePosition( target, true ); 2050 }, 2051 2052 /** 2053 * Get the single node enclosed within the range if there's one. 2054 */ 2055 getEnclosedNode : function() 2056 { 2057 var walkerRange = this.clone(); 2058 2059 // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780) 2060 walkerRange.optimize(); 2061 if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT 2062 || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT ) 2063 return null; 2064 2065 var walker = new CKEDITOR.dom.walker( walkerRange ), 2066 isNotBookmarks = CKEDITOR.dom.walker.bookmark( true ), 2067 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ), 2068 evaluator = function( node ) 2069 { 2070 return isNotWhitespaces( node ) && isNotBookmarks( node ); 2071 }; 2072 walkerRange.evaluator = evaluator; 2073 var node = walker.next(); 2074 walker.reset(); 2075 return node && node.equals( walker.previous() ) ? node : null; 2076 }, 2077 2078 getTouchedStartNode : function() 2079 { 2080 var container = this.startContainer ; 2081 2082 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT ) 2083 return container ; 2084 2085 return container.getChild( this.startOffset ) || container ; 2086 }, 2087 2088 getTouchedEndNode : function() 2089 { 2090 var container = this.endContainer ; 2091 2092 if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT ) 2093 return container ; 2094 2095 return container.getChild( this.endOffset - 1 ) || container ; 2096 } 2097 }; 2098 })(); 2099 2100 CKEDITOR.POSITION_AFTER_START = 1; // <element>^contents</element> "^text" 2101 CKEDITOR.POSITION_BEFORE_END = 2; // <element>contents^</element> "text^" 2102 CKEDITOR.POSITION_BEFORE_START = 3; // ^<element>contents</element> ^"text" 2103 CKEDITOR.POSITION_AFTER_END = 4; // <element>contents</element>^ "text" 2104 2105 CKEDITOR.ENLARGE_ELEMENT = 1; 2106 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2; 2107 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3; 2108 2109 // Check boundary types. 2110 // @see CKEDITOR.dom.range.prototype.checkBoundaryOfElement 2111 CKEDITOR.START = 1; 2112 CKEDITOR.END = 2; 2113 CKEDITOR.STARTEND = 3; 2114 2115 // Shrink range types. 2116 // @see CKEDITOR.dom.range.prototype.shrink 2117 CKEDITOR.SHRINK_ELEMENT = 1; 2118 CKEDITOR.SHRINK_TEXT = 2; 2119