Sfoglia il codice sorgente

Extracted a generic KeystrokeHandler helper from ckeditor5-core.

Aleksander Nowodzinski 9 anni fa
parent
commit
0e6a8d19cb

+ 111 - 0
packages/ckeditor5-utils/src/keystrokehandler.js

@@ -0,0 +1,111 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module utils/keystrokehandler
+ */
+
+import DomEmitterMixin from 'ckeditor5-utils/src/dom/emittermixin';
+import { getCode, parseKeystroke } from 'ckeditor5-utils/src/keyboard';
+
+/**
+ * Keystroke handler registers keystrokes so the callbacks associated
+ * with these keystrokes will be executed if the matching `keydown` is fired
+ * by a defined emitter.
+ *
+ *		const handler = new KeystrokeHandler();
+ *
+ *		handler.listenTo( emitter );
+ *
+ *		handler.set( 'ctrl + a', ( keyEventData, cancel ) => {
+ *			console.log( 'ctrl + a has been pressed' );
+ *			cancel();
+ *		} );
+ */
+export default class KeystrokeHandler {
+	/**
+	 * Creates an instance of the keystroke handler.
+	 */
+	constructor() {
+		/**
+		 * Listener used to listen to events for easier keystroke handler destruction.
+		 *
+		 * @private
+		 * @member {module:utils/dom/emittermixin~Emitter}
+		 */
+		this._listener = Object.create( DomEmitterMixin );
+
+		/**
+		 * Map of the defined keystrokes. Keystroke codes are the keys.
+		 *
+		 * @private
+		 * @member {Map}
+		 */
+		this._keystrokes = new Map();
+	}
+
+	/**
+	 * Starts listening for `keydown` events from a given emitter.
+	 *
+	 * @param {module:utils/emittermixin~Emitter} emitter
+	 */
+	listenTo( emitter ) {
+		this._listener.listenTo( emitter, 'keydown', ( evt, data ) => {
+			this.press( data );
+		} );
+	}
+
+	/**
+	 * Registers a handler for the specified keystroke.
+	 *
+	 * @param {String|Array.<String|Number>} keystroke Keystroke defined in a format accepted by
+	 * the {@link module:utils/keyboard~parseKeystroke} function.
+	 * @param {Function} callback A function called with the
+	 * {@link module:engine/view/observer/keyobserver~KeyEventData key event data} object and
+	 * a helper to both `preventDefault` and `stopPropagation` of the event.
+	 */
+	set( keystroke, callback ) {
+		const keyCode = parseKeystroke( keystroke );
+		const callbacks = this._keystrokes.get( keyCode );
+
+		if ( callbacks ) {
+			callbacks.push( callback );
+		} else {
+			this._keystrokes.set( keyCode, [ callback ] );
+		}
+	}
+
+	/**
+	 * Triggers a keystroke handler for a specified key combination, if such a keystroke was {@link #set defined}.
+	 *
+	 * @param {module:engine/view/observer/keyobserver~KeyEventData} keyEventData Key event data.
+	 * @returns {Boolean} Whether the keystroke was handled.
+	 */
+	press( keyEventData ) {
+		const keyCode = getCode( keyEventData );
+		const callbacks = this._keystrokes.get( keyCode );
+
+		if ( !callbacks ) {
+			return false;
+		}
+
+		for ( let callback of callbacks ) {
+			callback( keyEventData, () => {
+				keyEventData.preventDefault();
+				keyEventData.stopPropagation();
+			} );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Destroys the keystroke handler.
+	 */
+	destroy() {
+		this._keystrokes = new Map();
+		this._listener.stopListening();
+	}
+}

+ 152 - 0
packages/ckeditor5-utils/tests/keystrokehandler.js

@@ -0,0 +1,152 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import EmitterMixin from 'ckeditor5-utils/src/emittermixin';
+import KeystrokeHandler from 'ckeditor5-utils/src/keystrokehandler';
+import { keyCodes } from 'ckeditor5-utils/src/keyboard';
+
+describe( 'KeystrokeHandler', () => {
+	let emitter, keystrokes;
+
+	beforeEach( () => {
+		emitter = Object.create( EmitterMixin );
+		keystrokes = new KeystrokeHandler();
+
+		keystrokes.listenTo( emitter );
+	} );
+
+	describe( 'listenTo()', () => {
+		it( 'activates the listening on the emitter', () => {
+			emitter = Object.create( EmitterMixin );
+			keystrokes = new KeystrokeHandler();
+
+			const spy = sinon.spy( keystrokes, 'press' );
+			const keyEvtData = { keyCode: 1 };
+
+			emitter.fire( 'keydown', keyEvtData );
+
+			expect( spy.notCalled ).to.be.true;
+
+			keystrokes.listenTo( emitter );
+			emitter.fire( 'keydown', keyEvtData );
+
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWithExactly( spy, keyEvtData );
+		} );
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'triggers #press on #keydown', () => {
+			const spy = sinon.spy( keystrokes, 'press' );
+			const keyEvtData = { keyCode: 1 };
+
+			emitter.fire( 'keydown', keyEvtData );
+
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWithExactly( spy, keyEvtData );
+		} );
+	} );
+
+	describe( 'press()', () => {
+		it( 'executes a callback', () => {
+			const spy = sinon.spy();
+			const keyEvtData = getCtrlA();
+
+			keystrokes.set( 'ctrl + A', spy );
+
+			const wasHandled = keystrokes.press( keyEvtData );
+
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWithExactly( spy, keyEvtData, sinon.match.func );
+			expect( wasHandled ).to.be.true;
+		} );
+
+		it( 'provides a callback which both preventDefault and stopPropagation', ( done ) => {
+			const keyEvtData = getCtrlA();
+
+			Object.assign( keyEvtData, {
+				preventDefault: sinon.spy(),
+				stopPropagation: sinon.spy()
+			} );
+
+			keystrokes.set( 'ctrl + A', ( data, cancel ) => {
+				expect( data ).to.equal( keyEvtData );
+
+				sinon.assert.notCalled( keyEvtData.preventDefault );
+				sinon.assert.notCalled( keyEvtData.stopPropagation );
+
+				cancel();
+
+				sinon.assert.calledOnce( keyEvtData.preventDefault );
+				sinon.assert.calledOnce( keyEvtData.stopPropagation );
+
+				done();
+			} );
+
+			emitter.fire( 'keydown', keyEvtData );
+		} );
+
+		it( 'returns false when no handler', () => {
+			const keyEvtData = getCtrlA();
+
+			const wasHandled = keystrokes.press( keyEvtData );
+
+			expect( wasHandled ).to.be.false;
+		} );
+	} );
+
+	describe( 'set()', () => {
+		it( 'handles array format', () => {
+			const spy = sinon.spy();
+
+			keystrokes.set( [ 'ctrl', 'A' ], spy );
+
+			expect( keystrokes.press( getCtrlA() ) ).to.be.true;
+		} );
+
+		it( 'aggregates multiple callbacks for the same keystroke', () => {
+			const spy1 = sinon.spy();
+			const spy2 = sinon.spy();
+
+			keystrokes.set( [ 'ctrl', 'A' ], spy1 );
+			keystrokes.set( [ 'ctrl', 'A' ], spy2 );
+
+			keystrokes.press( getCtrlA() );
+
+			sinon.assert.calledOnce( spy1 );
+			sinon.assert.calledOnce( spy2 );
+		} );
+	} );
+
+	describe( 'destroy()', () => {
+		it( 'detaches #keydown listener', () => {
+			const spy = sinon.spy( keystrokes, 'press' );
+
+			keystrokes.destroy();
+
+			emitter.fire( 'keydown', { keyCode: 1 } );
+
+			sinon.assert.notCalled( spy );
+		} );
+
+		it( 'removes all keystrokes', () => {
+			const spy = sinon.spy();
+			const keystrokeHandler = keystrokes;
+
+			keystrokeHandler.set( 'ctrl + A', spy );
+
+			keystrokeHandler.destroy();
+
+			const wasHandled = keystrokeHandler.press( getCtrlA() );
+
+			expect( wasHandled ).to.be.false;
+			sinon.assert.notCalled( spy );
+		} );
+	} );
+} );
+
+function getCtrlA() {
+	return { keyCode: keyCodes.a, ctrlKey: true };
+}