Przeglądaj źródła

Merge branch 'master' into t/1417

Piotrek Koszuliński 7 lat temu
rodzic
commit
c7142a2831

+ 30 - 5
packages/ckeditor5-engine/src/model/differ.js

@@ -111,7 +111,7 @@ export default class Differ {
 	}
 
 	/**
-	 * Buffers a given operation. An operation has to be buffered before it is executed.
+	 * Buffers the given operation. An operation has to be buffered before it is executed.
 	 *
 	 * Operation type is checked and it is checked which nodes it will affect. These nodes are then stored in `Differ`
 	 * in the state before the operation is executed.
@@ -171,7 +171,7 @@ export default class Differ {
 				for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
 					const markerRange = marker.getRange();
 
-					this.bufferMarkerChange( marker.name, markerRange, markerRange );
+					this.bufferMarkerChange( marker.name, markerRange, markerRange, marker.affectsData );
 				}
 
 				break;
@@ -183,23 +183,26 @@ export default class Differ {
 	}
 
 	/**
-	 * Buffers marker change.
+	 * Buffers a marker change.
 	 *
 	 * @param {String} markerName The name of the marker that changed.
 	 * @param {module:engine/model/range~Range|null} oldRange Marker range before the change or `null` if the marker has just
 	 * been created.
 	 * @param {module:engine/model/range~Range|null} newRange Marker range after the change or `null` if the marker was removed.
+	 * @param {Boolean} affectsData Flag indicating whether marker affects the editor data.
 	 */
