@ckeditor/ckeditor5-typing 45.0.0 → 45.1.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/dist/index.js CHANGED
@@ -251,7 +251,7 @@ const TYPING_INPUT_TYPES_ANDROID = [
251
251
  if (!this.isEnabled) {
252
252
  return;
253
253
  }
254
- const { data: text, targetRanges, inputType, domEvent } = data;
254
+ const { data: text, targetRanges, inputType, domEvent, isComposing } = data;
255
255
  if (!typingInputTypes.includes(inputType)) {
256
256
  return;
257
257
  }
@@ -261,7 +261,8 @@ const TYPING_INPUT_TYPES_ANDROID = [
261
261
  const eventInfo = new EventInfo(viewDocument, 'insertText');
262
262
  viewDocument.fire(eventInfo, new DomEventData(view, domEvent, {
263
263
  text,
264
- selection: view.createSelection(targetRanges)
264
+ selection: view.createSelection(targetRanges),
265
+ isComposing
265
266
  }));
266
267
  // Stop the beforeinput event if `delete` event was stopped.
267
268
  // https://github.com/ckeditor/ckeditor5/issues/753
@@ -295,12 +296,13 @@ const TYPING_INPUT_TYPES_ANDROID = [
295
296
  // 1. The SelectionObserver is blocked and the view is not updated with the composition changes.
296
297
  // 2. The last moment before it's locked is the `compositionstart` event.
297
298
  // 3. The `SelectionObserver` is listening for `compositionstart` event and immediately converts
298
- // the selection. Handles this at the lowest priority so after the rendering is blocked.
299
+ // the selection. Handle this at the low priority so after the rendering is blocked.
299
300
  viewDocument.fire('insertText', new DomEventData(view, domEvent, {
300
- text: data
301
+ text: data,
302
+ isComposing: true
301
303
  }));
302
304
  }, {
303
- priority: 'lowest'
305
+ priority: 'low'
304
306
  });
305
307
  }
306
308
  }
