@ckeditor/ckeditor5-engine 37.0.0-alpha.2 → 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 (55) 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.d.ts +21 -3
  55. package/src/view/view.js +21 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-engine",
3
- "version": "37.0.0-alpha.2",
3
+ "version": "37.0.0-rc.0",
4
4
  "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
5
5
  "keywords": [
6
6
  "wysiwyg",
@@ -23,36 +23,36 @@
23
23
  ],
24
24
  "main": "src/index.js",
25
25
  "dependencies": {
26
- "@ckeditor/ckeditor5-utils": "^37.0.0-alpha.2",
26
+ "@ckeditor/ckeditor5-utils": "^37.0.0-rc.0",
27
27
  "lodash-es": "^4.17.15"
28
28
  },
29
29
  "devDependencies": {
30
- "@ckeditor/ckeditor5-basic-styles": "^37.0.0-alpha.2",
31
- "@ckeditor/ckeditor5-block-quote": "^37.0.0-alpha.2",
32
- "@ckeditor/ckeditor5-clipboard": "^37.0.0-alpha.2",
33
- "@ckeditor/ckeditor5-cloud-services": "^37.0.0-alpha.2",
34
- "@ckeditor/ckeditor5-core": "^37.0.0-alpha.2",
35
- "@ckeditor/ckeditor5-editor-classic": "^37.0.0-alpha.2",
36
- "@ckeditor/ckeditor5-enter": "^37.0.0-alpha.2",
37
- "@ckeditor/ckeditor5-essentials": "^37.0.0-alpha.2",
38
- "@ckeditor/ckeditor5-heading": "^37.0.0-alpha.2",
39
- "@ckeditor/ckeditor5-image": "^37.0.0-alpha.2",
40
- "@ckeditor/ckeditor5-link": "^37.0.0-alpha.2",
41
- "@ckeditor/ckeditor5-list": "^37.0.0-alpha.2",
42
- "@ckeditor/ckeditor5-mention": "^37.0.0-alpha.2",
43
- "@ckeditor/ckeditor5-paragraph": "^37.0.0-alpha.2",
44
- "@ckeditor/ckeditor5-table": "^37.0.0-alpha.2",
45
- "@ckeditor/ckeditor5-theme-lark": "^37.0.0-alpha.2",
46
- "@ckeditor/ckeditor5-typing": "^37.0.0-alpha.2",
47
- "@ckeditor/ckeditor5-ui": "^37.0.0-alpha.2",
48
- "@ckeditor/ckeditor5-undo": "^37.0.0-alpha.2",
49
- "@ckeditor/ckeditor5-widget": "^37.0.0-alpha.2",
30
+ "@ckeditor/ckeditor5-basic-styles": "^37.0.0-rc.0",
31
+ "@ckeditor/ckeditor5-block-quote": "^37.0.0-rc.0",
32
+ "@ckeditor/ckeditor5-clipboard": "^37.0.0-rc.0",
33
+ "@ckeditor/ckeditor5-cloud-services": "^37.0.0-rc.0",
34
+ "@ckeditor/ckeditor5-core": "^37.0.0-rc.0",
35
+ "@ckeditor/ckeditor5-editor-classic": "^37.0.0-rc.0",
36
+ "@ckeditor/ckeditor5-enter": "^37.0.0-rc.0",
37
+ "@ckeditor/ckeditor5-essentials": "^37.0.0-rc.0",
38
+ "@ckeditor/ckeditor5-heading": "^37.0.0-rc.0",
39
+ "@ckeditor/ckeditor5-image": "^37.0.0-rc.0",
40
+ "@ckeditor/ckeditor5-link": "^37.0.0-rc.0",
41
+ "@ckeditor/ckeditor5-list": "^37.0.0-rc.0",
42
+ "@ckeditor/ckeditor5-mention": "^37.0.0-rc.0",
43
+ "@ckeditor/ckeditor5-paragraph": "^37.0.0-rc.0",
44
+ "@ckeditor/ckeditor5-table": "^37.0.0-rc.0",
45
+ "@ckeditor/ckeditor5-theme-lark": "^37.0.0-rc.0",
46
+ "@ckeditor/ckeditor5-typing": "^37.0.0-rc.0",
47
+ "@ckeditor/ckeditor5-ui": "^37.0.0-rc.0",
48
+ "@ckeditor/ckeditor5-undo": "^37.0.0-rc.0",
49
+ "@ckeditor/ckeditor5-widget": "^37.0.0-rc.0",
50
50
  "typescript": "^4.8.4",
51
51
  "webpack": "^5.58.1",
52
52
  "webpack-cli": "^4.9.0"
53
53
  },
