@ckeditor/ckeditor5-engine 38.2.0-alpha.0 → 39.0.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 (37) hide show
  1. package/README.md +0 -1
  2. package/package.json +3 -4
  3. package/src/controller/editingcontroller.js +2 -2
  4. package/src/conversion/downcastdispatcher.d.ts +15 -0
  5. package/src/conversion/downcastdispatcher.js +28 -19
  6. package/src/conversion/downcasthelpers.d.ts +6 -6
  7. package/src/conversion/downcasthelpers.js +6 -6
  8. package/src/dev-utils/view.js +1 -1
  9. package/src/index.d.ts +1 -0
  10. package/src/index.js +1 -0
  11. package/src/model/differ.d.ts +14 -0
  12. package/src/model/differ.js +70 -11
  13. package/src/model/document.d.ts +9 -1
  14. package/src/model/document.js +14 -9
  15. package/src/model/documentselection.js +8 -2
  16. package/src/model/model.d.ts +0 -1
  17. package/src/model/model.js +0 -1
  18. package/src/model/operation/rootoperation.d.ts +0 -4
  19. package/src/model/operation/rootoperation.js +0 -24
  20. package/src/model/operation/transform.js +2 -2
  21. package/src/model/rootelement.d.ts +6 -0
  22. package/src/model/rootelement.js +6 -0
  23. package/src/model/schema.d.ts +10 -0
  24. package/src/model/schema.js +5 -0
  25. package/src/model/utils/autoparagraphing.js +1 -2
  26. package/src/view/domconverter.d.ts +43 -53
  27. package/src/view/domconverter.js +266 -214
  28. package/src/view/editableelement.d.ts +10 -0
  29. package/src/view/editableelement.js +1 -0
  30. package/src/view/filler.d.ts +2 -2
  31. package/src/view/filler.js +6 -4
  32. package/src/view/observer/selectionobserver.js +2 -2
  33. package/src/view/placeholder.d.ts +13 -5
  34. package/src/view/placeholder.js +21 -12
  35. package/src/view/renderer.js +1 -2
  36. package/src/view/view.d.ts +14 -7
  37. package/src/view/view.js +13 -1
package/README.md CHANGED
@@ -4,7 +4,6 @@ CKEditor 5 editing engine
4
4
  [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-engine.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5?branch=master)
