瀏覽代碼

Merge branch 'master' into i/3147

Piotrek Koszuliński 5 年之前
父節點
當前提交
d8d4e76a27

+ 89 - 29
packages/ckeditor5-utils/src/collection.js

@@ -10,6 +10,7 @@
 import EmitterMixin from './emittermixin';
 import CKEditorError from './ckeditorerror';
 import uid from './uid';
+import isIterable from './isiterable';
 import mix from './mix';
 
 /**
@@ -28,10 +29,46 @@ export default class Collection {
 	/**
 	 * Creates a new Collection instance.
 	 *
-	 * @param {Object} [options={}] The options object.
-	 * @param {String} [options.idProperty='id'] The name of the property which is considered to identify an item.
+	 * You can provide an iterable of initial items the collection will be created with:
+	 *
+	 *		const collection = new Collection( [ { id: 'John' }, { id: 'Mike' } ] );
+	 *
+	 *		console.log( collection.get( 0 ) ); // -> { id: 'John' }
+	 *		console.log( collection.get( 1 ) ); // -> { id: 'Mike' }
+	 *		console.log( collection.get( 'Mike' ) ); // -> { id: 'Mike' }
+	 *
+	 * Or you can first create a collection and then add new items using the {@link #add} method:
+	 *
+	 *		const collection = new Collection();
+	 *
+	 *		collection.add( { id: 'John' } );
+	 *		console.log( collection.get( 0 ) ); // -> { id: 'John' }
+	 *
+	 * Whatever option you choose, you can always pass a configuration object as the last argument
+	 * of the constructor:
+	 *
+	 *		const emptyCollection = new Collection( { idProperty: 'name' } );
+	 *		emptyCollection.add( { name: 'John' } );
+	 *		console.log( collection.get( 'John' ) ); // -> { name: 'John' }
+	 *
+	 *		const nonEmptyCollection = new Collection( [ { name: 'John' } ], { idProperty: 'name' } );
+	 *		nonEmptyCollection.add( { name: 'George' } );
+	 *		console.log( collection.get( 'George' ) ); // -> { name: 'George' }
+	 *		console.log( collection.get( 'John' ) ); // -> { name: 'John' }
+	 *
+	 * @param {Iterable.<Object>|Object} initialItemsOrOptions The initial items of the collection or
+	 * the options object.
+	 * @param {Object} [options={}] The options object, when the first argument is an array of initial items.
+	 * @param {String} [options.idProperty='id'] The name of the property which is used to identify an item.
+	 * Items that do not have such a property will be assigned one when added to the collection.
 	 */
