Piotr Jasiun преди 8 години
родител
ревизия
c1f5f15265
променени са 3 файла, в които са добавени 291 реда и са изтрити 59 реда
  1. 32 59
      packages/ckeditor5-engine/src/model/document.js
  2. 98 0
      packages/ckeditor5-engine/src/model/model.js
  3. 161 0
      packages/ckeditor5-engine/tests/model/model.js

+ 32 - 59
packages/ckeditor5-engine/src/model/document.js

@@ -14,12 +14,9 @@ import './delta/basic-transformations';
 import Range from './range';
 import Position from './position';
 import RootElement from './rootelement';
-import Batch from './batch';
 import History from './history';
 import DocumentSelection from './documentselection';
-import Schema from './schema';
 import TreeWalker from './treewalker';
-import MarkerCollection from './markercollection';
 import deltaTransform from './delta/transform';
 import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
@@ -49,7 +46,10 @@ export default class Document {
 	 * Creates an empty document instance with no {@link #roots} (other than
 	 * the {@link #graveyard graveyard root}).
 	 */
-	constructor() {
+	constructor( model ) {
+
+		this.model = model;
+
 		/**
 		 * Document version. It starts from `0` and every operation increases the version number. It is used to ensure that
 		 * operations are applied on the proper document version.
@@ -62,13 +62,6 @@ export default class Document {
 		this.version = 0;
 
 		/**
-		 * Schema for this document.
-		 *
-		 * @member {module:engine/model/schema~Schema}
-		 */
-		this.schema = new Schema();
-
-		/**
 		 * Document's history.
 		 *
 		 * **Note:** Be aware that deltas applied to the document might get removed or changed.
@@ -79,14 +72,6 @@ export default class Document {
 		this.history = new History( this );
 
 		/**
-		 * Document's markers' collection.
-		 *
-		 * @readonly
-		 * @member {module:engine/model/markercollection~MarkerCollection}
-		 */
-		this.markers = new MarkerCollection();
-
-		/**
 		 * Selection done on this document.
 		 *
 		 * @readonly
@@ -95,14 +80,6 @@ export default class Document {
 		this.selection = new DocumentSelection( this );
 
 		/**
-		 * Array of pending changes. See: {@link #enqueueChanges}.
-		 *
-		 * @private
-		 * @member {Array.<Function>}
-		 */
-		this._pendingChanges = [];
-
-		/**
 		 * List of roots that are owned and managed by this document. Use {@link #createRoot} and
 		 * {@link #getRoot} to manipulate it.
 		 *
@@ -130,6 +107,32 @@ export default class Document {
 
 		// Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
 		this.createRoot( '$root', graveyardName );
+
+		this.listenTo( model, 'applyOperation', () => {
+			const operation = args[ 0 ];
+
+			if ( operation.isDocumentOperation && operation.baseVersion !== this.version ) {
+				/**
+				 * Only operations with matching versions can be applied.
+				 *
+				 * @error document-applyOperation-wrong-version
+				 * @param {module:engine/model/operation/operation~Operation} operation
+				 */
+				throw new CKEditorError(
+					'model-document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
+					{ operation } );
+			}
+		}, { priority: 'high' } );
+
+		this.listenTo( model, 'applyOperation', ( evt, args ) => {
+			const operation = args[ 0 ];
+
+			if ( operation.isDocumentOperation ) {
+				this.version++;
+				this.history.addDelta( operation.delta );
+				this.fire( 'change', operation.type, evt.return, operation.delta.batch, operation.delta.type );
+			}
+		}, { priority: 'low' } );
 	}
 
 	/**
@@ -143,36 +146,6 @@ export default class Document {
 	}
 
 	/**
-	 * This is the entry point for all document changes. All changes on the document are done using
-	 * {@link module:engine/model/operation/operation~Operation operations}. To create operations in the simple way use the
-	 * {@link module:engine/model/batch~Batch} API available via {@link #batch} method.
-	 *
-	 * @fires event:change
-	 * @param {module:engine/model/operation/operation~Operation} operation Operation to be applied.
-	 */
-	applyOperation( operation ) {
-		if ( operation.baseVersion !== this.version ) {
-			/**
-			 * Only operations with matching versions can be applied.
-			 *
-			 * @error document-applyOperation-wrong-version
-			 * @param {module:engine/model/operation/operation~Operation} operation
-			 */
-			throw new CKEditorError(
-				'model-document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
-				{ operation } );
-		}
-
-		const changes = operation._execute();
-
-		if ( operation.isDocumentOperation ) {
-			this.version++;
-			this.history.addDelta( operation.delta );
-			this.fire( 'change', operation.type, changes, operation.delta.batch, operation.delta.type );
-		}
-	}
-
-	/**
 	 * Creates a {@link module:engine/model/batch~Batch} instance which allows to change the document.
 	 *
 	 * @param {String} [type] Batch type. See {@link module:engine/model/batch~Batch#type}.
@@ -499,7 +472,7 @@ function* combineWalkers( backward, forward ) {
 
 			if ( !step.done ) {
 				done = false;
-				yield{
+				yield {
 					walker: backward,
 					value: step.value
 				};
@@ -511,7 +484,7 @@ function* combineWalkers( backward, forward ) {
 
 			if ( !step.done ) {
 				done = false;
-				yield{
+				yield {
 					walker: forward,
 					value: step.value
 				};

+ 98 - 0
packages/ckeditor5-engine/src/model/model.js

@@ -0,0 +1,98 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model
+ */
+
+import Batch from './batch';
+import Schema from './schema';
+import Document from './document';
+import MarkerCollection from './markercollection';
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+
+export default class Model {
+	constructor() {
+		this._pendingChanges = [];
+
+		this.document = new Document( this );
+
+		/**
+		 * Schema for this document.
+		 *
+		 * @member {module:engine/model/schema~Schema}
+		 */
+		this.schema = new Schema();
+
+		/**
+		 * Document's markers' collection.
+		 *
+		 * @readonly
+		 * @member {module:engine/model/markercollection~MarkerCollection}
+		 */
+		this.markers = new MarkerCollection();
+
+		this.decorate( 'applyOperation' );
+	}
+
+	change( callback ) {
+		if ( arguments.length != 1 ) {
+			throw new CKEditorError( 'model-enqueueChange-two-arguments: Model.enqueueChange expect 1 argument.' );
+		}
+
+		if ( this._pendingChanges.length === 0 ) {
+			this._pendingChanges.push( { batch: new Batch(), callback } );
+
+			return this._runPendingChanges()[ 0 ];
+		} else {
+			return callback( this._currentWriter );
+		}
+	}
+
+	enqueueChange( batch, callback ) {
+		if ( arguments.length != 2 ) {
+			throw new CKEditorError( 'model-enqueueChange-two-arguments: Model.enqueueChange expect 2 arguments.' );
+		}
+
+		this._pendingChanges.push( { batch, callback } );
+
+		if ( this._pendingChanges.length == 1 ) {
+			this._runPendingChanges();
+
+			this.fire( 'changesDone' );
+		}
+	}
+
+	_runPendingChanges() {
+		const ret = [];
+
+		while ( this._pendingChanges.length ) {
+			this._currentWriter = this._pendingChanges[ 0 ].batch;
+
+			ret.push( this._pendingChanges[ 0 ].callback( this._currentWriter ) );
+
+			this.fire( 'change' );
+
+			this._pendingChanges.shift();
+
+			this._currentWriter = null;
+		}
+
+		this.fire( 'changesDone' );
+
+		return ret;
+	}
+
+	applyOperation( operation ) {
+		return operation._execute();
+	}
+
+	transformDeltas() {
+		// ...
+	}
+}
+
+mix( Model, ObservableMixin );

+ 161 - 0
packages/ckeditor5-engine/tests/model/model.js

@@ -0,0 +1,161 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Model from '../../src/model/model';
+import Batch from '../../src/model/batch';
+
+describe( 'Model', () => {
+	let model;
+	let changes = '';
+
+	beforeEach( () => {
+		model = new Model();
+		changes = '';
+	} );
+
+	describe( 'change & enqueueChange', () => {
+		it( 'should execute changes immediately', () => {
+			model.change( () => {
+				changes += 'A';
+			} );
+
+			expect( changes ).to.equal( 'A' );
+		} );
+
+		it( 'should pass returned value', () => {
+			const ret = model.change( () => {
+				changes += 'A';
+
+				return 'B';
+			} );
+
+			changes += ret;
+
+			expect( changes ).to.equal( 'AB' );
+		} );
+
+		it( 'should not mixed the order when nested change is called', () => {
+			const ret = model.change( () => {
+				changes += 'A';
+
+				nested();
+
+				return 'D';
+			} );
+
+			changes += ret;
+
+			expect( changes ).to.equal( 'ABCD' );
+
+			function nested() {
+				const ret = model.change( () => {
+					changes += 'B';
+
+					return 'C';
+				} );
+
+				changes += ret;
+			}
+		} );
+
+		it( 'should execute enqueueChanges immediately if its the first block', () => {
+			model.enqueueChange( new Batch(), () => {
+				changes += 'A';
+
+				nested();
+			} );
+
+			expect( changes ).to.equal( 'ABC' );
+
+			function nested() {
+				const ret = model.change( () => {
+					changes += 'B';
+
+					return 'C';
+				} );
+
+				changes += ret;
+			}
+		} );
+
+		it( 'should be possible to enqueueChanges immediately if its the first block', () => {
+			model.enqueueChange( new Batch(), () => {
+				changes += 'A';
+
+				nested();
+			} );
+
+			expect( changes ).to.equal( 'AB' );
+
+			function nested() {
+				const ret = model.change( () => {
+					changes += 'B';
+				} );
+			}
+		} );
+
+		it( 'should be possible to nest change in enqueueChanges', () => {
+			model.enqueueChange( new Batch(), () => {
+				changes += 'A';
+
+				nested();
+
+				changes += 'D';
+			} );
+
+			expect( changes ).to.equal( 'ABCD' );
+
+			function nested() {
+				const ret = model.change( () => {
+					changes += 'B';
+
+					return 'C';
+				} );
+
+				changes += ret;
+			}
+		} );
+
+		it( 'should be possible to nest enqueueChanges in enqueueChanges', () => {
+			model.enqueueChange( new Batch(), () => {
+				changes += 'A';
+
+				nestedEnqueue();
+
+				changes += 'B';
+			} );
+
+			expect( changes ).to.equal( 'ABC' );
+
+			function nestedEnqueue() {
+				model.enqueueChange( new Batch(), () => {
+					changes += 'C';
+				} );
+			}
+		} );
+
+		it( 'should be possible to nest enqueueChanges in changes', () => {
+			const ret = model.change( () => {
+				changes += 'A';
+
+				nestedEnqueue();
+
+				changes += 'B';
+
+				return 'D';
+			} );
+
+			changes += ret;
+
+			expect( changes ).to.equal( 'ABCD' );
+
+			function nestedEnqueue() {
+				model.enqueueChange( new Batch(), () => {
+					changes += 'C';
+				} );
+			}
+		} );
+	} );
+} );