aboutsummaryrefslogtreecommitdiff
path: root/sources/core/selection.js
diff options
context:
space:
mode:
Diffstat (limited to 'sources/core/selection.js')
-rw-r--r--sources/core/selection.js403
1 files changed, 336 insertions, 67 deletions
diff --git a/sources/core/selection.js b/sources/core/selection.js
index eef28a3..d44db3b 100644
--- a/sources/core/selection.js
+++ b/sources/core/selection.js
@@ -6,9 +6,211 @@
6( function() { 6( function() {
7 var isMSSelection = typeof window.getSelection != 'function', 7 var isMSSelection = typeof window.getSelection != 'function',
8 nextRev = 1, 8 nextRev = 1,
9 // #13816 9 // http://dev.ckeditor.com/ticket/13816
10 fillingCharSequence = CKEDITOR.tools.repeat( '\u200b', 7 ), 10 fillingCharSequence = CKEDITOR.tools.repeat( '\u200b', 7 ),
11 fillingCharSequenceRegExp = new RegExp( fillingCharSequence + '( )?', 'g' ); 11 fillingCharSequenceRegExp = new RegExp( fillingCharSequence + '( )?', 'g' ),
12 isSelectingTable;
13
14 // #### table selection : START
15 // @param {CKEDITOR.dom.range[]} ranges
16 // @param {Boolean} allowPartially Whether a collapsed selection within table is recognized to be a valid selection.
17 // This happens for WebKits on MacOS, when you right click inside the table.
18 function isTableSelection( ranges, allowPartially ) {
19 if ( ranges.length === 0 ) {
20 return false;
21 }
22
23 var node,
24 i;
25
26 function isPartiallySelected( range ) {
27 var startCell = range.startContainer.getAscendant( { td: 1, th: 1 }, true ),
28 endCell = range.endContainer.getAscendant( { td: 1, th: 1 }, true ),
29 trim = CKEDITOR.tools.trim,
30 selected;
31
32 // Check if the selection is inside one cell and we don't have any nested table contents selected.
33 if ( !startCell || !startCell.equals( endCell ) || startCell.findOne( 'td, th, tr, tbody, table' ) ) {
34 return false;
35 }
36
37 selected = range.cloneContents();
38
39 // Empty selection is still partially selected.
40 if ( !selected.getFirst() ) {
41 return true;
42 }
43
44 return trim( selected.getFirst().getText() ) !== trim( startCell.getText() );
45 }
46
47 // Edge case: partially selected text node inside one table cell or cursor inside cell.
48 if ( !allowPartially && ranges.length === 1 &&
49 ( ranges[ 0 ].collapsed || isPartiallySelected( ranges[ 0 ] ) ) ) {
50 return false;
51 }
52
53 for ( i = 0; i < ranges.length; i++ ) {
54 node = ranges[ i ]._getTableElement();
55
56 if ( !node ) {
57 return false;
58 }
59 }
60
61 return true;
62 }
63
64 // After performing fake table selection, the real selection is limited
65 // to the first selected cell. Therefore to check if the real selection
66 // matches the fake selection, we check if the table cell from fake selection's
67 // first range and real selection's range are the same.
68 // Also if the selection is collapsed, we should check if it's placed inside the table
69 // in which the fake selection is or inside nested table. Such selection occurs after right mouse click.
70 function isRealTableSelection( selection, fakeSelection ) {
71 var ranges = selection.getRanges(),
72 fakeRanges = fakeSelection.getRanges(),
73 table = ranges.length && ranges[ 0 ]._getTableElement() &&
74 ranges[ 0 ]._getTableElement().getAscendant( 'table', true ),
75 fakeTable = fakeRanges.length && fakeRanges[ 0 ]._getTableElement() &&
76 fakeRanges[ 0 ]._getTableElement().getAscendant( 'table', true ),
77 isTableRange = ranges.length === 1 && ranges[ 0 ]._getTableElement() &&
78 ranges[ 0 ]._getTableElement().is( 'table' ),
79 isFakeTableRange = fakeRanges.length === 1 && fakeRanges[ 0 ]._getTableElement() &&
80 fakeRanges[ 0 ]._getTableElement().is( 'table' );
81
82 function isValidTableSelection( table, fakeTable, ranges, fakeRanges ) {
83 var isMenuOpen = ranges.length === 1 && ranges[ 0 ].collapsed,
84 // In case of WebKit on MacOS, when checking real selection, we must allow selection to be partial.
85 // Otherwise the check will fail for table selection with opened context menu.
86 isInTable = isTableSelection( ranges, !!CKEDITOR.env.webkit ) && isTableSelection( fakeRanges );
87
88 return isSameTable( table, fakeTable ) && ( isMenuOpen || isInTable );
89 }
90
91 function isSameTable( table, fakeTable ) {
92 if ( !table || !fakeTable ) {
93 return false;
94 }
95
96 return table.equals( fakeTable ) || fakeTable.contains( table );
97 }
98
99 if ( isValidTableSelection( table, fakeTable, ranges, fakeRanges ) ) {
100 // Edge case: when editor contains only table and that table is selected using selectAll command,
101 // then the selection is not properly refreshed and it must be done manually.
102 if ( isTableRange && !isFakeTableRange ) {
103 fakeSelection.selectRanges( ranges );
104 }
105 return true;
106 }
107
108 return false;
109 }
110
111 function getSelectedCells( ranges ) {
112 var cells = [],
113 node,
114 i;
115
116 function getCellsFromElement( element ) {
117 var cells = element.find( 'td, th' ),
118 cellsArray = [],
119 i;
120
121 for ( i = 0; i < cells.count(); i++ ) {
122 cellsArray.push( cells.getItem( i ) );
123 }
124
125 return cellsArray;
126 }
127
128 for ( i = 0; i < ranges.length; i++ ) {
129 node = ranges[ i ]._getTableElement();
130
131 if ( node.is && node.is( { td: 1, th: 1 } ) ) {
132 cells.push( node );
133 } else {
134 cells = cells.concat( getCellsFromElement( node ) );
135 }
136 }
137
138 return cells;
139 }
140
141 // Cells in the same row are separated by tab and the rows are separated by new line, e.g.
142 // Cell 1.1 Cell 1.2
143 // Cell 2.1 Cell 2.2
144 function getTextFromSelectedCells( ranges ) {
145 var cells = getSelectedCells( ranges ),
146 txt = '',
147 currentRow = [],
148 lastRow,
149 i;
150
151 for ( i = 0; i < cells.length; i++ ) {
152 if ( lastRow && !lastRow.equals( cells[ i ].getAscendant( 'tr' ) ) ) {
153 txt += currentRow.join( '\t' ) + '\n';
154 lastRow = cells[ i ].getAscendant( 'tr' );
155 currentRow = [];
156 } else if ( i === 0 ) {
157 lastRow = cells[ i ].getAscendant( 'tr' );
158 }
159
160 currentRow.push( cells[ i ].getText() );
161 }
162
163 txt += currentRow.join( '\t' );
164
165 return txt;
166 }
167
168 function performFakeTableSelection( ranges ) {
169 var editor = this.root.editor,
170 realSelection = editor.getSelection( 1 ),
171 cache;
172
173 // Cleanup after previous selection - e.g. remove hidden sel container.
174 this.reset();
175
176 // Indicate that the table is being fake-selected to prevent infinite loop
177 // inside `selectRanges`.
178 isSelectingTable = true;
179
180 // Cancel selectionchange for the real selection.
181 realSelection.root.once( 'selectionchange', function( evt ) {
182 evt.cancel();
183 }, null, null, 0 );
184
185 // Move real selection to the first selected range.
186 realSelection.selectRanges( [ ranges[ 0 ] ] );
187
188 cache = this._.cache;
189
190 // Caches given ranges.
191 cache.ranges = new CKEDITOR.dom.rangeList( ranges );
192 cache.type = CKEDITOR.SELECTION_TEXT;
193 cache.selectedElement = ranges[ 0 ]._getTableElement();
194
195 // `selectedText` should contain text from all selected data ("plain text table")
196 // to be compatible with Firefox's implementation.
197 cache.selectedText = getTextFromSelectedCells( ranges );
198
199 // Properties that will not be available when isFake.
200 cache.nativeSel = null;
201
202 this.isFake = 1;
203 this.rev = nextRev++;
204
205 // Save this selection, so it can be returned by editor.getSelection().
206 editor._.fakeSelection = this;
207
208 isSelectingTable = false;
209
210 // Fire selectionchange, just like a normal selection.
211 this.root.fire( 'selectionchange' );
212 }
213 // #### table selection : END
12 214
13 // #### checkSelectionChange : START 215 // #### checkSelectionChange : START
14 216
@@ -22,10 +224,9 @@
22 224
23 if ( sel ) { 225 if ( sel ) {
24 realSel = this.getSelection( 1 ); 226 realSel = this.getSelection( 1 );
25 227 // If real (not locked/stored) selection was moved from hidden container
26 // If real (not locked/stored) selection was moved from hidden container, 228 // or is not a table one, then the fake-selection must be invalidated.
27 // then the fake-selection must be invalidated. 229 if ( !realSel || ( !realSel.isHidden() && !isRealTableSelection( realSel, sel ) ) ) {
28 if ( !realSel || !realSel.isHidden() ) {
29 // Remove the cache from fake-selection references in use elsewhere. 230 // Remove the cache from fake-selection references in use elsewhere.
30 sel.reset(); 231 sel.reset();
31 232
@@ -47,7 +248,7 @@
47 248
48 var currentPath = this.elementPath(); 249 var currentPath = this.elementPath();
49 if ( !currentPath.compare( this._.selectionPreviousPath ) ) { 250 if ( !currentPath.compare( this._.selectionPreviousPath ) ) {
50 // Handle case when dialog inserts new element but parent block and path (so also focus context) does not change. (#13362) 251 // Handle case when dialog inserts new element but parent block and path (so also focus context) does not change. (http://dev.ckeditor.com/ticket/13362)
51 var sameBlockParent = this._.selectionPreviousPath && this._.selectionPreviousPath.blockLimit.equals( currentPath.blockLimit ); 252 var sameBlockParent = this._.selectionPreviousPath && this._.selectionPreviousPath.blockLimit.equals( currentPath.blockLimit );
52 // Cache the active element, which we'll eventually lose on Webkit. 253 // Cache the active element, which we'll eventually lose on Webkit.
53 if ( CKEDITOR.env.webkit && !sameBlockParent ) 254 if ( CKEDITOR.env.webkit && !sameBlockParent )
@@ -97,7 +298,7 @@
97 // * is a visible node, 298 // * is a visible node,
98 // * is a non-empty element (this rule will accept elements like <strong></strong> because they 299 // * is a non-empty element (this rule will accept elements like <strong></strong> because they
99 // they were not accepted by the isVisible() check, not not <br> which cannot absorb the caret). 300 // they were not accepted by the isVisible() check, not not <br> which cannot absorb the caret).
100 // See #12621. 301 // See http://dev.ckeditor.com/ticket/12621.
101 function mayAbsorbCaret( node ) { 302 function mayAbsorbCaret( node ) {
102 if ( isVisible( node ) ) 303 if ( isVisible( node ) )
103 return true; 304 return true;
@@ -138,8 +339,8 @@
138 if ( ctxRequiresFix( previous ) || ctxRequiresFix( next, 1 ) ) 339 if ( ctxRequiresFix( previous ) || ctxRequiresFix( next, 1 ) )
139 return true; 340 return true;
140 341
141 // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (#7222) 342 // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (http://dev.ckeditor.com/ticket/7222)
142 // If you found this line confusing check #12655. 343 // If you found this line confusing check http://dev.ckeditor.com/ticket/12655.
143 if ( !( previous || next ) && !( ct.type == CKEDITOR.NODE_ELEMENT && ct.isBlockBoundary() && ct.getBogus() ) ) 344 if ( !( previous || next ) && !( ct.type == CKEDITOR.NODE_ELEMENT && ct.isBlockBoundary() && ct.getBogus() ) )
144 return true; 345 return true;
145 346
@@ -155,7 +356,7 @@
155 return fillingChar; 356 return fillingChar;
156 } 357 }
157 358
158 // Checks if a filling char has been used, eventualy removing it (#1272). 359 // Checks if a filling char has been used, eventually removing it (http://dev.ckeditor.com/ticket/1272).
159 function checkFillingCharSequenceNodeReady( editable ) { 360 function checkFillingCharSequenceNodeReady( editable ) {
160 var fillingChar = editable.getCustomData( 'cke-fillingChar' ); 361 var fillingChar = editable.getCustomData( 'cke-fillingChar' );
161 362
@@ -164,6 +365,7 @@
164 // creating it. 365 // creating it.
165 if ( fillingChar.getCustomData( 'ready' ) ) { 366 if ( fillingChar.getCustomData( 'ready' ) ) {
166 removeFillingCharSequenceNode( editable ); 367 removeFillingCharSequenceNode( editable );
368 editable.editor.fire( 'selectionCheck' );
167 } else { 369 } else {
168 fillingChar.setCustomData( 'ready', 1 ); 370 fillingChar.setCustomData( 'ready', 1 );
169 } 371 }
@@ -175,7 +377,7 @@
175 377
176 if ( fillingChar ) { 378 if ( fillingChar ) {
177 // Text selection position might get mangled by 379 // Text selection position might get mangled by
178 // subsequent dom modification, save it now for restoring. (#8617) 380 // subsequent dom modification, save it now for restoring. (http://dev.ckeditor.com/ticket/8617)
179 if ( keepSelection !== false ) { 381 if ( keepSelection !== false ) {
180 var sel = editable.getDocument().getSelection().getNative(), 382 var sel = editable.getDocument().getSelection().getNative(),
181 // Be error proof. 383 // Be error proof.
@@ -211,11 +413,11 @@
211 } 413 }
212 } 414 }
213 415
214 // #13816 416 // http://dev.ckeditor.com/ticket/13816
215 function removeFillingCharSequenceString( str, nbspAware ) { 417 function removeFillingCharSequenceString( str, nbspAware ) {
216 if ( nbspAware ) { 418 if ( nbspAware ) {
217 return str.replace( fillingCharSequenceRegExp, function( m, p ) { 419 return str.replace( fillingCharSequenceRegExp, function( m, p ) {
218 // #10291 if filling char is followed by a space replace it with NBSP. 420 // http://dev.ckeditor.com/ticket/10291 if filling char is followed by a space replace it with NBSP.
219 return p ? '\xa0' : ''; 421 return p ? '\xa0' : '';
220 } ); 422 } );
221 } else { 423 } else {
@@ -391,7 +593,7 @@
391 ( enclosedNode = range.getEnclosedNode() ) && enclosedNode.type == CKEDITOR.NODE_ELEMENT ) { 593 ( enclosedNode = range.getEnclosedNode() ) && enclosedNode.type == CKEDITOR.NODE_ELEMENT ) {
392 // So far we can't say that enclosed element is non-editable. Before checking, 594 // So far we can't say that enclosed element is non-editable. Before checking,
393 // we'll shrink range (clone). Shrinking will stop on non-editable range, or 595 // we'll shrink range (clone). Shrinking will stop on non-editable range, or
394 // innermost element (#11114). 596 // innermost element (http://dev.ckeditor.com/ticket/11114).
395 clone = range.clone(); 597 clone = range.clone();
396 clone.shrink( CKEDITOR.SHRINK_ELEMENT, true ); 598 clone.shrink( CKEDITOR.SHRINK_ELEMENT, true );
397 599
@@ -525,7 +727,7 @@
525 727
526 // Give the editable an initial selection on first focus, 728 // Give the editable an initial selection on first focus,
527 // put selection at a consistent position at the start 729 // put selection at a consistent position at the start
528 // of the contents. (#9507) 730 // of the contents. (http://dev.ckeditor.com/ticket/9507)
529 if ( CKEDITOR.env.gecko ) { 731 if ( CKEDITOR.env.gecko ) {
530 editable.attachListener( editable, 'focus', function( evt ) { 732 editable.attachListener( editable, 'focus', function( evt ) {
531 evt.removeListener(); 733 evt.removeListener();
@@ -533,7 +735,7 @@
533 if ( restoreSel !== 0 ) { 735 if ( restoreSel !== 0 ) {
534 var nativ = editor.getSelection().getNative(); 736 var nativ = editor.getSelection().getNative();
535 // Do it only if the native selection is at an unwanted 737 // Do it only if the native selection is at an unwanted
536 // place (at the very start of the editable). #10119 738 // place (at the very start of the editable). http://dev.ckeditor.com/ticket/10119
537 if ( nativ && nativ.isCollapsed && nativ.anchorNode == editable.$ ) { 739 if ( nativ && nativ.isCollapsed && nativ.anchorNode == editable.$ ) {
538 var rng = editor.createRange(); 740 var rng = editor.createRange();
539 rng.moveToElementEditStart( editable ); 741 rng.moveToElementEditStart( editable );
@@ -553,7 +755,7 @@
553 755
554 // On Webkit when editor uses divarea, native focus causes editable viewport to scroll 756 // On Webkit when editor uses divarea, native focus causes editable viewport to scroll
555 // to the top (when there is no active selection inside while focusing) so the scroll 757 // to the top (when there is no active selection inside while focusing) so the scroll
556 // position should be restored after focusing back editable area. (#14659) 758 // position should be restored after focusing back editable area. (http://dev.ckeditor.com/ticket/14659)
557 if ( restoreSel && editor._.previousScrollTop != null && editor._.previousScrollTop != editable.$.scrollTop ) { 759 if ( restoreSel && editor._.previousScrollTop != null && editor._.previousScrollTop != editable.$.scrollTop ) {
558 editable.$.scrollTop = editor._.previousScrollTop; 760 editable.$.scrollTop = editor._.previousScrollTop;
559 } 761 }
@@ -605,7 +807,7 @@
605 editable.attachListener( editable, 'mousedown', function( evt ) { 807 editable.attachListener( editable, 'mousedown', function( evt ) {
606 // IE scrolls document to top on right mousedown 808 // IE scrolls document to top on right mousedown
607 // when editor has no focus, remember this scroll 809 // when editor has no focus, remember this scroll
608 // position and revert it before context menu opens. (#5778) 810 // position and revert it before context menu opens. (http://dev.ckeditor.com/ticket/5778)
609 if ( evt.data.$.button == 2 ) { 811 if ( evt.data.$.button == 2 ) {
610 var sel = editor.document.getSelection(); 812 var sel = editor.document.getSelection();
611 if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE ) 813 if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE )
@@ -624,7 +826,7 @@
624 826
625 // When content doc is in standards mode, IE doesn't focus the editor when 827 // When content doc is in standards mode, IE doesn't focus the editor when
626 // clicking at the region below body (on html element) content, we emulate 828 // clicking at the region below body (on html element) content, we emulate
627 // the normal behavior on old IEs. (#1659, #7932) 829 // the normal behavior on old IEs. (http://dev.ckeditor.com/ticket/1659, http://dev.ckeditor.com/ticket/7932)
628 if ( doc.$.compatMode != 'BackCompat' ) { 830 if ( doc.$.compatMode != 'BackCompat' ) {
629 if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) { 831 if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) {
630 var textRng, 832 var textRng,
@@ -661,7 +863,7 @@
661 html.removeListener( 'mousemove', onHover ); 863 html.removeListener( 'mousemove', onHover );
662 removeListeners(); 864 removeListeners();
663 865
664 // Make it in effect on mouse up. (#9022) 866 // Make it in effect on mouse up. (http://dev.ckeditor.com/ticket/9022)
665 textRng.select(); 867 textRng.select();
666 } 868 }
667 869
@@ -690,7 +892,7 @@
690 if ( CKEDITOR.env.version > 7 && CKEDITOR.env.version < 11 ) { 892 if ( CKEDITOR.env.version > 7 && CKEDITOR.env.version < 11 ) {
691 html.on( 'mousedown', function( evt ) { 893 html.on( 'mousedown', function( evt ) {
692 if ( evt.data.getTarget().is( 'html' ) ) { 894 if ( evt.data.getTarget().is( 'html' ) ) {
693 // Limit the text selection mouse move inside of editable. (#9715) 895 // Limit the text selection mouse move inside of editable. (http://dev.ckeditor.com/ticket/9715)
694 outerDoc.on( 'mouseup', onSelectEnd ); 896 outerDoc.on( 'mouseup', onSelectEnd );
695 html.on( 'mouseup', onSelectEnd ); 897 html.on( 'mouseup', onSelectEnd );
696 } 898 }
@@ -704,6 +906,14 @@
704 // 2. After the accomplish of keyboard and mouse events. 906 // 2. After the accomplish of keyboard and mouse events.
705 editable.attachListener( editable, 'selectionchange', checkSelectionChange, editor ); 907 editable.attachListener( editable, 'selectionchange', checkSelectionChange, editor );
706 editable.attachListener( editable, 'keyup', checkSelectionChangeTimeout, editor ); 908 editable.attachListener( editable, 'keyup', checkSelectionChangeTimeout, editor );
909 // http://dev.ckeditor.com/ticket/14407 - Don't even let anything happen if the selection is in a non-editable element.
910 editable.attachListener( editable, 'keydown', function( evt ) {
911 var sel = this.getSelection( 1 );
912 if ( nonEditableAscendant( sel ) ) {
913 sel.selectElement( nonEditableAscendant( sel ) );
914 evt.data.preventDefault();
915 }
916 }, editor );
707 // Always fire the selection change on focus gain. 917 // Always fire the selection change on focus gain.
708 // On Webkit do this on DOMFocusIn, because the selection is unlocked on it too and 918 // On Webkit do this on DOMFocusIn, because the selection is unlocked on it too and
709 // we need synchronization between those listeners to not lost cached editor._.previousActive property 919 // we need synchronization between those listeners to not lost cached editor._.previousActive property
@@ -713,7 +923,7 @@
713 editor.selectionChange( 1 ); 923 editor.selectionChange( 1 );
714 } ); 924 } );
715 925
716 // #9699: On Webkit&Gecko in inline editor we have to check selection when it was changed 926 // http://dev.ckeditor.com/ticket/9699: On Webkit&Gecko in inline editor we have to check selection when it was changed
717 // by dragging and releasing mouse button outside editable. Dragging (mousedown) 927 // by dragging and releasing mouse button outside editable. Dragging (mousedown)
718 // has to be initialized in editable, but for mouseup we listen on document element. 928 // has to be initialized in editable, but for mouseup we listen on document element.
719 if ( isInline && ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) ) { 929 if ( isInline && ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) ) {
@@ -727,11 +937,11 @@
727 mouseDown = 0; 937 mouseDown = 0;
728 } ); 938 } );
729 } 939 }
730 // In all other cases listen on simple mouseup over editable, as we did before #9699. 940 // In all other cases listen on simple mouseup over editable, as we did before http://dev.ckeditor.com/ticket/9699.
731 // 941 //
732 // Use document instead of editable in non-IEs for observing mouseup 942 // Use document instead of editable in non-IEs for observing mouseup
733 // since editable won't fire the event if selection process started within iframe and ended out 943 // since editable won't fire the event if selection process started within iframe and ended out
734 // of the editor (#9851). 944 // of the editor (http://dev.ckeditor.com/ticket/9851).
735 else { 945 else {
736 editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor ); 946 editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor );
737 } 947 }
@@ -753,18 +963,18 @@
753 case 8: // BACKSPACE 963 case 8: // BACKSPACE
754 case 45: // INS 964 case 45: // INS
755 case 46: // DEl 965 case 46: // DEl
756 removeFillingCharSequenceNode( editable ); 966 if ( editable.hasFocus ) {
967 removeFillingCharSequenceNode( editable );
968 }
757 } 969 }
758 970
759 }, null, null, -1 ); 971 }, null, null, -1 );
760 } 972 }
761 973
762 // Automatically select non-editable element when navigating into
763 // it by left/right or backspace/del keys.
764 editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 ); 974 editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 );
765 975
766 function moveRangeToPoint( range, x, y ) { 976 function moveRangeToPoint( range, x, y ) {
767 // Error prune in IE7. (#9034, #9110) 977 // Error prune in IE7. (http://dev.ckeditor.com/ticket/9034, http://dev.ckeditor.com/ticket/9110)
768 try { 978 try {
769 range.moveToPoint( x, y ); 979 range.moveToPoint( x, y );
770 } catch ( e ) {} 980 } catch ( e ) {}
@@ -785,14 +995,27 @@
785 range = sel.createRange(); 995 range = sel.createRange();
786 996
787 // The selection range is reported on host, but actually it should applies to the content doc. 997 // The selection range is reported on host, but actually it should applies to the content doc.
788 if ( sel.type != 'None' && range.parentElement().ownerDocument == doc.$ ) 998 // The parentElement may be null for read only mode in IE10 and below (http://dev.ckeditor.com/ticket/9780).
999 if ( sel.type != 'None' && range.parentElement() && range.parentElement().ownerDocument == doc.$ )
789 range.select(); 1000 range.select();
790 } 1001 }
1002
1003 function nonEditableAscendant( sel ) {
1004 if ( CKEDITOR.env.ie ) {
1005 var range = sel.getRanges()[ 0 ],
1006 ascendant = range ? range.startContainer.getAscendant( function( parent ) {
1007 return parent.type == CKEDITOR.NODE_ELEMENT &&
1008 ( parent.getAttribute( 'contenteditable' ) == 'false' || parent.getAttribute( 'contenteditable' ) == 'true' );
1009 }, true ) : null ;
1010
1011 return range && ascendant.getAttribute( 'contenteditable' ) == 'false' && ascendant;
1012 }
1013 }
791 } ); 1014 } );
792 1015
793 editor.on( 'setData', function() { 1016 editor.on( 'setData', function() {
794 // Invalidate locked selection when unloading DOM. 1017 // Invalidate locked selection when unloading DOM.
795 // (#9521, #5217#comment:32 and #11500#comment:11) 1018 // (http://dev.ckeditor.com/ticket/9521, http://dev.ckeditor.com/ticket/5217#comment:32 and http://dev.ckeditor.com/ticket/11500#comment:11)
796 editor.unlockSelection(); 1019 editor.unlockSelection();
797 1020
798 // Webkit's selection will mess up after the data loading. 1021 // Webkit's selection will mess up after the data loading.
@@ -806,7 +1029,7 @@
806 editor.unlockSelection(); 1029 editor.unlockSelection();
807 } ); 1030 } );
808 1031
809 // IE9 might cease to work if there's an object selection inside the iframe (#7639). 1032 // IE9 might cease to work if there's an object selection inside the iframe (http://dev.ckeditor.com/ticket/7639).
810 if ( CKEDITOR.env.ie9Compat ) 1033 if ( CKEDITOR.env.ie9Compat )
811 editor.on( 'beforeDestroy', clearSelection, null, null, 9 ); 1034 editor.on( 'beforeDestroy', clearSelection, null, null, 9 );
812 1035
@@ -822,7 +1045,7 @@
822 // When loaded data are ready check whether hidden selection container was not loaded. 1045 // When loaded data are ready check whether hidden selection container was not loaded.
823 editor.on( 'loadSnapshot', function() { 1046 editor.on( 'loadSnapshot', function() {
824 var isElement = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ), 1047 var isElement = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ),
825 // TODO replace with el.find() which will be introduced in #9764, 1048 // TODO replace with el.find() which will be introduced in http://dev.ckeditor.com/ticket/9764,
826 // because it may happen that hidden sel container won't be the last element. 1049 // because it may happen that hidden sel container won't be the last element.
827 last = editor.editable().getLast( isElement ); 1050 last = editor.editable().getLast( isElement );
828 1051
@@ -869,7 +1092,7 @@
869 } ); 1092 } );
870 1093
871 // On WebKit only, we need a special "filling" char on some situations 1094 // On WebKit only, we need a special "filling" char on some situations
872 // (#1272). Here we set the events that should invalidate that char. 1095 // (http://dev.ckeditor.com/ticket/1272). Here we set the events that should invalidate that char.
873 if ( CKEDITOR.env.webkit ) { 1096 if ( CKEDITOR.env.webkit ) {
874 CKEDITOR.on( 'instanceReady', function( evt ) { 1097 CKEDITOR.on( 'instanceReady', function( evt ) {
875 var editor = evt.editor; 1098 var editor = evt.editor;
@@ -884,7 +1107,7 @@
884 1107
885 // Filter Undo snapshot's HTML to get rid of Filling Char Sequence. 1108 // Filter Undo snapshot's HTML to get rid of Filling Char Sequence.
886 // Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's 1109 // Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's
887 // bookmarks to anticipate the removal of FCSeq from the snapshot's HTML (#13816). 1110 // bookmarks to anticipate the removal of FCSeq from the snapshot's HTML (http://dev.ckeditor.com/ticket/13816).
888 editor.on( 'getSnapshot', function( evt ) { 1111 editor.on( 'getSnapshot', function( evt ) {
889 if ( evt.data ) { 1112 if ( evt.data ) {
890 evt.data = removeFillingCharSequenceString( evt.data ); 1113 evt.data = removeFillingCharSequenceString( evt.data );
@@ -893,7 +1116,7 @@
893 1116
894 // Filter data to get rid of Filling Char Sequence. Filter on #toDataFormat 1117 // Filter data to get rid of Filling Char Sequence. Filter on #toDataFormat
895 // instead of #getData because once removed, FCSeq may leave an empty element, 1118 // instead of #getData because once removed, FCSeq may leave an empty element,
896 // which should be pruned by the dataProcessor (#13816). 1119 // which should be pruned by the dataProcessor (http://dev.ckeditor.com/ticket/13816).
897 // Note: Used low priority to filter when dataProcessor works on strings, 1120 // Note: Used low priority to filter when dataProcessor works on strings,
898 // not pseudo–DOM. 1121 // not pseudo–DOM.
899 editor.on( 'toDataFormat', function( evt ) { 1122 editor.on( 'toDataFormat', function( evt ) {
@@ -1138,7 +1361,7 @@
1138 1361
1139 // Selection out of concerned range, empty the selection. 1362 // Selection out of concerned range, empty the selection.
1140 // TODO check whether this condition cannot be reverted to its old 1363 // TODO check whether this condition cannot be reverted to its old
1141 // form (commented out) after we closed #10438. 1364 // form (commented out) after we closed http://dev.ckeditor.com/ticket/10438.
1142 //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) { 1365 //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) {
1143 if ( !( 1366 if ( !(
1144 rangeParent && 1367 rangeParent &&
@@ -1180,7 +1403,7 @@
1180 * 1403 *
1181 * var selection = editor.getSelection().getNative(); 1404 * var selection = editor.getSelection().getNative();
1182 * 1405 *
1183 * @returns {Object} The native browser selection object. 1406 * @returns {Object} The native browser selection object or null if this is a fake selection.
1184 */ 1407 */
1185 getNative: function() { 1408 getNative: function() {
1186 if ( this._.cache.nativeSel !== undefined ) 1409 if ( this._.cache.nativeSel !== undefined )
@@ -1272,7 +1495,7 @@
1272 * alert( ranges.length ); 1495 * alert( ranges.length );
1273 * 1496 *
1274 * @method 1497 * @method
1275 * @param {Boolean} [onlyEditables] If set to `true`, this function retrives editable ranges only. 1498 * @param {Boolean} [onlyEditables] If set to `true`, this function retrieves editable ranges only.
1276 * @returns {Array} Range instances that represent the current selection. 1499 * @returns {Array} Range instances that represent the current selection.
1277 */ 1500 */
1278 getRanges: ( function() { 1501 getRanges: ( function() {
@@ -1303,7 +1526,7 @@
1303 index = -1, 1526 index = -1,
1304 position, distance, container; 1527 position, distance, container;
1305 1528
1306 // Binary search over all element childs to test the range to see whether 1529 // Binary search over all element children to test the range to see whether
1307 // range is right on the boundary of one element. 1530 // range is right on the boundary of one element.
1308 while ( startIndex <= endIndex ) { 1531 while ( startIndex <= endIndex ) {
1309 index = Math.floor( ( startIndex + endIndex ) / 2 ); 1532 index = Math.floor( ( startIndex + endIndex ) / 2 );
@@ -1319,8 +1542,8 @@
1319 return { container: parent, offset: getNodeIndex( child ) }; 1542 return { container: parent, offset: getNodeIndex( child ) };
1320 } 1543 }
1321 1544
1322 // All childs are text nodes, 1545 // All children are text nodes,
1323 // or to the right hand of test range are all text nodes. (#6992) 1546 // or to the right hand of test range are all text nodes. (http://dev.ckeditor.com/ticket/6992)
1324 if ( index == -1 || index == siblings.length - 1 && position < 0 ) { 1547 if ( index == -1 || index == siblings.length - 1 && position < 0 ) {
1325 // Adapt test range to embrace the entire parent contents. 1548 // Adapt test range to embrace the entire parent contents.
1326 testRange.moveToElementText( parent ); 1549 testRange.moveToElementText( parent );
@@ -1328,7 +1551,7 @@
1328 1551
1329 // IE report line break as CRLF with range.text but 1552 // IE report line break as CRLF with range.text but
1330 // only LF with textnode.nodeValue, normalize them to avoid 1553 // only LF with textnode.nodeValue, normalize them to avoid
1331 // breaking character counting logic below. (#3949) 1554 // breaking character counting logic below. (http://dev.ckeditor.com/ticket/3949)
1332 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; 1555 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
1333 1556
1334 siblings = parent.childNodes; 1557 siblings = parent.childNodes;
@@ -1364,7 +1587,7 @@
1364 1587
1365 // IE report line break as CRLF with range.text but 1588 // IE report line break as CRLF with range.text but
1366 // only LF with textnode.nodeValue, normalize them to avoid 1589 // only LF with textnode.nodeValue, normalize them to avoid
1367 // breaking character counting logic below. (#3949) 1590 // breaking character counting logic below. (http://dev.ckeditor.com/ticket/3949)
1368 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; 1591 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
1369 1592
1370 // Actual range anchor right beside test range at the inner boundary of text node. 1593 // Actual range anchor right beside test range at the inner boundary of text node.
@@ -1381,7 +1604,7 @@
1381 } 1604 }
1382 child = sibling; 1605 child = sibling;
1383 } 1606 }
1384 // Measurement in IE could be somtimes wrong because of <select> element. (#4611) 1607 // Measurement in IE could be sometimes wrong because of <select> element. (http://dev.ckeditor.com/ticket/4611)
1385 catch ( e ) { 1608 catch ( e ) {
1386 return { container: parent, offset: getNodeIndex( child ) }; 1609 return { container: parent, offset: getNodeIndex( child ) };
1387 } 1610 }
@@ -1413,7 +1636,7 @@
1413 boundaryInfo = getBoundaryInformation( nativeRange ); 1636 boundaryInfo = getBoundaryInformation( nativeRange );
1414 range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset ); 1637 range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
1415 1638
1416 // Correct an invalid IE range case on empty list item. (#5850) 1639 // Correct an invalid IE range case on empty list item. (http://dev.ckeditor.com/ticket/5850)
1417 if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING && range.endOffset <= range.startContainer.getIndex() ) 1640 if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING && range.endOffset <= range.startContainer.getIndex() )
1418 range.collapse(); 1641 range.collapse();
1419 1642
@@ -1445,7 +1668,7 @@
1445 } )() : 1668 } )() :
1446 function() { 1669 function() {
1447 // On browsers implementing the W3C range, we simply 1670 // On browsers implementing the W3C range, we simply
1448 // tranform the native ranges in CKEDITOR.dom.range 1671 // transform the native ranges in CKEDITOR.dom.range
1449 // instances. 1672 // instances.
1450 1673
1451 var ranges = [], 1674 var ranges = [],
@@ -1478,7 +1701,7 @@
1478 return ranges; 1701 return ranges;
1479 1702
1480 // Split range into multiple by read-only nodes. 1703 // Split range into multiple by read-only nodes.
1481 // Clone ranges array to avoid changing cached ranges (#11493). 1704 // Clone ranges array to avoid changing cached ranges (http://dev.ckeditor.com/ticket/11493).
1482 return extractEditableRanges( new CKEDITOR.dom.rangeList( ranges.slice() ) ); 1705 return extractEditableRanges( new CKEDITOR.dom.rangeList( ranges.slice() ) );
1483 }; 1706 };
1484 } )(), 1707 } )(),
@@ -1512,11 +1735,11 @@
1512 1735
1513 // Decrease the range content to exclude particial 1736 // Decrease the range content to exclude particial
1514 // selected node on the start which doesn't have 1737 // selected node on the start which doesn't have
1515 // visual impact. ( #3231 ) 1738 // visual impact. ( http://dev.ckeditor.com/ticket/3231 )
1516 while ( 1 ) { 1739 while ( 1 ) {
1517 var startContainer = range.startContainer, 1740 var startContainer = range.startContainer,
1518 startOffset = range.startOffset; 1741 startOffset = range.startOffset;
1519 // Limit the fix only to non-block elements.(#3950) 1742 // Limit the fix only to non-block elements.(http://dev.ckeditor.com/ticket/3950)
1520 if ( startOffset == ( startContainer.getChildCount ? startContainer.getChildCount() : startContainer.getLength() ) && !startContainer.isBlockBoundary() ) 1743 if ( startOffset == ( startContainer.getChildCount ? startContainer.getChildCount() : startContainer.getLength() ) && !startContainer.isBlockBoundary() )
1521 range.setStartAfter( startContainer ); 1744 range.setStartAfter( startContainer );
1522 else 1745 else
@@ -1558,7 +1781,7 @@
1558 * var element = editor.getSelection().getSelectedElement(); 1781 * var element = editor.getSelection().getSelectedElement();
1559 * alert( element.getName() ); 1782 * alert( element.getName() );
1560 * 1783 *
1561 * @returns {CKEDITOR.dom.element} The selected element. Null if no 1784 * @returns {CKEDITOR.dom.element/null} The selected element. `null` if no
1562 * selection is available or the selection type is not {@link CKEDITOR#SELECTION_ELEMENT}. 1785 * selection is available or the selection type is not {@link CKEDITOR#SELECTION_ELEMENT}.
1563 */ 1786 */
1564 getSelectedElement: function() { 1787 getSelectedElement: function() {
@@ -1641,7 +1864,7 @@
1641 1864
1642 if ( restore ) { 1865 if ( restore ) {
1643 var selectedElement = this.getSelectedElement(), 1866 var selectedElement = this.getSelectedElement(),
1644 ranges = !selectedElement && this.getRanges(), 1867 ranges = this.getRanges(),
1645 faked = this.isFake; 1868 faked = this.isFake;
1646 } 1869 }
1647 1870
@@ -1655,7 +1878,10 @@
1655 if ( !( common && common.getAscendant( 'body', 1 ) ) ) 1878 if ( !( common && common.getAscendant( 'body', 1 ) ) )
1656 return; 1879 return;
1657 1880
1658 if ( faked ) 1881 if ( isTableSelection( ranges ) ) {
1882 // Tables have it's own selection method.
1883 performFakeTableSelection.call( this, ranges );
1884 } else if ( faked )
1659 this.fake( selectedElement ); 1885 this.fake( selectedElement );
1660 else if ( selectedElement ) 1886 else if ( selectedElement )
1661 this.selectElement( selectedElement ); 1887 this.selectElement( selectedElement );
@@ -1727,7 +1953,7 @@
1727 // Check if there's a hiddenSelectionContainer in editable at some index. 1953 // Check if there's a hiddenSelectionContainer in editable at some index.
1728 // Some ranges may be anchored after the hiddenSelectionContainer and, 1954 // Some ranges may be anchored after the hiddenSelectionContainer and,
1729 // once the container is removed while resetting the selection, they 1955 // once the container is removed while resetting the selection, they
1730 // may need new endOffset (one element less within the range) (#11021 #11393). 1956 // may need new endOffset (one element less within the range) (http://dev.ckeditor.com/ticket/11021 http://dev.ckeditor.com/ticket/11393).
1731 if ( hadHiddenSelectionContainer ) 1957 if ( hadHiddenSelectionContainer )
1732 fixRangesAfterHiddenSelectionContainer( ranges, this.root ); 1958 fixRangesAfterHiddenSelectionContainer( ranges, this.root );
1733 1959
@@ -1755,6 +1981,15 @@
1755 return; 1981 return;
1756 } 1982 }
1757 1983
1984 // Handle special case - fake selection of table cells.
1985 if ( editor && editor.plugins.tableselection &&
1986 CKEDITOR.plugins.tableselection.isSupportedEnvironment &&
1987 isTableSelection( ranges ) && !isSelectingTable
1988 ) {
1989 performFakeTableSelection.call( this, ranges );
1990 return;
1991 }
1992
1758 if ( isMSSelection ) { 1993 if ( isMSSelection ) {
1759 var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ), 1994 var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
1760 fillerTextRegex = /\ufeff|\u00a0/, 1995 fillerTextRegex = /\ufeff|\u00a0/,
@@ -1788,7 +2023,7 @@
1788 if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in nonCells || 2023 if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in nonCells ||
1789 range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) { 2024 range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) {
1790 range.shrink( CKEDITOR.NODE_ELEMENT, true ); 2025 range.shrink( CKEDITOR.NODE_ELEMENT, true );
1791 // The range might get collapsed (#7975). Update cached variable. 2026 // The range might get collapsed (http://dev.ckeditor.com/ticket/7975). Update cached variable.
1792 collapsed = range.collapsed; 2027 collapsed = range.collapsed;
1793 } 2028 }
1794 2029
@@ -1830,18 +2065,18 @@
1830 2065
1831 // Append a temporary <span>&#65279;</span> before the selection. 2066 // Append a temporary <span>&#65279;</span> before the selection.
1832 // This is needed to avoid IE destroying selections inside empty 2067 // This is needed to avoid IE destroying selections inside empty
1833 // inline elements, like <b></b> (#253). 2068 // inline elements, like <b></b> (http://dev.ckeditor.com/ticket/253).
1834 // It is also needed when placing the selection right after an inline 2069 // It is also needed when placing the selection right after an inline
1835 // element to avoid the selection moving inside of it. 2070 // element to avoid the selection moving inside of it.
1836 dummySpan = range.document.createElement( 'span' ); 2071 dummySpan = range.document.createElement( 'span' );
1837 dummySpan.setHtml( '&#65279;' ); // Zero Width No-Break Space (U+FEFF). See #1359. 2072 dummySpan.setHtml( '&#65279;' ); // Zero Width No-Break Space (U+FEFF). See http://dev.ckeditor.com/ticket/1359.
1838 dummySpan.insertBefore( startNode ); 2073 dummySpan.insertBefore( startNode );
1839 2074
1840 if ( isStartMarkerAlone ) { 2075 if ( isStartMarkerAlone ) {
1841 // To expand empty blocks or line spaces after <br>, we need 2076 // To expand empty blocks or line spaces after <br>, we need
1842 // instead to have any char, which will be later deleted using the 2077 // instead to have any char, which will be later deleted using the
1843 // selection. 2078 // selection.
1844 // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359) 2079 // \ufeff = Zero Width No-Break Space (U+FEFF). (http://dev.ckeditor.com/ticket/1359)
1845 range.document.createText( '\ufeff' ).insertBefore( startNode ); 2080 range.document.createText( '\ufeff' ).insertBefore( startNode );
1846 } 2081 }
1847 } 2082 }
@@ -1873,7 +2108,7 @@
1873 } else { 2108 } else {
1874 var sel = this.getNative(); 2109 var sel = this.getNative();
1875 2110
1876 // getNative() returns null if iframe is "display:none" in FF. (#6577) 2111 // getNative() returns null if iframe is "display:none" in FF. (http://dev.ckeditor.com/ticket/6577)
1877 if ( !sel ) 2112 if ( !sel )
1878 return; 2113 return;
1879 2114
@@ -1889,7 +2124,7 @@
1889 between.setStart( left.endContainer, left.endOffset ); 2124 between.setStart( left.endContainer, left.endOffset );
1890 between.setEnd( right.startContainer, right.startOffset ); 2125 between.setEnd( right.startContainer, right.startOffset );
1891 2126
1892 // Don't confused by Firefox adjancent multi-ranges 2127 // Don't confused by Firefox adjacent multi-ranges
1893 // introduced by table cells selection. 2128 // introduced by table cells selection.
1894 if ( !between.collapsed ) { 2129 if ( !between.collapsed ) {
1895 between.shrink( CKEDITOR.NODE_ELEMENT, true ); 2130 between.shrink( CKEDITOR.NODE_ELEMENT, true );
@@ -1898,7 +2133,7 @@
1898 2133
1899 // The following cases has to be considered: 2134 // The following cases has to be considered:
1900 // 1. <span contenteditable="false">[placeholder]</span> 2135 // 1. <span contenteditable="false">[placeholder]</span>
1901 // 2. <input contenteditable="false" type="radio"/> (#6621) 2136 // 2. <input contenteditable="false" type="radio"/> (http://dev.ckeditor.com/ticket/6621)
1902 if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) { 2137 if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) {
1903 right.setStart( left.startContainer, left.startOffset ); 2138 right.setStart( left.startContainer, left.startOffset );
1904 ranges.splice( i--, 1 ); 2139 ranges.splice( i--, 1 );
@@ -1913,7 +2148,7 @@
1913 2148
1914 if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) { 2149 if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) {
1915 // Append a zero-width space so WebKit will not try to 2150 // Append a zero-width space so WebKit will not try to
1916 // move the selection by itself (#1272). 2151 // move the selection by itself (http://dev.ckeditor.com/ticket/1272).
1917 var fillingChar = createFillingCharSequenceNode( this.root ); 2152 var fillingChar = createFillingCharSequenceNode( this.root );
1918 range.insertNode( fillingChar ); 2153 range.insertNode( fillingChar );
1919 2154
@@ -1972,7 +2207,7 @@
1972 fake: function( element, ariaLabel ) { 2207 fake: function( element, ariaLabel ) {
1973 var editor = this.root.editor; 2208 var editor = this.root.editor;
1974 2209
1975 // Attempt to retreive aria-label if possible (#14539). 2210 // Attempt to retrieve aria-label if possible (http://dev.ckeditor.com/ticket/14539).
1976 if ( ariaLabel === undefined && element.hasAttribute( 'aria-label' ) ) { 2211 if ( ariaLabel === undefined && element.hasAttribute( 'aria-label' ) ) {
1977 ariaLabel = element.getAttribute( 'aria-label' ); 2212 ariaLabel = element.getAttribute( 'aria-label' );
1978 } 2213 }
@@ -2031,6 +2266,38 @@
2031 }, 2266 },
2032 2267
2033 /** 2268 /**
2269 * Checks if the selection contains an HTML element inside a table.
2270 * Returns `false` for text selection inside a table (e.g. it will return `false`
2271 * for text selected in one cell).
2272 *
2273 * editor.getSelection().isInTable();
2274 *
2275 * @since 4.7.0
2276 * @param {Boolean} [allowPartialSelection=false] Whether a partial cell selection should be included.
2277 * Added in 4.7.2.
2278 * @returns {Boolean}
2279 */
2280 isInTable: function( allowPartialSelection ) {
2281 return isTableSelection( this.getRanges(), allowPartialSelection );
2282 },
2283
2284 /**
2285 * Checks if the selection contains only one range which is collapsed.
2286 *
2287 * if ( editor.getSelection().isCollapsed() ) {
2288 * // Do something when the selection is collapsed.
2289 * }
2290 *
2291 * @since 4.7.3
2292 * @returns {Boolean}
2293 */
2294 isCollapsed: function() {
2295 var ranges = this.getRanges();
2296
2297 return ranges.length === 1 && ranges[ 0 ].collapsed;
2298 },
2299
2300 /**
2034 * Creates a bookmark for each range of this selection (from {@link #getRanges}) 2301 * Creates a bookmark for each range of this selection (from {@link #getRanges})
2035 * by calling the {@link CKEDITOR.dom.range#createBookmark} method, 2302 * by calling the {@link CKEDITOR.dom.range#createBookmark} method,
2036 * with extra care taken to avoid interference among those ranges. The arguments 2303 * with extra care taken to avoid interference among those ranges. The arguments
@@ -2083,17 +2350,19 @@
2083 2350
2084 // It may happen that the content change during loading, before selection is set so bookmark leads to text node. 2351 // It may happen that the content change during loading, before selection is set so bookmark leads to text node.
2085 if ( bookmarks.isFake ) { 2352 if ( bookmarks.isFake ) {
2086 node = ranges[ 0 ].getEnclosedNode(); 2353 node = isTableSelection( ranges ) ? ranges[ 0 ]._getTableElement() : ranges[ 0 ].getEnclosedNode();
2354
2087 if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) { 2355 if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) {
2088 CKEDITOR.warn( 'selection-not-fake' ); 2356 CKEDITOR.warn( 'selection-not-fake' );
2089 bookmarks.isFake = 0; 2357 bookmarks.isFake = 0;
2090 } 2358 }
2091 } 2359 }
2092 2360
2093 if ( bookmarks.isFake ) 2361 if ( bookmarks.isFake && !isTableSelection( ranges ) ) {
2094 this.fake( node ); 2362 this.fake( node );
2095 else 2363 } else {
2096 this.selectRanges( ranges ); 2364 this.selectRanges( ranges );
2365 }
2097 2366
2098 return this; 2367 return this;
2099 }, 2368 },
@@ -2130,7 +2399,7 @@
2130 * Remove all the selection ranges from the document. 2399 * Remove all the selection ranges from the document.
2131 */ 2400 */
2132 removeAllRanges: function() { 2401 removeAllRanges: function() {
2133 // Don't clear selection outside this selection's root (#11500). 2402 // Don't clear selection outside this selection's root (http://dev.ckeditor.com/ticket/11500).
2134 if ( this.getType() == CKEDITOR.SELECTION_NONE ) 2403 if ( this.getType() == CKEDITOR.SELECTION_NONE )
2135 return; 2404 return;
2136 2405