diff --git a/src/collection.js b/src/collection.js index bc22a90..8d6a8f7 100644 --- a/src/collection.js +++ b/src/collection.js @@ -28,10 +28,45 @@ 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 array 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' } + * + * @param {Array.|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 = initialItemsOrOptions instanceof Array; + + if ( !hasInitialItems ) { + options = initialItemsOrOptions; + } + /** * The internal list of items in the collection. * @@ -88,6 +123,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. @@ -125,61 +168,72 @@ export default class Collection { } /** - * Adds an item into the collection. + * Adds an item or multiple items into the collection. + * + * const collection = new Collection(); + * + * collection.add( { id: 'John' } ); + * collection.add( { id: 'Anna' }, { id: 'George' } ); + * + * console.log( collection.length ); // -> 3 + * console.log( collection.get( 2 ) ); // -> { name: 'George' } * - * If the item does not have an id, then it will be automatically generated and set on the item. + * You can specify the index of the item (or items) when adding to the collection: + * + * const collection = new Collection(); + * + * collection.add( { id: 'John' } ); + * collection.add( { id: 'Bob' }, 0 ); + * collection.add( { id: 'Anna' }, { id: 'George' }, 1 ); + * + * console.log( collection.get( 0 ) ); // -> { name: 'Bob' } + * console.log( collection.get( 1 ) ); // -> { name: 'Anna' } + * console.log( collection.get( 2 ) ); // -> { name: 'George' } + * console.log( collection.get( 3 ) ); // -> { name: 'John' } + * + * **Note**: If an item does not have an id, it will be automatically given one (see {@link #constructor}). * * @chainable - * @param {Object} item - * @param {Number} [index] The position of the item in the collection. The item - * is pushed to the collection when `index` not specified. + * @param {...(Object)} items Item or items to be added to the collection. + * @param {Number} [index] The position of the item (or items) when added to the collection. If not specified, + * the item (or items) is pushed to the collection. * @fires add */ - add( item, index ) { - let itemId; - const idProperty = this._idProperty; - - if ( ( idProperty in item ) ) { - itemId = item[ idProperty ]; + add( ...args ) { + let addIndex = args[ args.length - 1 ]; + const items = args; - if ( typeof itemId != 'string' ) { + // E.g. add( { ... }, { ... }, ..., 3 ) + if ( typeof addIndex === 'number' ) { + if ( addIndex > this._items.length || addIndex < 0 ) { /** - * This item's id should be a string. + * The index number has invalid value. * - * @error collection-add-invalid-id + * @error collection-add-item-bad-index + * @param {Number} index The index at which the item is to be added. + * @param {module:utils/collection~Collection} collection The collection the item is added to. */ - throw new CKEditorError( 'collection-add-invalid-id', this ); + throw new CKEditorError( 'collection-add-item-invalid-index', { + index: addIndex, + collection: 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(); + // The last argument was an index, remove it from the items. + items.pop(); } - // TODO: Use ES6 default function argument. - if ( index === undefined ) { - index = this._items.length; - } else if ( index > this._items.length || index < 0 ) { - /** - * The index number has invalid value. - * - * @error collection-add-item-bad-index - */ - throw new CKEditorError( 'collection-add-item-invalid-index', this ); + // E.g. add( { ... }, { ... } ) + else { + addIndex = this._items.length; } - this._items.splice( index, 0, item ); - - this._itemMap.set( itemId, item ); + this._items.splice( addIndex, 0, ...items ); - this.fire( 'add', item, index ); + items.forEach( ( item, itemIndex ) => { + this._itemMap.set( this._getItemIdBeforeAdding( item ), item ); + this.fire( 'add', item, addIndex + itemIndex ); + } ); return this; } @@ -604,6 +658,47 @@ export default class Collection { } ); } + _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 + * @param {Object} item The item being added to the collection. + * @param {module:utils/collection~Collection} collection The collection the item is added to. + */ + throw new CKEditorError( 'collection-add-invalid-id', { + item, + collection: this, + } ); + } + + if ( this.get( itemId ) ) { + /** + * This item already exists in the collection. + * + * @error collection-add-item-already-exists + * @param {Object} item The item being added to the collection. + * @param {module:utils/collection~Collection} collection The collection the item is added to. + */ + throw new CKEditorError( 'collection-add-item-already-exists', { + item, + collection: this + } ); + } + } else { + item[ idProperty ] = itemId = uid(); + } + + return itemId; + } + /** * Iterable interface. * diff --git a/tests/collection.js b/tests/collection.js index 0dc77c6..16ac87b 100644 --- a/tests/collection.js +++ b/tests/collection.js @@ -15,7 +15,7 @@ function getItem( id, idProperty ) { }; } -describe( 'Collection', () => { +describe.only( 'Collection', () => { let collection; testUtils.createSinonSandbox(); @@ -25,18 +25,44 @@ 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' } ); - - collection.add( item1 ); - collection.add( item2 ); + it( 'allows setting initial collection items', () => { + const item1 = getItem( 'foo' ); + const item2 = getItem( 'bar' ); + const collection = new Collection( [ item1, item2 ] ); expect( collection ).to.have.length( 2 ); - expect( collection.get( 'xx' ) ).to.equal( item1 ); - expect( collection.remove( 'yy' ) ).to.equal( item2 ); + 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 ); + } ); + + describe( 'options', () => { + it( '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 ).to.have.length( 2 ); + + expect( collection.get( 'xx' ) ).to.equal( item1 ); + expect( collection.remove( 'yy' ) ).to.equal( item2 ); + } ); + + it( 'allow to change the id property used by the collection (initial items)', () => { + 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 ); + } ); } ); } ); @@ -298,6 +324,61 @@ describe( 'Collection', () => { sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', collection ), item, 1 ); } ); + + describe( 'with multiple items', () => { + it( 'adds multiple items to the collection', () => { + const collection = new Collection(); + const item1 = getItem( 'foo' ); + const item2 = getItem( 'bar' ); + + collection.add( item1, item2 ); + + 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( 'adds multiple items to the collection (pushing)', () => { + const collection = new Collection( [ getItem( 'first' ) ] ); + const item1 = getItem( 'foo' ); + const item2 = getItem( 'bar' ); + + collection.add( item1, item2 ); + + expect( collection ).to.have.length( 3 ); + expect( collection.get( 0 ).id ).to.equal( 'first' ); + expect( collection.get( 1 ) ).to.equal( item1 ); + expect( collection.get( 2 ) ).to.equal( item2 ); + expect( collection.get( 'foo' ) ).to.equal( item1 ); + expect( collection.get( 'bar' ) ).to.equal( item2 ); + } ); + + it( 'adds multiple items to the collection at specific position', () => { + const first = getItem( 'first' ); + const last = getItem( 'last' ); + + const collection = new Collection( [ + first, + last + ] ); + + const item1 = getItem( 'foo' ); + const item2 = getItem( 'bar' ); + + collection.add( item1, item2, 1 ); + + expect( collection ).to.have.length( 4 ); + expect( collection.get( 0 ) ).to.equal( first ); + expect( collection.get( 1 ) ).to.equal( item1 ); + expect( collection.get( 2 ) ).to.equal( item2 ); + expect( collection.get( 3 ) ).to.equal( last ); + + expect( collection.get( 'foo' ) ).to.equal( item1 ); + expect( collection.get( 'bar' ) ).to.equal( item2 ); + } ); + } ); } ); describe( 'get()', () => {