Browse Source

Merge branch 'master' into t/ckeditor5-list/103

Piotrek Koszuliński 7 years ago
parent
commit
c32961785c

+ 22 - 3
packages/ckeditor5-engine/src/model/differ.js

@@ -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;
@@ -189,17 +189,20 @@ export default class Differ {
 	 * @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,22 @@ export default class Differ {
 	}
 
 	/**
+	 * Checks whether some of buffered marker affects the editor data or whether some element changed.
+	 *
+	 * @returns {Boolean} `true` if buffered markers or changes in elements affects the editor data.
+	 */
+	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.
 	 *

+ 31 - 9
packages/ckeditor5-engine/src/model/document.js

@@ -147,11 +147,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 +168,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 );
 				} );
 			}
 		} );
@@ -379,18 +384,35 @@ export default class Document {
 	 *			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 fires 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.
 	 */
+
+	/**
+	 * Fired when the data changes, which includes:
+	 * * 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's changes, then look at 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

+ 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.

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

@@ -794,6 +794,11 @@ 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 {@link module:core/editor/editor~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 +807,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 +838,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 +859,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 +878,11 @@ 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 {@link module:core/editor/editor~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 +897,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 +924,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 +964,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 +1001,7 @@ export default class Writer {
 
 		const oldRange = marker.getRange();
 
-		applyMarkerOperation( this, name, oldRange, null );
+		applyMarkerOperation( this, name, oldRange, null, marker.affectsData );
 	}
 
 	/**
@@ -1325,12 +1350,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 );

+ 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 );

+ 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 } );