@@ -57,13 +57,36 @@ export default class Collection {
5757 this . _idProperty = options && options . idProperty || 'id' ;
5858
5959 /**
60- * A helper mapping external items from bound collection ({@link #bindTo})
61- * and actual items of the collection.
60+ * A helper mapping external items of a bound collection ({@link #bindTo})
61+ * and actual items of this collection. It provides information
62+ * necessary to properly remove items bound to another collection.
63+ *
64+ * See {@link #_bindToInternalToExternalMap}.
6265 *
6366 * @protected
64- * @member {Map}
67+ * @member {WeakMap}
68+ */
69+ this . _bindToExternalToInternalMap = new WeakMap ( ) ;
70+
71+ /**
72+ * A helper mapping items of this collection to external items of a bound collection
73+ * ({@link #bindTo}). It provides information necessary to manage the bindings, e.g.
74+ * to avoid loops in two–way bindings.
75+ *
76+ * See {@link #_bindToExternalToInternalMap}.
77+ *
78+ * @protected
79+ * @member {WeakMap}
80+ */
81+ this . _bindToInternalToExternalMap = new WeakMap ( ) ;
82+
83+ /**
84+ * A collection instance this collection is bound to as a result
85+ * of calling {@link #bindTo} method.
86+ *
87+ * @protected
88+ * @member {module:utils/collection~Collection} #_bindToCollection
6589 */
66- this . _boundItemsMap = new Map ( ) ;
6790 }
6891
6992 /**
@@ -226,6 +249,10 @@ export default class Collection {
226249 this . _items . splice ( index , 1 ) ;
227250 this . _itemMap . delete ( id ) ;
228251
252+ const externalItem = this . _bindToInternalToExternalMap . get ( item ) ;
253+ this . _bindToInternalToExternalMap . delete ( item ) ;
254+ this . _bindToExternalToInternalMap . delete ( externalItem ) ;
255+
229256 this . fire ( 'remove' , item ) ;
230257
231258 return item ;
@@ -271,9 +298,15 @@ export default class Collection {
271298 }
272299
273300 /**
274- * Removes all items from the collection.
301+ * Removes all items from the collection and destroys the binding created using
302+ * {@link #bindTo}.
275303 */
276304 clear ( ) {
305+ if ( this . _bindToCollection ) {
306+ this . stopListening ( this . _bindToCollection ) ;
307+ this . _bindToCollection = null ;
308+ }
309+
277310 while ( this . length ) {
278311 this . remove ( 0 ) ;
279312 }
@@ -351,32 +384,24 @@ export default class Collection {
351384 * console.log( target.get( 0 ).value ); // 'foo'
352385 * console.log( target.get( 1 ).value ); // 'bar'
353386 *
387+ * **Note**: {@link #clear} can be used to break the binding.
388+ *
354389 * @param {module:utils/collection~Collection } collection A collection to be bound.
355- * @returns {module:ui/viewcollection~ViewCollection#bindTo#using }
390+ * @returns {Object }
391+ * @returns {module:utils/collection~Collection#bindTo#as } return.as
392+ * @returns {module:utils/collection~Collection#bindTo#using } return.using
356393 */
357- bindTo ( collection ) {
358- // Sets the actual binding using provided factory.
359- //
360- // @private
361- // @param {Function } factory A collection item factory returning collection items.
362- const bind = ( factory ) => {
363- // Load the initial content of the collection.
364- for ( let item of collection ) {
365- this . add ( factory ( item ) ) ;
366- }
367-
368- // Synchronize the with collection as new items are added.
369- this . listenTo ( collection , 'add' , ( evt , item , index ) => {
370- this . add ( factory ( item ) , index ) ;
371- } ) ;
372-
373- // Synchronize the with collection as new items are removed.
374- this . listenTo ( collection , 'remove' , ( evt , item ) => {
375- this . remove ( this . _boundItemsMap . get ( item ) ) ;
394+ bindTo ( externalCollection ) {
395+ if ( this . _bindToCollection ) {
396+ /**
397+ * The collection cannot be bound more than once.
398+ *
399+ * @error collection-bind-to-rebind
400+ */
401+ throw new CKEditorError ( 'collection-bind-to-rebind: The collection cannot be bound more than once.' ) ;
402+ }
376403
377- this . _boundItemsMap . delete ( item ) ;
378- } ) ;
379- } ;
404+ this . _bindToCollection = externalCollection ;
380405
381406 return {
382407 /**
@@ -386,13 +411,7 @@ export default class Collection {
386411 * @param {Function } Class Specifies which class factory is to be initialized.
387412 */
388413 as : ( Class ) => {
389- bind ( ( item ) => {
390- const instance = new Class ( item ) ;
391-
392- this . _boundItemsMap . set ( item , instance ) ;
393-
394- return instance ;
395- } ) ;
414+ this . _setUpBindToBinding ( item => new Class ( item ) ) ;
396415 } ,
397416
398417 /**
@@ -404,29 +423,64 @@ export default class Collection {
404423 * the bound collection items.
405424 */
406425 using : ( callbackOrProperty ) => {
407- let factory ;
408-
409426 if ( typeof callbackOrProperty == 'function' ) {
410- factory = ( item ) => {
411- const instance = callbackOrProperty ( item ) ;
427+ this . _setUpBindToBinding ( item => callbackOrProperty ( item ) ) ;
428+ } else {
429+ this . _setUpBindToBinding ( item => item [ callbackOrProperty ] ) ;
430+ }
431+ }
432+ } ;
433+ }
434+
435+ /**
436+ * Finalizes and activates a binding initiated by {#bindTo}.
437+ *
438+ * @protected
439+ * @param {Function } factory A function which produces collection items.
440+ */
441+ _setUpBindToBinding ( factory ) {
442+ const externalCollection = this . _bindToCollection ;
412443
413- this . _boundItemsMap . set ( item , instance ) ;
444+ // Adds the item to the collection once a change has been done to the external collection.
445+ //
446+ // @private
447+ const addItem = ( evt , externalItem , index ) => {
448+ const isExternalBoundToThis = externalCollection . _bindToCollection == this ;
449+ const externalItemBound = externalCollection . _bindToInternalToExternalMap . get ( externalItem ) ;
450+
451+ // If an external collection is bound to this collection, which makes it a 2–way binding,
452+ // and the particular external collection item is already bound, don't add it here.
453+ // The external item has been created **out of this collection's item** and (re)adding it will
454+ // cause a loop.
455+ if ( isExternalBoundToThis && externalItemBound ) {
456+ this . _bindToExternalToInternalMap . set ( externalItem , externalItemBound ) ;
457+ this . _bindToInternalToExternalMap . set ( externalItemBound , externalItem ) ;
458+ } else {
459+ const item = factory ( externalItem ) ;
460+
461+ this . _bindToExternalToInternalMap . set ( externalItem , item ) ;
462+ this . _bindToInternalToExternalMap . set ( item , externalItem ) ;
463+
464+ this . add ( item , index ) ;
465+ }
466+ } ;
414467
415- return instance ;
416- } ;
417- } else {
418- factory = ( item ) => {
419- const instance = item [ callbackOrProperty ] ;
468+ // Load the initial content of the collection.
469+ for ( let externalItem of externalCollection ) {
470+ addItem ( null , externalItem ) ;
471+ }
420472
421- this . _boundItemsMap . set ( item , instance ) ;
473+ // Synchronize the with collection as new items are added.
474+ this . listenTo ( externalCollection , 'add' , addItem ) ;
422475
423- return instance ;
424- } ;
425- }
476+ // Synchronize the with collection as new items are removed.
477+ this . listenTo ( externalCollection , 'remove' , ( evt , externalItem ) => {
478+ const item = this . _bindToExternalToInternalMap . get ( externalItem ) ;
426479
427- bind ( factory ) ;
480+ if ( item ) {
481+ this . remove ( item ) ;
428482 }
429- } ;
483+ } ) ;
430484 }
431485
432486 /**
0 commit comments