@ckeditor/ckeditor5-typing 35.2.1 → 35.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +31 -18
- package/src/delete.js +68 -123
- package/src/deletecommand.js +205 -242
- package/src/deleteobserver.js +205 -112
- package/src/index.js +0 -6
- package/src/input.js +125 -28
- package/src/inserttextcommand.js +96 -0
- package/src/inserttextobserver.js +104 -0
- package/src/texttransformation.js +174 -384
- package/src/textwatcher.js +131 -171
- package/src/twostepcaretmovement.js +300 -341
- package/src/typing.js +9 -43
- package/src/typingconfig.js +5 -0
- package/src/utils/changebuffer.js +142 -151
- package/src/utils/findattributerange.js +12 -24
- package/src/utils/getlasttextline.js +11 -29
- package/src/utils/inlinehighlight.js +38 -52
- package/src/inputcommand.js +0 -100
- package/src/utils/injecttypingmutationshandling.js +0 -331
- package/src/utils/injectunsafekeystrokeshandling.js +0 -189
- package/src/utils/utils.js +0 -104
package/src/inputcommand.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @module typing/inputcommand
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import Command from '@ckeditor/ckeditor5-core/src/command';
|
|
11
|
-
|
|
12
|
-
import ChangeBuffer from './utils/changebuffer';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The input command. Used by the {@link module:typing/input~Input input feature} to handle typing.
|
|
16
|
-
*
|
|
17
|
-
* @extends module:core/command~Command
|
|
18
|
-
*/
|
|
19
|
-
export default class InputCommand extends Command {
|
|
20
|
-
/**
|
|
21
|
-
* Creates an instance of the command.
|
|
22
|
-
*
|
|
23
|
-
* @param {module:core/editor/editor~Editor} editor
|
|
24
|
-
* @param {Number} undoStepSize The maximum number of atomic changes
|
|
25
|
-
* which can be contained in one batch in the command buffer.
|
|
26
|
-
*/
|
|
27
|
-
constructor( editor, undoStepSize ) {
|
|
28
|
-
super( editor );
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Typing's change buffer used to group subsequent changes into batches.
|
|
32
|
-
*
|
|
33
|
-
* @readonly
|
|
34
|
-
* @private
|
|
35
|
-
* @member {module:typing/utils/changebuffer~ChangeBuffer} #_buffer
|
|
36
|
-
*/
|
|
37
|
-
this._buffer = new ChangeBuffer( editor.model, undoStepSize );
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The current change buffer.
|
|
42
|
-
*
|
|
43
|
-
* @type {module:typing/utils/changebuffer~ChangeBuffer}
|
|
44
|
-
*/
|
|
45
|
-
get buffer() {
|
|
46
|
-
return this._buffer;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @inheritDoc
|
|
51
|
-
*/
|
|
52
|
-
destroy() {
|
|
53
|
-
super.destroy();
|
|
54
|
-
|
|
55
|
-
this._buffer.destroy();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Executes the input command. It replaces the content within the given range with the given text.
|
|
60
|
-
* Replacing is a two step process, first the content within the range is removed and then the new text is inserted
|
|
61
|
-
* at the beginning of the range (which after the removal is a collapsed range).
|
|
62
|
-
*
|
|
63
|
-
* @fires execute
|
|
64
|
-
* @param {Object} [options] The command options.
|
|
65
|
-
* @param {String} [options.text=''] The text to be inserted.
|
|
66
|
-
* @param {module:engine/model/range~Range} [options.range] The range in which the text is inserted. Defaults
|
|
67
|
-
* to the first range in the current selection.
|
|
68
|
-
* @param {module:engine/model/range~Range} [options.resultRange] The range where the selection
|
|
69
|
-
* should be placed after the insertion. If not specified, the selection will be placed right after
|
|
70
|
-
* the inserted text.
|
|
71
|
-
*/
|
|
72
|
-
execute( options = {} ) {
|
|
73
|
-
const model = this.editor.model;
|
|
74
|
-
const doc = model.document;
|
|
75
|
-
const text = options.text || '';
|
|
76
|
-
const textInsertions = text.length;
|
|
77
|
-
const selection = options.range ? model.createSelection( options.range ) : doc.selection;
|
|
78
|
-
const resultRange = options.resultRange;
|
|
79
|
-
|
|
80
|
-
model.enqueueChange( this._buffer.batch, writer => {
|
|
81
|
-
this._buffer.lock();
|
|
82
|
-
|
|
83
|
-
model.deleteContent( selection );
|
|
84
|
-
|
|
85
|
-
if ( text ) {
|
|
86
|
-
model.insertContent( writer.createText( text, doc.selection.getAttributes() ), selection );
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if ( resultRange ) {
|
|
90
|
-
writer.setSelection( resultRange );
|
|
91
|
-
} else if ( !selection.is( 'documentSelection' ) ) {
|
|
92
|
-
writer.setSelection( selection );
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this._buffer.unlock();
|
|
96
|
-
|
|
97
|
-
this._buffer.input( textInsertions );
|
|
98
|
-
} );
|
|
99
|
-
}
|
|
100
|
-
}
|
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @module typing/utils/injecttypingmutationshandling
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import diff from '@ckeditor/ckeditor5-utils/src/diff';
|
|
11
|
-
import DomConverter from '@ckeditor/ckeditor5-engine/src/view/domconverter';
|
|
12
|
-
|
|
13
|
-
import { getSingleTextNodeChange, containerChildrenMutated } from './utils';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Handles mutations caused by normal typing.
|
|
17
|
-
*
|
|
18
|
-
* @param {module:core/editor/editor~Editor} editor The editor instance.
|
|
19
|
-
*/
|
|
20
|
-
export default function injectTypingMutationsHandling( editor ) {
|
|
21
|
-
editor.editing.view.document.on( 'mutations', ( evt, mutations, viewSelection ) => {
|
|
22
|
-
new MutationHandler( editor ).handle( mutations, viewSelection );
|
|
23
|
-
} );
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Helper class for translating DOM mutations into model changes.
|
|
28
|
-
*
|
|
29
|
-
* @private
|
|
30
|
-
*/
|
|
31
|
-
class MutationHandler {
|
|
32
|
-
/**
|
|
33
|
-
* Creates an instance of the mutation handler.
|
|
34
|
-
*
|
|
35
|
-
* @param {module:core/editor/editor~Editor} editor
|
|
36
|
-
*/
|
|
37
|
-
constructor( editor ) {
|
|
38
|
-
/**
|
|
39
|
-
* Editor instance for which mutations are handled.
|
|
40
|
-
*
|
|
41
|
-
* @readonly
|
|
42
|
-
* @member {module:core/editor/editor~Editor} #editor
|
|
43
|
-
*/
|
|
44
|
-
this.editor = editor;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The editing controller.
|
|
48
|
-
*
|
|
49
|
-
* @readonly
|
|
50
|
-
* @member {module:engine/controller/editingcontroller~EditingController} #editing
|
|
51
|
-
*/
|
|
52
|
-
this.editing = this.editor.editing;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Handles given mutations.
|
|
57
|
-
*
|
|
58
|
-
* @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
|
|
59
|
-
* module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
|
|
60
|
-
* @param {module:engine/view/selection~Selection|null} viewSelection
|
|
61
|
-
*/
|
|
62
|
-
handle( mutations, viewSelection ) {
|
|
63
|
-
if ( containerChildrenMutated( mutations ) ) {
|
|
64
|
-
this._handleContainerChildrenMutations( mutations, viewSelection );
|
|
65
|
-
} else {
|
|
66
|
-
for ( const mutation of mutations ) {
|
|
67
|
-
// Fortunately it will never be both.
|
|
68
|
-
this._handleTextMutation( mutation, viewSelection );
|
|
69
|
-
this._handleTextNodeInsertion( mutation );
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Handles situations when container's children mutated during input. This can happen when
|
|
76
|
-
* the browser is trying to "fix" DOM in certain situations. For example, when the user starts to type
|
|
77
|
-
* in `<p><a href=""><i>Link{}</i></a></p>`, the browser might change the order of elements
|
|
78
|
-
* to `<p><i><a href="">Link</a>x{}</i></p>`. A similar situation happens when the spell checker
|
|
79
|
-
* replaces a word wrapped with `<strong>` with a word wrapped with a `<b>` element.
|
|
80
|
-
*
|
|
81
|
-
* To handle such situations, the common DOM ancestor of all mutations is converted to the model representation
|
|
82
|
-
* and then compared with the current model to calculate the proper text change.
|
|
83
|
-
*
|
|
84
|
-
* Note: Single text node insertion is handled in {@link #_handleTextNodeInsertion} and text node mutation is handled
|
|
85
|
-
* in {@link #_handleTextMutation}).
|
|
86
|
-
*
|
|
87
|
-
* @private
|
|
88
|
-
* @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
|
|
89
|
-
* module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
|
|
90
|
-
* @param {module:engine/view/selection~Selection|null} viewSelection
|
|
91
|
-
*/
|
|
92
|
-
_handleContainerChildrenMutations( mutations, viewSelection ) {
|
|
93
|
-
// Get common ancestor of all mutations.
|
|
94
|
-
const mutationsCommonAncestor = getMutationsContainer( mutations );
|
|
95
|
-
|
|
96
|
-
// Quit if there is no common ancestor.
|
|
97
|
-
if ( !mutationsCommonAncestor ) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const domConverter = this.editor.editing.view.domConverter;
|
|
102
|
-
|
|
103
|
-
// Get common ancestor in DOM.
|
|
104
|
-
const domMutationCommonAncestor = domConverter.mapViewToDom( mutationsCommonAncestor );
|
|
105
|
-
|
|
106
|
-
// Create fresh DomConverter so it will not use existing mapping and convert current DOM to model.
|
|
107
|
-
// This wouldn't be needed if DomConverter would allow to create fresh view without checking any mappings.
|
|
108
|
-
const freshDomConverter = new DomConverter( this.editor.editing.view.document );
|
|
109
|
-
const modelFromCurrentDom = this.editor.data.toModel(
|
|
110
|
-
freshDomConverter.domToView( domMutationCommonAncestor )
|
|
111
|
-
).getChild( 0 );
|
|
112
|
-
|
|
113
|
-
// Current model.
|
|
114
|
-
const currentModel = this.editor.editing.mapper.toModelElement( mutationsCommonAncestor );
|
|
115
|
-
|
|
116
|
-
// If common ancestor is not mapped, do not do anything. It probably is a parent of another view element.
|
|
117
|
-
// That means that we would need to diff model elements (see `if` below). Better return early instead of
|
|
118
|
-
// trying to get a reasonable model ancestor. It will fell into the `if` below anyway.
|
|
119
|
-
// This situation happens for example for lists. If `<ul>` is a common ancestor, `currentModel` is `undefined`
|
|
120
|
-
// because `<ul>` is not mapped (`<li>`s are).
|
|
121
|
-
// See https://github.com/ckeditor/ckeditor5/issues/718.
|
|
122
|
-
if ( !currentModel ) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Get children from both ancestors.
|
|
127
|
-
const modelFromDomChildren = Array.from( modelFromCurrentDom.getChildren() );
|
|
128
|
-
const currentModelChildren = Array.from( currentModel.getChildren() );
|
|
129
|
-
|
|
130
|
-
// Remove the last `<softBreak>` from the end of `modelFromDomChildren` if there is no `<softBreak>` in current model.
|
|
131
|
-
// If the described scenario happened, it means that this is a bogus `<br />` added by a browser.
|
|
132
|
-
const lastDomChild = modelFromDomChildren[ modelFromDomChildren.length - 1 ];
|
|
133
|
-
const lastCurrentChild = currentModelChildren[ currentModelChildren.length - 1 ];
|
|
134
|
-
|
|
135
|
-
const isLastDomChildSoftBreak = lastDomChild && lastDomChild.is( 'element', 'softBreak' );
|
|
136
|
-
const isLastCurrentChildSoftBreak = lastCurrentChild && !lastCurrentChild.is( 'element', 'softBreak' );
|
|
137
|
-
|
|
138
|
-
if ( isLastDomChildSoftBreak && isLastCurrentChildSoftBreak ) {
|
|
139
|
-
modelFromDomChildren.pop();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const schema = this.editor.model.schema;
|
|
143
|
-
|
|
144
|
-
// Skip situations when common ancestor has any container elements.
|
|
145
|
-
if ( !isSafeForTextMutation( modelFromDomChildren, schema ) || !isSafeForTextMutation( currentModelChildren, schema ) ) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Replace inserted by the browser with normal space. See comment in `_handleTextMutation`.
|
|
150
|
-
// Replace non-texts with any character. This is potentially dangerous but passes in manual tests. The thing is
|
|
151
|
-
// that we need to take care of proper indexes so we cannot simply remove non-text elements from the content.
|
|
152
|
-
// By inserting a character we keep all the real texts on their indexes.
|
|
153
|
-
const newText = modelFromDomChildren.map( item => item.is( '$text' ) ? item.data : '@' ).join( '' ).replace( /\u00A0/g, ' ' );
|
|
154
|
-
const oldText = currentModelChildren.map( item => item.is( '$text' ) ? item.data : '@' ).join( '' ).replace( /\u00A0/g, ' ' );
|
|
155
|
-
|
|
156
|
-
// Do nothing if mutations created same text.
|
|
157
|
-
if ( oldText === newText ) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const diffResult = diff( oldText, newText );
|
|
162
|
-
|
|
163
|
-
const { firstChangeAt, insertions, deletions } = calculateChanges( diffResult );
|
|
164
|
-
|
|
165
|
-
// Try setting new model selection according to passed view selection.
|
|
166
|
-
let modelSelectionRange = null;
|
|
167
|
-
|
|
168
|
-
if ( viewSelection ) {
|
|
169
|
-
modelSelectionRange = this.editing.mapper.toModelRange( viewSelection.getFirstRange() );
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const insertText = newText.substr( firstChangeAt, insertions );
|
|
173
|
-
const removeRange = this.editor.model.createRange(
|
|
174
|
-
this.editor.model.createPositionAt( currentModel, firstChangeAt ),
|
|
175
|
-
this.editor.model.createPositionAt( currentModel, firstChangeAt + deletions )
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
this.editor.execute( 'input', {
|
|
179
|
-
text: insertText,
|
|
180
|
-
range: removeRange,
|
|
181
|
-
resultRange: modelSelectionRange
|
|
182
|
-
} );
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* @private
|
|
187
|
-
*/
|
|
188
|
-
_handleTextMutation( mutation, viewSelection ) {
|
|
189
|
-
if ( mutation.type != 'text' ) {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Replace inserted by the browser with normal space.
|
|
194
|
-
// We want only normal spaces in the model and in the view. Renderer and DOM Converter will be then responsible
|
|
195
|
-
// for rendering consecutive spaces using , but the model and the view has to be clear.
|
|
196
|
-
// Other feature may introduce inserting non-breakable space on specific key stroke (for example shift + space).
|
|
197
|
-
// However then it will be handled outside of mutations, like enter key is.
|
|
198
|
-
// The replacing is here because it has to be done before `diff` and `diffToChanges` functions, as they
|
|
199
|
-
// take `newText` and compare it to (cleaned up) view.
|
|
200
|
-
// It could also be done in mutation observer too, however if any outside plugin would like to
|
|
201
|
-
// introduce additional events for mutations, they would get already cleaned up version (this may be good or not).
|
|
202
|
-
const newText = mutation.newText.replace( /\u00A0/g, ' ' );
|
|
203
|
-
// To have correct `diffResult`, we also compare view node text data with replaced by space.
|
|
204
|
-
const oldText = mutation.oldText.replace( /\u00A0/g, ' ' );
|
|
205
|
-
|
|
206
|
-
// Do nothing if mutations created same text.
|
|
207
|
-
if ( oldText === newText ) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const diffResult = diff( oldText, newText );
|
|
212
|
-
|
|
213
|
-
const { firstChangeAt, insertions, deletions } = calculateChanges( diffResult );
|
|
214
|
-
|
|
215
|
-
// Try setting new model selection according to passed view selection.
|
|
216
|
-
let modelSelectionRange = null;
|
|
217
|
-
|
|
218
|
-
if ( viewSelection ) {
|
|
219
|
-
modelSelectionRange = this.editing.mapper.toModelRange( viewSelection.getFirstRange() );
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Get the position in view and model where the changes will happen.
|
|
223
|
-
const viewPos = this.editing.view.createPositionAt( mutation.node, firstChangeAt );
|
|
224
|
-
const modelPos = this.editing.mapper.toModelPosition( viewPos );
|
|
225
|
-
const removeRange = this.editor.model.createRange( modelPos, modelPos.getShiftedBy( deletions ) );
|
|
226
|
-
const insertText = newText.substr( firstChangeAt, insertions );
|
|
227
|
-
|
|
228
|
-
this.editor.execute( 'input', {
|
|
229
|
-
text: insertText,
|
|
230
|
-
range: removeRange,
|
|
231
|
-
resultRange: modelSelectionRange
|
|
232
|
-
} );
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* @private
|
|
237
|
-
*/
|
|
238
|
-
_handleTextNodeInsertion( mutation ) {
|
|
239
|
-
if ( mutation.type != 'children' ) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const change = getSingleTextNodeChange( mutation );
|
|
244
|
-
const viewPos = this.editing.view.createPositionAt( mutation.node, change.index );
|
|
245
|
-
const modelPos = this.editing.mapper.toModelPosition( viewPos );
|
|
246
|
-
const insertedText = change.values[ 0 ].data;
|
|
247
|
-
|
|
248
|
-
this.editor.execute( 'input', {
|
|
249
|
-
// Replace inserted by the browser with normal space.
|
|
250
|
-
// See comment in `_handleTextMutation`.
|
|
251
|
-
// In this case we don't need to do this before `diff` because we diff whole nodes.
|
|
252
|
-
// Just change in case there are some.
|
|
253
|
-
text: insertedText.replace( /\u00A0/g, ' ' ),
|
|
254
|
-
range: this.editor.model.createRange( modelPos )
|
|
255
|
-
} );
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Returns first common ancestor of all mutations that is either {@link module:engine/view/containerelement~ContainerElement}
|
|
260
|
-
// or {@link module:engine/view/rootelement~RootElement}.
|
|
261
|
-
//
|
|
262
|
-
// @private
|
|
263
|
-
// @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
|
|
264
|
-
// module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
|
|
265
|
-
// @returns {module:engine/view/containerelement~ContainerElement|engine/view/rootelement~RootElement|undefined}
|
|
266
|
-
function getMutationsContainer( mutations ) {
|
|
267
|
-
const lca = mutations
|
|
268
|
-
.map( mutation => mutation.node )
|
|
269
|
-
.reduce( ( commonAncestor, node ) => {
|
|
270
|
-
return commonAncestor.getCommonAncestor( node, { includeSelf: true } );
|
|
271
|
-
} );
|
|
272
|
-
|
|
273
|
-
if ( !lca ) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// We need to look for container and root elements only, so check all LCA's
|
|
278
|
-
// ancestors (starting from itself).
|
|
279
|
-
return lca.getAncestors( { includeSelf: true, parentFirst: true } )
|
|
280
|
-
.find( element => element.is( 'containerElement' ) || element.is( 'rootElement' ) );
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Returns true if provided array contains content that won't be problematic during diffing and text mutation handling.
|
|
284
|
-
//
|
|
285
|
-
// @param {Array.<module:engine/model/node~Node>} children
|
|
286
|
-
// @param {module:engine/model/schema~Schema} schema
|
|
287
|
-
// @returns {Boolean}
|
|
288
|
-
function isSafeForTextMutation( children, schema ) {
|
|
289
|
-
return children.every( child => schema.isInline( child ) );
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Calculates first change index and number of characters that should be inserted and deleted starting from that index.
|
|
293
|
-
//
|
|
294
|
-
// @private
|
|
295
|
-
// @param diffResult
|
|
296
|
-
// @returns {{insertions: number, deletions: number, firstChangeAt: *}}
|
|
297
|
-
function calculateChanges( diffResult ) {
|
|
298
|
-
// Index where the first change happens. Used to set the position from which nodes will be removed and where will be inserted.
|
|
299
|
-
let firstChangeAt = null;
|
|
300
|
-
// Index where the last change happens. Used to properly count how many characters have to be removed and inserted.
|
|
301
|
-
let lastChangeAt = null;
|
|
302
|
-
|
|
303
|
-
// Get `firstChangeAt` and `lastChangeAt`.
|
|
304
|
-
for ( let i = 0; i < diffResult.length; i++ ) {
|
|
305
|
-
const change = diffResult[ i ];
|
|
306
|
-
|
|
307
|
-
if ( change != 'equal' ) {
|
|
308
|
-
firstChangeAt = firstChangeAt === null ? i : firstChangeAt;
|
|
309
|
-
lastChangeAt = i;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// How many characters, starting from `firstChangeAt`, should be removed.
|
|
314
|
-
let deletions = 0;
|
|
315
|
-
// How many characters, starting from `firstChangeAt`, should be inserted.
|
|
316
|
-
let insertions = 0;
|
|
317
|
-
|
|
318
|
-
for ( let i = firstChangeAt; i <= lastChangeAt; i++ ) {
|
|
319
|
-
// If there is no change (equal) or delete, the character is existing in `oldText`. We count it for removing.
|
|
320
|
-
if ( diffResult[ i ] != 'insert' ) {
|
|
321
|
-
deletions++;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// If there is no change (equal) or insert, the character is existing in `newText`. We count it for inserting.
|
|
325
|
-
if ( diffResult[ i ] != 'delete' ) {
|
|
326
|
-
insertions++;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return { insertions, deletions, firstChangeAt };
|
|
331
|
-
}
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @module typing/utils/injectunsafekeystrokeshandling
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
|
|
11
|
-
import env from '@ckeditor/ckeditor5-utils/src/env';
|
|
12
|
-
import { isShiftDeleteOnNonCollapsedSelection } from './utils';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Handles keystrokes which are unsafe for typing. This handler's logic is explained
|
|
16
|
-
* in https://github.com/ckeditor/ckeditor5-typing/issues/83#issuecomment-398690251.
|
|
17
|
-
*
|
|
18
|
-
* @param {module:core/editor/editor~Editor} editor The editor instance.
|
|
19
|
-
*/
|
|
20
|
-
export default function injectUnsafeKeystrokesHandling( editor ) {
|
|
21
|
-
let latestCompositionSelection = null;
|
|
22
|
-
|
|
23
|
-
const model = editor.model;
|
|
24
|
-
const view = editor.editing.view;
|
|
25
|
-
const inputCommand = editor.commands.get( 'input' );
|
|
26
|
-
|
|
27
|
-
// For Android, we want to handle keystrokes on `beforeinput` to be sure that code in `DeleteObserver` already had a chance to be fired.
|
|
28
|
-
if ( env.isAndroid ) {
|
|
29
|
-
view.document.on( 'beforeinput', ( evt, evtData ) => handleUnsafeKeystroke( evtData ), { priority: 'lowest' } );
|
|
30
|
-
} else {
|
|
31
|
-
view.document.on( 'keydown', ( evt, evtData ) => handleUnsafeKeystroke( evtData ), { priority: 'lowest' } );
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
view.document.on( 'compositionstart', handleCompositionStart, { priority: 'lowest' } );
|
|
35
|
-
|
|
36
|
-
view.document.on( 'compositionend', () => {
|
|
37
|
-
latestCompositionSelection = model.createSelection( model.document.selection );
|
|
38
|
-
}, { priority: 'lowest' } );
|
|
39
|
-
|
|
40
|
-
// Handles the keydown event. We need to guess whether such keystroke is going to result
|
|
41
|
-
// in typing. If so, then before character insertion happens, any selected content needs
|
|
42
|
-
// to be deleted. Otherwise the default browser deletion mechanism would be
|
|
43
|
-
// triggered, resulting in:
|
|
44
|
-
//
|
|
45
|
-
// * Hundreds of mutations which could not be handled.
|
|
46
|
-
// * But most importantly, loss of control over how the content is being deleted.
|
|
47
|
-
//
|
|
48
|
-
// The method is used in a low-priority listener, hence allowing other listeners (e.g. delete or enter features)
|
|
49
|
-
// to handle the event.
|
|
50
|
-
//
|
|
51
|
-
// @param {module:engine/view/observer/keyobserver~KeyEventData} evtData
|
|
52
|
-
function handleUnsafeKeystroke( evtData ) {
|
|
53
|
-
// Do not delete the content, if Shift + Delete key combination was pressed on a non-collapsed selection on Windows.
|
|
54
|
-
//
|
|
55
|
-
// The Shift + Delete key combination should work in the same way as the `cut` event on a non-collapsed selection on Windows.
|
|
56
|
-
// In fact, the native `cut` event is actually emitted in this case, but with lower priority. Therefore, in order to handle the
|
|
57
|
-
// Shift + Delete key combination correctly, it is enough to prevent the content deletion here.
|
|
58
|
-
if ( env.isWindows && isShiftDeleteOnNonCollapsedSelection( evtData, view.document ) ) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const doc = model.document;
|
|
63
|
-
const isComposing = view.document.isComposing;
|
|
64
|
-
const isSelectionUnchanged = latestCompositionSelection && latestCompositionSelection.isEqual( doc.selection );
|
|
65
|
-
|
|
66
|
-
// Reset stored composition selection.
|
|
67
|
-
latestCompositionSelection = null;
|
|
68
|
-
|
|
69
|
-
// By relying on the state of the input command we allow disabling the entire input easily
|
|
70
|
-
// by just disabling the input command. We could’ve used here the delete command but that
|
|
71
|
-
// would mean requiring the delete feature which would block loading one without the other.
|
|
72
|
-
// We could also check the editor.isReadOnly property, but that wouldn't allow to block
|
|
73
|
-
// the input without blocking other features.
|
|
74
|
-
if ( !inputCommand.isEnabled ) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if ( isNonTypingKeystroke( evtData ) || doc.selection.isCollapsed ) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// If during composition, deletion should be prevented as it may remove composed sequence (#83).
|
|
83
|
-
if ( isComposing && evtData.keyCode === 229 ) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// If there is a `keydown` event fired with '229' keycode it might be related
|
|
88
|
-
// to recent composition. Check if selection is the same as upon ending recent composition,
|
|
89
|
-
// if so do not remove selected content as it will remove composed sequence (#83).
|
|
90
|
-
if ( !isComposing && evtData.keyCode === 229 && isSelectionUnchanged ) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
deleteSelectionContent();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Handles the `compositionstart` event. It is used only in special cases to remove the contents
|
|
98
|
-
// of a non-collapsed selection so composition itself does not result in complex mutations.
|
|
99
|
-
//
|
|
100
|
-
// The special case mentioned above is a situation in which the `keydown` event is fired after
|
|
101
|
-
// `compositionstart` event. In such cases {@link #handleKeydown} cannot clear current selection
|
|
102
|
-
// contents (because it is too late and will break the composition) so the composition handler takes care of it.
|
|
103
|
-
function handleCompositionStart() {
|
|
104
|
-
const doc = model.document;
|
|
105
|
-
const isFlatSelection = doc.selection.rangeCount === 1 ? doc.selection.getFirstRange().isFlat : true;
|
|
106
|
-
|
|
107
|
-
// If on `compositionstart` there is a non-collapsed selection which start and end have different parents
|
|
108
|
-
// it means the `handleKeydown()` method did not remove its contents. It happens usually because
|
|
109
|
-
// of different order of events (`compositionstart` before `keydown` - in Safari). In such cases
|
|
110
|
-
// we need to remove selection contents on composition start (#83).
|
|
111
|
-
if ( doc.selection.isCollapsed || isFlatSelection ) {
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
deleteSelectionContent();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function deleteSelectionContent() {
|
|
119
|
-
const buffer = inputCommand.buffer;
|
|
120
|
-
|
|
121
|
-
buffer.lock();
|
|
122
|
-
|
|
123
|
-
const batch = buffer.batch;
|
|
124
|
-
|
|
125
|
-
model.enqueueChange( batch, () => {
|
|
126
|
-
model.deleteContent( model.document.selection );
|
|
127
|
-
} );
|
|
128
|
-
|
|
129
|
-
buffer.unlock();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const safeKeycodes = [
|
|
134
|
-
getCode( 'arrowUp' ),
|
|
135
|
-
getCode( 'arrowRight' ),
|
|
136
|
-
getCode( 'arrowDown' ),
|
|
137
|
-
getCode( 'arrowLeft' ),
|
|
138
|
-
9, // Tab
|
|
139
|
-
16, // Shift
|
|
140
|
-
17, // Ctrl
|
|
141
|
-
18, // Alt
|
|
142
|
-
19, // Pause
|
|
143
|
-
20, // CapsLock
|
|
144
|
-
27, // Escape
|
|
145
|
-
33, // PageUp
|
|
146
|
-
34, // PageDown
|
|
147
|
-
35, // Home
|
|
148
|
-
36, // End,
|
|
149
|
-
45, // Insert,
|
|
150
|
-
91, // Windows,
|
|
151
|
-
93, // Menu key,
|
|
152
|
-
144, // NumLock
|
|
153
|
-
145, // ScrollLock,
|
|
154
|
-
173, // Mute/Unmute
|
|
155
|
-
174, // Volume up
|
|
156
|
-
175, // Volume down,
|
|
157
|
-
176, // Next song,
|
|
158
|
-
177, // Previous song,
|
|
159
|
-
178, // Stop,
|
|
160
|
-
179, // Play/Pause,
|
|
161
|
-
255 // Display brightness (increase and decrease)
|
|
162
|
-
];
|
|
163
|
-
|
|
164
|
-
// Function keys.
|
|
165
|
-
for ( let code = 112; code <= 135; code++ ) {
|
|
166
|
-
safeKeycodes.push( code );
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Returns `true` if a keystroke will **not** result in "typing".
|
|
171
|
-
*
|
|
172
|
-
* For instance, keystrokes that result in typing are letters "a-zA-Z", numbers "0-9", delete, backspace, etc.
|
|
173
|
-
*
|
|
174
|
-
* Keystrokes that do not cause typing are, for instance, Fn keys (F5, F8, etc.), arrow keys (←, →, ↑, ↓),
|
|
175
|
-
* Tab (↹), "Windows logo key" (⊞ Win), etc.
|
|
176
|
-
*
|
|
177
|
-
* Note: This implementation is very simple and will need to be refined with time.
|
|
178
|
-
*
|
|
179
|
-
* @param {module:engine/view/observer/keyobserver~KeyEventData} keyData
|
|
180
|
-
* @returns {Boolean}
|
|
181
|
-
*/
|
|
182
|
-
export function isNonTypingKeystroke( keyData ) {
|
|
183
|
-
// Keystrokes which contain Ctrl or Cmd don't represent typing.
|
|
184
|
-
if ( keyData.ctrlKey || keyData.metaKey ) {
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return safeKeycodes.includes( keyData.keyCode );
|
|
189
|
-
}
|