@ckeditor/ckeditor5-engine 42.0.2-alpha.2 → 43.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +1 -820
  2. package/dist/dev-utils/model.d.ts +2 -0
  3. package/dist/dev-utils/view.d.ts +1 -0
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.js +466 -271
  6. package/dist/index.js.map +1 -1
  7. package/dist/model/schema.d.ts +149 -51
  8. package/dist/view/observer/focusobserver.d.ts +12 -0
  9. package/dist/view/observer/mutationobserver.d.ts +34 -5
  10. package/dist/view/observer/selectionobserver.d.ts +1 -2
  11. package/dist/view/renderer.d.ts +12 -0
  12. package/dist/view/view.d.ts +1 -4
  13. package/package.json +2 -2
  14. package/src/conversion/upcasthelpers.js +0 -7
  15. package/src/dev-utils/model.d.ts +2 -0
  16. package/src/dev-utils/model.js +4 -2
  17. package/src/dev-utils/utils.js +7 -0
  18. package/src/dev-utils/view.d.ts +1 -0
  19. package/src/dev-utils/view.js +3 -0
  20. package/src/index.d.ts +3 -1
  21. package/src/index.js +2 -0
  22. package/src/model/model.js +1 -5
  23. package/src/model/schema.d.ts +149 -51
  24. package/src/model/schema.js +200 -70
  25. package/src/model/utils/insertcontent.js +21 -65
  26. package/src/view/domconverter.js +13 -9
  27. package/src/view/observer/compositionobserver.js +2 -0
  28. package/src/view/observer/focusobserver.d.ts +12 -0
  29. package/src/view/observer/focusobserver.js +55 -25
  30. package/src/view/observer/inputobserver.js +7 -5
  31. package/src/view/observer/mutationobserver.d.ts +34 -5
  32. package/src/view/observer/mutationobserver.js +8 -11
  33. package/src/view/observer/selectionobserver.d.ts +1 -2
  34. package/src/view/observer/selectionobserver.js +27 -9
  35. package/src/view/renderer.d.ts +12 -0
  36. package/src/view/renderer.js +111 -63
  37. package/src/view/view.d.ts +1 -4
  38. package/src/view/view.js +9 -0
@@ -60,6 +60,7 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() {
60
60
  this.selection = selection;
61
61
  this.set('isFocused', false);
62
62
  this.set('isSelecting', false);
63
+ this.set('isComposing', false);
63
64
  // Rendering the selection and inline filler manipulation should be postponed in (non-Android) Blink until the user finishes
64
65
  // creating the selection in DOM to avoid accidental selection collapsing
65
66
  // (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
@@ -71,12 +72,6 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() {
71
72
  }
72
73
  });
73
74
  }
74
- this.set('isComposing', false);
75
- this.on('change:isComposing', () => {
76
- if (!this.isComposing) {
77
- this.render();
78
- }
79
- });
80
75
  }