@@ -318,7 +320,7 @@ const TYPING_INPUT_TYPES_ANDROID = [
318
320
  */ class Input extends Plugin {
319
321
  /**
320
322
  * The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
321
- */ _compositionQueue;
323
+ */ _typingQueue;
322
324
  /**
323
325
  * @inheritDoc
324
326
  */ static get pluginName() {
@@ -337,25 +339,36 @@ const TYPING_INPUT_TYPES_ANDROID = [
337
339
  const view = editor.editing.view;
338
340
  const mapper = editor.editing.mapper;
339
341
  const modelSelection = model.document.selection;
340
- this._compositionQueue = new CompositionQueue(editor);
342
+ this._typingQueue = new TypingQueue(editor);
341
343
  view.addObserver(InsertTextObserver);
342
344
  // TODO The above default configuration value should be defined using editor.config.define() once it's fixed.
343
345
  const insertTextCommand = new InsertTextCommand(editor, editor.config.get('typing.undoStep') || 20);
344
346
  // Register `insertText` command and add `input` command as an alias for backward compatibility.
345
347
  editor.commands.add('insertText', insertTextCommand);
346
348
  editor.commands.add('input', insertTextCommand);
349
+ this.listenTo(view.document, 'beforeinput', ()=>{
350
+ // Flush queue on the next beforeinput event because it could happen
351
+ // that the mutation observer does not notice the DOM change in time.
352
+ this._typingQueue.flush('next beforeinput');
353
+ }, {
354
+ priority: 'high'
355
+ });
347
356
  this.listenTo(view.document, 'insertText', (evt, data)=>{
348
- // Rendering is disabled while composing so prevent events that will be rendered by the engine
349
- // and should not be applied by the browser.
350
- if (!view.document.isComposing) {
357
+ const { text, selection: viewSelection } = data;
358
+ // In case of a synthetic event, make sure that selection is not fake.
359
+ if (view.document.selection.isFake && viewSelection && view.document.selection.isSimilar(viewSelection)) {
351
360
  data.preventDefault();
352
361
  }
353
- // Flush queue on the next beforeinput event because it could happen
354
- // that the mutation observer does not notice the DOM change in time.
355
- if (env.isAndroid && view.document.isComposing) {
356
- this._compositionQueue.flush('next beforeinput');
362
+ if (!insertTextCommand.isEnabled) {
363
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
364
+ // @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
365
+ // @if CK_DEBUG_TYPING // '%cInsertText command is disabled - prevent DOM change.',
366
+ // @if CK_DEBUG_TYPING // 'font-style: italic'
367
+ // @if CK_DEBUG_TYPING // ) );
368
+ // @if CK_DEBUG_TYPING // }
369
+ data.preventDefault();
370
+ return;
357
371
  }
358
- const { text, selection: viewSelection } = data;
359
372
  let modelRanges;
360
373
  // If view selection was specified, translate it to model selection.
361
374
  if (viewSelection) {
@@ -394,40 +407,31 @@ const TYPING_INPUT_TYPES_ANDROID = [
394
407
  return;
395
408
  }
396
409
  }
410
+ // Note: the TypingQueue stores live-ranges internally as RTC could change the model while waiting for mutations.
397
411
  const commandData = {
398
412
  text: insertText,
399
413
  selection: model.createSelection(modelRanges)
400
414
  };
401
- // This is a composition event and those are not cancellable, so we need to wait until browser updates the DOM
415
+ // This is a beforeinput event, so we need to wait until the browser updates the DOM,
402
416
  // and we could apply changes to the model and verify if the DOM is valid.
403
417
  // The browser applies changes to the DOM not immediately on beforeinput event.
404
418
  // We just wait for mutation observer to notice changes or as a fallback a timeout.
405
- if (env.isAndroid && view.document.isComposing) {
406
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
407
- // @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
408
- // @if CK_DEBUG_TYPING // `%cQueue insertText:%c "${ commandData.text }"%c ` +
409
- // @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
410
- // @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]` +
411
- // @if CK_DEBUG_TYPING // ` queue size: ${ this._compositionQueue.length + 1 }`,
412
- // @if CK_DEBUG_TYPING // 'font-weight: bold',
413
- // @if CK_DEBUG_TYPING // 'color: blue',
414
- // @if CK_DEBUG_TYPING // ''
415
- // @if CK_DEBUG_TYPING // ) );
416
- // @if CK_DEBUG_TYPING // }
417
- this._compositionQueue.push(commandData);
418
- } else {
419
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
420
- // @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
421
- // @if CK_DEBUG_TYPING // `%cExecute insertText:%c "${ commandData.text }"%c ` +
422
- // @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
423
- // @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]`,
424
- // @if CK_DEBUG_TYPING // 'font-weight: bold',
425
- // @if CK_DEBUG_TYPING // 'color: blue',
426
- // @if CK_DEBUG_TYPING // ''
427
- // @if CK_DEBUG_TYPING // ) );
428
- // @if CK_DEBUG_TYPING // }
429
- editor.execute('insertText', commandData);
430
- view.scrollToTheSelection();
419
+ //
420
+ // Previously we were cancelling the non-composition events, but it caused issues especially in Safari.
421
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
422
+ // @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
423
+ // @if CK_DEBUG_TYPING // `%cQueue insertText:%c "${ commandData.text }"%c ` +
424
+ // @if CK_DEBUG_TYPING // `[${ commandData.selection.getFirstPosition().path }]-` +
425
+ // @if CK_DEBUG_TYPING // `[${ commandData.selection.getLastPosition().path }]` +
426
+ // @if CK_DEBUG_TYPING // ` queue size: ${ this._typingQueue.length + 1 }`,
427
+ // @if CK_DEBUG_TYPING // 'font-weight: bold',
428
+ // @if CK_DEBUG_TYPING // 'color: blue',
429
+ // @if CK_DEBUG_TYPING // ''
430
+ // @if CK_DEBUG_TYPING // ) );
431
+ // @if CK_DEBUG_TYPING // }
432
+ this._typingQueue.push(commandData, Boolean(data.isComposing));
433
+ if (data.domEvent.defaultPrevented) {
434
+ this._typingQueue.flush('beforeinput default prevented');
431
435
  }
432
436
  });
433
437
  // Delete selected content on composition start.
@@ -469,44 +473,48 @@ const TYPING_INPUT_TYPES_ANDROID = [
469
473
  // @if CK_DEBUG_TYPING // ) );
470
474
  // @if CK_DEBUG_TYPING // }
471
475
  deleteSelectionContent(model, insertTextCommand);
476
+ }, {
477
+ priority: 'high'
472
478
  });
473
479
  }
474
- // Apply composed changes to the model.
475
- if (env.isAndroid) {
476
- // Apply changes to the model as they are applied to the DOM by the browser.
477
- // On beforeinput event, the DOM is not yet modified. We wait for detected mutations to apply model changes.
478
- this.listenTo(view.document, 'mutations', (evt, { mutations })=>{
479
- if (!view.document.isComposing) {
480
- return;
481
- }
482
- // Check if mutations are relevant for queued changes.
480
+ // Apply changes to the model as they are applied to the DOM by the browser.
481
+ // On beforeinput event, the DOM is not yet modified. We wait for detected mutations to apply model changes.
482
+ this.listenTo(view.document, 'mutations', (evt, { mutations })=>{
483
+ // Check if mutations are relevant for queued changes.
484
+ if (this._typingQueue.hasAffectedElements()) {
483
485
  for (const { node } of mutations){
484
486
  const viewElement = findMappedViewAncestor(node, mapper);
485
487
  const modelElement = mapper.toModelElement(viewElement);
486
- if (this._compositionQueue.isComposedElement(modelElement)) {
487
- this._compositionQueue.flush('mutations');
488
+ if (this._typingQueue.isElementAffected(modelElement)) {
489
+ this._typingQueue.flush('mutations');
488
490
  return;
489
491
  }
490
492
  }
491
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
492
- // @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
493
- // @if CK_DEBUG_TYPING // '%cMutations not related to the composition.',
494
- // @if CK_DEBUG_TYPING // 'font-style: italic'
495
- // @if CK_DEBUG_TYPING // ) );
496
- // @if CK_DEBUG_TYPING // }
497
- });
498
- // Make sure that all changes are applied to the model before the end of composition.
499
- this.listenTo(view.document, 'compositionend', ()=>{
500
- this._compositionQueue.flush('composition end');
501
- });
502
- // Trigger mutations check after the composition completes to fix all DOM changes that got ignored during composition.
503
- // On Android the Renderer is not disabled while composing. While updating DOM nodes we ignore some changes
504
- // that are not that important (like NBSP vs plain space character) and could break the composition flow.
505
- // After composition is completed we trigger additional `mutations` event for elements affected by the composition
506
- // so the Renderer can adjust the DOM to the expected structure without breaking the composition.
507
- this.listenTo(view.document, 'compositionend', ()=>{
508
- const mutations = [];
509
- for (const element of this._compositionQueue.flushComposedElements()){
493
+ }
494
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
495
+ // @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
496
+ // @if CK_DEBUG_TYPING // '%cMutations not related to the composition.',
497
+ // @if CK_DEBUG_TYPING // 'font-style: italic'
498
+ // @if CK_DEBUG_TYPING // ) );
499
+ // @if CK_DEBUG_TYPING // }
500
+ });
501
+ // Make sure that all changes are applied to the model before the end of composition.
502
+ this.listenTo(view.document, 'compositionend', ()=>{
503
+ this._typingQueue.flush('before composition end');
504
+ }, {
505
+ priority: 'high'
506
+ });
507
+ // Trigger mutations check after the composition completes to fix all DOM changes that got ignored during composition.
508
+ // On Android, the Renderer is not disabled while composing. While updating DOM nodes, we ignore some changes
509
+ // that are not that important (like NBSP vs. plain space character) and could break the composition flow.
510
+ // After composition is completed, we trigger additional `mutations` event for elements affected by the composition
511
+ // so the Renderer can adjust the DOM to the expected structure without breaking the composition.
512
+ this.listenTo(view.document, 'compositionend', ()=>{
513
+ // There could be new item queued on the composition end, so flush it.
514
+ this._typingQueue.flush('after composition end');
515
+ const mutations = [];
516
+ if (this._typingQueue.hasAffectedElements()) {
517
+ for (const element of this._typingQueue.flushAffectedElements()){
510
518
  const viewElement = mapper.toViewElement(element);
511
519
  if (!viewElement) {
512
520
  continue;
@@ -516,63 +524,49 @@ const TYPING_INPUT_TYPES_ANDROID = [
516
524
  node: viewElement
517
525
  });
518
526
  }
519
- if (mutations.length) {
520
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
521
- // @if CK_DEBUG_TYPING // console.group( ..._buildLogMessage( this, 'Input',
522
- // @if CK_DEBUG_TYPING // '%cFire post-composition mutation fixes.',
523
- // @if CK_DEBUG_TYPING // 'font-weight: bold'
524
- // @if CK_DEBUG_TYPING // ) );
525
- // @if CK_DEBUG_TYPING // }
526
- view.document.fire('mutations', {
527
- mutations
528
- });
529
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
530
- // @if CK_DEBUG_TYPING // console.groupEnd();
531
- // @if CK_DEBUG_TYPING // }
532
- }
533
- }, {
534
- priority: 'lowest'
535
- });
536
- } else {
537
- // After composition end we need to verify if there are no left-overs.
538
- // Listening at the lowest priority so after the `InsertTextObserver` added above (all composed text
527
+ }
528
+ // Fire composition mutations, if any.
529
+ //
530
+ // For non-Android:
531
+ // After the composition end, we need to verify if there are no left-overs.
532
+ // Listening at the lowest priority, so after the `InsertTextObserver` added above (all composed text
539
533
  // should already be applied to the model, view, and DOM).
540
- // On non-Android the `Renderer` is blocked while user is composing but the `MutationObserver` still collects
534
+ // On non-Android the `Renderer` is blocked while the user is composing, but the `MutationObserver` still collects
541
535
  // mutated nodes and fires `mutations` events.
542
536
  // Those events are recorded by the `Renderer` but not applied to the DOM while composing.
543
537
  // We need to trigger those checks (and fixes) once again but this time without specifying the exact mutations
544
538
  // since they are already recorded by the `Renderer`.
545
- // It in the most cases just clears the internal record of mutated text nodes
539
+ // It in most cases just clears the internal record of mutated text nodes
546
540
  // since all changes should already be applied to the DOM.
547
- // This is especially needed when user cancels composition, so we can clear nodes marked to sync.
548
- this.listenTo(view.document, 'compositionend', ()=>{
541
+ // This is especially needed when a user cancels composition, so we can clear nodes marked to sync.
542
+ if (mutations.length || !env.isAndroid) {
549
543
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
550
544
  // @if CK_DEBUG_TYPING // console.group( ..._buildLogMessage( this, 'Input',
551
- // @if CK_DEBUG_TYPING // '%cForce render after composition end.',
545
+ // @if CK_DEBUG_TYPING // '%cFire post-composition mutation fixes.',
552
546
  // @if CK_DEBUG_TYPING // 'font-weight: bold'
553
547
  // @if CK_DEBUG_TYPING // ) );
554
548
  // @if CK_DEBUG_TYPING // }
555
549
  view.document.fire('mutations', {
556
- mutations: []
550
+ mutations
557
551
  });
558
552
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
559
553
  // @if CK_DEBUG_TYPING // console.groupEnd();
560
554
  // @if CK_DEBUG_TYPING // }
561
- }, {
562
- priority: 'lowest'
563
- });
564
- }
555
+ }
556
+ }, {
557
+ priority: 'lowest'
558
+ });
565
559
  }
566
560
  /**
567
561
  * @inheritDoc
568
562
  */ destroy() {
569
563
  super.destroy();
570
- this._compositionQueue.destroy();
564
+ this._typingQueue.destroy();
571
565
  }
572
566
  }
573
567
  /**
574
568
  * The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
575
- */ class CompositionQueue {
569
+ */ class TypingQueue {
576
570
  /**
577
571
  * The editor instance.
578
572
  */ editor;
@@ -583,8 +577,11 @@ const TYPING_INPUT_TYPES_ANDROID = [
583
577
  * The queue of `insertText` command executions that are waiting for the DOM to get updated after beforeinput event.
584
578
  */ _queue = [];
585
579
  /**
586
- * A set of model elements. The composition happened in those elements. It's used for mutations check.
587
- */ _compositionElements = new Set();
580
+ * Whether there is any composition enqueued or plain typing only.
581
+ */ _isComposing = false;
582
+ /**
583
+ * A set of model elements. The typing happened in those elements. It's used for mutations check.
584
+ */ _affectedElements = new Set();
588
585
  /**
589
586
  * @inheritDoc
590
587
  */ constructor(editor){
@@ -594,7 +591,7 @@ const TYPING_INPUT_TYPES_ANDROID = [
594
591
  * Destroys the helper object.
595
592
  */ destroy() {
596
593
  this.flushDebounced.cancel();
597
- this._compositionElements.clear();
594
+ this._affectedElements.clear();
598
595
  while(this._queue.length){
599
596
  this.shift();
600
597
  }
@@ -606,7 +603,7 @@ const TYPING_INPUT_TYPES_ANDROID = [
606
603
  }
607
604
  /**
608
605
  * Push next insertText command data to the queue.
609
- */ push(commandData) {
606
+ */ push(commandData, isComposing) {
610
607
  const commandLiveData = {
611
608
  text: commandData.text
612
609
  };
@@ -615,10 +612,11 @@ const TYPING_INPUT_TYPES_ANDROID = [
615
612
  for (const range of commandData.selection.getRanges()){
616
613
  commandLiveData.selectionRanges.push(LiveRange.fromRange(range));
617
614
  // Keep reference to the model element for later mutation checks.
618
- this._compositionElements.add(range.start.parent);
615
+ this._affectedElements.add(range.start.parent);
619
616
  }
620
617
  }
621
618
  this._queue.push(commandLiveData);
619
+ this._isComposing ||= isComposing;
622
620
  this.flushDebounced();
623
621
  }
624
622
  /**
@@ -673,6 +671,15 @@ const TYPING_INPUT_TYPES_ANDROID = [
673
671
  editor.execute('insertText', commandData);
674
672
  }
675
673
  buffer.unlock();
674
+ if (!this._isComposing) {
675
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
676
+ // @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'Input',
677
+ // @if CK_DEBUG_TYPING // 'Clear affected elements set'
678
+ // @if CK_DEBUG_TYPING // ) );
679
+ // @if CK_DEBUG_TYPING // }
680
+ this._affectedElements.clear();
681
+ }
682
+ this._isComposing = false;
676
683
  });
677
684
  view.scrollToTheSelection();
678
685
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
@@ -680,15 +687,20 @@ const TYPING_INPUT_TYPES_ANDROID = [
680
687
  // @if CK_DEBUG_TYPING // }
681
688
  }
682
689
  /**
683
- * Returns `true` if the given model element is related to recent composition.
684
- */ isComposedElement(element) {
685
- return this._compositionElements.has(element);
690
+ * Returns `true` if the given model element is related to recent typing.
691
+ */ isElementAffected(element) {
692
+ return this._affectedElements.has(element);
693
+ }
694
+ /**
695
+ * Returns `true` if there are any affected elements in the queue.
696
+ */ hasAffectedElements() {
697
+ return this._affectedElements.size > 0;
686
698
  }
687
699
  /**
688
- * Returns an array of composition-related elements and clears the internal list.
689
- */ flushComposedElements() {
690
- const result = Array.from(this._compositionElements);
691
- this._compositionElements.clear();
700
+ * Returns an array of typing-related elements and clears the internal list.
701
+ */ flushAffectedElements() {
702
+ const result = Array.from(this._affectedElements);
703
+ this._affectedElements.clear();
692
704
  return result;
693
705
  }
694
706
  }
@@ -1232,7 +1244,22 @@ const DELETE_EVENT_TYPES = {
1232
1244
  const ancestorLimit = editor.model.schema.getLimitElement(modelDocument.selection);
1233
1245
  const limitStartPosition = editor.model.createPositionAt(ancestorLimit, 0);
1234
1246
  if (limitStartPosition.isTouching(modelDocument.selection.getFirstPosition())) {
1247
+ // Stop the beforeinput event as it could be invalid.
1235
1248
  data.preventDefault();
1249
+ // Create a fake delete event so all features can act on it and the target range is proper.
1250
+ const modelRange = editor.model.schema.getNearestSelectionRange(limitStartPosition, 'forward');
1251
+ if (!modelRange) {
1252
+ return;
1253
+ }
1254
+ const viewSelection = view.createSelection(editor.editing.mapper.toViewRange(modelRange));
1255
+ const targetRange = viewSelection.getFirstRange();
1256
+ const eventInfo = new BubblingEventInfo(document, 'delete', targetRange);
1257
+ const deleteData = {
1258
+ unit: 'selection',
1259
+ direction: 'backward',
1260
+ selectionToRemove: viewSelection
1261
+ };
1262
+ viewDocument.fire(eventInfo, new DomEventData(view, data.domEvent, deleteData));
1236
1263
  }
1237
1264
  });
1238
1265
  if (this.editor.plugins.has('UndoEditing')) {