@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +22 -0
  4. package/ckeditor5-metadata.json +11 -0
  5. package/dist/augmentation.d.ts +31 -0
  6. package/dist/delete.d.ts +40 -0
  7. package/dist/deletecommand.d.ts +87 -0
  8. package/dist/deleteobserver.d.ts +59 -0
  9. package/dist/index-content.css +4 -0
  10. package/dist/index-editor.css +4 -0
  11. package/dist/index.css +4 -0
  12. package/dist/index.d.ts +29 -0
  13. package/dist/index.js +2490 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/input.d.ts +37 -0
  16. package/dist/inserttextcommand.d.ts +80 -0
  17. package/dist/inserttextobserver.d.ts +59 -0
  18. package/dist/texttransformation.d.ts +41 -0
  19. package/dist/textwatcher.d.ts +142 -0
  20. package/dist/twostepcaretmovement.d.ts +240 -0
  21. package/dist/typing.d.ts +31 -0
  22. package/dist/typingconfig.d.ts +208 -0
  23. package/dist/utils/changebuffer.d.ts +107 -0
  24. package/dist/utils/findattributerange.d.ts +37 -0
  25. package/dist/utils/getlasttextline.d.ts +53 -0
  26. package/dist/utils/inlinehighlight.d.ts +37 -0
  27. package/package.json +40 -0
  28. package/src/augmentation.d.ts +27 -0
  29. package/src/augmentation.js +5 -0
  30. package/src/delete.d.ts +36 -0
  31. package/src/delete.js +88 -0
  32. package/src/deletecommand.d.ts +83 -0
  33. package/src/deletecommand.js +201 -0
  34. package/src/deleteobserver.d.ts +55 -0
  35. package/src/deleteobserver.js +260 -0
  36. package/src/index.d.ts +25 -0
  37. package/src/index.js +18 -0
  38. package/src/input.d.ts +33 -0
  39. package/src/input.js +422 -0
  40. package/src/inserttextcommand.d.ts +76 -0
  41. package/src/inserttextcommand.js +83 -0
  42. package/src/inserttextobserver.d.ts +55 -0
  43. package/src/inserttextobserver.js +102 -0
  44. package/src/texttransformation.d.ts +37 -0
  45. package/src/texttransformation.js +234 -0
  46. package/src/textwatcher.d.ts +138 -0
  47. package/src/textwatcher.js +105 -0
  48. package/src/twostepcaretmovement.d.ts +236 -0
  49. package/src/twostepcaretmovement.js +628 -0
  50. package/src/typing.d.ts +27 -0
  51. package/src/typing.js +33 -0
  52. package/src/typingconfig.d.ts +204 -0
  53. package/src/typingconfig.js +5 -0
  54. package/src/utils/changebuffer.d.ts +103 -0
  55. package/src/utils/changebuffer.js +123 -0
  56. package/src/utils/findattributerange.d.ts +33 -0
  57. package/src/utils/findattributerange.js +41 -0
  58. package/src/utils/getlasttextline.d.ts +49 -0
  59. package/src/utils/getlasttextline.js +43 -0
  60. package/src/utils/inlinehighlight.d.ts +33 -0
  61. 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
+ }