@ckeditor/ckeditor5-engine 32.0.0 → 34.1.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 (43) hide show
  1. package/LICENSE.md +2 -2
  2. package/README.md +2 -1
  3. package/package.json +22 -22
  4. package/src/controller/datacontroller.js +58 -66
  5. package/src/controller/editingcontroller.js +82 -5
  6. package/src/conversion/conversion.js +14 -13
  7. package/src/conversion/downcastdispatcher.js +297 -366
  8. package/src/conversion/downcasthelpers.js +859 -80
  9. package/src/conversion/mapper.js +104 -59
  10. package/src/conversion/modelconsumable.js +84 -34
  11. package/src/conversion/upcastdispatcher.js +33 -6
  12. package/src/conversion/upcasthelpers.js +21 -2
  13. package/src/dataprocessor/htmldataprocessor.js +1 -1
  14. package/src/dev-utils/model.js +13 -11
  15. package/src/index.js +12 -0
  16. package/src/model/batch.js +12 -12
  17. package/src/model/differ.js +114 -68
  18. package/src/model/document.js +32 -31
  19. package/src/model/history.js +160 -22
  20. package/src/model/markercollection.js +28 -4
  21. package/src/model/model.js +122 -4
  22. package/src/model/schema.js +79 -10
  23. package/src/model/treewalker.js +2 -3
  24. package/src/model/utils/deletecontent.js +15 -2
  25. package/src/model/utils/findoptimalinsertionrange.js +68 -0
  26. package/src/model/utils/insertobject.js +173 -0
  27. package/src/model/utils/modifyselection.js +14 -7
  28. package/src/model/writer.js +16 -26
  29. package/src/view/attributeelement.js +0 -10
  30. package/src/view/document.js +2 -1
  31. package/src/view/domconverter.js +47 -11
  32. package/src/view/downcastwriter.js +90 -49
  33. package/src/view/element.js +0 -27
  34. package/src/view/emptyelement.js +0 -3
  35. package/src/view/matcher.js +2 -2
  36. package/src/view/observer/clickobserver.js +0 -1
  37. package/src/view/observer/inputobserver.js +1 -1
  38. package/src/view/observer/tabobserver.js +68 -0
  39. package/src/view/placeholder.js +1 -1
  40. package/src/view/rawelement.js +0 -3
  41. package/src/view/uielement.js +0 -3
  42. package/src/view/view.js +4 -0
  43. package/theme/placeholder.css +9 -0
@@ -40,10 +40,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
40
40
  * The third parameter of the callback is an instance of {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi}
41
41
  * which provides additional tools for converters.
42
42
  *
43
- * You can read more about conversion in the following guides:
44
- *
45
- * * {@glink framework/guides/deep-dive/conversion/conversion-introduction Advanced conversion concepts — attributes}
46
- * * {@glink framework/guides/deep-dive/conversion/custom-element-conversion Custom element conversion}
43
+ * You can read more about conversion in the {@glink framework/guides/deep-dive/conversion/upcast Upcast conversion} guide.
47
44
  *
48
45
  * Examples of event-based converters:
49
46
  *
