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