8
0
Просмотр исходного кода

Merge pull request #102 from ckeditor/t/88

t/88: Allowed event rename on delegation and delegation of all events.
Piotrek Koszuliński 9 лет назад
Родитель
Сommit
02cb00aac0

+ 34 - 9
packages/ckeditor5-utils/src/emittermixin.js

@@ -275,11 +275,14 @@ const EmitterMixin = {
 		// Delegate event to other emitters if needed.
 		// Delegate event to other emitters if needed.
 		if ( this._delegations ) {
 		if ( this._delegations ) {
 			const destinations = this._delegations.get( event );
 			const destinations = this._delegations.get( event );
+			const passAllDestinations = this._delegations.get( '*' );
 
 
 			if ( destinations ) {
 			if ( destinations ) {
-				for ( let dest of destinations ) {
-					dest.fire( eventInfo, ...args );
-				}
+				fireDelegatedEvents( destinations, eventInfo, args );
+			}
+
+			if ( passAllDestinations ) {
+				fireDelegatedEvents( passAllDestinations, eventInfo, args );
 			}
 			}
 		}
 		}
 	},
 	},
@@ -309,8 +312,9 @@ const EmitterMixin = {
 			 *
 			 *
 			 * @method utils.EmitterMixin.delegate#to
 			 * @method utils.EmitterMixin.delegate#to
 			 * @param {utils.Emitter} emitter An `EmitterMixin` instance which is the destination for delegated events.
 			 * @param {utils.Emitter} emitter An `EmitterMixin` instance which is the destination for delegated events.
+			 * @param {String|Function} nameOrFunction A custom event name or function which converts the original name string.
 			 */
 			 */
-			to: ( emitter ) => {
+			to: ( emitter, nameOrFunction ) => {
 				if ( !this._delegations ) {
 				if ( !this._delegations ) {
 					this._delegations = new Map();
 					this._delegations = new Map();
 				}
 				}
@@ -319,9 +323,9 @@ const EmitterMixin = {
 					let destinations = this._delegations.get( eventName );
 					let destinations = this._delegations.get( eventName );
 
 
 					if ( !destinations ) {
 					if ( !destinations ) {
-						this._delegations.set( eventName, [ emitter ] );
+						this._delegations.set( eventName, new Map( [ [ emitter, nameOrFunction ] ] ) );
 					} else {
 					} else {
-						destinations.push( emitter );
+						destinations.set( emitter, nameOrFunction );
 					}
 					}
 				}
 				}
 			}
 			}
@@ -351,10 +355,9 @@ const EmitterMixin = {
 			this._delegations.delete( event );
 			this._delegations.delete( event );
 		} else {
 		} else {
 			const destinations = this._delegations.get( event );
 			const destinations = this._delegations.get( event );
-			const index = destinations.indexOf( emitter );
 
 
-			if ( index !== -1 ) {
-				destinations.splice( index, 1 );
+			if ( destinations ) {
+				destinations.delete( emitter );
 			}
 			}
 		}
 		}
 	}
 	}
@@ -493,6 +496,28 @@ function getCallbacksForEvent( source, eventName ) {
 	return event.callbacks;
 	return event.callbacks;
 }
 }
 
 
