@ckeditor/ckeditor5-engine 37.0.0-alpha.3 → 37.0.0-rc.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 (54) hide show
  1. package/package.json +23 -23
  2. package/src/controller/datacontroller.d.ts +3 -0
  3. package/src/controller/datacontroller.js +16 -1
  4. package/src/index.d.ts +4 -2
  5. package/src/index.js +4 -0
  6. package/src/model/differ.d.ts +52 -8
  7. package/src/model/differ.js +104 -4
  8. package/src/model/document.d.ts +17 -7
  9. package/src/model/document.js +44 -5
  10. package/src/model/documentfragment.d.ts +4 -0
  11. package/src/model/documentfragment.js +6 -0
  12. package/src/model/node.d.ts +4 -4
  13. package/src/model/node.js +9 -5
  14. package/src/model/operation/attributeoperation.d.ts +1 -1
  15. package/src/model/operation/attributeoperation.js +1 -1
  16. package/src/model/operation/insertoperation.d.ts +1 -1
  17. package/src/model/operation/insertoperation.js +1 -1
  18. package/src/model/operation/mergeoperation.d.ts +1 -1
  19. package/src/model/operation/mergeoperation.js +1 -1
  20. package/src/model/operation/moveoperation.d.ts +1 -1
  21. package/src/model/operation/moveoperation.js +1 -1
  22. package/src/model/operation/operation.d.ts +1 -1
  23. package/src/model/operation/operation.js +1 -1
  24. package/src/model/operation/operationfactory.js +2 -0
  25. package/src/model/operation/rootattributeoperation.d.ts +7 -11
  26. package/src/model/operation/rootattributeoperation.js +6 -6
  27. package/src/model/operation/rootoperation.d.ts +75 -0
  28. package/src/model/operation/rootoperation.js +108 -0
  29. package/src/model/operation/splitoperation.d.ts +1 -1
  30. package/src/model/operation/splitoperation.js +1 -1
  31. package/src/model/operation/transform.js +8 -0
  32. package/src/model/rootelement.d.ts +15 -1
  33. package/src/model/rootelement.js +17 -1
  34. package/src/model/writer.d.ts +29 -1
  35. package/src/model/writer.js +74 -1
  36. package/src/view/matcher.d.ts +2 -2
  37. package/src/view/matcher.js +2 -2
  38. package/src/view/observer/arrowkeysobserver.d.ts +4 -0
  39. package/src/view/observer/arrowkeysobserver.js +4 -0
  40. package/src/view/observer/domeventobserver.d.ts +4 -0
  41. package/src/view/observer/domeventobserver.js +6 -0
  42. package/src/view/observer/fakeselectionobserver.d.ts +4 -0
  43. package/src/view/observer/fakeselectionobserver.js +4 -0
  44. package/src/view/observer/mutationobserver.d.ts +4 -0
  45. package/src/view/observer/mutationobserver.js +16 -2
  46. package/src/view/observer/observer.d.ts +7 -2
  47. package/src/view/observer/selectionobserver.d.ts +4 -0
  48. package/src/view/observer/selectionobserver.js +6 -0
  49. package/src/view/observer/tabobserver.d.ts +4 -0
  50. package/src/view/observer/tabobserver.js +4 -0
  51. package/src/view/placeholder.js +3 -3
  52. package/src/view/renderer.d.ts +4 -4
  53. package/src/view/renderer.js +17 -25
  54. package/src/view/view.js +3 -0
@@ -13,6 +13,7 @@ import MergeOperation from './operation/mergeoperation';
13
13
  import MoveOperation from './operation/moveoperation';
14
14
  import RenameOperation from './operation/renameoperation';
15
15
  import RootAttributeOperation from './operation/rootattributeoperation';
16
+ import RootOperation from './operation/rootoperation';
16
17
  import SplitOperation from './operation/splitoperation';
17
18
  import DocumentFragment from './documentfragment';
18
19
  import DocumentSelection from './documentselection';
