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 (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 function checkSelectionChange() 14 { 15 try 16 { 17 // In IE, the "selectionchange" event may still get thrown when 18 // releasing the WYSIWYG mode, so we need to check it first. 19 var sel = this.getSelection(); 20 if ( !sel || !sel.document.getWindow().$ ) 21 return; 22 23 var firstElement = sel.getStartElement(); 24 var currentPath = new CKEDITOR.dom.elementPath( firstElement ); 25 26 if ( !currentPath.compare( this._.selectionPreviousPath ) ) 27 { 28 this._.selectionPreviousPath = currentPath; 29 this.fire( 'selectionChange', { selection : sel, path : currentPath, element : firstElement } ); 30 } 31 } 32 catch (e) 33 {} 34 } 35 36 var checkSelectionChangeTimer, 37 checkSelectionChangeTimeoutPending; 38 39 function checkSelectionChangeTimeout() 40 { 41 // Firing the "OnSelectionChange" event on every key press started to 42 // be too slow. This function guarantees that there will be at least 43 // 200ms delay between selection checks. 44 45 checkSelectionChangeTimeoutPending = true; 46 47 if ( checkSelectionChangeTimer ) 48 return; 49 50 checkSelectionChangeTimeoutExec.call( this ); 51 52 checkSelectionChangeTimer = CKEDITOR.tools.setTimeout( checkSelectionChangeTimeoutExec, 200, this ); 53 } 54 55 function checkSelectionChangeTimeoutExec() 56 { 57 checkSelectionChangeTimer = null; 58 59 if ( checkSelectionChangeTimeoutPending ) 60 { 61 // Call this with a timeout so the browser properly moves the 62 // selection after the mouseup. It happened that the selection was 63 // being moved after the mouseup when clicking inside selected text 64 // with Firefox. 65 CKEDITOR.tools.setTimeout( checkSelectionChange, 0, this ); 66 67 checkSelectionChangeTimeoutPending = false; 68 } 69 } 70 71 // #### checkSelectionChange : END 72 73 function rangeRequiresFix( range ) 74 { 75 function isTextCt( node, isAtEnd ) 76 { 77 if ( !node || node.type == CKEDITOR.NODE_TEXT ) 78 return false; 79 80 var testRng = range.clone(); 81 return testRng[ 'moveToElementEdit' + ( isAtEnd ? 'End' : 'Start' ) ]( node ); 82 } 83 84 var ct = range.startContainer; 85 86 var previous = range.getPreviousNode( isVisible, null, ct ), 87 next = range.getNextNode( isVisible, null, ct ); 88 89 // Any adjacent text container may absorb the cursor, e.g. 90 // <p><strong>text</strong>^foo</p> 91 // <p>foo^<strong>text</strong></p> 92 // <div>^<p>foo</p></div> 93 if ( isTextCt( previous ) || isTextCt( next, 1 ) ) 94 return true; 95 96 // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (#7222) 97 if ( !( previous || next ) && !( ct.type == CKEDITOR.NODE_ELEMENT && ct.isBlockBoundary() && ct.getBogus() ) ) 98 return true; 99 100 return false; 101 } 102 103 var selectAllCmd = 104 { 105 modes : { wysiwyg : 1, source : 1 }, 106 readOnly : CKEDITOR.env.ie || CKEDITOR.env.webkit, 107 exec : function( editor ) 108 { 109 switch ( editor.mode ) 110 { 111 case 'wysiwyg' : 112 editor.document.$.execCommand( 'SelectAll', false, null ); 113 // Force triggering selectionChange (#7008) 114 editor.forceNextSelectionCheck(); 115 editor.selectionChange(); 116 break; 117 case 'source' : 118 // Select the contents of the textarea 119 var textarea = editor.textarea.$; 120 if ( CKEDITOR.env.ie ) 121 textarea.createTextRange().execCommand( 'SelectAll' ); 122 else 123 { 124 textarea.selectionStart = 0; 125 textarea.selectionEnd = textarea.value.length; 126 } 127 textarea.focus(); 128 } 129 }, 130 canUndo : false 131 }; 132 133 function createFillingChar( doc ) 134 { 135 removeFillingChar( doc ); 136 137 var fillingChar = doc.createText( '\u200B' ); 138 doc.setCustomData( 'cke-fillingChar', fillingChar ); 139 140 return fillingChar; 141 } 142 143 function getFillingChar( doc ) 144 { 145 return doc && doc.getCustomData( 'cke-fillingChar' ); 146 } 147 148 // Checks if a filling char has been used, eventualy removing it (#1272). 149 function checkFillingChar( doc ) 150 { 151 var fillingChar = doc && getFillingChar( doc ); 152 if ( fillingChar ) 153 { 154 // Use this flag to avoid removing the filling char right after 155 // creating it. 156 if ( fillingChar.getCustomData( 'ready' ) ) 157 removeFillingChar( doc ); 158 else 159 fillingChar.setCustomData( 'ready', 1 ); 160 } 161 } 162 163 function removeFillingChar( doc ) 164 { 165 var fillingChar = doc && doc.removeCustomData( 'cke-fillingChar' ); 166 if ( fillingChar ) 167 { 168 var bm, 169 sel = doc.getSelection().getNative(), 170 // Be error proof. 171 range = sel && sel.type != 'None' && sel.getRangeAt( 0 ); 172 173 // Text selection position might get mangled by 174 // subsequent dom modification, save it now for restoring. (#8617) 175 if ( fillingChar.getLength() > 1 176 && range && range.intersectsNode( fillingChar.$ ) ) 177 { 178 bm = [ sel.anchorOffset, sel.focusOffset ]; 179 180 // Anticipate the offset change brought by the removed char. 181 var startAffected = sel.anchorNode == fillingChar.$ && sel.anchorOffset > 0, 182 endAffected = sel.focusNode == fillingChar.$ && sel.focusOffset > 0; 183 startAffected && bm[ 0 ]--; 184 endAffected && bm[ 1 ]--; 185 186 // Revert the bookmark order on reverse selection. 187 isReversedSelection( sel ) && bm.unshift( bm.pop() ); 188 } 189 190 // We can't simply remove the filling node because the user 191 // will actually enlarge it when typing, so we just remove the 192 // invisible char from it. 193 fillingChar.setText( fillingChar.getText().replace( /\u200B/g, '' ) ); 194 195 // Restore the bookmark. 196 if ( bm ) 197 { 198 var rng = sel.getRangeAt( 0 ); 199 rng.setStart( rng.startContainer, bm[ 0 ] ); 200 rng.setEnd( rng.startContainer, bm[ 1 ] ); 201 sel.removeAllRanges(); 202 sel.addRange( rng ); 203 } 204 } 205 } 206 207 function isReversedSelection( sel ) 208 { 209 if ( !sel.isCollapsed ) 210 { 211 var range = sel.getRangeAt( 0 ); 212 // Potentially alter an reversed selection range. 213 range.setStart( sel.anchorNode, sel.anchorOffset ); 214 range.setEnd( sel.focusNode, sel.focusOffset ); 215 return range.collapsed; 216 } 217 } 218 219 CKEDITOR.plugins.add( 'selection', 220 { 221 init : function( editor ) 222 { 223 // On WebKit only, we need a special "filling" char on some situations 224 // (#1272). Here we set the events that should invalidate that char. 225 if ( CKEDITOR.env.webkit ) 226 { 227 editor.on( 'selectionChange', function() { checkFillingChar( editor.document ); } ); 228 editor.on( 'beforeSetMode', function() { removeFillingChar( editor.document ); } ); 229 230 var fillingCharBefore, 231 resetSelection; 232 233 function beforeData() 234 { 235 var doc = editor.document, 236 fillingChar = getFillingChar( doc ); 237 238 if ( fillingChar ) 239 { 240 // If cursor is right blinking by side of the filler node, save it for restoring, 241 // as the following text substitution will blind it. (#7437) 242 var sel = doc.$.defaultView.getSelection(); 243 if ( sel.type == 'Caret' && sel.anchorNode == fillingChar.$ ) 244 resetSelection = 1; 245 246 fillingCharBefore = fillingChar.getText(); 247 fillingChar.setText( fillingCharBefore.replace( /\u200B/g, '' ) ); 248 } 249 } 250 function afterData() 251 { 252 var doc = editor.document, 253 fillingChar = getFillingChar( doc ); 254 255 if ( fillingChar ) 256 { 257 fillingChar.setText( fillingCharBefore ); 258 259 if ( resetSelection ) 260 { 261 doc.$.defaultView.getSelection().setPosition( fillingChar.$,fillingChar.getLength() ); 262 resetSelection = 0; 263 } 264 } 265 } 266 editor.on( 'beforeUndoImage', beforeData ); 267 editor.on( 'afterUndoImage', afterData ); 268 editor.on( 'beforeGetData', beforeData, null, null, 0 ); 269 editor.on( 'getData', afterData ); 270 } 271 272 editor.on( 'contentDom', function() 273 { 274 var doc = editor.document, 275 outerDoc = CKEDITOR.document, 276 body = doc.getBody(), 277 html = doc.getDocumentElement(); 278 279 if ( CKEDITOR.env.ie ) 280 { 281 // Other browsers don't loose the selection if the 282 // editor document loose the focus. In IE, we don't 283 // have support for it, so we reproduce it here, other 284 // than firing the selection change event. 285 286 var savedRange, 287 saveEnabled, 288 restoreEnabled = 1; 289 290 // "onfocusin" is fired before "onfocus". It makes it 291 // possible to restore the selection before click 292 // events get executed. 293 body.on( 'focusin', function( evt ) 294 { 295 // If there are elements with layout they fire this event but 296 // it must be ignored to allow edit its contents #4682 297 if ( evt.data.$.srcElement.nodeName != 'BODY' ) 298 return; 299 300 // Give the priority to locked selection since it probably 301 // reflects the actual situation, besides locked selection 302 // could be interfered because of text nodes normalizing. 303 // (#6083, #6987) 304 var lockedSelection = doc.getCustomData( 'cke_locked_selection' ); 305 if ( lockedSelection ) 306 { 307 lockedSelection.unlock( 1 ); 308 lockedSelection.lock(); 309 } 310 // Then check ff we have saved a range, restore it at this 311 // point. 312 else if ( savedRange && restoreEnabled ) 313 { 314 // Well not break because of this. 315 try { savedRange.select(); } catch (e) {} 316 savedRange = null; 317 } 318 }); 319 320 body.on( 'focus', function() 321 { 322 // Enable selections to be saved. 323 saveEnabled = 1; 324 325 saveSelection(); 326 }); 327 328 body.on( 'beforedeactivate', function( evt ) 329 { 330 // Ignore this event if it's caused by focus switch between 331 // internal editable control type elements, e.g. layouted paragraph. (#4682) 332 if ( evt.data.$.toElement ) 333 return; 334 335 // Disable selections from being saved. 336 saveEnabled = 0; 337 restoreEnabled = 1; 338 }); 339 340 // [IE] Iframe will still keep the selection when blurred, if 341 // focus is moved onto a non-editing host, e.g. link or button, but 342 // it becomes a problem for the object type selection, since the resizer 343 // handler attached on it will mark other part of the UI, especially 344 // for the dialog. (#8157) 345 // [IE<8] Even worse For old IEs, the cursor will not vanish even if 346 // the selection has been moved to another text input in some cases. (#4716) 347 // 348 // Now the range restore is disabled, so we simply force IE to clean 349 // up the selection before blur. 350 CKEDITOR.env.ie && editor.on( 'blur', function() 351 { 352 // Error proof when the editor is not visible. (#6375) 353 try{ doc.$.selection.empty(); } catch ( er){} 354 }); 355 356 // Listening on document element ensures that 357 // scrollbar is included. (#5280) 358 html.on( 'mousedown', function() 359 { 360 // Lock restore selection now, as we have 361 // a followed 'click' event which introduce 362 // new selection. (#5735) 363 restoreEnabled = 0; 364 }); 365 366 html.on( 'mouseup', function() 367 { 368 restoreEnabled = 1; 369 }); 370 371 var scroll; 372 // IE fires the "selectionchange" event when clicking 373 // inside a selection. We don't want to capture that. 374 body.on( 'mousedown', function( evt ) 375 { 376 // IE scrolls document to top on right mousedown 377 // when editor has no focus, remember this scroll 378 // position and revert it before context menu opens. (#5778) 379 if ( evt.data.$.button == 2 ) 380 { 381 var sel = editor.document.$.selection; 382 if ( sel.type == 'None' ) 383 scroll = editor.window.getScrollPosition(); 384 } 385 disableSave(); 386 }); 387 388 body.on( 'mouseup', 389 function( evt ) 390 { 391 // Restore recorded scroll position when needed on right mouseup. 392 if ( evt.data.$.button == 2 && scroll ) 393 { 394 editor.document.$.documentElement.scrollLeft = scroll.x; 395 editor.document.$.documentElement.scrollTop = scroll.y; 396 } 397 scroll = null; 398 399 saveEnabled = 1; 400 setTimeout( function() 401 { 402 saveSelection( true ); 403 }, 404 0 ); 405 }); 406 407 body.on( 'keydown', disableSave ); 408 body.on( 'keyup', 409 function() 410 { 411 saveEnabled = 1; 412 saveSelection(); 413 }); 414 415 // When content doc is in standards mode, IE doesn't produce text selection 416 // when click on the region outside of body, we emulate 417 // the correct behavior here. (#1659, #7932, # 9097) 418 if ( doc.$.compatMode != 'BackCompat' ) 419 { 420 if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) 421 { 422 function moveRangeToPoint( range, x, y ) 423 { 424 // Error prune in IE7. (#9034, #9110) 425 try { range.moveToPoint( x, y ); } catch ( e ) {} 426 } 427 428 html.on( 'mousedown', function( evt ) 429 { 430 // Expand the text range along with mouse move. 431 function onHover( evt ) 432 { 433 evt = evt.data.$; 434 if ( textRng ) 435 { 436 // Read the current cursor. 437 var rngEnd = body.$.createTextRange(); 438 439 moveRangeToPoint( rngEnd, evt.x, evt.y ); 440 441 // Handle drag directions. 442 textRng.setEndPoint( 443 startRng.compareEndPoints( 'StartToStart', rngEnd ) < 0 ? 444 'EndToEnd' : 445 'StartToStart', 446 rngEnd ); 447 448 // Update selection with new range. 449 textRng.select(); 450 } 451 } 452 453 function removeListeners() 454 { 455 outerDoc.removeListener( 'mouseup', onSelectEnd ); 456 html.removeListener( 'mouseup', onSelectEnd ); 457 } 458 459 function onSelectEnd() 460 { 461 462 html.removeListener( 'mousemove', onHover ); 463 removeListeners(); 464 465 // Make it in effect on mouse up. (#9022) 466 textRng.select(); 467 } 468 469 evt = evt.data; 470 471 // We're sure that the click happens at the region 472 // outside body, but not on scrollbar. 473 if ( evt.getTarget().is( 'html' ) && 474 evt.$.x < html.$.clientWidth && 475 evt.$.y < html.$.clientHeight ) 476 { 477 // Start to build the text range. 478 var textRng = body.$.createTextRange(); 479 moveRangeToPoint( textRng, evt.$.x, evt.$.y ); 480 // Records the dragging start of the above text range. 481 var startRng = textRng.duplicate(); 482 483 html.on( 'mousemove', onHover ); 484 outerDoc.on( 'mouseup', onSelectEnd ); 485 html.on( 'mouseup', onSelectEnd ); 486 } 487 }); 488 } 489 490 // It's much simpler for IE > 8, we just need to reselect the reported range. 491 if ( CKEDITOR.env.ie8 ) 492 { 493 html.on( 'mousedown', function( evt ) 494 { 495 if ( evt.data.getTarget().is( 'html' ) ) 496 { 497 // Limit the text selection mouse move inside of editable. (#9715) 498 outerDoc.on( 'mouseup', onSelectEnd ); 499 html.on( 'mouseup', onSelectEnd ); 500 } 501 502 }); 503 504 function removeListeners() 505 { 506 outerDoc.removeListener( 'mouseup', onSelectEnd ); 507 html.removeListener( 'mouseup', onSelectEnd ); 508 } 509 510 function onSelectEnd() 511 { 512 removeListeners(); 513 514 // The event is not fired when clicking on the scrollbars, 515 // so we can safely check the following to understand 516 // whether the empty space following <body> has been clicked. 517 var sel = CKEDITOR.document.$.selection, 518 range = sel.createRange(); 519 // The selection range is reported on host, but actually it should applies to the content doc. 520 if ( sel.type != 'None' && range.parentElement().ownerDocument == doc.$ ) 521 range.select(); 522 } 523 } 524 525 } 526 // IE is the only to provide the "selectionchange" 527 // event. 528 doc.on( 'selectionchange', saveSelection ); 529 530 function disableSave() 531 { 532 saveEnabled = 0; 533 } 534 535 function saveSelection( testIt ) 536 { 537 if ( saveEnabled ) 538 { 539 var doc = editor.document, 540 sel = editor.getSelection(), 541 nativeSel = sel && sel.getNative(); 542 543 // There is a very specific case, when clicking 544 // inside a text selection. In that case, the 545 // selection collapses at the clicking point, 546 // but the selection object remains in an 547 // unknown state, making createRange return a 548 // range at the very start of the document. In 549 // such situation we have to test the range, to 550 // be sure it's valid. 551 if ( testIt && nativeSel && nativeSel.type == 'None' ) 552 { 553 // The "InsertImage" command can be used to 554 // test whether the selection is good or not. 555 // If not, it's enough to give some time to 556 // IE to put things in order for us. 557 if ( !doc.$.queryCommandEnabled( 'InsertImage' ) ) 558 { 559 CKEDITOR.tools.setTimeout( saveSelection, 50, this, true ); 560 return; 561 } 562 } 563 564 // Avoid saving selection from within text input. (#5747) 565 var parentTag; 566 if ( nativeSel && nativeSel.type && nativeSel.type != 'Control' 567 && ( parentTag = nativeSel.createRange() ) 568 && ( parentTag = parentTag.parentElement() ) 569 && ( parentTag = parentTag.nodeName ) 570 && parentTag.toLowerCase() in { input: 1, textarea : 1 } ) 571 { 572 return; 573 } 574 575 // Not break because of this. (#9132) 576 try{ savedRange = nativeSel && sel.getRanges()[ 0 ]; } catch( er ) {} 577 578 checkSelectionChangeTimeout.call( editor ); 579 } 580 } 581 } 582 else 583 { 584 // In other browsers, we make the selection change 585 // check based on other events, like clicks or keys 586 // press. 587 588 doc.on( 'mouseup', checkSelectionChangeTimeout, editor ); 589 doc.on( 'keyup', checkSelectionChangeTimeout, editor ); 590 doc.on( 'selectionchange', checkSelectionChangeTimeout, editor ); 591 } 592 593 if ( CKEDITOR.env.webkit ) 594 { 595 // Before keystroke is handled by editor, check to remove the filling char. 596 doc.on( 'keydown', function( evt ) 597 { 598 var key = evt.data.getKey(); 599 // Remove the filling char before some keys get 600 // executed, so they'll not get blocked by it. 601 switch ( key ) 602 { 603 case 13 : // ENTER 604 case 33 : // PAGEUP 605 case 34 : // PAGEDOWN 606 case 35 : // HOME 607 case 36 : // END 608 case 37 : // LEFT-ARROW 609 case 39 : // RIGHT-ARROW 610 case 8 : // BACKSPACE 611 case 45 : // INS 612 case 46 : // DEl 613 removeFillingChar( editor.document ); 614 } 615 616 }, null, null, -1 ); 617 } 618 }); 619 620 // Clear the cached range path before unload. (#7174) 621 editor.on( 'contentDomUnload', editor.forceNextSelectionCheck, editor ); 622 623 editor.addCommand( 'selectAll', selectAllCmd ); 624 editor.ui.addButton( 'SelectAll', 625 { 626 label : editor.lang.selectAll, 627 command : 'selectAll' 628 }); 629 630 /** 631 * Check if to fire the {@link CKEDITOR.editor#selectionChange} event 632 * for the current editor instance. 633 * 634 * @param {Boolean} checkNow Check immediately without any delay. 635 */ 636 editor.selectionChange = function( checkNow ) 637 { 638 ( checkNow ? checkSelectionChange : checkSelectionChangeTimeout ).call( this ); 639 }; 640 641 // IE9 might cease to work if there's an object selection inside the iframe (#7639). 642 CKEDITOR.env.ie9Compat && editor.on( 'destroy', function() 643 { 644 var sel = editor.getSelection(); 645 sel && sel.getNative().clear(); 646 }, null, null, 9 ); 647 } 648 }); 649 650 /** 651 * Gets the current selection from the editing area when in WYSIWYG mode. 652 * @returns {CKEDITOR.dom.selection} A selection object or null if not in 653 * WYSIWYG mode or no selection is available. 654 * @example 655 * var selection = CKEDITOR.instances.editor1.<strong>getSelection()</strong>; 656 * alert( selection.getType() ); 657 */ 658 CKEDITOR.editor.prototype.getSelection = function() 659 { 660 return this.document && this.document.getSelection(); 661 }; 662 663 CKEDITOR.editor.prototype.forceNextSelectionCheck = function() 664 { 665 delete this._.selectionPreviousPath; 666 }; 667 668 /** 669 * Gets the current selection from the document. 670 * @returns {CKEDITOR.dom.selection} A selection object. 671 * @example 672 * var selection = CKEDITOR.instances.editor1.document.<strong>getSelection()</strong>; 673 * alert( selection.getType() ); 674 */ 675 CKEDITOR.dom.document.prototype.getSelection = function() 676 { 677 var sel = new CKEDITOR.dom.selection( this ); 678 return ( !sel || sel.isInvalid ) ? null : sel; 679 }; 680 681 /** 682 * No selection. 683 * @constant 684 * @example 685 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_NONE ) 686 * alert( 'Nothing is selected' ); 687 */ 688 CKEDITOR.SELECTION_NONE = 1; 689 690 /** 691 * A text or a collapsed selection. 692 * @constant 693 * @example 694 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT ) 695 * alert( 'A text is selected' ); 696 */ 697 CKEDITOR.SELECTION_TEXT = 2; 698 699 /** 700 * Element selection. 701 * @constant 702 * @example 703 * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_ELEMENT ) 704 * alert( 'An element is selected' ); 705 */ 706 CKEDITOR.SELECTION_ELEMENT = 3; 707 708 var isMSSelection = CKEDITOR.env.ie && CKEDITOR.env.version < 10; 709 710 /** 711 * Manipulates the selection in a DOM document. 712 * @constructor 713 * @param {CKEDITOR.dom.document} document The DOM document that contains the selection. 714 * @example 715 * var sel = new <strong>CKEDITOR.dom.selection( CKEDITOR.document )</strong>; 716 */ 717 CKEDITOR.dom.selection = function( document ) 718 { 719 var lockedSelection = document.getCustomData( 'cke_locked_selection' ); 720 721 if ( lockedSelection ) 722 return lockedSelection; 723 724 this.document = document; 725 this.isLocked = 0; 726 this._ = 727 { 728 cache : {} 729 }; 730 731 /** 732 * IE BUG: The selection's document may be a different document than the 733 * editor document. Return null if that is the case. 734 */ 735 if ( isMSSelection ) 736 { 737 // Avoid breaking because of it. (#8836) 738 try 739 { 740 var range = this.getNative().createRange(); 741 if ( !range || 742 ( range.item && range.item( 0 ).ownerDocument != this.document.$ ) || 743 ( range.parentElement && range.parentElement().ownerDocument != this.document.$ ) ) 744 { 745 throw 0; 746 } 747 } 748 catch ( e ) 749 { 750 this.isInvalid = true; 751 } 752 } 753 754 return this; 755 }; 756 757 var styleObjectElements = 758 { 759 img:1,hr:1,li:1,table:1,tr:1,td:1,th:1,embed:1,object:1,ol:1,ul:1, 760 a:1,input:1,form:1,select:1,textarea:1,button:1,fieldset:1,thead:1,tfoot:1 761 }; 762 763 CKEDITOR.dom.selection.prototype = 764 { 765 /** 766 * Gets the native selection object from the browser. 767 * @function 768 * @returns {Object} The native browser selection object. 769 * @example 770 * var selection = editor.getSelection().<strong>getNative()</strong>; 771 */ 772 getNative : function() 773 { 774 if ( this._.cache.nativeSel !== undefined ) 775 return this._.cache.nativeSel; 776 777 return ( this._.cache.nativeSel = isMSSelection ? this.document.$.selection : this.document.getWindow().$.getSelection() ); 778 }, 779 780 /** 781 * Gets the type of the current selection. The following values are 782 * available: 783 * <ul> 784 * <li><code>{@link CKEDITOR.SELECTION_NONE}</code> (1): No selection.</li> 785 * <li><code>{@link CKEDITOR.SELECTION_TEXT}</code> (2): A text or a collapsed 786 * selection is selected.</li> 787 * <li><code>{@link CKEDITOR.SELECTION_ELEMENT}</code> (3): An element is 788 * selected.</li> 789 * </ul> 790 * @function 791 * @returns {Number} One of the following constant values: 792 * <code>{@link CKEDITOR.SELECTION_NONE}</code>, <code>{@link CKEDITOR.SELECTION_TEXT}</code>, or 793 * <code>{@link CKEDITOR.SELECTION_ELEMENT}</code>. 794 * @example 795 * if ( editor.getSelection().<strong>getType()</strong> == CKEDITOR.SELECTION_TEXT ) 796 * alert( 'A text is selected' ); 797 */ 798 getType : 799 isMSSelection ? 800 function() 801 { 802 var cache = this._.cache; 803 if ( cache.type ) 804 return cache.type; 805 806 var type = CKEDITOR.SELECTION_NONE; 807 808 try 809 { 810 var sel = this.getNative(), 811 ieType = sel.type; 812 813 if ( ieType == 'Text' ) 814 type = CKEDITOR.SELECTION_TEXT; 815 816 if ( ieType == 'Control' ) 817 type = CKEDITOR.SELECTION_ELEMENT; 818 819 // It is possible that we can still get a text range 820 // object even when type == 'None' is returned by IE. 821 // So we'd better check the object returned by 822 // createRange() rather than by looking at the type. 823 if ( sel.createRange().parentElement ) 824 type = CKEDITOR.SELECTION_TEXT; 825 } 826 catch(e) {} 827 828 return ( cache.type = type ); 829 } 830 : 831 function() 832 { 833 var cache = this._.cache; 834 if ( cache.type ) 835 return cache.type; 836 837 var type = CKEDITOR.SELECTION_TEXT; 838 839 var sel = this.getNative(); 840 841 if ( !sel ) 842 type = CKEDITOR.SELECTION_NONE; 843 else if ( sel.rangeCount == 1 ) 844 { 845 // Check if the actual selection is a control (IMG, 846 // TABLE, HR, etc...). 847 848 var range = sel.getRangeAt(0), 849 startContainer = range.startContainer; 850 851 if ( startContainer == range.endContainer 852 && startContainer.nodeType == 1 853 && ( range.endOffset - range.startOffset ) == 1 854 && styleObjectElements[ startContainer.childNodes[ range.startOffset ].nodeName.toLowerCase() ] ) 855 { 856 type = CKEDITOR.SELECTION_ELEMENT; 857 } 858 } 859 860 return ( cache.type = type ); 861 }, 862 863 /** 864 * Retrieves the <code>{@link CKEDITOR.dom.range}</code> instances that represent the current selection. 865 * Note: Some browsers return multiple ranges even for a continuous selection. Firefox, for example, returns 866 * one range for each table cell when one or more table rows are selected. 867 * @function 868 * @param {Boolean} [onlyEditables] If set to <code>true</code>, this function retrives editable ranges only. 869 * @return {Array} Range instances that represent the current selection. 870 * @example 871 * var ranges = selection.<strong>getRanges()</strong>; 872 * alert( ranges.length ); 873 */ 874 getRanges : (function() 875 { 876 var func = isMSSelection ? 877 ( function() 878 { 879 function getNodeIndex( node ) { return new CKEDITOR.dom.node( node ).getIndex(); } 880 881 // Finds the container and offset for a specific boundary 882 // of an IE range. 883 var getBoundaryInformation = function( range, start ) 884 { 885 // Creates a collapsed range at the requested boundary. 886 range = range.duplicate(); 887 range.collapse( start ); 888 889 // Gets the element that encloses the range entirely. 890 var parent = range.parentElement(), 891 doc = parent.ownerDocument; 892 893 // Empty parent element, e.g. <i>^</i> 894 if ( !parent.hasChildNodes() ) 895 return { container : parent, offset : 0 }; 896 897 var siblings = parent.children, 898 child, 899 sibling, 900 testRange = range.duplicate(), 901 startIndex = 0, 902 endIndex = siblings.length - 1, 903 index = -1, 904 position, 905 distance, 906 container; 907 908 // Binary search over all element childs to test the range to see whether 909 // range is right on the boundary of one element. 910 while ( startIndex <= endIndex ) 911 { 912 index = Math.floor( ( startIndex + endIndex ) / 2 ); 913 child = siblings[ index ]; 914 testRange.moveToElementText( child ); 915 position = testRange.compareEndPoints( 'StartToStart', range ); 916 917 if ( position > 0 ) 918 endIndex = index - 1; 919 else if ( position < 0 ) 920 startIndex = index + 1; 921 else 922 { 923 // IE9 report wrong measurement with compareEndPoints when range anchors between two BRs. 924 // e.g. <p>text<br />^<br /></p> (#7433) 925 if ( CKEDITOR.env.ie9Compat && child.tagName == 'BR' ) 926 { 927 // "Fall back" to w3c selection. 928 var sel = doc.defaultView.getSelection(); 929 return { container : sel[ start ? 'anchorNode' : 'focusNode' ], 930 offset : sel[ start ? 'anchorOffset' : 'focusOffset' ] }; 931 } 932 else 933 return { container : parent, offset : getNodeIndex( child ) }; 934 } 935 } 936 937 // All childs are text nodes, 938 // or to the right hand of test range are all text nodes. (#6992) 939 if ( index == -1 || index == siblings.length - 1 && position < 0 ) 940 { 941 // Adapt test range to embrace the entire parent contents. 942 testRange.moveToElementText( parent ); 943 testRange.setEndPoint( 'StartToStart', range ); 944 945 // IE report line break as CRLF with range.text but 946 // only LF with textnode.nodeValue, normalize them to avoid 947 // breaking character counting logic below. (#3949) 948 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; 949 950 siblings = parent.childNodes; 951 952 // Actual range anchor right beside test range at the boundary of text node. 953 if ( !distance ) 954 { 955 child = siblings[ siblings.length - 1 ]; 956 957 if ( child.nodeType != CKEDITOR.NODE_TEXT ) 958 return { container : parent, offset : siblings.length }; 959 else 960 return { container : child, offset : child.nodeValue.length }; 961 } 962 963 // Start the measuring until distance overflows, meanwhile count the text nodes. 964 var i = siblings.length; 965 while ( distance > 0 && i > 0 ) 966 { 967 sibling = siblings[ --i ]; 968 if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) 969 { 970 container = sibling; 971 distance -= sibling.nodeValue.length; 972 } 973 } 974 975 return { container : container, offset : -distance }; 976 } 977 // Test range was one offset beyond OR behind the anchored text node. 978 else 979 { 980 // Adapt one side of test range to the actual range 981 // for measuring the offset between them. 982 testRange.collapse( position > 0 ? true : false ); 983 testRange.setEndPoint( position > 0 ? 'StartToStart' : 'EndToStart', range ); 984 985 // IE report line break as CRLF with range.text but 986 // only LF with textnode.nodeValue, normalize them to avoid 987 // breaking character counting logic below. (#3949) 988 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; 989 990 // Actual range anchor right beside test range at the inner boundary of text node. 991 if ( !distance ) 992 return { container : parent, offset : getNodeIndex( child ) + ( position > 0 ? 0 : 1 ) }; 993 994 // Start the measuring until distance overflows, meanwhile count the text nodes. 995 while ( distance > 0 ) 996 { 997 try 998 { 999 sibling = child[ position > 0 ? 'previousSibling' : 'nextSibling' ]; 1000 if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) 1001 { 1002 distance -= sibling.nodeValue.length; 1003 container = sibling; 1004 } 1005 child = sibling; 1006 } 1007 // Measurement in IE could be somtimes wrong because of <select> element. (#4611) 1008 catch( e ) 1009 { 1010 return { container : parent, offset : getNodeIndex( child ) }; 1011 } 1012 } 1013 1014 return { container : container, offset : position > 0 ? -distance : container.nodeValue.length + distance }; 1015 } 1016 }; 1017 1018 return function() 1019 { 1020 // IE doesn't have range support (in the W3C way), so we 1021 // need to do some magic to transform selections into 1022 // CKEDITOR.dom.range instances. 1023 1024 var sel = this.getNative(), 1025 nativeRange = sel && sel.createRange(), 1026 type = this.getType(), 1027 range; 1028 1029 if ( !sel ) 1030 return []; 1031 1032 if ( type == CKEDITOR.SELECTION_TEXT ) 1033 { 1034 range = new CKEDITOR.dom.range( this.document ); 1035 1036 var boundaryInfo = getBoundaryInformation( nativeRange, true ); 1037 range.setStart( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset ); 1038 1039 boundaryInfo = getBoundaryInformation( nativeRange ); 1040 range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset ); 1041 1042 // Correct an invalid IE range case on empty list item. (#5850) 1043 if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING 1044 && range.endOffset <= range.startContainer.getIndex() ) 1045 { 1046 range.collapse(); 1047 } 1048 1049 return [ range ]; 1050 } 1051 else if ( type == CKEDITOR.SELECTION_ELEMENT ) 1052 { 1053 var retval = []; 1054 1055 for ( var i = 0 ; i < nativeRange.length ; i++ ) 1056 { 1057 var element = nativeRange.item( i ), 1058 parentElement = element.parentNode, 1059 j = 0; 1060 1061 range = new CKEDITOR.dom.range( this.document ); 1062 1063 for (; j < parentElement.childNodes.length && parentElement.childNodes[j] != element ; j++ ) 1064 { /*jsl:pass*/ } 1065 1066 range.setStart( new CKEDITOR.dom.node( parentElement ), j ); 1067 range.setEnd( new CKEDITOR.dom.node( parentElement ), j + 1 ); 1068 retval.push( range ); 1069 } 1070 1071 return retval; 1072 } 1073 1074 return []; 1075 }; 1076 })() 1077 : 1078 function() 1079 { 1080 1081 // On browsers implementing the W3C range, we simply 1082 // tranform the native ranges in CKEDITOR.dom.range 1083 // instances. 1084 1085 var ranges = [], 1086 range, 1087 doc = this.document, 1088 sel = this.getNative(); 1089 1090 if ( !sel ) 1091 return ranges; 1092 1093 // On WebKit, it may happen that we'll have no selection 1094 // available. We normalize it here by replicating the 1095 // behavior of other browsers. 1096 if ( !sel.rangeCount ) 1097 { 1098 range = new CKEDITOR.dom.range( doc ); 1099 range.moveToElementEditStart( doc.getBody() ); 1100 ranges.push( range ); 1101 } 1102 1103 for ( var i = 0 ; i < sel.rangeCount ; i++ ) 1104 { 1105 var nativeRange = sel.getRangeAt( i ); 1106 1107 range = new CKEDITOR.dom.range( doc ); 1108 1109 range.setStart( new CKEDITOR.dom.node( nativeRange.startContainer ), nativeRange.startOffset ); 1110 range.setEnd( new CKEDITOR.dom.node( nativeRange.endContainer ), nativeRange.endOffset ); 1111 ranges.push( range ); 1112 } 1113 return ranges; 1114 }; 1115 1116 return function( onlyEditables ) 1117 { 1118 var cache = this._.cache; 1119 if ( cache.ranges && !onlyEditables ) 1120 return cache.ranges; 1121 else if ( !cache.ranges ) 1122 cache.ranges = new CKEDITOR.dom.rangeList( func.call( this ) ); 1123 1124 // Split range into multiple by read-only nodes. 1125 if ( onlyEditables ) 1126 { 1127 var ranges = cache.ranges; 1128 for ( var i = 0; i < ranges.length; i++ ) 1129 { 1130 var range = ranges[ i ]; 1131 1132 // Drop range spans inside one ready-only node. 1133 var parent = range.getCommonAncestor(); 1134 if ( parent.isReadOnly() ) 1135 ranges.splice( i, 1 ); 1136 1137 if ( range.collapsed ) 1138 continue; 1139 1140 // Range may start inside a non-editable element, 1141 // replace the range start after it. 1142 if ( range.startContainer.isReadOnly() ) 1143 { 1144 var current = range.startContainer; 1145 while( current ) 1146 { 1147 if ( current.is( 'body' ) || !current.isReadOnly() ) 1148 break; 1149 1150 if ( current.type == CKEDITOR.NODE_ELEMENT 1151 && current.getAttribute( 'contentEditable' ) == 'false' ) 1152 range.setStartAfter( current ); 1153 1154 current = current.getParent(); 1155 } 1156 } 1157 1158 var startContainer = range.startContainer, 1159 endContainer = range.endContainer, 1160 startOffset = range.startOffset, 1161 endOffset = range.endOffset, 1162 walkerRange = range.clone(); 1163 1164 // Enlarge range start/end with text node to avoid walker 1165 // being DOM destructive, it doesn't interfere our checking 1166 // of elements below as well. 1167 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) 1168 { 1169 if ( startOffset >= startContainer.getLength() ) 1170 walkerRange.setStartAfter( startContainer ); 1171 else 1172 walkerRange.setStartBefore( startContainer ); 1173 } 1174 1175 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) 1176 { 1177 if ( !endOffset ) 1178 walkerRange.setEndBefore( endContainer ); 1179 else 1180 walkerRange.setEndAfter( endContainer ); 1181 } 1182 1183 // Looking for non-editable element inside the range. 1184 var walker = new CKEDITOR.dom.walker( walkerRange ); 1185 walker.evaluator = function( node ) 1186 { 1187 if ( node.type == CKEDITOR.NODE_ELEMENT 1188 && node.isReadOnly() ) 1189 { 1190 var newRange = range.clone(); 1191 range.setEndBefore( node ); 1192 1193 // Drop collapsed range around read-only elements, 1194 // it make sure the range list empty when selecting 1195 // only non-editable elements. 1196 if ( range.collapsed ) 1197 ranges.splice( i--, 1 ); 1198 1199 // Avoid creating invalid range. 1200 if ( !( node.getPosition( walkerRange.endContainer ) & CKEDITOR.POSITION_CONTAINS ) ) 1201 { 1202 newRange.setStartAfter( node ); 1203 if ( !newRange.collapsed ) 1204 ranges.splice( i + 1, 0, newRange ); 1205 } 1206 1207 return true; 1208 } 1209 1210 return false; 1211 }; 1212 1213 walker.next(); 1214 } 1215 } 1216 1217 return cache.ranges; 1218 }; 1219 })(), 1220 1221 /** 1222 * Gets the DOM element in which the selection starts. 1223 * @returns {CKEDITOR.dom.element} The element at the beginning of the 1224 * selection. 1225 * @example 1226 * var element = editor.getSelection().<strong>getStartElement()</strong>; 1227 * alert( element.getName() ); 1228 */ 1229 getStartElement : function() 1230 { 1231 var cache = this._.cache; 1232 if ( cache.startElement !== undefined ) 1233 return cache.startElement; 1234 1235 var node, 1236 sel = this.getNative(); 1237 1238 switch ( this.getType() ) 1239 { 1240 case CKEDITOR.SELECTION_ELEMENT : 1241 return this.getSelectedElement(); 1242 1243 case CKEDITOR.SELECTION_TEXT : 1244 1245 var range = this.getRanges()[0]; 1246 1247 if ( range ) 1248 { 1249 if ( !range.collapsed ) 1250 { 1251 range.optimize(); 1252 1253 // Decrease the range content to exclude particial 1254 // selected node on the start which doesn't have 1255 // visual impact. ( #3231 ) 1256 while ( 1 ) 1257 { 1258 var startContainer = range.startContainer, 1259 startOffset = range.startOffset; 1260 // Limit the fix only to non-block elements.(#3950) 1261 if ( startOffset == ( startContainer.getChildCount ? 1262 startContainer.getChildCount() : startContainer.getLength() ) 1263 && !startContainer.isBlockBoundary() ) 1264 range.setStartAfter( startContainer ); 1265 else break; 1266 } 1267 1268 node = range.startContainer; 1269 1270 if ( node.type != CKEDITOR.NODE_ELEMENT ) 1271 return node.getParent(); 1272 1273 node = node.getChild( range.startOffset ); 1274 1275 if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) 1276 node = range.startContainer; 1277 else 1278 { 1279 var child = node.getFirst(); 1280 while ( child && child.type == CKEDITOR.NODE_ELEMENT ) 1281 { 1282 node = child; 1283 child = child.getFirst(); 1284 } 1285 } 1286 } 1287 else 1288 { 1289 node = range.startContainer; 1290 if ( node.type != CKEDITOR.NODE_ELEMENT ) 1291 node = node.getParent(); 1292 } 1293 1294 node = node.$; 1295 } 1296 } 1297 1298 return cache.startElement = ( node ? new CKEDITOR.dom.element( node ) : null ); 1299 }, 1300 1301 /** 1302 * Gets the currently selected element. 1303 * @returns {CKEDITOR.dom.element} The selected element. Null if no 1304 * selection is available or the selection type is not 1305 * <code>{@link CKEDITOR.SELECTION_ELEMENT}</code>. 1306 * @example 1307 * var element = editor.getSelection().<strong>getSelectedElement()</strong>; 1308 * alert( element.getName() ); 1309 */ 1310 getSelectedElement : function() 1311 { 1312 var cache = this._.cache; 1313 if ( cache.selectedElement !== undefined ) 1314 return cache.selectedElement; 1315 1316 var self = this; 1317 1318 var node = CKEDITOR.tools.tryThese( 1319 // Is it native IE control type selection? 1320 function() 1321 { 1322 return self.getNative().createRange().item( 0 ); 1323 }, 1324 // If a table or list is fully selected. 1325 function() 1326 { 1327 var root, 1328 retval, 1329 range = self.getRanges()[ 0 ], 1330 ancestor = range.getCommonAncestor( 1, 1 ), 1331 tags = { table:1,ul:1,ol:1,dl:1 }; 1332 1333 for ( var t in tags ) 1334 { 1335 if ( ( root = ancestor.getAscendant( t, 1 ) ) ) 1336 break; 1337 } 1338 1339 if ( root ) 1340 { 1341 // Enlarging the start boundary. 1342 var testRange = new CKEDITOR.dom.range( this.document ); 1343 testRange.setStartAt( root, CKEDITOR.POSITION_AFTER_START ); 1344 testRange.setEnd( range.startContainer, range.startOffset ); 1345 1346 var enlargeables = CKEDITOR.tools.extend( tags, CKEDITOR.dtd.$listItem, CKEDITOR.dtd.$tableContent ), 1347 walker = new CKEDITOR.dom.walker( testRange ), 1348 // Check the range is at the inner boundary of the structural element. 1349 guard = function( walker, isEnd ) 1350 { 1351 return function( node, isWalkOut ) 1352 { 1353 if ( node.type == CKEDITOR.NODE_TEXT && ( !CKEDITOR.tools.trim( node.getText() ) || node.getParent().data( 'cke-bookmark' ) ) ) 1354 return true; 1355 1356 var tag; 1357 if ( node.type == CKEDITOR.NODE_ELEMENT ) 1358 { 1359 tag = node.getName(); 1360 1361 // Bypass bogus br at the end of block. 1362 if ( tag == 'br' && isEnd && node.equals( node.getParent().getBogus() ) ) 1363 return true; 1364 1365 if ( isWalkOut && tag in enlargeables || tag in CKEDITOR.dtd.$removeEmpty ) 1366 return true; 1367 } 1368 1369 walker.halted = 1; 1370 return false; 1371 }; 1372 }; 1373 1374 walker.guard = guard( walker ); 1375 1376 if ( walker.checkBackward() && !walker.halted ) 1377 { 1378 walker = new CKEDITOR.dom.walker( testRange ); 1379 testRange.setStart( range.endContainer, range.endOffset ); 1380 testRange.setEndAt( root, CKEDITOR.POSITION_BEFORE_END ); 1381 walker.guard = guard( walker, 1 ); 1382 if ( walker.checkForward() && !walker.halted ) 1383 retval = root.$; 1384 } 1385 } 1386 1387 if ( !retval ) 1388 throw 0; 1389 1390 return retval; 1391 }, 1392 // Figure it out by checking if there's a single enclosed 1393 // node of the range. 1394 function() 1395 { 1396 var range = self.getRanges()[ 0 ], 1397 enclosed, 1398 selected; 1399 1400 // Check first any enclosed element, e.g. <ul>[<li><a href="#">item</a></li>]</ul> 1401 for ( var i = 2; i && !( ( enclosed = range.getEnclosedNode() ) 1402 && ( enclosed.type == CKEDITOR.NODE_ELEMENT ) 1403 && styleObjectElements[ enclosed.getName() ] 1404 && ( selected = enclosed ) ); i-- ) 1405 { 1406 // Then check any deep wrapped element, e.g. [<b><i><img /></i></b>] 1407 range.shrink( CKEDITOR.SHRINK_ELEMENT ); 1408 } 1409 1410 return selected.$; 1411 }); 1412 1413 return cache.selectedElement = ( node ? new CKEDITOR.dom.element( node ) : null ); 1414 }, 1415 1416 /** 1417 * Retrieves the text contained within the range. An empty string is returned for non-text selection. 1418 * @returns {String} A string of text within the current selection. 1419 * @since 3.6.1 1420 * @example 1421 * var text = editor.getSelection().<strong>getSelectedText()</strong>; 1422 * alert( text ); 1423 */ 1424 getSelectedText : function() 1425 { 1426 var cache = this._.cache; 1427 if ( cache.selectedText !== undefined ) 1428 return cache.selectedText; 1429 1430 var text = '', 1431 nativeSel = this.getNative(); 1432 if ( this.getType() == CKEDITOR.SELECTION_TEXT ) 1433 text = isMSSelection ? 1434 nativeSel.type == 'Control' ? '' : 1435 nativeSel.createRange().text : 1436 nativeSel.toString(); 1437 1438 return ( cache.selectedText = text ); 1439 }, 1440 1441 /** 1442 * Locks the selection made in the editor in order to make it possible to 1443 * manipulate it without browser interference. A locked selection is 1444 * cached and remains unchanged until it is released with the <code>#unlock</code> 1445 * method. 1446 * @example 1447 * editor.getSelection().<strong>lock()</strong>; 1448 */ 1449 lock : function() 1450 { 1451 // Call all cacheable function. 1452 this.getRanges(); 1453 this.getStartElement(); 1454 this.getSelectedElement(); 1455 this.getSelectedText(); 1456 1457 // The native selection is not available when locked. 1458 this._.cache.nativeSel = {}; 1459 1460 this.isLocked = 1; 1461 1462 // Save this selection inside the DOM document. 1463 this.document.setCustomData( 'cke_locked_selection', this ); 1464 }, 1465 1466 /** 1467 * Unlocks the selection made in the editor and locked with the <code>#lock</code> method. 1468 * An unlocked selection is no longer cached and can be changed. 1469 * @param {Boolean} [restore] If set to <code>true</code>, the selection is restored back to the selection saved earlier by using the <code>#lock</code> method. 1470 * @example 1471 * editor.getSelection().<strong>unlock()</strong>; 1472 */ 1473 unlock : function( restore ) 1474 { 1475 var doc = this.document, 1476 lockedSelection = doc.getCustomData( 'cke_locked_selection' ); 1477 1478 if ( lockedSelection ) 1479 { 1480 doc.setCustomData( 'cke_locked_selection', null ); 1481 1482 if ( restore ) 1483 { 1484 var selectedElement = lockedSelection.getSelectedElement(), 1485 ranges = !selectedElement && lockedSelection.getRanges(); 1486 1487 this.isLocked = 0; 1488 this.reset(); 1489 1490 if ( selectedElement ) 1491 this.selectElement( selectedElement ); 1492 else 1493 this.selectRanges( ranges ); 1494 } 1495 } 1496 1497 if ( !lockedSelection || !restore ) 1498 { 1499 this.isLocked = 0; 1500 this.reset(); 1501 } 1502 }, 1503 1504 /** 1505 * Clears the selection cache. 1506 * @example 1507 * editor.getSelection().<strong>reset()</strong>; 1508 */ 1509 reset : function() 1510 { 1511 this._.cache = {}; 1512 }, 1513 1514 /** 1515 * Makes the current selection of type <code>{@link CKEDITOR.SELECTION_ELEMENT}</code> by enclosing the specified element. 1516 * @param {CKEDITOR.dom.element} element The element to enclose in the selection. 1517 * @example 1518 * var element = editor.document.getById( 'sampleElement' ); 1519 * editor.getSelection.<strong>selectElement( element )</strong>; 1520 */ 1521 selectElement : function( element ) 1522 { 1523 if ( this.isLocked ) 1524 { 1525 var range = new CKEDITOR.dom.range( this.document ); 1526 range.setStartBefore( element ); 1527 range.setEndAfter( element ); 1528 1529 this._.cache.selectedElement = element; 1530 this._.cache.startElement = element; 1531 this._.cache.ranges = new CKEDITOR.dom.rangeList( range ); 1532 this._.cache.type = CKEDITOR.SELECTION_ELEMENT; 1533 1534 return; 1535 } 1536 1537 range = new CKEDITOR.dom.range( element.getDocument() ); 1538 range.setStartBefore( element ); 1539 range.setEndAfter( element ); 1540 range.select(); 1541 1542 this.document.fire( 'selectionchange' ); 1543 this.reset(); 1544 1545 }, 1546 1547 /** 1548 * Clears the original selection and adds the specified ranges 1549 * to the document selection. 1550 * @param {Array} ranges An array of <code>{@link CKEDITOR.dom.range}</code> instances representing ranges to be added to the document. 1551 * @example 1552 * var ranges = new CKEDITOR.dom.range( editor.document ); 1553 * editor.getSelection().<strong>selectRanges( [ ranges ] )</strong>; 1554 */ 1555 selectRanges : function( ranges ) 1556 { 1557 if ( this.isLocked ) 1558 { 1559 this._.cache.selectedElement = null; 1560 this._.cache.startElement = ranges[ 0 ] && ranges[ 0 ].getTouchedStartNode(); 1561 this._.cache.ranges = new CKEDITOR.dom.rangeList( ranges ); 1562 this._.cache.type = CKEDITOR.SELECTION_TEXT; 1563 1564 return; 1565 } 1566 1567 if ( isMSSelection ) 1568 { 1569 if ( ranges.length > 1 ) 1570 { 1571 // IE doesn't accept multiple ranges selection, so we join all into one. 1572 var last = ranges[ ranges.length -1 ] ; 1573 ranges[ 0 ].setEnd( last.endContainer, last.endOffset ); 1574 ranges.length = 1; 1575 } 1576 1577 if ( ranges[ 0 ] ) 1578 ranges[ 0 ].select(); 1579 1580 this.reset(); 1581 } 1582 else 1583 { 1584 var sel = this.getNative(); 1585 1586 // getNative() returns null if iframe is "display:none" in FF. (#6577) 1587 if ( !sel ) 1588 return; 1589 1590 // Opera: The above hack work around a *visually wrong* text selection that 1591 // happens in certain situation. (#6874) 1592 if ( CKEDITOR.env.opera ) 1593 this.document.$.execCommand( 'SelectAll', false ); 1594 1595 if ( ranges.length ) 1596 { 1597 sel.removeAllRanges(); 1598 // Remove any existing filling char first. 1599 CKEDITOR.env.webkit && removeFillingChar( this.document ); 1600 } 1601 1602 for ( var i = 0 ; i < ranges.length ; i++ ) 1603 { 1604 // Joining sequential ranges introduced by 1605 // readonly elements protection. 1606 if ( i < ranges.length -1 ) 1607 { 1608 var left = ranges[ i ], right = ranges[ i +1 ], 1609 between = left.clone(); 1610 between.setStart( left.endContainer, left.endOffset ); 1611 between.setEnd( right.startContainer, right.startOffset ); 1612 1613 // Don't confused by Firefox adjancent multi-ranges 1614 // introduced by table cells selection. 1615 if ( !between.collapsed ) 1616 { 1617 between.shrink( CKEDITOR.NODE_ELEMENT, true ); 1618 var ancestor = between.getCommonAncestor(), 1619 enclosed = between.getEnclosedNode(); 1620 1621 // The following cases has to be considered: 1622 // 1. <span contenteditable="false">[placeholder]</span> 1623 // 2. <input contenteditable="false" type="radio"/> (#6621) 1624 if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) 1625 { 1626 right.setStart( left.startContainer, left.startOffset ); 1627 ranges.splice( i--, 1 ); 1628 continue; 1629 } 1630 } 1631 } 1632 1633 var range = ranges[ i ]; 1634 var nativeRange = this.document.$.createRange(); 1635 var startContainer = range.startContainer; 1636 1637 // In FF2, if we have a collapsed range, inside an empty 1638 // element, we must add something to it otherwise the caret 1639 // will not be visible. 1640 // In Opera instead, the selection will be moved out of the 1641 // element. (#4657) 1642 if ( range.collapsed && 1643 ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.version < 10900 ) ) && 1644 startContainer.type == CKEDITOR.NODE_ELEMENT && 1645 !startContainer.getChildCount() ) 1646 { 1647 startContainer.appendText( '' ); 1648 } 1649 1650 if ( range.collapsed 1651 && CKEDITOR.env.webkit 1652 && rangeRequiresFix( range ) ) 1653 { 1654 // Append a zero-width space so WebKit will not try to 1655 // move the selection by itself (#1272). 1656 var fillingChar = createFillingChar( this.document ); 1657 range.insertNode( fillingChar ) ; 1658 1659 var next = fillingChar.getNext(); 1660 1661 // If the filling char is followed by a <br>, whithout 1662 // having something before it, it'll not blink. 1663 // Let's remove it in this case. 1664 if ( next && !fillingChar.getPrevious() && next.type == CKEDITOR.NODE_ELEMENT && next.getName() == 'br' ) 1665 { 1666 removeFillingChar( this.document ); 1667 range.moveToPosition( next, CKEDITOR.POSITION_BEFORE_START ); 1668 } 1669 else 1670 range.moveToPosition( fillingChar, CKEDITOR.POSITION_AFTER_END ); 1671 } 1672 1673 nativeRange.setStart( range.startContainer.$, range.startOffset ); 1674 1675 try 1676 { 1677 nativeRange.setEnd( range.endContainer.$, range.endOffset ); 1678 } 1679 catch ( e ) 1680 { 1681 // There is a bug in Firefox implementation (it would be too easy 1682 // otherwise). The new start can't be after the end (W3C says it can). 1683 // So, let's create a new range and collapse it to the desired point. 1684 if ( e.toString().indexOf( 'NS_ERROR_ILLEGAL_VALUE' ) >= 0 ) 1685 { 1686 range.collapse( 1 ); 1687 nativeRange.setEnd( range.endContainer.$, range.endOffset ); 1688 } 1689 else 1690 throw e; 1691 } 1692 1693 // Select the range. 1694 sel.addRange( nativeRange ); 1695 } 1696 1697 // Don't miss selection change event for non-IEs. 1698 this.document.fire( 'selectionchange' ); 1699 this.reset(); 1700 } 1701 }, 1702 1703 /** 1704 * Creates a bookmark for each range of this selection (from <code>#getRanges</code>) 1705 * by calling the <code>{@link CKEDITOR.dom.range.prototype.createBookmark}</code> method, 1706 * with extra care taken to avoid interference among those ranges. The arguments 1707 * received are the same as with the underlying range method. 1708 * @returns {Array} Array of bookmarks for each range. 1709 * @example 1710 * var bookmarks = editor.getSelection().<strong>createBookmarks()</strong>; 1711 */ 1712 createBookmarks : function( serializable ) 1713 { 1714 return this.getRanges().createBookmarks( serializable ); 1715 }, 1716 1717 /** 1718 * Creates a bookmark for each range of this selection (from <code>#getRanges</code>) 1719 * by calling the <code>{@link CKEDITOR.dom.range.prototype.createBookmark2}</code> method, 1720 * with extra care taken to avoid interference among those ranges. The arguments 1721 * received are the same as with the underlying range method. 1722 * @returns {Array} Array of bookmarks for each range. 1723 * @example 1724 * var bookmarks = editor.getSelection().<strong>createBookmarks2()</strong>; 1725 */ 1726 createBookmarks2 : function( normalized ) 1727 { 1728 return this.getRanges().createBookmarks2( normalized ); 1729 }, 1730 1731 /** 1732 * Selects the virtual ranges denoted by the bookmarks by calling <code>#selectRanges</code>. 1733 * @param {Array} bookmarks The bookmarks representing ranges to be selected. 1734 * @returns {CKEDITOR.dom.selection} This selection object, after the ranges were selected. 1735 * @example 1736 * var bookmarks = editor.getSelection().createBookmarks(); 1737 * editor.getSelection().<strong>selectBookmarks( bookmarks )</strong>; 1738 */ 1739 selectBookmarks : function( bookmarks ) 1740 { 1741 var ranges = []; 1742 for ( var i = 0 ; i < bookmarks.length ; i++ ) 1743 { 1744 var range = new CKEDITOR.dom.range( this.document ); 1745 range.moveToBookmark( bookmarks[i] ); 1746 ranges.push( range ); 1747 } 1748 this.selectRanges( ranges ); 1749 return this; 1750 }, 1751 1752 /** 1753 * Retrieves the common ancestor node of the first range and the last range. 1754 * @returns {CKEDITOR.dom.element} The common ancestor of the selection. 1755 * @example 1756 * var ancestor = editor.getSelection().<strong>getCommonAncestor()</strong>; 1757 */ 1758 getCommonAncestor : function() 1759 { 1760 var ranges = this.getRanges(), 1761 startNode = ranges[ 0 ].startContainer, 1762 endNode = ranges[ ranges.length - 1 ].endContainer; 1763 return startNode.getCommonAncestor( endNode ); 1764 }, 1765 1766 /** 1767 * Moves the scrollbar to the starting position of the current selection. 1768 * @example 1769 * editor.getSelection().<strong>scrollIntoView()</strong>; 1770 */ 1771 scrollIntoView : function() 1772 { 1773 // If we have split the block, adds a temporary span at the 1774 // range position and scroll relatively to it. 1775 var start = this.getStartElement(); 1776 start.scrollIntoView(); 1777 } 1778 }; 1779 1780 var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ), 1781 isVisible = CKEDITOR.dom.walker.invisible( 1 ), 1782 fillerTextRegex = /\ufeff|\u00a0/, 1783 nonCells = { table:1,tbody:1,tr:1 }; 1784 1785 CKEDITOR.dom.range.prototype.select = 1786 isMSSelection ? 1787 // V2 1788 function( forceExpand ) 1789 { 1790 var collapsed = this.collapsed, 1791 isStartMarkerAlone, dummySpan, ieRange; 1792 1793 // Try to make a object selection. 1794 var selected = this.getEnclosedNode(); 1795 if ( selected ) 1796 { 1797 try 1798 { 1799 ieRange = this.document.$.body.createControlRange(); 1800 ieRange.addElement( selected.$ ); 1801 ieRange.select(); 1802 return; 1803 } 1804 catch( er ) {} 1805 } 1806 1807 // IE doesn't support selecting the entire table row/cell, move the selection into cells, e.g. 1808 // <table><tbody><tr>[<td>cell</b></td>... => <table><tbody><tr><td>[cell</td>... 1809 if ( this.startContainer.type == CKEDITOR.NODE_ELEMENT && this.startContainer.getName() in nonCells 1810 || this.endContainer.type == CKEDITOR.NODE_ELEMENT && this.endContainer.getName() in nonCells ) 1811 { 1812 this.shrink( CKEDITOR.NODE_ELEMENT, true ); 1813 } 1814 1815 var bookmark = this.createBookmark(); 1816 1817 // Create marker tags for the start and end boundaries. 1818 var startNode = bookmark.startNode; 1819 1820 var endNode; 1821 if ( !collapsed ) 1822 endNode = bookmark.endNode; 1823 1824 // Create the main range which will be used for the selection. 1825 ieRange = this.document.$.body.createTextRange(); 1826 1827 // Position the range at the start boundary. 1828 ieRange.moveToElementText( startNode.$ ); 1829 ieRange.moveStart( 'character', 1 ); 1830 1831 if ( endNode ) 1832 { 1833 // Create a tool range for the end. 1834 var ieRangeEnd = this.document.$.body.createTextRange(); 1835 1836 // Position the tool range at the end. 1837 ieRangeEnd.moveToElementText( endNode.$ ); 1838 1839 // Move the end boundary of the main range to match the tool range. 1840 ieRange.setEndPoint( 'EndToEnd', ieRangeEnd ); 1841 ieRange.moveEnd( 'character', -1 ); 1842 } 1843 else 1844 { 1845 // The isStartMarkerAlone logic comes from V2. It guarantees that the lines 1846 // will expand and that the cursor will be blinking on the right place. 1847 // Actually, we are using this flag just to avoid using this hack in all 1848 // situations, but just on those needed. 1849 var next = startNode.getNext( notWhitespaces ); 1850 isStartMarkerAlone = ( !( next && next.getText && next.getText().match( fillerTextRegex ) ) // already a filler there? 1851 && ( forceExpand || !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ) ) ); 1852 1853 // Append a temporary <span></span> before the selection. 1854 // This is needed to avoid IE destroying selections inside empty 1855 // inline elements, like <b></b> (#253). 1856 // It is also needed when placing the selection right after an inline 1857 // element to avoid the selection moving inside of it. 1858 dummySpan = this.document.createElement( 'span' ); 1859 dummySpan.setHtml( '' ); // Zero Width No-Break Space (U+FEFF). See #1359. 1860 dummySpan.insertBefore( startNode ); 1861 1862 if ( isStartMarkerAlone ) 1863 { 1864 // To expand empty blocks or line spaces after <br>, we need 1865 // instead to have any char, which will be later deleted using the 1866 // selection. 1867 // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359) 1868 this.document.createText( '\ufeff' ).insertBefore( startNode ); 1869 } 1870 } 1871 1872 // Remove the markers (reset the position, because of the changes in the DOM tree). 1873 this.setStartBefore( startNode ); 1874 startNode.remove(); 1875 1876 if ( collapsed ) 1877 { 1878 if ( isStartMarkerAlone ) 1879 { 1880 // Move the selection start to include the temporary \ufeff. 1881 ieRange.moveStart( 'character', -1 ); 1882 1883 ieRange.select(); 1884 1885 // Remove our temporary stuff. 1886 this.document.$.selection.clear(); 1887 } 1888 else 1889 ieRange.select(); 1890 1891 this.moveToPosition( dummySpan, CKEDITOR.POSITION_BEFORE_START ); 1892 dummySpan.remove(); 1893 } 1894 else 1895 { 1896 this.setEndBefore( endNode ); 1897 endNode.remove(); 1898 ieRange.select(); 1899 } 1900 1901 this.document.fire( 'selectionchange' ); 1902 } 1903 : 1904 function() 1905 { 1906 this.document.getSelection().selectRanges( [ this ] ); 1907 }; 1908 } )(); 1909