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 (function() 7 { 8 // #### checkSelectionChange : START 9 10 // The selection change check basically saves the element parent tree of 11 // the current node and check it on successive requests. If there is any 12 // change on the tree, then the selectionChange event gets fired. 13 var checkSelectionChange = function() 14 { 15 // In IE, the "selectionchange" event may still get thrown when 16 // releasing the WYSIWYG mode, so we need to check it first. 17 var sel = this.getSelection(); 18 if ( !sel ) 19 return; 20 21 var firstElement = sel.getStartElement(); 22 var currentPath = new CKEDITOR.dom.elementPath( firstElement ); 23 24 if ( !currentPath.compare( this._.selectionPreviousPath ) ) 25 { 26 this._.selectionPreviousPath = currentPath; 27 this.fire( 'selectionChange', { selection : sel, path : currentPath, element : firstElement } ); 28 } 29 }; 30 31 var checkSelectionChangeTimer; 32 var checkSelectionChangeTimeoutPending; 33 var checkSelectionChangeTimeout = function() 34 { 35 // Firing the "OnSelectionChange" event on every key press started to 36 // be too slow. This function guarantees that there will be at least 37 // 200ms delay between selection checks. 38 39 checkSelectionChangeTimeoutPending = true; 40 41 if ( checkSelectionChangeTimer ) 42 return; 43 44 checkSelectionChangeTimeoutExec.call( this ); 45 46 checkSelectionChangeTimer = CKEDITOR.tools.setTimeout( checkSelectionChangeTimeoutExec, 200, this ); 47 }; 48 49 var checkSelectionChangeTimeoutExec = function() 50 { 51 checkSelectionChangeTimer = null; 52 53 if ( checkSelectionChangeTimeoutPending ) 54 { 55 // Call this with a timeout so the browser properly moves the 56 // selection after the mouseup. It happened that the selection was 57 // being moved after the mouseup when clicking inside selected text 58 // with Firefox. 59 CKEDITOR.tools.setTimeout( checkSelectionChange, 0, this ); 60 61 checkSelectionChangeTimeoutPending = false; 62 } 63 }; 64 65 // #### checkSelectionChange : END 66 67 var selectAllCmd = 68 { 69 exec : function( editor ) 70 { 71 switch ( editor.mode ) 72 { 73 case 'wysiwyg' : 74 editor.document.$.execCommand( 'SelectAll', false, null ); 75 break; 76 case 'source' : 77 // TODO 78 } 79 } 80 }; 81 82 CKEDITOR.plugins.add( 'selection', 83 { 84 init : function( editor ) 85 { 86 editor.on( 'contentDom', function() 87 { 88 if ( CKEDITOR.env.ie ) 89 { 90 // IE is the only to provide the "selectionchange" 91 // event. 92 editor.document.on( 'selectionchange', checkSelectionChangeTimeout, editor ); 93 } 94 else 95 { 96 // In other browsers, we make the selection change 97 // check based on other events, like clicks or keys 98 // press. 99 100 editor.document.on( 'mouseup', checkSelectionChangeTimeout, editor ); 101 editor.document.on( 'keyup', checkSelectionChangeTimeout, editor ); 102 } 103 }); 104 105 editor.addCommand( 'selectAll', selectAllCmd ); 106 editor.ui.addButton( 'SelectAll', 107 { 108 label : editor.lang.selectAll, 109 command : 'selectAll' 110 }); 111 112 editor.selectionChange = checkSelectionChangeTimeout; 113 } 114 }); 115 116 /** 117 * Gets the current selection from the editing area when in WYSIWYG mode. 118 * @returns {CKEDITOR.dom.selection} A selection object or null if not on 119 * WYSIWYG mode or no selection is available. 120 * @example 121 * var selection = CKEDITOR.instances.editor1.<b>getSelection()</b>; 122 * alert( selection.getType() ); 123 */ 124 CKEDITOR.editor.prototype.getSelection = function() 125 { 126 var retval = this.document ? this.document.getSelection() : null; 127 128 /** 129 * IE BUG: The selection's document may be a different document than the 130 * editor document. Return null if that's the case. 131 */ 132 if ( retval && CKEDITOR.env.ie ) 133 { 134 var range = retval.getNative().createRange(); 135 if ( !range ) 136 return null; 137 else if ( range.item ) 138 return range.item(0).ownerDocument == this.document.$ ? retval : null; 139 else 140 return range.parentElement().ownerDocument == this.document.$ ? retval : null; 141 } 142 143 return retval; 144 }; 145 146 CKEDITOR.editor.prototype.forceNextSelectionCheck = function() 147 { 148 delete this._.selectionPreviousPath; 149 }; 150 151 /** 152 * Gets the current selection from the document. 153 * @returns {CKEDITOR.dom.selection} A selection object. 154 * @example 155 * var selection = CKEDITOR.instances.editor1.document.<b>getSelection()</b>; 156 * alert( selection.getType() ); 157 */ 158 CKEDITOR.dom.document.prototype.getSelection = function() 159 { 160 return new CKEDITOR.dom.selection( this ); 161 }; 162 163 /** 164 * No selection. 165 * @constant 166 * @example 167 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_NONE ) 168 * alert( 'Nothing is selected' ); 169 */ 170 CKEDITOR.SELECTION_NONE = 1; 171 172 /** 173 * Text or collapsed selection. 174 * @constant 175 * @example 176 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT ) 177 * alert( 'Text is selected' ); 178 */ 179 CKEDITOR.SELECTION_TEXT = 2; 180 181 /** 182 * Element selection. 183 * @constant 184 * @example 185 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_ELEMENT ) 186 * alert( 'An element is selected' ); 187 */ 188 CKEDITOR.SELECTION_ELEMENT = 3; 189 190 /** 191 * Manipulates the selection in a DOM document. 192 * @constructor 193 * @example 194 */ 195 CKEDITOR.dom.selection = function( document ) 196 { 197 this.document = document; 198 this._ = 199 { 200 cache : {} 201 }; 202 }; 203 204 var styleObjectElements = { img:1,hr:1,li:1,table:1,tr:1,td:1,embed:1,object:1,ol:1,ul:1 }; 205 206 CKEDITOR.dom.selection.prototype = 207 { 208 /** 209 * Gets the native selection object from the browser. 210 * @function 211 * @returns {Object} The native selection object. 212 * @example 213 * var selection = editor.getSelection().<b>getNative()</b>; 214 */ 215 getNative : 216 CKEDITOR.env.ie ? 217 function() 218 { 219 return this._.cache.nativeSel || ( this._.cache.nativeSel = this.document.$.selection ); 220 } 221 : 222 function() 223 { 224 return this._.cache.nativeSel || ( this._.cache.nativeSel = this.document.getWindow().$.getSelection() ); 225 }, 226 227 /** 228 * Gets the type of the current selection. The following values are 229 * available: 230 * <ul> 231 * <li>{@link CKEDITOR.SELECTION_NONE} (1): No selection.</li> 232 * <li>{@link CKEDITOR.SELECTION_TEXT} (2): Text is selected or 233 * collapsed selection.</li> 234 * <li>{@link CKEDITOR.SELECTION_ELEMENT} (3): A element 235 * selection.</li> 236 * </ul> 237 * @function 238 * @returns {Number} One of the following constant values: 239 * {@link CKEDITOR.SELECTION_NONE}, {@link CKEDITOR.SELECTION_TEXT} or 240 * {@link CKEDITOR.SELECTION_ELEMENT}. 241 * @example 242 * if ( editor.getSelection().<b>getType()</b> == CKEDITOR.SELECTION_TEXT ) 243 * alert( 'Text is selected' ); 244 */ 245 getType : 246 CKEDITOR.env.ie ? 247 function() 248 { 249 if ( this._.cache.type ) 250 return this._.cache.type; 251 252 var type = CKEDITOR.SELECTION_NONE; 253 254 try 255 { 256 var sel = this.getNative(), 257 ieType = sel.type; 258 259 if ( ieType == 'Text' ) 260 type = CKEDITOR.SELECTION_TEXT; 261 262 if ( ieType == 'Control' ) 263 type = CKEDITOR.SELECTION_ELEMENT; 264 265 // It is possible that we can still get a text range 266 // object even when type == 'None' is returned by IE. 267 // So we'd better check the object returned by 268 // createRange() rather than by looking at the type. 269 if ( sel.createRange().parentElement ) 270 type = CKEDITOR.SELECTION_TEXT; 271 } 272 catch(e) {} 273 274 return ( this._.cache.type = type ); 275 } 276 : 277 function() 278 { 279 if ( this._.cache.type ) 280 return this._.cache.type; 281 282 var type = CKEDITOR.SELECTION_TEXT; 283 284 var sel = this.getNative(); 285 286 if ( !sel ) 287 type = CKEDITOR.SELECTION_NONE; 288 else if ( sel.rangeCount == 1 ) 289 { 290 // Check if the actual selection is a control (IMG, 291 // TABLE, HR, etc...). 292 293 var range = sel.getRangeAt(0), 294 startContainer = range.startContainer; 295 296 if ( startContainer == range.endContainer 297 && startContainer.nodeType == 1 298 && ( range.endOffset - range.startOffset ) == 1 299 && styleObjectElements[ startContainer.childNodes[ range.startOffset ].nodeName.toLowerCase() ] ) 300 { 301 type = CKEDITOR.SELECTION_ELEMENT; 302 } 303 } 304 305 return ( this._.cache.type = type ); 306 }, 307 308 getRanges : 309 CKEDITOR.env.ie ? 310 ( function() 311 { 312 // Finds the container and offset for a specific boundary 313 // of an IE range. 314 var getBoundaryInformation = function( range, start ) 315 { 316 // Creates a collapsed range at the requested boundary. 317 range = range.duplicate(); 318 range.collapse( start ); 319 320 // Gets the element that encloses the range entirely. 321 var parent = range.parentElement(); 322 var siblings = parent.childNodes; 323 324 var testRange; 325 326 for ( var i = 0 ; i < siblings.length ; i++ ) 327 { 328 var child = siblings[ i ]; 329 if ( child.nodeType == 1 ) 330 { 331 testRange = range.duplicate(); 332 333 testRange.moveToElementText( child ); 334 testRange.collapse(); 335 336 var comparison = testRange.compareEndPoints( 'StartToStart', range ); 337 338 if ( comparison > 0 ) 339 break; 340 else if ( comparison === 0 ) 341 return { 342 container : parent, 343 offset : i 344 }; 345 346 testRange = null; 347 } 348 } 349 350 if ( !testRange ) 351 { 352 testRange = range.duplicate(); 353 testRange.moveToElementText( parent ); 354 testRange.collapse( false ); 355 } 356 357 testRange.setEndPoint( 'StartToStart', range ); 358 var distance = testRange.text.length; 359 360 while ( distance > 0 ) 361 distance -= siblings[ --i ].nodeValue.length; 362 363 if ( distance === 0 ) 364 { 365 return { 366 container : parent, 367 offset : i 368 }; 369 } 370 else 371 { 372 return { 373 container : siblings[ i ], 374 offset : -distance 375 }; 376 } 377 }; 378 379 return function() 380 { 381 if ( this._.cache.ranges ) 382 return this._.cache.ranges; 383 384 // IE doesn't have range support (in the W3C way), so we 385 // need to do some magic to transform selections into 386 // CKEDITOR.dom.range instances. 387 388 var sel = this.getNative(), 389 nativeRange = sel.createRange(), 390 type = this.getType(), 391 range; 392 393 if ( type == CKEDITOR.SELECTION_TEXT ) 394 { 395 range = new CKEDITOR.dom.range( this.document ); 396 397 var boundaryInfo = getBoundaryInformation( nativeRange, true ); 398 range.setStart( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset ); 399 400 boundaryInfo = getBoundaryInformation( nativeRange ); 401 range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset ); 402 403 return ( this._.cache.ranges = [ range ] ); 404 } 405 else if ( type == CKEDITOR.SELECTION_ELEMENT ) 406 { 407 var retval = this._.cache.ranges = []; 408 409 for ( var i = 0 ; i < nativeRange.length ; i++ ) 410 { 411 var element = nativeRange.item( i ), 412 parentElement = element.parentNode, 413 j = 0; 414 415 range = new CKEDITOR.dom.range( this.document ); 416 417 for (; j < parentElement.childNodes.length && parentElement.childNodes[j] != element ; j++ ) 418 { /*jsl:pass*/ } 419 420 range.setStart( new CKEDITOR.dom.node( parentElement ), j ); 421 range.setEnd( new CKEDITOR.dom.node( parentElement ), j + 1 ); 422 retval.push( range ); 423 } 424 425 return retval; 426 } 427 428 return ( this._.cache.ranges = [] ); 429 }; 430 })() 431 : 432 function() 433 { 434 if ( this._.cache.ranges ) 435 return this._.cache.ranges; 436 437 // On browsers implementing the W3C range, we simply 438 // tranform the native ranges in CKEDITOR.dom.range 439 // instances. 440 441 var ranges = []; 442 var sel = this.getNative(); 443 444 for ( var i = 0 ; i < sel.rangeCount ; i++ ) 445 { 446 var nativeRange = sel.getRangeAt( i ); 447 var range = new CKEDITOR.dom.range( this.document ); 448 449 range.setStart( new CKEDITOR.dom.node( nativeRange.startContainer ), nativeRange.startOffset ); 450 range.setEnd( new CKEDITOR.dom.node( nativeRange.endContainer ), nativeRange.endOffset ); 451 ranges.push( range ); 452 } 453 454 return ( this._.cache.ranges = ranges ); 455 }, 456 457 /** 458 * Gets the DOM element in which the selection starts. 459 * @returns {CKEDITOR.dom.element} The element at the beginning of the 460 * selection. 461 * @example 462 * var element = editor.getSelection().<b>getStartElement()</b>; 463 * alert( element.getName() ); 464 */ 465 getStartElement : function() 466 { 467 var node, 468 sel = this.getNative(); 469 470 switch ( this.getType() ) 471 { 472 case CKEDITOR.SELECTION_ELEMENT : 473 return this.getSelectedElement(); 474 475 case CKEDITOR.SELECTION_TEXT : 476 477 var range = this.getRanges()[0]; 478 479 if ( range ) 480 { 481 if ( !range.collapsed ) 482 { 483 range.optimize(); 484 485 node = range.startContainer; 486 487 if ( node.type != CKEDITOR.NODE_ELEMENT ) 488 return node.getParent(); 489 490 node = node.getChild( range.startOffset ); 491 492 if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) 493 return range.startContainer; 494 495 var child = node.getFirst(); 496 while ( child && child.type == CKEDITOR.NODE_ELEMENT ) 497 { 498 node = child; 499 child = child.getFirst(); 500 } 501 502 return node; 503 } 504 } 505 506 if ( CKEDITOR.env.ie ) 507 { 508 range = sel.createRange(); 509 range.collapse( true ); 510 511 node = range.parentElement(); 512 } 513 else 514 { 515 node = sel.anchorNode; 516 517 if ( node.nodeType != 1 ) 518 node = node.parentNode; 519 } 520 } 521 522 return ( node ? new CKEDITOR.dom.element( node ) : null ); 523 }, 524 525 /** 526 * Gets the current selected element. 527 * @returns {CKEDITOR.dom.element} The selected element. Null if no 528 * selection is available or the selection type is not 529 * {@link CKEDITOR.SELECTION_ELEMENT}. 530 * @example 531 * var element = editor.getSelection().<b>getSelectedElement()</b>; 532 * alert( element.getName() ); 533 */ 534 getSelectedElement : function() 535 { 536 var node; 537 538 if ( this.getType() == CKEDITOR.SELECTION_ELEMENT ) 539 { 540 var sel = this.getNative(); 541 542 if ( CKEDITOR.env.ie ) 543 { 544 try 545 { 546 node = sel.createRange().item(0); 547 } 548 catch(e) {} 549 } 550 else 551 { 552 var range = sel.getRangeAt( 0 ); 553 node = range.startContainer.childNodes[ range.startOffset ]; 554 } 555 } 556 557 return ( node ? new CKEDITOR.dom.element( node ) : null ); 558 }, 559 560 reset : function() 561 { 562 this._.cache = {}; 563 }, 564 565 selectElement : 566 CKEDITOR.env.ie ? 567 function( element ) 568 { 569 this.getNative().empty(); 570 571 var range; 572 try 573 { 574 // Try to select the node as a control. 575 range = this.document.$.body.createControlRange(); 576 range.addElement( element.$ ); 577 } 578 catch(e) 579 { 580 // If failed, select it as a text range. 581 range = this.document.$.body.createTextRange(); 582 range.moveToElementText( element.$ ); 583 } 584 585 range.select(); 586 } 587 : 588 function( element ) 589 { 590 // Create the range for the element. 591 var range = this.document.$.createRange(); 592 range.selectNode( element.$ ); 593 594 // Select the range. 595 var sel = this.getNative(); 596 sel.removeAllRanges(); 597 sel.addRange( range ); 598 }, 599 600 selectRanges : 601 CKEDITOR.env.ie ? 602 function( ranges ) 603 { 604 // IE doesn't accept multiple ranges selection, so we just 605 // select the first one. 606 if ( ranges[ 0 ] ) 607 ranges[ 0 ].select(); 608 } 609 : 610 function( ranges ) 611 { 612 var sel = this.getNative(); 613 sel.removeAllRanges(); 614 615 for ( var i = 0 ; i < ranges.length ; i++ ) 616 { 617 var range = ranges[ i ]; 618 var nativeRange = this.document.$.createRange(); 619 nativeRange.setStart( range.startContainer.$, range.startOffset ); 620 nativeRange.setEnd( range.endContainer.$, range.endOffset ); 621 622 // Select the range. 623 sel.addRange( nativeRange ); 624 } 625 }, 626 627 createBookmarks : function( serializable ) 628 { 629 var retval = [], 630 ranges = this.getRanges(); 631 for ( var i = 0 ; i < ranges.length ; i++ ) 632 retval.push( ranges[i].createBookmark( serializable ) ); 633 return retval; 634 }, 635 636 createBookmarks2 : function( normalized ) 637 { 638 var bookmarks = [], 639 ranges = this.getRanges(); 640 641 for ( var i = 0 ; i < ranges.length ; i++ ) 642 bookmarks.push( ranges[i].createBookmark2( normalized ) ); 643 644 return bookmarks; 645 }, 646 647 selectBookmarks : function( bookmarks ) 648 { 649 var ranges = []; 650 for ( var i = 0 ; i < bookmarks.length ; i++ ) 651 { 652 var range = new CKEDITOR.dom.range( this.document ); 653 range.moveToBookmark( bookmarks[i] ); 654 ranges.push( range ); 655 } 656 this.selectRanges( ranges ); 657 return this; 658 } 659 }; 660 })(); 661 662 CKEDITOR.dom.range.prototype.select = 663 CKEDITOR.env.ie ? 664 // V2 665 function() 666 { 667 var collapsed = this.collapsed; 668 var isStartMakerAlone; 669 var dummySpan; 670 671 var bookmark = this.createBookmark(); 672 673 // Create marker tags for the start and end boundaries. 674 var startNode = bookmark.startNode; 675 676 var endNode; 677 if ( !collapsed ) 678 endNode = bookmark.endNode; 679 680 // Create the main range which will be used for the selection. 681 var ieRange = this.document.$.body.createTextRange(); 682 683 // Position the range at the start boundary. 684 ieRange.moveToElementText( startNode.$ ); 685 ieRange.moveStart( 'character', 1 ); 686 687 if ( endNode ) 688 { 689 // Create a tool range for the end. 690 var ieRangeEnd = this.document.$.body.createTextRange(); 691 692 // Position the tool range at the end. 693 ieRangeEnd.moveToElementText( endNode.$ ); 694 695 // Move the end boundary of the main range to match the tool range. 696 ieRange.setEndPoint( 'EndToEnd', ieRangeEnd ); 697 ieRange.moveEnd( 'character', -1 ); 698 } 699 else 700 { 701 // The isStartMakerAlone logic comes from V2. It guarantees that the lines 702 // will expand and that the cursor will be blinking on the right place. 703 // Actually, we are using this flag just to avoid using this hack in all 704 // situations, but just on those needed. 705 706 // But, in V3, somehow it is not interested on working whe hitting SHIFT+ENTER 707 // inside text. So, let's jsut leave the hack happen always. 708 709 // I'm still leaving the code here just in case. We may find some other IE 710 // weirdness and uncommenting this stuff may be useful. 711 712 // isStartMakerAlone = ( !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ) ) 713 // && !startNode.hasNext(); 714 715 // Append a temporary <span></span> before the selection. 716 // This is needed to avoid IE destroying selections inside empty 717 // inline elements, like <b></b> (#253). 718 // It is also needed when placing the selection right after an inline 719 // element to avoid the selection moving inside of it. 720 dummySpan = this.document.createElement( 'span' ); 721 dummySpan.setHtml( '' ); // Zero Width No-Break Space (U+FEFF). See #1359. 722 dummySpan.insertBefore( startNode ); 723 724 // if ( isStartMakerAlone ) 725 // { 726 // To expand empty blocks or line spaces after <br>, we need 727 // instead to have any char, which will be later deleted using the 728 // selection. 729 // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359) 730 this.document.createText( '\ufeff' ).insertBefore( startNode ); 731 // } 732 } 733 734 // Remove the markers (reset the position, because of the changes in the DOM tree). 735 this.setStartBefore( startNode ); 736 startNode.remove(); 737 738 if ( collapsed ) 739 { 740 // if ( isStartMakerAlone ) 741 // { 742 // Move the selection start to include the temporary \ufeff. 743 ieRange.moveStart( 'character', -1 ); 744 745 ieRange.select(); 746 747 // Remove our temporary stuff. 748 this.document.$.selection.clear(); 749 // } 750 // else 751 // ieRange.select(); 752 753 dummySpan.remove(); 754 } 755 else 756 { 757 this.setEndBefore( endNode ); 758 endNode.remove(); 759 ieRange.select(); 760 } 761 } 762 : 763 function() 764 { 765 var startContainer = this.startContainer; 766 767 // If we have a collapsed range, inside an empty element, we must add 768 // something to it, otherwise the caret will not be visible. 769 if ( this.collapsed && startContainer.type == CKEDITOR.NODE_ELEMENT && !startContainer.getChildCount() ) 770 startContainer.append( new CKEDITOR.dom.text( '' ) ); 771 772 var nativeRange = this.document.$.createRange(); 773 nativeRange.setStart( startContainer.$, this.startOffset ); 774 775 try 776 { 777 nativeRange.setEnd( this.endContainer.$, this.endOffset ); 778 } 779 catch ( e ) 780 { 781 // There is a bug in Firefox implementation (it would be too easy 782 // otherwise). The new start can't be after the end (W3C says it can). 783 // So, let's create a new range and collapse it to the desired point. 784 if ( e.toString().indexOf( 'NS_ERROR_ILLEGAL_VALUE' ) >= 0 ) 785 { 786 this.collapse( true ); 787 nativeRange.setEnd( this.endContainer.$, this.endOffset ); 788 } 789 else 790 throw( e ); 791 } 792 793 var selection = this.document.getSelection().getNative(); 794 selection.removeAllRanges(); 795 selection.addRange( nativeRange ); 796 }; 797