@@ -361,7 +362,7 @@ export default class Writer {
361
362
  * writer.move( sourceRange, image, 'after' );
362
363
  * ```
363
364
  *
364
- * These parameters works the same way as {@link #createPositionAt `writer.createPositionAt()`}.
365
+ * These parameters work the same way as {@link #createPositionAt `writer.createPositionAt()`}.
365
366
  *
366
367
  * Note that items can be moved only within the same tree. It means that you can move items within the same root
367
368
  * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
@@ -917,6 +918,78 @@ export default class Writer {
917
918
  const oldRange = marker.getRange();
918
919
  applyMarkerOperation(this, name, oldRange, null, marker.affectsData);
919
920
  }
921
+ /**
922
+ * Adds a new root to the document (or re-attaches a {@link #detachRoot detached root}).
923
+ *
924
+ * Throws an error, if trying to add a root that is already added and attached.
925
+ *
926
+ * @param rootName Name of the added root.
927
+ * @param elementName The element name. Defaults to `'$root'` which also has some basic schema defined
928
+ * (e.g. `$block` elements are allowed inside the `$root`). Make sure to define a proper schema if you use a different name.
929
+ * @returns The added root element.
930
+ */
931
+ addRoot(rootName, elementName = '$root') {
932
+ this._assertWriterUsedCorrectly();
933
+ const root = this.model.document.getRoot(rootName);
934
+ if (root && root.isAttached()) {
935
+ /**
936
+ * Root with provided name already exists and is attached.
937
+ *
938
+ * @error writer-addroot-root-exists
939
+ */
940
+ throw new CKEditorError('writer-addroot-root-exists', this);
941
+ }
942
+ const document = this.model.document;
943
+ const operation = new RootOperation(rootName, elementName, true, document, document.version);
944
+ this.batch.addOperation(operation);
945
+ this.model.applyOperation(operation);
946
+ return this.model.document.getRoot(rootName);
947
+ }
948
+ /**
949
+ * Detaches the root from the document.
950
+ *
951
+ * All content and markers are removed from the root upon detaching. New content and new markers cannot be added to the root, as long
952
+ * as it is detached.
953
+ *
954
+ * A root cannot be fully removed from the document, it can be only detached. A root is permanently removed only after you
955
+ * re-initialize the editor and do not specify the root in the initial data.
956
+ *
957
+ * A detached root can be re-attached using {@link #addRoot}.
958
+ *
959
+ * Throws an error if the root does not exist or the root is already detached.
960
+ *
961
+ * @param rootOrName Name of the detached root.
962
+ */
963
+ detachRoot(rootOrName) {
964
+ this._assertWriterUsedCorrectly();
965
+ const root = typeof rootOrName == 'string' ? this.model.document.getRoot(rootOrName) : rootOrName;
966
+ if (!root || !root.isAttached()) {
967
+ /**
968
+ * Root with provided name does not exist or is already detached.
969
+ *
970
+ * @error writer-detachroot-no-root
971
+ */
972
+ throw new CKEditorError('writer-detachroot-no-root', this);
973
+ }
974
+ // First, remove all markers from the root. It is better to do it before removing stuff for undo purposes.
975
+ // However, looking through all the markers may not be the best performance wise. But there's no better solution for now.
976
+ for (const marker of this.model.markers) {
977
+ if (marker.getRange().root === root) {
978
+ this.removeMarker(marker);
979
+ }
980
+ }
981
+ // Remove all attributes from the root.
982
+ for (const key of root.getAttributeKeys()) {
983
+ this.removeAttribute(key, root);
984
+ }
985
+ // Remove all contents of the root.
986
+ this.remove(this.createRangeIn(root));
987
+ // Finally, detach the root.
988
+ const document = this.model.document;
989
+ const operation = new RootOperation(root.rootName, root.name, false, document, document.version);
990
+ this.batch.addOperation(operation);
991
+ this.model.applyOperation(operation);
992
+ }
920
993
  setSelection(...args) {
921
994
  this._assertWriterUsedCorrectly();
922
995
  this.model.document.selection._setTo(...args);
@@ -447,7 +447,7 @@ export type ClassPatterns = PropertyPatterns<never>;
447
447
  * }
448
448
  * ```
449
449
  *
450
- * Refer to the {@glink updating/migration-to-29##migration-to-ckeditor-5-v2910 Migration to v29.1.0} guide
450
+ * Refer to the {@glink updating/guides/update-to-29##update-to-ckeditor-5-v2910 Migration to v29.1.0} guide
451
451
  * and {@link module:engine/view/matcher~MatcherPattern} documentation.
452
452
  *
453
453
  * @param pattern Pattern with missing properties.
@@ -478,7 +478,7 @@ export type ClassPatterns = PropertyPatterns<never>;
478
478
  * }
479
479
  * ```
480
480
  *
481
- * Refer to the {@glink updating/migration-to-29##migration-to-ckeditor-5-v2910 Migration to v29.1.0} guide
481
+ * Refer to the {@glink updating/guides/update-to-29##update-to-ckeditor-5-v2910 Migration to v29.1.0} guide
482
482
  * and the {@link module:engine/view/matcher~MatcherPattern} documentation.
483
483
  *
484
484
  * @param pattern Pattern with missing properties.
@@ -468,7 +468,7 @@ function matchStyles(patterns, element) {
468
468
  * }
469
469
  * ```
470
470
  *
471
- * Refer to the {@glink updating/migration-to-29##migration-to-ckeditor-5-v2910 Migration to v29.1.0} guide
471
+ * Refer to the {@glink updating/guides/update-to-29##update-to-ckeditor-5-v2910 Migration to v29.1.0} guide
472
472
  * and {@link module:engine/view/matcher~MatcherPattern} documentation.
473
473
  *
474
474
  * @param pattern Pattern with missing properties.
@@ -499,7 +499,7 @@ function matchStyles(patterns, element) {
499
499
  * }
500
500
  * ```
501
501
  *
502
- * Refer to the {@glink updating/migration-to-29##migration-to-ckeditor-5-v2910 Migration to v29.1.0} guide
502
+ * Refer to the {@glink updating/guides/update-to-29##update-to-ckeditor-5-v2910 Migration to v29.1.0} guide
503
503
  * and the {@link module:engine/view/matcher~MatcherPattern} documentation.
504
504
  *
505
505
  * @param pattern Pattern with missing properties.
@@ -23,6 +23,10 @@ export default class ArrowKeysObserver extends Observer {
23
23
  * @inheritDoc
24
24
  */
25
25
  observe(): void;
26
+ /**
27
+ * @inheritDoc
28
+ */
29
+ stopObserving(): void;
26
30
  }
27
31
  /**
28
32
  * Event fired when the user presses an arrow keys.
@@ -33,4 +33,8 @@ export default class ArrowKeysObserver extends Observer {
33
33
  * @inheritDoc
34
34
  */
35
35
  observe() { }
36
+ /**
37
+ * @inheritDoc
38
+ */
39
+ stopObserving() { }
36
40
  }
@@ -56,6 +56,10 @@ export default abstract class DomEventObserver<EventType extends keyof HTMLEleme
56
56
  * @inheritDoc
57
57
  */
58
58
  observe(domElement: HTMLElement): void;
59
+ /**
60
+ * @inheritDoc
61
+ */
62
+ stopObserving(domElement: HTMLElement): void;
59
63
  /**
60
64
  * Calls `Document#fire()` if observer {@link #isEnabled is enabled}.
61
65
  *
@@ -56,6 +56,12 @@ export default class DomEventObserver extends Observer {
56
56
  }, { useCapture: this.useCapture });
57
57
  });
58
58
  }
