Jelajahi Sumber

Merge pull request #183 from ckeditor/t/115

Implemented keystroke handler
Szymon Kupś 9 tahun lalu
induk
melakukan
2b1f297d09

+ 3 - 1
src/creator/standardcreator.js

@@ -6,11 +6,12 @@
 'use strict';
 
 import Creator from './creator.js';
-
 import Document from '../engine/model/document.js';
 import DataController from '../engine/datacontroller.js';
 import EditingController from '../engine/editingcontroller.js';
 import HtmlDataProcessor from '../engine/dataprocessor/htmldataprocessor.js';
+import KeystrokeHandler from '../keystrokehandler.js';
+
 /**
  * Standard creator for browser environment.
  *
@@ -34,6 +35,7 @@ export default class StandardCreator extends Creator {
 		editor.document = new Document();
 		editor.editing = new EditingController( editor.document );
 		editor.data = new DataController( editor.document, dataProcessor );
+		editor.keystrokes = new KeystrokeHandler( editor );
 
 		/**
 		 * The elements replaced by {@link ckeditor5.creator.StandardCreator#_replaceElement} and their replacements.

+ 9 - 0
src/editor.js

@@ -122,6 +122,15 @@ export default class Editor {
 		 * @member {engine.DataController} ckeditor5.Editor#data
 		 */
 
