Przeglądaj źródła

Merge pull request #6 from ckeditor/t/5

Implemented ChangeBuffer.
Piotrek Koszuliński 9 lat temu
rodzic
commit
fbe12a44a6

+ 132 - 0
packages/ckeditor5-typing/src/changebuffer.js

@@ -0,0 +1,132 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import utils from '../utils/utils.js';
+
+/**
+ * Change buffer allows to group atomic changes (like characters that have been typed) into
+ * {@link engine.treeModel.batch.Batch batches}.
+ *
+ * Batches represent single undo steps, hence changes added to one single batch are undone together.
+ *
+ * The buffer has a configurable limit of atomic changes that it can accomodate. After the limit was
+ * exceeded (see {@link typing.ChangeBuffer#input}), a new batch is created in {@link typing.ChangeBuffer#batch}.
+ *
+ * To use the change buffer you need to let it know about number of changes that has been added to the batch:
+ *
+ *		const buffer = new ChangeBuffer( document, LIMIT );
+ *
+ *		// Later on in your feature:
+ *		buffer.batch.insert( pos, insertedCharacters );
+ *		buffer.input( insertedCharacters.length );
+ *
+ * @memberOf typing
+ */
+export default class ChangeBuffer {
+	/**
+	 * Creates a new instance of the ChangeBuffer.
+	 *
+	 * @param {engine.treeModel.Document} document
+	 * @param {Number} limit Maximum number of atomic changes which can be contained in one batch.
+	 */
+	constructor( doc, limit ) {
+		/**
+		 * Instance of the document.
+		 *
+		 * @readonly
+		 * @property {engine.treeModel.Document} typing.ChangeBuffer#document
+		 */
+		this.document = doc;
+
+		/**
+		 * Number of atomic changes in the buffer. Once it exceeds the {@link typing.ChangeBuffer#limit},
+		 * {@link typing.ChangeBuffer#batch batch} is set to a new batch.
+		 *
+		 * @readonly
+		 * @property {Number} typing.ChangeBuffer#size
+		 */
+		this.size = 0;
+
+		/**
+		 * Maximum number of atomic changes which can be contained in one batch.
+		 *
+		 * @readonly
+		 * @property {Number} typing.ChangeBuffer#limit
+		 */
+		this.limit = limit;
+
+		this._changeCallback = ( evt, type, changes, batch ) => this._onBatch( batch );
+		doc.on( 'change', this._changeCallback );
+
+		/**
+		 * The current batch instance.
+		 *
+		 * @private
+		 * @property typing.ChangeBuffer#_batch
+		 */
+
+		/**
+		 * The callback to document change event which later needs to be removed.
+		 *
+		 * @private
+		 * @property typing.ChangeBuffer#_changeCallback
+		 */
+	}
+
+	/**
+	 * Current batch to which a feature should add its deltas. Once the {@link typing.ChangeBuffer#size}
+	 * exceedes the {@link typing.ChangeBuffer#limit}, the batch is set to a new instance and size is reset.
+	 *
+	 * @type {engine.treeModel.batch.Batch}
+	 */
+	get batch() {
+		if ( !this._batch ) {
+			this._batch = this.document.batch();
+		}
+
+		return this._batch;
+	}
+
+	/**
+	 * Input number of changes into the buffer. Once the {@link typing.ChangeBuffer#size}
+	 * exceedes the {@link typing.ChangeBuffer#limit}, the batch is set to a new instance and size is reset.
+	 *
+	 * @param {Number} changeCount Number of atomic changes to input.
+	 */
+	input( changeCount ) {
+		this.size += changeCount;
+
+		if ( this.size > this.limit ) {
+			this._batch = null;
+		}
+	}
+
+	/**
+	 * Destroys the buffer.
+	 */
+	destroy() {
+		this.document.off( 'change', this._changeCallback );
+	}
+
+	/**
+	 * To be called in order to notify the buffer about batches which appeared in the document.
+	 * The method will check whether it is a new batch and in that case the buffer will be flushed.
+	 *
+	 * The reason why the buffer needs to be flushed whenever a new batch appears is that changes added afterwards
+	 * should be added to a new batch. For instance, when a user types, then inserts an image and then types again,
+	 * the characters typed after inserting the image should be added to a different batch than the characters typed before.
+	 *
+	 * @private
+	 * @param {engine.treeModel.batch.Batch} batch The batch which appears in the document.
+	 */
+	_onBatch( batch ) {
+		// 1 operation means a newly created batch.
+		if ( batch !== this._batch && utils.count( batch.getOperations() ) <= 1 ) {
+			this._batch = null;
+		}
+	}
+}

