@ckeditor/ckeditor5-typing 42.0.2 → 43.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -233
- package/dist/index.js +332 -69
- package/dist/index.js.map +1 -1
- package/dist/input.d.ts +8 -0
- package/dist/inserttextobserver.d.ts +2 -6
- package/package.json +4 -4
- package/src/input.d.ts +8 -0
- package/src/input.js +297 -22
- package/src/inserttextobserver.d.ts +2 -6
- package/src/inserttextobserver.js +36 -42
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Command, Plugin } from '@ckeditor/ckeditor5-core/dist/index.js';
|
|
6
6
|
import { env, EventInfo, count, keyCodes, isInsideSurrogatePair, isInsideCombinedSymbol, isInsideEmojiSequence, ObservableMixin } from '@ckeditor/ckeditor5-utils/dist/index.js';
|
|
7
|
-
import { Observer, FocusObserver, DomEventData, BubblingEventInfo, MouseObserver } from '@ckeditor/ckeditor5-engine/dist/index.js';
|
|
8
|
-
import { escapeRegExp } from 'lodash-es';
|
|
7
|
+
import { Observer, FocusObserver, DomEventData, LiveRange, BubblingEventInfo, MouseObserver } from '@ckeditor/ckeditor5-engine/dist/index.js';
|
|
8
|
+
import { debounce, escapeRegExp } from 'lodash-es';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
|
@@ -225,6 +225,10 @@ const TYPING_INPUT_TYPES = [
|
|
|
225
225
|
// This one is used by Safari when accepting spell check suggestions from the autocorrection pop-up (Mac).
|
|
226
226
|
'insertReplacementText'
|
|
227
227
|
];
|
|
228
|
+
const TYPING_INPUT_TYPES_ANDROID = [
|
|
229
|
+
...TYPING_INPUT_TYPES,
|
|
230
|
+
'insertCompositionText'
|
|
231
|
+
];
|
|
228
232
|
/**
|
|
229
233
|
* Text insertion observer introduces the {@link module:engine/view/document~Document#event:insertText} event.
|
|
230
234
|
*/ class InsertTextObserver extends Observer {
|
|
@@ -240,16 +244,14 @@ const TYPING_INPUT_TYPES = [
|
|
|
240
244
|
// On Android composition events should immediately be applied to the model. Rendering is not disabled.
|
|
241
245
|
// On non-Android the model is updated only on composition end.
|
|
242
246
|
// On Android we can't rely on composition start/end to update model.
|
|
243
|
-
|
|
244
|
-
TYPING_INPUT_TYPES.push('insertCompositionText');
|
|
245
|
-
}
|
|
247
|
+
const typingInputTypes = env.isAndroid ? TYPING_INPUT_TYPES_ANDROID : TYPING_INPUT_TYPES;
|
|
246
248
|
const viewDocument = view.document;
|
|
247
249
|
viewDocument.on('beforeinput', (evt, data)=>{
|
|
248
250
|
if (!this.isEnabled) {
|
|
249
251
|
return;
|
|
250
252
|
}
|
|
251
253
|
const { data: text, targetRanges, inputType, domEvent } = data;
|
|
252
|
-
if (!
|
|
254
|
+
if (!typingInputTypes.includes(inputType)) {
|
|
253
255
|
return;
|
|
254
256
|
}
|
|
255
257
|
// Mark the latest focus change as complete (we are typing in editable after the focus
|
|
@@ -266,46 +268,38 @@ const TYPING_INPUT_TYPES = [
|
|
|
266
268
|
evt.stop();
|
|
267
269
|
}
|
|
268
270
|
});
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
// We decided to go with the 2nd option for its simplicity and stability.
|
|
302
|
-
viewDocument.fire('insertText', new DomEventData(view, domEvent, {
|
|
303
|
-
text: data,
|
|
304
|
-
selection: viewDocument.selection
|
|
305
|
-
}));
|
|
306
|
-
}, {
|
|
307
|
-
priority: 'lowest'
|
|
308
|
-
});
|
|
271
|
+
// On Android composition events are immediately applied to the model.
|
|
272
|
+
// On non-Android the model is updated only on composition end.
|
|
273
|
+
// On Android we can't rely on composition start/end to update model.
|
|
274
|
+
if (!env.isAndroid) {
|
|
275
|
+
// Note: The priority must be lower than the CompositionObserver handler to call it after the renderer is unblocked.
|
|
276
|
+
// This is important for view to DOM position mapping.
|
|
277
|
+
// This causes the effect of first remove composed DOM and then reapply it after model modification.
|
|
278
|
+
viewDocument.on('compositionend', (evt, { data, domEvent })=>{
|
|
279
|
+
if (!this.isEnabled) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// In case of aborted composition.
|
|
283
|
+
if (!data) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
287
|
+
// @if CK_DEBUG_TYPING // console.log( `%c[InsertTextObserver]%c Fire insertText event, %c${ JSON.stringify( data ) }`,
|
|
288
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', 'color: blue'
|
|
289
|
+
// @if CK_DEBUG_TYPING // );
|
|
290
|
+
// @if CK_DEBUG_TYPING // }
|
|
291
|
+
// How do we know where to insert the composed text?
|
|
292
|
+
// 1. The SelectionObserver is blocked and the view is not updated with the composition changes.
|
|
293
|
+
// 2. The last moment before it's locked is the `compositionstart` event.
|
|
294
|
+
// 3. The `SelectionObserver` is listening for `compositionstart` event and immediately converts
|
|
295
|
+
// the selection. Handles this at the lowest priority so after the rendering is blocked.
|
|
296
|
+
viewDocument.fire('insertText', new DomEventData(view, domEvent, {
|
|
297
|
+
text: data
|
|
298
|
+
}));
|
|
299
|
+
}, {
|
|
300
|
+
priority: 'lowest'
|
|
301
|
+
});
|
|
302
|
+
}
|
|
309
303
|
}
|
|
310
304
|
/**
|
|
311
305
|
* @inheritDoc
|
|
@@ -318,6 +312,9 @@ const TYPING_INPUT_TYPES = [
|
|
|
318
312
|
/**
|
|
319
313
|
* Handles text input coming from the keyboard or other input methods.
|
|
320
314
|
*/ class Input extends Plugin {
|
|
315
|
+
/**
|
|
316
|
+
* The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
|
|
317
|
+
*/ _compositionQueue;
|
|
321
318
|
/**
|
|
322
319
|
* @inheritDoc
|
|
323
320
|
*/ static get pluginName() {
|
|
@@ -329,7 +326,9 @@ const TYPING_INPUT_TYPES = [
|
|
|
329
326
|
const editor = this.editor;
|
|
330
327
|
const model = editor.model;
|
|
331
328
|
const view = editor.editing.view;
|
|
329
|
+
const mapper = editor.editing.mapper;
|
|
332
330
|
const modelSelection = model.document.selection;
|
|
331
|
+
this._compositionQueue = new CompositionQueue(editor);
|
|
333
332
|
view.addObserver(InsertTextObserver);
|
|
334
333
|
// TODO The above default configuration value should be defined using editor.config.define() once it's fixed.
|
|
335
334
|
const insertTextCommand = new InsertTextCommand(editor, editor.config.get('typing.undoStep') || 20);
|
|
@@ -342,11 +341,19 @@ const TYPING_INPUT_TYPES = [
|
|
|
342
341
|
if (!view.document.isComposing) {
|
|
343
342
|
data.preventDefault();
|
|
344
343
|
}
|
|
345
|
-
|
|
344
|
+
// Flush queue on the next beforeinput event because it could happen
|
|
345
|
+
// that the mutation observer does not notice the DOM change in time.
|
|
346
|
+
if (env.isAndroid && view.document.isComposing) {
|
|
347
|
+
this._compositionQueue.flush('next beforeinput');
|
|
348
|
+
}
|
|
349
|
+
const { text, selection: viewSelection } = data;
|
|
350
|
+
let modelRanges;
|
|
346
351
|
// If view selection was specified, translate it to model selection.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
352
|
+
if (viewSelection) {
|
|
353
|
+
modelRanges = Array.from(viewSelection.getRanges()).map((viewRange)=>mapper.toModelRange(viewRange));
|
|
354
|
+
} else {
|
|
355
|
+
modelRanges = Array.from(modelSelection.getRanges());
|
|
356
|
+
}
|
|
350
357
|
let insertText = text;
|
|
351
358
|
// Typing in English on Android is firing composition events for the whole typed word.
|
|
352
359
|
// We need to check the target range text to only apply the difference.
|
|
@@ -368,24 +375,46 @@ const TYPING_INPUT_TYPES = [
|
|
|
368
375
|
}
|
|
369
376
|
}
|
|
370
377
|
}
|
|
378
|
+
if (insertText.length == 0 && modelRanges[0].isCollapsed) {
|
|
379
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
380
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Ignore insertion of an empty data to the collapsed range.',
|
|
381
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-style: italic'
|
|
382
|
+
// @if CK_DEBUG_TYPING // );
|
|
383
|
+
// @if CK_DEBUG_TYPING // }
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
371
386
|
}
|
|
372
|
-
const
|
|
387
|
+
const commandData = {
|
|
373
388
|
text: insertText,
|
|
374
389
|
selection: model.createSelection(modelRanges)
|
|
375
390
|
};
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
391
|
+
// This is a composition event and those are not cancellable, so we need to wait until browser updates the DOM
|
|
392
|
+
// and we could apply changes to the model and verify if the DOM is valid.
|
|
393
|
+
// The browser applies changes to the DOM not immediately on beforeinput event.
|
|
394
|
+
// We just wait for mutation observer to notice changes or as a fallback a timeout.
|
|
395
|
+
if (env.isAndroid && view.document.isComposing) {
|
|
396
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
397
|
+
// @if CK_DEBUG_TYPING // console.log( `%c[Input]%c Queue insertText:%c "${ commandData.text }"%c ` +
|
|
398
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
|
|
399
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]` +
|
|
400
|
+
// @if CK_DEBUG_TYPING // ` queue size: ${ this._compositionQueue.length + 1 }`,
|
|
401
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', 'color: blue', ''
|
|
402
|
+
// @if CK_DEBUG_TYPING // );
|
|
403
|
+
// @if CK_DEBUG_TYPING // }
|
|
404
|
+
this._compositionQueue.push(commandData);
|
|
405
|
+
} else {
|
|
406
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
407
|
+
// @if CK_DEBUG_TYPING // console.log( `%c[Input]%c Execute insertText:%c "${ commandData.text }"%c ` +
|
|
408
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
|
|
409
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]`,
|
|
410
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', 'color: blue', ''
|
|
411
|
+
// @if CK_DEBUG_TYPING // );
|
|
412
|
+
// @if CK_DEBUG_TYPING // }
|
|
413
|
+
editor.execute('insertText', commandData);
|
|
414
|
+
view.scrollToTheSelection();
|
|
385
415
|
}
|
|
386
|
-
editor.execute('insertText', insertTextCommandData);
|
|
387
|
-
view.scrollToTheSelection();
|
|
388
416
|
});
|
|
417
|
+
// Delete selected content on composition start.
|
|
389
418
|
if (env.isAndroid) {
|
|
390
419
|
// On Android with English keyboard, the composition starts just by putting caret
|
|
391
420
|
// at the word end or by selecting a table column. This is not a real composition started.
|
|
@@ -397,9 +426,9 @@ const TYPING_INPUT_TYPES = [
|
|
|
397
426
|
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
398
427
|
// @if CK_DEBUG_TYPING // const firstPositionPath = modelSelection.getFirstPosition()!.path;
|
|
399
428
|
// @if CK_DEBUG_TYPING // const lastPositionPath = modelSelection.getLastPosition()!.path;
|
|
400
|
-
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c KeyDown 229 -> model.deleteContent()'
|
|
401
|
-
// @if CK_DEBUG_TYPING //
|
|
402
|
-
// @if CK_DEBUG_TYPING //
|
|
429
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c KeyDown 229%c -> model.deleteContent() ' +
|
|
430
|
+
// @if CK_DEBUG_TYPING // `[${ firstPositionPath }]-[${ lastPositionPath }]`,
|
|
431
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', '',
|
|
403
432
|
// @if CK_DEBUG_TYPING // );
|
|
404
433
|
// @if CK_DEBUG_TYPING // }
|
|
405
434
|
deleteSelectionContent(model, insertTextCommand);
|
|
@@ -414,17 +443,232 @@ const TYPING_INPUT_TYPES = [
|
|
|
414
443
|
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
415
444
|
// @if CK_DEBUG_TYPING // const firstPositionPath = modelSelection.getFirstPosition()!.path;
|
|
416
445
|
// @if CK_DEBUG_TYPING // const lastPositionPath = modelSelection.getLastPosition()!.path;
|
|
417
|
-
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Composition start -> model.deleteContent()'
|
|
418
|
-
// @if CK_DEBUG_TYPING //
|
|
419
|
-
// @if CK_DEBUG_TYPING //
|
|
446
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Composition start%c -> model.deleteContent() ' +
|
|
447
|
+
// @if CK_DEBUG_TYPING // `[${ firstPositionPath }]-[${ lastPositionPath }]`,
|
|
448
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', '',
|
|
420
449
|
// @if CK_DEBUG_TYPING // );
|
|
421
450
|
// @if CK_DEBUG_TYPING // }
|
|
422
451
|
deleteSelectionContent(model, insertTextCommand);
|
|
423
452
|
});
|
|
424
453
|
}
|
|
454
|
+
// Apply composed changes to the model.
|
|
455
|
+
if (env.isAndroid) {
|
|
456
|
+
// Apply changes to the model as they are applied to the DOM by the browser.
|
|
457
|
+
// On beforeinput event, the DOM is not yet modified. We wait for detected mutations to apply model changes.
|
|
458
|
+
this.listenTo(view.document, 'mutations', (evt, { mutations })=>{
|
|
459
|
+
if (!view.document.isComposing) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
// Check if mutations are relevant for queued changes.
|
|
463
|
+
for (const { node } of mutations){
|
|
464
|
+
const viewElement = findMappedViewAncestor(node, mapper);
|
|
465
|
+
const modelElement = mapper.toModelElement(viewElement);
|
|
466
|
+
if (this._compositionQueue.isComposedElement(modelElement)) {
|
|
467
|
+
this._compositionQueue.flush('mutations');
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
472
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Mutations not related to the composition.',
|
|
473
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-style: italic'
|
|
474
|
+
// @if CK_DEBUG_TYPING // );
|
|
475
|
+
// @if CK_DEBUG_TYPING // }
|
|
476
|
+
});
|
|
477
|
+
// Make sure that all changes are applied to the model before the end of composition.
|
|
478
|
+
this.listenTo(view.document, 'compositionend', ()=>{
|
|
479
|
+
this._compositionQueue.flush('composition end');
|
|
480
|
+
});
|
|
481
|
+
// Trigger mutations check after the composition completes to fix all DOM changes that got ignored during composition.
|
|
482
|
+
// On Android the Renderer is not disabled while composing. While updating DOM nodes we ignore some changes
|
|
483
|
+
// that are not that important (like NBSP vs plain space character) and could break the composition flow.
|
|
484
|
+
// After composition is completed we trigger additional `mutations` event for elements affected by the composition
|
|
485
|
+
// so the Renderer can adjust the DOM to the expected structure without breaking the composition.
|
|
486
|
+
this.listenTo(view.document, 'compositionend', ()=>{
|
|
487
|
+
const mutations = [];
|
|
488
|
+
for (const element of this._compositionQueue.flushComposedElements()){
|
|
489
|
+
const viewElement = mapper.toViewElement(element);
|
|
490
|
+
if (!viewElement) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
mutations.push({
|
|
494
|
+
type: 'children',
|
|
495
|
+
node: viewElement
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
if (mutations.length) {
|
|
499
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
500
|
+
// @if CK_DEBUG_TYPING // console.group( '%c[Input]%c Fire post-composition mutation fixes.',
|
|
501
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green', 'font-weight: bold', ''
|
|
502
|
+
// @if CK_DEBUG_TYPING // );
|
|
503
|
+
// @if CK_DEBUG_TYPING // }
|
|
504
|
+
view.document.fire('mutations', {
|
|
505
|
+
mutations
|
|
506
|
+
});
|
|
507
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
508
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
509
|
+
// @if CK_DEBUG_TYPING // }
|
|
510
|
+
}
|
|
511
|
+
}, {
|
|
512
|
+
priority: 'lowest'
|
|
513
|
+
});
|
|
514
|
+
} else {
|
|
515
|
+
// After composition end we need to verify if there are no left-overs.
|
|
516
|
+
// Listening at the lowest priority so after the `InsertTextObserver` added above (all composed text
|
|
517
|
+
// should already be applied to the model, view, and DOM).
|
|
518
|
+
// On non-Android the `Renderer` is blocked while user is composing but the `MutationObserver` still collects
|
|
519
|
+
// mutated nodes and fires `mutations` events.
|
|
520
|
+
// Those events are recorded by the `Renderer` but not applied to the DOM while composing.
|
|
521
|
+
// We need to trigger those checks (and fixes) once again but this time without specifying the exact mutations
|
|
522
|
+
// since they are already recorded by the `Renderer`.
|
|
523
|
+
// It in the most cases just clears the internal record of mutated text nodes
|
|
524
|
+
// since all changes should already be applied to the DOM.
|
|
525
|
+
// This is especially needed when user cancels composition, so we can clear nodes marked to sync.
|
|
526
|
+
this.listenTo(view.document, 'compositionend', ()=>{
|
|
527
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
528
|
+
// @if CK_DEBUG_TYPING // console.group( '%c[Input]%c Force render after composition end.',
|
|
529
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green', 'font-weight: bold', ''
|
|
530
|
+
// @if CK_DEBUG_TYPING // );
|
|
531
|
+
// @if CK_DEBUG_TYPING // }
|
|
532
|
+
view.document.fire('mutations', {
|
|
533
|
+
mutations: []
|
|
534
|
+
});
|
|
535
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
536
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
537
|
+
// @if CK_DEBUG_TYPING // }
|
|
538
|
+
}, {
|
|
539
|
+
priority: 'lowest'
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* @inheritDoc
|
|
545
|
+
*/ destroy() {
|
|
546
|
+
super.destroy();
|
|
547
|
+
this._compositionQueue.destroy();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
|
|
552
|
+
*/ class CompositionQueue {
|
|
553
|
+
/**
|
|
554
|
+
* The editor instance.
|
|
555
|
+
*/ editor;
|
|
556
|
+
/**
|
|
557
|
+
* Debounced queue flush as a safety mechanism for cases of mutation observer not triggering.
|
|
558
|
+
*/ flushDebounced = debounce(()=>this.flush('timeout'), 50);
|
|
559
|
+
/**
|
|
560
|
+
* The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
|
|
561
|
+
*/ _queue = [];
|
|
562
|
+
/**
|
|
563
|
+
* A set of model elements. The composition happened in those elements. It's used for mutations check.
|
|
564
|
+
*/ _compositionElements = new Set();
|
|
565
|
+
/**
|
|
566
|
+
* @inheritDoc
|
|
567
|
+
*/ constructor(editor){
|
|
568
|
+
this.editor = editor;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Destroys the helper object.
|
|
572
|
+
*/ destroy() {
|
|
573
|
+
this.flushDebounced.cancel();
|
|
574
|
+
this._compositionElements.clear();
|
|
575
|
+
while(this._queue.length){
|
|
576
|
+
this.shift();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Returns the size of the queue.
|
|
581
|
+
*/ get length() {
|
|
582
|
+
return this._queue.length;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Push next insertText command data to the queue.
|
|
586
|
+
*/ push(commandData) {
|
|
587
|
+
const commandLiveData = {
|
|
588
|
+
text: commandData.text
|
|
589
|
+
};
|
|
590
|
+
if (commandData.selection) {
|
|
591
|
+
commandLiveData.selectionRanges = [];
|
|
592
|
+
for (const range of commandData.selection.getRanges()){
|
|
593
|
+
commandLiveData.selectionRanges.push(LiveRange.fromRange(range));
|
|
594
|
+
// Keep reference to the model element for later mutation checks.
|
|
595
|
+
this._compositionElements.add(range.start.parent);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
this._queue.push(commandLiveData);
|
|
599
|
+
this.flushDebounced();
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Shift the first item from the insertText command data queue.
|
|
603
|
+
*/ shift() {
|
|
604
|
+
const commandLiveData = this._queue.shift();
|
|
605
|
+
const commandData = {
|
|
606
|
+
text: commandLiveData.text
|
|
607
|
+
};
|
|
608
|
+
if (commandLiveData.selectionRanges) {
|
|
609
|
+
const ranges = commandLiveData.selectionRanges.map((liveRange)=>detachLiveRange(liveRange)).filter((range)=>!!range);
|
|
610
|
+
if (ranges.length) {
|
|
611
|
+
commandData.selection = this.editor.model.createSelection(ranges);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return commandData;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Applies all queued insertText command executions.
|
|
618
|
+
*
|
|
619
|
+
* @param reason Used only for debugging.
|
|
620
|
+
*/ flush(reason) {
|
|
621
|
+
const editor = this.editor;
|
|
622
|
+
const model = editor.model;
|
|
623
|
+
const view = editor.editing.view;
|
|
624
|
+
this.flushDebounced.cancel();
|
|
625
|
+
if (!this._queue.length) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
629
|
+
// @if CK_DEBUG_TYPING // console.group( `%c[Input]%c Flush insertText queue on ${ reason }.`,
|
|
630
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold'
|
|
631
|
+
// @if CK_DEBUG_TYPING // );
|
|
632
|
+
// @if CK_DEBUG_TYPING // }
|
|
633
|
+
const insertTextCommand = editor.commands.get('insertText');
|
|
634
|
+
const buffer = insertTextCommand.buffer;
|
|
635
|
+
model.enqueueChange(buffer.batch, ()=>{
|
|
636
|
+
buffer.lock();
|
|
637
|
+
while(this._queue.length){
|
|
638
|
+
const commandData = this.shift();
|
|
639
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
640
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Execute queued insertText:%c ' +
|
|
641
|
+
// @if CK_DEBUG_TYPING // `"${ commandData.text }"%c ` +
|
|
642
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
|
|
643
|
+
// @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]`,
|
|
644
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', 'font-weight: bold', 'color: blue', ''
|
|
645
|
+
// @if CK_DEBUG_TYPING // );
|
|
646
|
+
// @if CK_DEBUG_TYPING // }
|
|
647
|
+
editor.execute('insertText', commandData);
|
|
648
|
+
}
|
|
649
|
+
buffer.unlock();
|
|
650
|
+
});
|
|
651
|
+
view.scrollToTheSelection();
|
|
652
|
+
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
653
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
654
|
+
// @if CK_DEBUG_TYPING // }
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Returns `true` if the given model element is related to recent composition.
|
|
658
|
+
*/ isComposedElement(element) {
|
|
659
|
+
return this._compositionElements.has(element);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Returns an array of composition-related elements and clears the internal list.
|
|
663
|
+
*/ flushComposedElements() {
|
|
664
|
+
const result = Array.from(this._compositionElements);
|
|
665
|
+
this._compositionElements.clear();
|
|
666
|
+
return result;
|
|
425
667
|
}
|
|
426
668
|
}
|
|
427
|
-
|
|
669
|
+
/**
|
|
670
|
+
* Deletes the content selected by the document selection at the start of composition.
|
|
671
|
+
*/ function deleteSelectionContent(model, insertTextCommand) {
|
|
428
672
|
// By relying on the state of the input command we allow disabling the entire input easily
|
|
429
673
|
// by just disabling the input command. We could’ve used here the delete command but that
|
|
430
674
|
// would mean requiring the delete feature which would block loading one without the other.
|
|
@@ -440,6 +684,25 @@ function deleteSelectionContent(model, insertTextCommand) {
|
|
|
440
684
|
});
|
|
441
685
|
buffer.unlock();
|
|
442
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* Detaches a LiveRange and returns the static range from it.
|
|
689
|
+
*/ function detachLiveRange(liveRange) {
|
|
690
|
+
const range = liveRange.toRange();
|
|
691
|
+
liveRange.detach();
|
|
692
|
+
if (range.root.rootName == '$graveyard') {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
return range;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* For the given `viewNode`, finds and returns the closest ancestor of this node that has a mapping to the model.
|
|
699
|
+
*/ function findMappedViewAncestor(viewNode, mapper) {
|
|
700
|
+
let node = viewNode.is('$text') ? viewNode.parent : viewNode;
|
|
701
|
+
while(!mapper.toModelElement(node)){
|
|
702
|
+
node = node.parent;
|
|
703
|
+
}
|
|
704
|
+
return node;
|
|
705
|
+
}
|
|
443
706
|
|
|
444
707
|
/**
|
|
445
708
|
* The delete command. Used by the {@link module:typing/delete~Delete delete feature} to handle the <kbd>Delete</kbd> and
|