59
+ /**
60
+ * @inheritDoc
61
+ */
62
+ stopObserving(domElement) {
63
+ this.stopListening(domElement);
64
+ }
59
65
  /**
60
66
  * Calls `Document#fire()` if observer {@link #isEnabled is enabled}.
61
67
  *
@@ -27,6 +27,10 @@ export default class FakeSelectionObserver extends Observer {
27
27
  * @inheritDoc
28
28
  */
29
29
  observe(): void;
30
+ /**
31
+ * @inheritDoc
32
+ */
33
+ stopObserving(): void;
30
34
  /**
31
35
  * @inheritDoc
32
36
  */
@@ -45,6 +45,10 @@ export default class FakeSelectionObserver extends Observer {
45
45
  }
46
46
  }, { priority: 'lowest' });
47
47
  }
48
+ /**
49
+ * @inheritDoc
50
+ */
51
+ stopObserving() { }
48
52
  /**
49
53
  * @inheritDoc
50
54
  */
@@ -53,6 +53,10 @@ export default class MutationObserver extends Observer {
53
53
  * @inheritDoc
54
54
  */
55
55
  observe(domElement: HTMLElement): void;
56
+ /**
57
+ * @inheritDoc
58
+ */
59
+ stopObserving(domElement: HTMLElement): void;
56
60
  /**
57
61
  * @inheritDoc
58
62
  */
@@ -33,7 +33,7 @@ export default class MutationObserver extends Observer {
33
33
  };
34
34
  this.domConverter = view.domConverter;
35
35
  this.renderer = view._renderer;
36
- this._domElements = [];
36
+ this._domElements = new Set();
37
37
  this._mutationObserver = new window.MutationObserver(this._onMutations.bind(this));
38
38
  }
