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