-	constructor( options = {} ) {
+	constructor( initialItemsOrOptions = {}, options = {} ) {
+		const hasInitialItems = isIterable( initialItemsOrOptions );
+
+		if ( !hasInitialItems ) {
+			options = initialItemsOrOptions;
+		}
+
 		/**
 		 * The internal list of items in the collection.
 		 *
@@ -88,6 +125,14 @@ export default class Collection {
 		 */
 		this._skippedIndexesFromExternal = [];
 
+		// Set the initial content of the collection (if provided in the constructor).
+		if ( hasInitialItems ) {
+			for ( const item of initialItemsOrOptions ) {
+				this._items.push( item );
+				this._itemMap.set( this._getItemIdBeforeAdding( item ), item );
+			}
+		}
+
 		/**
 		 * A collection instance this collection is bound to as a result
 		 * of calling {@link #bindTo} method.
@@ -136,32 +181,7 @@ export default class Collection {
 	 * @fires add
 	 */
 	add( item, index ) {
-		let itemId;
-		const idProperty = this._idProperty;
-
-		if ( ( idProperty in item ) ) {
-			itemId = item[ idProperty ];
-
-			if ( typeof itemId != 'string' ) {
-				/**
-				 * This item's id should be a string.
-				 *
-				 * @error collection-add-invalid-id
-				 */
-				throw new CKEditorError( 'collection-add-invalid-id', this );
-			}
-
-			if ( this.get( itemId ) ) {
-				/**
-				 * This item already exists in the collection.
-				 *
-				 * @error collection-add-item-already-exists
-				 */
-				throw new CKEditorError( 'collection-add-item-already-exists', this );
-			}
-		} else {
-			item[ idProperty ] = itemId = uid();
-		}
+		const itemId = this._getItemIdBeforeAdding( item );
 
 		// TODO: Use ES6 default function argument.
 		if ( index === undefined ) {
@@ -605,6 +625,46 @@ export default class Collection {
 	}
 
 	/**
+	 * Returns an unique id property for a given `item`.
+	 *
+	 * The method will generate new id and assign it to the `item` if it doesn't have any.
+	 *
+	 * @private
+	 * @param {Object} item Item to be added.
+	 * @returns {String}
+	 */
+	_getItemIdBeforeAdding( item ) {
+		const idProperty = this._idProperty;
+		let itemId;
+
+		if ( ( idProperty in item ) ) {
+			itemId = item[ idProperty ];
+
+			if ( typeof itemId != 'string' ) {
+				/**
+				 * This item's id should be a string.
+				 *
+				 * @error collection-add-invalid-id
+				 */
+				throw new CKEditorError( 'collection-add-invalid-id', this );
+			}
+
+			if ( this.get( itemId ) ) {
+				/**
+				 * This item already exists in the collection.
+				 *
+				 * @error collection-add-item-already-exists
+				 */
+				throw new CKEditorError( 'collection-add-item-already-exists', this );
+			}
+		} else {
+			item[ idProperty ] = itemId = uid();
+		}
+
+		return itemId;
+	}
+
+	/**
 	 * Iterable interface.
 	 *
 	 * @returns {Iterable.<*>}

+ 2 - 2
packages/ckeditor5-utils/src/observablemixin.js

@@ -172,7 +172,7 @@ const ObservableMixin = {
 	 */
 	unbind( ...unbindProperties ) {
 		// Nothing to do here if not inited yet.
-		if ( !( observablePropertiesSymbol in this ) ) {
+		if ( !( this[ observablePropertiesSymbol ] ) ) {
 			return;
 		}
 
@@ -271,7 +271,7 @@ export default ObservableMixin;
 // @param {module:utils/observablemixin~ObservableMixin} observable
 function initObservable( observable ) {
 	// Do nothing if already inited.
-	if ( observablePropertiesSymbol in observable ) {
+	if ( observable[ observablePropertiesSymbol ] ) {
 		return;
 	}
 

+ 74 - 9
packages/ckeditor5-utils/tests/collection.js

@@ -25,18 +25,83 @@ describe( 'Collection', () => {
 	} );
 
 	describe( 'constructor()', () => {
-		it( 'allows to change the id property used by the collection', () => {
-			const item1 = { id: 'foo', name: 'xx' };
-			const item2 = { id: 'foo', name: 'yy' };
-			const collection = new Collection( { idProperty: 'name' } );
+		describe( 'setting initial collection items', () => {
+			it( 'should work using an array', () => {
+				const item1 = getItem( 'foo' );
+				const item2 = getItem( 'bar' );
+				const collection = new Collection( [ item1, item2 ] );
 
-			collection.add( item1 );
-			collection.add( item2 );
+				expect( collection ).to.have.length( 2 );
 
-			expect( collection ).to.have.length( 2 );
+				expect( collection.get( 0 ) ).to.equal( item1 );
+				expect( collection.get( 1 ) ).to.equal( item2 );
+				expect( collection.get( 'foo' ) ).to.equal( item1 );
+				expect( collection.get( 'bar' ) ).to.equal( item2 );
+			} );
+
+			it( 'should work using an iterable', () => {
+				const item1 = getItem( 'foo' );
+				const item2 = getItem( 'bar' );
+				const itemsSet = new Set( [ item1, item2 ] );
+				const collection = new Collection( itemsSet );
+
+				expect( collection ).to.have.length( 2 );
+
+				expect( collection.get( 0 ) ).to.equal( item1 );
+				expect( collection.get( 1 ) ).to.equal( item2 );
+			} );
+
+			it( 'should generate ids for items that doesn\'t have it', () => {
+				const item = {};
+				const collection = new Collection( [ item ] );
+
+				expect( collection.get( 0 ).id ).to.be.a( 'string' );
+				expect( collection.get( 0 ).id ).not.to.be.empty;
+			} );
+
+			it( 'should throw an error when an invalid item key is provided', () => {
+				const badIdItem = getItem( 1 ); // Number id is not supported.
+
+				expectToThrowCKEditorError( () => {
+					return new Collection( [ badIdItem ] );
+				}, /^collection-add-invalid-id/ );
+			} );
+
+			it( 'should throw an error when two items have the same key', () => {
+				const item1 = getItem( 'foo' );
+				const item2 = getItem( 'foo' );
+
+				expectToThrowCKEditorError( () => {
+					return new Collection( [ item1, item2 ] );
+				}, /^collection-add-item-already-exists/ );
+			} );
+		} );
+
+		describe( 'options', () => {
+			it( 'should allow to change the id property used by the collection', () => {
+				const item1 = { id: 'foo', name: 'xx' };
+				const item2 = { id: 'foo', name: 'yy' };
+				const collection = new Collection( { idProperty: 'name' } );
+
+				collection.add( item1 );
+				collection.add( item2 );
 
-			expect( collection.get( 'xx' ) ).to.equal( item1 );
-			expect( collection.remove( 'yy' ) ).to.equal( item2 );
+				expect( collection ).to.have.length( 2 );
+
+				expect( collection.get( 'xx' ) ).to.equal( item1 );
+				expect( collection.remove( 'yy' ) ).to.equal( item2 );
+			} );
+
+			it( 'should allow to change the id property used by the collection (initial items passed to the constructor)', () => {
+				const item1 = { id: 'foo', name: 'xx' };
+				const item2 = { id: 'foo', name: 'yy' };
+				const collection = new Collection( [ item1, item2 ], { idProperty: 'name' } );
+
+				expect( collection ).to.have.length( 2 );
+
+				expect( collection.get( 'xx' ) ).to.equal( item1 );
+				expect( collection.remove( 'yy' ) ).to.equal( item2 );
+			} );
 		} );
 	} );