Explorar o código

Implemented ui.ControllerCollection#pipe method as an extension of utils.ObservableMixin#pipe.

Aleksander Nowodzinski %!s(int64=9) %!d(string=hai) anos
pai
achega
9dd8d879ad

+ 85 - 0
packages/ckeditor5-ui/src/controllercollection.js

@@ -175,6 +175,82 @@ export default class ControllerCollection extends Collection {
 			}
 		};
 	}
+
+	/**
+	 * Delegates selected events coming from within the controller models in the collection to desired
+	 * {@link ObservableMixin} instance. For instance:
+	 *
+	 *		const modelA = new Model();
+	 *		const modelB = new Model();
+	 *		const modelC = new Model();
+	 *
+	 *		const controllers = new ControllerCollection( 'name' );
+	 *
+	 *		controllers.pipe( 'eventX' ).to( modelB );
+	 *		controllers.pipe( 'eventX', 'eventY' ).to( modelC );
+	 *
+	 *		controllers.add( new Controller( modelA, ... ) );
+	 *
+	 * then `eventX` is piped (fired by) `modelB` and `modelC` along with `customData`:
+	 *
+	 *		modelA.fire( 'eventX', customData );
+	 *
+	 * and `eventY` is piped (fired by) `modelC` along with `customData`:
+	 *
+	 *		modelA.fire( 'eventY', customData );
+	 *
+	 * See {@link utils.ObservableMixin#pipe}.
+	 *
+	 * @param {String...} events {@link ui.Controller#model} event names to be piped to another {@link utils.ObservableMixin}.
+	 * @returns {ui.ControllerCollection.pipe#to}
+	 */
+	pipe( ...events ) {
+		if ( !events.length || !isStringArray( events ) ) {
+			/**
+			 * All event names must be strings.
+			 *
+			 * @error ui-controllercollection-pipe-wrong-events
+			 */
+			throw new CKEditorError( 'ui-controllercollection-pipe-wrong-events: All event names must be strings.' );
+		}
+
+		return {
+			/**
+			 * Selects destination for {@link utils.ObservableMixin#pipe} events.
+			 *
+			 * @method ui.ControllerCollection.pipe#to
+			 * @param {ObservableMixin} destination An `ObservableMixin` instance which is the destination for piped events.
+			 */
+			to: ( dest ) => {
+				const pipeEvent = evtName => {
+					return ( ...args ) => {
+						dest.fire( evtName, ...args );
+					};
+				};
+
+				// Activate piping on existing controllers in this collection.
+				for ( let controller of this ) {
+					for ( let evtName of events ) {
+						dest.listenTo( controller.model, evtName, pipeEvent( evtName ) );
+					}
+				}
+
+				// Activate piping on future controllers in this collection.
+				this.on( 'add', ( evt, controller ) => {
+					for ( let evtName of events ) {
+						dest.listenTo( controller.model, evtName, pipeEvent( evtName ) );
+					}
+				} );
+
+				// Deactivate piping when controller is removed from this collection.
+				this.on( 'remove', ( evt, controller ) => {
+					for ( let evtName of events ) {
+						dest.stopListening( controller.model, evtName );
+					}
+				} );
+			}
+		};
+	}
 }
 
 // Initializes controller factory with controller and view classes.
@@ -222,3 +298,12 @@ function flagController( controller, idProperty ) {
 
 	return controller;
 }
+
+// Check if all entries of the array are of `String` type.
+//
+// @private
+// @param {Array} arr An array to be checked.
+// @returns {Boolean}
+function isStringArray( arr ) {
+	return arr.every( a => typeof a == 'string' );
+}

+ 183 - 0
packages/ckeditor5-ui/tests/controllercollection.js

@@ -12,6 +12,7 @@ import Collection from '/ckeditor5/utils/collection.js';
 import Model from '/ckeditor5/ui/model.js';
 import View from '/ckeditor5/ui/view.js';
 import Template from '/ckeditor5/ui/template.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 
 testUtils.createSinonSandbox();
 
@@ -244,6 +245,177 @@ describe( 'ControllerCollection', () => {
 			} );
 		} );
 	} );