-	bufferMarkerChange( markerName, oldRange, newRange ) {
+	bufferMarkerChange( markerName, oldRange, newRange, affectsData ) {
 		const buffered = this._changedMarkers.get( markerName );
 
 		if ( !buffered ) {
 			this._changedMarkers.set( markerName, {
 				oldRange,
-				newRange
+				newRange,
+				affectsData
 			} );
 		} else {
 			buffered.newRange = newRange;
+			buffered.affectsData = affectsData;
 
 			if ( buffered.oldRange == null && buffered.newRange == null ) {
 				// The marker is going to be removed (`newRange == null`) but it did not exist before the first buffered change
@@ -244,6 +247,28 @@ export default class Differ {
 	}
 
 	/**
+	 * Checks whether some of the buffered changes affect the editor data.
+	 *
+	 * Types of changes which affect the editor data:
+	 *
+	 * * model structure changes,
+	 * * attribute changes,
+	 * * changes of markers which were defined as `affectingData`.
+	 *
+	 * @returns {Boolean}
+	 */
+	hasDataChanges() {
+		for ( const [ , change ] of this._changedMarkers ) {
+			if ( change.affectsData ) {
+				return true;
+			}
+		}
+
+		// If markers do not affect the data, check whether there are some changes in elements.
+		return this._changesInElement.size > 0;
+	}
+
+	/**
 	 * Calculates the diff between the old model tree state (the state before the first buffered operations since the last {@link #reset}
 	 * call) and the new model tree state (actual one). It should be called after all buffered operations are executed.
 	 *

+ 48 - 14
packages/ckeditor5-engine/src/model/document.js

@@ -23,9 +23,18 @@ import { isInsideSurrogatePair, isInsideCombinedSymbol } from '@ckeditor/ckedito
 const graveyardName = '$graveyard';
 
 /**
- * Document tree model describes all editable data in the editor. It may contain multiple
- * {@link module:engine/model/document~Document#roots root elements}. For example, if the editor has multiple editable areas,
- * each area will be represented by a separate root.
+ * Data model's document. It contains the model's structure, its selection and the history of changes.
+ *
+ * Read more about working with the model in
+ * {@glink framework/guides/architecture/editing-engine#model introduction to the the editing engine's architecture}.
+ *
+ * Usually, the document contains just one {@link module:engine/model/document~Document#roots root element}, so
+ * you can retrieve it by just calling {@link module:engine/model/document~Document#getRoot} without specifying its name:
+ *
+ *		model.document.getRoot(); // -> returns the main root
+ *
+ * However, the document may contain multiple roots – e.g. when the editor has multiple editable areas
+ * (e.g. a title and a body of a message).
  *
  * @mixes module:utils/emittermixin~EmitterMixin
  */
@@ -46,6 +55,7 @@ export default class Document {
 		/**
 		 * The document version. It starts from `0` and every operation increases the version number. It is used to ensure that
 		 * operations are applied on a proper document version.
+		 *
 		 * If the {@link module:engine/model/operation/operation~Operation#baseVersion base version} does not match the document version,
 		 * a {@link module:utils/ckeditorerror~CKEditorError model-document-applyOperation-wrong-version} error is thrown.
 		 *
@@ -65,7 +75,7 @@ export default class Document {
 		this.history = new History( this );
 
 		/**
-		 * The selection done on this document.
+		 * The selection in this document.
 		 *
 		 * @readonly
 		 * @member {module:engine/model/documentselection~DocumentSelection}
@@ -147,11 +157,16 @@ export default class Document {
 		// Wait for `_change` event from model, which signalizes that outermost change block has finished.
 		// When this happens, check if there were any changes done on document, and if so, call post fixers,
 		// fire `change` event for features and conversion and then reset the differ.
+		// Fire `change:data` event when at least one operation or buffered marker changes the data.
 		this.listenTo( model, '_change', ( evt, writer ) => {
 			if ( !this.differ.isEmpty || hasSelectionChanged ) {
 				this._callPostFixers( writer );
 
-				this.fire( 'change', writer.batch );
+				if ( this.differ.hasDataChanges() ) {
+					this.fire( 'change:data', writer.batch );
+				} else {
+					this.fire( 'change', writer.batch );
+				}
 
 				this.differ.reset();
 				hasSelectionChanged = false;
@@ -163,12 +178,12 @@ export default class Document {
 		// are modified using `model.markers` collection, not through `MarkerOperation`).
 		this.listenTo( model.markers, 'update', ( evt, marker, oldRange, newRange ) => {
 			// Whenever marker is updated, buffer that change.
-			this.differ.bufferMarkerChange( marker.name, oldRange, newRange );
+			this.differ.bufferMarkerChange( marker.name, oldRange, newRange, marker.affectsData );
 
 			if ( oldRange === null ) {
 				// If this is a new marker, add a listener that will buffer change whenever marker changes.
 				marker.on( 'change', ( evt, oldRange ) => {
-					this.differ.bufferMarkerChange( marker.name, oldRange, marker.getRange() );
+					this.differ.bufferMarkerChange( marker.name, oldRange, marker.getRange(), marker.affectsData );
 				} );
 			}
 		} );
@@ -376,21 +391,40 @@ export default class Document {
 	 * If you want to be notified about all these changes, then simply listen to this event like this:
 	 *
 	 *		model.document.on( 'change', () => {
-	 *			console.log( 'The Document has changed!' );
+	 *			console.log( 'The document has changed!' );
 	 *		} );
 	 *
-	 * If, however, you only want to be notified about structure changes, then check whether the
-	 * {@link module:engine/model/differ~Differ differ} contains any changes:
+	 * If, however, you only want to be notified about the data changes, then use the
+	 * {@link module:engine/model/document~Document#event:change:data change:data} event,
+	 * which is fired for document structure changes and marker changes (which affects the data).
 	 *
-	 *		model.document.on( 'change', () => {
-	 *			if ( model.document.differ.getChanges().length > 0 ) {
-	 *				console.log( 'The Document has changed!' );
-	 *			}
+	 *		model.document.on( 'change:data', () => {
+	 *			console.log( 'The data has changed!' );
 	 *		} );
 	 *
 	 * @event change
 	 * @param {module:engine/model/batch~Batch} batch The batch that was used in the executed changes block.
 	 */
+
+	/**
+	 * It is a narrower version of the {@link #event:change} event. It is fired for changes which
+	 * affect the editor data. This is:
+	 *
+	 * * document structure changes,
+	 * * marker changes (which affects the data).
+	 *
+	 * If you want to be notified about the data changes, then listen to this event:
+	 *
+	 *		model.document.on( 'change:data', () => {
+	 *			console.log( 'The data has changed!' );
+	 *		} );
+	 *
+	 * If you would like to listen to all document changes, then check out the
+	 * {@link module:engine/model/document~Document#event:change change} event.
+	 *
+	 * @event change:data
+	 * @param {module:engine/model/batch~Batch} batch The batch that was used in the executed changes block.
+	 */
 }
 
 mix( Document, EmitterMixin );

+ 41 - 10
packages/ckeditor5-engine/src/model/markercollection.js

@@ -88,9 +88,11 @@ export default class MarkerCollection {
 	 * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of marker to set or marker instance to update.
 	 * @param {module:engine/model/range~Range} range Marker range.
 	 * @param {Boolean} [managedUsingOperations=false] Specifies whether the marker is managed using operations.
+	 * @param {Boolean} [affectsData=false] Specifies whether the marker affects the data produced by the data pipeline
+	 * (is persisted in the editor's data).
 	 * @returns {module:engine/model/markercollection~Marker} `Marker` instance which was added or updated.
 	 */
-	_set( markerOrName, range, managedUsingOperations = false ) {
+	_set( markerOrName, range, managedUsingOperations = false, affectsData = false ) {
 		const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
 		const oldMarker = this._markers.get( markerName );
 
@@ -108,6 +110,11 @@ export default class MarkerCollection {
 				hasChanged = true;
 			}
 
+			if ( typeof affectsData === 'boolean' && affectsData != oldMarker.affectsData ) {
+				oldMarker._affectsData = affectsData;
+				hasChanged = true;
+			}
+
 			if ( hasChanged ) {
 				this.fire( 'update:' + markerName, oldMarker, oldRange, range );
 			}
@@ -116,7 +123,7 @@ export default class MarkerCollection {
 		}
 
 		const liveRange = LiveRange.createFromRange( range );
-		const marker = new Marker( markerName, liveRange, managedUsingOperations );
+		const marker = new Marker( markerName, liveRange, managedUsingOperations, affectsData );
 
 		this._markers.set( markerName, marker );
 		this.fire( 'update:' + markerName, marker, null, range );
@@ -313,8 +320,10 @@ class Marker {
 	 * @param {String} name Marker name.
 	 * @param {module:engine/model/liverange~LiveRange} liveRange Range marked by the marker.
 	 * @param {Boolean} managedUsingOperations Specifies whether the marker is managed using operations.
+	 * @param {Boolean} affectsData Specifies whether the marker affects the data produced by the data pipeline
+	 * (is persisted in the editor's data).
 	 */
-	constructor( name, liveRange, managedUsingOperations ) {
+	constructor( name, liveRange, managedUsingOperations, affectsData ) {
 		/**
 		 * Marker's name.
 		 *
@@ -324,24 +333,33 @@ class Marker {
 		this.name = name;
 
 		/**
-		 * Flag indicates if the marker is managed using operations or not.
+		 * Range marked by the marker.
 		 *
 		 * @protected
+		 * @member {module:engine/model/liverange~LiveRange}
+		 */
+		this._liveRange = this._attachLiveRange( liveRange );
+
+		/**
+		 * Flag indicates if the marker is managed using operations or not.
+		 *
+		 * @private
 		 * @member {Boolean}
 		 */
 		this._managedUsingOperations = managedUsingOperations;
 
 		/**
-		 * Range marked by the marker.
+		 * Specifies whether the marker affects the data produced by the data pipeline
+		 * (is persisted in the editor's data).
 		 *
 		 * @private
-		 * @member {module:engine/model/liverange~LiveRange} #_liveRange
+		 * @member {Boolean}
 		 */
-		this._liveRange = this._attachLiveRange( liveRange );
+		this._affectsData = affectsData;
 	}
 
 	/**
-	 * Returns value of flag indicates if the marker is managed using operations or not.
+	 * A value indicating if the marker is managed using operations.
 	 * See {@link ~Marker marker class description} to learn more about marker types.
 	 * See {@link module:engine/model/writer~Writer#addMarker}.
 	 *
@@ -356,6 +374,19 @@ class Marker {
 	}
 
 	/**
+	 * A value indicating if the marker changes the data.
+	 *
+	 * @returns {Boolean}
+	 */
+	get affectsData() {
+		if ( !this._liveRange ) {
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+		}
+
+		return this._affectsData;
+	}
+
+	/**
 	 * Returns current marker start position.
 	 *
 	 * @returns {module:engine/model/position~Position}
@@ -382,7 +413,7 @@ class Marker {
 	}
 
 	/**
-	 * Returns a range that represents current state of marker.
+	 * Returns a range that represents the current state of the marker.
 	 *
 	 * Keep in mind that returned value is a {@link module:engine/model/range~Range Range}, not a
 	 * {@link module:engine/model/liverange~LiveRange LiveRange}. This means that it is up-to-date and relevant only
@@ -402,7 +433,7 @@ class Marker {
 	}
 
 	/**
-	 * Binds new live range to marker and detach the old one if is attached.
+	 * Binds new live range to the marker and detach the old one if is attached.
 	 *
 	 * @protected
 	 * @param {module:engine/model/liverange~LiveRange} liveRange Live range to attach

+ 7 - 2
packages/ckeditor5-engine/src/model/nodelist.js

@@ -154,9 +154,14 @@ export default class NodeList {
 			/**
 			 * Given offset cannot be found in the node list.
 			 *
-			 * @error nodelist-offset-out-of-bounds
+			 * @error model-nodelist-offset-out-of-bounds
+			 * @param {Number} offset
+			 * @param {module:engine/model/nodelist~NodeList} nodeList Stringified node list.
 			 */
-			throw new CKEditorError( 'model-nodelist-offset-out-of-bounds: Given offset cannot be found in the node list.' );
+			throw new CKEditorError( 'model-nodelist-offset-out-of-bounds: Given offset cannot be found in the node list.', {
+				offset,
+				nodeList: this
+			} );
 		}
 
 		return this.length;

+ 18 - 6
packages/ckeditor5-engine/src/model/operation/markeroperation.js

@@ -20,9 +20,11 @@ export default class MarkerOperation extends Operation {
 	 * @param {module:engine/model/range~Range} newRange Marker range after the change.
 	 * @param {module:engine/model/markercollection~MarkerCollection} markers Marker collection on which change should be executed.
 	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
+	 * @param {Boolean} affectsData Specifies whether the marker operation affects the data produced by the data pipeline
+	 * (is persisted in the editor's data).
 	 * can be applied or `null` if the operation operates on detached (non-document) tree.
 	 */
-	constructor( name, oldRange, newRange, markers, baseVersion ) {
+	constructor( name, oldRange, newRange, markers, baseVersion, affectsData ) {
 		super( baseVersion );
 
 		/**
@@ -50,6 +52,15 @@ export default class MarkerOperation extends Operation {
 		this.newRange = newRange ? Range.createFromRange( newRange ) : null;
 
 		/**
+		 * Specifies whether the marker operation affects the data produced by the data pipeline
+		 * (is persisted in the editor's data).
+		 *
+		 * @readonly
+		 * @member {Boolean}
+		 */
+		this.affectsData = affectsData;
+
+		/**
 		 * Marker collection on which change should be executed.
 		 *
 		 * @private
@@ -71,7 +82,7 @@ export default class MarkerOperation extends Operation {
 	 * @returns {module:engine/model/operation/markeroperation~MarkerOperation} Clone of this operation.
 	 */
 	clone() {
-		return new MarkerOperation( this.name, this.oldRange, this.newRange, this._markers, this.baseVersion );
+		return new MarkerOperation( this.name, this.oldRange, this.newRange, this._markers, this.baseVersion, this.affectsData );
 	}
 
 	/**
@@ -80,7 +91,7 @@ export default class MarkerOperation extends Operation {
 	 * @returns {module:engine/model/operation/markeroperation~MarkerOperation}
 	 */
 	getReversed() {
-		return new MarkerOperation( this.name, this.newRange, this.oldRange, this._markers, this.baseVersion + 1 );
+		return new MarkerOperation( this.name, this.newRange, this.oldRange, this._markers, this.baseVersion + 1, this.affectsData );
 	}
 
 	/**
@@ -89,7 +100,7 @@ export default class MarkerOperation extends Operation {
 	_execute() {
 		const type = this.newRange ? '_set' : '_remove';
 
-		this._markers[ type ]( this.name, this.newRange, true );
+		this._markers[ type ]( this.name, this.newRange, true, this.affectsData );
 	}
 
 	/**
@@ -111,7 +122,7 @@ export default class MarkerOperation extends Operation {
 	}
 
 	/**
-	 * Creates `MarkerOperation` object from deserilized object, i.e. from parsed JSON string.
+	 * Creates `MarkerOperation` object from deserialized object, i.e. from parsed JSON string.
 	 *
 	 * @param {Object} json Deserialized JSON object.
 	 * @param {module:engine/model/document~Document} document Document on which this operation will be applied.
@@ -123,7 +134,8 @@ export default class MarkerOperation extends Operation {
 			json.oldRange ? Range.fromJSON( json.oldRange, document ) : null,
 			json.newRange ? Range.fromJSON( json.newRange, document ) : null,
 			document.model.markers,
-			json.baseVersion
+			json.baseVersion,
+			json.affectsData
 		);
 	}
 }

+ 1 - 1
packages/ckeditor5-engine/src/model/operation/operationfactory.js

@@ -37,7 +37,7 @@ operations[ RootAttributeOperation.className ] = RootAttributeOperation;
  */
 export default class OperationFactory {
 	/**
-	 * Creates concrete `Operation` object from deserilized object, i.e. from parsed JSON string.
+	 * Creates concrete `Operation` object from deserialized object, i.e. from parsed JSON string.
 	 *
 	 * @param {Object} json Deserialized JSON object.
 	 * @param {module:engine/model/document~Document} document Document on which this operation will be applied.

+ 2 - 2
packages/ckeditor5-engine/src/model/schema.js

@@ -1047,7 +1047,7 @@ mix( Schema, ObservableMixin );
  *
  *		schema.register( 'listItem', {
  *			inheritAllFrom: '$block',
- *			allowAttributes: [ 'type', 'indent' ]
+ *			allowAttributes: [ 'listType', 'listIndent' ]
  *		} );
  *
  * Which translates to:
@@ -1057,7 +1057,7 @@ mix( Schema, ObservableMixin );
  *			allowContentOf: '$block',
  *			allowAttributesOf: '$block',
  *			inheritTypesFrom: '$block',
- *			allowAttributes: [ 'type', 'indent' ]
+ *			allowAttributes: [ 'listType', 'listIndent' ]
  *		} );
  *
  * # Tips

+ 49 - 21
packages/ckeditor5-engine/src/model/writer.js

@@ -794,6 +794,12 @@ export default class Writer {
 	 * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
 	 * markers managed by operations and not-managed by operations.
 	 *
+	 * The `options.affectsData` parameter, which defaults to `false`, allows you to define if a marker affects the data. It should be
+	 * `true` when the marker change changes the data returned by the
+	 * {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`} method.
+	 * When set to `true` it fires the {@link module:engine/model/document~Document#event:change:data `change:data`} event.
+	 * When set to `false` it fires the {@link module:engine/model/document~Document#event:change `change`} event.
+	 *
 	 * Create marker directly base on marker's name:
 	 *
 	 *		addMarker( markerName, { range, usingOperation: false } );
@@ -802,14 +808,19 @@ export default class Writer {
 	 *
 	 *		addMarker( markerName, { range, usingOperation: true } );
 	 *
+	 * Create marker that affects the editor data:
+	 *
+	 *		addMarker( markerName, { range, usingOperation: false, affectsData: true } );
+	 *
 	 * Note: For efficiency reasons, it's best to create and keep as little markers as possible.
 	 *
 	 * @see module:engine/model/markercollection~Marker
 	 * @param {String} name Name of a marker to create - must be unique.
 	 * @param {Object} options
-	 * @param {Boolean} options.usingOperation Flag indicated whether the marker should be added by MarkerOperation.
+	 * @param {Boolean} options.usingOperation Flag indicating that the marker should be added by MarkerOperation.
 	 * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
 	 * @param {module:engine/model/range~Range} options.range Marker range.
+	 * @param {Boolean} [options.affectsData=false] Flag indicating that the marker changes the editor data.
 	 * @returns {module:engine/model/markercollection~Marker} Marker that was set.
 	 */
 	addMarker( name, options ) {
@@ -828,6 +839,7 @@ export default class Writer {
 
 		const usingOperation = options.usingOperation;
 		const range = options.range;
+		const affectsData = options.affectsData === undefined ? false : options.affectsData;
 
 		if ( this.model.markers.has( name ) ) {
 			/**
@@ -848,10 +860,10 @@ export default class Writer {
 		}
 
 		if ( !usingOperation ) {
-			return this.model.markers._set( name, range, usingOperation );
+			return this.model.markers._set( name, range, usingOperation, affectsData );
 		}
 
-		applyMarkerOperation( this, name, null, range );
+		applyMarkerOperation( this, name, null, range, affectsData );
 
 		return this.model.markers.get( name );
 	}
@@ -867,7 +879,12 @@ export default class Writer {
 	 * The `options.usingOperation` parameter lets you change if the marker should be managed by operations or not. See
 	 * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
 	 * markers managed by operations and not-managed by operations. It is possible to change this option for an existing marker.
-	 * This is useful when a marker have been created earlier and then later, it needs to be added to the document history.
+	 *
+	 * The `options.affectsData` parameter, which defaults to `false`, allows you to define if a marker affects the data. It should be
+	 * `true` when the marker change changes the data returned by
+	 * the {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`} method.
+	 * When set to `true` it fires the {@link module:engine/model/document~Document#event:change:data `change:data`} event.
+	 * When set to `false` it fires the {@link module:engine/model/document~Document#event:change `change`} event.
 	 *
 	 * Update marker directly base on marker's name:
 	 *
@@ -882,18 +899,22 @@ export default class Writer {
 	 *
 	 *		updateMarker( marker, { usingOperation: true } );
 	 *
+	 * Change marker's option (inform the engine, that the marker does not affect the data anymore):
+	 *
+	 *		updateMarker( markerName, { affectsData: false } );
+	 *
 	 * @see module:engine/model/markercollection~Marker
 	 * @param {String} markerOrName Name of a marker to update, or a marker instance.
 	 * @param {Object} options
 	 * @param {module:engine/model/range~Range} [options.range] Marker range to update.
 	 * @param {Boolean} [options.usingOperation] Flag indicated whether the marker should be added by MarkerOperation.
 	 * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
+	 * @param {Boolean} [options.affectsData] Flag indicating that the marker changes the editor data.
 	 */
-	updateMarker( markerOrName, options ) {
+	updateMarker( markerOrName, options = {} ) {
 		this._assertWriterUsedCorrectly();
 
 		const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
-
 		const currentMarker = this.model.markers.get( markerName );
 
 		if ( !currentMarker ) {
@@ -905,33 +926,39 @@ export default class Writer {
 			throw new CKEditorError( 'writer-updateMarker-marker-not-exists: Marker with provided name does not exists.' );
 		}
 
-		const newRange = options && options.range;
-		const hasUsingOperationDefined = !!options && typeof options.usingOperation == 'boolean';
+		const hasUsingOperationDefined = typeof options.usingOperation == 'boolean';
+		const affectsDataDefined = typeof options.affectsData == 'boolean';
+
+		// Use previously defined marker's affectsData if the property is not provided.
+		const affectsData = affectsDataDefined ? options.affectsData : currentMarker.affectsData;
 
-		if ( !hasUsingOperationDefined && !newRange ) {
+		if ( !hasUsingOperationDefined && !options.range && !affectsDataDefined ) {
 			/**
-			 * One of options is required - provide range or usingOperations.
+			 * One of the options is required - provide range, usingOperations or affectsData.
 			 *
 			 * @error writer-updateMarker-wrong-options
 			 */
-			throw new CKEditorError( 'writer-updateMarker-wrong-options: One of options is required - provide range or usingOperations.' );
+			throw new CKEditorError(
+				'writer-updateMarker-wrong-options: One of the options is required - provide range, usingOperations or affectsData.'
+			);
 		}
 
+		const currentRange = currentMarker.getRange();
+		const updatedRange = options.range ? options.range : currentRange;
+
 		if ( hasUsingOperationDefined && options.usingOperation !== currentMarker.managedUsingOperations ) {
 			// The marker type is changed so it's necessary to create proper operations.
 			if ( options.usingOperation ) {
 				// If marker changes to a managed one treat this as synchronizing existing marker.
-				// If marker changes to a managed one treat this as synchronizing existing marker.
 				// Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
-				applyMarkerOperation( this, markerName, null, newRange ? newRange : currentMarker.getRange() );
+				applyMarkerOperation( this, markerName, null, updatedRange, affectsData );
 			} else {
 				// If marker changes to a marker that do not use operations then we need to create additional operation
 				// that removes that marker first.
-				const currentRange = currentMarker.getRange();
-				applyMarkerOperation( this, markerName, currentRange, null );
+				applyMarkerOperation( this, markerName, currentRange, null, affectsData );
 
 				// Although not managed the marker itself should stay in model and its range should be preserver or changed to passed range.
-				this.model.markers._set( markerName, newRange ? newRange : currentRange );
+				this.model.markers._set( markerName, updatedRange, undefined, affectsData );
 			}
 
 			return;
@@ -939,9 +966,9 @@ export default class Writer {
 
 		// Marker's type doesn't change so update it accordingly.
 		if ( currentMarker.managedUsingOperations ) {
-			applyMarkerOperation( this, markerName, currentMarker.getRange(), newRange );
+			applyMarkerOperation( this, markerName, currentRange, updatedRange, affectsData );
 		} else {
-			this.model.markers._set( markerName, newRange );
+			this.model.markers._set( markerName, updatedRange, undefined, affectsData );
 		}
 	}
 
@@ -976,7 +1003,7 @@ export default class Writer {
 
 		const oldRange = marker.getRange();
 
-		applyMarkerOperation( this, name, oldRange, null );
+		applyMarkerOperation( this, name, oldRange, null, marker.affectsData );
 	}
 
 	/**
@@ -1325,12 +1352,13 @@ function setAttributeOnItem( writer, key, value, item ) {
 // @param {String} name Marker name.
 // @param {module:engine/model/range~Range} oldRange Marker range before the change.
 // @param {module:engine/model/range~Range} newRange Marker range after the change.
-function applyMarkerOperation( writer, name, oldRange, newRange ) {
+// @param {Boolean} affectsData
+function applyMarkerOperation( writer, name, oldRange, newRange, affectsData ) {
 	const model = writer.model;
 	const doc = model.document;
 	const delta = new MarkerDelta();
 
-	const operation = new MarkerOperation( name, oldRange, newRange, model.markers, doc.version );
+	const operation = new MarkerOperation( name, oldRange, newRange, model.markers, doc.version, affectsData );
 
 	writer.batch.addDelta( delta );
 	delta.addOperation( operation );

+ 2 - 2
packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js

@@ -634,7 +634,7 @@ describe( 'debug tools', () => {
 				new ModelElement( 'paragraph', { foo: 'bar' }, [
 					new ModelText( 'This is ' ), new ModelText( 'bold', { bold: true } ), new ModelText( '.' )
 				] ),
-				new ModelElement( 'listItem', { type: 'numbered', indent: 0 }, new ModelText( 'One.' ) ),
+				new ModelElement( 'listItem', { listType: 'numbered', listIndent: 0 }, new ModelText( 'One.' ) ),
 			] );
 
 			const modelRootTree = modelRoot.printTree();
@@ -646,7 +646,7 @@ describe( 'debug tools', () => {
 				'\n\t\t<$text bold=true>bold</$text>' +
 				'\n\t\t.' +
 				'\n\t</paragraph>' +
-				'\n\t<listItem type="numbered" indent=0>' +
+				'\n\t<listItem listType="numbered" listIndent=0>' +
 				'\n\t\tOne.' +
 				'\n\t</listItem>' +
 				'\n</main>'

+ 13 - 1
packages/ckeditor5-engine/tests/manual/markers.js

@@ -47,6 +47,18 @@ ClassicEditor
 			}
 		} ) );
 
+		editor.conversion.for( 'dataDowncast' ).add( downcastMarkerToHighlight( {
+			model: 'highlight',
+			view: data => {
+				const color = data.markerName.split( ':' )[ 1 ];
+
+				return {
+					classes: 'h-' + color,
+					priority: 1
+				};
+			}
+		} ) );
+
 		window.document.getElementById( 'add-yellow' ).addEventListener( 'mousedown', e => {
 			e.preventDefault();
 			addHighlight( 'yellow' );
@@ -83,7 +95,7 @@ ClassicEditor
 			const name = 'highlight:yellow:' + uid();
 
 			markerNames.push( name );
-			writer.addMarker( name, { range, usingOperation: false } );
+			writer.addMarker( name, { range, usingOperation: false, affectsData: true } );
 		} );
 	} )
 	.catch( err => {

+ 34 - 13
packages/ckeditor5-engine/tests/model/differ.js

@@ -1274,7 +1274,7 @@ describe( 'Differ', () => {
 		} );
 
 		it( 'add marker', () => {
-			differ.bufferMarkerChange( 'name', null, range );
+			differ.bufferMarkerChange( 'name', null, range, true );
 
 			expect( differ.getMarkersToRemove() ).to.deep.equal( [] );
 
@@ -1284,7 +1284,7 @@ describe( 'Differ', () => {
 		} );
 
 		it( 'remove marker', () => {
-			differ.bufferMarkerChange( 'name', range, null );
+			differ.bufferMarkerChange( 'name', range, null, true );
 
 			expect( differ.getMarkersToRemove() ).to.deep.equal( [
 				{ name: 'name', range }
@@ -1293,8 +1293,8 @@ describe( 'Differ', () => {
 			expect( differ.getMarkersToAdd() ).to.deep.equal( [] );
 		} );
 
-		it( 'change marker', () => {
-			differ.bufferMarkerChange( 'name', range, rangeB );
+		it( 'change marker\'s range', () => {
+			differ.bufferMarkerChange( 'name', range, rangeB, true );
 
 			expect( differ.getMarkersToRemove() ).to.deep.equal( [
 				{ name: 'name', range }
@@ -1305,17 +1305,30 @@ describe( 'Differ', () => {
 			] );
 		} );
 
+		it( 'add marker not affecting data', () => {
+			differ.bufferMarkerChange( 'name', range, rangeB, false );
+
+			expect( differ.hasDataChanges() ).to.be.false;
+		} );
+
+		it( 'add marker affecting data', () => {
+			differ.bufferMarkerChange( 'name', range, rangeB, true );
+
+			expect( differ.hasDataChanges() ).to.be.true;
+		} );
+
 		it( 'add marker and remove it', () => {
-			differ.bufferMarkerChange( 'name', null, range );
-			differ.bufferMarkerChange( 'name', range, null );
+			differ.bufferMarkerChange( 'name', null, range, true );
+			differ.bufferMarkerChange( 'name', range, null, true );
 
 			expect( differ.getMarkersToRemove() ).to.deep.equal( [] );
 			expect( differ.getMarkersToAdd() ).to.deep.equal( [] );
+			expect( differ.hasDataChanges() ).to.be.false;
 		} );
 
 		it( 'add marker and change it', () => {
-			differ.bufferMarkerChange( 'name', null, range );
-			differ.bufferMarkerChange( 'name', range, rangeB );
+			differ.bufferMarkerChange( 'name', null, range, true );
+			differ.bufferMarkerChange( 'name', range, rangeB, true );
 
 			expect( differ.getMarkersToRemove() ).to.deep.equal( [] );
 
@@ -1324,20 +1337,28 @@ describe( 'Differ', () => {
 			] );
 		} );
 
+		it( 'change marker to not affecting data', () => {
+			differ.bufferMarkerChange( 'name', range, rangeB, true );
+			differ.bufferMarkerChange( 'name', range, rangeB, false );
+
+			expect( differ.hasDataChanges() ).to.be.false;
+		} );
+
 		it( 'change marker and remove it', () => {
-			differ.bufferMarkerChange( 'name', range, rangeB );
-			differ.bufferMarkerChange( 'name', rangeB, null );
+			differ.bufferMarkerChange( 'name', range, rangeB, true );
+			differ.bufferMarkerChange( 'name', rangeB, null, true );
 
 			expect( differ.getMarkersToRemove() ).to.deep.equal( [
 				{ name: 'name', range }
 			] );
 
 			expect( differ.getMarkersToAdd() ).to.deep.equal( [] );
+			expect( differ.hasDataChanges() ).to.be.true;
 		} );
 
 		it( 'remove marker and add it at same range', () => {
-			differ.bufferMarkerChange( 'name', range, null );
-			differ.bufferMarkerChange( 'name', null, range );
+			differ.bufferMarkerChange( 'name', range, null, true );
+			differ.bufferMarkerChange( 'name', null, range, true );
 
 			expect( differ.getMarkersToRemove() ).to.deep.equal( [
 				{ name: 'name', range }
@@ -1349,7 +1370,7 @@ describe( 'Differ', () => {
 		} );
 
 		it( 'change marker to the same range', () => {
-			differ.bufferMarkerChange( 'name', range, range );
+			differ.bufferMarkerChange( 'name', range, range, true );
 
 			expect( differ.getMarkersToRemove() ).to.deep.equal( [
 				{ name: 'name', range }

+ 146 - 12
packages/ckeditor5-engine/tests/model/document.js

@@ -318,11 +318,25 @@ describe( 'Document', () => {
 				writer.insertText( 'foo', doc.getRoot(), 0 );
 			} );
 
-			expect( spy.calledOnce ).to.be.true;
+			sinon.assert.calledOnce( spy );
 		} );
 
-		it( 'should be fired if there was a change in a document tree in a change block and have a batch as param', () => {
-			doc.createRoot();
+		it( 'should be fired if there was a selection change in an (enqueue)change block', () => {
+			const root = doc.createRoot();
+			const spy = sinon.spy();
+
+			root._appendChild( new Text( 'foo' ) );
+
+			doc.on( 'change', spy );
+
+			model.change( writer => {
+				writer.setSelection( Range.createFromParentsAndOffsets( root, 2, root, 2 ) );
+			} );
+
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'should not be fired if writer was used on non-document tree', () => {
 			const spy = sinon.spy();
 
 			doc.on( 'change', ( evt, batch ) => {
@@ -330,33 +344,115 @@ describe( 'Document', () => {
 				expect( batch ).to.be.instanceof( Batch );
 			} );
 
-			model.enqueueChange( writer => {
-				writer.insertText( 'foo', doc.getRoot(), 0 );
+			model.change( writer => {
+				const docFrag = writer.createDocumentFragment();
+				writer.insertText( 'foo', docFrag, 0 );
 			} );
 
-			expect( spy.calledOnce ).to.be.true;
+			sinon.assert.notCalled( spy );
 		} );
+	} );
 
-		it( 'should be fired if there was a selection change in an (enqueue)change block', () => {
+	describe( 'event change:data', () => {
+		it( 'should be fired if there was a change in a document tree in a change block and have a batch as a param', () => {
 			doc.createRoot();
 			const spy = sinon.spy();
 
-			const root = doc.getRoot();
+			doc.on( 'change:data', ( evt, batch ) => {
+				spy();
+				expect( batch ).to.be.instanceof( Batch );
+			} );
+
+			model.change( writer => {
+				writer.insertText( 'foo', doc.getRoot(), 0 );
+			} );
+
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'should not be fired if only selection changes', () => {
+			const root = doc.createRoot();
+			const spy = sinon.spy();
+
 			root._appendChild( new Text( 'foo' ) );
 
-			doc.on( 'change', spy );
+			doc.on( 'change:data', spy );
 
 			model.change( writer => {
 				writer.setSelection( Range.createFromParentsAndOffsets( root, 2, root, 2 ) );
 			} );
 
-			expect( spy.calledOnce ).to.be.true;
+			sinon.assert.notCalled( spy );
+		} );
+
+		it( 'should be fired if default marker operation is applied', () => {
+			const root = doc.createRoot();
+			const spy = sinon.spy();
+
+			root._appendChild( new Text( 'foo' ) );
+
+			doc.on( 'change:data', spy );
+
+			model.change( writer => {
+				const range = Range.createFromParentsAndOffsets( root, 2, root, 4 );
+				writer.addMarker( 'name', { range, usingOperation: true, affectsData: true } );
+			} );
+
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'should not be fired if the marker operation is applied and marker does not affect data', () => {
+			const root = doc.createRoot();
+			const spy = sinon.spy();
+
+			root._appendChild( new Text( 'foo' ) );
+
+			doc.on( 'change:data', spy );
+
+			model.change( writer => {
+				const range = Range.createFromParentsAndOffsets( root, 2, root, 4 );
+				writer.addMarker( 'name', { range, usingOperation: true } );
+			} );
+
+			sinon.assert.notCalled( spy );
+		} );
+
+		it( 'should be fired if the writer adds marker not managed by using operations', () => {
+			const root = doc.createRoot();
+			const spy = sinon.spy();
+
+			root._appendChild( new Text( 'foo' ) );
+
+			doc.on( 'change:data', spy );
+
+			model.change( writer => {
+				const range = Range.createFromParentsAndOffsets( root, 2, root, 4 );
+				writer.addMarker( 'name', { range, usingOperation: false, affectsData: true } );
+			} );
+
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'should not be fired if the writer adds marker not managed by using operations with affectsData set to false', () => {
+			const root = doc.createRoot();
+			const spy = sinon.spy();
+
+			root._appendChild( new Text( 'foo' ) );
+
+			doc.on( 'change:data', spy );
+
+			model.change( writer => {
+				const range = Range.createFromParentsAndOffsets( root, 2, root, 4 );
+				writer.addMarker( 'name', { range, usingOperation: false } );
+			} );
+
+			sinon.assert.notCalled( spy );
 		} );
 
 		it( 'should not be fired if writer was used on non-document tree', () => {
 			const spy = sinon.spy();
 
-			doc.on( 'change', ( evt, batch ) => {
+			doc.on( 'change:data', ( evt, batch ) => {
 				spy();
 				expect( batch ).to.be.instanceof( Batch );
 			} );
@@ -366,7 +462,45 @@ describe( 'Document', () => {
 				writer.insertText( 'foo', docFrag, 0 );
 			} );
 
-			expect( spy.calledOnce ).to.be.false;
+			sinon.assert.notCalled( spy );
+		} );
+
+		it( 'should be fired when updated marker affects data', () => {
+			const root = doc.createRoot();
+			root._appendChild( new Text( 'foo' ) );
+
+			const sandbox = sinon.createSandbox();
+			const changeDataSpy = sandbox.spy();
+			const changeSpy = sandbox.spy();
+
+			doc.on( 'change:data', changeDataSpy );
+			doc.on( 'change', changeSpy );
+
+			model.change( writer => {
+				const range = Range.createFromParentsAndOffsets( root, 2, root, 4 );
+				writer.addMarker( 'name', { range, usingOperation: false } );
+			} );
+
+			sinon.assert.calledOnce( changeSpy );
+			sinon.assert.notCalled( changeDataSpy );
+
+			sandbox.resetHistory();
+
+			model.change( writer => {
+				writer.updateMarker( 'name', { affectsData: true } );
+			} );
+
+			sinon.assert.calledOnce( changeSpy );
+			sinon.assert.calledOnce( changeDataSpy );
+
+			sandbox.resetHistory();
+
+			model.change( writer => {
+				writer.updateMarker( 'name', { affectsData: false } );
+			} );
+
+			sinon.assert.calledOnce( changeSpy );
+			sinon.assert.notCalled( changeDataSpy );
 		} );
 	} );
 

+ 37 - 11
packages/ckeditor5-engine/tests/model/markercollection.js

@@ -50,7 +50,8 @@ describe( 'MarkerCollection', () => {
 
 			expect( result ).to.equal( marker );
 			expect( marker.name ).to.equal( 'name' );
-			expect( marker.managedUsingOperations ).to.false;
+			expect( marker.managedUsingOperations ).to.be.false;
+			expect( marker.affectsData ).to.be.false;
 			expect( marker.getRange().isEqual( range ) ).to.be.true;
 			sinon.assert.calledWithExactly( markers.fire, 'update:name', result, null, range );
 		} );
@@ -58,7 +59,13 @@ describe( 'MarkerCollection', () => {
 		it( 'should create a marker marked as managed by operations', () => {
 			const marker = markers._set( 'name', range, true );
 
-			expect( marker.managedUsingOperations ).to.true;
+			expect( marker.managedUsingOperations ).to.be.true;
+		} );
+
+		it( 'should create a marker marked as affecting the data', () => {
+			const marker = markers._set( 'name', range, false, true );
+
+			expect( marker.affectsData ).to.be.true;
 		} );
 
 		it( 'should update marker range and fire update:<markerName> event if marker with given name was in the collection', () => {
@@ -90,7 +97,7 @@ describe( 'MarkerCollection', () => {
 			const result = markers._set( 'name', range, true );
 
 			expect( result ).to.equal( marker );
-			expect( marker.managedUsingOperations ).to.true;
+			expect( marker.managedUsingOperations ).to.be.true;
 			expect( marker.getRange().isEqual( range ) ).to.be.true;
 
 			sinon.assert.calledWithExactly( markers.fire, 'update:name', marker, range, range );
@@ -114,7 +121,7 @@ describe( 'MarkerCollection', () => {
 
 			markers._set( marker, range2 );
 
-			expect( marker.getRange().isEqual( range2 ) ).to.true;
+			expect( marker.getRange().isEqual( range2 ) ).to.be.true;
 		} );
 	} );
 
@@ -282,6 +289,10 @@ describe( 'Marker', () => {
 		expect( () => {
 			marker.managedUsingOperations;
 		} ).to.throw( CKEditorError, /^marker-destroyed/ );
+
+		expect( () => {
+			marker.affectsData;
+		} ).to.throw( CKEditorError, /^marker-destroyed/ );
 	} );
 
 	it( 'should attach live range to marker', () => {
@@ -320,7 +331,7 @@ describe( 'Marker', () => {
 
 		expect( eventRange.notCalled ).to.be.true;
 		expect( eventContent.notCalled ).to.be.true;
-		expect( liveRange.detach.calledOnce ).to.true;
+		expect( liveRange.detach.calledOnce ).to.be.true;
 	} );
 
 	it( 'should reattach live range to marker', () => {
@@ -343,7 +354,7 @@ describe( 'Marker', () => {
 
 		expect( eventRange.notCalled ).to.be.true;
 		expect( eventContent.notCalled ).to.be.true;
-		expect( oldLiveRange.detach.calledOnce ).to.true;
+		expect( oldLiveRange.detach.calledOnce ).to.be.true;
 
 		newLiveRange.fire( 'change:range', null, {} );
 		newLiveRange.fire( 'change:content', null, {} );
@@ -356,14 +367,29 @@ describe( 'Marker', () => {
 		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
 		const marker = model.markers._set( 'name', range, false );
 
-		expect( marker.managedUsingOperations ).to.false;
+		expect( marker.managedUsingOperations ).to.be.false;
+
+		model.markers._set( 'name', range, true );
+
+		expect( marker.managedUsingOperations ).to.be.true;
+
+		model.markers._set( 'name', range, false );
+
+		expect( marker.managedUsingOperations ).to.be.false;
+	} );
+
+	it( 'should change affectsData flag', () => {
+		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
+		const marker = model.markers._set( 'name', range, false, false );
+
+		expect( marker.affectsData ).to.be.false;
 
-		marker._managedUsingOperations = true;
+		model.markers._set( 'name', range, false, true );
 
-		expect( marker.managedUsingOperations ).to.true;
+		expect( marker.affectsData ).to.be.true;
 
-		marker._managedUsingOperations = false;
+		model.markers._set( 'name', range, false, false );
 
-		expect( marker.managedUsingOperations ).to.false;
+		expect( marker.affectsData ).to.be.false;
 	} );
 } );

+ 18 - 15
packages/ckeditor5-engine/tests/model/operation/markeroperation.js

@@ -25,7 +25,7 @@ describe( 'MarkerOperation', () => {
 	} );
 
 	it( 'should have property type equal to "marker"', () => {
-		const op = new MarkerOperation( 'name', null, range, model.markers, 0 );
+		const op = new MarkerOperation( 'name', null, range, model.markers, 0, true );
 		expect( op.type ).to.equal( 'marker' );
 	} );
 
@@ -33,7 +33,7 @@ describe( 'MarkerOperation', () => {
 		sinon.spy( model.markers, '_set' );
 
 		model.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', null, range, model.markers, doc.version )
+			new MarkerOperation( 'name', null, range, model.markers, doc.version, true )
 		) );
 
 		expect( doc.version ).to.equal( 1 );
@@ -43,7 +43,7 @@ describe( 'MarkerOperation', () => {
 
 	it( 'should update marker in document marker collection', () => {
 		model.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', null, range, model.markers, doc.version )
+			new MarkerOperation( 'name', null, range, model.markers, doc.version, true )
 		) );
 
 		const range2 = Range.createFromParentsAndOffsets( root, 0, root, 3 );
@@ -51,7 +51,7 @@ describe( 'MarkerOperation', () => {
 		sinon.spy( model.markers, '_set' );
 
 		model.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', range, range2, model.markers, doc.version )
+			new MarkerOperation( 'name', range, range2, model.markers, doc.version, true )
 		) );
 
 		expect( doc.version ).to.equal( 2 );
@@ -61,13 +61,13 @@ describe( 'MarkerOperation', () => {
 
 	it( 'should remove marker from document marker collection', () => {
 		model.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', null, range, model.markers, doc.version )
+			new MarkerOperation( 'name', null, range, model.markers, doc.version, true )
 		) );
 
 		sinon.spy( model.markers, '_remove' );
 
 		model.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', range, null, model.markers, doc.version )
+			new MarkerOperation( 'name', range, null, model.markers, doc.version, true )
 		) );
 
 		expect( doc.version ).to.equal( 2 );
@@ -79,7 +79,7 @@ describe( 'MarkerOperation', () => {
 		sinon.spy( model.markers, 'fire' );
 
 		model.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', null, null, model.markers, doc.version )
+			new MarkerOperation( 'name', null, null, model.markers, doc.version, true )
 		) );
 
 		expect( model.markers.fire.notCalled ).to.be.true;
@@ -93,7 +93,7 @@ describe( 'MarkerOperation', () => {
 		sinon.spy( model.markers, 'fire' );
 
 		model.applyOperation( wrapInDelta(
-			new MarkerOperation( 'name', range, range, model.markers, doc.version )
+			new MarkerOperation( 'name', range, range, model.markers, doc.version, false )
 		) );
 
 		expect( model.markers.fire.notCalled ).to.be.true;
@@ -102,10 +102,10 @@ describe( 'MarkerOperation', () => {
 	it( 'should return MarkerOperation with swapped ranges as reverse operation', () => {
 		const range2 = Range.createFromParentsAndOffsets( root, 0, root, 3 );
 
-		const op1 = new MarkerOperation( 'name', null, range, model.markers, doc.version );
+		const op1 = new MarkerOperation( 'name', null, range, model.markers, doc.version, true );
 		const reversed1 = op1.getReversed();
 
-		const op2 = new MarkerOperation( 'name', range, range2, model.markers, doc.version );
+		const op2 = new MarkerOperation( 'name', range, range2, model.markers, doc.version, true );
 		const reversed2 = op2.getReversed();
 
 		expect( reversed1 ).to.be.an.instanceof( MarkerOperation );
@@ -115,15 +115,17 @@ describe( 'MarkerOperation', () => {
 		expect( reversed1.oldRange.isEqual( range ) ).to.be.true;
 		expect( reversed1.newRange ).to.be.null;
 		expect( reversed1.baseVersion ).to.equal( 1 );
+		expect( reversed1.affectsData ).to.be.true;
 
 		expect( reversed2.name ).to.equal( 'name' );
 		expect( reversed2.oldRange.isEqual( range2 ) ).to.be.true;
 		expect( reversed2.newRange.isEqual( range ) ).to.be.true;
 		expect( reversed2.baseVersion ).to.equal( 1 );
+		expect( reversed2.affectsData ).to.be.true;
 	} );
 
 	it( 'should create a MarkerOperation with the same parameters when cloned', () => {
-		const op = new MarkerOperation( 'name', null, range, model.markers, 0 );
+		const op = new MarkerOperation( 'name', null, range, model.markers, 0, true );
 		const clone = op.clone();
 
 		expect( clone ).to.be.an.instanceof( MarkerOperation );
@@ -132,7 +134,7 @@ describe( 'MarkerOperation', () => {
 
 	describe( 'toJSON', () => {
 		it( 'should create proper serialized object', () => {
-			const op = new MarkerOperation( 'name', null, range, model.markers, doc.version );
+			const op = new MarkerOperation( 'name', null, range, model.markers, doc.version, true );
 			const serialized = jsonParseStringify( op );
 
 			expect( serialized ).to.deep.equal( {
@@ -140,14 +142,15 @@ describe( 'MarkerOperation', () => {
 				baseVersion: 0,
 				name: 'name',
 				oldRange: null,
-				newRange: jsonParseStringify( range )
+				newRange: jsonParseStringify( range ),
+				affectsData: true
 			} );
 		} );
 	} );
 
 	describe( 'fromJSON', () => {
 		it( 'should create proper MarkerOperation from json object #1', () => {
-			const op = new MarkerOperation( 'name', null, range, model.markers, doc.version );
+			const op = new MarkerOperation( 'name', null, range, model.markers, doc.version, true );
 
 			const serialized = jsonParseStringify( op );
 			const deserialized = MarkerOperation.fromJSON( serialized, doc );
@@ -157,7 +160,7 @@ describe( 'MarkerOperation', () => {
 
 		it( 'should create proper MarkerOperation from json object #2', () => {
 			// Gotta love 100% CC.
-			const op = new MarkerOperation( 'name', range, null, model.markers, doc.version );
+			const op = new MarkerOperation( 'name', range, null, model.markers, doc.version, true );
 
 			const serialized = jsonParseStringify( op );
 			const deserialized = MarkerOperation.fromJSON( serialized, doc );

+ 9 - 9
packages/ckeditor5-engine/tests/model/schema.js

@@ -2270,7 +2270,7 @@ describe( 'Schema', () => {
 			() => {
 				schema.register( 'listItem', {
 					inheritAllFrom: '$block',
-					allowAttributes: [ 'indent', 'type' ]
+					allowAttributes: [ 'listIndent', 'listType' ]
 				} );
 			},
 			() => {
@@ -2351,11 +2351,11 @@ describe( 'Schema', () => {
 			root1 = new Element( '$root', null, [
 				new Element( 'paragraph', null, 'foo' ),
 				new Element( 'paragraph', { alignment: 'right' }, 'bar' ),
-				new Element( 'listItem', { type: 'x', indent: 0 }, 'foo' ),
+				new Element( 'listItem', { listType: 'x', listIndent: 0 }, 'foo' ),
 				new Element( 'heading1', null, 'foo' ),
 				new Element( 'blockQuote', null, [
 					new Element( 'paragraph', null, 'foo' ),
-					new Element( 'listItem', { type: 'x', indent: 0 }, 'foo' ),
+					new Element( 'listItem', { listType: 'x', listIndent: 0 }, 'foo' ),
 					new Element( 'image', null, [
 						new Element( 'caption', null, 'foo' )
 					] )
@@ -2522,11 +2522,11 @@ describe( 'Schema', () => {
 		} );
 
 		it( 'accepts attribute $root>listItem[indent]', () => {
-			expect( schema.checkAttribute( r1lI, 'indent' ) ).to.be.true;
+			expect( schema.checkAttribute( r1lI, 'listIndent' ) ).to.be.true;
 		} );
 
 		it( 'accepts attribute $root>listItem[type]', () => {
-			expect( schema.checkAttribute( r1lI, 'type' ) ).to.be.true;
+			expect( schema.checkAttribute( r1lI, 'listType' ) ).to.be.true;
 		} );
 
 		it( 'accepts attribute $root>image[src]', () => {
@@ -2542,11 +2542,11 @@ describe( 'Schema', () => {
 		} );
 
 		it( 'rejects attribute $root[indent]', () => {
-			expect( schema.checkAttribute( root1, 'indent' ) ).to.be.false;
+			expect( schema.checkAttribute( root1, 'listIndent' ) ).to.be.false;
 		} );
 
 		it( 'rejects attribute $root>paragraph[indent]', () => {
-			expect( schema.checkAttribute( r1p1, 'indent' ) ).to.be.false;
+			expect( schema.checkAttribute( r1p1, 'listIndent' ) ).to.be.false;
 		} );
 
 		it( 'accepts attribute $root>heading1>$text[bold]', () => {
@@ -2558,7 +2558,7 @@ describe( 'Schema', () => {
 		} );
 
 		it( 'rejects attribute $root>blockQuote[indent]', () => {
-			expect( schema.checkAttribute( r1bQ, 'indent' ) ).to.be.false;
+			expect( schema.checkAttribute( r1bQ, 'listIndent' ) ).to.be.false;
 		} );
 
 		it( 'rejects attribute $root>blockQuote[alignment]', () => {
@@ -2566,7 +2566,7 @@ describe( 'Schema', () => {
 		} );
 
 		it( 'rejects attribute $root>image[indent]', () => {
-			expect( schema.checkAttribute( r1i, 'indent' ) ).to.be.false;
+			expect( schema.checkAttribute( r1i, 'listIndent' ) ).to.be.false;
 		} );
 
 		it( 'rejects attribute $root>image[alignment]', () => {

+ 13 - 13
packages/ckeditor5-engine/tests/model/utils/insertcontent.js

@@ -247,7 +247,7 @@ describe( 'DataController utils', () => {
 				} );
 				schema.register( 'listItem', {
 					inheritAllFrom: '$block',
-					allowAttributes: [ 'type', 'indent' ]
+					allowAttributes: [ 'listType', 'listIndent' ]
 				} );
 			} );
 
@@ -370,40 +370,40 @@ describe( 'DataController utils', () => {
 
 				it( 'inserts one list item', () => {
 					setData( model, '<paragraph>f[]oo</paragraph>' );
-					insertHelper( '<listItem indent="0" type="bulleted">xyz</listItem>' );
+					insertHelper( '<listItem listIndent="0" listType="bulleted">xyz</listItem>' );
 					expect( getData( model ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
 				} );
 
 				it( 'inserts list item to empty element', () => {
 					setData( model, '<paragraph>[]</paragraph>' );
-					insertHelper( '<listItem indent="0" type="bulleted">xyz</listItem>' );
-					expect( getData( model ) ).to.equal( '<listItem indent="0" type="bulleted">xyz[]</listItem>' );
+					insertHelper( '<listItem listIndent="0" listType="bulleted">xyz</listItem>' );
+					expect( getData( model ) ).to.equal( '<listItem listIndent="0" listType="bulleted">xyz[]</listItem>' );
 				} );
 
 				it( 'inserts three list items at the end of paragraph', () => {
 					setData( model, '<paragraph>foo[]</paragraph>' );
 					insertHelper(
-						'<listItem indent="0" type="bulleted">xxx</listItem>' +
-						'<listItem indent="0" type="bulleted">yyy</listItem>' +
-						'<listItem indent="0" type="bulleted">zzz</listItem>'
+						'<listItem listIndent="0" listType="bulleted">xxx</listItem>' +
+						'<listItem listIndent="0" listType="bulleted">yyy</listItem>' +
+						'<listItem listIndent="0" listType="bulleted">zzz</listItem>'
 					);
 					expect( getData( model ) ).to.equal(
 						'<paragraph>fooxxx</paragraph>' +
-						'<listItem indent="0" type="bulleted">yyy</listItem>' +
-						'<listItem indent="0" type="bulleted">zzz[]</listItem>'
+						'<listItem listIndent="0" listType="bulleted">yyy</listItem>' +
+						'<listItem listIndent="0" listType="bulleted">zzz[]</listItem>'
 					);
 				} );
 
 				it( 'inserts two list items to an empty paragraph', () => {
 					setData( model, '<paragraph>a</paragraph><paragraph>[]</paragraph><paragraph>b</paragraph>' );
 					insertHelper(
-						'<listItem indent="0" type="bulleted">xxx</listItem>' +
-						'<listItem indent="0" type="bulleted">yyy</listItem>'
+						'<listItem listIndent="0" listType="bulleted">xxx</listItem>' +
+						'<listItem listIndent="0" listType="bulleted">yyy</listItem>'
 					);
 					expect( getData( model ) ).to.equal(
 						'<paragraph>a</paragraph>' +
-						'<listItem indent="0" type="bulleted">xxx</listItem>' +
-						'<listItem indent="0" type="bulleted">yyy[]</listItem>' +
+						'<listItem listIndent="0" listType="bulleted">xxx</listItem>' +
+						'<listItem listIndent="0" listType="bulleted">yyy[]</listItem>' +
 						'<paragraph>b</paragraph>'
 					);
 				} );

+ 77 - 2
packages/ckeditor5-engine/tests/model/writer.js

@@ -1975,13 +1975,25 @@ describe( 'Writer', () => {
 		it( 'should return marker with properly set managedUsingOperations (to true)', () => {
 			const marker = addMarker( 'name', { range, usingOperation: true } );
 
-			expect( marker.managedUsingOperations ).to.equal( true );
+			expect( marker.managedUsingOperations ).to.be.true;
 		} );
 
 		it( 'should return marker with properly set managedUsingOperations (to false)', () => {
 			const marker = addMarker( 'name', { range, usingOperation: false } );
 
-			expect( marker.managedUsingOperations ).to.equal( false );
+			expect( marker.managedUsingOperations ).to.be.false;
+		} );
+
+		it( 'should return marker with properly set affectsData (default to false)', () => {
+			const marker = addMarker( 'name', { range, usingOperation: false } );
+
+			expect( marker.affectsData ).to.be.false;
+		} );
+
+		it( 'should return marker with properly set affectsData (to false)', () => {
+			const marker = addMarker( 'name', { range, usingOperation: false, affectsData: true } );
+
+			expect( marker.affectsData ).to.be.true;
 		} );
 
 		it( 'should throw when trying to update existing marker in the document marker collection', () => {
@@ -2196,6 +2208,69 @@ describe( 'Writer', () => {
 			expect( marker.managedUsingOperations ).to.be.true;
 		} );
 
+		it( 'should allow changing affectsData property not using operations', () => {
+			addMarker( 'name', { range, usingOperation: false } );
+			updateMarker( 'name', { affectsData: false } );
+
+			const marker = model.markers.get( 'name' );
+
+			expect( marker.affectsData ).to.be.false;
+		} );
+
+		it( 'should allow changing affectsData property using operations', () => {
+			addMarker( 'name', { range, usingOperation: true } );
+			updateMarker( 'name', { affectsData: true } );
+
+			const op1 = batch.deltas[ 0 ].operations[ 0 ];
+			const op2 = batch.deltas[ 1 ].operations[ 0 ];
+			const marker = model.markers.get( 'name' );
+
+			expect( op1.affectsData ).to.be.false;
+			expect( op2.affectsData ).to.be.true;
+
+			expect( marker.affectsData ).to.be.true;
+		} );
+
+		it( 'should not change affectsData property if not provided', () => {
+			const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 );
+
+			addMarker( 'name', { range, affectsData: false, usingOperation: false } );
+			updateMarker( 'name', { range: range2 } );
+
+			const marker = model.markers.get( 'name' );
+
+			expect( marker.affectsData ).to.be.false;
+		} );
+
+		it( 'should allow changing affectsData and usingOperation', () => {
+			addMarker( 'name', { range, usingOperation: true } );
+			updateMarker( 'name', { affectsData: true, usingOperation: false } );
+
+			const op1 = batch.deltas[ 0 ].operations[ 0 ];
+			const op2 = batch.deltas[ 1 ].operations[ 0 ];
+			const marker = model.markers.get( 'name' );
+
+			expect( op1.affectsData ).to.be.false;
+			expect( op2.affectsData ).to.be.true;
+
+			expect( marker.affectsData ).to.be.true;
+		} );
+
+		it( 'should allow changing affectsData and usingOperation #2', () => {
+			const spy = sinon.spy();
+			model.on( 'applyOperation', spy );
+
+			addMarker( 'name', { range, usingOperation: false, affectsData: true } );
+			updateMarker( 'name', { usingOperation: true, affectsData: false } );
+
+			const marker = model.markers.get( 'name' );
+
+			sinon.assert.calledOnce( spy );
+			expect( spy.firstCall.args[ 1 ][ 0 ].type ).to.equal( 'marker' );
+
+			expect( marker.affectsData ).to.be.false;
+		} );
+
 		it( 'should throw when range and usingOperations were not provided', () => {
 			expect( () => {
 				addMarker( 'name', { range, usingOperation: false } );