39
39
  /**
@@ -46,11 +46,25 @@ export default class MutationObserver extends Observer {
46
46
  * @inheritDoc
47
47
  */
48
48
  observe(domElement) {
49
- this._domElements.push(domElement);
49
+ this._domElements.add(domElement);
50
50
  if (this.isEnabled) {
51
51
  this._mutationObserver.observe(domElement, this._config);
52
52
  }
53
53
  }
54
+ /**
55
+ * @inheritDoc
56
+ */
57
+ stopObserving(domElement) {
58
+ this._domElements.delete(domElement);
59
+ if (this.isEnabled) {
60
+ // Unfortunately, it is not possible to stop observing particular DOM element.
61
+ // In order to stop observing one of multiple DOM elements, we need to re-connect the mutation observer.
62
+ this._mutationObserver.disconnect();
63
+ for (const domElement of this._domElements) {
64
+ this._mutationObserver.observe(domElement, this._config);
65
+ }
66
+ }
67
+ }
54
68
  /**
55
69
  * @inheritDoc
56
70
  */
@@ -71,11 +71,16 @@ export default abstract class Observer extends Observer_base {
71
71
  */
72
72
  checkShouldIgnoreEventFromTarget(domTarget: Node | null): boolean;
73
73
  /**
74
- * Starts observing the given root element.
74
+ * Starts observing given DOM element.
75
75
  *
76
- * @param name The name of the root element.
76
+ * @param domElement DOM element to observe.
77
+ * @param name The name of the related root element.
77
78
  */
78
79
  abstract observe(domElement: HTMLElement, name: string): void;
80
+ /**
81
+ * Stops observing given DOM element.
82
+ */
83
+ abstract stopObserving(domElement: HTMLElement): void;
79
84
  }
80
85
  /**
81
86
  * The constructor of {@link ~Observer} subclass.
@@ -74,6 +74,10 @@ export default class SelectionObserver extends Observer {
74
74
  * @inheritDoc
75
75
  */
76
76
  observe(domElement: HTMLElement): void;
77
+ /**
78
+ * @inheritDoc
79
+ */
80
+ stopObserving(domElement: HTMLElement): void;
77
81
  /**
78
82
  * @inheritDoc
79
83
  */
@@ -101,6 +101,12 @@ export default class SelectionObserver extends Observer {
101
101
  });
102
102
  this._documents.add(domDocument);
103
103
  }
104
+ /**
105
+ * @inheritDoc
106
+ */
107
+ stopObserving(domElement) {
108
+ this.stopListening(domElement);
109
+ }
104
110
  /**
105
111
  * @inheritDoc
106
112
  */
@@ -24,6 +24,10 @@ export default class TabObserver extends Observer {
24
24
  * @inheritDoc
25
25
  */
26
26
  observe(): void;
27
+ /**
28
+ * @inheritDoc
29
+ */
30
+ stopObserving(): void;
27
31
  }
28
32
  /**
29
33
  * Event fired when the user presses a tab key.
@@ -35,4 +35,8 @@ export default class TabObserver extends Observer {
35
35
  * @inheritDoc
36
36
  */
