@ckeditor/ckeditor5-utils 34.2.0 → 35.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +324 -0
  2. package/LICENSE.md +1 -1
  3. package/package.json +19 -8
  4. package/src/areconnectedthroughproperties.js +54 -71
  5. package/src/ckeditorerror.js +92 -114
  6. package/src/collection.js +594 -762
  7. package/src/comparearrays.js +22 -28
  8. package/src/config.js +193 -223
  9. package/src/count.js +8 -12
  10. package/src/diff.js +85 -110
  11. package/src/difftochanges.js +47 -57
  12. package/src/dom/createelement.js +17 -25
  13. package/src/dom/emittermixin.js +202 -263
  14. package/src/dom/getancestors.js +9 -13
  15. package/src/dom/getborderwidths.js +10 -13
  16. package/src/dom/getcommonancestor.js +9 -15
  17. package/src/dom/getdatafromelement.js +5 -9
  18. package/src/dom/getpositionedancestor.js +9 -14
  19. package/src/dom/global.js +15 -4
  20. package/src/dom/indexof.js +7 -11
  21. package/src/dom/insertat.js +2 -4
  22. package/src/dom/iscomment.js +2 -5
  23. package/src/dom/isnode.js +10 -12
  24. package/src/dom/isrange.js +2 -4
  25. package/src/dom/istext.js +2 -4
  26. package/src/dom/isvisible.js +2 -4
  27. package/src/dom/iswindow.js +11 -16
  28. package/src/dom/position.js +220 -410
  29. package/src/dom/rect.js +335 -414
  30. package/src/dom/remove.js +5 -8
  31. package/src/dom/resizeobserver.js +109 -342
  32. package/src/dom/scroll.js +151 -183
  33. package/src/dom/setdatainelement.js +5 -9
  34. package/src/dom/tounit.js +10 -12
  35. package/src/elementreplacer.js +30 -44
  36. package/src/emittermixin.js +368 -634
  37. package/src/env.js +109 -116
  38. package/src/eventinfo.js +12 -65
  39. package/src/fastdiff.js +96 -128
  40. package/src/first.js +8 -12
  41. package/src/focustracker.js +77 -133
  42. package/src/index.js +0 -9
  43. package/src/inserttopriorityarray.js +9 -30
  44. package/src/isiterable.js +2 -4
  45. package/src/keyboard.js +117 -196
  46. package/src/keystrokehandler.js +72 -88
  47. package/src/language.js +9 -15
  48. package/src/locale.js +61 -158
  49. package/src/mapsequal.js +12 -17
  50. package/src/mix.js +17 -16
  51. package/src/nth.js +8 -11
  52. package/src/objecttomap.js +7 -11
  53. package/src/observablemixin.js +474 -778
  54. package/src/priorities.js +20 -32
  55. package/src/spy.js +3 -6
  56. package/src/toarray.js +2 -13
  57. package/src/tomap.js +8 -10
  58. package/src/translation-service.js +57 -93
  59. package/src/uid.js +34 -38
  60. package/src/unicode.js +28 -43
  61. package/src/version.js +134 -143
package/src/collection.js CHANGED
@@ -2,17 +2,13 @@
2
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module utils/collection
8
7
  */
9
-
10
- import EmitterMixin from './emittermixin';
8
+ import { Emitter } from './emittermixin';
11
9
  import CKEditorError from './ckeditorerror';
12
10
  import uid from './uid';
13
11
  import isIterable from './isiterable';
14
- import mix from './mix';
15
-
16
12
  /**
17
13
  * Collections are ordered sets of objects. Items in the collection can be retrieved by their indexes
18
14
  * in the collection (like in an array) or by their ids.
@@ -25,761 +21,597 @@ import mix from './mix';
25
21
  *
26
22
  * @mixes module:utils/emittermixin~EmitterMixin
27
23
  */
