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.js446
1 files changed, 367 insertions, 79 deletions
diff --git a/sources/core/selection.js b/sources/core/selection.js
index 573b890..d44db3b 100644
--- a/sources/core/selection.js
+++ b/sources/core/selection.js
@@ -1,9 +1,217 @@
1/** 1/**
2 * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. 2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license 3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */ 4 */
5 5
6( function() { 6( function() {
7 var isMSSelection = typeof window.getSelection != 'function',
8 nextRev = 1,
9 // http://dev.ckeditor.com/ticket/13816
10 fillingCharSequence = CKEDITOR.tools.repeat( '\u200b', 7 ),
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
214
7 // #### checkSelectionChange : START 215 // #### checkSelectionChange : START
8 216
9 // The selection change check basically saves the element parent tree of 217 // The selection change check basically saves the element parent tree of
@@ -16,10 +224,9 @@
16 224
17 if ( sel ) { 225 if ( sel ) {
18 realSel = this.getSelection( 1 ); 226 realSel = this.getSelection( 1 );
19 227 // If real (not locked/stored) selection was moved from hidden container
20 // 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.
21 // then the fake-selection must be invalidated. 229 if ( !realSel || ( !realSel.isHidden() && !isRealTableSelection( realSel, sel ) ) ) {
22 if ( !realSel || !realSel.isHidden() ) {
23 // Remove the cache from fake-selection references in use elsewhere. 230 // Remove the cache from fake-selection references in use elsewhere.
24 sel.reset(); 231 sel.reset();
25 232
@@ -41,8 +248,10 @@
41 248
42 var currentPath = this.elementPath(); 249 var currentPath = this.elementPath();
43 if ( !currentPath.compare( this._.selectionPreviousPath ) ) { 250 if ( !currentPath.compare( this._.selectionPreviousPath ) ) {
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)
252 var sameBlockParent = this._.selectionPreviousPath && this._.selectionPreviousPath.blockLimit.equals( currentPath.blockLimit );
44 // Cache the active element, which we'll eventually lose on Webkit. 253 // Cache the active element, which we'll eventually lose on Webkit.
45 if ( CKEDITOR.env.webkit ) 254 if ( CKEDITOR.env.webkit && !sameBlockParent )
46 this._.previousActive = this.document.getActive(); 255 this._.previousActive = this.document.getActive();
47 256
48 this._.selectionPreviousPath = currentPath; 257 this._.selectionPreviousPath = currentPath;
@@ -89,7 +298,7 @@
89 // * is a visible node, 298 // * is a visible node,
90 // * 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
91 // 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).
92 // See #12621. 301 // See http://dev.ckeditor.com/ticket/12621.
93 function mayAbsorbCaret( node ) { 302 function mayAbsorbCaret( node ) {
94 if ( isVisible( node ) ) 303 if ( isVisible( node ) )
95 return true; 304 return true;
@@ -130,8 +339,8 @@
130 if ( ctxRequiresFix( previous ) || ctxRequiresFix( next, 1 ) ) 339 if ( ctxRequiresFix( previous ) || ctxRequiresFix( next, 1 ) )
131 return true; 340 return true;
132 341
133 // 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)
134 // If you found this line confusing check #12655. 343 // If you found this line confusing check http://dev.ckeditor.com/ticket/12655.
135 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() ) )
136 return true; 345 return true;
137 346
@@ -147,7 +356,7 @@
147 return fillingChar; 356 return fillingChar;
148 } 357 }
149 358
150 // 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).
151 function checkFillingCharSequenceNodeReady( editable ) { 360 function checkFillingCharSequenceNodeReady( editable ) {
152 var fillingChar = editable.getCustomData( 'cke-fillingChar' ); 361 var fillingChar = editable.getCustomData( 'cke-fillingChar' );
153 362
@@ -156,6 +365,7 @@
156 // creating it. 365 // creating it.
157 if ( fillingChar.getCustomData( 'ready' ) ) { 366 if ( fillingChar.getCustomData( 'ready' ) ) {
158 removeFillingCharSequenceNode( editable ); 367 removeFillingCharSequenceNode( editable );
368 editable.editor.fire( 'selectionCheck' );
159 } else { 369 } else {
160 fillingChar.setCustomData( 'ready', 1 ); 370 fillingChar.setCustomData( 'ready', 1 );
161 } 371 }
@@ -167,7 +377,7 @@
167 377
168 if ( fillingChar ) { 378 if ( fillingChar ) {
169 // Text selection position might get mangled by 379 // Text selection position might get mangled by
170 // subsequent dom modification, save it now for restoring. (#8617) 380 // subsequent dom modification, save it now for restoring. (http://dev.ckeditor.com/ticket/8617)
171 if ( keepSelection !== false ) { 381 if ( keepSelection !== false ) {
172 var sel = editable.getDocument().getSelection().getNative(), 382 var sel = editable.getDocument().getSelection().getNative(),
173 // Be error proof. 383 // Be error proof.
@@ -203,11 +413,11 @@
203 } 413 }
204 } 414 }
205 415
206 // #13816 416 // http://dev.ckeditor.com/ticket/13816
207 function removeFillingCharSequenceString( str, nbspAware ) { 417 function removeFillingCharSequenceString( str, nbspAware ) {
208 if ( nbspAware ) { 418 if ( nbspAware ) {
209 return str.replace( fillingCharSequenceRegExp, function( m, p ) { 419 return str.replace( fillingCharSequenceRegExp, function( m, p ) {
210 // #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.
211 return p ? '\xa0' : ''; 421 return p ? '\xa0' : '';
212 } ); 422 } );
213 } else { 423 } else {
@@ -234,10 +444,11 @@
234 } 444 }
235 445
236 // Creates cke_hidden_sel container and puts real selection there. 446 // Creates cke_hidden_sel container and puts real selection there.
237 function hideSelection( editor ) { 447 function hideSelection( editor, ariaLabel ) {
238 var style = CKEDITOR.env.ie ? 'display:none' : 'position:fixed;top:0;left:-1000px', 448 var content = ariaLabel || '&nbsp;',
449 style = CKEDITOR.env.ie && CKEDITOR.env.version < 14 ? 'display:none' : 'position:fixed;top:0;left:-1000px',
239 hiddenEl = CKEDITOR.dom.element.createFromHtml( 450 hiddenEl = CKEDITOR.dom.element.createFromHtml(
240 '<div data-cke-hidden-sel="1" data-cke-temp="1" style="' + style + '">&nbsp;</div>', 451 '<div data-cke-hidden-sel="1" data-cke-temp="1" style="' + style + '">' + content + '</div>',
241 editor.document ); 452 editor.document );
242 453
243 editor.fire( 'lockSnapshot' ); 454 editor.fire( 'lockSnapshot' );
@@ -382,7 +593,7 @@
382 ( enclosedNode = range.getEnclosedNode() ) && enclosedNode.type == CKEDITOR.NODE_ELEMENT ) { 593 ( enclosedNode = range.getEnclosedNode() ) && enclosedNode.type == CKEDITOR.NODE_ELEMENT ) {
383 // 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,
384 // 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
385 // innermost element (#11114). 596 // innermost element (http://dev.ckeditor.com/ticket/11114).
386 clone = range.clone(); 597 clone = range.clone();
387 clone.shrink( CKEDITOR.SHRINK_ELEMENT, true ); 598 clone.shrink( CKEDITOR.SHRINK_ELEMENT, true );
388 599
@@ -516,7 +727,7 @@
516 727
517 // Give the editable an initial selection on first focus, 728 // Give the editable an initial selection on first focus,
518 // put selection at a consistent position at the start 729 // put selection at a consistent position at the start
519 // of the contents. (#9507) 730 // of the contents. (http://dev.ckeditor.com/ticket/9507)
520 if ( CKEDITOR.env.gecko ) { 731 if ( CKEDITOR.env.gecko ) {
521 editable.attachListener( editable, 'focus', function( evt ) { 732 editable.attachListener( editable, 'focus', function( evt ) {
522 evt.removeListener(); 733 evt.removeListener();
@@ -524,7 +735,7 @@
524 if ( restoreSel !== 0 ) { 735 if ( restoreSel !== 0 ) {
525 var nativ = editor.getSelection().getNative(); 736 var nativ = editor.getSelection().getNative();
526 // Do it only if the native selection is at an unwanted 737 // Do it only if the native selection is at an unwanted
527 // place (at the very start of the editable). #10119 738 // place (at the very start of the editable). http://dev.ckeditor.com/ticket/10119
528 if ( nativ && nativ.isCollapsed && nativ.anchorNode == editable.$ ) { 739 if ( nativ && nativ.isCollapsed && nativ.anchorNode == editable.$ ) {
529 var rng = editor.createRange(); 740 var rng = editor.createRange();
530 rng.moveToElementEditStart( editable ); 741 rng.moveToElementEditStart( editable );
@@ -539,9 +750,17 @@
539 // On Webkit we use DOMFocusIn which is fired more often than focus - e.g. when moving from main editable 750 // On Webkit we use DOMFocusIn which is fired more often than focus - e.g. when moving from main editable
540 // to nested editable (or the opposite). Unlock selection all, but restore only when it was locked 751 // to nested editable (or the opposite). Unlock selection all, but restore only when it was locked
541 // for the same active element, what will e.g. mean restoring after displaying dialog. 752 // for the same active element, what will e.g. mean restoring after displaying dialog.
542 if ( restoreSel && CKEDITOR.env.webkit ) 753 if ( restoreSel && CKEDITOR.env.webkit ) {
543 restoreSel = editor._.previousActive && editor._.previousActive.equals( doc.getActive() ); 754 restoreSel = editor._.previousActive && editor._.previousActive.equals( doc.getActive() );
544 755
756 // On Webkit when editor uses divarea, native focus causes editable viewport to scroll
757 // to the top (when there is no active selection inside while focusing) so the scroll
758 // position should be restored after focusing back editable area. (http://dev.ckeditor.com/ticket/14659)
759 if ( restoreSel && editor._.previousScrollTop != null && editor._.previousScrollTop != editable.$.scrollTop ) {
760 editable.$.scrollTop = editor._.previousScrollTop;
761 }
762 }
763
545 editor.unlockSelection( restoreSel ); 764 editor.unlockSelection( restoreSel );
546 restoreSel = 0; 765 restoreSel = 0;
547 }, null, null, -1 ); 766 }, null, null, -1 );
@@ -588,7 +807,7 @@
588 editable.attachListener( editable, 'mousedown', function( evt ) { 807 editable.attachListener( editable, 'mousedown', function( evt ) {
589 // IE scrolls document to top on right mousedown 808 // IE scrolls document to top on right mousedown
590 // when editor has no focus, remember this scroll 809 // when editor has no focus, remember this scroll
591 // position and revert it before context menu opens. (#5778) 810 // position and revert it before context menu opens. (http://dev.ckeditor.com/ticket/5778)
592 if ( evt.data.$.button == 2 ) { 811 if ( evt.data.$.button == 2 ) {
593 var sel = editor.document.getSelection(); 812 var sel = editor.document.getSelection();
594 if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE ) 813 if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE )
@@ -607,9 +826,12 @@
607 826
608 // 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
609 // 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
610 // 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)
611 if ( doc.$.compatMode != 'BackCompat' ) { 830 if ( doc.$.compatMode != 'BackCompat' ) {
612 if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) { 831 if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) {
832 var textRng,
833 startRng;
834
613 html.on( 'mousedown', function( evt ) { 835 html.on( 'mousedown', function( evt ) {
614 evt = evt.data; 836 evt = evt.data;
615 837
@@ -641,7 +863,7 @@
641 html.removeListener( 'mousemove', onHover ); 863 html.removeListener( 'mousemove', onHover );
642 removeListeners(); 864 removeListeners();
643 865
644 // Make it in effect on mouse up. (#9022) 866 // Make it in effect on mouse up. (http://dev.ckeditor.com/ticket/9022)
645 textRng.select(); 867 textRng.select();
646 } 868 }
647 869
@@ -652,11 +874,11 @@
652 evt.$.y < html.$.clientHeight && 874 evt.$.y < html.$.clientHeight &&
653 evt.$.x < html.$.clientWidth ) { 875 evt.$.x < html.$.clientWidth ) {
654 // Start to build the text range. 876 // Start to build the text range.
655 var textRng = body.$.createTextRange(); 877 textRng = body.$.createTextRange();
656 moveRangeToPoint( textRng, evt.$.clientX, evt.$.clientY ); 878 moveRangeToPoint( textRng, evt.$.clientX, evt.$.clientY );
657 879
658 // Records the dragging start of the above text range. 880 // Records the dragging start of the above text range.
659 var startRng = textRng.duplicate(); 881 startRng = textRng.duplicate();
660 882
661 html.on( 'mousemove', onHover ); 883 html.on( 'mousemove', onHover );
662 outerDoc.on( 'mouseup', onSelectEnd ); 884 outerDoc.on( 'mouseup', onSelectEnd );
@@ -670,7 +892,7 @@
670 if ( CKEDITOR.env.version > 7 && CKEDITOR.env.version < 11 ) { 892 if ( CKEDITOR.env.version > 7 && CKEDITOR.env.version < 11 ) {
671 html.on( 'mousedown', function( evt ) { 893 html.on( 'mousedown', function( evt ) {
672 if ( evt.data.getTarget().is( 'html' ) ) { 894 if ( evt.data.getTarget().is( 'html' ) ) {
673 // 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)
674 outerDoc.on( 'mouseup', onSelectEnd ); 896 outerDoc.on( 'mouseup', onSelectEnd );
675 html.on( 'mouseup', onSelectEnd ); 897 html.on( 'mouseup', onSelectEnd );
676 } 898 }
@@ -684,6 +906,14 @@
684 // 2. After the accomplish of keyboard and mouse events. 906 // 2. After the accomplish of keyboard and mouse events.
685 editable.attachListener( editable, 'selectionchange', checkSelectionChange, editor ); 907 editable.attachListener( editable, 'selectionchange', checkSelectionChange, editor );
686 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 );
687 // Always fire the selection change on focus gain. 917 // Always fire the selection change on focus gain.
688 // 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
689 // 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
@@ -693,7 +923,7 @@
693 editor.selectionChange( 1 ); 923 editor.selectionChange( 1 );
694 } ); 924 } );
695 925
696 // #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
697 // by dragging and releasing mouse button outside editable. Dragging (mousedown) 927 // by dragging and releasing mouse button outside editable. Dragging (mousedown)
698 // 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.
699 if ( isInline && ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) ) { 929 if ( isInline && ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) ) {
@@ -707,11 +937,11 @@
707 mouseDown = 0; 937 mouseDown = 0;
708 } ); 938 } );
709 } 939 }
710 // 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.
711 // 941 //
712 // Use document instead of editable in non-IEs for observing mouseup 942 // Use document instead of editable in non-IEs for observing mouseup
713 // 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
714 // of the editor (#9851). 944 // of the editor (http://dev.ckeditor.com/ticket/9851).
715 else { 945 else {
716 editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor ); 946 editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor );
717 } 947 }
@@ -733,18 +963,18 @@
733 case 8: // BACKSPACE 963 case 8: // BACKSPACE
734 case 45: // INS 964 case 45: // INS
735 case 46: // DEl 965 case 46: // DEl
736 removeFillingCharSequenceNode( editable ); 966 if ( editable.hasFocus ) {
967 removeFillingCharSequenceNode( editable );
968 }
737 } 969 }
738 970
739 }, null, null, -1 ); 971 }, null, null, -1 );
740 } 972 }
741 973
742 // Automatically select non-editable element when navigating into
743 // it by left/right or backspace/del keys.
744 editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 ); 974 editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 );
745 975
746 function moveRangeToPoint( range, x, y ) { 976 function moveRangeToPoint( range, x, y ) {
747 // Error prune in IE7. (#9034, #9110) 977 // Error prune in IE7. (http://dev.ckeditor.com/ticket/9034, http://dev.ckeditor.com/ticket/9110)
748 try { 978 try {
749 range.moveToPoint( x, y ); 979 range.moveToPoint( x, y );
750 } catch ( e ) {} 980 } catch ( e ) {}
@@ -765,14 +995,27 @@
765 range = sel.createRange(); 995 range = sel.createRange();
766 996
767 // 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.
768 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.$ )
769 range.select(); 1000 range.select();
770 } 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 }
771 } ); 1014 } );
772 1015
773 editor.on( 'setData', function() { 1016 editor.on( 'setData', function() {
774 // Invalidate locked selection when unloading DOM. 1017 // Invalidate locked selection when unloading DOM.
775 // (#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)
776 editor.unlockSelection(); 1019 editor.unlockSelection();
777 1020
778 // Webkit's selection will mess up after the data loading. 1021 // Webkit's selection will mess up after the data loading.
@@ -786,7 +1029,7 @@
786 editor.unlockSelection(); 1029 editor.unlockSelection();
787 } ); 1030 } );
788 1031
789 // 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).
790 if ( CKEDITOR.env.ie9Compat ) 1033 if ( CKEDITOR.env.ie9Compat )
791 editor.on( 'beforeDestroy', clearSelection, null, null, 9 ); 1034 editor.on( 'beforeDestroy', clearSelection, null, null, 9 );
792 1035
@@ -802,7 +1045,7 @@
802 // 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.
803 editor.on( 'loadSnapshot', function() { 1046 editor.on( 'loadSnapshot', function() {
804 var isElement = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ), 1047 var isElement = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ),
805 // 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,
806 // 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.
807 last = editor.editable().getLast( isElement ); 1050 last = editor.editable().getLast( isElement );
808 1051
@@ -849,7 +1092,7 @@
849 } ); 1092 } );
850 1093
851 // 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
852 // (#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.
853 if ( CKEDITOR.env.webkit ) { 1096 if ( CKEDITOR.env.webkit ) {
854 CKEDITOR.on( 'instanceReady', function( evt ) { 1097 CKEDITOR.on( 'instanceReady', function( evt ) {
855 var editor = evt.editor; 1098 var editor = evt.editor;
@@ -864,7 +1107,7 @@
864 1107
865 // 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.
866 // Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's 1109 // Note: CKEDITOR.dom.range.createBookmark2() normalizes snapshot's
867 // 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).
868 editor.on( 'getSnapshot', function( evt ) { 1111 editor.on( 'getSnapshot', function( evt ) {
869 if ( evt.data ) { 1112 if ( evt.data ) {
870 evt.data = removeFillingCharSequenceString( evt.data ); 1113 evt.data = removeFillingCharSequenceString( evt.data );
@@ -873,7 +1116,7 @@
873 1116
874 // 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
875 // 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,
876 // which should be pruned by the dataProcessor (#13816). 1119 // which should be pruned by the dataProcessor (http://dev.ckeditor.com/ticket/13816).
877 // Note: Used low priority to filter when dataProcessor works on strings, 1120 // Note: Used low priority to filter when dataProcessor works on strings,
878 // not pseudo–DOM. 1121 // not pseudo–DOM.
879 editor.on( 'toDataFormat', function( evt ) { 1122 editor.on( 'toDataFormat', function( evt ) {
@@ -1039,9 +1282,6 @@
1039 */ 1282 */
1040 CKEDITOR.SELECTION_ELEMENT = 3; 1283 CKEDITOR.SELECTION_ELEMENT = 3;
1041 1284
1042 var isMSSelection = typeof window.getSelection != 'function',
1043 nextRev = 1;
1044
1045 /** 1285 /**
1046 * Manipulates the selection within a DOM element. If the current browser selection 1286 * Manipulates the selection within a DOM element. If the current browser selection
1047 * spans outside of the element, an empty selection object is returned. 1287 * spans outside of the element, an empty selection object is returned.
@@ -1121,7 +1361,7 @@
1121 1361
1122 // Selection out of concerned range, empty the selection. 1362 // Selection out of concerned range, empty the selection.
1123 // TODO check whether this condition cannot be reverted to its old 1363 // TODO check whether this condition cannot be reverted to its old
1124 // form (commented out) after we closed #10438. 1364 // form (commented out) after we closed http://dev.ckeditor.com/ticket/10438.
1125 //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) { 1365 //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) {
1126 if ( !( 1366 if ( !(
1127 rangeParent && 1367 rangeParent &&
@@ -1142,10 +1382,6 @@
1142 var styleObjectElements = { img: 1, hr: 1, li: 1, table: 1, tr: 1, td: 1, th: 1, embed: 1, object: 1, ol: 1, ul: 1, 1382 var styleObjectElements = { img: 1, hr: 1, li: 1, table: 1, tr: 1, td: 1, th: 1, embed: 1, object: 1, ol: 1, ul: 1,
1143 a: 1, input: 1, form: 1, select: 1, textarea: 1, button: 1, fieldset: 1, thead: 1, tfoot: 1 }; 1383 a: 1, input: 1, form: 1, select: 1, textarea: 1, button: 1, fieldset: 1, thead: 1, tfoot: 1 };
1144 1384
1145 // #13816
1146 var fillingCharSequence = CKEDITOR.tools.repeat( '\u200b', 7 ),
1147 fillingCharSequenceRegExp = new RegExp( fillingCharSequence + '( )?', 'g' );
1148
1149 CKEDITOR.tools.extend( CKEDITOR.dom.selection, { 1385 CKEDITOR.tools.extend( CKEDITOR.dom.selection, {
1150 _removeFillingCharSequenceString: removeFillingCharSequenceString, 1386 _removeFillingCharSequenceString: removeFillingCharSequenceString,
1151 _createFillingCharSequenceNode: createFillingCharSequenceNode, 1387 _createFillingCharSequenceNode: createFillingCharSequenceNode,
@@ -1167,7 +1403,7 @@
1167 * 1403 *
1168 * var selection = editor.getSelection().getNative(); 1404 * var selection = editor.getSelection().getNative();
1169 * 1405 *
1170 * @returns {Object} The native browser selection object. 1406 * @returns {Object} The native browser selection object or null if this is a fake selection.
1171 */ 1407 */
1172 getNative: function() { 1408 getNative: function() {
1173 if ( this._.cache.nativeSel !== undefined ) 1409 if ( this._.cache.nativeSel !== undefined )
@@ -1259,7 +1495,7 @@
1259 * alert( ranges.length ); 1495 * alert( ranges.length );
1260 * 1496 *
1261 * @method 1497 * @method
1262 * @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.
1263 * @returns {Array} Range instances that represent the current selection. 1499 * @returns {Array} Range instances that represent the current selection.
1264 */ 1500 */
1265 getRanges: ( function() { 1501 getRanges: ( function() {
@@ -1290,7 +1526,7 @@
1290 index = -1, 1526 index = -1,
1291 position, distance, container; 1527 position, distance, container;
1292 1528
1293 // 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
1294 // range is right on the boundary of one element. 1530 // range is right on the boundary of one element.
1295 while ( startIndex <= endIndex ) { 1531 while ( startIndex <= endIndex ) {
1296 index = Math.floor( ( startIndex + endIndex ) / 2 ); 1532 index = Math.floor( ( startIndex + endIndex ) / 2 );
@@ -1306,8 +1542,8 @@
1306 return { container: parent, offset: getNodeIndex( child ) }; 1542 return { container: parent, offset: getNodeIndex( child ) };
1307 } 1543 }
1308 1544
1309 // All childs are text nodes, 1545 // All children are text nodes,
1310 // 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)
1311 if ( index == -1 || index == siblings.length - 1 && position < 0 ) { 1547 if ( index == -1 || index == siblings.length - 1 && position < 0 ) {
1312 // Adapt test range to embrace the entire parent contents. 1548 // Adapt test range to embrace the entire parent contents.
1313 testRange.moveToElementText( parent ); 1549 testRange.moveToElementText( parent );
@@ -1315,7 +1551,7 @@
1315 1551
1316 // IE report line break as CRLF with range.text but 1552 // IE report line break as CRLF with range.text but
1317 // only LF with textnode.nodeValue, normalize them to avoid 1553 // only LF with textnode.nodeValue, normalize them to avoid
1318 // breaking character counting logic below. (#3949) 1554 // breaking character counting logic below. (http://dev.ckeditor.com/ticket/3949)
1319 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; 1555 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
1320 1556
1321 siblings = parent.childNodes; 1557 siblings = parent.childNodes;
@@ -1351,7 +1587,7 @@
1351 1587
1352 // IE report line break as CRLF with range.text but 1588 // IE report line break as CRLF with range.text but
1353 // only LF with textnode.nodeValue, normalize them to avoid 1589 // only LF with textnode.nodeValue, normalize them to avoid
1354 // breaking character counting logic below. (#3949) 1590 // breaking character counting logic below. (http://dev.ckeditor.com/ticket/3949)
1355 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length; 1591 distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
1356 1592
1357 // 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.
@@ -1368,7 +1604,7 @@
1368 } 1604 }
1369 child = sibling; 1605 child = sibling;
1370 } 1606 }
1371 // 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)
1372 catch ( e ) { 1608 catch ( e ) {
1373 return { container: parent, offset: getNodeIndex( child ) }; 1609 return { container: parent, offset: getNodeIndex( child ) };
1374 } 1610 }
@@ -1400,7 +1636,7 @@
1400 boundaryInfo = getBoundaryInformation( nativeRange ); 1636 boundaryInfo = getBoundaryInformation( nativeRange );
1401 range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset ); 1637 range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
1402 1638
1403 // 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)
1404 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() )
1405 range.collapse(); 1641 range.collapse();
1406 1642
@@ -1432,7 +1668,7 @@
1432 } )() : 1668 } )() :
1433 function() { 1669 function() {
1434 // On browsers implementing the W3C range, we simply 1670 // On browsers implementing the W3C range, we simply
1435 // tranform the native ranges in CKEDITOR.dom.range 1671 // transform the native ranges in CKEDITOR.dom.range
1436 // instances. 1672 // instances.
1437 1673
1438 var ranges = [], 1674 var ranges = [],
@@ -1465,7 +1701,7 @@
1465 return ranges; 1701 return ranges;
1466 1702
1467 // Split range into multiple by read-only nodes. 1703 // Split range into multiple by read-only nodes.
1468 // Clone ranges array to avoid changing cached ranges (#11493). 1704 // Clone ranges array to avoid changing cached ranges (http://dev.ckeditor.com/ticket/11493).
1469 return extractEditableRanges( new CKEDITOR.dom.rangeList( ranges.slice() ) ); 1705 return extractEditableRanges( new CKEDITOR.dom.rangeList( ranges.slice() ) );
1470 }; 1706 };
1471 } )(), 1707 } )(),
@@ -1499,11 +1735,11 @@
1499 1735
1500 // Decrease the range content to exclude particial 1736 // Decrease the range content to exclude particial
1501 // selected node on the start which doesn't have 1737 // selected node on the start which doesn't have
1502 // visual impact. ( #3231 ) 1738 // visual impact. ( http://dev.ckeditor.com/ticket/3231 )
1503 while ( 1 ) { 1739 while ( 1 ) {
1504 var startContainer = range.startContainer, 1740 var startContainer = range.startContainer,
1505 startOffset = range.startOffset; 1741 startOffset = range.startOffset;
1506 // Limit the fix only to non-block elements.(#3950) 1742 // Limit the fix only to non-block elements.(http://dev.ckeditor.com/ticket/3950)
1507 if ( startOffset == ( startContainer.getChildCount ? startContainer.getChildCount() : startContainer.getLength() ) && !startContainer.isBlockBoundary() ) 1743 if ( startOffset == ( startContainer.getChildCount ? startContainer.getChildCount() : startContainer.getLength() ) && !startContainer.isBlockBoundary() )
1508 range.setStartAfter( startContainer ); 1744 range.setStartAfter( startContainer );
1509 else 1745 else
@@ -1545,7 +1781,7 @@
1545 * var element = editor.getSelection().getSelectedElement(); 1781 * var element = editor.getSelection().getSelectedElement();
1546 * alert( element.getName() ); 1782 * alert( element.getName() );
1547 * 1783 *
1548 * @returns {CKEDITOR.dom.element} The selected element. Null if no 1784 * @returns {CKEDITOR.dom.element/null} The selected element. `null` if no
1549 * 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}.
1550 */ 1786 */
1551 getSelectedElement: function() { 1787 getSelectedElement: function() {
@@ -1628,7 +1864,7 @@
1628 1864
1629 if ( restore ) { 1865 if ( restore ) {
1630 var selectedElement = this.getSelectedElement(), 1866 var selectedElement = this.getSelectedElement(),
1631 ranges = !selectedElement && this.getRanges(), 1867 ranges = this.getRanges(),
1632 faked = this.isFake; 1868 faked = this.isFake;
1633 } 1869 }
1634 1870
@@ -1642,7 +1878,10 @@
1642 if ( !( common && common.getAscendant( 'body', 1 ) ) ) 1878 if ( !( common && common.getAscendant( 'body', 1 ) ) )
1643 return; 1879 return;
1644 1880
1645 if ( faked ) 1881 if ( isTableSelection( ranges ) ) {
1882 // Tables have it's own selection method.
1883 performFakeTableSelection.call( this, ranges );
1884 } else if ( faked )
1646 this.fake( selectedElement ); 1885 this.fake( selectedElement );
1647 else if ( selectedElement ) 1886 else if ( selectedElement )
1648 this.selectElement( selectedElement ); 1887 this.selectElement( selectedElement );
@@ -1714,7 +1953,7 @@
1714 // Check if there's a hiddenSelectionContainer in editable at some index. 1953 // Check if there's a hiddenSelectionContainer in editable at some index.
1715 // Some ranges may be anchored after the hiddenSelectionContainer and, 1954 // Some ranges may be anchored after the hiddenSelectionContainer and,
1716 // once the container is removed while resetting the selection, they 1955 // once the container is removed while resetting the selection, they
1717 // 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).
1718 if ( hadHiddenSelectionContainer ) 1957 if ( hadHiddenSelectionContainer )
1719 fixRangesAfterHiddenSelectionContainer( ranges, this.root ); 1958 fixRangesAfterHiddenSelectionContainer( ranges, this.root );
1720 1959
@@ -1742,6 +1981,15 @@
1742 return; 1981 return;
1743 } 1982 }
1744 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
1745 if ( isMSSelection ) { 1993 if ( isMSSelection ) {
1746 var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ), 1994 var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
1747 fillerTextRegex = /\ufeff|\u00a0/, 1995 fillerTextRegex = /\ufeff|\u00a0/,
@@ -1775,7 +2023,7 @@
1775 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 ||
1776 range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) { 2024 range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) {
1777 range.shrink( CKEDITOR.NODE_ELEMENT, true ); 2025 range.shrink( CKEDITOR.NODE_ELEMENT, true );
1778 // The range might get collapsed (#7975). Update cached variable. 2026 // The range might get collapsed (http://dev.ckeditor.com/ticket/7975). Update cached variable.
1779 collapsed = range.collapsed; 2027 collapsed = range.collapsed;
1780 } 2028 }
1781 2029
@@ -1817,18 +2065,18 @@
1817 2065
1818 // Append a temporary <span>&#65279;</span> before the selection. 2066 // Append a temporary <span>&#65279;</span> before the selection.
1819 // This is needed to avoid IE destroying selections inside empty 2067 // This is needed to avoid IE destroying selections inside empty
1820 // inline elements, like <b></b> (#253). 2068 // inline elements, like <b></b> (http://dev.ckeditor.com/ticket/253).
1821 // 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
1822 // element to avoid the selection moving inside of it. 2070 // element to avoid the selection moving inside of it.
1823 dummySpan = range.document.createElement( 'span' ); 2071 dummySpan = range.document.createElement( 'span' );
1824 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.
1825 dummySpan.insertBefore( startNode ); 2073 dummySpan.insertBefore( startNode );
1826 2074
1827 if ( isStartMarkerAlone ) { 2075 if ( isStartMarkerAlone ) {
1828 // To expand empty blocks or line spaces after <br>, we need 2076 // To expand empty blocks or line spaces after <br>, we need
1829 // 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
1830 // selection. 2078 // selection.
1831 // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359) 2079 // \ufeff = Zero Width No-Break Space (U+FEFF). (http://dev.ckeditor.com/ticket/1359)
1832 range.document.createText( '\ufeff' ).insertBefore( startNode ); 2080 range.document.createText( '\ufeff' ).insertBefore( startNode );
1833 } 2081 }
1834 } 2082 }
@@ -1860,7 +2108,7 @@
1860 } else { 2108 } else {
1861 var sel = this.getNative(); 2109 var sel = this.getNative();
1862 2110
1863 // 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)
1864 if ( !sel ) 2112 if ( !sel )
1865 return; 2113 return;
1866 2114
@@ -1876,7 +2124,7 @@
1876 between.setStart( left.endContainer, left.endOffset ); 2124 between.setStart( left.endContainer, left.endOffset );
1877 between.setEnd( right.startContainer, right.startOffset ); 2125 between.setEnd( right.startContainer, right.startOffset );
1878 2126
1879 // Don't confused by Firefox adjancent multi-ranges 2127 // Don't confused by Firefox adjacent multi-ranges
1880 // introduced by table cells selection. 2128 // introduced by table cells selection.
1881 if ( !between.collapsed ) { 2129 if ( !between.collapsed ) {
1882 between.shrink( CKEDITOR.NODE_ELEMENT, true ); 2130 between.shrink( CKEDITOR.NODE_ELEMENT, true );
@@ -1885,7 +2133,7 @@
1885 2133
1886 // The following cases has to be considered: 2134 // The following cases has to be considered:
1887 // 1. <span contenteditable="false">[placeholder]</span> 2135 // 1. <span contenteditable="false">[placeholder]</span>
1888 // 2. <input contenteditable="false" type="radio"/> (#6621) 2136 // 2. <input contenteditable="false" type="radio"/> (http://dev.ckeditor.com/ticket/6621)
1889 if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) { 2137 if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) {
1890 right.setStart( left.startContainer, left.startOffset ); 2138 right.setStart( left.startContainer, left.startOffset );
1891 ranges.splice( i--, 1 ); 2139 ranges.splice( i--, 1 );
@@ -1900,7 +2148,7 @@
1900 2148
1901 if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) { 2149 if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) {
1902 // Append a zero-width space so WebKit will not try to 2150 // Append a zero-width space so WebKit will not try to
1903 // move the selection by itself (#1272). 2151 // move the selection by itself (http://dev.ckeditor.com/ticket/1272).
1904 var fillingChar = createFillingCharSequenceNode( this.root ); 2152 var fillingChar = createFillingCharSequenceNode( this.root );
1905 range.insertNode( fillingChar ); 2153 range.insertNode( fillingChar );
1906 2154
@@ -1954,14 +2202,20 @@
1954 * displayed to the user. 2202 * displayed to the user.
1955 * 2203 *
1956 * @param {CKEDITOR.dom.element} element The element to be "selected". 2204 * @param {CKEDITOR.dom.element} element The element to be "selected".
2205 * @param {String} [ariaLabel] A string to be used by the screen reader to describe the selection.
1957 */ 2206 */
1958 fake: function( element ) { 2207 fake: function( element, ariaLabel ) {
1959 var editor = this.root.editor; 2208 var editor = this.root.editor;
1960 2209
2210 // Attempt to retrieve aria-label if possible (http://dev.ckeditor.com/ticket/14539).
2211 if ( ariaLabel === undefined && element.hasAttribute( 'aria-label' ) ) {
2212 ariaLabel = element.getAttribute( 'aria-label' );
2213 }
2214
1961 // Cleanup after previous selection - e.g. remove hidden sel container. 2215 // Cleanup after previous selection - e.g. remove hidden sel container.
1962 this.reset(); 2216 this.reset();
1963 2217
1964 hideSelection( editor ); 2218 hideSelection( editor, ariaLabel );
1965 2219
1966 // Set this value after executing hiseSelection, because it may 2220 // Set this value after executing hiseSelection, because it may
1967 // cause reset() which overwrites cache. 2221 // cause reset() which overwrites cache.
@@ -2012,6 +2266,38 @@
2012 }, 2266 },
2013 2267
2014 /** 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 /**
2015 * 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})
2016 * by calling the {@link CKEDITOR.dom.range#createBookmark} method, 2302 * by calling the {@link CKEDITOR.dom.range#createBookmark} method,
2017 * 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
@@ -2064,17 +2350,19 @@
2064 2350
2065 // 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.
2066 if ( bookmarks.isFake ) { 2352 if ( bookmarks.isFake ) {
2067 node = ranges[ 0 ].getEnclosedNode(); 2353 node = isTableSelection( ranges ) ? ranges[ 0 ]._getTableElement() : ranges[ 0 ].getEnclosedNode();
2354
2068 if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) { 2355 if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) {
2069 CKEDITOR.warn( 'selection-not-fake' ); 2356 CKEDITOR.warn( 'selection-not-fake' );
2070 bookmarks.isFake = 0; 2357 bookmarks.isFake = 0;
2071 } 2358 }
2072 } 2359 }
2073 2360
2074 if ( bookmarks.isFake ) 2361 if ( bookmarks.isFake && !isTableSelection( ranges ) ) {
2075 this.fake( node ); 2362 this.fake( node );
2076 else 2363 } else {
2077 this.selectRanges( ranges ); 2364 this.selectRanges( ranges );
2365 }
2078 2366
2079 return this; 2367 return this;
2080 }, 2368 },
@@ -2111,7 +2399,7 @@
2111 * Remove all the selection ranges from the document. 2399 * Remove all the selection ranges from the document.
2112 */ 2400 */
2113 removeAllRanges: function() { 2401 removeAllRanges: function() {
2114 // 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).
2115 if ( this.getType() == CKEDITOR.SELECTION_NONE ) 2403 if ( this.getType() == CKEDITOR.SELECTION_NONE )
2116 return; 2404 return;
2117 2405