37
37
  observe() { }
38
+ /**
39
+ * @inheritDoc
40
+ */
41
+ stopObserving() { }
38
42
  }
@@ -56,10 +56,10 @@ export function enablePlaceholder({ view, element, text, isDirectHost = true, ke
56
56
  */
57
57
  export function disablePlaceholder(view, element) {
58
58
  const doc = element.document;
59
+ if (!documentPlaceholders.has(doc)) {
60
+ return;
61
+ }
59
62
  view.change(writer => {
60
- if (!documentPlaceholders.has(doc)) {
61
- return;
62
- }
63
63
  const placeholders = documentPlaceholders.get(doc);
64
64
  const config = placeholders.get(element);
65
65
  writer.removeAttribute('data-placeholder', config.hostElement);
@@ -208,11 +208,11 @@ export default class Renderer extends Renderer_base {
208
208
  * @param actions Actions array which is a result of the {@link module:utils/diff~diff} function.
209
209
  * @param actualDom Actual DOM children
210
210
  * @param expectedDom Expected DOM children.
211
- * @param options Options
212
- * @param options.replaceText Mark text nodes replacement.
213
- * @returns Actions array modified with the `replace` actions.
211
+ * @param comparator A comparator function that should return `true` if the given node should be reused
212
+ * (either by the update of a text node data or an element children list for similar elements).
213
+ * @returns Actions array modified with the `update` actions.
214
214
  */
215
- private _findReplaceActions;
215
+ private _findUpdateActions;
216
216
  /**
217
217
  * Marks text nodes to be synchronized.
218
218
  *
@@ -261,11 +261,11 @@ export default class Renderer extends ObservableMixin() {
261
261
  const actualDomChildren = Array.from(this.domConverter.mapViewToDom(viewElement).childNodes);
262
262
  const expectedDomChildren = Array.from(this.domConverter.viewChildrenToDom(viewElement, { withChildren: false }));
263
263
  const diff = this._diffNodeLists(actualDomChildren, expectedDomChildren);
264
- const actions = this._findReplaceActions(diff, actualDomChildren, expectedDomChildren);
265
- if (actions.indexOf('replace') !== -1) {
264
+ const actions = this._findUpdateActions(diff, actualDomChildren, expectedDomChildren, areSimilarElements);
265
+ if (actions.indexOf('update') !== -1) {
266
266
  const counter = { equal: 0, insert: 0, delete: 0 };
267
267
  for (const action of actions) {
268
- if (action === 'replace') {
268
+ if (action === 'update') {
269
269
  const insertIndex = counter.equal + counter.insert;
270
270
  const deleteIndex = counter.equal + counter.delete;
271
271
  const viewChild = viewElement.getChild(insertIndex);
@@ -512,17 +512,9 @@ export default class Renderer extends ObservableMixin() {
512
512
  addInlineFiller(domElement.ownerDocument, expectedDomChildren, inlineFillerPosition.offset);
513
513
  }
514
514
  const diff = this._diffNodeLists(actualDomChildren, expectedDomChildren);
515
- // The rendering is not disabled on Android in the composition mode.
516
- // Composition events are not cancellable and browser will modify the DOM tree.
517
- // On Android composition events are immediately applied to the model, so we don't need to skip rendering,
518
- // and we should not do it because the difference between view and DOM could lead to position mapping problems.
519
- // Since the composition is fragile and often breaks if the composed text node is replaced while composing
520
- // we need to make sure that we update the existing text node and not replace it with another one.
521
- // We don't want to change the behavior on other browsers for safety, but maybe one day cause it seems to make sense.
522
- // https://github.com/ckeditor/ckeditor5/issues/12455.
523
- const actions = env.isAndroid ?
524
- this._findReplaceActions(diff, actualDomChildren, expectedDomChildren, { replaceText: true }) :
525
- diff;
515
+ // We need to make sure that we update the existing text node and not replace it with another one.
516
+ // The composition and different "language" browser extensions are fragile to text node being completely replaced.
517
+ const actions = this._findUpdateActions(diff, actualDomChildren, expectedDomChildren, areTextNodes);
526
518
  let i = 0;
527
519
  const nodesToUnbind = new Set();
528
520
  // Handle deletions first.
@@ -541,7 +533,7 @@ export default class Renderer extends ObservableMixin() {
541
533
  nodesToUnbind.add(actualDomChildren[i]);
542
534
  remove(actualDomChildren[i]);
543
535
  }
544
- else if (action === 'equal' || action === 'replace') {
536
+ else if (action === 'equal' || action === 'update') {
545
537
  i++;
546
538
  }
547
539
  }
@@ -557,7 +549,7 @@ export default class Renderer extends ObservableMixin() {
557
549
  i++;
558
550
  }
559
551
  // Update the existing text node data. Note that replace action is generated only for Android for now.
560
- else if (action === 'replace') {
552
+ else if (action === 'update') {
561
553
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
562
554
  // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update text node',
563
555
  // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
@@ -613,11 +605,11 @@ export default class Renderer extends ObservableMixin() {
613
605
  * @param actions Actions array which is a result of the {@link module:utils/diff~diff} function.
614
606
  * @param actualDom Actual DOM children
615
607
  * @param expectedDom Expected DOM children.
616
- * @param options Options
617
- * @param options.replaceText Mark text nodes replacement.
618
- * @returns Actions array modified with the `replace` actions.
608
+ * @param comparator A comparator function that should return `true` if the given node should be reused
609
+ * (either by the update of a text node data or an element children list for similar elements).
610
+ * @returns Actions array modified with the `update` actions.
619
611
  */
620
- _findReplaceActions(actions, actualDom, expectedDom, options = {}) {
612
+ _findUpdateActions(actions, actualDom, expectedDom, comparator) {
621
613
  // If there is no both 'insert' and 'delete' actions, no need to check for replaced elements.
622
614
  if (actions.indexOf('insert') === -1 || actions.indexOf('delete') === -1) {
623
615
  return actions;
@@ -634,8 +626,8 @@ export default class Renderer extends ObservableMixin() {
634
626
  actualSlice.push(actualDom[counter.equal + counter.delete]);
635
627
  }
636
628
  else { // equal
637
- newActions = newActions.concat(diff(actualSlice, expectedSlice, options.replaceText ? areTextNodes : areSimilar)
638
- .map(x => x === 'equal' ? 'replace' : x));
629
+ newActions = newActions.concat(diff(actualSlice, expectedSlice, comparator)
630
+ .map(action => action === 'equal' ? 'update' : action));
639
631
  newActions.push('equal');
640
632
  // Reset stored elements on 'equal'.
641
633
  actualSlice = [];
@@ -643,8 +635,8 @@ export default class Renderer extends ObservableMixin() {
643
635
  }
644
636
  counter[action]++;
645
637
  }
646
- return newActions.concat(diff(actualSlice, expectedSlice, options.replaceText ? areTextNodes : areSimilar)
647
- .map(x => x === 'equal' ? 'replace' : x));
638
+ return newActions.concat(diff(actualSlice, expectedSlice, comparator)
639
+ .map(action => action === 'equal' ? 'update' : action));
648
640
  }
649
641
  /**
650
642
  * Marks text nodes to be synchronized.
@@ -879,7 +871,7 @@ function addInlineFiller(domDocument, domParentOrArray, offset) {
879
871
  * Whether two DOM nodes should be considered as similar.
880
872
  * Nodes are considered similar if they have the same tag name.
881
873
  */
882
- function areSimilar(node1, node2) {
874
+ function areSimilarElements(node1, node2) {
883
875
  return isNode(node1) && isNode(node2) &&
884
876
  !isText(node1) && !isText(node2) &&
885
877
  !isComment(node1) && !isComment(node2) &&
package/src/view/view.js CHANGED
@@ -215,6 +215,9 @@ export default class View extends ObservableMixin() {
215
215
  }
216
216
  this.domRoots.delete(name);
217
217
  this.domConverter.unbindDomElement(domRoot);
218
+ for (const observer of this._observers.values()) {
219
+ observer.stopObserving(domRoot);
220
+ }
218
221
  }
219
222
  /**
220
223
  * Gets DOM root element.