8
0
Quellcode durchsuchen

Merge remote-tracking branch 'origin/master' into ckeditor5/t/389

Piotrek Koszuliński vor 9 Jahren
Ursprung
Commit
c42877881c

+ 11 - 11
packages/ckeditor5-utils/src/focustracker.js

@@ -39,6 +39,14 @@ export default class FocusTracker {
 		this.set( 'isFocused', false );
 
 		/**
+		 * Currently focused element.
+		 *
+		 * @readonly
+		 * @member {HTMLElement}
+		 */
+		this.focusedElement = null;
+
+		/**
 		 * List of registered elements.
 		 *
 		 * @private
@@ -53,14 +61,6 @@ export default class FocusTracker {
 		 * @member {Number}
 		 */
 		this._nextEventLoopTimeout = null;
-
-		/**
-		 * Currently focused element.
-		 *
-		 * @private
-		 * @member {HTMLElement}
-		 */
-		this._focusedElement = null;
 	}
 
 	/**
@@ -84,7 +84,7 @@ export default class FocusTracker {
 	 * @param {HTMLElement} element
 	 */
 	remove( element ) {
-		if ( element === this._focusedElement ) {
+		if ( element === this.focusedElement ) {
 			this._blur( element );
 		}
 
@@ -103,7 +103,7 @@ export default class FocusTracker {
 	_focus( element ) {
 		clearTimeout( this._nextEventLoopTimeout );
 
-		this._focusedElement = element;
+		this.focusedElement = element;
 		this.isFocused = true;
 	}
 
@@ -116,7 +116,7 @@ export default class FocusTracker {
 	 */
 	_blur() {
 		this._nextEventLoopTimeout = setTimeout( () => {
-			this._focusedElement = null;
+			this.focusedElement = null;
 			this.isFocused = false;
 		}, 0 );
 	}

+ 6 - 0
packages/ckeditor5-utils/src/keyboard.js

@@ -19,6 +19,7 @@ import env from './env';
  *
  * * `a-z`,
  * * `0-9`,
+ * * `f1-f12`,
  * * `arrow(left|up|right|bottom)`,
  * * `backspace`, `delete`, `enter`, `esc`, `tab`,
  * * `ctrl`, `cmd`, `shift`, `alt`.
@@ -140,6 +141,11 @@ function generateKnownKeyCodes() {
 		keyCodes[ code - 48 ] = code;
 	}
 
+	// F1-F12
+	for ( let code = 112; code <= 123; code++ ) {
+		keyCodes[ 'f' + ( code - 111 ) ] = code;
+	}
+
 	return keyCodes;
 }
 

+ 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();
+	}
+}

+ 8 - 1
packages/ckeditor5-utils/tests/keyboard.js

@@ -23,11 +23,14 @@ describe( 'Keyboard', () => {
 			expect( keyCodes.delete ).to.equal( 46 );
 			expect( keyCodes.ctrl ).to.equal( 0x110000 );
 			expect( keyCodes.cmd ).to.equal( 0x110000 );
+			expect( keyCodes.f1 ).to.equal( 112 );
+			expect( keyCodes.f12 ).to.equal( 123 );
 
 			expect( keyCodes ).to.include.keys(
 				'ctrl', 'cmd', 'shift', 'alt',
 				'arrowleft', 'arrowup', 'arrowright', 'arrowdown',
-				'backspace', 'delete', 'enter', 'esc', 'tab'
+				'backspace', 'delete', 'enter', 'esc', 'tab',
+				'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12'
 			);
 		} );
 	} );
@@ -41,6 +44,10 @@ describe( 'Keyboard', () => {
 			expect( getCode( 'a' ) ).to.equal( 65 );
 		} );
 
+		it( 'gets code of a function key', () => {
+			expect( getCode( 'f6' ) ).to.equal( 117 );
+		} );
+
 		it( 'is case insensitive', () => {
 			expect( getCode( 'A' ) ).to.equal( 65 );
 			expect( getCode( 'Ctrl' ) ).to.equal( 0x110000 );

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

@@ -0,0 +1,150 @@
+/**
+ * @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 );
+		} );
+
+		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 };
+}