Forráskód Böngészése

Added: engine.model.MarkersCollection.

Szymon Cofalik 9 éve
szülő
commit
a724a19e3c

+ 10 - 1
packages/ckeditor5-engine/src/model/document.js

@@ -19,6 +19,7 @@ import History from './history.js';
 import LiveSelection from './liveselection.js';
 import Schema from './schema.js';
 import TreeWalker from './treewalker.js';
+import MarkersCollection from './markerscollection.js';
 import clone from '../../utils/lib/lodash/clone.js';
 import EmitterMixin from '../../utils/emittermixin.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
@@ -75,13 +76,21 @@ export default class Document {
 		/**
 		 * Document's history.
 		 *
-		 * **Note:** Be aware that deltas applied to the stored deltas might be removed or changed.
+		 * **Note:** Be aware that deltas applied to the document might get removed or changed.
 		 *
 		 * @readonly
 		 * @member {module:engine/model/history~History}
 		 */
 		this.history = new History( this );
 
+		/**
+		 * Document's markers' collection.
+		 *
+		 * @readonly
+		 * @member {module:engine/model/markerscollection~MarkersCollection} engine.model.Document#markers
+		 */
+		this.markers = new MarkersCollection();
+
 		/**
 		 * Array of pending changes. See: {@link #enqueueChanges}.
 		 *

+ 142 - 0
packages/ckeditor5-engine/src/model/markerscollection.js

@@ -0,0 +1,142 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import LiveRange from './liverange.js';
+import Range from './range.js';
+import EmitterMixin from '../../utils/emittermixin.js';
+import CKEditorError from '../../utils/ckeditorerror.js';
+import mix from '../../utils/mix.js';
+
+/**
+ * Manages and stores markers.
+ *
+ * Markers are simply {@link module:engine/model/liverange~LiveRange live ranges} added to `MarkersCollection`.
+ * Markers are used to represent information connected with model document. In contrary to
+ * {@link module:engine/model/node~Node nodes}, which are bits of data, markers are marking a part of model document.
+ * Each live range/marker is added with `name` parameter. Name is used to group and identify markers. Multiple live
+ * ranges can be added under the same name.
+ *
+ * Whenever live range is added or removed from markers collection, markers collection fires `addMarker` and `removeMarker`
+ * events. Same happens when a live range is changed due to changes in the model (both events are fired, `removeMarker` first,
+ * then `addMarker`).
+ *
+ * Markers can be converted to view by adding appropriate converters for
+ * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addMarker addMarker} and
+ * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:removeMarker removeMarker}
+ * events, or by building converters for {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}
+ * using {@link module:engine/conversion/buildmodelconverter~buildModelConverter model converter builder}.
+ *
+ * Markers are similar to adding and converting attributes on nodes. The difference is that attribute is connected to
+ * a given node (i.e. a character is bold no matter if it gets moved or content around it changes). Markers on the
+ * other hand are continuous ranges (i.e. if a character from inside of marker range is moved somewhere else, marker
+ * range is shrunk and the character does not have any attribute or information that it was in the marked range).
+ *
+ * @memberOf engine.model
+ */
+export default class MarkersCollection {
+	/**
+	 * Creates a markers collection.
+	 */
+	constructor() {
+		/**
+		 * Stores range to marker name bindings for added ranges.
+		 *
+		 * @private
+		 * @member {Map} #_markerNames
+		 */
+		this._markerNames = new Map();
+	}
+
+	/**
+	 * Sets a name for given live range and adds it to the markers collection. Multiple live ranges may have same name.
+	 * Markers can also be grouped while still having different names, i.e.: `search:22`, `search:37`.
+	 *
+	 * Throws, if given `liveRange` is not an instance of {@link module:engine/model/liverange~LiveRange}.
+	 *
+	 * Throws, if given {@link module:engine/model/liverange~LiveRange LiveRange} instance is already added to the collection.
+	 *
+	 * `MarkersCollection` instance listens to `change` event of all live ranges added to it and fires `removeMarker`
+	 * and `addMarker` events when the live range changes.
+	 *
+	 * @param {module:engine/model/liverange~LiveRange} liveRange Live range to be added as a marker to markers collection.
+	 * @param {String} markerName Name to be associated with given `liveRange`.
+	 */
+	addRange( liveRange, markerName ) {
+		if ( this._markerNames.has( liveRange ) ) {
+			/**
+			 * Given range instance was already added.
+			 *
+			 * @error markers-collection-add-range-range-already-added
+			 */
+			throw new CKEditorError( 'markers-collection-add-range-already-added: Given range instance was already added.' );
+		}
+
+		if ( !( liveRange instanceof LiveRange ) ) {
+			/**
+			 * Added range is not an instance of LiveRange.
+			 *
+			 * @error markers-collection-add-range-not-live-range
+			 */
+			throw new CKEditorError( 'markers-collection-add-range-not-live-range: Added range is not an instance of LiveRange.' );
+		}
+
+		this._markerNames.set( liveRange, markerName );
+		this.fire( 'addMarker', markerName, Range.createFromRange( liveRange ) );
+	}
+
+	/**
+	 * Removes a live range from markers collection and makes markers collection stop listening to that live range
+	 * `change` event.
+	 *
+	 * @param {module:engine/model/liverange~LiveRange} liveRange Live range to be removed from markers collection.
+	 * @returns {Boolean} `true` is passed `liveRange` was found and removed from the markers collection, `false` otherwise.
+	 */
+	removeRange( liveRange ) {
+		const markerName = this._markerNames.get( liveRange );
+
+		if ( markerName ) {
+			this._markerNames.delete( liveRange );
+			this.fire( 'removeMarker', markerName, Range.createFromRange( liveRange ) );
+
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Substitutes given `oldLiveRange`, that was already added to the markers collection, with given `newLiveRange`.
+	 *
+	 * This method is basically a wrapper for using {@link module:engine/model/markerscollection~MarkersCollection#removeRange removeRange}
+	 * followed by using {@link module:engine/model/markerscollection~MarkersCollection#addRange addRange}.
+	 *
+	 * **Note**: this method does not change properties of `oldLiveRange`.
+	 *
+	 * @param {module:engine/model/liverange~LiveRange} oldLiveRange Live range to be changed.
+	 * @param {module:engine/model/liverange~LiveRange} newLiveRange Live range to be added.
+	 * @returns {Boolean} `true` if `oldLiveRange` was found and changed, `false` otherwise.
+	 */
+	updateRange( oldLiveRange, newLiveRange ) {
+		const markerName = this._markerNames.get( oldLiveRange );
+
+		if ( markerName ) {
+			this.removeRange( oldLiveRange );
+			this.addRange( newLiveRange, markerName );
+
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Destroys markers collection.
+	 */
+	destroy() {
+		this.stopListening();
+	}
+}
+
+mix( MarkersCollection, EmitterMixin );

+ 142 - 0
packages/ckeditor5-engine/tests/model/markerscollection.js

@@ -0,0 +1,142 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: model */
+
+import MarkersCollection from 'ckeditor5/engine/model/markerscollection.js';
+import Range from 'ckeditor5/engine/model/range.js';
+import LiveRange from 'ckeditor5/engine/model/liverange.js';
+import Document from 'ckeditor5/engine/model/document.js';
+import CKEditorError from 'ckeditor5/utils/ckeditorerror.js';
+
+let markers, live, doc, root;
+
+beforeEach( () => {
+	doc = new Document();
+	markers = new MarkersCollection();
+
+	root = doc.createRoot();
+	live = LiveRange.createFromParentsAndOffsets( root, 0, root, 4 );
+} );
+
+afterEach( () => {
+	markers.destroy();
+	live.detach();
+} );
+
+describe( 'MarkersCollection', () => {
+	describe( 'addRange', () => {
+		it( 'should throw if passed parameter is not a LiveRange', () => {
+			const range = Range.createFromParentsAndOffsets( root, 1, root, 3 );
+
+			expect( () => {
+				markers.addRange( range, 'name' );
+			} ).to.throw( CKEditorError, /^markers-collection-add-range-not-live-range/ );
+		} );
+
+		it( 'should fire addMarker event when range is added', () => {
+			sinon.spy( markers, 'fire' );
+
+			markers.on( 'addMarker', ( evt, name, range ) => {
+				expect( name ).to.equal( 'name' );
+				expect( range.isEqual( live ) ).to.be.true;
+				expect( range ).not.to.equal( live );
+			} );
+
+			markers.addRange( live, 'name' );
+
+			expect( markers.fire.calledWith( 'addMarker' ) ).to.be.true;
+		} );
+
+		it( 'should throw if given range was already added', () => {
+			markers.addRange( live, 'name' );
+
+			expect( () => {
+				markers.addRange( live, 'otherName' );
+			} ).to.throw( CKEditorError, /^markers-collection-add-range-already-added/ );
+		} );
+	} );
+
+	describe( 'removeRange', () => {
+		it( 'should return false if range has not been found in collection', () => {
+			const result = markers.removeRange( live );
+
+			expect( result ).to.be.false;
+		} );
+
+		it( 'should return true and fire removeMarker event if range is removed', () => {
+			markers.addRange( live, 'name' );
+
+			sinon.spy( markers, 'fire' );
+
+			markers.on( 'removeMarker', ( evt, name, range ) => {
+				expect( name ).to.equal( 'name' );
+				expect( range.isEqual( live ) ).to.be.true;
+				expect( range ).not.to.equal( live );
+			} );
+
+			const result = markers.removeRange( live );
+
+			expect( result ).to.be.true;
+			expect( markers.fire.calledWith( 'removeMarker' ) ).to.be.true;
+		} );
+
+		it( 'should make MarkersCollection stop listening to range change event', () => {
+			markers.addRange( live, 'name' );
+			markers.removeRange( live );
+
+			sinon.spy( markers, 'fire' );
+
+			// Simulate LiveRange change.
+			const oldRange = Range.createFromRange( live );
+			live.end.path[ 0 ]++;
+			live.fire( 'change', oldRange );
+
+			expect( markers.fire.called ).to.be.false;
+		} );
+	} );
+
+	describe( 'updateRange', () => {
+		let newLive;
+
+		beforeEach( () => {
+			newLive = LiveRange.createFromParentsAndOffsets( root, 1, root, 5 );
+		} );
+
+		afterEach( () => {
+			newLive.detach();
+		} );
+
+		it( 'should return false if given range was not found in collection', () => {
+			const result = markers.updateRange( live, newLive );
+
+			expect( result ).to.be.false;
+		} );
+
+		it( 'should return true and use removeRange and addRange methods if range was found in collection', () => {
+			const newLive = LiveRange.createFromParentsAndOffsets( root, 1, root, 5 );
+			markers.addRange( live, 'name' );
+
+			sinon.spy( markers, 'removeRange' );
+			sinon.spy( markers, 'addRange' );
+
+			const result = markers.updateRange( live, newLive );
+
+			expect( markers.removeRange.calledWith( live ) ).to.be.true;
+			expect( markers.addRange.calledWith( newLive, 'name' ) ).to.be.true;
+			expect( result ).to.be.true;
+		} );
+	} );
+
+	describe( 'destroy', () => {
+		it( 'should make MarkersCollection stop listening to all events', () => {
+			sinon.spy( markers, 'stopListening' );
+
+			markers.destroy();
+
+			expect( markers.stopListening.calledWithExactly() ).to.be.true;
+		} );
+	} );
+} );