Ver código fonte

Merge branch 'master' into docs-fixes

Piotrek Koszuliński 9 anos atrás
pai
commit
d31319afc3

+ 48 - 16
packages/ckeditor5-utils/src/emittermixin.js

@@ -279,11 +279,14 @@ const EmitterMixin = {
 		// Delegate event to other emitters if needed.
 		if ( this._delegations ) {
 			const destinations = this._delegations.get( event );
+			const passAllDestinations = this._delegations.get( '*' );
 
 			if ( destinations ) {
-				for ( let dest of destinations ) {
-					dest.fire( eventInfo, ...args );
-				}
+				fireDelegatedEvents( destinations, eventInfo, args );
+			}
+
+			if ( passAllDestinations ) {
+				fireDelegatedEvents( passAllDestinations, eventInfo, args );
 			}
 		}
 	},
@@ -304,17 +307,11 @@ const EmitterMixin = {
 	 *
 	 * @method #delegate
 	 * @param {...String} events Event names that will be delegated to another emitter.
-	 * @returns {module:utils/emittermixin~EmitterMixin.delegate#to}
+	 * @returns {module:utils/emittermixin~EmitterMixinDelegateChain}
 	 */
 	delegate( ...events ) {
 		return {
-			/**
-			 * Selects destination for {@link module:utils/emittermixin~EmitterMixin#delegate} events.
-			 *
-			 * @method module:utils/emittermixin~EmitterMixin.delegate#to
-			 * @param {module:utils/emittermixin~Emitter} emitter An `EmitterMixin` instance which is the destination for delegated events.
-			 */
-			to: ( emitter ) => {
+			to: ( emitter, nameOrFunction ) => {
 				if ( !this._delegations ) {
 					this._delegations = new Map();
 				}
@@ -323,9 +320,9 @@ const EmitterMixin = {
 					let destinations = this._delegations.get( eventName );
 
 					if ( !destinations ) {
-						this._delegations.set( eventName, [ emitter ] );
+						this._delegations.set( eventName, new Map( [ [ emitter, nameOrFunction ] ] ) );
 					} else {
-						destinations.push( emitter );
+						destinations.set( emitter, nameOrFunction );
 					}
 				}
 			}
@@ -355,10 +352,9 @@ const EmitterMixin = {
 			this._delegations.delete( event );
 		} else {
 			const destinations = this._delegations.get( event );
-			const index = destinations.indexOf( emitter );
 
-			if ( index !== -1 ) {
-				destinations.splice( index, 1 );
+			if ( destinations ) {
+				destinations.delete( emitter );
 			}
 		}
 	}
@@ -497,8 +493,44 @@ function getCallbacksForEvent( source, eventName ) {
 	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 module:utils/emittermixin~EmitterMixin}.
  *
  * @interface Emitter
  */
+
+/**
+ * The return value of {@link ~EmitterMixin#delegate}.
+ *
+ * @interface module:utils/emittermixin~EmitterMixinDelegateChain
+ */
+
+/**
+ * Selects destination for {@link module:utils/emittermixin~EmitterMixin#delegate} events.
+ *
+ * @method #to
+ * @param {module:utils/emittermixin~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.
+ */

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

@@ -22,6 +22,7 @@ export default class EventInfo {
 		/**
 		 * The object that fired the event.
 		 *
+		 * @readonly
 		 * @member {Object}
 		 */
 		this.source = source;
@@ -29,6 +30,7 @@ export default class EventInfo {
 		/**
 		 * The event name.
 		 *
+		 * @readonly
 		 * @member {String}
 		 */
 		this.name = name;
@@ -36,6 +38,7 @@ export default class EventInfo {
 		/**
 		 * Path this event has followed. See {@link module:utils/emittermixin~EmitterMixin#delegate}.
 		 *
+		 * @readonly
 		 * @member {Array.<Object>}
 		 */
 		this.path = [];

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

@@ -670,6 +670,32 @@ describe( 'delegate', () => {
 			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', () => {
 			const emitterA = getEmitterInstance();
 			const emitterB = getEmitterInstance();
@@ -685,6 +711,170 @@ describe( 'delegate', () => {
 
 			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 );
 	} );
 
+	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', () => {
 		const emitterA = getEmitterInstance();
 		const emitterB = getEmitterInstance();
@@ -784,6 +1079,18 @@ describe( 'stopDelegating', () => {
 			emitterA.stopDelegating( 'foo', emitterC );
 		} ).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() {