diff options
Diffstat (limited to 'sources/core/selection.js')
-rw-r--r-- | sources/core/selection.js | 403 |
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></span> before the selection. | 2066 | // Append a temporary <span></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( '' ); // Zero Width No-Break Space (U+FEFF). See #1359. | 2072 | dummySpan.setHtml( '' ); // 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 | ||