81
76
  /**
82
77
  * Marks a view node to be updated in the DOM by {@link #render `render()`}.
@@ -138,15 +133,15 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() {
138
133
  // and we should not do it because the difference between view and DOM could lead to position mapping problems.
139
134
  if (this.isComposing && !env.isAndroid) {
140
135
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
141
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Rendering aborted while isComposing',
142
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
136
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Rendering aborted while isComposing.',
137
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-style: italic'
143
138
  // @if CK_DEBUG_TYPING // );
144
139
  // @if CK_DEBUG_TYPING // }
145
140
  return;
146
141
  }
147
142
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
148
143
  // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Rendering',
149
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
144
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: bold'
150
145
  // @if CK_DEBUG_TYPING // );
151
146
  // @if CK_DEBUG_TYPING // }
152
147
  let inlineFillerPosition = null;
@@ -432,10 +427,10 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() {
432
427
  }
433
428
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
434
429
  // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update text',
435
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
430
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: normal'
436
431
  // @if CK_DEBUG_TYPING // );
437
432
  // @if CK_DEBUG_TYPING // }
438
- updateTextNode(domText, expectedText);
433
+ this._updateTextNode(domText, expectedText);
439
434
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
440
435
  // @if CK_DEBUG_TYPING // console.groupEnd();
441
436
  // @if CK_DEBUG_TYPING // }
@@ -486,7 +481,7 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() {
486
481
  }
487
482
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
488
483
  // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update children',
489
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
484
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: normal'
490
485
  // @if CK_DEBUG_TYPING // );
491
486
  // @if CK_DEBUG_TYPING // }
492
487
  // IME on Android inserts a new text node while typing after a link
@@ -516,6 +511,11 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() {
516
511
  // We need to make sure that we update the existing text node and not replace it with another one.
517
512
  // The composition and different "language" browser extensions are fragile to text node being completely replaced.
518
513
  const actions = this._findUpdateActions(diff, actualDomChildren, expectedDomChildren, areTextNodes);
514
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping && actions.every( a => a == 'equal' ) ) {
515
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Nothing to update.',
516
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-style: italic'
517
+ // @if CK_DEBUG_TYPING // );
518
+ // @if CK_DEBUG_TYPING // }
519
519
  let i = 0;
520
520
  const nodesToUnbind = new Set();
521
521
  // Handle deletions first.
@@ -527,9 +527,22 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() {
527
527
  for (const action of actions) {
528
528
  if (action === 'delete') {
529
529
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
530
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Remove node',
531
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', actualDomChildren[ i ]
532
- // @if CK_DEBUG_TYPING // );
530
+ // @if CK_DEBUG_TYPING // const node = actualDomChildren[ i ];
531
+ // @if CK_DEBUG_TYPING // if ( isText( node ) ) {
532
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Remove text node' +
533
+ // @if CK_DEBUG_TYPING // `${ this.isComposing ? ' while composing (may break composition)' : '' }: ` +
534
+ // @if CK_DEBUG_TYPING // `%c${ _escapeTextNodeData( node.data ) }%c (${ node.data.length })`,
535
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold',
536
+ // @if CK_DEBUG_TYPING // this.isComposing ? 'color: red; font-weight: bold' : '', 'color: blue', ''
537
+ // @if CK_DEBUG_TYPING // );
538
+ // @if CK_DEBUG_TYPING // } else {
539
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Remove element' +
540
+ // @if CK_DEBUG_TYPING // `${ this.isComposing ? ' while composing (may break composition)' : '' }: `,
541
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold',
542
+ // @if CK_DEBUG_TYPING // this.isComposing ? 'color: red; font-weight: bold' : '',
543
+ // @if CK_DEBUG_TYPING // node
544
+ // @if CK_DEBUG_TYPING // );
545
+ // @if CK_DEBUG_TYPING // }
533
546
  // @if CK_DEBUG_TYPING // }
534
547
  nodesToUnbind.add(actualDomChildren[i]);
535
548
  remove(actualDomChildren[i]);
@@ -542,25 +555,29 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() {
542
555
  for (const action of actions) {
543
556
  if (action === 'insert') {
544
557
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
545
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Insert node',
546
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', expectedDomChildren[ i ]
547
- // @if CK_DEBUG_TYPING // );
558
+ // @if CK_DEBUG_TYPING // const node = expectedDomChildren[ i ];
559
+ // @if CK_DEBUG_TYPING // if ( isText( node ) ) {
560
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Insert text node' +
561
+ // @if CK_DEBUG_TYPING // `${ this.isComposing ? ' while composing (may break composition)' : '' }: ` +
562
+ // @if CK_DEBUG_TYPING // `%c${ _escapeTextNodeData( node.data ) }%c (${ node.data.length })`,
563
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold',
564
+ // @if CK_DEBUG_TYPING // this.isComposing ? 'color: red; font-weight: bold' : '',
565
+ // @if CK_DEBUG_TYPING // 'color: blue', ''
566
+ // @if CK_DEBUG_TYPING // );
567
+ // @if CK_DEBUG_TYPING // } else {
568
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Insert element:',
569
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: normal',
570
+ // @if CK_DEBUG_TYPING // node
571
+ // @if CK_DEBUG_TYPING // );
572
+ // @if CK_DEBUG_TYPING // }
548
573
  // @if CK_DEBUG_TYPING // }
549
574
  insertAt(domElement, i, expectedDomChildren[i]);
550
575
  i++;
551
576
  }
552
- // Update the existing text node data. Note that replace action is generated only for Android for now.
577
+ // Update the existing text node data.
553
578
  else if (action === 'update') {
554
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
555
- // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update text node',
556
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
557
- // @if CK_DEBUG_TYPING // );
558
- // @if CK_DEBUG_TYPING // }
559
- updateTextNode(actualDomChildren[i], expectedDomChildren[i].data);
579
+ this._updateTextNode(actualDomChildren[i], expectedDomChildren[i].data);
560
580
  i++;
561
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
562
- // @if CK_DEBUG_TYPING // console.groupEnd();
563
- // @if CK_DEBUG_TYPING // }
564
581
  }
565
582
  else if (action === 'equal') {
566
583
  // Force updating text nodes inside elements which did not change and do not need to be re-rendered (#1125).
@@ -639,6 +656,63 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() {
639
656
  return newActions.concat(diff(actualSlice, expectedSlice, comparator)
640
657
  .map(action => action === 'equal' ? 'update' : action));
641
658
  }
659
+ /**
660
+ * Checks if text needs to be updated and possibly updates it by removing and inserting only parts
661
+ * of the data from the existing text node to reduce impact on the IME composition.
662
+ *
663
+ * @param domText DOM text node to update.
664
+ * @param expectedText The expected data of a text node.
665
+ */
666
+ _updateTextNode(domText, expectedText) {
667
+ const actualText = domText.data;
668
+ if (actualText == expectedText) {
669
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
670
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Text node does not need update:%c ' +
671
+ // @if CK_DEBUG_TYPING // `${ _escapeTextNodeData( actualText ) }%c (${ actualText.length })`,
672
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-style: italic', 'color: blue', ''
673
+ // @if CK_DEBUG_TYPING // );
674
+ // @if CK_DEBUG_TYPING // }
675
+ return;
676
+ }
677
+ // Our approach to interleaving space character with NBSP might differ with the one implemented by the browser.
678
+ // Avoid modifying the text node in the DOM if only NBSPs and spaces are interchanged.
679
+ // We should avoid DOM modifications while composing to avoid breakage of composition.
680
+ // See: https://github.com/ckeditor/ckeditor5/issues/13994.
681
+ if (env.isAndroid && this.isComposing && actualText.replace(/\u00A0/g, ' ') == expectedText.replace(/\u00A0/g, ' ')) {
682
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
683
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Text node ignore NBSP changes while composing: ' +
684
+ // @if CK_DEBUG_TYPING // `%c${ _escapeTextNodeData( actualText ) }%c (${ actualText.length }) ->` +
685
+ // @if CK_DEBUG_TYPING // ` %c${ _escapeTextNodeData( expectedText ) }%c (${ expectedText.length })`,
686
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-style: italic', 'color: blue', '', 'color: blue', ''
687
+ // @if CK_DEBUG_TYPING // );
688
+ // @if CK_DEBUG_TYPING // }
689
+ return;
690
+ }
691
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
692
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Update text node' +
693
+ // @if CK_DEBUG_TYPING // `${ this.isComposing ? ' while composing (may break composition)' : '' }: ` +
694
+ // @if CK_DEBUG_TYPING // `%c${ _escapeTextNodeData( actualText ) }%c (${ actualText.length }) ->` +
695
+ // @if CK_DEBUG_TYPING // ` %c${ _escapeTextNodeData( expectedText ) }%c (${ expectedText.length })`,
696
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', this.isComposing ? 'color: red; font-weight: bold' : '',
697
+ // @if CK_DEBUG_TYPING // 'color: blue', '', 'color: blue', ''
698
+ // @if CK_DEBUG_TYPING // );
699
+ // @if CK_DEBUG_TYPING // }
700
+ this._updateTextNodeInternal(domText, expectedText);
701
+ }
702
+ /**
703
+ * Part of the `_updateTextNode` method extracted for easier testing.
704
+ */
705
+ _updateTextNodeInternal(domText, expectedText) {
706
+ const actions = fastDiff(domText.data, expectedText);
707
+ for (const action of actions) {
708
+ if (action.type === 'insert') {
709
+ domText.insertData(action.index, action.values.join(''));
710
+ }
711
+ else { // 'delete'
712
+ domText.deleteData(action.index, action.howMany);
713
+ }
714
+ }
715
+ }
642
716
  /**
643
717
  * Marks text nodes to be synchronized.
644
718
  *
@@ -745,7 +819,7 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() {
745
819
  const focus = this.domConverter.viewPositionToDom(this.selection.focus);
746
820
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
747
821
  // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Update DOM selection:',
748
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', anchor, focus
822
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', '', anchor, focus
749
823
  // @if CK_DEBUG_TYPING // );
750
824
  // @if CK_DEBUG_TYPING // }
751
825
  domSelection.setBaseAndExtent(anchor.parent, anchor.offset, focus.parent, focus.offset);
@@ -969,37 +1043,11 @@ function createFakeSelectionContainer(domDocument) {
969
1043
  container.textContent = '\u00A0';
970
1044
  return container;
971
1045
  }
972
- /**
973
- * Checks if text needs to be updated and possibly updates it by removing and inserting only parts
974
- * of the data from the existing text node to reduce impact on the IME composition.
975
- *
976
- * @param domText DOM text node to update.
977
- * @param expectedText The expected data of a text node.
978
- */
979
- function updateTextNode(domText, expectedText) {
980
- const actualText = domText.data;
981
- if (actualText == expectedText) {
982
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
983
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Text node does not need update:',
984
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '',
985
- // @if CK_DEBUG_TYPING // `"${ domText.data }" (${ domText.data.length })`
986
- // @if CK_DEBUG_TYPING // );
987
- // @if CK_DEBUG_TYPING // }
988
- return;
989
- }
990
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
991
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Update text node:',
992
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '',
993
- // @if CK_DEBUG_TYPING // `"${ domText.data }" (${ domText.data.length }) -> "${ expectedText }" (${ expectedText.length })`
994
- // @if CK_DEBUG_TYPING // );
995
- // @if CK_DEBUG_TYPING // }
996
- const actions = fastDiff(actualText, expectedText);
997
- for (const action of actions) {
998
- if (action.type === 'insert') {
999
- domText.insertData(action.index, action.values.join(''));
1000
- }
1001
- else { // 'delete'
1002
- domText.deleteData(action.index, action.howMany);
1003
- }
1004
- }
1005
- }
1046
+ // @if CK_DEBUG_TYPING // function _escapeTextNodeData( text ) {
1047
+ // @if CK_DEBUG_TYPING // const escapedText = text
1048
+ // @if CK_DEBUG_TYPING // .replace( /&/g, '&' )
1049
+ // @if CK_DEBUG_TYPING // .replace( /\u00A0/g, ' ' )
1050
+ // @if CK_DEBUG_TYPING // .replace( /\u2060/g, '⁠' );
1051
+ // @if CK_DEBUG_TYPING //
1052
+ // @if CK_DEBUG_TYPING // return `"${ escapedText }"`;
1053
+ // @if CK_DEBUG_TYPING // }
@@ -7,7 +7,6 @@
7
7
  */