+
+	describe( 'pipe', () => {
+		it( 'should throw when event names are not strings', () => {
+			const collection = new ControllerCollection( 'foo' );
+
+			expect( () => {
+				collection.pipe();
+			} ).to.throw( CKEditorError, /ui-controllercollection-pipe-wrong-events/ );
+
+			expect( () => {
+				collection.pipe( new Date() );
+			} ).to.throw( CKEditorError, /ui-controllercollection-pipe-wrong-events/ );
+
+			expect( () => {
+				collection.pipe( 'color', new Date() );
+			} ).to.throw( CKEditorError, /ui-controllercollection-pipe-wrong-events/ );
+		} );
+
+		it( 'returns object', () => {
+			expect( new ControllerCollection( 'foo' ).pipe( 'foo' ) ).to.be.an( 'object' );
+		} );
+
+		it( 'provides "to" interface', () => {
+			const pipe = new ControllerCollection( 'foo' ).pipe( 'foo' );
+
+			expect( pipe ).to.have.keys( 'to' );
+			expect( pipe.to ).to.be.a( 'function' );
+		} );
+
+		describe( 'to', () => {
+			it( 'does not chain', () => {
+				const collection = new ControllerCollection( 'foo' );
+				const returned = collection.pipe( 'foo' ).to( {} );
+
+				expect( returned ).to.be.undefined;
+			} );
+
+			it( 'forwards an event to another observable – existing controller', ( done ) => {
+				const target = new Model();
+				const collection = new ControllerCollection( 'foo' );
+				const model = new Model();
+
+				collection.add( new Controller( model ) );
+				collection.pipe( 'foo' ).to( target );
+
+				target.on( 'foo', ( ...args ) => {
+					assertPipe( args, 'foo', target, model );
+					done();
+				} );
+
+				model.fire( 'foo' );
+			} );
+
+			it( 'forwards an event to another observable – new controller', ( done ) => {
+				const target = new Model();
+				const collection = new ControllerCollection( 'foo' );
+				const model = new Model();
+
+				collection.pipe( 'foo' ).to( target );
+				collection.add( new Controller( model ) );
+
+				target.on( 'foo', ( ...args ) => {
+					assertPipe( args, 'foo', target, model );
+					done();
+				} );
+
+				model.fire( 'foo' );
+			} );
+
+			it( 'forwards multiple events to another observable', () => {
+				const target = new Model();
+				const collection = new ControllerCollection( 'foo' );
+				const modelA = new Model();
+				const modelB = new Model();
+				const modelC = new Model();
+
+				collection.pipe( 'foo', 'bar', 'baz' ).to( target );
+				collection.add( new Controller( modelA ) );
+				collection.add( new Controller( modelB ) );
+				collection.add( new Controller( modelC ) );
+
+				const evts = [];
+
+				function recordEvent( ...args ) {
+					evts.push( args );
+				}
+
+				target.on( 'foo', recordEvent );
+				target.on( 'bar', recordEvent );
+				target.on( 'baz', recordEvent );
+
+				modelA.fire( 'foo' );
+
+				expect( evts ).to.have.length( 1 );
+				assertPipe( evts[ 0 ], 'foo', target, modelA );
+
+				modelB.fire( 'bar' );
+
+				expect( evts ).to.have.length( 2 );
+				assertPipe( evts[ 1 ], 'bar', target, modelB );
+
+				modelC.fire( 'baz' );
+
+				expect( evts ).to.have.length( 3 );
+				assertPipe( evts[ 2 ], 'baz', target, modelC );
+			} );
+
+			it( 'does not forward events which are not supposed to be piped', () => {
+				const target = new Model();
+				const collection = new ControllerCollection( 'foo' );
+				const model = new Model();
+
+				collection.pipe( 'foo', 'bar', 'baz' ).to( target );
+				collection.add( new Controller( model ) );
+
+				let firedCounter = 0;
+				target.on( 'foo', () => ++firedCounter );
+				target.on( 'bar', () => ++firedCounter );
+				target.on( 'baz', () => ++firedCounter );
+
+				model.fire( 'foo' );
+				model.fire( 'baz' );
+				model.fire( 'baz' );
+				model.fire( 'not-piped' );
+
+				expect( firedCounter ).to.equal( 3 );
+			} );
+
+			it( 'stops forwarding when controller removed from the collection', () => {
+				const target = new Model();
+				const collection = new ControllerCollection( 'foo' );
+				const model = new Model();
+
+				collection.pipe( 'foo' ).to( target );
+				collection.add( new Controller( model ) );
+
+				let firedCounter = 0;
+				target.on( 'foo', () => ++firedCounter );
+
+				model.fire( 'foo' );
+				collection.remove( 0 );
+				model.fire( 'foo' );
+
+				expect( firedCounter ).to.equal( 1 );
+			} );
+
+			it( 'supports deep event piping', ( done ) => {
+				const collection = new ControllerCollection( 'foo' );
+				const target = new Model();
+				const modelA = new Model();
+				const modelAA = new Model();
+
+				const controllerA = new Controller( modelA );
+				const controllerAA = new Controller( modelAA );
+				const barCollection = controllerA.addCollection( 'bar' );
+
+				collection.add( controllerA );
+				collection.pipe( 'foo' ).to( target );
+
+				barCollection.add( controllerAA );
+				barCollection.pipe( 'foo' ).to( modelA );
+
+				target.on( 'foo', ( ...args ) => {
+					assertPipe( args, 'foo', target, modelA, modelAA );
+					done();
+				} );
+
+				modelAA.fire( 'foo' );
+			} );
+		} );
+	} );
 } );
 
 function defineParentViewClass() {
@@ -294,3 +466,14 @@ function createModelCollection() {
 		} ) );
 	}
 }
+
+function assertPipe( evtArgs, expectedName, ...observables ) {
+	let pipeNumber = 0;
+
+	for ( let observable of observables ) {
+		expect( evtArgs[ pipeNumber ].name ).to.equal( expectedName );
+		expect( evtArgs[ pipeNumber ].source ).to.equal( observable );
+
+		++pipeNumber;
+	}
+}