@ckeditor/ckeditor5-typing 0.0.0-internal-20241017.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/CHANGELOG.md +4 -0
- package/LICENSE.md +21 -0
- package/README.md +22 -0
- package/ckeditor5-metadata.json +11 -0
- package/dist/augmentation.d.ts +31 -0
- package/dist/delete.d.ts +40 -0
- package/dist/deletecommand.d.ts +87 -0
- package/dist/deleteobserver.d.ts +59 -0
- package/dist/index-content.css +4 -0
- package/dist/index-editor.css +4 -0
- package/dist/index.css +4 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +2490 -0
- package/dist/index.js.map +1 -0
- package/dist/input.d.ts +37 -0
- package/dist/inserttextcommand.d.ts +80 -0
- package/dist/inserttextobserver.d.ts +59 -0
- package/dist/texttransformation.d.ts +41 -0
- package/dist/textwatcher.d.ts +142 -0
- package/dist/twostepcaretmovement.d.ts +240 -0
- package/dist/typing.d.ts +31 -0
- package/dist/typingconfig.d.ts +208 -0
- package/dist/utils/changebuffer.d.ts +107 -0
- package/dist/utils/findattributerange.d.ts +37 -0
- package/dist/utils/getlasttextline.d.ts +53 -0
- package/dist/utils/inlinehighlight.d.ts +37 -0
- package/package.json +40 -0
- package/src/augmentation.d.ts +27 -0
- package/src/augmentation.js +5 -0
- package/src/delete.d.ts +36 -0
- package/src/delete.js +88 -0
- package/src/deletecommand.d.ts +83 -0
- package/src/deletecommand.js +201 -0
- package/src/deleteobserver.d.ts +55 -0
- package/src/deleteobserver.js +260 -0
- package/src/index.d.ts +25 -0
- package/src/index.js +18 -0
- package/src/input.d.ts +33 -0
- package/src/input.js +422 -0
- package/src/inserttextcommand.d.ts +76 -0
- package/src/inserttextcommand.js +83 -0
- package/src/inserttextobserver.d.ts +55 -0
- package/src/inserttextobserver.js +102 -0
- package/src/texttransformation.d.ts +37 -0
- package/src/texttransformation.js +234 -0
- package/src/textwatcher.d.ts +138 -0
- package/src/textwatcher.js +105 -0
- package/src/twostepcaretmovement.d.ts +236 -0
- package/src/twostepcaretmovement.js +628 -0
- package/src/typing.d.ts +27 -0
- package/src/typing.js +33 -0
- package/src/typingconfig.d.ts +204 -0
- package/src/typingconfig.js +5 -0
- package/src/utils/changebuffer.d.ts +103 -0
- package/src/utils/changebuffer.js +123 -0
- package/src/utils/findattributerange.d.ts +33 -0
- package/src/utils/findattributerange.js +41 -0
- package/src/utils/getlasttextline.d.ts +49 -0
- package/src/utils/getlasttextline.js +43 -0
- package/src/utils/inlinehighlight.d.ts +33 -0
- package/src/utils/inlinehighlight.js +74 -0
package/src/input.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2024, 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
|
+
* @module typing/input
|
|
7
|
+
*/
|
|
8
|
+
import { Plugin } from '@ckeditor/ckeditor5-core';
|
|
9
|
+
import { env } from '@ckeditor/ckeditor5-utils';
|
|
10
|
+
import InsertTextCommand from './inserttextcommand.js';
|
|
11
|
+
import InsertTextObserver from './inserttextobserver.js';
|
|
12
|
+
import { LiveRange } from '@ckeditor/ckeditor5-engine';
|
|
13
|
+
import { debounce } from 'lodash-es';
|
|
14
|
+
/**
|
|
15
|
+
* Handles text input coming from the keyboard or other input methods.
|
|
16
|
+
*/
|
|
17
|
+
export default class Input extends Plugin {
|
|
18
|
+
/**
|
|
19
|
+
* @inheritDoc
|
|
20
|
+
*/
|
|
21
|
+
static get pluginName() {
|
|
22
|
+
return 'Input';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @inheritDoc
|
|
26
|
+
*/
|
|
27
|
+
static get isOfficialPlugin() {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* @inheritDoc
|
|
32
|
+
*/
|
|
33
|
+
init() {
|
|
34
|
+
const editor = this.editor;
|
|
35
|
+
const model = editor.model;
|
|
36
|
+
const view = editor.editing.view;
|
|
37
|
+
const mapper = editor.editing.mapper;
|
|
38
|
+
const modelSelection = model.document.selection;
|
|
39
|
+
this._compositionQueue = new CompositionQueue(editor);
|
|
40
|
+
view.addObserver(InsertTextObserver);
|
|
41
|
+
// TODO The above default configuration value should be defined using editor.config.define() once it's fixed.
|
|
42
|
+
const insertTextCommand = new InsertTextCommand(editor, editor.config.get('typing.undoStep') || 20);
|
|
43
|
+
// Register `insertText` command and add `input` command as an alias for backward compatibility.
|
|
44
|
+
editor.commands.add('insertText', insertTextCommand);
|
|
45
|
+
editor.commands.add('input', insertTextCommand);
|
|
46
|
+
this.listenTo(view.document, 'insertText', (evt, data) => {
|
|
47
|
+
// Rendering is disabled while composing so prevent events that will be rendered by the engine
|
|
48
|
+
// and should not be applied by the browser.
|
|
49
|
+
if (!view.document.isComposing) {
|
|
50
|
+
data.preventDefault();
|
|
51
|
+
}
|
|
52
|
+
// Flush queue on the next beforeinput event because it could happen
|
|
53
|
+
// that the mutation observer does not notice the DOM change in time.
|
|
54
|
+
if (env.isAndroid && view.document.isComposing) {
|
|
55
|
+
this._compositionQueue.flush('next beforeinput');
|
|
56
|
+
}
|
|
57
|
+
const { text, selection: viewSelection } = data;
|
|
58
|
+
let modelRanges;
|
|
59
|
+
// If view selection was specified, translate it to model selection.
|
|
60
|
+
if (viewSelection) {
|
|
61
|
+
modelRanges = Array.from(viewSelection.getRanges()).map(viewRange => mapper.toModelRange(viewRange));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
modelRanges = Array.from(modelSelection.getRanges());
|
|
65
|
+
}
|
|
66
|
+
let insertText = text;
|
|
67
|
+
// Typing in English on Android is firing composition events for the whole typed word.
|
|
68
|
+
// We need to check the target range text to only apply the difference.
|
|
69
|
+
if (env.isAndroid) {
|
|
70
|
+
const selectedText = Array.from(modelRanges[0].getItems()).reduce((rangeText, node) => {
|
|
71
|
+
return rangeText + (node.is('$textProxy') ? node.data : '');
|
|
72
|
+
}, '');
|
|
73
|
+
if (selectedText) {
|
|
74
|
+
if (selectedText.length <= insertText.length) {
|
|
75
|
+
if (insertText.startsWith(selectedText)) {
|
|
76
|
+
insertText = insertText.substring(selectedText.length);
|
|
77
|
+
modelRanges[0].start = modelRanges[0].start.getShiftedBy(selectedText.length);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
if (selectedText.startsWith(insertText)) {
|
|
82
|
+
// TODO this should be mapped as delete?
|
|
83
|
+
modelRanges[0].start = modelRanges[0].start.getShiftedBy(insertText.length);
|
|
84
|
+
insertText = '';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (insertText.length == 0 && modelRanges[0].isCollapsed) {
|
|
89
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
90
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Ignore insertion of an empty data to the collapsed range.',
|
|
91
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-style: italic'
|
|
92
|
+
// @if CK_DEBUG_TYPING // );
|
|
93
|
+
// @if CK_DEBUG_TYPING // }
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const commandData = {
|
|
98
|
+
text: insertText,
|
|
99
|
+
selection: model.createSelection(modelRanges)
|
|
100
|
+
};
|
|
101
|
+
// This is a composition event and those are not cancellable, so we need to wait until browser updates the DOM
|
|
102
|
+
// and we could apply changes to the model and verify if the DOM is valid.
|
|
103
|
+
// The browser applies changes to the DOM not immediately on beforeinput event.
|
|
104
|
+
// We just wait for mutation observer to notice changes or as a fallback a timeout.
|
|
105
|
+
if (env.isAndroid && view.document.isComposing) {
|
|
106
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
107
|
+
// @if CK_DEBUG_TYPING // console.log( `%c[Input]%c Queue insertText:%c "${ commandData.text }"%c ` +
|
|
108
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
|
|
109
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]` +
|
|
110
|
+
// @if CK_DEBUG_TYPING // ` queue size: ${ this._compositionQueue.length + 1 }`,
|
|
111
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', 'color: blue', ''
|
|
112
|
+
// @if CK_DEBUG_TYPING // );
|
|
113
|
+
// @if CK_DEBUG_TYPING // }
|
|
114
|
+
this._compositionQueue.push(commandData);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
118
|
+
// @if CK_DEBUG_TYPING // console.log( `%c[Input]%c Execute insertText:%c "${ commandData.text }"%c ` +
|
|
119
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
|
|
120
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]`,
|
|
121
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', 'color: blue', ''
|
|
122
|
+
// @if CK_DEBUG_TYPING // );
|
|
123
|
+
// @if CK_DEBUG_TYPING // }
|
|
124
|
+
editor.execute('insertText', commandData);
|
|
125
|
+
view.scrollToTheSelection();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// Delete selected content on composition start.
|
|
129
|
+
if (env.isAndroid) {
|
|
130
|
+
// On Android with English keyboard, the composition starts just by putting caret
|
|
131
|
+
// at the word end or by selecting a table column. This is not a real composition started.
|
|
132
|
+
// Trigger delete content on first composition key pressed.
|
|
133
|
+
this.listenTo(view.document, 'keydown', (evt, data) => {
|
|
134
|
+
if (modelSelection.isCollapsed || data.keyCode != 229 || !view.document.isComposing) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
138
|
+
// @if CK_DEBUG_TYPING // const firstPositionPath = modelSelection.getFirstPosition()!.path;
|
|
139
|
+
// @if CK_DEBUG_TYPING // const lastPositionPath = modelSelection.getLastPosition()!.path;
|
|
140
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c KeyDown 229%c -> model.deleteContent() ' +
|
|
141
|
+
// @if CK_DEBUG_TYPING // `[${ firstPositionPath }]-[${ lastPositionPath }]`,
|
|
142
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', '',
|
|
143
|
+
// @if CK_DEBUG_TYPING // );
|
|
144
|
+
// @if CK_DEBUG_TYPING // }
|
|
145
|
+
deleteSelectionContent(model, insertTextCommand);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Note: The priority must precede the CompositionObserver handler to call it before
|
|
150
|
+
// the renderer is blocked, because we want to render this change.
|
|
151
|
+
this.listenTo(view.document, 'compositionstart', () => {
|
|
152
|
+
if (modelSelection.isCollapsed) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
156
|
+
// @if CK_DEBUG_TYPING // const firstPositionPath = modelSelection.getFirstPosition()!.path;
|
|
157
|
+
// @if CK_DEBUG_TYPING // const lastPositionPath = modelSelection.getLastPosition()!.path;
|
|
158
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Composition start%c -> model.deleteContent() ' +
|
|
159
|
+
// @if CK_DEBUG_TYPING // `[${ firstPositionPath }]-[${ lastPositionPath }]`,
|
|
160
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', '',
|
|
161
|
+
// @if CK_DEBUG_TYPING // );
|
|
162
|
+
// @if CK_DEBUG_TYPING // }
|
|
163
|
+
deleteSelectionContent(model, insertTextCommand);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Apply composed changes to the model.
|
|
167
|
+
if (env.isAndroid) {
|
|
168
|
+
// Apply changes to the model as they are applied to the DOM by the browser.
|
|
169
|
+
// On beforeinput event, the DOM is not yet modified. We wait for detected mutations to apply model changes.
|
|
170
|
+
this.listenTo(view.document, 'mutations', (evt, { mutations }) => {
|
|
171
|
+
if (!view.document.isComposing) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Check if mutations are relevant for queued changes.
|
|
175
|
+
for (const { node } of mutations) {
|
|
176
|
+
const viewElement = findMappedViewAncestor(node, mapper);
|
|
177
|
+
const modelElement = mapper.toModelElement(viewElement);
|
|
178
|
+
if (this._compositionQueue.isComposedElement(modelElement)) {
|
|
179
|
+
this._compositionQueue.flush('mutations');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
184
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Mutations not related to the composition.',
|
|
185
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-style: italic'
|
|
186
|
+
// @if CK_DEBUG_TYPING // );
|
|
187
|
+
// @if CK_DEBUG_TYPING // }
|
|
188
|
+
});
|
|
189
|
+
// Make sure that all changes are applied to the model before the end of composition.
|
|
190
|
+
this.listenTo(view.document, 'compositionend', () => {
|
|
191
|
+
this._compositionQueue.flush('composition end');
|
|
192
|
+
});
|
|
193
|
+
// Trigger mutations check after the composition completes to fix all DOM changes that got ignored during composition.
|
|
194
|
+
// On Android the Renderer is not disabled while composing. While updating DOM nodes we ignore some changes
|
|
195
|
+
// that are not that important (like NBSP vs plain space character) and could break the composition flow.
|
|
196
|
+
// After composition is completed we trigger additional `mutations` event for elements affected by the composition
|
|
197
|
+
// so the Renderer can adjust the DOM to the expected structure without breaking the composition.
|
|
198
|
+
this.listenTo(view.document, 'compositionend', () => {
|
|
199
|
+
const mutations = [];
|
|
200
|
+
for (const element of this._compositionQueue.flushComposedElements()) {
|
|
201
|
+
const viewElement = mapper.toViewElement(element);
|
|
202
|
+
if (!viewElement) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
mutations.push({ type: 'children', node: viewElement });
|
|
206
|
+
}
|
|
207
|
+
if (mutations.length) {
|
|
208
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
209
|
+
// @if CK_DEBUG_TYPING // console.group( '%c[Input]%c Fire post-composition mutation fixes.',
|
|
210
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green', 'font-weight: bold', ''
|
|
211
|
+
// @if CK_DEBUG_TYPING // );
|
|
212
|
+
// @if CK_DEBUG_TYPING // }
|
|
213
|
+
view.document.fire('mutations', { mutations });
|
|
214
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
215
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
216
|
+
// @if CK_DEBUG_TYPING // }
|
|
217
|
+
}
|
|
218
|
+
}, { priority: 'lowest' });
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// After composition end we need to verify if there are no left-overs.
|
|
222
|
+
// Listening at the lowest priority so after the `InsertTextObserver` added above (all composed text
|
|
223
|
+
// should already be applied to the model, view, and DOM).
|
|
224
|
+
// On non-Android the `Renderer` is blocked while user is composing but the `MutationObserver` still collects
|
|
225
|
+
// mutated nodes and fires `mutations` events.
|
|
226
|
+
// Those events are recorded by the `Renderer` but not applied to the DOM while composing.
|
|
227
|
+
// We need to trigger those checks (and fixes) once again but this time without specifying the exact mutations
|
|
228
|
+
// since they are already recorded by the `Renderer`.
|
|
229
|
+
// It in the most cases just clears the internal record of mutated text nodes
|
|
230
|
+
// since all changes should already be applied to the DOM.
|
|
231
|
+
// This is especially needed when user cancels composition, so we can clear nodes marked to sync.
|
|
232
|
+
this.listenTo(view.document, 'compositionend', () => {
|
|
233
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
234
|
+
// @if CK_DEBUG_TYPING // console.group( '%c[Input]%c Force render after composition end.',
|
|
235
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green', 'font-weight: bold', ''
|
|
236
|
+
// @if CK_DEBUG_TYPING // );
|
|
237
|
+
// @if CK_DEBUG_TYPING // }
|
|
238
|
+
view.document.fire('mutations', { mutations: [] });
|
|
239
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
240
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
241
|
+
// @if CK_DEBUG_TYPING // }
|
|
242
|
+
}, { priority: 'lowest' });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* @inheritDoc
|
|
247
|
+
*/
|
|
248
|
+
destroy() {
|
|
249
|
+
super.destroy();
|
|
250
|
+
this._compositionQueue.destroy();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
|
|
255
|
+
*/
|
|
256
|
+
class CompositionQueue {
|
|
257
|
+
/**
|
|
258
|
+
* @inheritDoc
|
|
259
|
+
*/
|
|
260
|
+
constructor(editor) {
|
|
261
|
+
/**
|
|
262
|
+
* Debounced queue flush as a safety mechanism for cases of mutation observer not triggering.
|
|
263
|
+
*/
|
|
264
|
+
this.flushDebounced = debounce(() => this.flush('timeout'), 50);
|
|
265
|
+
/**
|
|
266
|
+
* The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
|
|
267
|
+
*/
|
|
268
|
+
this._queue = [];
|
|
269
|
+
/**
|
|
270
|
+
* A set of model elements. The composition happened in those elements. It's used for mutations check.
|
|
271
|
+
*/
|
|
272
|
+
this._compositionElements = new Set();
|
|
273
|
+
this.editor = editor;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Destroys the helper object.
|
|
277
|
+
*/
|
|
278
|
+
destroy() {
|
|
279
|
+
this.flushDebounced.cancel();
|
|
280
|
+
this._compositionElements.clear();
|
|
281
|
+
while (this._queue.length) {
|
|
282
|
+
this.shift();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Returns the size of the queue.
|
|
287
|
+
*/
|
|
288
|
+
get length() {
|
|
289
|
+
return this._queue.length;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Push next insertText command data to the queue.
|
|
293
|
+
*/
|
|
294
|
+
push(commandData) {
|
|
295
|
+
const commandLiveData = {
|
|
296
|
+
text: commandData.text
|
|
297
|
+
};
|
|
298
|
+
if (commandData.selection) {
|
|
299
|
+
commandLiveData.selectionRanges = [];
|
|
300
|
+
for (const range of commandData.selection.getRanges()) {
|
|
301
|
+
commandLiveData.selectionRanges.push(LiveRange.fromRange(range));
|
|
302
|
+
// Keep reference to the model element for later mutation checks.
|
|
303
|
+
this._compositionElements.add(range.start.parent);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
this._queue.push(commandLiveData);
|
|
307
|
+
this.flushDebounced();
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Shift the first item from the insertText command data queue.
|
|
311
|
+
*/
|
|
312
|
+
shift() {
|
|
313
|
+
const commandLiveData = this._queue.shift();
|
|
314
|
+
const commandData = {
|
|
315
|
+
text: commandLiveData.text
|
|
316
|
+
};
|
|
317
|
+
if (commandLiveData.selectionRanges) {
|
|
318
|
+
const ranges = commandLiveData.selectionRanges
|
|
319
|
+
.map(liveRange => detachLiveRange(liveRange))
|
|
320
|
+
.filter((range) => !!range);
|
|
321
|
+
if (ranges.length) {
|
|
322
|
+
commandData.selection = this.editor.model.createSelection(ranges);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return commandData;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Applies all queued insertText command executions.
|
|
329
|
+
*
|
|
330
|
+
* @param reason Used only for debugging.
|
|
331
|
+
*/
|
|
332
|
+
flush(reason) {
|
|
333
|
+
const editor = this.editor;
|
|
334
|
+
const model = editor.model;
|
|
335
|
+
const view = editor.editing.view;
|
|
336
|
+
this.flushDebounced.cancel();
|
|
337
|
+
if (!this._queue.length) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
341
|
+
// @if CK_DEBUG_TYPING // console.group( `%c[Input]%c Flush insertText queue on ${ reason }.`,
|
|
342
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold'
|
|
343
|
+
// @if CK_DEBUG_TYPING // );
|
|
344
|
+
// @if CK_DEBUG_TYPING // }
|
|
345
|
+
const insertTextCommand = editor.commands.get('insertText');
|
|
346
|
+
const buffer = insertTextCommand.buffer;
|
|
347
|
+
model.enqueueChange(buffer.batch, () => {
|
|
348
|
+
buffer.lock();
|
|
349
|
+
while (this._queue.length) {
|
|
350
|
+
const commandData = this.shift();
|
|
351
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
352
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Execute queued insertText:%c ' +
|
|
353
|
+
// @if CK_DEBUG_TYPING // `"${ commandData.text }"%c ` +
|
|
354
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
|
|
355
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]`,
|
|
356
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', 'color: blue', ''
|
|
357
|
+
// @if CK_DEBUG_TYPING // );
|
|
358
|
+
// @if CK_DEBUG_TYPING // }
|
|
359
|
+
editor.execute('insertText', commandData);
|
|
360
|
+
}
|
|
361
|
+
buffer.unlock();
|
|
362
|
+
});
|
|
363
|
+
view.scrollToTheSelection();
|
|
364
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
365
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
366
|
+
// @if CK_DEBUG_TYPING // }
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Returns `true` if the given model element is related to recent composition.
|
|
370
|
+
*/
|
|
371
|
+
isComposedElement(element) {
|
|
372
|
+
return this._compositionElements.has(element);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Returns an array of composition-related elements and clears the internal list.
|
|
376
|
+
*/
|
|
377
|
+
flushComposedElements() {
|
|
378
|
+
const result = Array.from(this._compositionElements);
|
|
379
|
+
this._compositionElements.clear();
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Deletes the content selected by the document selection at the start of composition.
|
|
385
|
+
*/
|
|
386
|
+
function deleteSelectionContent(model, insertTextCommand) {
|
|
387
|
+
// By relying on the state of the input command we allow disabling the entire input easily
|
|
388
|
+
// by just disabling the input command. We could’ve used here the delete command but that
|
|
389
|
+
// would mean requiring the delete feature which would block loading one without the other.
|
|
390
|
+
// We could also check the editor.isReadOnly property, but that wouldn't allow to block
|
|
391
|
+
// the input without blocking other features.
|
|
392
|
+
if (!insertTextCommand.isEnabled) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const buffer = insertTextCommand.buffer;
|
|
396
|
+
buffer.lock();
|
|
397
|
+
model.enqueueChange(buffer.batch, () => {
|
|
398
|
+
model.deleteContent(model.document.selection);
|
|
399
|
+
});
|
|
400
|
+
buffer.unlock();
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Detaches a LiveRange and returns the static range from it.
|
|
404
|
+
*/
|
|
405
|
+
function detachLiveRange(liveRange) {
|
|
406
|
+
const range = liveRange.toRange();
|
|
407
|
+
liveRange.detach();
|
|
408
|
+
if (range.root.rootName == '$graveyard') {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
return range;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* For the given `viewNode`, finds and returns the closest ancestor of this node that has a mapping to the model.
|
|
415
|
+
*/
|
|
416
|
+
function findMappedViewAncestor(viewNode, mapper) {
|
|
417
|
+
let node = (viewNode.is('$text') ? viewNode.parent : viewNode);
|
|
418
|
+
while (!mapper.toModelElement(node)) {
|
|
419
|
+
node = node.parent;
|
|
420
|
+
}
|
|
421
|
+
return node;
|
|
422
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2024, 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
|
+
* @module typing/inserttextcommand
|
|
7
|
+
*/
|
|
8
|
+
import { Command, type Editor } from '@ckeditor/ckeditor5-core';
|
|
9
|
+
import ChangeBuffer from './utils/changebuffer.js';
|
|
10
|
+
import type { DocumentSelection, Range, Selection } from '@ckeditor/ckeditor5-engine';
|
|
11
|
+
/**
|
|
12
|
+
* The insert text command. Used by the {@link module:typing/input~Input input feature} to handle typing.
|
|
13
|
+
*/
|
|
14
|
+
export default class InsertTextCommand extends Command {
|
|
15
|
+
/**
|
|
16
|
+
* Typing's change buffer used to group subsequent changes into batches.
|
|
17
|
+
*/
|
|
18
|
+
private readonly _buffer;
|
|
19
|
+
/**
|
|
20
|
+
* Creates an instance of the command.
|
|
21
|
+
*
|
|
22
|
+
* @param undoStepSize The maximum number of atomic changes
|
|
23
|
+
* which can be contained in one batch in the command buffer.
|
|
24
|
+
*/
|
|
25
|
+
constructor(editor: Editor, undoStepSize: number);
|
|
26
|
+
/**
|
|
27
|
+
* The current change buffer.
|
|
28
|
+
*/
|
|
29
|
+
get buffer(): ChangeBuffer;
|
|
30
|
+
/**
|
|
31
|
+
* @inheritDoc
|
|
32
|
+
*/
|
|
33
|
+
destroy(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Executes the input command. It replaces the content within the given range with the given text.
|
|
36
|
+
* Replacing is a two step process, first the content within the range is removed and then the new text is inserted
|
|
37
|
+
* at the beginning of the range (which after the removal is a collapsed range).
|
|
38
|
+
*
|
|
39
|
+
* @fires execute
|
|
40
|
+
* @param options The command options.
|
|
41
|
+
*/
|
|
42
|
+
execute(options?: InsertTextCommandOptions): void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Interface with parameters for executing InsertTextCommand.
|
|
46
|
+
*
|
|
47
|
+
* Both `range` and `selection` parameters are used for defining selection but should not be used together.
|
|
48
|
+
* If both are defined, only `selection` will be considered.
|
|
49
|
+
*/
|
|
50
|
+
export interface InsertTextCommandOptions {
|
|
51
|
+
/**
|
|
52
|
+
* The text to be inserted.
|
|
53
|
+
*/
|
|
54
|
+
text?: string;
|
|
55
|
+
/**
|
|
56
|
+
* The selection in which the text is inserted.
|
|
57
|
+
* Inserting a text into a selection deletes the current content within selection ranges. If the selection is not specified,
|
|
58
|
+
* the current selection in the model will be used instead.
|
|
59
|
+
*/
|
|
60
|
+
selection?: Selection | DocumentSelection;
|
|
61
|
+
/**
|
|
62
|
+
* The range in which the text is inserted. Defaults to the first range in the current selection.
|
|
63
|
+
*/
|
|
64
|
+
range?: Range;
|
|
65
|
+
/**
|
|
66
|
+
* The range where the selection should be placed after the insertion.
|
|
67
|
+
* If not specified, the selection will be placed right after the inserted text.
|
|
68
|
+
*/
|
|
69
|
+
resultRange?: Range;
|
|
70
|
+
}
|
|
71
|
+
export interface InsertTextCommandExecuteEvent {
|
|
72
|
+
name: 'execute';
|
|
73
|
+
args: [
|
|
74
|
+
data: [options: InsertTextCommandOptions]
|
|
75
|
+
];
|
|
76
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2024, 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
|
+
* @module typing/inserttextcommand
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from '@ckeditor/ckeditor5-core';
|
|
9
|
+
import ChangeBuffer from './utils/changebuffer.js';
|
|
10
|
+
/**
|
|
11
|
+
* The insert text command. Used by the {@link module:typing/input~Input input feature} to handle typing.
|
|
12
|
+
*/
|
|
13
|
+
export default class InsertTextCommand extends Command {
|
|
14
|
+
/**
|
|
15
|
+
* Creates an instance of the command.
|
|
16
|
+
*
|
|
17
|
+
* @param undoStepSize The maximum number of atomic changes
|
|
18
|
+
* which can be contained in one batch in the command buffer.
|
|
19
|
+
*/
|
|
20
|
+
constructor(editor, undoStepSize) {
|
|
21
|
+
super(editor);
|
|
22
|
+
this._buffer = new ChangeBuffer(editor.model, undoStepSize);
|
|
23
|
+
// Since this command may execute on different selectable than selection, it should be checked directly in execute block.
|
|
24
|
+
this._isEnabledBasedOnSelection = false;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The current change buffer.
|
|
28
|
+
*/
|
|
29
|
+
get buffer() {
|
|
30
|
+
return this._buffer;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @inheritDoc
|
|
34
|
+
*/
|
|
35
|
+
destroy() {
|
|
36
|
+
super.destroy();
|
|
37
|
+
this._buffer.destroy();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Executes the input command. It replaces the content within the given range with the given text.
|
|
41
|
+
* Replacing is a two step process, first the content within the range is removed and then the new text is inserted
|
|
42
|
+
* at the beginning of the range (which after the removal is a collapsed range).
|
|
43
|
+
*
|
|
44
|
+
* @fires execute
|
|
45
|
+
* @param options The command options.
|
|
46
|
+
*/
|
|
47
|
+
execute(options = {}) {
|
|
48
|
+
const model = this.editor.model;
|
|
49
|
+
const doc = model.document;
|
|
50
|
+
const text = options.text || '';
|
|
51
|
+
const textInsertions = text.length;
|
|
52
|
+
let selection = doc.selection;
|
|
53
|
+
if (options.selection) {
|
|
54
|
+
selection = options.selection;
|
|
55
|
+
}
|
|
56
|
+
else if (options.range) {
|
|
57
|
+
selection = model.createSelection(options.range);
|
|
58
|
+
}
|
|
59
|
+
// Stop executing if selectable is in non-editable place.
|
|
60
|
+
if (!model.canEditAt(selection)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const resultRange = options.resultRange;
|
|
64
|
+
model.enqueueChange(this._buffer.batch, writer => {
|
|
65
|
+
this._buffer.lock();
|
|
66
|
+
// Store selection attributes before deleting old content to preserve formatting and link.
|
|
67
|
+
// This unifies the behavior between DocumentSelection and Selection provided as input option.
|
|
68
|
+
const selectionAttributes = Array.from(doc.selection.getAttributes());
|
|
69
|
+
model.deleteContent(selection);
|
|
70
|
+
if (text) {
|
|
71
|
+
model.insertContent(writer.createText(text, selectionAttributes), selection);
|
|
72
|
+
}
|
|
73
|
+
if (resultRange) {
|
|
74
|
+
writer.setSelection(resultRange);
|
|
75
|
+
}
|
|
76
|
+
else if (!selection.is('documentSelection')) {
|
|
77
|
+
writer.setSelection(selection);
|
|
78
|
+
}
|
|
79
|
+
this._buffer.unlock();
|
|
80
|
+
this._buffer.input(textInsertions);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2024, 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
|
+
import { DomEventData, Observer, FocusObserver, type EditingView, type ViewDocumentSelection, type ViewSelection } from '@ckeditor/ckeditor5-engine';
|
|
6
|
+
/**
|
|
7
|
+
* Text insertion observer introduces the {@link module:engine/view/document~Document#event:insertText} event.
|
|
8
|
+
*/
|
|
9
|
+
export default class InsertTextObserver extends Observer {
|
|
10
|
+
/**
|
|
11
|
+
* Instance of the focus observer. Insert text observer calls
|
|
12
|
+
* {@link module:engine/view/observer/focusobserver~FocusObserver#flush} to mark the latest focus change as complete.
|
|
13
|
+
*/
|
|
14
|
+
readonly focusObserver: FocusObserver;
|
|
15
|
+
/**
|
|
16
|
+
* @inheritDoc
|
|
17
|
+
*/
|
|
18
|
+
constructor(view: EditingView);
|
|
19
|
+
/**
|
|
20
|
+
* @inheritDoc
|
|
21
|
+
*/
|
|
22
|
+
observe(): void;
|
|
23
|
+
/**
|
|
24
|
+
* @inheritDoc
|
|
25
|
+
*/
|
|
26
|
+
stopObserving(): void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Event fired when the user types text, for instance presses <kbd>A</kbd> or <kbd>?</kbd> in the
|
|
30
|
+
* editing view document.
|
|
31
|
+
*
|
|
32
|
+
* **Note**: This event will **not** fire for keystrokes such as <kbd>Delete</kbd> or <kbd>Enter</kbd>.
|
|
33
|
+
* They have dedicated events, see {@link module:engine/view/document~Document#event:delete} and
|
|
34
|
+
* {@link module:engine/view/document~Document#event:enter} to learn more.
|
|
35
|
+
*
|
|
36
|
+
* **Note**: This event is fired by the {@link module:typing/inserttextobserver~InsertTextObserver input feature}.
|
|
37
|
+
*
|
|
38
|
+
* @eventName module:engine/view/document~Document#insertText
|
|
39
|
+
* @param data The event data.
|
|
40
|
+
*/
|
|
41
|
+
export type ViewDocumentInsertTextEvent = {
|
|
42
|
+
name: 'insertText';
|
|
43
|
+
args: [data: InsertTextEventData];
|
|
44
|
+
};
|
|
45
|
+
export interface InsertTextEventData extends DomEventData {
|
|
46
|
+
/**
|
|
47
|
+
* The text to be inserted.
|
|
48
|
+
*/
|
|
49
|
+
text: string;
|
|
50
|
+
/**
|
|
51
|
+
* The selection into which the text should be inserted.
|
|
52
|
+
* If not specified, the insertion should occur at the current view selection.
|
|
53
|
+
*/
|
|
54
|
+
selection?: ViewSelection | ViewDocumentSelection;
|
|
55
|
+
}
|