+// Fires delegated events for given map of destinations.
+//
+// @private
+// * @param {Map.<utils.Emitter>} destinations A map containing `[ {@link utils.Emitter}, "event name" ]` pair destinations.
+// * @param {utils.EventInfo} eventInfo The original event info object.
+// * @param {Array.<*>} fireArgs Arguments the original event was fired with.
+function fireDelegatedEvents( destinations, eventInfo, fireArgs ) {
+	for ( let [ emitter, name ] of destinations ) {
+		if ( !name ) {
+			name = eventInfo.name;
+		} else if ( typeof name == 'function' ) {
+			name = name( eventInfo.name );
+		}
+
+		const delegatedInfo = new EventInfo( eventInfo.source, name );
+
+		delegatedInfo.path = [ ...eventInfo.path ];
+
+		emitter.fire( delegatedInfo, ...fireArgs );
+	}
+}
+
 /**
 /**
  * Interface representing classes which mix in {@link utils.EmitterMixin}.
  * Interface representing classes which mix in {@link utils.EmitterMixin}.
  *
  *

+ 3 - 0
packages/ckeditor5-utils/src/eventinfo.js

@@ -16,6 +16,7 @@ export default class EventInfo {
 		/**
 		/**
 		 * The object that fired the event.
 		 * The object that fired the event.
 		 *
 		 *
+		 * @readonly
 		 * @member utils.EventInfo#source
 		 * @member utils.EventInfo#source
 		 */
 		 */
 		this.source = source;
 		this.source = source;
@@ -23,6 +24,7 @@ export default class EventInfo {
 		/**
 		/**
 		 * The event name.
 		 * The event name.
 		 *
 		 *
+		 * @readonly
 		 * @member utils.EventInfo#name
 		 * @member utils.EventInfo#name
 		 */
 		 */
 		this.name = name;
 		this.name = name;
@@ -30,6 +32,7 @@ export default class EventInfo {
 		/**
 		/**
 		 * Path this event has followed. See {@link utils.EmitterMixin#delegate}.
 		 * Path this event has followed. See {@link utils.EmitterMixin#delegate}.
 		 *
 		 *
+		 * @readonly
 		 * @member utils.EventInfo#path
 		 * @member utils.EventInfo#path
 		 */
 		 */
 		this.path = [];
 		this.path = [];

+ 307 - 0
packages/ckeditor5-utils/tests/emittermixin.js

@@ -670,6 +670,32 @@ describe( 'delegate', () => {
 			emitterC.fire( 'foo', data );
 			emitterC.fire( 'foo', data );
 		} );
 		} );
 
 