6
6
  [![Build Status](https://travis-ci.com/ckeditor/ckeditor5.svg?branch=master)](https://app.travis-ci.com/github/ckeditor/ckeditor5)
7
- ![Dependency Status](https://img.shields.io/librariesio/release/npm/@ckeditor/ckeditor5-engine)
8
7
 
9
8
  The CKEditor 5 editing engine implements a flexible MVC-based architecture for creating rich text editing features.
10
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-engine",
3
- "version": "38.2.0-alpha.0",
3
+ "version": "39.0.0",
4
4
  "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
5
5
  "keywords": [
6
6
  "wysiwyg",
@@ -22,10 +22,9 @@
22
22
  "ckeditor5-dll"
23
23
  ],
24
24
  "main": "src/index.js",
25
- "type": "module",
26
25
  "dependencies": {
27
- "@ckeditor/ckeditor5-utils": "38.2.0-alpha.0",
28
- "lodash-es": "^4.17.15"
26
+ "@ckeditor/ckeditor5-utils": "39.0.0",
27
+ "lodash-es": "4.17.21"
29
28
  },
30
29
  "engines": {
31
30
  "node": ">=16.0.0",
@@ -10,7 +10,7 @@ import RootEditableElement from '../view/rooteditableelement';
10
10
  import View from '../view/view';
11
11
  import Mapper from '../conversion/mapper';
12
12
  import DowncastDispatcher from '../conversion/downcastdispatcher';
13
- import { clearAttributes, convertCollapsedSelection, convertRangeSelection, insertAttributesAndChildren, insertText, remove } from '../conversion/downcasthelpers';
13
+ import { cleanSelection, convertCollapsedSelection, convertRangeSelection, insertAttributesAndChildren, insertText, remove } from '../conversion/downcasthelpers';
14
14
  import { convertSelectionChange } from '../conversion/upcasthelpers';
15
15
  import { tryFixingRange } from '../model/utils/selection-post-fixer';
16
16
  // @if CK_DEBUG_ENGINE // const { dumpTrees, initDocumentDumping } = require( '../dev-utils/utils' );
@@ -67,7 +67,7 @@ export default class EditingController extends ObservableMixin() {
67
67
  this.downcastDispatcher.on('insert', insertAttributesAndChildren(), { priority: 'lowest' });
68
68
  this.downcastDispatcher.on('remove', remove(), { priority: 'low' });
69
69
  // Attach default model selection converters.
70
- this.downcastDispatcher.on('selection', clearAttributes(), { priority: 'high' });
70
+ this.downcastDispatcher.on('cleanSelection', cleanSelection());
71
71
  this.downcastDispatcher.on('selection', convertRangeSelection(), { priority: 'low' });
72
72
  this.downcastDispatcher.on('selection', convertCollapsedSelection(), { priority: 'low' });
73
73
  // Binds {@link module:engine/view/document~Document#roots view roots collection} to
@@ -346,6 +346,9 @@ type EventMap<TItem = Item> = {
346
346
  attributeOldValue: unknown;
347
347
  attributeNewValue: unknown;
348
348
  };
349
+ cleanSelection: {
350
+ selection: Selection | DocumentSelection;
351
+ };
349
352
  selection: {
350
353
  selection: Selection | DocumentSelection;
351
354
  };
@@ -433,6 +436,18 @@ export type DowncastAttributeEvent<TItem = Item | Selection | DocumentSelection>
433
436
  * to be used by callback, passed in `DowncastDispatcher` constructor.
434
437
  */
435
438
  export type DowncastSelectionEvent = DowncastEvent<'selection'>;
439
+ /**
440
+ * Fired at the beginning of selection conversion, before
441
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:selection selection} events.
442
+ *
443
+ * Should be used to clean up the view state at the current selection position, before the selection is moved to another place.
444
+ *
445
+ * @eventName ~DowncastDispatcher#cleanSelection
446
+ * @param {module:engine/model/selection~Selection} selection Selection that is converted.
447
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
448
+ * to be used by callback, passed in `DowncastDispatcher` constructor.
449
+ */
450
+ export type DowncastCleanSelectionEvent = DowncastEvent<'cleanSelection'>;
436
451
  /**
437
452
  * Fired when a new marker is added to the model. Also fired when a collapsed model selection that is inside a marker is converted.
438
453
  *
@@ -198,38 +198,47 @@ export default class DowncastDispatcher extends EmitterMixin() {
198
198
  * @param writer View writer that should be used to modify the view document.
199
199
  */
200
200
  convertSelection(selection, markers, writer) {
201
- const markersAtSelection = Array.from(markers.getMarkersAtPosition(selection.getFirstPosition()));
202
201
  const conversionApi = this._createConversionApi(writer);
202
+ // First perform a clean-up at the current position of the selection.
203
+ this.fire('cleanSelection', { selection }, conversionApi);
204
+ // Don't convert selection if it is in a model root that does not have a view root (for now this is only the graveyard root).
205
+ const modelRoot = selection.getFirstPosition().root;
206
+ if (!conversionApi.mapper.toViewElement(modelRoot)) {
207
+ return;
208
+ }
209
+ // Now, perform actual selection conversion.
210
+ const markersAtSelection = Array.from(markers.getMarkersAtPosition(selection.getFirstPosition()));
203
211
  this._addConsumablesForSelection(conversionApi.consumable, selection, markersAtSelection);
204
212
  this.fire('selection', { selection }, conversionApi);
205
213
  if (!selection.isCollapsed) {
206
214
  return;
207
215
  }
208
216
  for (const marker of markersAtSelection) {
209
- const markerRange = marker.getRange();
210
- if (!shouldMarkerChangeBeConverted(selection.getFirstPosition(), marker, conversionApi.mapper)) {
211
- continue;
212
- }
213
- const data = {
214
- item: selection,
215
- markerName: marker.name,
216
- markerRange
217
- };
217
+ // Do not fire event if the marker has been consumed.
218
218
  if (conversionApi.consumable.test(selection, 'addMarker:' + marker.name)) {
219
+ const markerRange = marker.getRange();
220
+ if (!shouldMarkerChangeBeConverted(selection.getFirstPosition(), marker, conversionApi.mapper)) {
221
+ continue;
222
+ }
223
+ const data = {
224
+ item: selection,
225
+ markerName: marker.name,
226
+ markerRange
227
+ };
219
228
  this.fire(`addMarker:${marker.name}`, data, conversionApi);
220
229
  }
221
230
  }
222
231
  for (const key of selection.getAttributeKeys()) {
223
- const data = {
224
- item: selection,
225
- range: selection.getFirstRange(),
226
- attributeKey: key,
227
- attributeOldValue: null,
228
- attributeNewValue: selection.getAttribute(key)
229
- };
230
232
  // Do not fire event if the attribute has been consumed.
231
- if (conversionApi.consumable.test(selection, 'attribute:' + data.attributeKey)) {
232
- this.fire(`attribute:${data.attributeKey}:$text`, data, conversionApi);
233
+ if (conversionApi.consumable.test(selection, 'attribute:' + key)) {
234
+ const data = {
235
+ item: selection,
236
+ range: selection.getFirstRange(),
237
+ attributeKey: key,
238
+ attributeOldValue: null,
239
+ attributeNewValue: selection.getAttribute(key)
240
+ };
241
+ this.fire(`attribute:${key}:$text`, data, conversionApi);
233
242
  }
234
243
  }
235
244
  }
@@ -765,7 +765,7 @@ export default class DowncastHelpers extends ConversionHelpers<DowncastDispatche
765
765
  * ```ts
766
766
  * // Using a custom function which is the same as the default conversion:
767
767
  * editor.conversion.for( 'dataDowncast' ).markerToData( {
768
- * model: 'comment'
768
+ * model: 'comment',
769
769
  * view: markerName => ( {
770
770
  * group: 'comment',
771
771
  * name: markerName.substr( 8 ) // Removes 'comment:' part.
@@ -774,7 +774,7 @@ export default class DowncastHelpers extends ConversionHelpers<DowncastDispatche
774
774
  *
775
775
  * // Using the converter priority:
776
776
  * editor.conversion.for( 'dataDowncast' ).markerToData( {
777
- * model: 'comment'
777
+ * model: 'comment',
778
778
  * view: markerName => ( {
779
779
  * group: 'comment',
780
780
  * name: markerName.substr( 8 ) // Removes 'comment:' part.
@@ -880,7 +880,7 @@ export declare function convertRangeSelection(): (evt: EventInfo, data: {
880
880
  * converted, broken attributes might be merged again, or the position where the selection is may be wrapped
881
881
  * with different, appropriate attribute elements.
882
882
  *
883
- * See also {@link module:engine/conversion/downcasthelpers~clearAttributes} which does a clean-up
883
+ * See also {@link module:engine/conversion/downcasthelpers~cleanSelection} which does a clean-up
884
884
  * by merging attributes.
885
885
  *
886
886
  * @returns Selection converter.
@@ -889,7 +889,7 @@ export declare function convertCollapsedSelection(): (evt: EventInfo, data: {
889
889
  selection: ModelSelection | ModelDocumentSelection;
890
890
  }, conversionApi: DowncastConversionApi) => void;
891
891
  /**
892
- * Function factory that creates a converter which clears artifacts after the previous
892
+ * Function factory that creates a converter which cleans artifacts after the previous
893
893
  * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty
894
894
  * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merges sibling attributes at all start and end
895
895
  * positions of all ranges.
@@ -908,7 +908,7 @@ export declare function convertCollapsedSelection(): (evt: EventInfo, data: {
908
908
  * This listener should be assigned before any converter for the new selection:
909
909
  *
910
910
  * ```ts
911
- * modelDispatcher.on( 'selection', clearAttributes() );
911
+ * modelDispatcher.on( 'cleanSelection', cleanSelection() );
912
912
  * ```
913
913
  *
914
914
  * See {@link module:engine/conversion/downcasthelpers~convertCollapsedSelection}
@@ -916,7 +916,7 @@ export declare function convertCollapsedSelection(): (evt: EventInfo, data: {
916
916
  *
917
917
  * @returns Selection converter.
918
918
  */
919
- export declare function clearAttributes(): (evt: EventInfo, data: unknown, conversionApi: DowncastConversionApi) => void;
919
+ export declare function cleanSelection(): (evt: EventInfo, data: unknown, conversionApi: DowncastConversionApi) => void;
920
920
  /**
921
921
  * Function factory that creates a converter which converts the set/change/remove attribute changes from the model to the view.
922
922
  * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the
@@ -715,7 +715,7 @@ export default class DowncastHelpers extends ConversionHelpers {
715
715
  * ```ts
716
716
  * // Using a custom function which is the same as the default conversion:
717
717
  * editor.conversion.for( 'dataDowncast' ).markerToData( {
718
- * model: 'comment'
718
+ * model: 'comment',
719
719
  * view: markerName => ( {
720
720
  * group: 'comment',
721
721
  * name: markerName.substr( 8 ) // Removes 'comment:' part.
@@ -724,7 +724,7 @@ export default class DowncastHelpers extends ConversionHelpers {
724
724
  *
725
725
  * // Using the converter priority:
726
726
  * editor.conversion.for( 'dataDowncast' ).markerToData( {
727
- * model: 'comment'
727
+ * model: 'comment',
728
728
  * view: markerName => ( {
729
729
  * group: 'comment',
730
730
  * name: markerName.substr( 8 ) // Removes 'comment:' part.
@@ -876,7 +876,7 @@ export function convertRangeSelection() {
876
876
  * converted, broken attributes might be merged again, or the position where the selection is may be wrapped
877
877
  * with different, appropriate attribute elements.
878
878
  *
879
- * See also {@link module:engine/conversion/downcasthelpers~clearAttributes} which does a clean-up
879
+ * See also {@link module:engine/conversion/downcasthelpers~cleanSelection} which does a clean-up
880
880
  * by merging attributes.
881
881
  *
882
882
  * @returns Selection converter.
@@ -898,7 +898,7 @@ export function convertCollapsedSelection() {
898
898
  };
899
899
  }
900
900
  /**
901
- * Function factory that creates a converter which clears artifacts after the previous
901
+ * Function factory that creates a converter which cleans artifacts after the previous
902
902
  * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty
903
903
  * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merges sibling attributes at all start and end
904
904
  * positions of all ranges.
@@ -917,7 +917,7 @@ export function convertCollapsedSelection() {
917
917
  * This listener should be assigned before any converter for the new selection:
918
918
  *
919
919
  * ```ts
920
- * modelDispatcher.on( 'selection', clearAttributes() );
920
+ * modelDispatcher.on( 'cleanSelection', cleanSelection() );
921
921
  * ```
922
922
  *
923
923
  * See {@link module:engine/conversion/downcasthelpers~convertCollapsedSelection}
@@ -925,7 +925,7 @@ export function convertCollapsedSelection() {
925
925
  *
926
926
  * @returns Selection converter.
927
927
  */
928
- export function clearAttributes() {
928
+ export function cleanSelection() {
929
929
  return (evt, data, conversionApi) => {
930
930
  const viewWriter = conversionApi.writer;
931
931
  const viewSelection = viewWriter.document.selection;
@@ -820,7 +820,7 @@ class ViewStringify {
820
820
  else if (attribute === 'style') {
821
821
  attributeValue = [...element.getStyleNames()]
822
822
  .sort()
823
- .map(style => `${style}:${element.getStyle(style)}`)
823
+ .map(style => `${style}:${element.getStyle(style).replace(/"/g, '&quot;')}`)
824
824
  .join(';');
825
825
  }
826
826
  else {
package/src/index.d.ts CHANGED
@@ -89,6 +89,7 @@ export { default as ClickObserver } from './view/observer/clickobserver';
89
89
  export { default as DomEventObserver } from './view/observer/domeventobserver';
90
90
  export { default as MouseObserver } from './view/observer/mouseobserver';
91
91
  export { default as TabObserver } from './view/observer/tabobserver';
92
+ export { default as FocusObserver } from './view/observer/focusobserver';
92
93
  export { default as DowncastWriter } from './view/downcastwriter';
93
94
  export { default as UpcastWriter } from './view/upcastwriter';
94
95
  export { default as Matcher, type MatcherPattern, type MatcherObjectPattern, type Match, type MatchResult } from './view/matcher';
package/src/index.js CHANGED
@@ -63,6 +63,7 @@ export { default as ClickObserver } from './view/observer/clickobserver';
63
63
  export { default as DomEventObserver } from './view/observer/domeventobserver';
64
64
  export { default as MouseObserver } from './view/observer/mouseobserver';
65
65
  export { default as TabObserver } from './view/observer/tabobserver';
66
+ export { default as FocusObserver } from './view/observer/focusobserver';
66
67
  export { default as DowncastWriter } from './view/downcastwriter';
67
68
  export { default as UpcastWriter } from './view/upcastwriter';
68
69
  export { default as Matcher } from './view/matcher';
@@ -9,6 +9,7 @@ import Position from './position';
9
9
  import Range from './range';
10
10
  import type { default as MarkerCollection, MarkerData } from './markercollection';
11
11
  import type Item from './item';
12
+ import type RootElement from './rootelement';
12
13
  import type Operation from './operation/operation';
13
14
  /**
14
15
  * Calculates the difference between two model states.
@@ -191,6 +192,19 @@ export default class Differ {
191
192
  * @param item Item to refresh.
192
193
  */
193
194
  _refreshItem(item: Item): void;
195
+ /**
196
+ * Buffers all the data related to given root like it was all just added to the editor.
197
+ *
198
+ * Following changes are buffered:
199
+ *
200
+ * * root is attached,
201
+ * * all root content is inserted,
202
+ * * all root attributes are added,
203
+ * * all markers inside the root are added.
204
+ *
205
+ * @internal
206
+ */
207
+ _bufferRootLoad(root: RootElement): void;
194
208
  /**
195
209
  * Saves and handles an insert change.
196
210
  */
@@ -94,6 +94,9 @@ export default class Differ {
94
94
  // Marking changes in them would cause a "double" changing then.
95
95
  //
96
96
  const operation = operationToBuffer;
97
+ // Note: an operation that happens inside a non-loaded root will be ignored. If the operation happens partially inside
98
+ // a non-loaded root, that part will be ignored (this may happen for move or marker operations).
99
+ //
97
100
  switch (operation.type) {
98
101
  case 'insert': {
99
102
  if (this._isInInsertedElement(operation.position.parent)) {
@@ -179,12 +182,23 @@ export default class Differ {
179
182
  }
180
183
  case 'detachRoot':
181
184
  case 'addRoot': {
185
+ const root = operation.affectedSelectable;
186
+ if (!root._isLoaded) {
187
+ return;
188
+ }
189
+ // Don't buffer if the root state does not change.
190
+ if (root.isAttached() == operation.isAdd) {
191
+ return;
192
+ }
182
193
  this._bufferRootStateChange(operation.rootName, operation.isAdd);
183
194
  break;
184
195
  }
185
196
  case 'addRootAttribute':
186
197
  case 'removeRootAttribute':
187
198
  case 'changeRootAttribute': {
199
+ if (!operation.root._isLoaded) {
200
+ return;
201
+ }
188
202
  const rootName = operation.root.rootName;
189
203
  this._bufferRootAttributeChange(rootName, operation.key, operation.oldValue, operation.newValue);
190
204
  break;
@@ -201,20 +215,24 @@ export default class Differ {
201
215
  * @param newMarkerData Marker data after the change.
202
216
  */
203
217
  bufferMarkerChange(markerName, oldMarkerData, newMarkerData) {
204
- const buffered = this._changedMarkers.get(markerName);
218
+ if (oldMarkerData.range && oldMarkerData.range.root.is('rootElement') && !oldMarkerData.range.root._isLoaded) {
219
+ oldMarkerData.range = null;
220
+ }
221
+ if (newMarkerData.range && newMarkerData.range.root.is('rootElement') && !newMarkerData.range.root._isLoaded) {
222
+ newMarkerData.range = null;
223
+ }
224
+ let buffered = this._changedMarkers.get(markerName);
205
225
  if (!buffered) {
206
- this._changedMarkers.set(markerName, {
207
- newMarkerData,
208
- oldMarkerData
209
- });
226
+ buffered = { newMarkerData, oldMarkerData };
227
+ this._changedMarkers.set(markerName, buffered);
210
228
  }
211
229
  else {
212
230
  buffered.newMarkerData = newMarkerData;
213
- if (buffered.oldMarkerData.range == null && newMarkerData.range == null) {
214
- // The marker is going to be removed (`newMarkerData.range == null`) but it did not exist before the first buffered change
215
- // (`buffered.oldMarkerData.range == null`). In this case, do not keep the marker in buffer at all.
216
- this._changedMarkers.delete(markerName);
217
- }
231
+ }
232
+ if (buffered.oldMarkerData.range == null && newMarkerData.range == null) {
233
+ // The marker is going to be removed (`newMarkerData.range == null`) but it did not exist before the first buffered change
234
+ // (`buffered.oldMarkerData.range == null`). In this case, do not keep the marker in buffer at all.
235
+ this._changedMarkers.delete(markerName);
218
236
  }
219
237
  }
220
238
  /**
@@ -496,7 +514,7 @@ export default class Differ {
496
514
  }
497
515
  const diffItem = this._changedRoots.get(rootName);
498
516
  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,
517
+ // Root `state` can only toggle between one of the values and no value. It cannot be any other way,
500
518
  // because if the root was originally attached it can only become detached. Then, if it is re-attached in the same batch of
501
519
  // changes, it gets back to "no change" (which means no value). Same if the root was originally detached.
502
520
  delete diffItem.state;
@@ -567,10 +585,45 @@ export default class Differ {
567
585
  // Clear cache after each buffered operation as it is no longer valid.
568
586
  this._cachedChanges = null;
569
587
  }
588
+ /**
589
+ * Buffers all the data related to given root like it was all just added to the editor.
590
+ *
591
+ * Following changes are buffered:
592
+ *
593
+ * * root is attached,
594
+ * * all root content is inserted,
595
+ * * all root attributes are added,
596
+ * * all markers inside the root are added.
597
+ *
598
+ * @internal
599
+ */
600
+ _bufferRootLoad(root) {
601
+ if (!root.isAttached()) {
602
+ return;
603
+ }
604
+ this._bufferRootStateChange(root.rootName, true);
605
+ this._markInsert(root, 0, root.maxOffset);
606
+ // Buffering root attribute changes makes sense and is actually needed, even though we buffer root state change above.
607
+ // Because the root state change is buffered, the root attributes changes are not returned by the differ.
608
+ // But, if the root attribute is removed in the same change block, or the root is detached, then the differ results would be wrong.
609
+ //
610
+ for (const key of root.getAttributeKeys()) {
611
+ this._bufferRootAttributeChange(root.rootName, key, null, root.getAttribute(key));
612
+ }
613
+ for (const marker of this._markerCollection) {
614
+ if (marker.getRange().root == root) {
615
+ const markerData = marker.getData();
616
+ this.bufferMarkerChange(marker.name, { ...markerData, range: null }, markerData);
617
+ }
618
+ }
619
+ }
570
620
  /**
571
621
  * Saves and handles an insert change.
572
622
  */
573
623
  _markInsert(parent, offset, howMany) {
624
+ if (parent.root.is('rootElement') && !parent.root._isLoaded) {
625
+ return;
626
+ }
574
627
  const changeItem = { type: 'insert', offset, howMany, count: this._changeCount++ };
575
628
  this._markChange(parent, changeItem);
576
629
  }
@@ -578,6 +631,9 @@ export default class Differ {
578
631
  * Saves and handles a remove change.
579
632
  */
580
633
  _markRemove(parent, offset, howMany) {
634
+ if (parent.root.is('rootElement') && !parent.root._isLoaded) {
635
+ return;
636
+ }
581
637
  const changeItem = { type: 'remove', offset, howMany, count: this._changeCount++ };
582
638
  this._markChange(parent, changeItem);
583
639
  this._removeAllNestedChanges(parent, offset, howMany);
@@ -586,6 +642,9 @@ export default class Differ {
586
642
  * Saves and handles an attribute change.
587
643
  */
588
644
  _markAttribute(item) {
645
+ if (item.root.is('rootElement') && !item.root._isLoaded) {
646
+ return;
647
+ }
589
648
  const changeItem = { type: 'attribute', offset: item.startOffset, howMany: item.offsetSize, count: this._changeCount++ };
590
649
  this._markChange(item.parent, changeItem);
591
650
  }
@@ -125,9 +125,17 @@ export default class Document extends Document_base {
125
125
  * on the document data know which roots are still a part of the document and should be processed.
126
126
  *
127
127
  * @param includeDetached Specified whether detached roots should be returned as well.
128
- * @returns Roots names.
129
128
  */
130
129
  getRootNames(includeDetached?: boolean): Array<string>;
130
+ /**
131
+ * Returns an array with all roots added to the document (except the {@link #graveyard graveyard root}).
132
+ *
133
+ * Detached roots **are not** returned by this method by default. This is to make sure that all features or algorithms that operate
134
+ * on the document data know which roots are still a part of the document and should be processed.
135
+ *
136
+ * @param includeDetached Specified whether detached roots should be returned as well.
137
+ */
138
+ getRoots(includeDetached?: boolean): Array<RootElement>;
131
139
  /**
132
140
  * Used to register a post-fixer callback. A post-fixer mechanism guarantees that the features
133
141
  * will operate on a correct model state.
@@ -179,12 +179,21 @@ export default class Document extends EmitterMixin() {
179
179
  * on the document data know which roots are still a part of the document and should be processed.
180
180
  *
181
181
  * @param includeDetached Specified whether detached roots should be returned as well.
182
- * @returns Roots names.
183
182
  */
184
183
  getRootNames(includeDetached = false) {
184
+ return this.getRoots(includeDetached).map(root => root.rootName);
185
+ }
186
+ /**
187
+ * Returns an array with all roots added to the document (except the {@link #graveyard graveyard root}).
188
+ *
189
+ * Detached roots **are not** returned by this method by default. This is to make sure that all features or algorithms that operate
190
+ * on the document data know which roots are still a part of the document and should be processed.
191
+ *
192
+ * @param includeDetached Specified whether detached roots should be returned as well.
193
+ */
194
+ getRoots(includeDetached = false) {
185
195
  return Array.from(this.roots)
186
- .filter(root => root.rootName != graveyardName && (includeDetached || root.isAttached()))
187
- .map(root => root.rootName);
196
+ .filter(root => root != this.graveyard && (includeDetached || root.isAttached()) && root._isLoaded);
188
197
  }
189
198
  /**
190
199
  * Used to register a post-fixer callback. A post-fixer mechanism guarantees that the features
@@ -283,12 +292,8 @@ export default class Document extends EmitterMixin() {
283
292
  * @returns The default root for this document.
284
293
  */
285
294
  _getDefaultRoot() {
286
- for (const root of this.roots) {
287
- if (root !== this.graveyard) {
288
- return root;
289
- }
290
- }
291
- return this.graveyard;
295
+ const roots = this.getRoots();
296
+ return roots.length ? roots[0] : this.graveyard;
292
297
  }
293
298
  /**
294
299
  * Returns the default range for this selection. The default range is a collapsed range that starts and ends
@@ -866,14 +866,19 @@ class LiveSelection extends Selection {
866
866
  _getSurroundingAttributes() {
867
867
  const position = this.getFirstPosition();
868
868
  const schema = this._model.schema;
869
+ if (position.root.rootName == '$graveyard') {
870
+ return null;
871
+ }
869
872
  let attrs = null;
870
873
  if (!this.isCollapsed) {
871
874
  // 1. If selection is a range...
872
875
  const range = this.getFirstRange();
873
876
  // ...look for a first character node in that range and take attributes from it.
874
877
  for (const value of range) {
875
- // If the item is an object, we don't want to get attributes from its children.
878
+ // If the item is an object, we don't want to get attributes from its children...
876
879
  if (value.item.is('element') && schema.isObject(value.item)) {
880
+ // ...but collect attributes from inline object.
881
+ attrs = getTextAttributes(value.item, schema);
877
882
  break;
878
883
  }
879
884
  if (value.type == 'text') {
@@ -957,7 +962,8 @@ function getTextAttributes(node, schema) {
957
962
  const attributes = [];
958
963
  // Collect all attributes that can be applied to the text node.
959
964
  for (const [key, value] of node.getAttributes()) {
960
- if (schema.checkAttribute('$text', key)) {
965
+ if (schema.checkAttribute('$text', key) &&
966
+ schema.getAttributeProperties(key).copyFromObject !== false) {
961
967
  attributes.push([key, value]);
962
968
  }
963
969
  }
@@ -769,7 +769,6 @@ export default class Model extends Model_base {
769
769
  /**
770
770
  * Common part of {@link module:engine/model/model~Model#change} and {@link module:engine/model/model~Model#enqueueChange}
771
771
  * which calls callbacks and returns array of values returned by these callbacks.
772
- *
773
772
  */
774
773
  private _runPendingChanges;
775
774
  }
@@ -792,7 +792,6 @@ export default class Model extends ObservableMixin() {
792
792
  /**
793
793
  * Common part of {@link module:engine/model/model~Model#change} and {@link module:engine/model/model~Model#enqueueChange}
794
794
  * which calls callbacks and returns array of values returned by these callbacks.
795
- *
796
795
  */
797
796
  _runPendingChanges() {
798
797
  const ret = [];
@@ -54,10 +54,6 @@ export default class RootOperation extends Operation {
54
54
  * @inheritDoc
55
55
  */
56
56
  getReversed(): RootOperation;
57
- /**
58
- * @inheritDoc
59
- */
60
- _validate(): void;
61
57
  /**
62
58
  * @inheritDoc
63
59
  */
@@ -6,7 +6,6 @@
6
6
  * @module engine/model/operation/rootoperation
7
7
  */
8
8
  import Operation from './operation';
9
- import { CKEditorError } from '@ckeditor/ckeditor5-utils';
10
9
  /**
11
10
  * Operation that creates (or attaches) or detaches a root element.
12
11
  */
@@ -59,29 +58,6 @@ export default class RootOperation extends Operation {
59
58
  getReversed() {
60
59
  return new RootOperation(this.rootName, this.elementName, !this.isAdd, this._document, this.baseVersion + 1);
61
60
  }
62
- /**
63
- * @inheritDoc
64
- */
65
- _validate() {
66
- // Keep in mind that at this point the root will always exist as it was created in the `constructor()`, even for detach operation.
67
- const root = this._document.getRoot(this.rootName);
68
- if (root.isAttached() && this.isAdd) {
69
- /**
70
- * Trying to attach a root that is already attached.
71
- *
72
- * @error root-operation-root-attached
73
- */
74
- throw new CKEditorError('root-operation-root-attached', this);
75
- }
76
- else if (!root.isAttached() && !this.isAdd) {
77
- /**
78
- * Trying to detach a root that is already detached.
79
- *
80
- * @error root-operation-root-detached
81
- */
82
- throw new CKEditorError('root-operation-root-detached', this);
83
- }
84
- }
85
61
  /**
86
62
  * @inheritDoc
87
63
  */
@@ -1641,8 +1641,8 @@ setTransformation(RootAttributeOperation, RootAttributeOperation, (a, b, context
1641
1641
  return [a];
1642
1642
  });
1643
1643
  // -----------------------
1644
- setTransformation(RootOperation, RootOperation, (a, b, context) => {
1645
- if (a.rootName === b.rootName && a.isAdd === b.isAdd && !context.bWasUndone) {
1644
+ setTransformation(RootOperation, RootOperation, (a, b) => {
1645
+ if (a.rootName === b.rootName && a.isAdd === b.isAdd) {
1646
1646
  return [new NoOperation(0)];
1647
1647
  }
1648
1648
  return [a];
@@ -23,6 +23,12 @@ export default class RootElement extends Element {
23
23
  * @internal
24
24
  */
25
25
  _isAttached: boolean;
26
+ /**
27
+ * Informs if the root element is loaded (default).
28
+ *
29
+ * @internal
30
+ */
31
+ _isLoaded: boolean;
26
32
  /**
27
33
  * Creates root element.
28
34
  *