28
- export default class Collection {
29
- /**
30
- * Creates a new Collection instance.
31
- *
32
- * You can provide an iterable of initial items the collection will be created with:
33
- *
34
- * const collection = new Collection( [ { id: 'John' }, { id: 'Mike' } ] );
35
- *
36
- * console.log( collection.get( 0 ) ); // -> { id: 'John' }
37
- * console.log( collection.get( 1 ) ); // -> { id: 'Mike' }
38
- * console.log( collection.get( 'Mike' ) ); // -> { id: 'Mike' }
39
- *
40
- * Or you can first create a collection and then add new items using the {@link #add} method:
41
- *
42
- * const collection = new Collection();
43
- *
44
- * collection.add( { id: 'John' } );
45
- * console.log( collection.get( 0 ) ); // -> { id: 'John' }
46
- *
47
- * Whatever option you choose, you can always pass a configuration object as the last argument
48
- * of the constructor:
49
- *
50
- * const emptyCollection = new Collection( { idProperty: 'name' } );
51
- * emptyCollection.add( { name: 'John' } );
52
- * console.log( collection.get( 'John' ) ); // -> { name: 'John' }
53
- *
54
- * const nonEmptyCollection = new Collection( [ { name: 'John' } ], { idProperty: 'name' } );
55
- * nonEmptyCollection.add( { name: 'George' } );
56
- * console.log( collection.get( 'George' ) ); // -> { name: 'George' }
57
- * console.log( collection.get( 'John' ) ); // -> { name: 'John' }
58
- *
59
- * @param {Iterable.<Object>|Object} [initialItemsOrOptions] The initial items of the collection or
60
- * the options object.
61
- * @param {Object} [options={}] The options object, when the first argument is an array of initial items.
62
- * @param {String} [options.idProperty='id'] The name of the property which is used to identify an item.
63
- * Items that do not have such a property will be assigned one when added to the collection.
64
- */
65
- constructor( initialItemsOrOptions = {}, options = {} ) {
66
- const hasInitialItems = isIterable( initialItemsOrOptions );
67
-
68
- if ( !hasInitialItems ) {
69
- options = initialItemsOrOptions;
70
- }
71
-
72
- /**
73
- * The internal list of items in the collection.
74
- *
75
- * @private
76
- * @member {Object[]}
77
- */
78
- this._items = [];
79
-
80
- /**
81
- * The internal map of items in the collection.
82
- *
83
- * @private
84
- * @member {Map}
85
- */
86
- this._itemMap = new Map();
87
-
88
- /**
89
- * The name of the property which is considered to identify an item.
90
- *
91
- * @private
92
- * @member {String}
93
- */
94
- this._idProperty = options.idProperty || 'id';
95
-
96
- /**
97
- * A helper mapping external items of a bound collection ({@link #bindTo})
98
- * and actual items of this collection. It provides information
99
- * necessary to properly remove items bound to another collection.
100
- *
101
- * See {@link #_bindToInternalToExternalMap}.
102
- *
103
- * @protected
104
- * @member {WeakMap}
105
- */
106
- this._bindToExternalToInternalMap = new WeakMap();
107
-
108
- /**
109
- * A helper mapping items of this collection to external items of a bound collection
110
- * ({@link #bindTo}). It provides information necessary to manage the bindings, e.g.
111
- * to avoid loops in two–way bindings.
112
- *
113
- * See {@link #_bindToExternalToInternalMap}.
114
- *
115
- * @protected
116
- * @member {WeakMap}
117
- */
118
- this._bindToInternalToExternalMap = new WeakMap();
119
-
120
- /**
121
- * Stores indexes of skipped items from bound external collection.
122
- *
123
- * @private
124
- * @member {Array}
125
- */
126
- this._skippedIndexesFromExternal = [];
127
-
128
- // Set the initial content of the collection (if provided in the constructor).
129
- if ( hasInitialItems ) {
130
- for ( const item of initialItemsOrOptions ) {
131
- this._items.push( item );
132
- this._itemMap.set( this._getItemIdBeforeAdding( item ), item );
133
- }
134
- }
135
-
136
- /**
137
- * A collection instance this collection is bound to as a result
138
- * of calling {@link #bindTo} method.
139
- *
140
- * @protected
141
- * @member {module:utils/collection~Collection} #_bindToCollection
142
- */
143
- }
144
-
145
- /**
146
- * The number of items available in the collection.
147
- *
148
- * @member {Number} #length
149
- */
150
- get length() {
151
- return this._items.length;
152
- }
153
-
154
- /**
155
- * Returns the first item from the collection or null when collection is empty.
156
- *
157
- * @returns {Object|null} The first item or `null` if collection is empty.
158
- */
159
- get first() {
160
- return this._items[ 0 ] || null;
161
- }
162
-
163
- /**
164
- * Returns the last item from the collection or null when collection is empty.
165
- *
166
- * @returns {Object|null} The last item or `null` if collection is empty.
167
- */
168
- get last() {
169
- return this._items[ this.length - 1 ] || null;
170
- }
171
-
172
- /**
173
- * Adds an item into the collection.
174
- *
175
- * If the item does not have an id, then it will be automatically generated and set on the item.
176
- *
177
- * @chainable
178
- * @param {Object} item
179
- * @param {Number} [index] The position of the item in the collection. The item
180
- * is pushed to the collection when `index` not specified.
181
- * @fires add
182
- * @fires change
183
- */
184
- add( item, index ) {
185
- return this.addMany( [ item ], index );
186
- }
187
-
188
- /**
189
- * Adds multiple items into the collection.
190
- *
191
- * Any item not containing an id will get an automatically generated one.
192
- *
193
- * @chainable
194
- * @param {Iterable.<Object>} item
195
- * @param {Number} [index] The position of the insertion. Items will be appended if no `index` is specified.
196
- * @fires add
197
- * @fires change
198
- */
199
- addMany( items, index ) {
200
- if ( index === undefined ) {
201
- index = this._items.length;
202
- } else if ( index > this._items.length || index < 0 ) {
203
- /**
204
- * The `index` passed to {@link module:utils/collection~Collection#addMany `Collection#addMany()`}
205
- * is invalid. It must be a number between 0 and the collection's length.
206
- *
207
- * @error collection-add-item-invalid-index
208
- */
209
- throw new CKEditorError( 'collection-add-item-invalid-index', this );
210
- }
211
-
212
- for ( let offset = 0; offset < items.length; offset++ ) {
213
- const item = items[ offset ];
214
- const itemId = this._getItemIdBeforeAdding( item );
215
- const currentItemIndex = index + offset;
216
-
217
- this._items.splice( currentItemIndex, 0, item );
218
- this._itemMap.set( itemId, item );
219
-
220
- this.fire( 'add', item, currentItemIndex );
221
- }
222
-
223
- this.fire( 'change', {
224
- added: items,
225
- removed: [],
226
- index
227
- } );
228
-
229
- return this;
230
- }
231
-
232
- /**
233
- * Gets an item by its ID or index.
234
- *
235
- * @param {String|Number} idOrIndex The item ID or index in the collection.
236
- * @returns {Object|null} The requested item or `null` if such item does not exist.
237
- */
238
- get( idOrIndex ) {
239
- let item;
240
-
241
- if ( typeof idOrIndex == 'string' ) {
242
- item = this._itemMap.get( idOrIndex );
243
- } else if ( typeof idOrIndex == 'number' ) {
244
- item = this._items[ idOrIndex ];
245
- } else {
246
- /**
247
- * An index or ID must be given.
248
- *
249
- * @error collection-get-invalid-arg
250
- */
251
- throw new CKEditorError( 'collection-get-invalid-arg', this );
252
- }
253
-
254
- return item || null;
255
- }
256
-
257
- /**
258
- * Returns a Boolean indicating whether the collection contains an item.
259
- *
260
- * @param {Object|String} itemOrId The item or its ID in the collection.
261
- * @returns {Boolean} `true` if the collection contains the item, `false` otherwise.
262
- */
263
- has( itemOrId ) {
264
- if ( typeof itemOrId == 'string' ) {
265
- return this._itemMap.has( itemOrId );
266
- } else { // Object
267
- const idProperty = this._idProperty;
268
- const id = itemOrId[ idProperty ];
269
-
270
- return this._itemMap.has( id );
271
- }
272
- }
273
-
274
- /**
275
- * Gets an index of an item in the collection.
276
- * When an item is not defined in the collection, the index will equal -1.
277
- *
278
- * @param {Object|String} itemOrId The item or its ID in the collection.
279
- * @returns {Number} The index of a given item.
280
- */
281
- getIndex( itemOrId ) {
282
- let item;
283
-
284
- if ( typeof itemOrId == 'string' ) {
285
- item = this._itemMap.get( itemOrId );
286
- } else {
287
- item = itemOrId;
288
- }
289
-
290
- return this._items.indexOf( item );
291
- }
292
-
293
- /**
294
- * Removes an item from the collection.
295
- *
296
- * @param {Object|Number|String} subject The item to remove, its ID or index in the collection.
297
- * @returns {Object} The removed item.
298
- * @fires remove
299
- * @fires change
300
- */
301
- remove( subject ) {
302
- const [ item, index ] = this._remove( subject );
303
-
304
- this.fire( 'change', {
305
- added: [],
306
- removed: [ item ],
307
- index
308
- } );
309
-
310
- return item;
311
- }
312
-
313
- /**
314
- * Executes the callback for each item in the collection and composes an array or values returned by this callback.
315
- *
316
- * @param {Function} callback
317
- * @param {Object} callback.item
318
- * @param {Number} callback.index
319
- * @param {Object} ctx Context in which the `callback` will be called.
320
- * @returns {Array} The result of mapping.
321
- */
322
- map( callback, ctx ) {
323
- return this._items.map( callback, ctx );
324
- }
325
-
326
- /**
327
- * Finds the first item in the collection for which the `callback` returns a true value.
328
- *
329
- * @param {Function} callback
330
- * @param {Object} callback.item
331
- * @param {Number} callback.index
332
- * @param {Object} ctx Context in which the `callback` will be called.
333
- * @returns {Object} The item for which `callback` returned a true value.
334
- */
335
- find( callback, ctx ) {
336
- return this._items.find( callback, ctx );
337
- }
338
-
339
- /**
340
- * Returns an array with items for which the `callback` returned a true value.
341
- *
342
- * @param {Function} callback
343
- * @param {Object} callback.item
344
- * @param {Number} callback.index
345
- * @param {Object} ctx Context in which the `callback` will be called.
346
- * @returns {Object[]} The array with matching items.
347
- */
348
- filter( callback, ctx ) {
349
- return this._items.filter( callback, ctx );
350
- }
351
-
352
- /**
353
- * Removes all items from the collection and destroys the binding created using
354
- * {@link #bindTo}.
355
- *
356
- * @fires remove
357
- * @fires change
358
- */
359
- clear() {
360
- if ( this._bindToCollection ) {
361
- this.stopListening( this._bindToCollection );
362
- this._bindToCollection = null;
363
- }
364
-
365
- const removedItems = Array.from( this._items );
366
-
367
- while ( this.length ) {
368
- this._remove( 0 );
369
- }
370
-
371
- this.fire( 'change', {
372
- added: [],
373
- removed: removedItems,
374
- index: 0
375
- } );
376
- }
377
-
378
- /**
379
- * Binds and synchronizes the collection with another one.
380
- *
381
- * The binding can be a simple factory:
382
- *
383
- * class FactoryClass {
384
- * constructor( data ) {
385
- * this.label = data.label;
386
- * }
387
- * }
388
- *
389
- * const source = new Collection( { idProperty: 'label' } );
390
- * const target = new Collection();
391
- *
392
- * target.bindTo( source ).as( FactoryClass );
393
- *
394
- * source.add( { label: 'foo' } );
395
- * source.add( { label: 'bar' } );
396
- *
397
- * console.log( target.length ); // 2
398
- * console.log( target.get( 1 ).label ); // 'bar'
399
- *
400
- * source.remove( 0 );
401
- * console.log( target.length ); // 1
402
- * console.log( target.get( 0 ).label ); // 'bar'
403
- *
404
- * or the factory driven by a custom callback:
405
- *
406
- * class FooClass {
407
- * constructor( data ) {
408
- * this.label = data.label;
409
- * }
410
- * }
411
- *
412
- * class BarClass {
413
- * constructor( data ) {
414
- * this.label = data.label;
415
- * }
416
- * }
417
- *
418
- * const source = new Collection( { idProperty: 'label' } );
419
- * const target = new Collection();
420
- *
421
- * target.bindTo( source ).using( ( item ) => {
422
- * if ( item.label == 'foo' ) {
423
- * return new FooClass( item );
424
- * } else {
425
- * return new BarClass( item );
426
- * }
427
- * } );
428
- *
429
- * source.add( { label: 'foo' } );
430
- * source.add( { label: 'bar' } );
431
- *
432
- * console.log( target.length ); // 2
433
- * console.log( target.get( 0 ) instanceof FooClass ); // true
434
- * console.log( target.get( 1 ) instanceof BarClass ); // true
435
- *
436
- * or the factory out of property name:
437
- *
438
- * const source = new Collection( { idProperty: 'label' } );
439
- * const target = new Collection();
440
- *
441
- * target.bindTo( source ).using( 'label' );
442
- *
443
- * source.add( { label: { value: 'foo' } } );
444
- * source.add( { label: { value: 'bar' } } );
445
- *
446
- * console.log( target.length ); // 2
447
- * console.log( target.get( 0 ).value ); // 'foo'
448
- * console.log( target.get( 1 ).value ); // 'bar'
449
- *
450
- * It's possible to skip specified items by returning falsy value:
451
- *
452
- * const source = new Collection();
453
- * const target = new Collection();
454
- *
455
- * target.bindTo( source ).using( item => {
456
- * if ( item.hidden ) {
457
- * return null;
458
- * }
459
- *
460
- * return item;
461
- * } );
462
- *
463
- * source.add( { hidden: true } );
464
- * source.add( { hidden: false } );
465
- *
466
- * console.log( source.length ); // 2
467
- * console.log( target.length ); // 1
468
- *
469
- * **Note**: {@link #clear} can be used to break the binding.
470
- *
471
- * @param {module:utils/collection~Collection} externalCollection A collection to be bound.
472
- * @returns {Object}
473
- * @returns {module:utils/collection~CollectionBindToChain} The binding chain object.
474
- */
475
- bindTo( externalCollection ) {
476
- if ( this._bindToCollection ) {
477
- /**
478
- * The collection cannot be bound more than once.
479
- *
480
- * @error collection-bind-to-rebind
481
- */
482
- throw new CKEditorError( 'collection-bind-to-rebind', this );
483
- }
484
-
485
- this._bindToCollection = externalCollection;
486
-
487
- return {
488
- as: Class => {
489
- this._setUpBindToBinding( item => new Class( item ) );
490
- },
491
-
492
- using: callbackOrProperty => {
493
- if ( typeof callbackOrProperty == 'function' ) {
494
- this._setUpBindToBinding( item => callbackOrProperty( item ) );
495
- } else {
496
- this._setUpBindToBinding( item => item[ callbackOrProperty ] );
497
- }
498
- }
499
- };
500
- }
501
-
502
- /**
503
- * Finalizes and activates a binding initiated by {#bindTo}.
504
- *
505
- * @protected
506
- * @param {Function} factory A function which produces collection items.
507
- */
508
- _setUpBindToBinding( factory ) {
509
- const externalCollection = this._bindToCollection;
510
-
511
- // Adds the item to the collection once a change has been done to the external collection.
512
- //
513
- // @private
514
- const addItem = ( evt, externalItem, index ) => {
515
- const isExternalBoundToThis = externalCollection._bindToCollection == this;
516
- const externalItemBound = externalCollection._bindToInternalToExternalMap.get( externalItem );
517
-
518
- // If an external collection is bound to this collection, which makes it a 2–way binding,
519
- // and the particular external collection item is already bound, don't add it here.
520
- // The external item has been created **out of this collection's item** and (re)adding it will
521
- // cause a loop.
522
- if ( isExternalBoundToThis && externalItemBound ) {
523
- this._bindToExternalToInternalMap.set( externalItem, externalItemBound );
524
- this._bindToInternalToExternalMap.set( externalItemBound, externalItem );
525
- } else {
526
- const item = factory( externalItem );
527
-
528
- // When there is no item we need to remember skipped index first and then we can skip this item.
529
- if ( !item ) {
530
- this._skippedIndexesFromExternal.push( index );
531
-
532
- return;
533
- }
534
-
535
- // Lets try to put item at the same index as index in external collection
536
- // but when there are a skipped items in one or both collections we need to recalculate this index.
537
- let finalIndex = index;
538
-
539
- // When we try to insert item after some skipped items from external collection we need
540
- // to include this skipped items and decrease index.
541
- //
542
- // For the following example:
543
- // external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal' ]
544
- // internal -> [ A ]
545
- //
546
- // Another item is been added at the end of external collection:
547
- // external.add( 'D' )
548
- // external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal', 'D' ]
549
- //
550
- // We can't just add 'D' to internal at the same index as index in external because
551
- // this will produce empty indexes what is invalid:
552
- // internal -> [ 'A', empty, empty, 'D' ]
553
- //
554
- // So we need to include skipped items and decrease index
555
- // internal -> [ 'A', 'D' ]
556
- for ( const skipped of this._skippedIndexesFromExternal ) {
557
- if ( index > skipped ) {
558
- finalIndex--;
559
- }
560
- }
561
-
562
- // We need to take into consideration that external collection could skip some items from
563
- // internal collection.
564
- //
565
- // For the following example:
566
- // internal -> [ 'A', 'B - skipped for external', 'C - skipped for external' ]
567
- // external -> [ A ]
568
- //
569
- // Another item is been added at the end of external collection:
570
- // external.add( 'D' )
571
- // external -> [ 'A', 'D' ]
572
- //
573
- // We need to include skipped items and place new item after them:
574
- // internal -> [ 'A', 'B - skipped for external', 'C - skipped for external', 'D' ]
575
- for ( const skipped of externalCollection._skippedIndexesFromExternal ) {
576
- if ( finalIndex >= skipped ) {
577
- finalIndex++;
578
- }
579
- }
580
-
581
- this._bindToExternalToInternalMap.set( externalItem, item );
582
- this._bindToInternalToExternalMap.set( item, externalItem );
583
- this.add( item, finalIndex );
584
-
585
- // After adding new element to internal collection we need update indexes
586
- // of skipped items in external collection.
587
- for ( let i = 0; i < externalCollection._skippedIndexesFromExternal.length; i++ ) {
588
- if ( finalIndex <= externalCollection._skippedIndexesFromExternal[ i ] ) {
589
- externalCollection._skippedIndexesFromExternal[ i ]++;
590
- }
591
- }
592
- }
593
- };
594
-
595
- // Load the initial content of the collection.
596
- for ( const externalItem of externalCollection ) {
597
- addItem( null, externalItem, externalCollection.getIndex( externalItem ) );
598
- }
599
-
600
- // Synchronize the with collection as new items are added.
601
- this.listenTo( externalCollection, 'add', addItem );
602
-
603
- // Synchronize the with collection as new items are removed.
604
- this.listenTo( externalCollection, 'remove', ( evt, externalItem, index ) => {
605
- const item = this._bindToExternalToInternalMap.get( externalItem );
606
-
607
- if ( item ) {
608
- this.remove( item );
609
- }
610
-
611
- // After removing element from external collection we need update/remove indexes
612
- // of skipped items in internal collection.
613
- this._skippedIndexesFromExternal = this._skippedIndexesFromExternal.reduce( ( result, skipped ) => {
614
- if ( index < skipped ) {
615
- result.push( skipped - 1 );
616
- }
617
-
618
- if ( index > skipped ) {
619
- result.push( skipped );
620
- }
621
-
622
- return result;
623
- }, [] );
624
- } );
625
- }
626
-
627
- /**
628
- * Returns an unique id property for a given `item`.
629
- *
630
- * The method will generate new id and assign it to the `item` if it doesn't have any.
631
- *
632
- * @private
633
- * @param {Object} item Item to be added.
634
- * @returns {String}
635
- */
636
- _getItemIdBeforeAdding( item ) {
637
- const idProperty = this._idProperty;
638
- let itemId;
639
-
640
- if ( ( idProperty in item ) ) {
641
- itemId = item[ idProperty ];
642
-
643
- if ( typeof itemId != 'string' ) {
644
- /**
645
- * This item's ID should be a string.
646
- *
647
- * @error collection-add-invalid-id
648
- */
649
- throw new CKEditorError( 'collection-add-invalid-id', this );
650
- }
651
-
652
- if ( this.get( itemId ) ) {
653
- /**
654
- * This item already exists in the collection.
655
- *
656
- * @error collection-add-item-already-exists
657
- */
658
- throw new CKEditorError( 'collection-add-item-already-exists', this );
659
- }
660
- } else {
661
- item[ idProperty ] = itemId = uid();
662
- }
663
-
664
- return itemId;
665
- }
666
-
667
- /**
668
- * Core {@link #remove} method implementation shared in other functions.
669
- *
670
- * In contrast this method **does not** fire the {@link #event:change} event.
671
- *
672
- * @private
673
- * @param {Object} subject The item to remove, its id or index in the collection.
674
- * @returns {Array} Returns an array with the removed item and its index.
675
- * @fires remove
676
- */
677
- _remove( subject ) {
678
- let index, id, item;
679
- let itemDoesNotExist = false;
680
- const idProperty = this._idProperty;
681
-
682
- if ( typeof subject == 'string' ) {
683
- id = subject;
684
- item = this._itemMap.get( id );
685
- itemDoesNotExist = !item;
686
-
687
- if ( item ) {
688
- index = this._items.indexOf( item );
689
- }
690
- } else if ( typeof subject == 'number' ) {
691
- index = subject;
692
- item = this._items[ index ];
693
- itemDoesNotExist = !item;
694
-
695
- if ( item ) {
696
- id = item[ idProperty ];
697
- }
698
- } else {
699
- item = subject;
700
- id = item[ idProperty ];
701
- index = this._items.indexOf( item );
702
- itemDoesNotExist = ( index == -1 || !this._itemMap.get( id ) );
703
- }
704
-
705
- if ( itemDoesNotExist ) {
706
- /**
707
- * Item not found.
708
- *
709
- * @error collection-remove-404
710
- */
711
- throw new CKEditorError( 'collection-remove-404', this );
712
- }
713
-
714
- this._items.splice( index, 1 );
715
- this._itemMap.delete( id );
716
-
717
- const externalItem = this._bindToInternalToExternalMap.get( item );
718
- this._bindToInternalToExternalMap.delete( item );
719
- this._bindToExternalToInternalMap.delete( externalItem );
720
-
721
- this.fire( 'remove', item, index );
722
-
723
- return [ item, index ];
724
- }
725
-
726
- /**
727
- * Iterable interface.
728
- *
729
- * @returns {Iterable.<*>}
730
- */
731
- [ Symbol.iterator ]() {
732
- return this._items[ Symbol.iterator ]();
733
- }
734
-
735
- /**
736
- * Fired when an item is added to the collection.
737
- *
738
- * @event add
739
- * @param {Object} item The added item.
740
- */
741
-
742
- /**
743
- * Fired when the collection was changed due to adding or removing items.
744
- *
745
- * @event change
746
- * @param {Iterable.<Object>} added A list of added items.
747
- * @param {Iterable.<Object>} removed A list of removed items.
748
- * @param {Number} index An index where the addition or removal occurred.
749
- */
750
-
751
- /**
752
- * Fired when an item is removed from the collection.
753
- *
754
- * @event remove
755
- * @param {Object} item The removed item.
756
- * @param {Number} index Index from which item was removed.
757
- */
24
+ export default class Collection extends Emitter {
25
+ /**
26
+ * Creates a new Collection instance.
27
+ *
28
+ * You can provide an iterable of initial items the collection will be created with:
29
+ *
30
+ * const collection = new Collection( [ { id: 'John' }, { id: 'Mike' } ] );
31
+ *
32
+ * console.log( collection.get( 0 ) ); // -> { id: 'John' }
33
+ * console.log( collection.get( 1 ) ); // -> { id: 'Mike' }
34
+ * console.log( collection.get( 'Mike' ) ); // -> { id: 'Mike' }
35
+ *
36
+ * Or you can first create a collection and then add new items using the {@link #add} method:
37
+ *
38
+ * const collection = new Collection();
39
+ *
40
+ * collection.add( { id: 'John' } );
41
+ * console.log( collection.get( 0 ) ); // -> { id: 'John' }
42
+ *
43
+ * Whatever option you choose, you can always pass a configuration object as the last argument
44
+ * of the constructor:
45
+ *
46
+ * const emptyCollection = new Collection( { idProperty: 'name' } );
47
+ * emptyCollection.add( { name: 'John' } );
48
+ * console.log( collection.get( 'John' ) ); // -> { name: 'John' }
49
+ *
50
+ * const nonEmptyCollection = new Collection( [ { name: 'John' } ], { idProperty: 'name' } );
51
+ * nonEmptyCollection.add( { name: 'George' } );
52
+ * console.log( collection.get( 'George' ) ); // -> { name: 'George' }
53
+ * console.log( collection.get( 'John' ) ); // -> { name: 'John' }
54
+ *
55
+ * @param {Iterable.<Object>|Object} [initialItemsOrOptions] The initial items of the collection or
56
+ * the options object.
57
+ * @param {Object} [options={}] The options object, when the first argument is an array of initial items.
58
+ * @param {String} [options.idProperty='id'] The name of the property which is used to identify an item.
59
+ * Items that do not have such a property will be assigned one when added to the collection.
60
+ */
61
+ constructor(initialItemsOrOptions = {}, options = {}) {
62
+ super();
63
+ const hasInitialItems = isIterable(initialItemsOrOptions);
64
+ if (!hasInitialItems) {
65
+ options = initialItemsOrOptions;
66
+ }
67
+ this._items = [];
68
+ this._itemMap = new Map();
69
+ this._idProperty = options.idProperty || 'id';
70
+ this._bindToExternalToInternalMap = new WeakMap();
71
+ this._bindToInternalToExternalMap = new WeakMap();
72
+ this._skippedIndexesFromExternal = [];
73
+ // Set the initial content of the collection (if provided in the constructor).
74
+ if (hasInitialItems) {
75
+ for (const item of initialItemsOrOptions) {
76
+ this._items.push(item);
77
+ this._itemMap.set(this._getItemIdBeforeAdding(item), item);
78
+ }
79
+ }
80
+ }
81
+ /**
82
+ * The number of items available in the collection.
83
+ *
84
+ * @member {Number} #length
85
+ */
86
+ get length() {
87
+ return this._items.length;
88
+ }
89
+ /**
90
+ * Returns the first item from the collection or null when collection is empty.
91
+ *
92
+ * @returns {Object|null} The first item or `null` if collection is empty.
93
+ */
94
+ get first() {
95
+ return this._items[0] || null;
96
+ }
97
+ /**
98
+ * Returns the last item from the collection or null when collection is empty.
99
+ *
100
+ * @returns {Object|null} The last item or `null` if collection is empty.
101
+ */
102
+ get last() {
103
+ return this._items[this.length - 1] || null;
104
+ }
105
+ /**
106
+ * Adds an item into the collection.
107
+ *
108
+ * If the item does not have an id, then it will be automatically generated and set on the item.
109
+ *
110
+ * @chainable
111
+ * @param {Object} item
112
+ * @param {Number} [index] The position of the item in the collection. The item
113
+ * is pushed to the collection when `index` not specified.
114
+ * @fires add
115
+ * @fires change
116
+ */
117
+ add(item, index) {
118
+ return this.addMany([item], index);
119
+ }
120
+ /**
121
+ * Adds multiple items into the collection.
122
+ *
123
+ * Any item not containing an id will get an automatically generated one.
124
+ *
125
+ * @chainable
126
+ * @param {Iterable.<Object>} items
127
+ * @param {Number} [index] The position of the insertion. Items will be appended if no `index` is specified.
128
+ * @fires add
129
+ * @fires change
130
+ */
131
+ addMany(items, index) {
132
+ if (index === undefined) {
133
+ index = this._items.length;
134
+ }
135
+ else if (index > this._items.length || index < 0) {
136
+ /**
137
+ * The `index` passed to {@link module:utils/collection~Collection#addMany `Collection#addMany()`}
138
+ * is invalid. It must be a number between 0 and the collection's length.
139
+ *
140
+ * @error collection-add-item-invalid-index
141
+ */
142
+ throw new CKEditorError('collection-add-item-invalid-index', this);
143
+ }
144
+ let offset = 0;
145
+ for (const item of items) {
146
+ const itemId = this._getItemIdBeforeAdding(item);
147
+ const currentItemIndex = index + offset;
148
+ this._items.splice(currentItemIndex, 0, item);
149
+ this._itemMap.set(itemId, item);
150
+ this.fire('add', item, currentItemIndex);
151
+ offset++;
152
+ }
153
+ this.fire('change', {
154
+ added: items,
155
+ removed: [],
156
+ index
157
+ });
158
+ return this;
159
+ }
160
+ /**
161
+ * Gets an item by its ID or index.
162
+ *
163
+ * @param {String|Number} idOrIndex The item ID or index in the collection.
164
+ * @returns {Object|null} The requested item or `null` if such item does not exist.
165
+ */
166
+ get(idOrIndex) {
167
+ let item;
168
+ if (typeof idOrIndex == 'string') {
169
+ item = this._itemMap.get(idOrIndex);
170
+ }
171
+ else if (typeof idOrIndex == 'number') {
172
+ item = this._items[idOrIndex];
173
+ }
174
+ else {
175
+ /**
176
+ * An index or ID must be given.
177
+ *
178
+ * @error collection-get-invalid-arg
179
+ */
180
+ throw new CKEditorError('collection-get-invalid-arg', this);
181
+ }
182
+ return item || null;
183
+ }
184
+ /**
185
+ * Returns a Boolean indicating whether the collection contains an item.
186
+ *
187
+ * @param {Object|String} itemOrId The item or its ID in the collection.
188
+ * @returns {Boolean} `true` if the collection contains the item, `false` otherwise.
189
+ */
190
+ has(itemOrId) {
191
+ if (typeof itemOrId == 'string') {
192
+ return this._itemMap.has(itemOrId);
193
+ }
194
+ else { // Object
195
+ const idProperty = this._idProperty;
196
+ const id = itemOrId[idProperty];
197
+ return id && this._itemMap.has(id);
198
+ }
199
+ }
200
+ /**
201
+ * Gets an index of an item in the collection.
202
+ * When an item is not defined in the collection, the index will equal -1.
203
+ *
204
+ * @param {Object|String} itemOrId The item or its ID in the collection.
205
+ * @returns {Number} The index of a given item.
206
+ */
207
+ getIndex(itemOrId) {
208
+ let item;
209
+ if (typeof itemOrId == 'string') {
210
+ item = this._itemMap.get(itemOrId);
211
+ }
212
+ else {
213
+ item = itemOrId;
214
+ }
215
+ return item ? this._items.indexOf(item) : -1;
216
+ }
217
+ /**
218
+ * Removes an item from the collection.
219
+ *
220
+ * @param {Object|Number|String} subject The item to remove, its ID or index in the collection.
221
+ * @returns {Object} The removed item.
222
+ * @fires remove
223
+ * @fires change
224
+ */
225
+ remove(subject) {
226
+ const [item, index] = this._remove(subject);
227
+ this.fire('change', {
228
+ added: [],
229
+ removed: [item],
230
+ index
231
+ });
232
+ return item;
233
+ }
234
+ /**
235
+ * Executes the callback for each item in the collection and composes an array or values returned by this callback.
236
+ *
237
+ * @param {Function} callback
238
+ * @param {Object} callback.item
239
+ * @param {Number} callback.index
240
+ * @param {Object} [ctx] Context in which the `callback` will be called.
241
+ * @returns {Array} The result of mapping.
242
+ */
243
+ map(callback, ctx) {
244
+ return this._items.map(callback, ctx);
245
+ }
246
+ /**
247
+ * Finds the first item in the collection for which the `callback` returns a true value.
248
+ *
249
+ * @param {Function} callback
250
+ * @param {Object} callback.item
251
+ * @param {Number} callback.index
252
+ * @param {Object} [ctx] Context in which the `callback` will be called.
253
+ * @returns {Object|undefined} The item for which `callback` returned a true value.
254
+ */
255
+ find(callback, ctx) {
256
+ return this._items.find(callback, ctx);
257
+ }
258
+ /**
259
+ * Returns an array with items for which the `callback` returned a true value.
260
+ *
261
+ * @param {Function} callback
262
+ * @param {Object} callback.item
263
+ * @param {Number} callback.index
264
+ * @param {Object} [ctx] Context in which the `callback` will be called.
265
+ * @returns {Array} The array with matching items.
266
+ */
267
+ filter(callback, ctx) {
268
+ return this._items.filter(callback, ctx);
269
+ }
270
+ /**
271
+ * Removes all items from the collection and destroys the binding created using
272
+ * {@link #bindTo}.
273
+ *
274
+ * @fires remove
275
+ * @fires change
276
+ */
277
+ clear() {
278
+ if (this._bindToCollection) {
279
+ this.stopListening(this._bindToCollection);
280
+ this._bindToCollection = null;
281
+ }
282
+ const removedItems = Array.from(this._items);
283
+ while (this.length) {
284
+ this._remove(0);
285
+ }
286
+ this.fire('change', {
287
+ added: [],
288
+ removed: removedItems,
289
+ index: 0
290
+ });
291
+ }
292
+ /**
293
+ * Binds and synchronizes the collection with another one.
294
+ *
295
+ * The binding can be a simple factory:
296
+ *
297
+ * class FactoryClass {
298
+ * constructor( data ) {
299
+ * this.label = data.label;
300
+ * }
301
+ * }
302
+ *
303
+ * const source = new Collection( { idProperty: 'label' } );
304
+ * const target = new Collection();
305
+ *
306
+ * target.bindTo( source ).as( FactoryClass );
307
+ *
308
+ * source.add( { label: 'foo' } );
309
+ * source.add( { label: 'bar' } );
310
+ *
311
+ * console.log( target.length ); // 2
312
+ * console.log( target.get( 1 ).label ); // 'bar'
313
+ *
314
+ * source.remove( 0 );
315
+ * console.log( target.length ); // 1
316
+ * console.log( target.get( 0 ).label ); // 'bar'
317
+ *
318
+ * or the factory driven by a custom callback:
319
+ *
320
+ * class FooClass {
321
+ * constructor( data ) {
322
+ * this.label = data.label;
323
+ * }
324
+ * }
325
+ *
326
+ * class BarClass {
327
+ * constructor( data ) {
328
+ * this.label = data.label;
329
+ * }
330
+ * }
331
+ *
332
+ * const source = new Collection( { idProperty: 'label' } );
333
+ * const target = new Collection();
334
+ *
335
+ * target.bindTo( source ).using( ( item ) => {
336
+ * if ( item.label == 'foo' ) {
337
+ * return new FooClass( item );
338
+ * } else {
339
+ * return new BarClass( item );
340
+ * }
341
+ * } );
342
+ *
343
+ * source.add( { label: 'foo' } );
344
+ * source.add( { label: 'bar' } );
345
+ *
346
+ * console.log( target.length ); // 2
347
+ * console.log( target.get( 0 ) instanceof FooClass ); // true
348
+ * console.log( target.get( 1 ) instanceof BarClass ); // true
349
+ *
350
+ * or the factory out of property name:
351
+ *
352
+ * const source = new Collection( { idProperty: 'label' } );
353
+ * const target = new Collection();
354
+ *
355
+ * target.bindTo( source ).using( 'label' );
356
+ *
357
+ * source.add( { label: { value: 'foo' } } );
358
+ * source.add( { label: { value: 'bar' } } );
359
+ *
360
+ * console.log( target.length ); // 2
361
+ * console.log( target.get( 0 ).value ); // 'foo'
362
+ * console.log( target.get( 1 ).value ); // 'bar'
363
+ *
364
+ * It's possible to skip specified items by returning falsy value:
365
+ *
366
+ * const source = new Collection();
367
+ * const target = new Collection();
368
+ *
369
+ * target.bindTo( source ).using( item => {
370
+ * if ( item.hidden ) {
371
+ * return null;
372
+ * }
373
+ *
374
+ * return item;
375
+ * } );
376
+ *
377
+ * source.add( { hidden: true } );
378
+ * source.add( { hidden: false } );
379
+ *
380
+ * console.log( source.length ); // 2
381
+ * console.log( target.length ); // 1
382
+ *
383
+ * **Note**: {@link #clear} can be used to break the binding.
384
+ *
385
+ * @param {module:utils/collection~Collection} externalCollection A collection to be bound.
386
+ * @returns {module:utils/collection~CollectionBindToChain} The binding chain object.
387
+ */
388
+ bindTo(externalCollection) {
389
+ if (this._bindToCollection) {
390
+ /**
391
+ * The collection cannot be bound more than once.
392
+ *
393
+ * @error collection-bind-to-rebind
394
+ */
395
+ throw new CKEditorError('collection-bind-to-rebind', this);
396
+ }
397
+ this._bindToCollection = externalCollection;
398
+ return {
399
+ as: Class => {
400
+ this._setUpBindToBinding(item => new Class(item));
401
+ },
402
+ using: callbackOrProperty => {
403
+ if (typeof callbackOrProperty == 'function') {
404
+ this._setUpBindToBinding(callbackOrProperty);
405
+ }
406
+ else {
407
+ this._setUpBindToBinding(item => item[callbackOrProperty]);
408
+ }
409
+ }
410
+ };
411
+ }
412
+ /**
413
+ * Finalizes and activates a binding initiated by {#bindTo}.
414
+ *
415
+ * @private
416
+ * @param {Function} factory A function which produces collection items.
417
+ */
418
+ _setUpBindToBinding(factory) {
419
+ const externalCollection = this._bindToCollection;
420
+ // Adds the item to the collection once a change has been done to the external collection.
421
+ //
422
+ // @private
423
+ const addItem = (evt, externalItem, index) => {
424
+ const isExternalBoundToThis = externalCollection._bindToCollection == this;
425
+ const externalItemBound = externalCollection._bindToInternalToExternalMap.get(externalItem);
426
+ // If an external collection is bound to this collection, which makes it a 2–way binding,
427
+ // and the particular external collection item is already bound, don't add it here.
428
+ // The external item has been created **out of this collection's item** and (re)adding it will
429
+ // cause a loop.
430
+ if (isExternalBoundToThis && externalItemBound) {
431
+ this._bindToExternalToInternalMap.set(externalItem, externalItemBound);
432
+ this._bindToInternalToExternalMap.set(externalItemBound, externalItem);
433
+ }
434
+ else {
435
+ const item = factory(externalItem);
436
+ // When there is no item we need to remember skipped index first and then we can skip this item.
437
+ if (!item) {
438
+ this._skippedIndexesFromExternal.push(index);
439
+ return;
440
+ }
441
+ // Lets try to put item at the same index as index in external collection
442
+ // but when there are a skipped items in one or both collections we need to recalculate this index.
443
+ let finalIndex = index;
444
+ // When we try to insert item after some skipped items from external collection we need
445
+ // to include this skipped items and decrease index.
446
+ //
447
+ // For the following example:
448
+ // external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal' ]
449
+ // internal -> [ A ]
450
+ //
451
+ // Another item is been added at the end of external collection:
452
+ // external.add( 'D' )
453
+ // external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal', 'D' ]
454
+ //
455
+ // We can't just add 'D' to internal at the same index as index in external because
456
+ // this will produce empty indexes what is invalid:
457
+ // internal -> [ 'A', empty, empty, 'D' ]
458
+ //
459
+ // So we need to include skipped items and decrease index
460
+ // internal -> [ 'A', 'D' ]
461
+ for (const skipped of this._skippedIndexesFromExternal) {
462
+ if (index > skipped) {
463
+ finalIndex--;
464
+ }
465
+ }
466
+ // We need to take into consideration that external collection could skip some items from
467
+ // internal collection.
468
+ //
469
+ // For the following example:
470
+ // internal -> [ 'A', 'B - skipped for external', 'C - skipped for external' ]
471
+ // external -> [ A ]
472
+ //
473
+ // Another item is been added at the end of external collection:
474
+ // external.add( 'D' )
475
+ // external -> [ 'A', 'D' ]
476
+ //
477
+ // We need to include skipped items and place new item after them:
478
+ // internal -> [ 'A', 'B - skipped for external', 'C - skipped for external', 'D' ]
479
+ for (const skipped of externalCollection._skippedIndexesFromExternal) {
480
+ if (finalIndex >= skipped) {
481
+ finalIndex++;
482
+ }
483
+ }
484
+ this._bindToExternalToInternalMap.set(externalItem, item);
485
+ this._bindToInternalToExternalMap.set(item, externalItem);
486
+ this.add(item, finalIndex);
487
+ // After adding new element to internal collection we need update indexes
488
+ // of skipped items in external collection.
489
+ for (let i = 0; i < externalCollection._skippedIndexesFromExternal.length; i++) {
490
+ if (finalIndex <= externalCollection._skippedIndexesFromExternal[i]) {
491
+ externalCollection._skippedIndexesFromExternal[i]++;
492
+ }
493
+ }
494
+ }
495
+ };
496
+ // Load the initial content of the collection.
497
+ for (const externalItem of externalCollection) {
498
+ addItem(null, externalItem, externalCollection.getIndex(externalItem));
499
+ }
500
+ // Synchronize the with collection as new items are added.
501
+ this.listenTo(externalCollection, 'add', addItem);
502
+ // Synchronize the with collection as new items are removed.
503
+ this.listenTo(externalCollection, 'remove', (evt, externalItem, index) => {
504
+ const item = this._bindToExternalToInternalMap.get(externalItem);
505
+ if (item) {
506
+ this.remove(item);
507
+ }
508
+ // After removing element from external collection we need update/remove indexes
509
+ // of skipped items in internal collection.
510
+ this._skippedIndexesFromExternal = this._skippedIndexesFromExternal.reduce((result, skipped) => {
511
+ if (index < skipped) {
512
+ result.push(skipped - 1);
513
+ }
514
+ if (index > skipped) {
515
+ result.push(skipped);
516
+ }
517
+ return result;
518
+ }, []);
519
+ });
520
+ }
521
+ /**
522
+ * Returns an unique id property for a given `item`.
523
+ *
524
+ * The method will generate new id and assign it to the `item` if it doesn't have any.
525
+ *
526
+ * @private
527
+ * @param {Object} item Item to be added.
528
+ * @returns {String}
529
+ */
530
+ _getItemIdBeforeAdding(item) {
531
+ const idProperty = this._idProperty;
532
+ let itemId;
533
+ if ((idProperty in item)) {
534
+ itemId = item[idProperty];
535
+ if (typeof itemId != 'string') {
536
+ /**
537
+ * This item's ID should be a string.
538
+ *
539
+ * @error collection-add-invalid-id
540
+ */
541
+ throw new CKEditorError('collection-add-invalid-id', this);
542
+ }
543
+ if (this.get(itemId)) {
544
+ /**
545
+ * This item already exists in the collection.
546
+ *
547
+ * @error collection-add-item-already-exists
548
+ */
549
+ throw new CKEditorError('collection-add-item-already-exists', this);
550
+ }
551
+ }
552
+ else {
553
+ item[idProperty] = itemId = uid();
554
+ }
555
+ return itemId;
556
+ }
557
+ /**
558
+ * Core {@link #remove} method implementation shared in other functions.
559
+ *
560
+ * In contrast this method **does not** fire the {@link #event:change} event.
561
+ *
562
+ * @private
563
+ * @param {Object|Number|String} subject The item to remove, its id or index in the collection.
564
+ * @returns {Array} Returns an array with the removed item and its index.
565
+ * @fires remove
566
+ */
567
+ _remove(subject) {
568
+ let index, id, item;
569
+ let itemDoesNotExist = false;
570
+ const idProperty = this._idProperty;
571
+ if (typeof subject == 'string') {
572
+ id = subject;
573
+ item = this._itemMap.get(id);
574
+ itemDoesNotExist = !item;
575
+ if (item) {
576
+ index = this._items.indexOf(item);
577
+ }
578
+ }
579
+ else if (typeof subject == 'number') {
580
+ index = subject;
581
+ item = this._items[index];
582
+ itemDoesNotExist = !item;
583
+ if (item) {
584
+ id = item[idProperty];
585
+ }
586
+ }
587
+ else {
588
+ item = subject;
589
+ id = item[idProperty];
590
+ index = this._items.indexOf(item);
591
+ itemDoesNotExist = (index == -1 || !this._itemMap.get(id));
592
+ }
593
+ if (itemDoesNotExist) {
594
+ /**
595
+ * Item not found.
596
+ *
597
+ * @error collection-remove-404
598
+ */
599
+ throw new CKEditorError('collection-remove-404', this);
600
+ }
601
+ this._items.splice(index, 1);
602
+ this._itemMap.delete(id);
603
+ const externalItem = this._bindToInternalToExternalMap.get(item);
604
+ this._bindToInternalToExternalMap.delete(item);
605
+ this._bindToExternalToInternalMap.delete(externalItem);
606
+ this.fire('remove', item, index);
607
+ return [item, index];
608
+ }
609
+ /**
610
+ * Iterable interface.
611
+ *
612
+ * @returns {Iterator.<*>}
613
+ */
614
+ [Symbol.iterator]() {
615
+ return this._items[Symbol.iterator]();
616
+ }
758
617
  }
759
-
760
- mix( Collection, EmitterMixin );
761
-
762
- /**
763
- * An object returned by the {@link module:utils/collection~Collection#bindTo `bindTo()`} method
764
- * providing functions that specify the type of the binding.
765
- *
766
- * See the {@link module:utils/collection~Collection#bindTo `bindTo()`} documentation for examples.
767
- *
768
- * @interface module:utils/collection~CollectionBindToChain
769
- */
770
-
771
- /**
772
- * Creates a callback or a property binding.
773
- *
774
- * @method #using
775
- * @param {Function|String} callbackOrProperty When the function is passed, it should return
776
- * the collection items. When the string is provided, the property value is used to create the bound collection items.
777
- */
778
-
779
- /**
780
- * Creates the class factory binding in which items of the source collection are passed to
781
- * the constructor of the specified class.
782
- *
783
- * @method #as
784
- * @param {Function} Class The class constructor used to create instances in the factory.
785
- */