+		it( 'preserves path in event delegation', ( done ) => {
+			const data = {};
+			const emitterA = getEmitterInstance();
+			const emitterB = getEmitterInstance();
+			const emitterC = getEmitterInstance();
+			const emitterD = getEmitterInstance();
+
+			emitterB.delegate( 'foo' ).to( emitterA );
+			emitterB.delegate( 'foo' ).to( emitterC );
+			emitterB.delegate( 'foo' ).to( emitterD );
+
+			emitterD.on( 'foo', ( ...args ) => {
+				assertDelegated( args, {
+					expectedSource: emitterB,
+					expectedName: 'foo',
+					expectedPath: [ emitterB, emitterD ],
+					expectedData: [ data ]
+				} );
+
+				done();
+			} );
+
+			emitterB.fire( 'foo', data );
+			emitterC.fire( 'foo', data );
+		} );
+
 		it( 'executes callbacks first, then delegates further', () => {
 		it( 'executes callbacks first, then delegates further', () => {
 			const emitterA = getEmitterInstance();
 			const emitterA = getEmitterInstance();
 			const emitterB = getEmitterInstance();
 			const emitterB = getEmitterInstance();
@@ -685,6 +711,170 @@ describe( 'delegate', () => {
 
 
 			sinon.assert.callOrder( spyB, spyA );
 			sinon.assert.callOrder( spyB, spyA );
 		} );
 		} );
+
+		it( 'supports delegation under a different name', () => {
+			const emitterA = getEmitterInstance();
+			const emitterB = getEmitterInstance();
+			const emitterC = getEmitterInstance();
+			const emitterD = getEmitterInstance();
+			const spyAFoo = sinon.spy();
+			const spyABar = sinon.spy();
+			const spyCBaz = sinon.spy();
+			const spyDFoo = sinon.spy();
+
+			emitterB.delegate( 'foo' ).to( emitterA, 'bar' );
+			emitterB.delegate( 'foo' ).to( emitterC, name => name + '-baz' );
+			emitterB.delegate( 'foo' ).to( emitterD );
+
+			emitterA.on( 'foo', spyAFoo );
+			emitterA.on( 'bar', spyABar );
+			emitterC.on( 'foo-baz', spyCBaz );
+			emitterD.on( 'foo', spyDFoo );
+
+			emitterB.fire( 'foo' );
+
+			sinon.assert.calledOnce( spyABar );
+			sinon.assert.calledOnce( spyCBaz );
+			sinon.assert.calledOnce( spyDFoo );
+			sinon.assert.notCalled( spyAFoo );
+		} );
+
+		it( 'supports delegation under a different name with multiple events', () => {
+			const emitterA = getEmitterInstance();
+			const emitterB = getEmitterInstance();
+			const spyAFoo = sinon.spy();
+			const spyABar = sinon.spy();
+			const spyABaz = sinon.spy();
+			const spyAQux = sinon.spy();
+
+			emitterB.delegate( 'foo', 'bar', 'baz' ).to( emitterA, 'qux' );
+
+			emitterA.on( 'foo', spyAFoo );
+			emitterA.on( 'bar', spyABar );
+			emitterA.on( 'baz', spyABaz );
+			emitterA.on( 'qux', spyAQux );
+
+			emitterB.fire( 'foo' );
+			emitterB.fire( 'baz' );
+			emitterB.fire( 'bar' );
+
+			sinon.assert.notCalled( spyAFoo );
+			sinon.assert.notCalled( spyABar );
+			sinon.assert.notCalled( spyABaz );
+
+			sinon.assert.calledThrice( spyAQux );
+		} );
+
+		it( 'supports delegation with multiple events, each under a different name', () => {
+			const emitterA = getEmitterInstance();
+			const emitterB = getEmitterInstance();
+			const spyAFoo = sinon.spy();
+			const spyABar = sinon.spy();
+			const spyABaz = sinon.spy();
+			const spyAFooQux = sinon.spy();
+			const spyABarQux = sinon.spy();
+			const spyABazQux = sinon.spy();
+
+			emitterB.delegate( 'foo', 'bar', 'baz' ).to( emitterA, name => name + '-qux' );
+
+			emitterA.on( 'foo', spyAFoo );
+			emitterA.on( 'bar', spyABar );
+			emitterA.on( 'baz', spyABaz );
+
+			emitterA.on( 'foo-qux', spyAFooQux );
+			emitterA.on( 'bar-qux', spyABarQux );
+			emitterA.on( 'baz-qux', spyABazQux );
+
+			emitterB.fire( 'foo' );
+			emitterB.fire( 'baz' );
+			emitterB.fire( 'bar' );
+
+			sinon.assert.notCalled( spyAFoo );
+			sinon.assert.notCalled( spyABar );
+			sinon.assert.notCalled( spyABaz );
+
+			sinon.assert.calledOnce( spyAFooQux );
+			sinon.assert.calledOnce( spyABarQux );
+			sinon.assert.calledOnce( spyABazQux );
+
+			sinon.assert.callOrder( spyAFooQux, spyABazQux, spyABarQux );
+		} );
+
+		it( 'preserves path in delegation under a different name', ( done ) => {
+			const data = {};
+			const emitterA = getEmitterInstance();
+			const emitterB = getEmitterInstance();
+			const emitterC = getEmitterInstance();
+			const emitterD = getEmitterInstance();
+
+			emitterB.delegate( 'foo' ).to( emitterA, 'bar' );
+			emitterB.delegate( 'foo' ).to( emitterC, 'baz' );
+			emitterB.delegate( 'foo' ).to( emitterD );
+
+			emitterD.on( 'foo', ( ...args ) => {
+				assertDelegated( args, {
+					expectedSource: emitterB,
+					expectedName: 'foo',
+					expectedPath: [ emitterB, emitterD ],
+					expectedData: [ data ]
+				} );
+
+				done();
+			} );
+
+			emitterB.fire( 'foo', data );
+		} );
+
+		it( 'supports delegation of all events', () => {
+			const emitterA = getEmitterInstance();
+			const emitterB = getEmitterInstance();
+			const spyAFoo = sinon.spy();
+			const spyABar = sinon.spy();
+			const spyABaz = sinon.spy();
+
+			emitterB.delegate( '*' ).to( emitterA );
+
+			emitterA.on( 'foo', spyAFoo );
+			emitterA.on( 'bar', spyABar );
+			emitterA.on( 'baz', spyABaz );
+
+			emitterB.fire( 'foo' );
+			emitterB.fire( 'baz' );
+			emitterB.fire( 'bar' );
+
+			sinon.assert.callOrder( spyAFoo, spyABaz, spyABar );
+		} );
+
+		it( 'supports delegation of all events under different names', () => {
+			const emitterA = getEmitterInstance();
+			const emitterB = getEmitterInstance();
+			const spyAFoo = sinon.spy();
+			const spyABar = sinon.spy();
+			const spyABaz = sinon.spy();
+			const spyAFooDel = sinon.spy();
+			const spyABarDel = sinon.spy();
+			const spyABazDel = sinon.spy();
+
+			emitterB.delegate( '*' ).to( emitterA, name => name + '-delegated' );
+
+			emitterA.on( 'foo', spyAFoo );
+			emitterA.on( 'bar', spyABar );
+			emitterA.on( 'baz', spyABaz );
+
+			emitterA.on( 'foo-delegated', spyAFooDel );
+			emitterA.on( 'bar-delegated', spyABarDel );
+			emitterA.on( 'baz-delegated', spyABazDel );
+
+			emitterB.fire( 'foo' );
+			emitterB.fire( 'baz' );
+			emitterB.fire( 'bar' );
+
+			sinon.assert.notCalled( spyAFoo );
+			sinon.assert.notCalled( spyABar );
+			sinon.assert.notCalled( spyABaz );
+
+			sinon.assert.callOrder( spyAFooDel, spyABazDel, spyABarDel );
+		} );
 	} );
 	} );
 } );
 } );
 
 
