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 /** 709 * Manipulates the selection in a DOM document. 710 * @constructor 711 * @param {CKEDITOR.dom.document} document The DOM document that contains the selection. 712 * @example 713 * var sel = new <strong>CKEDITOR.dom.selection( CKEDITOR.document )</strong>; 714 */ 715 CKEDITOR.dom.selection = function( document ) 716 { 717 var lockedSelection = document.getCustomData( 'cke_locked_selection' ); 718 719 if ( lockedSelection ) 720 return lockedSelection; 721 722 this.document = document; 723 this.isLocked = 0; 724 this._ = 725 { 726 cache : {} 727 }; 728 729 /** 730 * IE BUG: The selection's document may be a different document than the 731 * editor document. Return null if that is the case. 732 */ 733 if ( CKEDITOR.env.ie ) 734 { 735 // Avoid breaking because of it. (#8836) 736 try 737 { 738 var range = this.getNative().createRange(); 739 if ( !range || 740 ( range.item && range.item( 0 ).ownerDocument != this.document.$ ) || 741 ( range.parentElement && range.parentElement().ownerDocument != this.document.$ ) ) 742 { 743 throw 0; 744 } 745 } 746 catch ( e ) 747 { 748 this.isInvalid = true; 749 } 750 } 751 752 return this; 753 }; 754 755 var styleObjectElements = 756 { 757 img:1,hr:1,li:1,table:1,tr:1,td:1,th:1,embed:1,object:1,ol:1,ul:1, 758 a:1,input:1,form:1,select:1,textarea:1,button:1,fieldset:1,thead:1,tfoot:1 759 }; 760 761 CKEDITOR.dom.selection.prototype = 762 { 763 /** 764 * Gets the native selection object from the browser. 765 * @function 766 * @returns {Object} The native browser selection object. 767 * @example 768 * var selection = editor.getSelection().<strong>getNative()</strong>; 769 */ 770 getNative : 771 CKEDITOR.env.ie ? 772 function() 773 { 774 return this._.cache.nativeSel || ( this._.cache.nativeSel = this.document.$.selection ); 775 } 776 : 777 function() 778 { 779 return this._.cache.nativeSel || ( this._.cache.nativeSel = this.document.getWindow().$.getSelection() ); 780 }, 781 782 /** 783 * Gets the type of the current selection. The following values are 784 * available: 785 * <ul> 786 * <li><code>{@link CKEDITOR.SELECTION_NONE}</code> (1): No selection.</li> 787 * <li><code>{@link CKEDITOR.SELECTION_TEXT}</code> (2): A text or a collapsed 788 * selection is selected.</li> 789 * <li><code>{@link CKEDITOR.SELECTION_ELEMENT}</code> (3): An element is 790 * selected.</li> 791 * </ul> 792 * @function 793 * @returns {Number} One of the following constant values: 794 * <code>{@link CKEDITOR.SELECTION_NONE}</code>, <code>{@link CKEDITOR.SELECTION_TEXT}</code>, or 795 * <code>{@link CKEDITOR.SELECTION_ELEMENT}</code>. 796 * @example 797 * if ( editor.getSelection().<strong>getType()</strong> == CKEDITOR.SELECTION_TEXT ) 798 * alert( 'A text is selected' ); 799 */ 800 getType : 801 CKEDITOR.env.ie ? 802 function() 803 { 804 var cache = this._.cache; 805 if ( cache.type ) 806 return cache.type; 807 808 var type = CKEDITOR.SELECTION_NONE; 809 810 try 811 { 812 var sel = this.getNative(), 813 ieType = sel.type; 814 815 if ( ieType == 'Text' ) 816 type = CKEDITOR.SELECTION_TEXT; 817 818 if ( ieType == 'Control' ) 819 type = CKEDITOR.SELECTION_ELEMENT; 820 821 // It is possible that we can still get a text range 822 // object even when type == 'None' is returned by IE. 823 // So we'd better check the object returned by 824 // createRange() rather than by looking at the type. 825 if ( sel.createRange().parentElement ) 826 type = CKEDITOR.SELECTION_TEXT; 827 } 828 catch(e) {} 829 830 return ( cache.type = type ); 831 } 832 : 833 function() 834 { 835 var cache = this._.cache; 836 if ( cache.type ) 837 return cache.type; 838 839 var type = CKEDITOR.SELECTION_TEXT; 840 841 var sel = this.getNative(); 842 843 if ( !sel ) 844 type = CKEDITOR.SELECTION_NONE; 845 else if ( sel.rangeCount == 1 ) 846 { 847 // Check if the actual selection is a control (IMG, 848 // TABLE, HR, etc...). 849 850 var range = sel.getRangeAt(0), 851 startContainer = range.startContainer; 852 853 if ( startContainer == range.endContainer 854 && startContainer.nodeType == 1 855 && ( range.endOffset - range.startOffset ) == 1 856 && styleObjectElements[ startContainer.childNodes[ range.startOffset ].nodeName.toLowerCase() ] ) 857 { 858 type = CKEDITOR.SELECTION_ELEMENT; 859 } 860 } 861 862 return ( cache.type = type ); 863 }, 864 865 /** 866 * Retrieves the <code>{@link CKEDITOR.dom.range}</code> instances that represent the current selection. 867 * Note: Some browsers return multiple ranges even for a continuous selection. Firefox, for example, returns 868 * one range for each table cell when one or more table rows are selected. 869 * @function 870 * @param {Boolean} [onlyEditables] If set to <code>true</code>, this function retrives editable ranges only. 871 * @return {Array} Range instances that represent the current selection. 872 * @example 873 * var ranges = selection.<strong>getRanges()</strong>; 874 * alert( ranges.length ); 875 */ 876 getRanges : (function() 877 { 878 var func = CKEDITOR.env.ie ? 879 ( function() 880 { 881 function getNodeIndex( node ) { return new CKEDITOR.dom.node( node ).getIndex(); } 882 883 // Finds the container and offset for a specific boundary 884 // of an IE range. 885 var getBoundaryInformation = function( range, start ) 886 { 887 // Creates a collapsed range at the requested boundary. 888 range = range.duplicate(); 889 range.collapse( start ); 890 891 // Gets the element that encloses the range entirely. 892 var parent = range.parentElement(), 893 doc = parent.ownerDocument; 894 895 // Empty parent element, e.g. <i>^</i> 896 if ( !parent.hasChildNodes() ) 897 return { container : parent, offset : 0 }; 898 899 var siblings = parent.children, 900 child, 901 sibling, 902 testRange = range.duplicate(), 903 startIndex = 0, 904 endIndex = siblings.length - 1, 905 index = -1, 906 position, 907 distance, 908 container; 909 910 // Binary search over all element childs to test the range to see whether 911 // range is right on the boundary of one element. 912 while ( startIndex <= endIndex ) 913 { 914 index = Math.floor( ( startIndex + endIndex ) / 2 ); 915 child = siblings[ index ]; 916 testRange.moveToElementText( child ); 917 position = testRange.compareEndPoints( 'StartToStart', range ); 918 919 if ( position > 0 ) 920 endIndex = index - 1; 921 else if ( position < 0 ) 922 startIndex = index + 1; 923 else 924 { 925 // IE9 report wrong measurement with compareEndPoints when range anchors between two BRs. 926 // e.g. <p>text<br />^<br /></p> (#7433) 927 if ( CKEDITOR.env.ie9Compat && child.tagName == 'BR' ) 928 { 929 // "Fall back" to w3c selection. 930 var sel = doc.defaultView.getSelection(); 931 return { container : sel[ start ? 'anchorNode' : 'focusNode' ], 932 offset : sel[ start ? 'anchorOffset' : 'focusOffset' ] }; 933 } 934 else 935 return { container : parent, offset : getNodeIndex( child ) }; 936 } 937 } 938 939 // All childs are text nodes, 940 // or to the right hand of test range are all text nodes. (#6992) 941 if ( index == -1 || index == siblings.length - 1 && position < 0 ) 942 { 943 // Adapt test range to embrace the entire parent contents. 944 testRange.moveToElementText( parent ); 945 testRange.setEndPoint( 'StartToStart', range ); 946 947 // IE report line break as CRLF with range.text but 948 // only LF with textnode.nodeValue, normalize them to avoid 949 // breaking character counting logic below. (#3949) 950 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; 951 952 siblings = parent.childNodes; 953 954 // Actual range anchor right beside test range at the boundary of text node. 955 if ( !distance ) 956 { 957 child = siblings[ siblings.length - 1 ]; 958 959 if ( child.nodeType != CKEDITOR.NODE_TEXT ) 960 return { container : parent, offset : siblings.length }; 961 else 962 return { container : child, offset : child.nodeValue.length }; 963 } 964 965 // Start the measuring until distance overflows, meanwhile count the text nodes. 966 var i = siblings.length; 967 while ( distance > 0 && i > 0 ) 968 { 969 sibling = siblings[ --i ]; 970 if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) 971 { 972 container = sibling; 973 distance -= sibling.nodeValue.length; 974 } 975 } 976 977 return { container : container, offset : -distance }; 978 } 979 // Test range was one offset beyond OR behind the anchored text node. 980 else 981 { 982 // Adapt one side of test range to the actual range 983 // for measuring the offset between them. 984 testRange.collapse( position > 0 ? true : false ); 985 testRange.setEndPoint( position > 0 ? 'StartToStart' : 'EndToStart', range ); 986 987 // IE report line break as CRLF with range.text but 988 // only LF with textnode.nodeValue, normalize them to avoid 989 // breaking character counting logic below. (#3949) 990 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; 991 992 // Actual range anchor right beside test range at the inner boundary of text node. 993 if ( !distance ) 994 return { container : parent, offset : getNodeIndex( child ) + ( position > 0 ? 0 : 1 ) }; 995 996 // Start the measuring until distance overflows, meanwhile count the text nodes. 997 while ( distance > 0 ) 998 { 999 try 1000 { 1001 sibling = child[ position > 0 ? 'previousSibling' : 'nextSibling' ]; 1002 if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) 1003 { 1004 distance -= sibling.nodeValue.length; 1005 container = sibling; 1006 } 1007 child = sibling; 1008 } 1009 // Measurement in IE could be somtimes wrong because of <select> element. (#4611) 1010 catch( e ) 1011 { 1012 return { container : parent, offset : getNodeIndex( child ) }; 1013 } 1014 } 1015 1016 return { container : container, offset : position > 0 ? -distance : container.nodeValue.length + distance }; 1017 } 1018 }; 1019 1020 return function() 1021 { 1022 // IE doesn't have range support (in the W3C way), so we 1023 // need to do some magic to transform selections into 1024 // CKEDITOR.dom.range instances. 1025 1026 var sel = this.getNative(), 1027 nativeRange = sel && sel.createRange(), 1028 type = this.getType(), 1029 range; 1030 1031 if ( !sel ) 1032 return []; 1033 1034 if ( type == CKEDITOR.SELECTION_TEXT ) 1035 { 1036 range = new CKEDITOR.dom.range( this.document ); 1037 1038 var boundaryInfo = getBoundaryInformation( nativeRange, true ); 1039 range.setStart( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset ); 1040 1041 boundaryInfo = getBoundaryInformation( nativeRange ); 1042 range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset ); 1043 1044 // Correct an invalid IE range case on empty list item. (#5850) 1045 if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING 1046 && range.endOffset <= range.startContainer.getIndex() ) 1047 { 1048 range.collapse(); 1049 } 1050 1051 return [ range ]; 1052 } 1053 else if ( type == CKEDITOR.SELECTION_ELEMENT ) 1054 { 1055 var retval = []; 1056 1057 for ( var i = 0 ; i < nativeRange.length ; i++ ) 1058 { 1059 var element = nativeRange.item( i ), 1060 parentElement = element.parentNode, 1061 j = 0; 1062 1063 range = new CKEDITOR.dom.range( this.document ); 1064 1065 for (; j < parentElement.childNodes.length && parentElement.childNodes[j] != element ; j++ ) 1066 { /*jsl:pass*/ } 1067 1068 range.setStart( new CKEDITOR.dom.node( parentElement ), j ); 1069 range.setEnd( new CKEDITOR.dom.node( parentElement ), j + 1 ); 1070 retval.push( range ); 1071 } 1072 1073 return retval; 1074 } 1075 1076 return []; 1077 }; 1078 })() 1079 : 1080 function() 1081 { 1082 1083 // On browsers implementing the W3C range, we simply 1084 // tranform the native ranges in CKEDITOR.dom.range 1085 // instances. 1086 1087 var ranges = [], 1088 range, 1089 doc = this.document, 1090 sel = this.getNative(); 1091 1092 if ( !sel ) 1093 return ranges; 1094 1095 // On WebKit, it may happen that we'll have no selection 1096 // available. We normalize it here by replicating the 1097 // behavior of other browsers. 1098 if ( !sel.rangeCount ) 1099 { 1100 range = new CKEDITOR.dom.range( doc ); 1101 range.moveToElementEditStart( doc.getBody() ); 1102 ranges.push( range ); 1103 } 1104 1105 for ( var i = 0 ; i < sel.rangeCount ; i++ ) 1106 { 1107 var nativeRange = sel.getRangeAt( i ); 1108 1109 range = new CKEDITOR.dom.range( doc ); 1110 1111 range.setStart( new CKEDITOR.dom.node( nativeRange.startContainer ), nativeRange.startOffset ); 1112 range.setEnd( new CKEDITOR.dom.node( nativeRange.endContainer ), nativeRange.endOffset ); 1113 ranges.push( range ); 1114 } 1115 return ranges; 1116 }; 1117 1118 return function( onlyEditables ) 1119 { 1120 var cache = this._.cache; 1121 if ( cache.ranges && !onlyEditables ) 1122 return cache.ranges; 1123 else if ( !cache.ranges ) 1124 cache.ranges = new CKEDITOR.dom.rangeList( func.call( this ) ); 1125 1126 // Split range into multiple by read-only nodes. 1127 if ( onlyEditables ) 1128 { 1129 var ranges = cache.ranges; 1130 for ( var i = 0; i < ranges.length; i++ ) 1131 { 1132 var range = ranges[ i ]; 1133 1134 // Drop range spans inside one ready-only node. 1135 var parent = range.getCommonAncestor(); 1136 if ( parent.isReadOnly() ) 1137 ranges.splice( i, 1 ); 1138 1139 if ( range.collapsed ) 1140 continue; 1141 1142 // Range may start inside a non-editable element, 1143 // replace the range start after it. 1144 if ( range.startContainer.isReadOnly() ) 1145 { 1146 var current = range.startContainer; 1147 while( current ) 1148 { 1149 if ( current.is( 'body' ) || !current.isReadOnly() ) 1150 break; 1151 1152 if ( current.type == CKEDITOR.NODE_ELEMENT 1153 && current.getAttribute( 'contentEditable' ) == 'false' ) 1154 range.setStartAfter( current ); 1155 1156 current = current.getParent(); 1157 } 1158 } 1159 1160 var startContainer = range.startContainer, 1161 endContainer = range.endContainer, 1162 startOffset = range.startOffset, 1163 endOffset = range.endOffset, 1164 walkerRange = range.clone(); 1165 1166 // Enlarge range start/end with text node to avoid walker 1167 // being DOM destructive, it doesn't interfere our checking 1168 // of elements below as well. 1169 if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) 1170 { 1171 if ( startOffset >= startContainer.getLength() ) 1172 walkerRange.setStartAfter( startContainer ); 1173 else 1174 walkerRange.setStartBefore( startContainer ); 1175 } 1176 1177 if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) 1178 { 1179 if ( !endOffset ) 1180 walkerRange.setEndBefore( endContainer ); 1181 else 1182 walkerRange.setEndAfter( endContainer ); 1183 } 1184 1185 // Looking for non-editable element inside the range. 1186 var walker = new CKEDITOR.dom.walker( walkerRange ); 1187 walker.evaluator = function( node ) 1188 { 1189 if ( node.type == CKEDITOR.NODE_ELEMENT 1190 && node.isReadOnly() ) 1191 { 1192 var newRange = range.clone(); 1193 range.setEndBefore( node ); 1194 1195 // Drop collapsed range around read-only elements, 1196 // it make sure the range list empty when selecting 1197 // only non-editable elements. 1198 if ( range.collapsed ) 1199 ranges.splice( i--, 1 ); 1200 1201 // Avoid creating invalid range. 1202 if ( !( node.getPosition( walkerRange.endContainer ) & CKEDITOR.POSITION_CONTAINS ) ) 1203 { 1204 newRange.setStartAfter( node ); 1205 if ( !newRange.collapsed ) 1206 ranges.splice( i + 1, 0, newRange ); 1207 } 1208 1209 return true; 1210 } 1211 1212 return false; 1213 }; 1214 1215 walker.next(); 1216 } 1217 } 1218 1219 return cache.ranges; 1220 }; 1221 })(), 1222 1223 /** 1224 * Gets the DOM element in which the selection starts. 1225 * @returns {CKEDITOR.dom.element} The element at the beginning of the 1226 * selection. 1227 * @example 1228 * var element = editor.getSelection().<strong>getStartElement()</strong>; 1229 * alert( element.getName() ); 1230 */ 1231 getStartElement : function() 1232 { 1233 var cache = this._.cache; 1234 if ( cache.startElement !== undefined ) 1235 return cache.startElement; 1236 1237 var node, 1238 sel = this.getNative(); 1239 1240 switch ( this.getType() ) 1241 { 1242 case CKEDITOR.SELECTION_ELEMENT : 1243 return this.getSelectedElement(); 1244 1245 case CKEDITOR.SELECTION_TEXT : 1246 1247 var range = this.getRanges()[0]; 1248 1249 if ( range ) 1250 { 1251 if ( !range.collapsed ) 1252 { 1253 range.optimize(); 1254 1255 // Decrease the range content to exclude particial 1256 // selected node on the start which doesn't have 1257 // visual impact. ( #3231 ) 1258 while ( 1 ) 1259 { 1260 var startContainer = range.startContainer, 1261 startOffset = range.startOffset; 1262 // Limit the fix only to non-block elements.(#3950) 1263 if ( startOffset == ( startContainer.getChildCount ? 1264 startContainer.getChildCount() : startContainer.getLength() ) 1265 && !startContainer.isBlockBoundary() ) 1266 range.setStartAfter( startContainer ); 1267 else break; 1268 } 1269 1270 node = range.startContainer; 1271 1272 if ( node.type != CKEDITOR.NODE_ELEMENT ) 1273 return node.getParent(); 1274 1275 node = node.getChild( range.startOffset ); 1276 1277 if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) 1278 node = range.startContainer; 1279 else 1280 { 1281 var child = node.getFirst(); 1282 while ( child && child.type == CKEDITOR.NODE_ELEMENT ) 1283 { 1284 node = child; 1285 child = child.getFirst(); 1286 } 1287 } 1288 } 1289 else 1290 { 1291 node = range.startContainer; 1292 if ( node.type != CKEDITOR.NODE_ELEMENT ) 1293 node = node.getParent(); 1294 } 1295 1296 node = node.$; 1297 } 1298 } 1299 1300 return cache.startElement = ( node ? new CKEDITOR.dom.element( node ) : null ); 1301 }, 1302 1303 /** 1304 * Gets the currently selected element. 1305 * @returns {CKEDITOR.dom.element} The selected element. Null if no 1306 * selection is available or the selection type is not 1307 * <code>{@link CKEDITOR.SELECTION_ELEMENT}</code>. 1308 * @example 1309 * var element = editor.getSelection().<strong>getSelectedElement()</strong>; 1310 * alert( element.getName() ); 1311 */ 1312 getSelectedElement : function() 1313 { 1314 var cache = this._.cache; 1315 if ( cache.selectedElement !== undefined ) 1316 return cache.selectedElement; 1317 1318 var self = this; 1319 1320 var node = CKEDITOR.tools.tryThese( 1321 // Is it native IE control type selection? 1322 function() 1323 { 1324 return self.getNative().createRange().item( 0 ); 1325 }, 1326 // If a table or list is fully selected. 1327 function() 1328 { 1329 var root, 1330 retval, 1331 range = self.getRanges()[ 0 ], 1332 ancestor = range.getCommonAncestor( 1, 1 ), 1333 tags = { table:1,ul:1,ol:1,dl:1 }; 1334 1335 for ( var t in tags ) 1336 { 1337 if ( ( root = ancestor.getAscendant( t, 1 ) ) ) 1338 break; 1339 } 1340 1341 if ( root ) 1342 { 1343 // Enlarging the start boundary. 1344 var testRange = new CKEDITOR.dom.range( this.document ); 1345 testRange.setStartAt( root, CKEDITOR.POSITION_AFTER_START ); 1346 testRange.setEnd( range.startContainer, range.startOffset ); 1347 1348 var enlargeables = CKEDITOR.tools.extend( tags, CKEDITOR.dtd.$listItem, CKEDITOR.dtd.$tableContent ), 1349 walker = new CKEDITOR.dom.walker( testRange ), 1350 // Check the range is at the inner boundary of the structural element. 1351 guard = function( walker, isEnd ) 1352 { 1353 return function( node, isWalkOut ) 1354 { 1355 if ( node.type == CKEDITOR.NODE_TEXT && ( !CKEDITOR.tools.trim( node.getText() ) || node.getParent().data( 'cke-bookmark' ) ) ) 1356 return true; 1357 1358 var tag; 1359 if ( node.type == CKEDITOR.NODE_ELEMENT ) 1360 { 1361 tag = node.getName(); 1362 1363 // Bypass bogus br at the end of block. 1364 if ( tag == 'br' && isEnd && node.equals( node.getParent().getBogus() ) ) 1365 return true; 1366 1367 if ( isWalkOut && tag in enlargeables || tag in CKEDITOR.dtd.$removeEmpty ) 1368 return true; 1369 } 1370 1371 walker.halted = 1; 1372 return false; 1373 }; 1374 }; 1375 1376 walker.guard = guard( walker ); 1377 1378 if ( walker.checkBackward() && !walker.halted ) 1379 { 1380 walker = new CKEDITOR.dom.walker( testRange ); 1381 testRange.setStart( range.endContainer, range.endOffset ); 1382 testRange.setEndAt( root, CKEDITOR.POSITION_BEFORE_END ); 1383 walker.guard = guard( walker, 1 ); 1384 if ( walker.checkForward() && !walker.halted ) 1385 retval = root.$; 1386 } 1387 } 1388 1389 if ( !retval ) 1390 throw 0; 1391 1392 return retval; 1393 }, 1394 // Figure it out by checking if there's a single enclosed 1395 // node of the range. 1396 function() 1397 { 1398 var range = self.getRanges()[ 0 ], 1399 enclosed, 1400 selected; 1401 1402 // Check first any enclosed element, e.g. <ul>[<li><a href="#">item</a></li>]</ul> 1403 for ( var i = 2; i && !( ( enclosed = range.getEnclosedNode() ) 1404 && ( enclosed.type == CKEDITOR.NODE_ELEMENT ) 1405 && styleObjectElements[ enclosed.getName() ] 1406 && ( selected = enclosed ) ); i-- ) 1407 { 1408 // Then check any deep wrapped element, e.g. [<b><i><img /></i></b>] 1409 range.shrink( CKEDITOR.SHRINK_ELEMENT ); 1410 } 1411 1412 return selected.$; 1413 }); 1414 1415 return cache.selectedElement = ( node ? new CKEDITOR.dom.element( node ) : null ); 1416 }, 1417 1418 /** 1419 * Retrieves the text contained within the range. An empty string is returned for non-text selection. 1420 * @returns {String} A string of text within the current selection. 1421 * @since 3.6.1 1422 * @example 1423 * var text = editor.getSelection().<strong>getSelectedText()</strong>; 1424 * alert( text ); 1425 */ 1426 getSelectedText : function() 1427 { 1428 var cache = this._.cache; 1429 if ( cache.selectedText !== undefined ) 1430 return cache.selectedText; 1431 1432 var text = '', 1433 nativeSel = this.getNative(); 1434 if ( this.getType() == CKEDITOR.SELECTION_TEXT ) 1435 text = CKEDITOR.env.ie ? nativeSel.createRange().text : nativeSel.toString(); 1436 1437 return ( cache.selectedText = text ); 1438 }, 1439 1440 /** 1441 * Locks the selection made in the editor in order to make it possible to 1442 * manipulate it without browser interference. A locked selection is 1443 * cached and remains unchanged until it is released with the <code>#unlock</code> 1444 * method. 1445 * @example 1446 * editor.getSelection().<strong>lock()</strong>; 1447 */ 1448 lock : function() 1449 { 1450 // Call all cacheable function. 1451 this.getRanges(); 1452 this.getStartElement(); 1453 this.getSelectedElement(); 1454 this.getSelectedText(); 1455 1456 // The native selection is not available when locked. 1457 this._.cache.nativeSel = {}; 1458 1459 this.isLocked = 1; 1460 1461 // Save this selection inside the DOM document. 1462 this.document.setCustomData( 'cke_locked_selection', this ); 1463 }, 1464 1465 /** 1466 * Unlocks the selection made in the editor and locked with the <code>#lock</code> method. 1467 * An unlocked selection is no longer cached and can be changed. 1468 * @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. 1469 * @example 1470 * editor.getSelection().<strong>unlock()</strong>; 1471 */ 1472 unlock : function( restore ) 1473 { 1474 var doc = this.document, 1475 lockedSelection = doc.getCustomData( 'cke_locked_selection' ); 1476 1477 if ( lockedSelection ) 1478 { 1479 doc.setCustomData( 'cke_locked_selection', null ); 1480 1481 if ( restore ) 1482 { 1483 var selectedElement = lockedSelection.getSelectedElement(), 1484 ranges = !selectedElement && lockedSelection.getRanges(); 1485 1486 this.isLocked = 0; 1487 this.reset(); 1488 1489 if ( selectedElement ) 1490 this.selectElement( selectedElement ); 1491 else 1492 this.selectRanges( ranges ); 1493 } 1494 } 1495 1496 if ( !lockedSelection || !restore ) 1497 { 1498 this.isLocked = 0; 1499 this.reset(); 1500 } 1501 }, 1502 1503 /** 1504 * Clears the selection cache. 1505 * @example 1506 * editor.getSelection().<strong>reset()</strong>; 1507 */ 1508 reset : function() 1509 { 1510 this._.cache = {}; 1511 }, 1512 1513 /** 1514 * Makes the current selection of type <code>{@link CKEDITOR.SELECTION_ELEMENT}</code> by enclosing the specified element. 1515 * @param {CKEDITOR.dom.element} element The element to enclose in the selection. 1516 * @example 1517 * var element = editor.document.getById( 'sampleElement' ); 1518 * editor.getSelection.<strong>selectElement( element )</strong>; 1519 */ 1520 selectElement : function( element ) 1521 { 1522 if ( this.isLocked ) 1523 { 1524 var range = new CKEDITOR.dom.range( this.document ); 1525 range.setStartBefore( element ); 1526 range.setEndAfter( element ); 1527 1528 this._.cache.selectedElement = element; 1529 this._.cache.startElement = element; 1530 this._.cache.ranges = new CKEDITOR.dom.rangeList( range ); 1531 this._.cache.type = CKEDITOR.SELECTION_ELEMENT; 1532 1533 return; 1534 } 1535 1536 range = new CKEDITOR.dom.range( element.getDocument() ); 1537 range.setStartBefore( element ); 1538 range.setEndAfter( element ); 1539 range.select(); 1540 1541 this.document.fire( 'selectionchange' ); 1542 this.reset(); 1543 1544 }, 1545 1546 /** 1547 * Clears the original selection and adds the specified ranges 1548 * to the document selection. 1549 * @param {Array} ranges An array of <code>{@link CKEDITOR.dom.range}</code> instances representing ranges to be added to the document. 1550 * @example 1551 * var ranges = new CKEDITOR.dom.range( editor.document ); 1552 * editor.getSelection().<strong>selectRanges( [ ranges ] )</strong>; 1553 */ 1554 selectRanges : function( ranges ) 1555 { 1556 if ( this.isLocked ) 1557 { 1558 this._.cache.selectedElement = null; 1559 this._.cache.startElement = ranges[ 0 ] && ranges[ 0 ].getTouchedStartNode(); 1560 this._.cache.ranges = new CKEDITOR.dom.rangeList( ranges ); 1561 this._.cache.type = CKEDITOR.SELECTION_TEXT; 1562 1563 return; 1564 } 1565 1566 if ( CKEDITOR.env.ie ) 1567 { 1568 if ( ranges.length > 1 ) 1569 { 1570 // IE doesn't accept multiple ranges selection, so we join all into one. 1571 var last = ranges[ ranges.length -1 ] ; 1572 ranges[ 0 ].setEnd( last.endContainer, last.endOffset ); 1573 ranges.length = 1; 1574 } 1575 1576 if ( ranges[ 0 ] ) 1577 ranges[ 0 ].select(); 1578 1579 this.reset(); 1580 } 1581 else 1582 { 1583 var sel = this.getNative(); 1584 1585 // getNative() returns null if iframe is "display:none" in FF. (#6577) 1586 if ( !sel ) 1587 return; 1588 1589 // Opera: The above hack work around a *visually wrong* text selection that 1590 // happens in certain situation. (#6874) 1591 if ( CKEDITOR.env.opera ) 1592 this.document.$.execCommand( 'SelectAll', false ); 1593 1594 if ( ranges.length ) 1595 { 1596 sel.removeAllRanges(); 1597 // Remove any existing filling char first. 1598 CKEDITOR.env.webkit && removeFillingChar( this.document ); 1599 } 1600 1601 for ( var i = 0 ; i < ranges.length ; i++ ) 1602 { 1603 // Joining sequential ranges introduced by 1604 // readonly elements protection. 1605 if ( i < ranges.length -1 ) 1606 { 1607 var left = ranges[ i ], right = ranges[ i +1 ], 1608 between = left.clone(); 1609 between.setStart( left.endContainer, left.endOffset ); 1610 between.setEnd( right.startContainer, right.startOffset ); 1611 1612 // Don't confused by Firefox adjancent multi-ranges 1613 // introduced by table cells selection. 1614 if ( !between.collapsed ) 1615 { 1616 between.shrink( CKEDITOR.NODE_ELEMENT, true ); 1617 var ancestor = between.getCommonAncestor(), 1618 enclosed = between.getEnclosedNode(); 1619 1620 // The following cases has to be considered: 1621 // 1. <span contenteditable="false">[placeholder]</span> 1622 // 2. <input contenteditable="false" type="radio"/> (#6621) 1623 if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) 1624 { 1625 right.setStart( left.startContainer, left.startOffset ); 1626 ranges.splice( i--, 1 ); 1627 continue; 1628 } 1629 } 1630 } 1631 1632 var range = ranges[ i ]; 1633 var nativeRange = this.document.$.createRange(); 1634 var startContainer = range.startContainer; 1635 1636 // In FF2, if we have a collapsed range, inside an empty 1637 // element, we must add something to it otherwise the caret 1638 // will not be visible. 1639 // In Opera instead, the selection will be moved out of the 1640 // element. (#4657) 1641 if ( range.collapsed && 1642 ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.version < 10900 ) ) && 1643 startContainer.type == CKEDITOR.NODE_ELEMENT && 1644 !startContainer.getChildCount() ) 1645 { 1646 startContainer.appendText( '' ); 1647 } 1648 1649 if ( range.collapsed 1650 && CKEDITOR.env.webkit 1651 && rangeRequiresFix( range ) ) 1652 { 1653 // Append a zero-width space so WebKit will not try to 1654 // move the selection by itself (#1272). 1655 var fillingChar = createFillingChar( this.document ); 1656 range.insertNode( fillingChar ) ; 1657 1658 var next = fillingChar.getNext(); 1659 1660 // If the filling char is followed by a <br>, whithout 1661 // having something before it, it'll not blink. 1662 // Let's remove it in this case. 1663 if ( next && !fillingChar.getPrevious() && next.type == CKEDITOR.NODE_ELEMENT && next.getName() == 'br' ) 1664 { 1665 removeFillingChar( this.document ); 1666 range.moveToPosition( next, CKEDITOR.POSITION_BEFORE_START ); 1667 } 1668 else 1669 range.moveToPosition( fillingChar, CKEDITOR.POSITION_AFTER_END ); 1670 } 1671 1672 nativeRange.setStart( range.startContainer.$, range.startOffset ); 1673 1674 try 1675 { 1676 nativeRange.setEnd( range.endContainer.$, range.endOffset ); 1677 } 1678 catch ( e ) 1679 { 1680 // There is a bug in Firefox implementation (it would be too easy 1681 // otherwise). The new start can't be after the end (W3C says it can). 1682 // So, let's create a new range and collapse it to the desired point. 1683 if ( e.toString().indexOf( 'NS_ERROR_ILLEGAL_VALUE' ) >= 0 ) 1684 { 1685 range.collapse( 1 ); 1686 nativeRange.setEnd( range.endContainer.$, range.endOffset ); 1687 } 1688 else 1689 throw e; 1690 } 1691 1692 // Select the range. 1693 sel.addRange( nativeRange ); 1694 } 1695 1696 // Don't miss selection change event for non-IEs. 1697 this.document.fire( 'selectionchange' ); 1698 this.reset(); 1699 } 1700 }, 1701 1702 /** 1703 * Creates a bookmark for each range of this selection (from <code>#getRanges</code>) 1704 * by calling the <code>{@link CKEDITOR.dom.range.prototype.createBookmark}</code> method, 1705 * with extra care taken to avoid interference among those ranges. The arguments 1706 * received are the same as with the underlying range method. 1707 * @returns {Array} Array of bookmarks for each range. 1708 * @example 1709 * var bookmarks = editor.getSelection().<strong>createBookmarks()</strong>; 1710 */ 1711 createBookmarks : function( serializable ) 1712 { 1713 return this.getRanges().createBookmarks( serializable ); 1714 }, 1715 1716 /** 1717 * Creates a bookmark for each range of this selection (from <code>#getRanges</code>) 1718 * by calling the <code>{@link CKEDITOR.dom.range.prototype.createBookmark2}</code> method, 1719 * with extra care taken to avoid interference among those ranges. The arguments 1720 * received are the same as with the underlying range method. 1721 * @returns {Array} Array of bookmarks for each range. 1722 * @example 1723 * var bookmarks = editor.getSelection().<strong>createBookmarks2()</strong>; 1724 */ 1725 createBookmarks2 : function( normalized ) 1726 { 1727 return this.getRanges().createBookmarks2( normalized ); 1728 }, 1729 1730 /** 1731 * Selects the virtual ranges denoted by the bookmarks by calling <code>#selectRanges</code>. 1732 * @param {Array} bookmarks The bookmarks representing ranges to be selected. 1733 * @returns {CKEDITOR.dom.selection} This selection object, after the ranges were selected. 1734 * @example 1735 * var bookmarks = editor.getSelection().createBookmarks(); 1736 * editor.getSelection().<strong>selectBookmarks( bookmarks )</strong>; 1737 */ 1738 selectBookmarks : function( bookmarks ) 1739 { 1740 var ranges = []; 1741 for ( var i = 0 ; i < bookmarks.length ; i++ ) 1742 { 1743 var range = new CKEDITOR.dom.range( this.document ); 1744 range.moveToBookmark( bookmarks[i] ); 1745 ranges.push( range ); 1746 } 1747 this.selectRanges( ranges ); 1748 return this; 1749 }, 1750 1751 /** 1752 * Retrieves the common ancestor node of the first range and the last range. 1753 * @returns {CKEDITOR.dom.element} The common ancestor of the selection. 1754 * @example 1755 * var ancestor = editor.getSelection().<strong>getCommonAncestor()</strong>; 1756 */ 1757 getCommonAncestor : function() 1758 { 1759 var ranges = this.getRanges(), 1760 startNode = ranges[ 0 ].startContainer, 1761 endNode = ranges[ ranges.length - 1 ].endContainer; 1762 return startNode.getCommonAncestor( endNode ); 1763 }, 1764 1765 /** 1766 * Moves the scrollbar to the starting position of the current selection. 1767 * @example 1768 * editor.getSelection().<strong>scrollIntoView()</strong>; 1769 */ 1770 scrollIntoView : function() 1771 { 1772 // If we have split the block, adds a temporary span at the 1773 // range position and scroll relatively to it. 1774 var start = this.getStartElement(); 1775 start.scrollIntoView(); 1776 } 1777 }; 1778 1779 var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ), 1780 isVisible = CKEDITOR.dom.walker.invisible( 1 ), 1781 fillerTextRegex = /\ufeff|\u00a0/, 1782 nonCells = { table:1,tbody:1,tr:1 }; 1783 1784 CKEDITOR.dom.range.prototype.select = 1785 CKEDITOR.env.ie ? 1786 // V2 1787 function( forceExpand ) 1788 { 1789 var collapsed = this.collapsed, 1790 isStartMarkerAlone, dummySpan, ieRange; 1791 1792 // Try to make a object selection. 1793 var selected = this.getEnclosedNode(); 1794 if ( selected ) 1795 { 1796 try 1797 { 1798 ieRange = this.document.$.body.createControlRange(); 1799 ieRange.addElement( selected.$ ); 1800 ieRange.select(); 1801 return; 1802 } 1803 catch( er ) {} 1804 } 1805 1806 // IE doesn't support selecting the entire table row/cell, move the selection into cells, e.g. 1807 // <table><tbody><tr>[<td>cell</b></td>... => <table><tbody><tr><td>[cell</td>... 1808 if ( this.startContainer.type == CKEDITOR.NODE_ELEMENT && this.startContainer.getName() in nonCells 1809 || this.endContainer.type == CKEDITOR.NODE_ELEMENT && this.endContainer.getName() in nonCells ) 1810 { 1811 this.shrink( CKEDITOR.NODE_ELEMENT, true ); 1812 } 1813 1814 var bookmark = this.createBookmark(); 1815 1816 // Create marker tags for the start and end boundaries. 1817 var startNode = bookmark.startNode; 1818 1819 var endNode; 1820 if ( !collapsed ) 1821 endNode = bookmark.endNode; 1822 1823 // Create the main range which will be used for the selection. 1824 ieRange = this.document.$.body.createTextRange(); 1825 1826 // Position the range at the start boundary. 1827 ieRange.moveToElementText( startNode.$ ); 1828 ieRange.moveStart( 'character', 1 ); 1829 1830 if ( endNode ) 1831 { 1832 // Create a tool range for the end. 1833 var ieRangeEnd = this.document.$.body.createTextRange(); 1834 1835 // Position the tool range at the end. 1836 ieRangeEnd.moveToElementText( endNode.$ ); 1837 1838 // Move the end boundary of the main range to match the tool range. 1839 ieRange.setEndPoint( 'EndToEnd', ieRangeEnd ); 1840 ieRange.moveEnd( 'character', -1 ); 1841 } 1842 else 1843 { 1844 // The isStartMarkerAlone logic comes from V2. It guarantees that the lines 1845 // will expand and that the cursor will be blinking on the right place. 1846 // Actually, we are using this flag just to avoid using this hack in all 1847 // situations, but just on those needed. 1848 var next = startNode.getNext( notWhitespaces ); 1849 isStartMarkerAlone = ( !( next && next.getText && next.getText().match( fillerTextRegex ) ) // already a filler there? 1850 && ( forceExpand || !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ) ) ); 1851 1852 // Append a temporary <span></span> before the selection. 1853 // This is needed to avoid IE destroying selections inside empty 1854 // inline elements, like <b></b> (#253). 1855 // It is also needed when placing the selection right after an inline 1856 // element to avoid the selection moving inside of it. 1857 dummySpan = this.document.createElement( 'span' ); 1858 dummySpan.setHtml( '' ); // Zero Width No-Break Space (U+FEFF). See #1359. 1859 dummySpan.insertBefore( startNode ); 1860 1861 if ( isStartMarkerAlone ) 1862 { 1863 // To expand empty blocks or line spaces after <br>, we need 1864 // instead to have any char, which will be later deleted using the 1865 // selection. 1866 // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359) 1867 this.document.createText( '\ufeff' ).insertBefore( startNode ); 1868 } 1869 } 1870 1871 // Remove the markers (reset the position, because of the changes in the DOM tree). 1872 this.setStartBefore( startNode ); 1873 startNode.remove(); 1874 1875 if ( collapsed ) 1876 { 1877 if ( isStartMarkerAlone ) 1878 { 1879 // Move the selection start to include the temporary \ufeff. 1880 ieRange.moveStart( 'character', -1 ); 1881 1882 ieRange.select(); 1883 1884 // Remove our temporary stuff. 1885 this.document.$.selection.clear(); 1886 } 1887 else 1888 ieRange.select(); 1889 1890 this.moveToPosition( dummySpan, CKEDITOR.POSITION_BEFORE_START ); 1891 dummySpan.remove(); 1892 } 1893 else 1894 { 1895 this.setEndBefore( endNode ); 1896 endNode.remove(); 1897 ieRange.select(); 1898 } 1899 1900 this.document.fire( 'selectionchange' ); 1901 } 1902 : 1903 function() 1904 { 1905 this.document.getSelection().selectRanges( [ this ] ); 1906 }; 1907 } )(); 1908