54
54
  "engines": {
55
- "node": ">=14.0.0",
55
+ "node": ">=16.0.0",
56
56
  "npm": ">=5.7.1"
57
57
  },
58
58
  "author": "CKSource (http://cksource.com/)",
@@ -90,6 +90,9 @@ export default class DataController extends DataController_base {
90
90
  * Returns the model's data converted by downcast dispatchers attached to {@link #downcastDispatcher} and
91
91
  * formatted by the {@link #processor data processor}.
92
92
  *
93
+ * A warning is logged when you try to retrieve data for a detached root, as most probably this is a mistake. A detached root should
94
+ * be treated like it is removed, and you should not save its data. Note, that the detached root data is always an empty string.
95
+ *
93
96
  * @fires get
94
97
  * @param options Additional configuration for the retrieved data. `DataController` provides two optional
95
98
  * properties: `rootName` and `trim`. Other properties of this object are specified by various editor features.
@@ -17,6 +17,7 @@ import ViewDowncastWriter from '../view/downcastwriter';
17
17
  import ModelRange from '../model/range';
18
18
  import { autoParagraphEmptyRoots } from '../model/utils/autoparagraphing';
19
19
  import HtmlDataProcessor from '../dataprocessor/htmldataprocessor';
20
+ import { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
20
21
  /**
21
22
  * Controller for the data pipeline. The data pipeline controls how data is retrieved from the document
22
23
  * and set inside it. Hence, the controller features two methods which allow to {@link ~DataController#get get}
@@ -87,6 +88,9 @@ export default class DataController extends EmitterMixin() {
87
88
  * Returns the model's data converted by downcast dispatchers attached to {@link #downcastDispatcher} and
88
89
  * formatted by the {@link #processor data processor}.
89
90
  *
91
+ * A warning is logged when you try to retrieve data for a detached root, as most probably this is a mistake. A detached root should
92
+ * be treated like it is removed, and you should not save its data. Note, that the detached root data is always an empty string.
93
+ *
90
94
  * @fires get
91
95
  * @param options Additional configuration for the retrieved data. `DataController` provides two optional
92
96
  * properties: `rootName` and `trim`. Other properties of this object are specified by various editor features.
@@ -116,6 +120,17 @@ export default class DataController extends EmitterMixin() {
116
120
  throw new CKEditorError('datacontroller-get-non-existent-root', this);
117
121
  }
118
122
  const root = this.model.document.getRoot(rootName);
123
+ if (!root.isAttached()) {
124
+ /**
125
+ * Retrieving document data for a detached root.
126
+ *
127
+ * This usually indicates an error as a detached root should be considered "removed" and should not be included in the
128
+ * document data.
129
+ *
130
+ * @error datacontroller-get-detached-root
131
+ */
132
+ logWarning('datacontroller-get-detached-root', this);
133
+ }
119
134
  if (trim === 'empty' && !this.model.hasContent(root, { ignoreWhitespaces: true })) {
120
135
  return '';
121
136
  }
@@ -387,7 +402,7 @@ export default class DataController extends EmitterMixin() {
387
402
  */
388
403
  _checkIfRootsExists(rootNames) {
389
404
  for (const rootName of rootNames) {
390
- if (!this.model.document.getRootNames().includes(rootName)) {
405
+ if (!this.model.document.getRoot(rootName)) {
391
406
  return false;
392
407
  }
393
408
  }
package/src/index.d.ts CHANGED
@@ -24,8 +24,10 @@ export { default as MergeOperation } from './model/operation/mergeoperation';
24
24
  export { default as SplitOperation } from './model/operation/splitoperation';
25
25
  export { default as MarkerOperation } from './model/operation/markeroperation';
26
26
  export { default as OperationFactory } from './model/operation/operationfactory';
27
- export type { default as AttributeOperation } from './model/operation/attributeoperation';
28
- export type { default as RenameOperation } from './model/operation/renameoperation';
27
+ export { default as AttributeOperation } from './model/operation/attributeoperation';
28
+ export { default as RenameOperation } from './model/operation/renameoperation';
29
+ export { default as RootAttributeOperation } from './model/operation/rootattributeoperation';
30
+ export { default as RootOperation } from './model/operation/rootoperation';
29
31
  export { transformSets } from './model/operation/transform';
30
32
  export { default as DocumentSelection, type DocumentSelectionChangeRangeEvent } from './model/documentselection';
31
33
  export { default as Range } from './model/range';
package/src/index.js CHANGED
@@ -18,6 +18,10 @@ export { default as MergeOperation } from './model/operation/mergeoperation';
18
18
  export { default as SplitOperation } from './model/operation/splitoperation';
19
19
  export { default as MarkerOperation } from './model/operation/markeroperation';
20
20
  export { default as OperationFactory } from './model/operation/operationfactory';
21
+ export { default as AttributeOperation } from './model/operation/attributeoperation';
22
+ export { default as RenameOperation } from './model/operation/renameoperation';
23
+ export { default as RootAttributeOperation } from './model/operation/rootattributeoperation';
24
+ export { default as RootOperation } from './model/operation/rootoperation';
21
25
  export { transformSets } from './model/operation/transform';
22
26
  // Model.
23
27
  export { default as DocumentSelection } from './model/documentselection';
@@ -45,6 +45,12 @@ export default class Differ {
45
45
  * - `newMarkerData`.
46
46
  */
47
47
  private readonly _changedMarkers;
48
+ /**
49
+ * A map that stores all roots that have been changed.
50
+ *
51
+ * The keys are the names of the roots while value represents the changes.
52
+ */
53
+ private readonly _changedRoots;
48
54
  /**
49
55
  * Stores the number of changes that were processed. Used to order the changes chronologically. It is important
50
56
  * when changes are sorted.
@@ -83,9 +89,6 @@ export default class Differ {
83
89
  /**
84
90
  * Buffers the given operation. An operation has to be buffered before it is executed.
85
91
  *
86
- * Operation type is checked and it is checked which nodes it will affect. These nodes are then stored in `Differ`
87
- * in the state before the operation is executed.
88
- *
89
92
  * @param operationToBuffer An operation to buffer.
90
93
  */
91
94
  bufferOperation(operationToBuffer: Operation): void;
@@ -132,6 +135,7 @@ export default class Differ {
132
135
  *
133
136
  * * model structure changes,
134
137
  * * attribute changes,
138
+ * * a root is added or detached,
135
139
  * * changes of markers which were defined as `affectsData`,
136
140
  * * changes of markers' `affectsData` property.
137
141
  */
@@ -157,6 +161,12 @@ export default class Differ {
157
161
  getChanges(options?: {
158
162
  includeChangesInGraveyard?: boolean;
159
163
  }): Array<DiffItem>;
164
+ /**
165
+ * Returns all roots that have changed (either were attached, or detached, or their attributes changed).
166
+ *
167
+ * @returns Diff between the old and the new roots state.
168
+ */
169
+ getChangedRoots(): Array<DiffItemRoot>;
160
170
  /**
161
171
  * Returns a set of model items that were marked to get refreshed.
162
172
  */
@@ -165,6 +175,14 @@ export default class Differ {
165
175
  * Resets `Differ`. Removes all buffered changes.
166
176
  */
167
177
  reset(): void;
178
+ /**
179
+ * Buffers the root state change after the root was attached or detached
180
+ */
181
+ private _bufferRootStateChange;
182
+ /**
183
+ * Buffers a root attribute change.
184
+ */
185
+ private _bufferRootAttributeChange;
168
186
  /**
169
187
  * Marks the given `item` in differ to be "refreshed". It means that the item will be marked as removed and inserted
170
188
  * in the differ changes set, so it will be effectively re-converted when the differ changes are handled by a dispatcher.
@@ -253,7 +271,7 @@ export default class Differ {
253
271
  */
254
272
  export type DiffItem = DiffItemInsert | DiffItemRemove | DiffItemAttribute;
255
273
  /**
256
- * The single diff item for inserted nodes.
274
+ * A single diff item for inserted nodes.
257
275
  */
258
276
  export interface DiffItemInsert {
259
277
  /**
@@ -273,12 +291,12 @@ export interface DiffItemInsert {
273
291
  */
274
292
  position: Position;
275
293
  /**
276
- * The length of an inserted text node. For elements it is always 1 as each inserted element is counted as a one.
294
+ * The length of an inserted text node. For elements, it is always 1 as each inserted element is counted as a one.
277
295
  */
278
296
  length: number;
279
297
  }
280
298
  /**
281
- * The single diff item for removed nodes.
299
+ * A single diff item for removed nodes.
282
300
  */
283
301
  export interface DiffItemRemove {
284
302
  /**
@@ -298,12 +316,12 @@ export interface DiffItemRemove {
298
316
  */
299
317
  position: Position;
300
318
  /**
301
- * The length of a removed text node. For elements it is always 1 as each removed element is counted as a one.
319
+ * The length of a removed text node. For elements, it is always 1, as each removed element is counted as a one.
302
320
  */
303
321
  length: number;
304
322
  }
305
323
  /**
306
- * The single diff item for attribute change.
324
+ * A single diff item for attribute change.
307
325
  */
308
326
  export interface DiffItemAttribute {
309
327
  /**
@@ -327,3 +345,29 @@ export interface DiffItemAttribute {
327
345
  */
328
346
  range: Range;
329
347
  }
348
+ /**
349
+ * A single diff item for a changed root.
350
+ */
351
+ export interface DiffItemRoot {
352
+ /**
353
+ * Name of the changed root.
354
+ */
355
+ name: string;
356
+ /**
357
+ * Set accordingly if the root got attached or detached. Otherwise, not set.
358
+ */
359
+ state?: 'attached' | 'detached';
360
+ /**
361
+ * Keeps all attribute changes that happened on the root.
362
+ *
363
+ * The keys are keys of the changed attributes. The values are objects containing the attribute value before the change
364
+ * (`oldValue`) and after the change (`newValue`).
365
+ *
366
+ * Note, that if the root state changed (`state` is set), then `attributes` property will not be set. All attributes should be
367
+ * handled together with the root being attached or detached.
368
+ */
369
+ attributes?: Record<string, {
370
+ oldValue: unknown;
371
+ newValue: unknown;
372
+ }>;
373
+ }
@@ -44,6 +44,12 @@ export default class Differ {
44
44
  * - `newMarkerData`.
45
45
  */
46
46
  this._changedMarkers = new Map();
47
+ /**
48
+ * A map that stores all roots that have been changed.
49
+ *
50
+ * The keys are the names of the roots while value represents the changes.
51
+ */
52
+ this._changedRoots = new Map();
47
53
  /**
48
54
  * Stores the number of changes that were processed. Used to order the changes chronologically. It is important
49
55
  * when changes are sorted.
@@ -75,14 +81,11 @@ export default class Differ {
75
81
  * Informs whether there are any changes buffered in `Differ`.
76
82
  */
77
83
  get isEmpty() {
78
- return this._changesInElement.size == 0 && this._changedMarkers.size == 0;
84
+ return this._changesInElement.size == 0 && this._changedMarkers.size == 0 && this._changedRoots.size == 0;
79
85
  }
80
86
  /**
81
87
  * Buffers the given operation. An operation has to be buffered before it is executed.
82
88
  *
83
- * Operation type is checked and it is checked which nodes it will affect. These nodes are then stored in `Differ`
84
- * in the state before the operation is executed.
85
- *
86
89
  * @param operationToBuffer An operation to buffer.
87
90
  */
88
91
  bufferOperation(operationToBuffer) {
@@ -174,6 +177,18 @@ export default class Differ {
174
177
  }
175
178
  break;
176
179
  }
180
+ case 'detachRoot':
181
+ case 'addRoot': {
182
+ this._bufferRootStateChange(operation.rootName, operation.isAdd);
183
+ break;
184
+ }
185
+ case 'addRootAttribute':
186
+ case 'removeRootAttribute':
187
+ case 'changeRootAttribute': {
188
+ const rootName = operation.root.rootName;
189
+ this._bufferRootAttributeChange(rootName, operation.key, operation.oldValue, operation.newValue);
190
+ break;
191
+ }
177
192
  }
178
193
  // Clear cache after each buffered operation as it is no longer valid.
179
194
  this._cachedChanges = null;
@@ -249,6 +264,7 @@ export default class Differ {
249
264
  *
250
265
  * * model structure changes,
251
266
  * * attribute changes,
267
+ * * a root is added or detached,
252
268
  * * changes of markers which were defined as `affectsData`,
253
269
  * * changes of markers' `affectsData` property.
254
270
  */
@@ -256,6 +272,9 @@ export default class Differ {
256
272
  if (this._changesInElement.size > 0) {
257
273
  return true;
258
274
  }
275
+ if (this._changedRoots.size > 0) {
276
+ return true;
277
+ }
259
278
  for (const { newMarkerData, oldMarkerData } of this._changedMarkers.values()) {
260
279
  if (newMarkerData.affectsData !== oldMarkerData.affectsData) {
261
280
  return true;
@@ -429,6 +448,27 @@ export default class Differ {
429
448
  return this._cachedChanges.slice();
430
449
  }
431
450
  }
451
+ /**
452
+ * Returns all roots that have changed (either were attached, or detached, or their attributes changed).
453
+ *
454
+ * @returns Diff between the old and the new roots state.
455
+ */
456
+ getChangedRoots() {
457
+ return Array.from(this._changedRoots.values()).map(diffItem => {
458
+ const entry = { ...diffItem };
459
+ if (entry.state !== undefined) {
460
+ // The root was attached or detached -- do not return its attributes changes.
461
+ // If the root was attached, it should be handled as a whole, together with its attributes, the same way as model nodes.
462
+ // If the root was detached, its attributes should be discarded anyway.
463
+ //
464
+ // Keep in mind that filtering must happen on this stage (when retrieving changes). If filtering happens on-the-fly as
465
+ // the attributes change, it may lead to incorrect situation, e.g.: detach root, change attribute, re-attach root.
466
+ // In this case, attribute change cannot be filtered. After the root is re-attached, the attribute change must be kept.
467
+ delete entry.attributes;
468
+ }
469
+ return entry;
470
+ });
471
+ }
432
472
  /**
433
473
  * Returns a set of model items that were marked to get refreshed.
434
474
  */
@@ -442,9 +482,69 @@ export default class Differ {
442
482
  this._changesInElement.clear();
443
483
  this._elementSnapshots.clear();
444
484
  this._changedMarkers.clear();
485
+ this._changedRoots.clear();
445
486
  this._refreshedItems = new Set();
446
487
  this._cachedChanges = null;
447
488
  }
489
+ /**
490
+ * Buffers the root state change after the root was attached or detached
491
+ */
492
+ _bufferRootStateChange(rootName, isAttached) {
493
+ if (!this._changedRoots.has(rootName)) {
494
+ this._changedRoots.set(rootName, { name: rootName, state: isAttached ? 'attached' : 'detached' });
495
+ return;
496
+ }
497
+ const diffItem = this._changedRoots.get(rootName);
498
+ if (diffItem.state !== undefined) {
499
+ // Root `state` can only toggle between of the values ('attached' or 'detached') and no value. It cannot be any other way,
500
+ // because if the root was originally attached it can only become detached. Then, if it is re-attached in the same batch of
501
+ // changes, it gets back to "no change" (which means no value). Same if the root was originally detached.
502
+ delete diffItem.state;
503
+ if (diffItem.attributes === undefined) {
504
+ // If there is no `state` change and no `attributes` change, remove the entry.
505
+ this._changedRoots.delete(rootName);
506
+ }
507
+ }
508
+ else {
509
+ diffItem.state = isAttached ? 'attached' : 'detached';
510
+ }
511
+ }
512
+ /**
513
+ * Buffers a root attribute change.
514
+ */
515
+ _bufferRootAttributeChange(rootName, key, oldValue, newValue) {
516
+ const diffItem = this._changedRoots.get(rootName) || { name: rootName };
517
+ const attrs = diffItem.attributes || {};
518
+ if (attrs[key]) {
519
+ // If this attribute or metadata was already changed earlier and is changed again, check to what value it is changed.
520
+ const attrEntry = attrs[key];
521
+ if (newValue === attrEntry.oldValue) {
522
+ // If it was changed back to the old value, remove the entry.
523
+ delete attrs[key];
524
+ }
525
+ else {
526
+ // If it was changed to a different value, update the entry.
527
+ attrEntry.newValue = newValue;
528
+ }
529
+ }
530
+ else {
531
+ // If this attribute or metadata was not set earlier, add an entry.
532
+ attrs[key] = { oldValue, newValue };
533
+ }
534
+ if (Object.entries(attrs).length === 0) {
535
+ // If attributes or metadata changes set became empty, remove it from the diff item.
536
+ delete diffItem.attributes;
537
+ if (diffItem.state === undefined) {
538
+ // If there is no `state` change and no `attributes` change, remove the entry.
539
+ this._changedRoots.delete(rootName);
540
+ }
541
+ }
542
+ else {
543
+ // Make sure that, if a new object in the structure was created, it gets set.
544
+ diffItem.attributes = attrs;
545
+ this._changedRoots.set(rootName, diffItem);
546
+ }
547
+ }
448
548
  /**
449
549
  * Marks the given `item` in differ to be "refreshed". It means that the item will be marked as removed and inserted
450
550
  * in the differ changes set, so it will be effectively re-converted when the differ changes are handled by a dispatcher.
@@ -48,8 +48,8 @@ export default class Document extends Document_base {
48
48
  */
49
49
  readonly selection: DocumentSelection;
50
50
  /**
51
- * A list of roots that are owned and managed by this document. Use {@link #createRoot} and
52
- * {@link #getRoot} to manipulate it.
51
+ * A list of roots that are owned and managed by this document. Use {@link #createRoot}, {@link #getRoot} and
52
+ * {@link #getRootNames} to manipulate it.
53
53
  */
54
54
  readonly roots: Collection<RootElement>;
55
55
  /**
@@ -61,7 +61,7 @@ export default class Document extends Document_base {
61
61
  */
62
62
  private readonly _postFixers;
63
63
  /**
64
- * A boolean indicates whether the selection has changed until
64
+ * A flag that indicates whether the selection has changed since last change block.
65
65
  */
66
66
  private _hasSelectionChangedFromTheLastChangeBlock;
67
67
  /**
@@ -87,8 +87,11 @@ export default class Document extends Document_base {
87
87
  /**
88
88
  * Creates a new root.
89
89
  *
90
+ * **Note:** do not use this method after the editor has been initialized! If you want to dynamically add a root, use
91
+ * {@link module:engine/model/writer~Writer#addRoot `model.Writer#addRoot`} instead.
92
+ *
90
93
  * @param elementName The element name. Defaults to `'$root'` which also has some basic schema defined
91
- * (`$block`s are allowed inside the `$root`). Make sure to define a proper schema if you use a different name.
94
+ * (e.g. `$block` elements are allowed inside the `$root`). Make sure to define a proper schema if you use a different name.
92
95
  * @param rootName A unique root name.
93
96
  * @returns The created root.
94
97
  */
@@ -100,16 +103,23 @@ export default class Document extends Document_base {
100
103
  /**
101
104
  * Returns a root by its name.
102
105
  *
103
- * @param name A unique root name.
106
+ * Detached roots are returned by this method. This is to be able to operate on the detached root (for example, to be able to create
107
+ * a position inside such a root for undo feature purposes).
108
+ *
109
+ * @param name The root name of the root to return.
104
110
  * @returns The root registered under a given name or `null` when there is no root with the given name.
105
111
  */
106
112
  getRoot(name?: string): RootElement | null;
107
113
  /**
108
- * Returns an array with names of all roots (without the {@link #graveyard}) added to the document.
114
+ * Returns an array with names of all roots added to the document (except the {@link #graveyard graveyard root}).
115
+ *
116
+ * Detached roots **are not** returned by this method by default. This is to make sure that all features or algorithms that operate
117
+ * on the document data know which roots are still a part of the document and should be processed.
109
118
  *
119
+ * @param includeDetached Specified whether detached roots should be returned as well.
110
120
  * @returns Roots names.
111
121
  */
112
- getRootNames(): Array<string>;
122
+ getRootNames(includeDetached?: boolean): Array<string>;
113
123
  /**
114
124
  * Used to register a post-fixer callback. A post-fixer mechanism guarantees that the features
115
125
  * will operate on a correct model state.
@@ -79,6 +79,33 @@ export default class Document extends EmitterMixin() {
79
79
  });
80
80
  }
81
81
  });
82
+ // This is a solution for a problem that may occur during real-time editing. If one client detached a root and another added
83
+ // something there at the same moment, the OT does not solve this problem currently. In such situation, the added elements would
84
+ // stay in the detached root.
85
+ //
86
+ // This is incorrect, a detached root should be empty and all elements from it should be removed. To solve this, the post-fixer will
87
+ // remove any element that is left in a detached root.
88
+ //
89
+ // Similarly, markers that are created at the beginning or at the end of the detached root will not be removed as well.
90
+ //
91
+ // The drawback of this solution over the OT solution is that the elements removed by the post-fixer will never be brought back.
92
+ // If the root detachment gets undone (and the root is brought back), the removed elements will not be there.
93
+ this.registerPostFixer(writer => {
94
+ let result = false;
95
+ for (const root of this.roots) {
96
+ if (!root.isAttached() && !root.isEmpty) {
97
+ writer.remove(writer.createRangeIn(root));
98
+ result = true;
99
+ }
100
+ }
101
+ for (const marker of this.model.markers) {
102
+ if (!marker.getRange().root.isAttached()) {
103
+ writer.removeMarker(marker);
104
+ result = true;
105
+ }
106
+ }
107
+ return result;
108
+ });
82
109
  }
83
110
  /**
84
111
  * The document version. Every applied operation increases the version number. It is used to
@@ -104,8 +131,11 @@ export default class Document extends EmitterMixin() {
104
131
  /**
105
132
  * Creates a new root.
106
133
  *
134
+ * **Note:** do not use this method after the editor has been initialized! If you want to dynamically add a root, use
135
+ * {@link module:engine/model/writer~Writer#addRoot `model.Writer#addRoot`} instead.
136
+ *
107
137
  * @param elementName The element name. Defaults to `'$root'` which also has some basic schema defined
108
- * (`$block`s are allowed inside the `$root`). Make sure to define a proper schema if you use a different name.
138
+ * (e.g. `$block` elements are allowed inside the `$root`). Make sure to define a proper schema if you use a different name.
109
139
  * @param rootName A unique root name.
110
140
  * @returns The created root.
111
141
  */
@@ -132,19 +162,28 @@ export default class Document extends EmitterMixin() {
132
162
  /**
133
163
  * Returns a root by its name.
134
164
  *
135
- * @param name A unique root name.
165
+ * Detached roots are returned by this method. This is to be able to operate on the detached root (for example, to be able to create
166
+ * a position inside such a root for undo feature purposes).
167
+ *
168
+ * @param name The root name of the root to return.
136
169
  * @returns The root registered under a given name or `null` when there is no root with the given name.
137
170
  */
138
171
  getRoot(name = 'main') {
139
172
  return this.roots.get(name);
140
173
  }
141
174
  /**
142
- * Returns an array with names of all roots (without the {@link #graveyard}) added to the document.
175
+ * Returns an array with names of all roots added to the document (except the {@link #graveyard graveyard root}).
176
+ *
177
+ * Detached roots **are not** returned by this method by default. This is to make sure that all features or algorithms that operate
178
+ * on the document data know which roots are still a part of the document and should be processed.
143
179
  *
180
+ * @param includeDetached Specified whether detached roots should be returned as well.
144
181
  * @returns Roots names.
145
182
  */
146
- getRootNames() {
147
- return Array.from(this.roots, root => root.rootName).filter(name => name != graveyardName);
183
+ getRootNames(includeDetached = false) {
184
+ return Array.from(this.roots)
185
+ .filter(root => root.rootName != graveyardName && (includeDetached || root.isAttached()))
186
+ .map(root => root.rootName);
148
187
  }
149
188
  /**
150
189
  * Used to register a post-fixer callback. A post-fixer mechanism guarantees that the features
@@ -82,6 +82,10 @@ export default class DocumentFragment extends TypeCheckable implements Iterable<
82
82
  * Artificial owner of `DocumentFragment`. Returns `null`. Added for compatibility reasons.
83
83
  */
84
84
  get document(): null;
85
+ /**
86
+ * Returns `false` as `DocumentFragment` by definition is not attached to a document. Added for compatibility reasons.
87
+ */
88
+ isAttached(): false;
85
89
  /**
86
90
  * Returns empty array. Added for compatibility reasons.
87
91
  */
@@ -100,6 +100,12 @@ export default class DocumentFragment extends TypeCheckable {
100
100
  get document() {
101
101
  return null;
102
102
  }
103
+ /**
104
+ * Returns `false` as `DocumentFragment` by definition is not attached to a document. Added for compatibility reasons.
105
+ */
106
+ isAttached() {
107
+ return false;
108
+ }
103
109
  /**
104
110
  * Returns empty array. Added for compatibility reasons.
105
111
  */
@@ -66,15 +66,15 @@ export default abstract class Node extends TypeCheckable {
66
66
  */
67
67
  get document(): Document | null;
68
68
  /**
69
- * Index of this node in it's parent or `null` if the node has no parent.
69
+ * Index of this node in its parent or `null` if the node has no parent.
70
70
  *
71
71
  * Accessing this property throws an error if this node's parent element does not contain it.
72
72
  * This means that model tree got broken.
73
73
  */
74
74
  get index(): number | null;
75
75
  /**
76
- * Offset at which this node starts in it's parent. It is equal to the sum of {@link #offsetSize offsetSize}
77
- * of all it's previous siblings. Equals to `null` if node has no parent.
76
+ * Offset at which this node starts in its parent. It is equal to the sum of {@link #offsetSize offsetSize}
77
+ * of all its previous siblings. Equals to `null` if node has no parent.
78
78
  *
79
79
  * Accessing this property throws an error if this node's parent element does not contain it.
80
80
  * This means that model tree got broken.
@@ -107,7 +107,7 @@ export default abstract class Node extends TypeCheckable {
107
107
  */
108
108
  get root(): Node | DocumentFragment;
109
109
  /**
110
- * Returns true if the node is in a tree rooted in the document (is a descendant of one of its roots).
110
+ * Returns `true` if the node is inside a document root that is attached to the document.
111
111
  */
112
112
  isAttached(): boolean;
113
113
  /**