@ckeditor/ckeditor5-typing 42.0.2 → 43.0.0-alpha.1
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/CHANGELOG.md +1 -233
- package/dist/index.js +332 -69
- package/dist/index.js.map +1 -1
- package/dist/input.d.ts +8 -0
- package/dist/inserttextobserver.d.ts +2 -6
- package/package.json +4 -4
- package/src/input.d.ts +8 -0
- package/src/input.js +297 -22
- package/src/inserttextobserver.d.ts +2 -6
- package/src/inserttextobserver.js +36 -42
package/dist/input.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ import { Plugin } from '@ckeditor/ckeditor5-core';
|
|
|
14
14
|
* Handles text input coming from the keyboard or other input methods.
|
|
15
15
|
*/
|
|
16
16
|
export default class Input extends Plugin {
|
|
17
|
+
/**
|
|
18
|
+
* The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
|
|
19
|
+
*/
|
|
20
|
+
private _compositionQueue;
|
|
17
21
|
/**
|
|
18
22
|
* @inheritDoc
|
|
19
23
|
*/
|
|
@@ -22,4 +26,8 @@ export default class Input extends Plugin {
|
|
|
22
26
|
* @inheritDoc
|
|
23
27
|
*/
|
|
24
28
|
init(): void;
|
|
29
|
+
/**
|
|
30
|
+
* @inheritDoc
|
|
31
|
+
*/
|
|
32
|
+
destroy(): void;
|
|
25
33
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
7
7
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
8
8
|
*/
|
|
9
|
-
import { DomEventData, Observer, FocusObserver, type EditingView, type ViewDocumentSelection, type
|
|
9
|
+
import { DomEventData, Observer, FocusObserver, type EditingView, type ViewDocumentSelection, type ViewSelection } from '@ckeditor/ckeditor5-engine';
|
|
10
10
|
/**
|
|
11
11
|
* Text insertion observer introduces the {@link module:engine/view/document~Document#event:insertText} event.
|
|
12
12
|
*/
|
|
@@ -55,9 +55,5 @@ export interface InsertTextEventData extends DomEventData {
|
|
|
55
55
|
* The selection into which the text should be inserted.
|
|
56
56
|
* If not specified, the insertion should occur at the current view selection.
|
|
57
57
|
*/
|
|
58
|
-
selection
|
|
59
|
-
/**
|
|
60
|
-
* The range that view selection should be set to after insertion.
|
|
61
|
-
*/
|
|
62
|
-
resultRange?: ViewRange;
|
|
58
|
+
selection?: ViewSelection | ViewDocumentSelection;
|
|
63
59
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ckeditor/ckeditor5-typing",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "43.0.0-alpha.1",
|
|
4
4
|
"description": "Typing feature for CKEditor 5.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ckeditor",
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "src/index.js",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@ckeditor/ckeditor5-core": "
|
|
17
|
-
"@ckeditor/ckeditor5-engine": "
|
|
18
|
-
"@ckeditor/ckeditor5-utils": "
|
|
16
|
+
"@ckeditor/ckeditor5-core": "43.0.0-alpha.1",
|
|
17
|
+
"@ckeditor/ckeditor5-engine": "43.0.0-alpha.1",
|
|
18
|
+
"@ckeditor/ckeditor5-utils": "43.0.0-alpha.1",
|
|
19
19
|
"lodash-es": "4.17.21"
|
|
20
20
|
},
|
|
21
21
|
"author": "CKSource (http://cksource.com/)",
|
package/src/input.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ import { Plugin } from '@ckeditor/ckeditor5-core';
|
|
|
10
10
|
* Handles text input coming from the keyboard or other input methods.
|
|
11
11
|
*/
|
|
12
12
|
export default class Input extends Plugin {
|
|
13
|
+
/**
|
|
14
|
+
* The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
|
|
15
|
+
*/
|
|
16
|
+
private _compositionQueue;
|
|
13
17
|
/**
|
|
14
18
|
* @inheritDoc
|
|
15
19
|
*/
|
|
@@ -18,4 +22,8 @@ export default class Input extends Plugin {
|
|
|
18
22
|
* @inheritDoc
|
|
19
23
|
*/
|
|
20
24
|
init(): void;
|
|
25
|
+
/**
|
|
26
|
+
* @inheritDoc
|
|
27
|
+
*/
|
|
28
|
+
destroy(): void;
|
|
21
29
|
}
|
package/src/input.js
CHANGED
|
@@ -9,6 +9,8 @@ import { Plugin } from '@ckeditor/ckeditor5-core';
|
|
|
9
9
|
import { env } from '@ckeditor/ckeditor5-utils';
|
|
10
10
|
import InsertTextCommand from './inserttextcommand.js';
|
|
11
11
|
import InsertTextObserver from './inserttextobserver.js';
|
|
12
|
+
import { LiveRange } from '@ckeditor/ckeditor5-engine';
|
|
13
|
+
import { debounce } from 'lodash-es';
|
|
12
14
|
/**
|
|
13
15
|
* Handles text input coming from the keyboard or other input methods.
|
|
14
16
|
*/
|
|
@@ -26,7 +28,9 @@ export default class Input extends Plugin {
|
|
|
26
28
|
const editor = this.editor;
|
|
27
29
|
const model = editor.model;
|
|
28
30
|
const view = editor.editing.view;
|
|
31
|
+
const mapper = editor.editing.mapper;
|
|
29
32
|
const modelSelection = model.document.selection;
|
|
33
|
+
this._compositionQueue = new CompositionQueue(editor);
|
|
30
34
|
view.addObserver(InsertTextObserver);
|
|
31
35
|
// TODO The above default configuration value should be defined using editor.config.define() once it's fixed.
|
|
32
36
|
const insertTextCommand = new InsertTextCommand(editor, editor.config.get('typing.undoStep') || 20);
|
|
@@ -39,11 +43,20 @@ export default class Input extends Plugin {
|
|
|
39
43
|
if (!view.document.isComposing) {
|
|
40
44
|
data.preventDefault();
|
|
41
45
|
}
|
|
42
|
-
|
|
46
|
+
// Flush queue on the next beforeinput event because it could happen
|
|
47
|
+
// that the mutation observer does not notice the DOM change in time.
|
|
48
|
+
if (env.isAndroid && view.document.isComposing) {
|
|
49
|
+
this._compositionQueue.flush('next beforeinput');
|
|
50
|
+
}
|
|
51
|
+
const { text, selection: viewSelection } = data;
|
|
52
|
+
let modelRanges;
|
|
43
53
|
// If view selection was specified, translate it to model selection.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
54
|
+
if (viewSelection) {
|
|
55
|
+
modelRanges = Array.from(viewSelection.getRanges()).map(viewRange => mapper.toModelRange(viewRange));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
modelRanges = Array.from(modelSelection.getRanges());
|
|
59
|
+
}
|
|
47
60
|
let insertText = text;
|
|
48
61
|
// Typing in English on Android is firing composition events for the whole typed word.
|
|
49
62
|
// We need to check the target range text to only apply the difference.
|
|
@@ -66,24 +79,47 @@ export default class Input extends Plugin {
|
|
|
66
79
|
}
|
|
67
80
|
}
|
|
68
81
|
}
|
|
82
|
+
if (insertText.length == 0 && modelRanges[0].isCollapsed) {
|
|
83
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
84
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Ignore insertion of an empty data to the collapsed range.',
|
|
85
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-style: italic'
|
|
86
|
+
// @if CK_DEBUG_TYPING // );
|
|
87
|
+
// @if CK_DEBUG_TYPING // }
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
69
90
|
}
|
|
70
|
-
const
|
|
91
|
+
const commandData = {
|
|
71
92
|
text: insertText,
|
|
72
93
|
selection: model.createSelection(modelRanges)
|
|
73
94
|
};
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
// This is a composition event and those are not cancellable, so we need to wait until browser updates the DOM
|
|
96
|
+
// and we could apply changes to the model and verify if the DOM is valid.
|
|
97
|
+
// The browser applies changes to the DOM not immediately on beforeinput event.
|
|
98
|
+
// We just wait for mutation observer to notice changes or as a fallback a timeout.
|
|
99
|
+
if (env.isAndroid && view.document.isComposing) {
|
|
100
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
101
|
+
// @if CK_DEBUG_TYPING // console.log( `%c[Input]%c Queue insertText:%c "${ commandData.text }"%c ` +
|
|
102
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
|
|
103
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]` +
|
|
104
|
+
// @if CK_DEBUG_TYPING // ` queue size: ${ this._compositionQueue.length + 1 }`,
|
|
105
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', 'color: blue', ''
|
|
106
|
+
// @if CK_DEBUG_TYPING // );
|
|
107
|
+
// @if CK_DEBUG_TYPING // }
|
|
108
|
+
this._compositionQueue.push(commandData);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
112
|
+
// @if CK_DEBUG_TYPING // console.log( `%c[Input]%c Execute insertText:%c "${ commandData.text }"%c ` +
|
|
113
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
|
|
114
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]`,
|
|
115
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', 'color: blue', ''
|
|
116
|
+
// @if CK_DEBUG_TYPING // );
|
|
117
|
+
// @if CK_DEBUG_TYPING // }
|
|
118
|
+
editor.execute('insertText', commandData);
|
|
119
|
+
view.scrollToTheSelection();
|
|
83
120
|
}
|
|
84
|
-
editor.execute('insertText', insertTextCommandData);
|
|
85
|
-
view.scrollToTheSelection();
|
|
86
121
|
});
|
|
122
|
+
// Delete selected content on composition start.
|
|
87
123
|
if (env.isAndroid) {
|
|
88
124
|
// On Android with English keyboard, the composition starts just by putting caret
|
|
89
125
|
// at the word end or by selecting a table column. This is not a real composition started.
|
|
@@ -95,9 +131,9 @@ export default class Input extends Plugin {
|
|
|
95
131
|
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
96
132
|
// @if CK_DEBUG_TYPING // const firstPositionPath = modelSelection.getFirstPosition()!.path;
|
|
97
133
|
// @if CK_DEBUG_TYPING // const lastPositionPath = modelSelection.getLastPosition()!.path;
|
|
98
|
-
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c KeyDown 229 -> model.deleteContent()'
|
|
99
|
-
// @if CK_DEBUG_TYPING //
|
|
100
|
-
// @if CK_DEBUG_TYPING //
|
|
134
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c KeyDown 229%c -> model.deleteContent() ' +
|
|
135
|
+
// @if CK_DEBUG_TYPING // `[${ firstPositionPath }]-[${ lastPositionPath }]`,
|
|
136
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', '',
|
|
101
137
|
// @if CK_DEBUG_TYPING // );
|
|
102
138
|
// @if CK_DEBUG_TYPING // }
|
|
103
139
|
deleteSelectionContent(model, insertTextCommand);
|
|
@@ -113,16 +149,234 @@ export default class Input extends Plugin {
|
|
|
113
149
|
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
114
150
|
// @if CK_DEBUG_TYPING // const firstPositionPath = modelSelection.getFirstPosition()!.path;
|
|
115
151
|
// @if CK_DEBUG_TYPING // const lastPositionPath = modelSelection.getLastPosition()!.path;
|
|
116
|
-
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Composition start -> model.deleteContent()'
|
|
117
|
-
// @if CK_DEBUG_TYPING //
|
|
118
|
-
// @if CK_DEBUG_TYPING //
|
|
152
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Composition start%c -> model.deleteContent() ' +
|
|
153
|
+
// @if CK_DEBUG_TYPING // `[${ firstPositionPath }]-[${ lastPositionPath }]`,
|
|
154
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', '',
|
|
119
155
|
// @if CK_DEBUG_TYPING // );
|
|
120
156
|
// @if CK_DEBUG_TYPING // }
|
|
121
157
|
deleteSelectionContent(model, insertTextCommand);
|
|
122
158
|
});
|
|
123
159
|
}
|
|
160
|
+
// Apply composed changes to the model.
|
|
161
|
+
if (env.isAndroid) {
|
|
162
|
+
// Apply changes to the model as they are applied to the DOM by the browser.
|
|
163
|
+
// On beforeinput event, the DOM is not yet modified. We wait for detected mutations to apply model changes.
|
|
164
|
+
this.listenTo(view.document, 'mutations', (evt, { mutations }) => {
|
|
165
|
+
if (!view.document.isComposing) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Check if mutations are relevant for queued changes.
|
|
169
|
+
for (const { node } of mutations) {
|
|
170
|
+
const viewElement = findMappedViewAncestor(node, mapper);
|
|
171
|
+
const modelElement = mapper.toModelElement(viewElement);
|
|
172
|
+
if (this._compositionQueue.isComposedElement(modelElement)) {
|
|
173
|
+
this._compositionQueue.flush('mutations');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
178
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Mutations not related to the composition.',
|
|
179
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-style: italic'
|
|
180
|
+
// @if CK_DEBUG_TYPING // );
|
|
181
|
+
// @if CK_DEBUG_TYPING // }
|
|
182
|
+
});
|
|
183
|
+
// Make sure that all changes are applied to the model before the end of composition.
|
|
184
|
+
this.listenTo(view.document, 'compositionend', () => {
|
|
185
|
+
this._compositionQueue.flush('composition end');
|
|
186
|
+
});
|
|
187
|
+
// Trigger mutations check after the composition completes to fix all DOM changes that got ignored during composition.
|
|
188
|
+
// On Android the Renderer is not disabled while composing. While updating DOM nodes we ignore some changes
|
|
189
|
+
// that are not that important (like NBSP vs plain space character) and could break the composition flow.
|
|
190
|
+
// After composition is completed we trigger additional `mutations` event for elements affected by the composition
|
|
191
|
+
// so the Renderer can adjust the DOM to the expected structure without breaking the composition.
|
|
192
|
+
this.listenTo(view.document, 'compositionend', () => {
|
|
193
|
+
const mutations = [];
|
|
194
|
+
for (const element of this._compositionQueue.flushComposedElements()) {
|
|
195
|
+
const viewElement = mapper.toViewElement(element);
|
|
196
|
+
if (!viewElement) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
mutations.push({ type: 'children', node: viewElement });
|
|
200
|
+
}
|
|
201
|
+
if (mutations.length) {
|
|
202
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
203
|
+
// @if CK_DEBUG_TYPING // console.group( '%c[Input]%c Fire post-composition mutation fixes.',
|
|
204
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green', 'font-weight: bold', ''
|
|
205
|
+
// @if CK_DEBUG_TYPING // );
|
|
206
|
+
// @if CK_DEBUG_TYPING // }
|
|
207
|
+
view.document.fire('mutations', { mutations });
|
|
208
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
209
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
210
|
+
// @if CK_DEBUG_TYPING // }
|
|
211
|
+
}
|
|
212
|
+
}, { priority: 'lowest' });
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
// After composition end we need to verify if there are no left-overs.
|
|
216
|
+
// Listening at the lowest priority so after the `InsertTextObserver` added above (all composed text
|
|
217
|
+
// should already be applied to the model, view, and DOM).
|
|
218
|
+
// On non-Android the `Renderer` is blocked while user is composing but the `MutationObserver` still collects
|
|
219
|
+
// mutated nodes and fires `mutations` events.
|
|
220
|
+
// Those events are recorded by the `Renderer` but not applied to the DOM while composing.
|
|
221
|
+
// We need to trigger those checks (and fixes) once again but this time without specifying the exact mutations
|
|
222
|
+
// since they are already recorded by the `Renderer`.
|
|
223
|
+
// It in the most cases just clears the internal record of mutated text nodes
|
|
224
|
+
// since all changes should already be applied to the DOM.
|
|
225
|
+
// This is especially needed when user cancels composition, so we can clear nodes marked to sync.
|
|
226
|
+
this.listenTo(view.document, 'compositionend', () => {
|
|
227
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
228
|
+
// @if CK_DEBUG_TYPING // console.group( '%c[Input]%c Force render after composition end.',
|
|
229
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green', 'font-weight: bold', ''
|
|
230
|
+
// @if CK_DEBUG_TYPING // );
|
|
231
|
+
// @if CK_DEBUG_TYPING // }
|
|
232
|
+
view.document.fire('mutations', { mutations: [] });
|
|
233
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
234
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
235
|
+
// @if CK_DEBUG_TYPING // }
|
|
236
|
+
}, { priority: 'lowest' });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* @inheritDoc
|
|
241
|
+
*/
|
|
242
|
+
destroy() {
|
|
243
|
+
super.destroy();
|
|
244
|
+
this._compositionQueue.destroy();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
|
|
249
|
+
*/
|
|
250
|
+
class CompositionQueue {
|
|
251
|
+
/**
|
|
252
|
+
* @inheritDoc
|
|
253
|
+
*/
|
|
254
|
+
constructor(editor) {
|
|
255
|
+
/**
|
|
256
|
+
* Debounced queue flush as a safety mechanism for cases of mutation observer not triggering.
|
|
257
|
+
*/
|
|
258
|
+
this.flushDebounced = debounce(() => this.flush('timeout'), 50);
|
|
259
|
+
/**
|
|
260
|
+
* The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
|
|
261
|
+
*/
|
|
262
|
+
this._queue = [];
|
|
263
|
+
/**
|
|
264
|
+
* A set of model elements. The composition happened in those elements. It's used for mutations check.
|
|
265
|
+
*/
|
|
266
|
+
this._compositionElements = new Set();
|
|
267
|
+
this.editor = editor;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Destroys the helper object.
|
|
271
|
+
*/
|
|
272
|
+
destroy() {
|
|
273
|
+
this.flushDebounced.cancel();
|
|
274
|
+
this._compositionElements.clear();
|
|
275
|
+
while (this._queue.length) {
|
|
276
|
+
this.shift();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Returns the size of the queue.
|
|
281
|
+
*/
|
|
282
|
+
get length() {
|
|
283
|
+
return this._queue.length;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Push next insertText command data to the queue.
|
|
287
|
+
*/
|
|
288
|
+
push(commandData) {
|
|
289
|
+
const commandLiveData = {
|
|
290
|
+
text: commandData.text
|
|
291
|
+
};
|
|
292
|
+
if (commandData.selection) {
|
|
293
|
+
commandLiveData.selectionRanges = [];
|
|
294
|
+
for (const range of commandData.selection.getRanges()) {
|
|
295
|
+
commandLiveData.selectionRanges.push(LiveRange.fromRange(range));
|
|
296
|
+
// Keep reference to the model element for later mutation checks.
|
|
297
|
+
this._compositionElements.add(range.start.parent);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
this._queue.push(commandLiveData);
|
|
301
|
+
this.flushDebounced();
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Shift the first item from the insertText command data queue.
|
|
305
|
+
*/
|
|
306
|
+
shift() {
|
|
307
|
+
const commandLiveData = this._queue.shift();
|
|
308
|
+
const commandData = {
|
|
309
|
+
text: commandLiveData.text
|
|
310
|
+
};
|
|
311
|
+
if (commandLiveData.selectionRanges) {
|
|
312
|
+
const ranges = commandLiveData.selectionRanges
|
|
313
|
+
.map(liveRange => detachLiveRange(liveRange))
|
|
314
|
+
.filter((range) => !!range);
|
|
315
|
+
if (ranges.length) {
|
|
316
|
+
commandData.selection = this.editor.model.createSelection(ranges);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return commandData;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Applies all queued insertText command executions.
|
|
323
|
+
*
|
|
324
|
+
* @param reason Used only for debugging.
|
|
325
|
+
*/
|
|
326
|
+
flush(reason) {
|
|
327
|
+
const editor = this.editor;
|
|
328
|
+
const model = editor.model;
|
|
329
|
+
const view = editor.editing.view;
|
|
330
|
+
this.flushDebounced.cancel();
|
|
331
|
+
if (!this._queue.length) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
335
|
+
// @if CK_DEBUG_TYPING // console.group( `%c[Input]%c Flush insertText queue on ${ reason }.`,
|
|
336
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold'
|
|
337
|
+
// @if CK_DEBUG_TYPING // );
|
|
338
|
+
// @if CK_DEBUG_TYPING // }
|
|
339
|
+
const insertTextCommand = editor.commands.get('insertText');
|
|
340
|
+
const buffer = insertTextCommand.buffer;
|
|
341
|
+
model.enqueueChange(buffer.batch, () => {
|
|
342
|
+
buffer.lock();
|
|
343
|
+
while (this._queue.length) {
|
|
344
|
+
const commandData = this.shift();
|
|
345
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
346
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Execute queued insertText:%c ' +
|
|
347
|
+
// @if CK_DEBUG_TYPING // `"${ commandData.text }"%c ` +
|
|
348
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
|
|
349
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]`,
|
|
350
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', 'color: blue', ''
|
|
351
|
+
// @if CK_DEBUG_TYPING // );
|
|
352
|
+
// @if CK_DEBUG_TYPING // }
|
|
353
|
+
editor.execute('insertText', commandData);
|
|
354
|
+
}
|
|
355
|
+
buffer.unlock();
|
|
356
|
+
});
|
|
357
|
+
view.scrollToTheSelection();
|
|
358
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
359
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
360
|
+
// @if CK_DEBUG_TYPING // }
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Returns `true` if the given model element is related to recent composition.
|
|
364
|
+
*/
|
|
365
|
+
isComposedElement(element) {
|
|
366
|
+
return this._compositionElements.has(element);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Returns an array of composition-related elements and clears the internal list.
|
|
370
|
+
*/
|
|
371
|
+
flushComposedElements() {
|
|
372
|
+
const result = Array.from(this._compositionElements);
|
|
373
|
+
this._compositionElements.clear();
|
|
374
|
+
return result;
|
|
124
375
|
}
|
|
125
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Deletes the content selected by the document selection at the start of composition.
|
|
379
|
+
*/
|
|
126
380
|
function deleteSelectionContent(model, insertTextCommand) {
|
|
127
381
|
// By relying on the state of the input command we allow disabling the entire input easily
|
|
128
382
|
// by just disabling the input command. We could’ve used here the delete command but that
|
|
@@ -139,3 +393,24 @@ function deleteSelectionContent(model, insertTextCommand) {
|
|
|
139
393
|
});
|
|
140
394
|
buffer.unlock();
|
|
141
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Detaches a LiveRange and returns the static range from it.
|
|
398
|
+
*/
|
|
399
|
+
function detachLiveRange(liveRange) {
|
|
400
|
+
const range = liveRange.toRange();
|
|
401
|
+
liveRange.detach();
|
|
402
|
+
if (range.root.rootName == '$graveyard') {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
return range;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* For the given `viewNode`, finds and returns the closest ancestor of this node that has a mapping to the model.
|
|
409
|
+
*/
|
|
410
|
+
function findMappedViewAncestor(viewNode, mapper) {
|
|
411
|
+
let node = (viewNode.is('$text') ? viewNode.parent : viewNode);
|
|
412
|
+
while (!mapper.toModelElement(node)) {
|
|
413
|
+
node = node.parent;
|
|
414
|
+
}
|
|
415
|
+
return node;
|
|
416
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
3
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
4
|
*/
|
|
5
|
-
import { DomEventData, Observer, FocusObserver, type EditingView, type ViewDocumentSelection, type
|
|
5
|
+
import { DomEventData, Observer, FocusObserver, type EditingView, type ViewDocumentSelection, type ViewSelection } from '@ckeditor/ckeditor5-engine';
|
|
6
6
|
/**
|
|
7
7
|
* Text insertion observer introduces the {@link module:engine/view/document~Document#event:insertText} event.
|
|
8
8
|
*/
|
|
@@ -51,9 +51,5 @@ export interface InsertTextEventData extends DomEventData {
|
|
|
51
51
|
* The selection into which the text should be inserted.
|
|
52
52
|
* If not specified, the insertion should occur at the current view selection.
|
|
53
53
|
*/
|
|
54
|
-
selection
|
|
55
|
-
/**
|
|
56
|
-
* The range that view selection should be set to after insertion.
|
|
57
|
-
*/
|
|
58
|
-
resultRange?: ViewRange;
|
|
54
|
+
selection?: ViewSelection | ViewDocumentSelection;
|
|
59
55
|
}
|
|
@@ -19,6 +19,10 @@ const TYPING_INPUT_TYPES = [
|
|
|
19
19
|
// This one is used by Safari when accepting spell check suggestions from the autocorrection pop-up (Mac).
|
|
20
20
|
'insertReplacementText'
|
|
21
21
|
];
|
|
22
|
+
const TYPING_INPUT_TYPES_ANDROID = [
|
|
23
|
+
...TYPING_INPUT_TYPES,
|
|
24
|
+
'insertCompositionText'
|
|
25
|
+
];
|
|
22
26
|
/**
|
|
23
27
|
* Text insertion observer introduces the {@link module:engine/view/document~Document#event:insertText} event.
|
|
24
28
|
*/
|
|
@@ -32,16 +36,14 @@ export default class InsertTextObserver extends Observer {
|
|
|
32
36
|
// On Android composition events should immediately be applied to the model. Rendering is not disabled.
|
|
33
37
|
// On non-Android the model is updated only on composition end.
|
|
34
38
|
// On Android we can't rely on composition start/end to update model.
|
|
35
|
-
|
|
36
|
-
TYPING_INPUT_TYPES.push('insertCompositionText');
|
|
37
|
-
}
|
|
39
|
+
const typingInputTypes = env.isAndroid ? TYPING_INPUT_TYPES_ANDROID : TYPING_INPUT_TYPES;
|
|
38
40
|
const viewDocument = view.document;
|
|
39
41
|
viewDocument.on('beforeinput', (evt, data) => {
|
|
40
42
|
if (!this.isEnabled) {
|
|
41
43
|
return;
|
|
42
44
|
}
|
|
43
45
|
const { data: text, targetRanges, inputType, domEvent } = data;
|
|
44
|
-
if (!
|
|
46
|
+
if (!typingInputTypes.includes(inputType)) {
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
47
49
|
// Mark the latest focus change as complete (we are typing in editable after the focus
|
|
@@ -58,44 +60,36 @@ export default class InsertTextObserver extends Observer {
|
|
|
58
60
|
evt.stop();
|
|
59
61
|
}
|
|
60
62
|
});
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// - Try to follow it from the `beforeinput` events. This would be really complex as each
|
|
92
|
-
// `beforeinput` would come with just the range it's changing and we'd need to calculate that.
|
|
93
|
-
// We decided to go with the 2nd option for its simplicity and stability.
|
|
94
|
-
viewDocument.fire('insertText', new DomEventData(view, domEvent, {
|
|
95
|
-
text: data,
|
|
96
|
-
selection: viewDocument.selection
|
|
97
|
-
}));
|
|
98
|
-
}, { priority: 'lowest' });
|
|
63
|
+
// On Android composition events are immediately applied to the model.
|
|
64
|
+
// On non-Android the model is updated only on composition end.
|
|
65
|
+
// On Android we can't rely on composition start/end to update model.
|
|
66
|
+
if (!env.isAndroid) {
|
|
67
|
+
// Note: The priority must be lower than the CompositionObserver handler to call it after the renderer is unblocked.
|
|
68
|
+
// This is important for view to DOM position mapping.
|
|
69
|
+
// This causes the effect of first remove composed DOM and then reapply it after model modification.
|
|
70
|
+
viewDocument.on('compositionend', (evt, { data, domEvent }) => {
|
|
71
|
+
if (!this.isEnabled) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// In case of aborted composition.
|
|
75
|
+
if (!data) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
79
|
+
// @if CK_DEBUG_TYPING // console.log( `%c[InsertTextObserver]%c Fire insertText event, %c${ JSON.stringify( data ) }`,
|
|
80
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', 'color: blue'
|
|
81
|
+
// @if CK_DEBUG_TYPING // );
|
|
82
|
+
// @if CK_DEBUG_TYPING // }
|
|
83
|
+
// How do we know where to insert the composed text?
|
|
84
|
+
// 1. The SelectionObserver is blocked and the view is not updated with the composition changes.
|
|
85
|
+
// 2. The last moment before it's locked is the `compositionstart` event.
|
|
86
|
+
// 3. The `SelectionObserver` is listening for `compositionstart` event and immediately converts
|
|
87
|
+
// the selection. Handles this at the lowest priority so after the rendering is blocked.
|
|
88
|
+
viewDocument.fire('insertText', new DomEventData(view, domEvent, {
|
|
89
|
+
text: data
|
|
90
|
+
}));
|
|
91
|
+
}, { priority: 'lowest' });
|
|
92
|
+
}
|
|
99
93
|
}
|
|
100
94
|
/**
|
|
101
95
|
* @inheritDoc
|