浏览代码

Merge pull request #1818 from ckeditor/i/6133

Feature: Introduced `DocumentSelection#event:change:marker`. Closes ckeditor/ckeditor5#6133.
Piotrek Koszuliński 5 年之前
父节点
当前提交
1be565daf0

+ 25 - 0
packages/ckeditor5-engine/src/model/documentselection.js

@@ -63,6 +63,7 @@ export default class DocumentSelection {
 
 		this._selection.delegate( 'change:range' ).to( this );
 		this._selection.delegate( 'change:attribute' ).to( this );
+		this._selection.delegate( 'change:marker' ).to( this );
 	}
 
 	/**
@@ -542,6 +543,17 @@ mix( DocumentSelection, EmitterMixin );
  * @param {Array.<String>} attributeKeys Array containing keys of attributes that changed.
  */
 
+/**
+ * Fired when selection marker(s) changed.
+ *
+ * @event change:marker
+ * @param {Boolean} directChange This is always set to `false` in case of `change:marker` event as there is no possibility
+ * to change markers directly through {@link module:engine/model/documentselection~DocumentSelection} API.
+ * See also {@link module:engine/model/documentselection~DocumentSelection#event:change:range} and
+ * {@link module:engine/model/documentselection~DocumentSelection#event:change:attribute}.
+ * @param {Array.<module:engine/model/markercollection~Marker>} oldMarkers Markers in which the selection was before the change.
+ */
+
 // `LiveSelection` is used internally by {@link module:engine/model/documentselection~DocumentSelection} and shouldn't be used directly.
 //
 // LiveSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