@@ -773,6 +963,111 @@ describe( 'stopDelegating', () => {
 		sinon.assert.callOrder( spyFooB, spyFooC, spyFooB );
 		sinon.assert.callOrder( spyFooB, spyFooC, spyFooB );
 	} );
 	} );
 
 
+	it( 'stops delegating a specific event under a different name to a specific emitter', () => {
+		const emitterA = getEmitterInstance();
+		const emitterB = getEmitterInstance();
+		const emitterC = getEmitterInstance();
+		const spyFooB = sinon.spy();
+		const spyFooC = sinon.spy();
+
+		emitterA.delegate( 'foo' ).to( emitterB );
+		emitterA.delegate( 'foo' ).to( emitterC, 'bar' );
+
+		emitterB.on( 'foo', spyFooB );
+		emitterC.on( 'bar', spyFooC );
+
+		emitterA.fire( 'foo' );
+
+		sinon.assert.callOrder( spyFooB, spyFooC );
+
+		emitterA.stopDelegating( 'foo', emitterC );
+		emitterA.fire( 'foo' );
+
+		sinon.assert.callOrder( spyFooB, spyFooC, spyFooB );
+	} );
+
+	it( 'stops delegating all ("*")', () => {
+		const emitterA = getEmitterInstance();
+		const emitterB = getEmitterInstance();
+		const emitterC = getEmitterInstance();
+		const spyAFoo = sinon.spy();
+		const spyABar = sinon.spy();
+		const spyCFoo = sinon.spy();
+		const spyCBar = sinon.spy();
+
+		emitterB.delegate( '*' ).to( emitterA );
+		emitterB.delegate( '*' ).to( emitterC );
+
+		emitterA.on( 'foo', spyAFoo );
+		emitterA.on( 'bar', spyABar );
+		emitterC.on( 'foo', spyCFoo );
+		emitterC.on( 'bar', spyCBar );
+
+		emitterB.fire( 'foo' );
+		emitterB.fire( 'bar' );
+
+		sinon.assert.calledOnce( spyAFoo );
+		sinon.assert.calledOnce( spyABar );
+		sinon.assert.calledOnce( spyCFoo );
+		sinon.assert.calledOnce( spyCBar );
+
+		emitterB.stopDelegating( '*' );
+
+		emitterB.fire( 'foo' );
+		emitterB.fire( 'bar' );
+
+		sinon.assert.calledOnce( spyAFoo );
+		sinon.assert.calledOnce( spyABar );
+		sinon.assert.calledOnce( spyCFoo );
+		sinon.assert.calledOnce( spyCBar );
+	} );
+
+	it( 'stops delegating all ("*") to a specific emitter', () => {
+		const emitterA = getEmitterInstance();
+		const emitterB = getEmitterInstance();
+		const emitterC = getEmitterInstance();
+		const spyAFoo = sinon.spy();
+		const spyABar = sinon.spy();
+		const spyCFoo = sinon.spy();
+		const spyCBar = sinon.spy();
+
+		emitterB.delegate( '*' ).to( emitterA );
+		emitterB.delegate( 'foo' ).to( emitterC );
+
+		emitterA.on( 'foo', spyAFoo );
+		emitterA.on( 'bar', spyABar );
+		emitterC.on( 'foo', spyCFoo );
+		emitterC.on( 'bar', spyCBar );
+
+		emitterB.fire( 'foo' );
+		emitterB.fire( 'bar' );
+
+		sinon.assert.calledOnce( spyAFoo );
+		sinon.assert.calledOnce( spyABar );
+		sinon.assert.calledOnce( spyCFoo );
+		sinon.assert.notCalled( spyCBar );
+
+		emitterB.stopDelegating( '*', emitterA );
+
+		emitterB.fire( 'foo' );
+		emitterB.fire( 'bar' );
+
+		sinon.assert.calledOnce( spyAFoo );
+		sinon.assert.calledOnce( spyABar );
+		sinon.assert.calledTwice( spyCFoo );
+		sinon.assert.notCalled( spyCBar );
+	} );
+
+	it( 'passes when stopping delegation of a specific event which has never been delegated', () => {
+		const emitterA = getEmitterInstance();
+		const emitterB = getEmitterInstance();
+
+		expect( () => {
+			emitterA.stopDelegating( 'bar' );
+			emitterA.stopDelegating( 'bar', emitterB );
+		} ).to.not.throw();
+	} );
+
 	it( 'passes when stopping delegation of a specific event to an emitter which wasn\'t a destination', () => {
 	it( 'passes when stopping delegation of a specific event to an emitter which wasn\'t a destination', () => {
 		const emitterA = getEmitterInstance();
 		const emitterA = getEmitterInstance();
 		const emitterB = getEmitterInstance();
 		const emitterB = getEmitterInstance();
@@ -784,6 +1079,18 @@ describe( 'stopDelegating', () => {
 			emitterA.stopDelegating( 'foo', emitterC );
 			emitterA.stopDelegating( 'foo', emitterC );
 		} ).to.not.throw();
 		} ).to.not.throw();
 	} );
 	} );
+
+	it( 'passes when stopping delegation of a specific event to a specific emitter which has never been delegated', () => {
+		const emitterA = getEmitterInstance();
+		const emitterB = getEmitterInstance();
+		const emitterC = getEmitterInstance();
+
+		emitterA.delegate( 'foo' ).to( emitterB );
+
+		expect( () => {
+			emitterA.stopDelegating( 'bar', emitterC );
+		} ).to.not.throw();
+	} );
 } );
 } );
 
 
 function refreshEmitter() {
 function refreshEmitter() {