Переглянути джерело

Merge pull request #111 from ckeditor/t/97

t/97: Document.enqueue method && changesDone event
Szymon Cofalik 10 роки тому
батько
коміт
1d2ca4f7ea

+ 39 - 0
packages/ckeditor5-engine/src/treemodel/document.js

@@ -55,6 +55,14 @@ CKEDITOR.define( [
 			 * @property {Number} version
 			 */
 			this.version = 0;
+
+			/**
+			 * Array of pending changes. See: {@link #enqueueChanges}.
+			 *
+			 * @private
+			 * @property {Array.<Function>}
+			 */
+			this._pendingChanges = [];
 		}
 
 		/**
@@ -72,6 +80,8 @@ CKEDITOR.define( [
 		 * {@link treeModel.operation.Operation operations}. To create operations in the simple way use the
 		 * {@link treeModel.Batch} API available via {@link #batch} method.
 		 *
+		 * This method calls {@link #change} event.
+		 *
 		 * @param {treeModel.operation.Operation} operation Operation to be applied.
 		 */
 		applyOperation( operation ) {
@@ -131,6 +141,29 @@ CKEDITOR.define( [
 			return root;
 		}
 
+		/**
+		 * Enqueue a callback with document changes. Any changes to be done on document (mostly using {@link #batch} should
+		 * be placed in the queued callback. If no other plugin is changing document at the moment, the callback will be
+		 * called immediately. Otherwise it will wait for all previously queued changes to finish happening. This way
+		 * queued callback will not interrupt other callbacks.
+		 *
+		 * When all queued changes are done {@link #changesDone} event is fired.
+		 *
+		 * @param {Function} callback Callback to enqueue.
+		 */
+		enqueueChanges( callback ) {
+			this._pendingChanges.push( callback );
+
+			if ( this._pendingChanges.length == 1 ) {
+				while ( this._pendingChanges.length ) {
+					this._pendingChanges[ 0 ]();
+					this._pendingChanges.shift();
+				}
+
+				this.fire( 'changesDone' );
+			}
+		}
+
 		/**
 		 * Returns top-level root by it's name.
 		 *
@@ -181,6 +214,12 @@ CKEDITOR.define( [
 		 * is `undefined` it means that attribute was removed. Otherwise it contains changed or inserted attribute.
 		 * @param {treeModel.Batch} {@link treeModel.Batch} of changes which this change is a part of.
 		 */
+
+		/**
+		 * Fired when all queued document changes are done. See {@link #enqueueChanges}.
+		 *
+		 * @event changesDone
+		 */
 	}
 
 	utils.extend( Document.prototype, EmitterMixin );

+ 76 - 19
packages/ckeditor5-engine/tests/treemodel/document/document.js

@@ -24,36 +24,36 @@ describe( 'Document', () => {
 		CKEditorError = modules.ckeditorerror;
 	} );
 
-	let document;
+	let doc;
 
 	beforeEach( () => {
-		document = new Document();
+		doc = new Document();
 	} );
 
 	describe( 'constructor', () => {
 		it( 'should create Document with no data and empty graveyard', () => {
-			expect( document ).to.have.property( 'roots' ).that.is.instanceof( Map );
-			expect( document.roots.size ).to.equal( 1 );
-			expect( document.graveyard ).to.be.instanceof( RootElement );
-			expect( document.graveyard.getChildCount() ).to.equal( 0 );
+			expect( doc ).to.have.property( 'roots' ).that.is.instanceof( Map );
+			expect( doc.roots.size ).to.equal( 1 );
+			expect( doc.graveyard ).to.be.instanceof( RootElement );
+			expect( doc.graveyard.getChildCount() ).to.equal( 0 );
 		} );
 	} );
 
 	describe( 'createRoot', () => {
 		it( 'should create a new RootElement, add it to roots map and return it', () => {
-			let root = document.createRoot( 'root' );
+			let root = doc.createRoot( 'root' );
 
-			expect( document.roots.size ).to.equal( 2 );
+			expect( doc.roots.size ).to.equal( 2 );
 			expect( root ).to.be.instanceof( RootElement );
 			expect( root.getChildCount() ).to.equal( 0 );
 		} );
 
 		it( 'should throw an error when trying to create a second root with the same name', () => {
-			document.createRoot( 'root' );
+			doc.createRoot( 'root' );
 
 			expect(
 				() => {
-					document.createRoot( 'root' );
+					doc.createRoot( 'root' );
 				}
 			).to.throw( CKEditorError, /document-createRoot-name-exists/ );
 		} );
@@ -61,8 +61,8 @@ describe( 'Document', () => {
 
 	describe( 'getRoot', () => {
 		it( 'should return a RootElement previously created with given name', () => {
-			let newRoot = document.createRoot( 'root' );
-			let getRoot = document.getRoot( 'root' );
+			let newRoot = doc.createRoot( 'root' );
+			let getRoot = doc.getRoot( 'root' );
 
 			expect( getRoot ).to.equal( newRoot );
 		} );
@@ -70,7 +70,7 @@ describe( 'Document', () => {
 		it( 'should throw an error when trying to get non-existent root', () => {
 			expect(
 				() => {
-					document.getRoot( 'root' );
+					doc.getRoot( 'root' );
 				}
 			).to.throw( CKEditorError, /document-createRoot-root-not-exist/ );
 		} );
@@ -90,10 +90,10 @@ describe( 'Document', () => {
 				_execute: sinon.stub().returns( data )
 			};
 
-			document.on( 'change', changeCallback );
-			document.applyOperation( operation );
+			doc.on( 'change', changeCallback );
+			doc.applyOperation( operation );
 
-			expect( document.version ).to.equal( 1 );
+			expect( doc.version ).to.equal( 1 );
 			sinon.assert.calledOnce( operation._execute );
 
 			sinon.assert.calledOnce( changeCallback );
@@ -109,7 +109,7 @@ describe( 'Document', () => {
 
 			expect(
 				() => {
-					document.applyOperation( operation );
+					doc.applyOperation( operation );
 				}
 			).to.throw( CKEditorError, /document-applyOperation-wrong-version/ );
 		} );
@@ -117,10 +117,67 @@ describe( 'Document', () => {
 
 	describe( 'batch', () => {
 		it( 'should create a new batch with the document property', () => {
-			const batch = document.batch();
+			const batch = doc.batch();
 
 			expect( batch ).to.be.instanceof( Batch );
-			expect( batch ).to.have.property( 'doc' ).that.equals( document );
+			expect( batch ).to.have.property( 'doc' ).that.equals( doc );
+		} );
+	} );
+
+	describe( 'enqueue', () => {
+		it( 'should be executed immediately and fire changesDone event', () => {
+			let order = [];
+
+			doc.on( 'changesDone', () => order.push( 'done' ) );
+
+			doc.enqueueChanges( () => order.push( 'enqueue1' ) );
+
+			expect( order ).to.have.length( 2 );
+			expect( order[ 0 ] ).to.equal( 'enqueue1' );
+			expect( order[ 1 ] ).to.equal( 'done' );
+		} );
+
+		it( 'should fire done every time queue is empty', () => {
+			let order = [];
+
+			doc.on( 'changesDone', () => order.push( 'done' ) );
+
+			doc.enqueueChanges( () => order.push( 'enqueue1' ) );
+			doc.enqueueChanges( () => order.push( 'enqueue2' ) );
+
+			expect( order ).to.have.length( 4 );
+			expect( order[ 0 ] ).to.equal( 'enqueue1' );
+			expect( order[ 1 ] ).to.equal( 'done' );
+			expect( order[ 2 ] ).to.equal( 'enqueue2' );
+			expect( order[ 3 ] ).to.equal( 'done' );
+		} );
+
+		it( 'should put callbacks in the proper order', () => {
+			let order = [];
+
+			doc.on( 'changesDone', () => order.push( 'done' ) );
+
+			doc.enqueueChanges( () => {
+				order.push( 'enqueue1 start' );
+				doc.enqueueChanges( () => {
+					order.push( 'enqueue2 start' );
+					doc.enqueueChanges( () => order.push( 'enqueue4' ) );
+					order.push( 'enqueue2 end' );
+				} );
+
+				doc.enqueueChanges( () => order.push( 'enqueue3' ) );
+
+				order.push( 'enqueue1 end' );
+			} );
+
+			expect( order ).to.have.length( 7 );
+			expect( order[ 0 ] ).to.equal( 'enqueue1 start' );
+			expect( order[ 1 ] ).to.equal( 'enqueue1 end' );
+			expect( order[ 2 ] ).to.equal( 'enqueue2 start' );
+			expect( order[ 3 ] ).to.equal( 'enqueue2 end' );
+			expect( order[ 4 ] ).to.equal( 'enqueue3' );
+			expect( order[ 5 ] ).to.equal( 'enqueue4' );
+			expect( order[ 6 ] ).to.equal( 'done' );
 		} );
 	} );
 } );