+		/**
+		 * Instance of the {@link ckeditor5.KeystrokeHandler}.
+		 *
+		 * This property is set by the {@link ckeditor5.creator.Creator}.
+		 *
+		 * @readonly
+		 * @member {engine.treecontroller.DataController} ckeditor5.Editor#keystrokes
+		 */
+
 		/**
 		 * The chosen creator.
 		 *

+ 112 - 0
src/keystrokehandler.js

@@ -0,0 +1,112 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import EmitterMixin from './utils/emittermixin.js';
+import KeyObserver from './engine/view/observer/keyobserver.js';
+import { getCode, parseKeystroke } from './utils/keyboard.js';
+
+/**
+ * Keystroke handler. Its instance is available in {@link ckeditor5.Editor#keystrokes} so features
+ * can register their keystrokes.
+ *
+ * E.g. an undo feature would do this:
+ *
+ *		editor.keystrokes.set( 'ctrl + Z', 'undo' );
+ *		editor.keystrokes.set( 'ctrl + shift + Z', 'redo' );
+ *		editor.keystrokes.set( 'ctrl + Y', 'redo' );
+ *
+ * @memberOf ckeditor5
+ */
+export default class KeystrokeHandler {
+	/**
+	 * Creates an instance of the keystroke handler.
+	 *
+	 * @param {engine.treeView.TreeView} editingView
+	 */
+	constructor( editor ) {
+		/**
+		 * The editor instance.
+		 *
+		 * @readonly
+		 * @member {ckeditor5.Editor} ckeditor5.KeystrokeHandler#editor
+		 */
+		this.editor = editor;
+
+		/**
+		 * Observer used to listen to events for easier keystroke handler destruction.
+		 *
+		 * @private
+		 * @member {utils.Emitter} ckeditor5.KeystrokeHandler#_observer
+		 */
+		this._observer = Object.create( EmitterMixin );
+
+		/**
+		 * Map of the defined keystrokes. Keystroke codes are the keys.
+		 *
+		 * @private
+		 * @member {Map} ckeditor5.KeystrokeHandler#_keystrokes
+		 */
+		this._keystrokes = new Map();
+
+		editor.editing.view.addObserver( KeyObserver );
+
+		this._observer.listenTo( editor.editing.view, 'keydown', ( evt, data ) => {
+			const handled = this.press( data );
+
+			if ( handled ) {
+				data.preventDefault();
+			}
+		} );
+	}
+
+	/**
+	 * Registers a handler for the specified keystroke.
+	 *
+	 * The handler can be specified as a command name or a callback.
+	 *
+	 * @param {String|Array.<String|Number>} keystroke Keystroke defined in a format accepted by
+	 * the {@link utils.keyboard.parseKeystroke} function.
+	 * @param {String|Function} callback If a string is passed, then the keystroke will
+	 * {@link ckeditor5.Editor#execute execute a command}.
+	 * If a function, then it will be called with the
+	 * {@link engine.view.observer.keyObserver.KeyEventData key event data} object.
+	 */
+	set( keystroke, callback ) {
+		this._keystrokes.set( parseKeystroke( keystroke ), callback );
+	}
+
+	/**
+	 * Triggers a keystroke handler for a specified key combination, if such a keystroke was {@link #set defined}.
+	 *
+	 * @param {engine.view.observer.keyObserver.KeyEventData} keyEventData Key event data.
+	 * @returns {Boolean} Whether the keystroke was handled.
+	 */
+	press( keyEventData ) {
+		const keyCode = getCode( keyEventData );
+		const callback = this._keystrokes.get( keyCode );
+
+		if ( !callback ) {
+			return false;
+		}
+
+		if ( typeof callback == 'string' ) {
+			this.editor.execute( callback );
+		} else {
+			callback( keyEventData );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Destroys the keystroke handler.
+	 */
+	destroy() {
+		this._keystrokes = new Map();
+		this._observer.stopListening();
+	}
+}

+ 5 - 0
tests/creator/standardcreator.js

@@ -15,6 +15,7 @@ import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor
 import Document from '/ckeditor5/engine/model/document.js';
 import EditingController from '/ckeditor5/engine/editingcontroller.js';
 import DataController from '/ckeditor5/engine/datacontroller.js';
+import KeystrokeHandler from '/ckeditor5/keystrokehandler.js';
 
 testUtils.createSinonSandbox();
 
@@ -44,6 +45,10 @@ describe( 'Creator', () => {
 			expect( editor.data.processor ).to.be.instanceof( HtmlDataProcessor );
 		} );
 
+		it( 'creates the keystroke handler', () => {
+			expect( editor.keystrokes ).to.be.instanceof( KeystrokeHandler );
+		} );
+
 		it( 'uses HtmlDataProcessor if no processor is provided in constructor', () => {
 			creator = new StandardCreator( editor );
 

+ 134 - 0
tests/keystrokehandler.js

@@ -0,0 +1,134 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: browser-only */
+
+'use strict';
+
+import Editor from '/ckeditor5/editor.js';
+import StandardCreator from '/ckeditor5/creator/standardcreator.js';
+import { keyCodes } from '/ckeditor5/utils/keyboard.js';
+
+describe( 'KeystrokeHandler', () => {
+	let editor;
+
+	beforeEach( () => {
+		editor = new Editor( null, {
+			creator: StandardCreator
+		} );
+
+		return editor.init();
+	} );
+
+	describe( 'constructor', () => {
+		it( 'triggers #press on #keydown', () => {
+			const spy = sinon.spy( editor.keystrokes, 'press' );
+			const keyEvtData = { keyCode: 1 };
+
+			editor.editing.view.fire( 'keydown', keyEvtData );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.calledWithExactly( keyEvtData ) );
+		} );
+
+		it( 'prevents default when keystroke was handled', () => {
+			editor.keystrokes.press = () => true;
+
+			const keyEvtData = { keyCode: 1, preventDefault: sinon.spy() };
+
+			editor.editing.view.fire( 'keydown', keyEvtData );
+
+			expect( keyEvtData.preventDefault.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'press', () => {
+		it( 'executes a command', () => {
+			const spy = sinon.stub( editor, 'execute' );
+
+			editor.keystrokes.set( 'ctrl + A', 'foo' );
+
+			const wasHandled = editor.keystrokes.press( getCtrlA() );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.calledWithExactly( 'foo' ) ).to.be.true;
+			expect( wasHandled ).to.be.true;
+		} );
+
+		it( 'executes a callback', () => {
+			const spy = sinon.spy();
+			const keyEvtData = getCtrlA();
+
+			editor.keystrokes.set( 'ctrl + A', spy );
+
+			const wasHandled = editor.keystrokes.press( keyEvtData );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.calledWithExactly( keyEvtData ) ).to.be.true;
+			expect( wasHandled ).to.be.true;
+		} );
+
+		it( 'returns false when no handler', () => {
+			const keyEvtData = getCtrlA();
+
+			const wasHandled = editor.keystrokes.press( keyEvtData );
+
+			expect( wasHandled ).to.be.false;
+		} );
+	} );
+
+	describe( 'set', () => {
+		it( 'handles array format', () => {
+			const spy = sinon.spy();
+
+			editor.keystrokes.set( [ 'ctrl', 'A' ], spy );
+
+			expect( editor.keystrokes.press( getCtrlA() ) ).to.be.true;
+		} );
+
+		it( 'overrides existing keystroke', () => {
+			const spy1 = sinon.spy();
+			const spy2 = sinon.spy();
+
+			editor.keystrokes.set( [ 'ctrl', 'A' ], spy1 );
+			editor.keystrokes.set( [ 'ctrl', 'A' ], spy2 );
+
+			editor.keystrokes.press( getCtrlA() );
+
+			expect( spy1.calledOnce ).to.be.false;
+			expect( spy2.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'destroy', () => {
+		it( 'detaches #keydown listener', () => {
+			const spy = sinon.spy( editor.keystrokes, 'press' );
+
+			editor.keystrokes.destroy();
+
+			editor.editing.view.fire( 'keydown', { keyCode: 1 } );
+
+			expect( spy.called ).to.be.false;
+		} );
+
+		it( 'removes all keystrokes', () => {
+			const spy = sinon.spy();
+			const keystrokeHandler = editor.keystrokes;
+
+			keystrokeHandler.set( 'ctrl + A', spy );
+
+			keystrokeHandler.destroy();
+
+			const wasHandled = keystrokeHandler.press( getCtrlA() );
+
+			expect( wasHandled ).to.be.false;
+			expect( spy.called ).to.be.false;
+		} );
+	} );
+} );
+
+function getCtrlA() {
+	return { keyCode: keyCodes.a, ctrlKey: true };
+}