8
8
  import Document from './document.js';
9
9
  import DowncastWriter from './downcastwriter.js';
10
- import Renderer from './renderer.js';
11
10
  import DomConverter from './domconverter.js';
12
11
  import Position, { type PositionOffset } from './position.js';
13
12
  import Range from './range.js';
@@ -95,10 +94,8 @@ export default class View extends /* #__PURE__ */ View_base {
95
94
  hasDomSelection: boolean;
96
95
  /**
97
96
  * Instance of the {@link module:engine/view/renderer~Renderer renderer}.
98
- *
99
- * @internal
100
97
  */
101
- readonly _renderer: Renderer;
98
+ private readonly _renderer;
102
99
  /**
103
100
  * A DOM root attributes cache. It saves the initial values of DOM root attributes before the DOM element
104
101
  * is {@link module:engine/view/view~View#attachDomRoot attached} to the view so later on, when
package/src/view/view.js CHANGED
@@ -144,6 +144,15 @@ export default class View extends /* #__PURE__ */ ObservableMixin() {
144
144
  }
145
145
  });
146
146
  }
147
+ // Listen to external content mutations (directly in the DOM) and mark them to get verified by the renderer.
148
+ this.listenTo(this.document, 'mutations', (evt, { mutations }) => {
149
+ mutations.forEach(mutation => this._renderer.markToSync(mutation.type, mutation.node));
150
+ }, { priority: 'low' });
151
+ // After all mutated nodes were marked to sync we can trigger view to DOM synchronization
152
+ // to make sure the DOM structure matches the view.
153
+ this.listenTo(this.document, 'mutations', () => {
154
+ this.forceRender();
155
+ }, { priority: 'lowest' });
147
156
  }
148
157
  /**
149
158
  * Attaches a DOM root element to the view element and enable all observers on that element.