@ckeditor/ckeditor5-typing 47.6.1-alpha.1 → 48.0.0-alpha.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/LICENSE.md +1 -1
- package/ckeditor5-metadata.json +1 -1
- package/dist/index.css +3 -0
- package/dist/index.css.map +1 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/{src → dist}/texttransformation.d.ts +3 -1
- package/package.json +22 -41
- package/src/augmentation.js +0 -5
- package/src/delete.js +0 -122
- package/src/deletecommand.js +0 -212
- package/src/deleteobserver.js +0 -262
- package/src/index.js +0 -21
- package/src/input.js +0 -486
- package/src/inserttextcommand.js +0 -87
- package/src/inserttextobserver.js +0 -112
- package/src/texttransformation.js +0 -237
- package/src/textwatcher.js +0 -123
- package/src/twostepcaretmovement.js +0 -661
- package/src/typing.js +0 -33
- package/src/typingconfig.js +0 -5
- package/src/utils/changebuffer.js +0 -148
- package/src/utils/findattributerange.js +0 -41
- package/src/utils/getlasttextline.js +0 -43
- package/src/utils/inlinehighlight.js +0 -74
- /package/{src → dist}/augmentation.d.ts +0 -0
- /package/{src → dist}/delete.d.ts +0 -0
- /package/{src → dist}/deletecommand.d.ts +0 -0
- /package/{src → dist}/deleteobserver.d.ts +0 -0
- /package/{src → dist}/index.d.ts +0 -0
- /package/{src → dist}/input.d.ts +0 -0
- /package/{src → dist}/inserttextcommand.d.ts +0 -0
- /package/{src → dist}/inserttextobserver.d.ts +0 -0
- /package/{src → dist}/textwatcher.d.ts +0 -0
- /package/{src → dist}/twostepcaretmovement.d.ts +0 -0
- /package/{src → dist}/typing.d.ts +0 -0
- /package/{src → dist}/typingconfig.d.ts +0 -0
- /package/{src → dist}/utils/changebuffer.d.ts +0 -0
- /package/{src → dist}/utils/findattributerange.d.ts +0 -0
- /package/{src → dist}/utils/getlasttextline.d.ts +0 -0
- /package/{src → dist}/utils/inlinehighlight.d.ts +0 -0
package/src/input.js
DELETED
|
@@ -1,486 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
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 { ModelLiveRange, _tryFixingModelRange } from '@ckeditor/ckeditor5-engine';
|
|
13
|
-
import { debounce } from 'es-toolkit/compat';
|
|
14
|
-
// @if CK_DEBUG_TYPING // const { _debouncedLine, _buildLogMessage } = require( '@ckeditor/ckeditor5-engine/src/dev-utils/utils.js' );
|
|
15
|
-
/**
|
|
16
|
-
* Handles text input coming from the keyboard or other input methods.
|
|
17
|
-
*/
|
|
18
|
-
export class Input extends Plugin {
|
|
19
|
-
/**
|
|
20
|
-
* The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
|
|
21
|
-
*/
|
|
22
|
-
_typingQueue;
|
|
23
|
-
/**
|
|
24
|
-
* @inheritDoc
|
|
25
|
-
*/
|
|
26
|
-
static get pluginName() {
|
|
27
|
-
return 'Input';
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* @inheritDoc
|
|
31
|
-
*/
|
|
32
|
-
static get isOfficialPlugin() {
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* @inheritDoc
|
|
37
|
-
*/
|
|
38
|
-
init() {
|
|
39
|
-
const editor = this.editor;
|
|
40
|
-
const model = editor.model;
|
|
41
|
-
const view = editor.editing.view;
|
|
42
|
-
const mapper = editor.editing.mapper;
|
|
43
|
-
const modelSelection = model.document.selection;
|
|
44
|
-
this._typingQueue = new TypingQueue(editor);
|
|
45
|
-
view.addObserver(InsertTextObserver);
|
|
46
|
-
// TODO The above default configuration value should be defined using editor.config.define() once it's fixed.
|
|
47
|
-
const insertTextCommand = new InsertTextCommand(editor, editor.config.get('typing.undoStep') || 20);
|
|
48
|
-
// Register `insertText` command and add `input` command as an alias for backward compatibility.
|
|
49
|
-
editor.commands.add('insertText', insertTextCommand);
|
|
50
|
-
editor.commands.add('input', insertTextCommand);
|
|
51
|
-
this.listenTo(view.document, 'beforeinput', () => {
|
|
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
|
-
this._typingQueue.flush('next beforeinput');
|
|
55
|
-
}, { priority: 'high' });
|
|
56
|
-
this.listenTo(view.document, 'insertText', (evt, data) => {
|
|
57
|
-
const { text, selection: viewSelection } = data;
|
|
58
|
-
// In case of a synthetic event, make sure that selection is not fake.
|
|
59
|
-
if (view.document.selection.isFake && viewSelection && view.document.selection.isSimilar(viewSelection)) {
|
|
60
|
-
data.preventDefault();
|
|
61
|
-
}
|
|
62
|
-
// In case of typing on a non-collapsed range, we have to handle it ourselves as a browser
|
|
63
|
-
// could modify the DOM unpredictably.
|
|
64
|
-
// Noticed cases:
|
|
65
|
-
// * <pre><code>[foo</code></pre><p>]bar</p>
|
|
66
|
-
// * <p>[foo</p><pre>]<code>bar</code></pre>
|
|
67
|
-
// * <p>[foo</p><blockquote><p>]bar</p></blockquote>
|
|
68
|
-
//
|
|
69
|
-
// Especially tricky case is when a code block follows a paragraph as code block on the view side
|
|
70
|
-
// is rendered as a <code> element inside a <pre> element, but only the <code> element is mapped to the model.
|
|
71
|
-
// While mapping view position <pre>]<code> to model, the model position results before the <codeBlock> element,
|
|
72
|
-
// and this triggers selection fixer to cover only text in the previous paragraph.
|
|
73
|
-
//
|
|
74
|
-
// This is safe for composition as those events are not cancellable
|
|
75
|
-
// and the preventDefault() and defaultPrevented are not affected.
|
|
76
|
-
if (viewSelection && Array.from(viewSelection.getRanges()).some(range => !range.isCollapsed)) {
|
|
77
|
-
data.preventDefault();
|
|
78
|
-
}
|
|
79
|
-
if (!insertTextCommand.isEnabled) {
|
|
80
|
-
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
81
|
-
// @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
|
|
82
|
-
// @if CK_DEBUG_TYPING // '%cInsertText command is disabled - prevent DOM change.',
|
|
83
|
-
// @if CK_DEBUG_TYPING // 'font-style: italic'
|
|
84
|
-
// @if CK_DEBUG_TYPING // ) );
|
|
85
|
-
// @if CK_DEBUG_TYPING // }
|
|
86
|
-
data.preventDefault();
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
let modelRanges;
|
|
90
|
-
// If view selection was specified, translate it to model selection.
|
|
91
|
-
if (viewSelection) {
|
|
92
|
-
modelRanges = Array.from(viewSelection.getRanges())
|
|
93
|
-
.filter(viewRange => {
|
|
94
|
-
// On Windows 11 with the US International keyboard, events are batched.
|
|
95
|
-
// In other words, the first backtick press does not send an `insertText` event,
|
|
96
|
-
// and only the second one sends it double (batched).
|
|
97
|
-
// This causes a race condition if during the first backtick insert the element is removed from the tree,
|
|
98
|
-
// and the second event flushes changes, and it's original targetRanges,
|
|
99
|
-
// which were initially good, now refer to a removed element.
|
|
100
|
-
// This is not reproducible on Mac/Linux as they enter composition mode.
|
|
101
|
-
// See more: https://github.com/ckeditor/ckeditor5/issues/18926.
|
|
102
|
-
return viewRange.root.is('rootElement');
|
|
103
|
-
})
|
|
104
|
-
.map(viewRange => mapper.toModelRange(viewRange))
|
|
105
|
-
.map(modelRange => _tryFixingModelRange(modelRange, model.schema) || modelRange);
|
|
106
|
-
}
|
|
107
|
-
if (!modelRanges || !modelRanges.length) {
|
|
108
|
-
modelRanges = Array.from(modelSelection.getRanges());
|
|
109
|
-
}
|
|
110
|
-
let insertText = text;
|
|
111
|
-
// Typing in English on Android is firing composition events for the whole typed word.
|
|
112
|
-
// We need to check the target range text to only apply the difference.
|
|
113
|
-
if (env.isAndroid) {
|
|
114
|
-
const selectedText = Array.from(modelRanges[0].getItems()).reduce((rangeText, node) => {
|
|
115
|
-
return rangeText + (node.is('$textProxy') ? node.data : '');
|
|
116
|
-
}, '');
|
|
117
|
-
if (selectedText) {
|
|
118
|
-
if (selectedText.length <= insertText.length) {
|
|
119
|
-
if (insertText.startsWith(selectedText)) {
|
|
120
|
-
insertText = insertText.substring(selectedText.length);
|
|
121
|
-
modelRanges[0].start = modelRanges[0].start.getShiftedBy(selectedText.length);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
if (selectedText.startsWith(insertText)) {
|
|
126
|
-
// TODO this should be mapped as delete?
|
|
127
|
-
modelRanges[0].start = modelRanges[0].start.getShiftedBy(insertText.length);
|
|
128
|
-
insertText = '';
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (insertText.length == 0 && modelRanges[0].isCollapsed) {
|
|
133
|
-
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
134
|
-
// @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
|
|
135
|
-
// @if CK_DEBUG_TYPING // '%cIgnore insertion of an empty data to the collapsed range.',
|
|
136
|
-
// @if CK_DEBUG_TYPING // 'font-style: italic'
|
|
137
|
-
// @if CK_DEBUG_TYPING // ) );
|
|
138
|
-
// @if CK_DEBUG_TYPING // }
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// Note: the TypingQueue stores live-ranges internally as RTC could change the model while waiting for mutations.
|
|
143
|
-
const commandData = {
|
|
144
|
-
text: insertText,
|
|
145
|
-
selection: model.createSelection(modelRanges)
|
|
146
|
-
};
|
|
147
|
-
// This is a beforeinput event, so we need to wait until the browser updates the DOM,
|
|
148
|
-
// and we could apply changes to the model and verify if the DOM is valid.
|
|
149
|
-
// The browser applies changes to the DOM not immediately on beforeinput event.
|
|
150
|
-
// We just wait for mutation observer to notice changes or as a fallback a timeout.
|
|
151
|
-
//
|
|
152
|
-
// Previously we were cancelling the non-composition events, but it caused issues especially in Safari.
|
|
153
|
-
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
154
|
-
// @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
|
|
155
|
-
// @if CK_DEBUG_TYPING // `%cQueue insertText:%c "${ commandData.text }"%c ` +
|
|
156
|
-
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
|
|
157
|
-
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]` +
|
|
158
|
-
// @if CK_DEBUG_TYPING // ` queue size: ${ this._typingQueue.length + 1 }`,
|
|
159
|
-
// @if CK_DEBUG_TYPING // 'font-weight: bold',
|
|
160
|
-
// @if CK_DEBUG_TYPING // 'color: blue',
|
|
161
|
-
// @if CK_DEBUG_TYPING // ''
|
|
162
|
-
// @if CK_DEBUG_TYPING // ) );
|
|
163
|
-
// @if CK_DEBUG_TYPING // }
|
|
164
|
-
this._typingQueue.push(commandData, Boolean(data.isComposing));
|
|
165
|
-
if (data.domEvent.defaultPrevented) {
|
|
166
|
-
this._typingQueue.flush('beforeinput default prevented');
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
// Delete selected content on composition start.
|
|
170
|
-
if (env.isAndroid) {
|
|
171
|
-
// On Android with English keyboard, the composition starts just by putting caret
|
|
172
|
-
// at the word end or by selecting a table column. This is not a real composition started.
|
|
173
|
-
// Trigger delete content on first composition key pressed.
|
|
174
|
-
this.listenTo(view.document, 'keydown', (evt, data) => {
|
|
175
|
-
if (modelSelection.isCollapsed || data.keyCode != 229 || !view.document.isComposing) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
179
|
-
// @if CK_DEBUG_TYPING // const firstPositionPath = modelSelection.getFirstPosition()!.path;
|
|
180
|
-
// @if CK_DEBUG_TYPING // const lastPositionPath = modelSelection.getLastPosition()!.path;
|
|
181
|
-
// @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
|
|
182
|
-
// @if CK_DEBUG_TYPING // '%cKeyDown 229%c -> model.deleteContent() ' +
|
|
183
|
-
// @if CK_DEBUG_TYPING // `[${ firstPositionPath }]-[${ lastPositionPath }]`,
|
|
184
|
-
// @if CK_DEBUG_TYPING // 'font-weight: bold',
|
|
185
|
-
// @if CK_DEBUG_TYPING // ''
|
|
186
|
-
// @if CK_DEBUG_TYPING // ) );
|
|
187
|
-
// @if CK_DEBUG_TYPING // }
|
|
188
|
-
deleteSelectionContent(model, insertTextCommand);
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
// Note: The priority must precede the CompositionObserver handler to call it before
|
|
193
|
-
// the renderer is blocked, because we want to render this change.
|
|
194
|
-
this.listenTo(view.document, 'compositionstart', () => {
|
|
195
|
-
if (modelSelection.isCollapsed) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
199
|
-
// @if CK_DEBUG_TYPING // const firstPositionPath = modelSelection.getFirstPosition()!.path;
|
|
200
|
-
// @if CK_DEBUG_TYPING // const lastPositionPath = modelSelection.getLastPosition()!.path;
|
|
201
|
-
// @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
|
|
202
|
-
// @if CK_DEBUG_TYPING // '%cComposition start%c -> model.deleteContent() ' +
|
|
203
|
-
// @if CK_DEBUG_TYPING // `[${ firstPositionPath }]-[${ lastPositionPath }]`,
|
|
204
|
-
// @if CK_DEBUG_TYPING // 'font-weight: bold',
|
|
205
|
-
// @if CK_DEBUG_TYPING // '',
|
|
206
|
-
// @if CK_DEBUG_TYPING // ) );
|
|
207
|
-
// @if CK_DEBUG_TYPING // }
|
|
208
|
-
deleteSelectionContent(model, insertTextCommand);
|
|
209
|
-
}, { priority: 'high' });
|
|
210
|
-
}
|
|
211
|
-
// Apply changes to the model as they are applied to the DOM by the browser.
|
|
212
|
-
// On beforeinput event, the DOM is not yet modified. We wait for detected mutations to apply model changes.
|
|
213
|
-
this.listenTo(view.document, 'mutations', (evt, { mutations }) => {
|
|
214
|
-
// Check if mutations are relevant for queued changes.
|
|
215
|
-
if (this._typingQueue.hasAffectedElements()) {
|
|
216
|
-
for (const { node } of mutations) {
|
|
217
|
-
const viewElement = findMappedViewAncestor(node, mapper);
|
|
218
|
-
const modelElement = mapper.toModelElement(viewElement);
|
|
219
|
-
if (this._typingQueue.isElementAffected(modelElement)) {
|
|
220
|
-
this._typingQueue.flush('mutations');
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
226
|
-
// @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
|
|
227
|
-
// @if CK_DEBUG_TYPING // '%cMutations not related to the composition.',
|
|
228
|
-
// @if CK_DEBUG_TYPING // 'font-style: italic'
|
|
229
|
-
// @if CK_DEBUG_TYPING // ) );
|
|
230
|
-
// @if CK_DEBUG_TYPING // }
|
|
231
|
-
});
|
|
232
|
-
// Make sure that all changes are applied to the model before the end of composition.
|
|
233
|
-
this.listenTo(view.document, 'compositionend', () => {
|
|
234
|
-
this._typingQueue.flush('before composition end');
|
|
235
|
-
}, { priority: 'high' });
|
|
236
|
-
// Trigger mutations check after the composition completes to fix all DOM changes that got ignored during composition.
|
|
237
|
-
// On Android, the Renderer is not disabled while composing. While updating DOM nodes, we ignore some changes
|
|
238
|
-
// that are not that important (like NBSP vs. plain space character) and could break the composition flow.
|
|
239
|
-
// After composition is completed, we trigger additional `mutations` event for elements affected by the composition
|
|
240
|
-
// so the Renderer can adjust the DOM to the expected structure without breaking the composition.
|
|
241
|
-
this.listenTo(view.document, 'compositionend', () => {
|
|
242
|
-
// There could be new item queued on the composition end, so flush it.
|
|
243
|
-
this._typingQueue.flush('after composition end');
|
|
244
|
-
const mutations = [];
|
|
245
|
-
if (this._typingQueue.hasAffectedElements()) {
|
|
246
|
-
for (const element of this._typingQueue.flushAffectedElements()) {
|
|
247
|
-
const viewElement = mapper.toViewElement(element);
|
|
248
|
-
if (!viewElement) {
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
mutations.push({ type: 'children', node: viewElement });
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
// Fire composition mutations, if any.
|
|
255
|
-
//
|
|
256
|
-
// For non-Android:
|
|
257
|
-
// After the composition end, we need to verify if there are no left-overs.
|
|
258
|
-
// Listening at the lowest priority, so after the `InsertTextObserver` added above (all composed text
|
|
259
|
-
// should already be applied to the model, view, and DOM).
|
|
260
|
-
// On non-Android the `Renderer` is blocked while the user is composing, but the `MutationObserver` still collects
|
|
261
|
-
// mutated nodes and fires `mutations` events.
|
|
262
|
-
// Those events are recorded by the `Renderer` but not applied to the DOM while composing.
|
|
263
|
-
// We need to trigger those checks (and fixes) once again but this time without specifying the exact mutations
|
|
264
|
-
// since they are already recorded by the `Renderer`.
|
|
265
|
-
// It in most cases just clears the internal record of mutated text nodes
|
|
266
|
-
// since all changes should already be applied to the DOM.
|
|
267
|
-
// This is especially needed when a user cancels composition, so we can clear nodes marked to sync.
|
|
268
|
-
if (mutations.length || !env.isAndroid) {
|
|
269
|
-
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
270
|
-
// @if CK_DEBUG_TYPING // console.group( ..._buildLogMessage( this, 'Input',
|
|
271
|
-
// @if CK_DEBUG_TYPING // '%cFire post-composition mutation fixes.',
|
|
272
|
-
// @if CK_DEBUG_TYPING // 'font-weight: bold'
|
|
273
|
-
// @if CK_DEBUG_TYPING // ) );
|
|
274
|
-
// @if CK_DEBUG_TYPING // }
|
|
275
|
-
view.document.fire('mutations', { mutations });
|
|
276
|
-
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
277
|
-
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
278
|
-
// @if CK_DEBUG_TYPING // }
|
|
279
|
-
}
|
|
280
|
-
}, { priority: 'lowest' });
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* @inheritDoc
|
|
284
|
-
*/
|
|
285
|
-
destroy() {
|
|
286
|
-
super.destroy();
|
|
287
|
-
this._typingQueue.destroy();
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
|
|
292
|
-
*/
|
|
293
|
-
class TypingQueue {
|
|
294
|
-
/**
|
|
295
|
-
* The editor instance.
|
|
296
|
-
*/
|
|
297
|
-
editor;
|
|
298
|
-
/**
|
|
299
|
-
* Debounced queue flush as a safety mechanism for cases of mutation observer not triggering.
|
|
300
|
-
*/
|
|
301
|
-
flushDebounced = debounce(() => this.flush('timeout'), 50);
|
|
302
|
-
/**
|
|
303
|
-
* The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
|
|
304
|
-
*/
|
|
305
|
-
_queue = [];
|
|
306
|
-
/**
|
|
307
|
-
* Whether there is any composition enqueued or plain typing only.
|
|
308
|
-
*/
|
|
309
|
-
_isComposing = false;
|
|
310
|
-
/**
|
|
311
|
-
* A set of model elements. The typing happened in those elements. It's used for mutations check.
|
|
312
|
-
*/
|
|
313
|
-
_affectedElements = new Set();
|
|
314
|
-
/**
|
|
315
|
-
* @inheritDoc
|
|
316
|
-
*/
|
|
317
|
-
constructor(editor) {
|
|
318
|
-
this.editor = editor;
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Destroys the helper object.
|
|
322
|
-
*/
|
|
323
|
-
destroy() {
|
|
324
|
-
this.flushDebounced.cancel();
|
|
325
|
-
this._affectedElements.clear();
|
|
326
|
-
while (this._queue.length) {
|
|
327
|
-
this.shift();
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Returns the size of the queue.
|
|
332
|
-
*/
|
|
333
|
-
get length() {
|
|
334
|
-
return this._queue.length;
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Push next insertText command data to the queue.
|
|
338
|
-
*/
|
|
339
|
-
push(commandData, isComposing) {
|
|
340
|
-
const commandLiveData = {
|
|
341
|
-
text: commandData.text
|
|
342
|
-
};
|
|
343
|
-
if (commandData.selection) {
|
|
344
|
-
commandLiveData.selectionRanges = [];
|
|
345
|
-
for (const range of commandData.selection.getRanges()) {
|
|
346
|
-
commandLiveData.selectionRanges.push(ModelLiveRange.fromRange(range));
|
|
347
|
-
// Keep reference to the model element for later mutation checks.
|
|
348
|
-
this._affectedElements.add(range.start.parent);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
this._queue.push(commandLiveData);
|
|
352
|
-
this._isComposing ||= isComposing;
|
|
353
|
-
this.flushDebounced();
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Shift the first item from the insertText command data queue.
|
|
357
|
-
*/
|
|
358
|
-
shift() {
|
|
359
|
-
const commandLiveData = this._queue.shift();
|
|
360
|
-
const commandData = {
|
|
361
|
-
text: commandLiveData.text
|
|
362
|
-
};
|
|
363
|
-
if (commandLiveData.selectionRanges) {
|
|
364
|
-
const ranges = commandLiveData.selectionRanges
|
|
365
|
-
.map(liveRange => detachLiveRange(liveRange))
|
|
366
|
-
.filter((range) => !!range);
|
|
367
|
-
if (ranges.length) {
|
|
368
|
-
commandData.selection = this.editor.model.createSelection(ranges);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return commandData;
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Applies all queued insertText command executions.
|
|
375
|
-
*
|
|
376
|
-
* @param reason Used only for debugging.
|
|
377
|
-
*/
|
|
378
|
-
flush(reason) {
|
|
379
|
-
const editor = this.editor;
|
|
380
|
-
const model = editor.model;
|
|
381
|
-
const view = editor.editing.view;
|
|
382
|
-
this.flushDebounced.cancel();
|
|
383
|
-
if (!this._queue.length) {
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
387
|
-
// @if CK_DEBUG_TYPING // console.group( ..._buildLogMessage( this, 'Input',
|
|
388
|
-
// @if CK_DEBUG_TYPING // `%cFlush insertText queue on ${ reason }.`,
|
|
389
|
-
// @if CK_DEBUG_TYPING // 'font-weight: bold'
|
|
390
|
-
// @if CK_DEBUG_TYPING // ) );
|
|
391
|
-
// @if CK_DEBUG_TYPING // }
|
|
392
|
-
const insertTextCommand = editor.commands.get('insertText');
|
|
393
|
-
const buffer = insertTextCommand.buffer;
|
|
394
|
-
model.enqueueChange(buffer.batch, () => {
|
|
395
|
-
buffer.lock();
|
|
396
|
-
while (this._queue.length) {
|
|
397
|
-
const commandData = this.shift();
|
|
398
|
-
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
399
|
-
// @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
|
|
400
|
-
// @if CK_DEBUG_TYPING // `%cExecute queued insertText:%c "${ commandData.text }"%c ` +
|
|
401
|
-
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
|
|
402
|
-
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]`,
|
|
403
|
-
// @if CK_DEBUG_TYPING // 'font-weight: bold',
|
|
404
|
-
// @if CK_DEBUG_TYPING // 'color: blue',
|
|
405
|
-
// @if CK_DEBUG_TYPING // ''
|
|
406
|
-
// @if CK_DEBUG_TYPING // ) );
|
|
407
|
-
// @if CK_DEBUG_TYPING // }
|
|
408
|
-
editor.execute('insertText', commandData);
|
|
409
|
-
}
|
|
410
|
-
buffer.unlock();
|
|
411
|
-
if (!this._isComposing) {
|
|
412
|
-
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
413
|
-
// @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
|
|
414
|
-
// @if CK_DEBUG_TYPING // 'Clear affected elements set'
|
|
415
|
-
// @if CK_DEBUG_TYPING // ) );
|
|
416
|
-
// @if CK_DEBUG_TYPING // }
|
|
417
|
-
this._affectedElements.clear();
|
|
418
|
-
}
|
|
419
|
-
this._isComposing = false;
|
|
420
|
-
});
|
|
421
|
-
view.scrollToTheSelection();
|
|
422
|
-
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
423
|
-
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
424
|
-
// @if CK_DEBUG_TYPING // }
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* Returns `true` if the given model element is related to recent typing.
|
|
428
|
-
*/
|
|
429
|
-
isElementAffected(element) {
|
|
430
|
-
return this._affectedElements.has(element);
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Returns `true` if there are any affected elements in the queue.
|
|
434
|
-
*/
|
|
435
|
-
hasAffectedElements() {
|
|
436
|
-
return this._affectedElements.size > 0;
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Returns an array of typing-related elements and clears the internal list.
|
|
440
|
-
*/
|
|
441
|
-
flushAffectedElements() {
|
|
442
|
-
const result = Array.from(this._affectedElements);
|
|
443
|
-
this._affectedElements.clear();
|
|
444
|
-
return result;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
/**
|
|
448
|
-
* Deletes the content selected by the document selection at the start of composition.
|
|
449
|
-
*/
|
|
450
|
-
function deleteSelectionContent(model, insertTextCommand) {
|
|
451
|
-
// By relying on the state of the input command we allow disabling the entire input easily
|
|
452
|
-
// by just disabling the input command. We could’ve used here the delete command but that
|
|
453
|
-
// would mean requiring the delete feature which would block loading one without the other.
|
|
454
|
-
// We could also check the editor.isReadOnly property, but that wouldn't allow to block
|
|
455
|
-
// the input without blocking other features.
|
|
456
|
-
if (!insertTextCommand.isEnabled) {
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
const buffer = insertTextCommand.buffer;
|
|
460
|
-
buffer.lock();
|
|
461
|
-
model.enqueueChange(buffer.batch, () => {
|
|
462
|
-
model.deleteContent(model.document.selection);
|
|
463
|
-
});
|
|
464
|
-
buffer.unlock();
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Detaches a ModelLiveRange and returns the static range from it.
|
|
468
|
-
*/
|
|
469
|
-
function detachLiveRange(liveRange) {
|
|
470
|
-
const range = liveRange.toRange();
|
|
471
|
-
liveRange.detach();
|
|
472
|
-
if (range.root.rootName == '$graveyard') {
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
return range;
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* For the given `viewNode`, finds and returns the closest ancestor of this node that has a mapping to the model.
|
|
479
|
-
*/
|
|
480
|
-
function findMappedViewAncestor(viewNode, mapper) {
|
|
481
|
-
let node = (viewNode.is('$text') ? viewNode.parent : viewNode);
|
|
482
|
-
while (!mapper.toModelElement(node)) {
|
|
483
|
-
node = node.parent;
|
|
484
|
-
}
|
|
485
|
-
return node;
|
|
486
|
-
}
|
package/src/inserttextcommand.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* @module typing/inserttextcommand
|
|
7
|
-
*/
|
|
8
|
-
import { Command } from '@ckeditor/ckeditor5-core';
|
|
9
|
-
import { TypingChangeBuffer } 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 class InsertTextCommand extends Command {
|
|
14
|
-
/**
|
|
15
|
-
* Typing's change buffer used to group subsequent changes into batches.
|
|
16
|
-
*/
|
|
17
|
-
_buffer;
|
|
18
|
-
/**
|
|
19
|
-
* Creates an instance of the command.
|
|
20
|
-
*
|
|
21
|
-
* @param undoStepSize The maximum number of atomic changes
|
|
22
|
-
* which can be contained in one batch in the command buffer.
|
|
23
|
-
*/
|
|
24
|
-
constructor(editor, undoStepSize) {
|
|
25
|
-
super(editor);
|
|
26
|
-
this._buffer = new TypingChangeBuffer(editor.model, undoStepSize);
|
|
27
|
-
// Since this command may execute on different selectable than selection, it should be checked directly in execute block.
|
|
28
|
-
this._isEnabledBasedOnSelection = false;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* The current change buffer.
|
|
32
|
-
*/
|
|
33
|
-
get buffer() {
|
|
34
|
-
return this._buffer;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* @inheritDoc
|
|
38
|
-
*/
|
|
39
|
-
destroy() {
|
|
40
|
-
super.destroy();
|
|
41
|
-
this._buffer.destroy();
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Executes the input command. It replaces the content within the given range with the given text.
|
|
45
|
-
* Replacing is a two step process, first the content within the range is removed and then the new text is inserted
|
|
46
|
-
* at the beginning of the range (which after the removal is a collapsed range).
|
|
47
|
-
*
|
|
48
|
-
* @fires execute
|
|
49
|
-
* @param options The command options.
|
|
50
|
-
*/
|
|
51
|
-
execute(options = {}) {
|
|
52
|
-
const model = this.editor.model;
|
|
53
|
-
const doc = model.document;
|
|
54
|
-
const text = options.text || '';
|
|
55
|
-
const textInsertions = text.length;
|
|
56
|
-
let selection = doc.selection;
|
|
57
|
-
if (options.selection) {
|
|
58
|
-
selection = options.selection;
|
|
59
|
-
}
|
|
60
|
-
else if (options.range) {
|
|
61
|
-
selection = model.createSelection(options.range);
|
|
62
|
-
}
|
|
63
|
-
// Stop executing if selectable is in non-editable place.
|
|
64
|
-
if (!model.canEditAt(selection)) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const resultRange = options.resultRange;
|
|
68
|
-
model.enqueueChange(this._buffer.batch, writer => {
|
|
69
|
-
this._buffer.lock();
|
|
70
|
-
// Store selection attributes before deleting old content to preserve formatting and link.
|
|
71
|
-
// This unifies the behavior between ModelDocumentSelection and Selection provided as input option.
|
|
72
|
-
const selectionAttributes = Array.from(doc.selection.getAttributes());
|
|
73
|
-
model.deleteContent(selection);
|
|
74
|
-
if (text) {
|
|
75
|
-
model.insertContent(writer.createText(text, selectionAttributes), selection);
|
|
76
|
-
}
|
|
77
|
-
if (resultRange) {
|
|
78
|
-
writer.setSelection(resultRange);
|
|
79
|
-
}
|
|
80
|
-
else if (!selection.is('documentSelection')) {
|
|
81
|
-
writer.setSelection(selection);
|
|
82
|
-
}
|
|
83
|
-
this._buffer.unlock();
|
|
84
|
-
this._buffer.input(textInsertions);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|