瀏覽代碼

Merge pull request #59 from ckeditor/t/58

t/58: Implemented EditingKeystrokeHandler on top of KeystrokeHandler util.
Piotrek Koszuliński 9 年之前
父節點
當前提交
4e897f56a9

+ 80 - 0
packages/ckeditor5-core/src/editingkeystrokehandler.js

@@ -0,0 +1,80 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module core/editingkeystrokehandler
+ */
+
+import KeystrokeHandler from 'ckeditor5-utils/src/keystrokehandler';
+
+/**
+ * A keystroke handler for editor editing. Its instance is available
+ * in {@link module:core/editor/standardeditor~StandardEditor#keystrokes} so plugins
+ * can register their keystrokes.
+ *
+ * E.g. an undo plugin would do this:
+ *
+ *		editor.keystrokes.set( 'ctrl + Z', 'undo' );
+ *		editor.keystrokes.set( 'ctrl + shift + Z', 'redo' );
+ *		editor.keystrokes.set( 'ctrl + Y', 'redo' );
+ *
+ * @extends utils/keystrokehandler~KeystrokeHandler
+ */
+export default class EditingKeystrokeHandler extends KeystrokeHandler {
+	/**
+	 * Creates an instance of the keystroke handler.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor
+	 */
+	constructor( editor ) {
+		super();
+
+		/**
+		 * The editor instance.
+		 *
+		 * @readonly
+		 * @member {module:core/editor/editor~Editor}
+		 */
+		this.editor = editor;
+	}
+
+	/**
+	 * 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 module:utils/keyboard~parseKeystroke} function.
+	 * @param {Function} callback If a string is passed, then the keystroke will
+	 * {@link module:core/editor/editor~Editor#execute execute a command}.
+	 * If a function, then it will be 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 ) {
+		if ( typeof callback == 'string' ) {
+			const commandName = callback;
+
+			callback = () => {
+				this.editor.execute( commandName );
+			};
+		}
+
+		super.set( keystroke, callback );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	listenTo( emitter ) {
+		this._listener.listenTo( emitter, 'keydown', ( evt, data ) => {
+			const handled = this.press( data );
+
+			if ( handled ) {
+				data.preventDefault();
+			}
+		} );
+	}
+}

+ 5 - 2
packages/ckeditor5-core/src/editor/standardeditor.js

@@ -8,7 +8,7 @@
  */
 
 import Editor from './editor';
-import KeystrokeHandler from '../keystrokehandler';
+import EditingKeystrokeHandler from '../editingkeystrokehandler';
 import EditingController from 'ckeditor5-engine/src/controller/editingcontroller';
 
 import getDataFromElement from 'ckeditor5-utils/src/dom/getdatafromelement';
@@ -46,7 +46,7 @@ export default class StandardEditor extends Editor {
 		 * @readonly
 		 * @member {module:core/keystrokehandler~KeystrokeHandler}
 		 */
-		this.keystrokes = new KeystrokeHandler( this );
+		this.keystrokes = new EditingKeystrokeHandler( this );
 
 		/**
 		 * Editor UI instance.
@@ -58,6 +58,8 @@ export default class StandardEditor extends Editor {
 		 * @readonly
 		 * @member {module:core/editor/editorui~EditorUI} #ui
 		 */
+
+		this.keystrokes.listenTo( this.editing.view );
 	}
 
 	/**
@@ -65,6 +67,7 @@ export default class StandardEditor extends Editor {
 	 */
 	destroy() {
 		return Promise.resolve()
+			.then( () => this.keystrokes.destroy() )
 			.then( () => this.editing.destroy() )
 			.then( super.destroy() );
 	}

+ 0 - 109
packages/ckeditor5-core/src/keystrokehandler.js

@@ -1,109 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module core/keystrokehandler
- */
-
-import EmitterMixin from 'ckeditor5-utils/src/emittermixin';
-import { getCode, parseKeystroke } from 'ckeditor5-utils/src/keyboard';
-
-/**
- * Keystroke handler. Its instance is available in {@link module:core/editor/standardeditor~StandardEditor#keystrokes} so plugins
- * can register their keystrokes.
- *
- * E.g. an undo plugin would do this:
- *
- *		editor.keystrokes.set( 'ctrl + Z', 'undo' );
- *		editor.keystrokes.set( 'ctrl + shift + Z', 'redo' );
- *		editor.keystrokes.set( 'ctrl + Y', 'redo' );
- */
-export default class KeystrokeHandler {
-	/**
-	 * Creates an instance of the keystroke handler.
-	 *
-	 * @param {module:core/editor/editor~Editor} editor
-	 */
-	constructor( editor ) {
-		/**
-		 * The editor instance.
-		 *
-		 * @readonly
-		 * @member {module:core/editor/editor~Editor}
-		 */
-		this.editor = editor;
-
-		/**
-		 * Listener used to listen to events for easier keystroke handler destruction.
-		 *
-		 * @private
-		 * @member {module:utils/emittermixin~Emitter}
-		 */
-		this._listener = Object.create( EmitterMixin );
-
-		/**
-		 * Map of the defined keystrokes. Keystroke codes are the keys.
-		 *
-		 * @private
-		 * @member {Map}
-		 */
-		this._keystrokes = new Map();
-
-		this._listener.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 module:utils/keyboard~parseKeystroke} function.
-	 * @param {String|Function} callback If a string is passed, then the keystroke will
-	 * {@link module:core/editor/editor~Editor#execute execute a command}.
-	 * If a function, then it will be called with the
-	 * {@link module: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 {module: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._listener.stopListening();
-	}
-}

+ 75 - 0
packages/ckeditor5-core/tests/editingkeystrokehandler.js

@@ -0,0 +1,75 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import VirtualTestEditor from 'ckeditor5-core/tests/_utils/virtualtesteditor';
+import EditingKeystrokeHandler from 'ckeditor5-core/src/editingkeystrokehandler';
+import { keyCodes } from 'ckeditor5-utils/src/keyboard';
+
+describe( 'EditingKeystrokeHandler', () => {
+	let editor, keystrokes;
+
+	beforeEach( () => {
+		return VirtualTestEditor.create()
+			.then( newEditor => {
+				editor = newEditor;
+				keystrokes = new EditingKeystrokeHandler( editor );
+			} );
+	} );
+
+	describe( 'listenTo()', () => {
+		it( 'prevents default when keystroke was handled', () => {
+			const keyEvtData = { keyCode: 1, preventDefault: sinon.spy() };
+
+			sinon.stub( keystrokes, 'press' ).returns( true );
+
+			keystrokes.listenTo( editor.editing.view );
+			editor.editing.view.fire( 'keydown', keyEvtData );
+
+			sinon.assert.calledOnce( keyEvtData.preventDefault );
+		} );
+
+		it( 'does not prevent default when keystroke was not handled', () => {
+			const keyEvtData = { keyCode: 1, preventDefault: sinon.spy() };
+
+			sinon.stub( keystrokes, 'press' ).returns( false );
+
+			keystrokes.listenTo( editor.editing.view );
+			editor.editing.view.fire( 'keydown', keyEvtData );
+
+			sinon.assert.notCalled( keyEvtData.preventDefault );
+		} );
+	} );
+
+	describe( 'press()', () => {
+		it( 'executes a command', () => {
+			const spy = sinon.stub( editor, 'execute' );
+
+			keystrokes.set( 'ctrl + A', 'foo' );
+
+			const wasHandled = keystrokes.press( getCtrlA() );
+
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWithExactly( spy, 'foo' );
+			expect( wasHandled ).to.be.true;
+		} );
+
+		it( 'executes a callback', () => {
+			const executeSpy = sinon.stub( editor, 'execute' );
+			const callback = sinon.spy();
+
+			keystrokes.set( 'ctrl + A', callback );
+
+			const wasHandled = keystrokes.press( getCtrlA() );
+
+			expect( executeSpy.called ).to.be.false;
+			expect( callback.calledOnce ).to.be.true;
+			expect( wasHandled ).to.be.true;
+		} );
+	} );
+} );
+
+function getCtrlA() {
+	return { keyCode: keyCodes.a, ctrlKey: true };
+}

+ 59 - 7
packages/ckeditor5-core/tests/editor/standardeditor.js

@@ -5,12 +5,13 @@
 
 /* globals document */
 
+import Editor from 'ckeditor5-core/src/editor/editor';
 import StandardEditor from 'ckeditor5-core/src/editor/standardeditor';
 import HtmlDataProcessor from 'ckeditor5-engine/src/dataprocessor/htmldataprocessor';
 import { getData, setData } from 'ckeditor5-engine/src/dev-utils/model';
 
 import EditingController from 'ckeditor5-engine/src/controller/editingcontroller';
-import KeystrokeHandler from 'ckeditor5-core/src/keystrokehandler';
+import EditingKeystrokeHandler from 'ckeditor5-core/src/editingkeystrokehandler';
 import Plugin from 'ckeditor5-core/src/plugin';
 
 describe( 'StandardEditor', () => {
@@ -27,7 +28,14 @@ describe( 'StandardEditor', () => {
 
 			expect( editor ).to.have.property( 'element', editorElement );
 			expect( editor.editing ).to.be.instanceof( EditingController );
-			expect( editor.keystrokes ).to.be.instanceof( KeystrokeHandler );
+			expect( editor.keystrokes ).to.be.instanceof( EditingKeystrokeHandler );
+		} );
+
+		it( 'activates #keystrokes', () => {
+			const spy = sinon.spy( EditingKeystrokeHandler.prototype, 'listenTo' );
+			const editor = new StandardEditor( editorElement, { foo: 1 } );
+
+			sinon.assert.calledWith( spy, editor.editing.view );
 		} );
 
 		it( 'sets config', () => {
@@ -37,7 +45,51 @@ describe( 'StandardEditor', () => {
 		} );
 	} );
 
-	describe( 'create', () => {
+	describe( 'destroy()', () => {
+		it( 'returns a Promise', () => {
+			const editor = new StandardEditor( editorElement, { foo: 1 } );
+
+			expect( editor.destroy() ).to.be.an.instanceof( Promise );
+		} );
+
+		it( 'destroys the #keystrokes', () => {
+			const editor = new StandardEditor( editorElement, { foo: 1 } );
+			const spy = sinon.spy( editor.keystrokes, 'destroy' );
+
+			sinon.assert.notCalled( spy );
+
+			return editor.destroy()
+				.then( () => {
+					sinon.assert.calledOnce( spy );
+				} );
+		} );
+
+		it( 'destroys the #editing', () => {
+			const editor = new StandardEditor( editorElement, { foo: 1 } );
+			const spy = sinon.spy( editor.editing, 'destroy' );
+
+			sinon.assert.notCalled( spy );
+
+			return editor.destroy()
+				.then( () => {
+					sinon.assert.calledOnce( spy );
+				} );
+		} );
+
+		it( 'destroys the parent', () => {
+			const editor = new StandardEditor( editorElement, { foo: 1 } );
+			const spy = sinon.spy( Editor.prototype, 'destroy' );
+
+			sinon.assert.notCalled( spy );
+
+			return editor.destroy()
+				.then( () => {
+					sinon.assert.calledOnce( spy );
+				} );
+		} );
+	} );
+
+	describe( 'create()', () => {
 		it( 'initializes editor with plugins and config', () => {
 			class PluginFoo extends Plugin {}
 
@@ -79,7 +131,7 @@ describe( 'StandardEditor', () => {
 		} );
 	} );
 
-	describe( 'setData', () => {
+	describe( 'setData()', () => {
 		let editor;
 
 		beforeEach( () => {
@@ -106,7 +158,7 @@ describe( 'StandardEditor', () => {
 		} );
 	} );
 
-	describe( 'getData', () => {
+	describe( 'getData()', () => {
 		let editor;
 
 		beforeEach( () => {
@@ -133,7 +185,7 @@ describe( 'StandardEditor', () => {
 		} );
 	} );
 
-	describe( 'updateEditorElement', () => {
+	describe( 'updateEditorElement()', () => {
 		it( 'sets data to editor element', () => {
 			const editor = new StandardEditor( editorElement );
 
@@ -145,7 +197,7 @@ describe( 'StandardEditor', () => {
 		} );
 	} );
 
-	describe( 'loadDataFromEditorElement', () => {
+	describe( 'loadDataFromEditorElement()', () => {
 		it( 'sets data to editor element', () => {
 			const editor = new StandardEditor( editorElement );
 

+ 0 - 130
packages/ckeditor5-core/tests/keystrokehandler.js

@@ -1,130 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import VirtualTestEditor from 'ckeditor5-core/tests/_utils/virtualtesteditor';
-import KeystrokeHandler from 'ckeditor5-core/src/keystrokehandler';
-import { keyCodes } from 'ckeditor5-utils/src/keyboard';
-
-describe( 'KeystrokeHandler', () => {
-	let editor;
-
-	beforeEach( () => {
-		return VirtualTestEditor.create()
-			.then( newEditor => {
-				editor = newEditor;
-				editor.keystrokes = new KeystrokeHandler( editor );
-			} );
-	} );
-
-	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 };
-}