@@ -721,11 +733,13 @@ class LiveSelection extends Selection {
 	setTo( selectable, optionsOrPlaceOrOffset, options ) {
 		super.setTo( selectable, optionsOrPlaceOrOffset, options );
 		this._updateAttributes( true );
+		this._updateMarkers();
 	}
 
 	setFocus( itemOrPosition, offset ) {
 		super.setFocus( itemOrPosition, offset );
 		this._updateAttributes( true );
+		this._updateMarkers();
 	}
 
 	setAttribute( key, value ) {
@@ -830,6 +844,7 @@ class LiveSelection extends Selection {
 
 	_updateMarkers() {
 		const markers = [];
+		let changed = false;
 
 		for ( const marker of this._model.markers ) {
 			const markerRange = marker.getRange();
@@ -841,17 +856,27 @@ class LiveSelection extends Selection {
 			}
 		}
 
+		const oldMarkers = Array.from( this.markers );
+
 		for ( const marker of markers ) {
 			if ( !this.markers.has( marker ) ) {
 				this.markers.add( marker );
+
+				changed = true;
 			}
 		}
 
 		for ( const marker of Array.from( this.markers ) ) {
 			if ( !markers.includes( marker ) ) {
 				this.markers.remove( marker );
+
+				changed = true;
 			}
 		}
+
+		if ( changed ) {
+			this.fire( 'change:marker', { oldMarkers, directChange: false } );
+		}
 	}
 
 	// Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.

+ 218 - 0
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -442,6 +442,224 @@ describe( 'DocumentSelection', () => {
 
 			expect( selection.markers.map( marker => marker.name ) ).to.have.members( [ 'marker' ] );
 		} );
+
+		describe( 'should fire change:marker event when', () => {
+			// Set marker to range 0-4.
+			beforeEach( () => {
+				model.change( writer => {
+					writer.addMarker( 'marker-1', {
+						range: writer.createRange(
+							writer.createPositionFromPath( root, [ 2, 0 ] ),
+							writer.createPositionFromPath( root, [ 2, 4 ] )
+						),
+						usingOperation: false
+					} );
+				} );
+			} );
+
+			it( 'selection ranges change (marker added to the selection)', () => {
+				const spy = sinon.spy();
+
+				model.change( writer => {
+					// The selection has no markers before the change.
+					model.document.selection.on( 'change:marker', ( evt, data ) => {
+						expect( data.oldMarkers ).to.deep.equal( [] );
+						spy();
+					} );
+
+					// Move selection to 1-2, that is inside 0-4 marker.
+					writer.setSelection( writer.createRange(
+						writer.createPositionFromPath( root, [ 2, 1 ] ),
+						writer.createPositionFromPath( root, [ 2, 2 ] )
+					) );
+				} );
+
+				expect( spy.calledOnce ).to.be.true;
+			} );
+
+			it( 'selection ranges change (marker removed from the selection)', () => {
+				const spy = sinon.spy();
+
+				model.change( writer => {
+					writer.setSelection( writer.createRange(
+						writer.createPositionFromPath( root, [ 2, 1 ] ),
+						writer.createPositionFromPath( root, [ 2, 2 ] )
+					) );
+
+					// The selection is in a marker before the change.
+					model.document.selection.on( 'change:marker', ( evt, data ) => {
+						expect( data.oldMarkers.map( marker => marker.name ) ).to.deep.equal( [ 'marker-1' ] );
+						spy();
+					} );
+
+					// Move the selection out of the marker.
+					writer.setSelection( writer.createPositionFromPath( root, [ 2, 5 ] ) );
+				} );
+
+				expect( spy.calledOnce ).to.be.true;
+			} );
+
+			it( 'selection focus changes (marker removed from the selection)', () => {
+				const spy = sinon.spy();
+
+				model.change( writer => {
+					writer.setSelection( writer.createPositionFromPath( root, [ 2, 2 ] ) );
+
+					// The selection is in a marker before the change.
+					model.document.selection.on( 'change:marker', ( evt, data ) => {
+						expect( data.oldMarkers.map( marker => marker.name ) ).to.deep.equal( [ 'marker-1' ] );
+						spy();
+					} );
+
+					// Move the selection focus out of the marker.
+					writer.setSelectionFocus( writer.createPositionFromPath( root, [ 2, 5 ] ) );
+				} );
+
+				expect( spy.calledOnce ).to.be.true;
+			} );
+
+			it( 'a new marker contains the selection', () => {
+				const spy = sinon.spy();
+
+				model.change( writer => {
+					writer.setSelection( writer.createPositionFromPath( root, [ 2, 5 ] ) );
+
+					// The selection is not in a marker before the change.
+					model.document.selection.on( 'change:marker', ( evt, data ) => {
+						expect( data.oldMarkers ).to.deep.equal( [] );
+						spy();
+					} );
+
+					writer.updateMarker( 'marker-1', {
+						range: writer.createRange(
+							writer.createPositionFromPath( root, [ 2, 0 ] ),
+							writer.createPositionFromPath( root, [ 2, 6 ] )
+						)
+					} );
+				} );
+
+				expect( spy.calledOnce ).to.be.true;
+			} );
+
+			it( 'a marker stops contains the selection', () => {
+				const spy = sinon.spy();
+
+				model.change( writer => {
+					writer.setSelection( writer.createPositionFromPath( root, [ 2, 3 ] ) );
+
+					// The selection is in a marker before the change.
+					model.document.selection.on( 'change:marker', ( evt, data ) => {
+						expect( data.oldMarkers.map( marker => marker.name ) ).to.deep.equal( [ 'marker-1' ] );
+						spy();
+					} );
+
+					writer.updateMarker( 'marker-1', {
+						range: writer.createRange(
+							writer.createPositionFromPath( root, [ 2, 0 ] ),
+							writer.createPositionFromPath( root, [ 2, 1 ] )
+						)
+					} );
+				} );
+
+				expect( spy.calledOnce ).to.be.true;
+			} );
+		} );
+
+		describe( 'should not fire change:marker event when', () => {
+			// Set marker to range 0-4.
+			beforeEach( () => {
+				model.change( writer => {
+					writer.addMarker( 'marker-1', {
+						range: writer.createRange(
+							writer.createPositionFromPath( root, [ 2, 0 ] ),
+							writer.createPositionFromPath( root, [ 2, 4 ] )
+						),
+						usingOperation: false
+					} );
+				} );
+			} );
+
+			it( 'selection ranges change does not change selection markers (no markers)', () => {
+				const spy = sinon.spy();
+
+				model.document.selection.on( 'change:marker', spy );
+
+				model.change( writer => {
+					writer.setSelection( writer.createPositionFromPath( root, [ 2, 5 ] ) );
+				} );
+
+				expect( spy.called ).to.be.false;
+			} );
+
+			it( 'selection ranges change does not change selection markers (same markers)', () => {
+				model.change( writer => {
+					writer.setSelection( writer.createPositionFromPath( root, [ 2, 2 ] ) );
+				} );
+
+				const spy = sinon.spy();
+
+				model.document.selection.on( 'change:marker', spy );
+
+				model.change( writer => {
+					writer.setSelection( writer.createPositionFromPath( root, [ 2, 3 ] ) );
+				} );
+
+				expect( spy.called ).to.be.false;
+			} );
+
+			it( 'selection focus change does not change selection markers', () => {
+				model.change( writer => {
+					writer.setSelection( writer.createPositionFromPath( root, [ 2, 2 ] ) );
+				} );
+
+				const spy = sinon.spy();
+
+				model.document.selection.on( 'change:marker', spy );
+
+				model.change( writer => {
+					writer.setSelectionFocus( writer.createPositionFromPath( root, [ 2, 3 ] ) );
+				} );
+
+				expect( spy.called ).to.be.false;
+			} );
+
+			it( 'changed marker still contains the selection', () => {
+				model.change( writer => {
+					writer.setSelection( writer.createPositionFromPath( root, [ 2, 2 ] ) );
+				} );
+
+				const spy = sinon.spy();
+
+				model.document.selection.on( 'change:marker', spy );
+
+				model.change( writer => {
+					writer.updateMarker( 'marker-1', {
+						range: writer.createRange(
+							writer.createPositionFromPath( root, [ 2, 0 ] ),
+							writer.createPositionFromPath( root, [ 2, 5 ] )
+						)
+					} );
+				} );
+
+				expect( spy.called ).to.be.false;
+			} );
+
+			it( 'removed marker did not contain the selection', () => {
+				model.change( writer => {
+					writer.setSelection( writer.createPositionFromPath( root, [ 2, 5 ] ) );
+				} );
+
+				const spy = sinon.spy();
+
+				model.document.selection.on( 'change:marker', spy );
+
+				model.change( writer => {
+					writer.removeMarker( 'marker-1' );
+				} );
+
+				expect( spy.called ).to.be.false;
+			} );
+		} );
 	} );
 
 	describe( 'destroy()', () => {