@@ -127,7 +124,7 @@ export default class UpcastDispatcher {
127
124
  /**
128
125
  * The list of elements that were created during splitting.
129
126
  *
130
- * After the conversion process the list is cleared.
127
+ * After the conversion process, the list is cleared.
131
128
  *
132
129
  * @private
133
130
  * @type {Map.<module:engine/model/element~Element,Array.<module:engine/model/element~Element>>}
@@ -154,6 +151,16 @@ export default class UpcastDispatcher {
154
151
  */
155
152
  this._modelCursor = null;
156
153
 
154
+ /**
155
+ * The list of elements that were created during the splitting but should not get removed on conversion end even if they are empty.
156
+ *
157
+ * The list is cleared after the conversion process.
158
+ *
159
+ * @private
160
+ * @type {Set.<module:engine/model/element~Element>}
161
+ */
162
+ this._emptyElementsToKeep = new Set();
163
+
157
164
  /**
158
165
  * An interface passed by the dispatcher to the event callbacks.
159
166
  *
@@ -170,6 +177,7 @@ export default class UpcastDispatcher {
170
177
  // Advanced API - use only if custom position handling is needed.
171
178
  this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
172
179
  this.conversionApi.getSplitParts = this._getSplitParts.bind( this );
180
+ this.conversionApi.keepEmptyElement = this._keepEmptyElement.bind( this );
173
181
  }
174
182
 
175
183
  /**
@@ -229,6 +237,7 @@ export default class UpcastDispatcher {
229
237
  // Clear split elements & parents lists.
230
238
  this._splitParts.clear();
231
239
  this._cursorParents.clear();
240
+ this._emptyElementsToKeep.clear();
232
241
 
233
242
  // Clear conversion API.
234
243
  this.conversionApi.writer = null;
@@ -454,6 +463,15 @@ export default class UpcastDispatcher {
454
463
  return parts;
455
464
  }
456
465
 
466
+ /**
467
+ * Mark an element that were created during the splitting to not get removed on conversion end even if it is empty.
468
+ *
469
+ * @private
470
+ */
471
+ _keepEmptyElement( element ) {
472
+ this._emptyElementsToKeep.add( element );
473
+ }
474
+
457
475
  /**
458
476
  * Checks if there are any empty elements created while splitting and removes them.
459
477
  *
@@ -466,7 +484,7 @@ export default class UpcastDispatcher {
466
484
  let anyRemoved = false;
467
485
 
468
486
  for ( const element of this._splitParts.keys() ) {
469
- if ( element.isEmpty ) {
487
+ if ( element.isEmpty && !this._emptyElementsToKeep.has( element ) ) {
470
488
  this.conversionApi.writer.remove( element );
471
489
  this._splitParts.delete( element );
472
490
 
@@ -760,6 +778,15 @@ function createContextTree( contextDefinition, writer ) {
760
778
  * @returns {Array.<module:engine/model/element~Element>}
761
779
  */
762
780
 
781
+ /**
782
+ * Mark an element that was created during splitting to not get removed on conversion end even if it is empty.
783
+ *
784
+ * **Note:** This is an advanced method. For most cases you will not need to keep the split empty element.
785
+ *
786
+ * @method #keepEmptyElement
787
+ * @param {module:engine/model/element~Element} element
788
+ */
789
+
763
790
  /**
764
791
  * Stores information about what parts of the processed view item are still waiting to be handled. After a piece of view item
765
792
  * was converted, an appropriate consumable value should be
@@ -12,7 +12,7 @@ import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
12
12
  import { isParagraphable, wrapInParagraph } from '../model/utils/autoparagraphing';
13
13
 
14
14
  /**
15
- * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
15
+ * Contains the {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
16
16
  * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}.
17
17
  *
18
18
  * @module engine/conversion/upcasthelpers
@@ -21,6 +21,8 @@ import { isParagraphable, wrapInParagraph } from '../model/utils/autoparagraphin
21
21
  /**
22
22
  * Upcast conversion helper functions.
23
23
  *
24
+ * Learn more about {@glink framework/guides/deep-dive/conversion/upcast upcast helpers}.
25
+ *
24
26
  * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
25
27
  */
26
28
  export default class UpcastHelpers extends ConversionHelpers {
@@ -865,6 +867,13 @@ function prepareToAttributeConverter( config, shallow ) {
865
867
  const matcher = new Matcher( config.view );
866
868
 
867
869
  return ( evt, data, conversionApi ) => {
870
+ // Converting an attribute of an element that has not been converted to anything does not make sense
871
+ // because there will be nowhere to set that attribute on. At this stage, the element should've already
872
+ // been converted (https://github.com/ckeditor/ckeditor5/issues/11000).
873
+ if ( !data.modelRange && shallow ) {
874
+ return;
875
+ }
876
+
868
877
  const match = matcher.match( data.viewItem );
869
878
 
870
879
  // If there is no match, this callback should not do anything.
@@ -875,7 +884,8 @@ function prepareToAttributeConverter( config, shallow ) {
875
884
  if ( onlyViewNameIsDefined( config.view, data.viewItem ) ) {
876
885
  match.match.name = true;
877
886
  } else {
878
- // Do not test or consume `name` consumable.
887
+ // Do not test `name` consumable because it could get consumed already while upcasting some other attribute
888
+ // on the same element (for example <span class="big" style="color: red">foo</span>).
879
889
  delete match.match.name;
880
890
  }
881
891
 
@@ -906,6 +916,15 @@ function prepareToAttributeConverter( config, shallow ) {
906
916
  // It may happen that a converter will try to set an attribute that is not allowed in the given context.
907
917
  // In such a situation we cannot consume the attribute. See: https://github.com/ckeditor/ckeditor5/pull/9249#issuecomment-815658459.
908
918
  if ( attributeWasSet ) {
919
+ // Verify if the element itself wasn't consumed yet. It could be consumed already while upcasting some other attribute
920
+ // on the same element (for example <span class="big" style="color: red">foo</span>).
921
+ // We need to consume it so other features (especially GHS) won't try to convert it.
922
+ // Note that it's not tested by the other element-to-attribute converters whether an element was consumed before
923
+ // (in case of converters that the element itself is just a context and not the primary information to convert).
924
+ if ( conversionApi.consumable.test( data.viewItem, { name: true } ) ) {
925
+ match.match.name = true;
926
+ }
927
+
909
928
  conversionApi.consumable.consume( data.viewItem, match.match );
910
929
  }
911
930
  };
@@ -114,7 +114,7 @@ export default class HtmlDataProcessor {
114
114
  * @returns {DocumentFragment}
115
115
  */
116
116
  _toDom( data ) {
117
- // Wrap data with a <body> so leading non-layout nodes (like <script>, <style>, HTML comment)
117
+ // Wrap data with a <body> tag so leading non-layout nodes (like <script>, <style>, HTML comment)
118
118
  // will be preserved in the body collection.
119
119
  // Do it only for data that is not a full HTML document.
120
120
  if ( !data.match( /<(?:html|body|head|meta)(?:\s[^>]*)?>/i ) ) {
@@ -31,6 +31,7 @@ import Mapper from '../conversion/mapper';
31
31
  import {
32
32
  convertCollapsedSelection,
33
33
  convertRangeSelection,
34
+ insertAttributesAndChildren,
34
35
  insertElement,
35
36
  insertText,
36
37
  insertUIElement,
@@ -233,6 +234,7 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu
233
234
  mapper.bindElements( node.root, viewRoot );
234
235
 
235
236
  downcastDispatcher.on( 'insert:$text', insertText() );
237
+ downcastDispatcher.on( 'insert', insertAttributesAndChildren(), { priority: 'lowest' } );
236
238
  downcastDispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
237
239
  if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection || data.item.is( '$textProxy' ) ) {
238
240
  const converter = wrap( ( modelAttributeValue, { writer } ) => {
@@ -260,28 +262,28 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu
260
262
  return writer.createUIElement( name );
261
263
  } ) );
262
264
 
265
+ const markersMap = new Map();
266
+
267
+ if ( markers ) {
268
+ // To provide stable results, sort markers by name.
269
+ for ( const marker of Array.from( markers ).sort( ( a, b ) => a.name < b.name ? 1 : -1 ) ) {
270
+ markersMap.set( marker.name, marker.getRange() );
271
+ }
272
+ }
273
+
263
274
  // Convert model to view.
264
275
  const writer = view._writer;
265
- downcastDispatcher.convertInsert( range, writer );
276
+ downcastDispatcher.convert( range, markersMap, writer );
266
277
 
267
278
  // Convert model selection to view selection.
268
279
  if ( selection ) {
269
280
  downcastDispatcher.convertSelection( selection, markers || model.markers, writer );
270
281
  }
271
282
 
272
- if ( markers ) {
273
- // To provide stable results, sort markers by name.
274
- markers = Array.from( markers ).sort( ( a, b ) => a.name < b.name ? 1 : -1 );
275
-
276
- for ( const marker of markers ) {
277
- downcastDispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
278
- }
279
- }
280
-
281
283
  // Parse view to data string.
282
284
  let data = viewStringify( viewRoot, viewDocument.selection, { sameSelectionCharacters: true } );
283
285
 
284
- // Removing unneccessary <div> and </div> added because `viewRoot` was also stringified alongside input data.
286
+ // Removing unnecessary <div> and </div> added because `viewRoot` was also stringified alongside input data.
285
287
  data = data.substr( 5, data.length - 11 );
286
288
 
287
289
  view.destroy();
package/src/index.js CHANGED
@@ -28,10 +28,22 @@ export { default as LivePosition } from './model/liveposition';
28
28
  export { default as Model } from './model/model';
29
29
  export { default as TreeWalker } from './model/treewalker';
30
30
  export { default as Element } from './model/element';
31
+ export { default as Position } from './model/position';
32
+ export { default as DocumentFragment } from './model/documentfragment';
33
+ export { default as History } from './model/history';
34
+ export { default as Text } from './model/text';
31
35
 
32
36
  export { default as DomConverter } from './view/domconverter';
33
37
  export { default as Renderer } from './view/renderer';
34
38
  export { default as ViewDocument } from './view/document';
39
+ export { default as ViewText } from './view/text';
40
+ export { default as ViewElement } from './view/element';
41
+ export { default as ViewContainerElement } from './view/containerelement';
42
+ export { default as ViewAttributeElement } from './view/attributeelement';
43
+ export { default as ViewEmptyElement } from './view/emptyelement';
44
+ export { default as ViewRawElement } from './view/rawelement';
45
+ export { default as ViewUIElement } from './view/uielement';
46
+ export { default as ViewDocumentFragment } from './view/documentfragment';
35
47
 
36
48
  export { getFillerOffset } from './view/containerelement';
37
49
  export { default as Observer } from './view/observer/observer';
@@ -27,20 +27,20 @@ export default class Batch {
27
27
  *
28
28
  * @see module:engine/model/model~Model#enqueueChange
29
29
  * @see module:engine/model/model~Model#change
30
- * @param {Object} [type] Set of flags that specifies the type of the batch. Batch type can alter how some of the features work when
31
- * encountering given `Batch` instance (for example, when a feature listens to applied operations).
32
- * @param {Boolean} [type.isUndoable=true] Whether batch can be undone through undo feature.
33
- * @param {Boolean} [type.isLocal=true] Whether batch includes operations created locally (`true`) or operations created on
30
+ * @param {Object} [type] A set of flags that specify the type of the batch. Batch type can alter how some of the features work
31
+ * when encountering a given `Batch` instance (for example, when a feature listens to applied operations).
32
+ * @param {Boolean} [type.isUndoable=true] Whether a batch can be undone through undo feature.
33
+ * @param {Boolean} [type.isLocal=true] Whether a batch includes operations created locally (`true`) or operations created on
34
34
  * other, remote editors (`false`).
35
- * @param {Boolean} [type.isUndo=false] Whether batch was created by the undo feature and undoes other operations.
36
- * @param {Boolean} [type.isTyping=false] Whether batch includes operations connected with typing action.
35
+ * @param {Boolean} [type.isUndo=false] Whether a batch was created by the undo feature and undoes other operations.
36
+ * @param {Boolean} [type.isTyping=false] Whether a batch includes operations connected with a typing action.
37
37
  */
38
38
  constructor( type = {} ) {
39
39
  if ( typeof type === 'string' ) {
40
40
  type = type === 'transparent' ? { isUndoable: false } : {};
41
41
 
42
42
  /**
43
- * The string value for `type` property of the `Batch` constructor has been deprecated and will be removed in the near future.
43
+ * The string value for a `type` property of the `Batch` constructor has been deprecated and will be removed in the near future.
44
44
  * Please refer to the {@link module:engine/model/batch~Batch#constructor `Batch` constructor API documentation} for more
45
45
  * information.
46
46
  *
@@ -60,7 +60,7 @@ export default class Batch {
60
60
  this.operations = [];
61
61
 
62
62
  /**
63
- * Whether batch can be undone through the undo feature.
63
+ * Whether the batch can be undone through the undo feature.
64
64
  *
65
65
  * @readonly
66
66
  * @type {Boolean}
@@ -68,7 +68,7 @@ export default class Batch {
68
68
  this.isUndoable = isUndoable;
69
69
 
70
70
  /**
71
- * Whether batch includes operations created locally (`true`) or operations created on other, remote editors (`false`).
71
+ * Whether the batch includes operations created locally (`true`) or operations created on other, remote editors (`false`).
72
72
  *
73
73
  * @readonly
74
74
  * @type {Boolean}
@@ -76,7 +76,7 @@ export default class Batch {
76
76
  this.isLocal = isLocal;
77
77
 
78
78
  /**
79
- * Whether batch was created by the undo feature and undoes other operations.
79
+ * Whether the batch was created by the undo feature and undoes other operations.
80
80
  *
81
81
  * @readonly
82
82
  * @type {Boolean}
@@ -84,7 +84,7 @@ export default class Batch {
84
84
  this.isUndo = isUndo;
85
85
 
86
86
  /**
87
- * Whether batch includes operations connected with typing.
87
+ * Whether the batch includes operations connected with typing.
88
88
  *
89
89
  * @readonly
90
90
  * @type {Boolean}
@@ -95,7 +95,7 @@ export default class Batch {
95
95
  /**
96
96
  * The type of the batch.
97
97
  *
98
- * **This property has been deprecated and is always set to `'default'` value.**
98
+ * **This property has been deprecated and is always set to the `'default'` value.**
99
99
  *
100
100
  * It can be one of the following values:
101
101
  * * `'default'` &ndash; All "normal" batches. This is the most commonly used type.
@@ -58,11 +58,12 @@ export default class Differ {
58
58
  * A map that stores all changed markers.
59
59
  *
60
60
  * The keys of the map are marker names.
61
- * The values of the map are objects with the `oldRange` and `newRange` properties. They store the marker range
62
- * state before and after the change.
61
+ * The values of the map are objects with the following properties:
62
+ * - `oldMarkerData`,
63
+ * - `newMarkerData`.
63
64
  *
64
65
  * @private
65
- * @type {Map}
66
+ * @type {Map.<String, Object>}
66
67
  */
67
68
  this._changedMarkers = new Map();
68
69
 
@@ -98,6 +99,14 @@ export default class Differ {
98
99
  * @type {Array.<Object>|null}
99
100
  */
100
101
  this._cachedChangesWithGraveyard = null;
102
+
103
+ /**
104
+ * Set of model items that were marked to get refreshed in {@link #_refreshItem}.
105
+ *
106
+ * @private
107
+ * @type {Set.<module:engine/model/item~Item>}
108
+ */
109
+ this._refreshedItems = new Set();
101
110
  }
102
111
 
103
112
  /**
@@ -110,32 +119,6 @@ export default class Differ {
110
119
  return this._changesInElement.size == 0 && this._changedMarkers.size == 0;
111
120
  }
112
121
 
113
- /**
114
- * Marks given `item` in differ to be "refreshed". It means that the item will be marked as removed and inserted in the differ changes
115
- * set, so it will be effectively re-converted when differ changes will be handled by a dispatcher.
116
- *
117
- * @param {module:engine/model/item~Item} item Item to refresh.
118
- */
119
- refreshItem( item ) {
120
- if ( this._isInInsertedElement( item.parent ) ) {
121
- return;
122
- }
123
-
124
- this._markRemove( item.parent, item.startOffset, item.offsetSize );
125
- this._markInsert( item.parent, item.startOffset, item.offsetSize );
126
-
127
- const range = Range._createOn( item );
128
-
129
- for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
130
- const markerRange = marker.getRange();
131
-
132
- this.bufferMarkerChange( marker.name, markerRange, markerRange, marker.affectsData );
133
- }
134
-
135
- // Clear cache after each buffered operation as it is no longer valid.
136
- this._cachedChanges = null;
137
- }
138
-
139
122
  /**
140
123
  * Buffers the given operation. An operation has to be buffered before it is executed.
141
124
  *
@@ -208,9 +191,9 @@ export default class Differ {
208
191
  const range = Range._createFromPositionAndShift( operation.position, 1 );
209
192
 
210
193
  for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
211
- const markerRange = marker.getRange();
194
+ const markerData = marker.getData();
212
195
 
213
- this.bufferMarkerChange( marker.name, markerRange, markerRange, marker.affectsData );
196
+ this.bufferMarkerChange( marker.name, markerData, markerData );
214
197
  }
215
198
 
216
199
  break;
@@ -267,27 +250,23 @@ export default class Differ {
267
250
  * Buffers a marker change.
268
251
  *
269
252
  * @param {String} markerName The name of the marker that changed.
270
- * @param {module:engine/model/range~Range|null} oldRange Marker range before the change or `null` if the marker has just
271
- * been created.
272
- * @param {module:engine/model/range~Range|null} newRange Marker range after the change or `null` if the marker was removed.
273
- * @param {Boolean} affectsData Flag indicating whether marker affects the editor data.
253
+ * @param {module:engine/model/markercollection~MarkerData} oldMarkerData Marker data before the change.
254
+ * @param {module:engine/model/markercollection~MarkerData} newMarkerData Marker data after the change.
274
255
  */
275
- bufferMarkerChange( markerName, oldRange, newRange, affectsData ) {
256
+ bufferMarkerChange( markerName, oldMarkerData, newMarkerData ) {
276
257
  const buffered = this._changedMarkers.get( markerName );
277
258
 
278
259
  if ( !buffered ) {
279
260
  this._changedMarkers.set( markerName, {
280
- oldRange,
281
- newRange,
282
- affectsData
261
+ newMarkerData,
262
+ oldMarkerData
283
263
  } );
284
264
  } else {
285
- buffered.newRange = newRange;
286
- buffered.affectsData = affectsData;
265
+ buffered.newMarkerData = newMarkerData;
287
266
 
288
- if ( buffered.oldRange == null && buffered.newRange == null ) {
289
- // The marker is going to be removed (`newRange == null`) but it did not exist before the first buffered change
290
- // (`buffered.oldRange == null`). In this case, do not keep the marker in buffer at all.
267
+ if ( buffered.oldMarkerData.range == null && newMarkerData.range == null ) {
268
+ // The marker is going to be removed (`newMarkerData.range == null`) but it did not exist before the first buffered change
269
+ // (`buffered.oldMarkerData.range == null`). In this case, do not keep the marker in buffer at all.
291
270
  this._changedMarkers.delete( markerName );
292
271
  }
293
272
  }
@@ -302,8 +281,8 @@ export default class Differ {
302
281
  const result = [];
303
282
 
304
283
  for ( const [ name, change ] of this._changedMarkers ) {
305
- if ( change.oldRange != null ) {
306
- result.push( { name, range: change.oldRange } );
284
+ if ( change.oldMarkerData.range != null ) {
285
+ result.push( { name, range: change.oldMarkerData.range } );
307
286
  }
308
287
  }
309
288
 
@@ -319,8 +298,8 @@ export default class Differ {
319
298
  const result = [];
320
299
 
321
300
  for ( const [ name, change ] of this._changedMarkers ) {
322
- if ( change.newRange != null ) {
323
- result.push( { name, range: change.newRange } );
301
+ if ( change.newMarkerData.range != null ) {
302
+ result.push( { name, range: change.newMarkerData.range } );
324
303
  }
325
304
  }
326
305
 
@@ -333,12 +312,12 @@ export default class Differ {
333
312
  * @returns {Array.<Object>}
334
313
  */
335
314
  getChangedMarkers() {
336
- return Array.from( this._changedMarkers ).map( item => (
315
+ return Array.from( this._changedMarkers ).map( ( [ name, change ] ) => (
337
316
  {
338
- name: item[ 0 ],
317
+ name,
339
318
  data: {
340
- oldRange: item[ 1 ].oldRange,
341
- newRange: item[ 1 ].newRange
319
+ oldRange: change.oldMarkerData.range,
320
+ newRange: change.newMarkerData.range
342
321
  }
343
322
  }
344
323
  ) );
@@ -351,19 +330,33 @@ export default class Differ {
351
330
  *
352
331
  * * model structure changes,
353
332
  * * attribute changes,
354
- * * changes of markers which were defined as `affectingData`.
333
+ * * changes of markers which were defined as `affectsData`,
334
+ * * changes of markers' `affectsData` property.
355
335
  *
356
336
  * @returns {Boolean}
357
337
  */
358
338
  hasDataChanges() {
359
- for ( const [ , change ] of this._changedMarkers ) {
360
- if ( change.affectsData ) {
339
+ if ( this._changesInElement.size > 0 ) {
340
+ return true;
341
+ }
342
+
343
+ for ( const { newMarkerData, oldMarkerData } of this._changedMarkers.values() ) {
344
+ if ( newMarkerData.affectsData !== oldMarkerData.affectsData ) {
361
345
  return true;
362
346
  }
347
+
348
+ if ( newMarkerData.affectsData ) {
349
+ const markerAdded = newMarkerData.range && !oldMarkerData.range;
350
+ const markerRemoved = !newMarkerData.range && oldMarkerData.range;
351
+ const markerChanged = newMarkerData.range && oldMarkerData.range && !newMarkerData.range.isEqual( oldMarkerData.range );
352
+
353
+ if ( markerAdded || markerRemoved || markerChanged ) {
354
+ return true;
355
+ }
356
+ }
363
357
  }
364
358
 
365
- // If markers do not affect the data, check whether there are some changes in elements.
366
- return this._changesInElement.size > 0;
359
+ return false;
367
360
  }
368
361
 
369
362
  /**
@@ -430,12 +423,12 @@ export default class Differ {
430
423
  for ( const action of actions ) {
431
424
  if ( action === 'i' ) {
432
425
  // Generate diff item for this element and insert it into the diff set.
433
- diffSet.push( this._getInsertDiff( element, i, elementChildren[ i ].name ) );
426
+ diffSet.push( this._getInsertDiff( element, i, elementChildren[ i ] ) );
434
427
 
435
428
  i++;
436
429
  } else if ( action === 'r' ) {
437
430
  // Generate diff item for this element and insert it into the diff set.
438
- diffSet.push( this._getRemoveDiff( element, i, snapshotChildren[ j ].name ) );
431
+ diffSet.push( this._getRemoveDiff( element, i, snapshotChildren[ j ] ) );
439
432
 
440
433
  j++;
441
434
  } else if ( action === 'a' ) {
@@ -540,16 +533,25 @@ export default class Differ {
540
533
  this._changeCount = 0;
541
534
 
542
535
  // Cache changes.
543
- this._cachedChangesWithGraveyard = diffSet.slice();
536
+ this._cachedChangesWithGraveyard = diffSet;
544
537
  this._cachedChanges = diffSet.filter( _changesInGraveyardFilter );
545
538
 
546
539
  if ( options.includeChangesInGraveyard ) {
547
- return this._cachedChangesWithGraveyard;
540
+ return this._cachedChangesWithGraveyard.slice();
548
541
  } else {
549
- return this._cachedChanges;
542
+ return this._cachedChanges.slice();
550
543
  }
551
544
  }
552
545
 
546
+ /**
547
+ * Returns a set of model items that were marked to get refreshed.
548
+ *
549
+ * @return {Set.<module:engine/model/item~Item>}
550
+ */
551
+ getRefreshedItems() {
552
+ return new Set( this._refreshedItems );
553
+ }
554
+
553
555
  /**
554
556
  * Resets `Differ`. Removes all buffered changes.
555
557
  */
@@ -557,6 +559,36 @@ export default class Differ {
557
559
  this._changesInElement.clear();
558
560
  this._elementSnapshots.clear();
559
561
  this._changedMarkers.clear();
562
+ this._refreshedItems = new Set();
563
+ this._cachedChanges = null;
564
+ }
565
+
566
+ /**
567
+ * Marks the given `item` in differ to be "refreshed". It means that the item will be marked as removed and inserted
568
+ * in the differ changes set, so it will be effectively re-converted when the differ changes are handled by a dispatcher.
569
+ *
570
+ * @protected
571
+ * @param {module:engine/model/item~Item} item Item to refresh.
572
+ */
573
+ _refreshItem( item ) {
574
+ if ( this._isInInsertedElement( item.parent ) ) {
575
+ return;
576
+ }
577
+
578
+ this._markRemove( item.parent, item.startOffset, item.offsetSize );
579
+ this._markInsert( item.parent, item.startOffset, item.offsetSize );
580
+
581
+ this._refreshedItems.add( item );
582
+
583
+ const range = Range._createOn( item );
584
+
585
+ for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
586
+ const markerData = marker.getData();
587
+
588
+ this.bufferMarkerChange( marker.name, markerData, markerData );
589
+ }
590
+
591
+ // Clear cache after each buffered operation as it is no longer valid.
560
592
  this._cachedChanges = null;
561
593
  }
562
594
 
@@ -897,14 +929,15 @@ export default class Differ {
897
929
  * @private
898
930
  * @param {module:engine/model/element~Element} parent The element in which the change happened.
899
931
  * @param {Number} offset The offset at which change happened.
900
- * @param {String} name The name of the removed element or `'$text'` for a character.
932
+ * @param {Object} elementSnapshot The snapshot of the removed element a character.
901
933
  * @returns {Object} The diff item.
902
934
  */
903
- _getInsertDiff( parent, offset, name ) {
935
+ _getInsertDiff( parent, offset, elementSnapshot ) {
904
936
  return {
905
937
  type: 'insert',
906
938
  position: Position._createAt( parent, offset ),
907
- name,
939
+ name: elementSnapshot.name,
940
+ attributes: new Map( elementSnapshot.attributes ),
908
941
  length: 1,
909
942
  changeCount: this._changeCount++
910
943
  };
@@ -916,14 +949,15 @@ export default class Differ {
916
949
  * @private
917
950
  * @param {module:engine/model/element~Element} parent The element in which change happened.
918
951
  * @param {Number} offset The offset at which change happened.
919
- * @param {String} name The name of the removed element or `'$text'` for a character.
952
+ * @param {Object} elementSnapshot The snapshot of the removed element a character.
920
953
  * @returns {Object} The diff item.
921
954
  */
922
- _getRemoveDiff( parent, offset, name ) {
955
+ _getRemoveDiff( parent, offset, elementSnapshot ) {
923
956
  return {
924
957
  type: 'remove',
925
958
  position: Position._createAt( parent, offset ),
926
- name,
959
+ name: elementSnapshot.name,
960
+ attributes: new Map( elementSnapshot.attributes ),
927
961
  length: 1,
928
962
  changeCount: this._changeCount++
929
963
  };
@@ -1201,6 +1235,12 @@ function _changesInGraveyardFilter( entry ) {
1201
1235
  * @member {String} module:engine/model/differ~DiffItemInsert#name
1202
1236
  */
1203
1237
 
1238
+ /**
1239
+ * Map of attributes that were set on the item while it was inserted.
1240
+ *
1241
+ * @member {Map.<String,*>} module:engine/model/differ~DiffItemInsert#attributes
1242
+ */
1243
+
1204
1244
  /**
1205
1245
  * The position where the node was inserted.
1206
1246
  *
@@ -1232,6 +1272,12 @@ function _changesInGraveyardFilter( entry ) {
1232
1272
  * @member {String} module:engine/model/differ~DiffItemRemove#name
1233
1273
  */
1234
1274
 
1275
+ /**
1276
+ * Map of attributes that were set on the item while it was removed.
1277
+ *
1278
+ * @member {Map.<String,*>} module:engine/model/differ~DiffItemRemove#attributes
1279
+ */
1280
+
1235
1281
  /**
1236
1282
  * The position where the node was removed.
1237
1283
  *