+ 96 - 0
packages/ckeditor5-typing/tests/changebuffer.js

@@ -0,0 +1,96 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import ChangeBuffer from '/ckeditor5/typing/changebuffer.js';
+import Document from '/ckeditor5/engine/treemodel/document.js';
+import Batch from '/ckeditor5/engine/treemodel/batch.js';
+import Position from '/ckeditor5/engine/treemodel/position.js';
+
+describe( 'ChangeBuffer', () => {
+	const CHANGE_LIMIT = 3;
+	let doc, buffer, root;
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'main' );
+		buffer = new ChangeBuffer( doc, CHANGE_LIMIT );
+	} );
+
+	describe( 'constructor', () => {
+		it( 'sets all properties', () => {
+			expect( buffer ).to.have.property( 'document', doc );
+			expect( buffer ).to.have.property( 'limit', CHANGE_LIMIT );
+			expect( buffer ).to.have.property( 'size', 0 );
+		} );
+	} );
+
+	describe( 'batch', () => {
+		it( 'it is set initially', () => {
+			expect( buffer ).to.have.property( 'batch' );
+			expect( buffer.batch ).to.be.instanceof( Batch );
+		} );
+
+		it( 'is reset once changes exceed the limit', () => {
+			const batch1 = buffer.batch;
+
+			buffer.input( CHANGE_LIMIT );
+
+			expect( buffer.batch ).to.equal( batch1 );
+
+			buffer.input( 1 );
+
+			const batch2 = buffer.batch;
+
+			expect( batch2 ).to.be.instanceof( Batch );
+			expect( batch2 ).to.not.equal( batch1 );
+		} );
+
+		it( 'is reset once a new batch appears in the document', () => {
+			const batch1 = buffer.batch;
+
+			doc.batch().insert( Position.createAt( root, 0 ), 'a' );
+
+			expect( buffer.batch ).to.not.equal( batch1 );
+		} );
+
+		it( 'is not reset when changes are added to the buffer\'s batch', () => {
+			const batch1 = buffer.batch;
+
+			buffer.batch.insert( Position.createAt( root, 0 ), 'a' );
+			expect( buffer.batch ).to.equal( batch1 );
+
+			buffer.batch.insert( Position.createAt( root, 0 ), 'b' );
+			expect( buffer.batch ).to.equal( batch1 );
+		} );
+
+		it( 'is not reset when changes are added to batch which existed previously', () => {
+			const externalBatch = doc.batch();
+
+			externalBatch.insert( Position.createAt( root, 0 ), 'a' );
+
+			const bufferBatch = buffer.batch;
+
+			buffer.batch.insert( Position.createAt( root, 0 ), 'b' );
+			expect( buffer.batch ).to.equal( bufferBatch );
+
+			doc.batch().insert( Position.createAt( root, 0 ), 'c' );
+			expect( buffer.batch ).to.not.equal( bufferBatch );
+		} );
+	} );
+
+	describe( 'destory', () => {
+		it( 'offs the buffer from the document', () => {
+			const batch1 = buffer.batch;
+
+			buffer.destroy();
+
+			doc.batch().insert( Position.createAt( root, 0 ), 'a' );
+
+			expect( buffer.batch ).to.equal( batch1 );
+		} );
+	} );
+} );