@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/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
- if (env.isAndroid) {
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 (!TYPING_INPUT_TYPES.includes(inputType)) {
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
- // Note: The priority must be lower than the CompositionObserver handler to call it after the renderer is unblocked.
270
- viewDocument.on('compositionend', (evt, { data, domEvent })=>{
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 (!this.isEnabled || env.isAndroid) {
275
- return;
276
- }
277
- // In case of aborted composition.
278
- if (!data) {
279
- return;
280
- }
281
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
282
- // @if CK_DEBUG_TYPING // console.log( `%c[InsertTextObserver]%c Fire insertText event, text: ${ JSON.stringify( data ) }`,
283
- // @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', ''
284
- // @if CK_DEBUG_TYPING // );
285
- // @if CK_DEBUG_TYPING // }
286
- // How do we know where to insert the composed text?
287
- // The selection observer is blocked and the view is not updated with the composition changes.
288
- // There were three options:
289
- // - Store the selection on `compositionstart` and use it now. This wouldn't work in RTC
290
- // where the view would change and the stored selection might get incorrect.
291
- // We'd need to fallback to the current view selection anyway.
292
- // - Use the current view selection. This is a bit weird and non-intuitive because
293
- // this isn't necessarily the selection on which the user started composing.
294
- // We cannot even know whether it's still collapsed (there might be some weird
295
- // editor feature that changed it in unpredictable ways for us). But it's by far
296
- // the simplest solution and should be stable (the selection is definitely correct)
297
- // and probably mostly predictable (features usually don't modify the selection
298
- // unless called explicitly by the user).
299
- // - Try to follow it from the `beforeinput` events. This would be really complex as each
300
- // `beforeinput` would come with just the range it's changing and we'd need to calculate that.
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
- const { text, selection: viewSelection, resultRange: viewResultRange } = data;
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
- const modelRanges = Array.from(viewSelection.getRanges()).map((viewRange)=>{
348
- return editor.editing.mapper.toModelRange(viewRange);
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 insertTextCommandData = {
387
+ const commandData = {
373
388
  text: insertText,
374
389
  selection: model.createSelection(modelRanges)
375
390
  };
376
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
377
- // @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Execute insertText:',
378
- // @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', '',
379
- // @if CK_DEBUG_TYPING // insertText,
380
- // @if CK_DEBUG_TYPING // `[${ modelRanges[ 0 ].start.path }]-[${ modelRanges[ 0 ].end.path }]`
381
- // @if CK_DEBUG_TYPING // );
382
- // @if CK_DEBUG_TYPING // }
383
- if (viewResultRange) {
384
- insertTextCommandData.resultRange = editor.editing.mapper.toModelRange(viewResultRange);
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 // 'font-weight: bold; color: green;', '',
402
- // @if CK_DEBUG_TYPING // `[${ firstPositionPath }]-[${ lastPositionPath }]`
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 // 'font-weight: bold; color: green;', '',
419
- // @if CK_DEBUG_TYPING // `[${ firstPositionPath }]-[${ lastPositionPath }]`
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
- function deleteSelectionContent(model, insertTextCommand) {
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