Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit 14af24c

Browse files
authored
Merge pull request #181 from ckeditor/t/ckeditor5/447
Feature: `KeystrokeHandler` should support priorities and proper keystroke cancelling. Closes #180.
2 parents 8c131a9 + a5018b6 commit 14af24c

File tree

2 files changed

+110
-89
lines changed

2 files changed

+110
-89
lines changed

src/keystrokehandler.js

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { getCode, parseKeystroke } from './keyboard';
1919
*
2020
* handler.listenTo( emitter );
2121
*
22-
* handler.set( 'ctrl + a', ( keyEventData, cancel ) => {
23-
* console.log( 'ctrl + a has been pressed' );
22+
* handler.set( 'Ctrl+A', ( keyEvtData, cancel ) => {
23+
* console.log( 'Ctrl+A has been pressed' );
2424
* cancel();
2525
* } );
2626
*/
@@ -36,14 +36,6 @@ export default class KeystrokeHandler {
3636
* @member {module:utils/dom/emittermixin~Emitter}
3737
*/
3838
this._listener = Object.create( DomEmitterMixin );
39-
40-
/**
41-
* Map of the defined keystrokes. Keystroke codes are the keys.
42-
*
43-
* @private
44-
* @member {Map}
45-
*/
46-
this._keystrokes = new Map();
4739
}
4840

4941
/**
@@ -52,8 +44,17 @@ export default class KeystrokeHandler {
5244
* @param {module:utils/emittermixin~Emitter} emitter
5345
*/
5446
listenTo( emitter ) {
55-
this._listener.listenTo( emitter, 'keydown', ( evt, data ) => {
56-
this.press( data );
47+
// The #_listener works here as a kind of dispatcher. It groups the events coming from the same
48+
// keystroke so the listeners can be attached to them with different priorities.
49+
//
50+
// E.g. all the keystrokes with the `keyCode` of 42 coming from the `emitter` are propagated
51+
// as a `_keydown:42` event by the `_listener`. If there's a callback created by the `set`
52+
// method for this 42 keystroke, it listens to the `_listener#_keydown:42` event only and interacts
53+
// only with other listeners of this particular event, thus making it possible to prioritize
54+
// the listeners and safely cancel execution, when needed. Instead of duplicating the Emitter logic,
55+
// the KeystrokeHandler re–uses it to do its job.
56+
this._listener.listenTo( emitter, 'keydown', ( evt, keyEvtData ) => {
57+
this._listener.fire( '_keydown:' + getCode( keyEvtData ), keyEvtData );
5758
} );
5859
}
5960

@@ -65,47 +66,48 @@ export default class KeystrokeHandler {
6566
* @param {Function} callback A function called with the
6667
* {@link module:engine/view/observer/keyobserver~KeyEventData key event data} object and
6768
* a helper to both `preventDefault` and `stopPropagation` of the event.
69+
* @param {Object} [options={}] Additional options.
70+
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of the keystroke
71+
* callback. The higher the priority value the sooner the callback will be executed. Keystrokes having the same priority
72+
* are called in the order they were added.
6873
*/
69-
set( keystroke, callback ) {
74+
set( keystroke, callback, options = {} ) {
7075
const keyCode = parseKeystroke( keystroke );
71-
const callbacks = this._keystrokes.get( keyCode );
76+
const priority = options.priority;
77+
78+
// Execute the passed callback on KeystrokeHandler#_keydown.
79+
// TODO: https://github.com/ckeditor/ckeditor5-utils/issues/144
80+
this._listener.listenTo( this._listener, '_keydown:' + keyCode, ( evt, keyEvtData ) => {
81+
callback( keyEvtData, () => {
82+
// Stop the event in the DOM: no listener in the web page
83+
// will be triggered by this event.
84+
keyEvtData.preventDefault();
85+
keyEvtData.stopPropagation();
7286

73-
if ( callbacks ) {
74-
callbacks.push( callback );
75-
} else {
76-
this._keystrokes.set( keyCode, [ callback ] );
77-
}
87+
// Stop the event in the KeystrokeHandler: no more callbacks
88+
// will be executed for this keystroke.
89+
evt.stop();
90+
} );
91+
92+
// Mark this keystroke as handled by the callback. See: #press.
93+
evt.return = true;
94+
}, { priority } );
7895
}
7996

8097
/**
8198
* Triggers a keystroke handler for a specified key combination, if such a keystroke was {@link #set defined}.
8299
*
83-
* @param {module:engine/view/observer/keyobserver~KeyEventData} keyEventData Key event data.
100+
* @param {module:engine/view/observer/keyobserver~KeyEventData} keyEvtData Key event data.
84101
* @returns {Boolean} Whether the keystroke was handled.
85102
*/
86-
press( keyEventData ) {
87-
const keyCode = getCode( keyEventData );
88-
const callbacks = this._keystrokes.get( keyCode );
89-
90-
if ( !callbacks ) {
91-
return false;
92-
}
93-
94-
for ( const callback of callbacks ) {
95-
callback( keyEventData, () => {
96-
keyEventData.preventDefault();
97-
keyEventData.stopPropagation();
98-
} );
99-
}
100-
101-
return true;
103+
press( keyEvtData ) {
104+
return !!this._listener.fire( '_keydown:' + getCode( keyEvtData ), keyEvtData );
102105
}
103106

104107
/**
105108
* Destroys the keystroke handler.
106109
*/
107110
destroy() {
108-
this._keystrokes = new Map();
109111
this._listener.stopListening();
110112
}
111113
}

tests/keystrokehandler.js

Lines changed: 71 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,14 @@ describe( 'KeystrokeHandler', () => {
1919

2020
describe( 'listenTo()', () => {
2121
it( 'activates the listening on the emitter', () => {
22-
emitter = Object.create( EmitterMixin );
23-
keystrokes = new KeystrokeHandler();
24-
25-
const spy = sinon.spy( keystrokes, 'press' );
26-
const keyEvtData = { keyCode: 1 };
27-
28-
emitter.fire( 'keydown', keyEvtData );
29-
30-
expect( spy.notCalled ).to.be.true;
31-
32-
keystrokes.listenTo( emitter );
33-
emitter.fire( 'keydown', keyEvtData );
34-
35-
sinon.assert.calledOnce( spy );
36-
sinon.assert.calledWithExactly( spy, keyEvtData );
37-
} );
38-
39-
it( 'triggers #press on #keydown', () => {
40-
const spy = sinon.spy( keystrokes, 'press' );
41-
const keyEvtData = { keyCode: 1 };
22+
const spy = sinon.spy();
23+
const keyEvtData = getCtrlA();
4224

25+
keystrokes.set( 'Ctrl+A', spy );
4326
emitter.fire( 'keydown', keyEvtData );
4427

4528
sinon.assert.calledOnce( spy );
46-
sinon.assert.calledWithExactly( spy, keyEvtData );
29+
sinon.assert.calledWithExactly( spy, keyEvtData, sinon.match.func );
4730
} );
4831
} );
4932

@@ -52,7 +35,7 @@ describe( 'KeystrokeHandler', () => {
5235
const spy = sinon.spy();
5336
const keyEvtData = getCtrlA();
5437

55-
keystrokes.set( 'ctrl + A', spy );
38+
keystrokes.set( 'Ctrl+A', spy );
5639

5740
const wasHandled = keystrokes.press( keyEvtData );
5841

@@ -61,31 +44,6 @@ describe( 'KeystrokeHandler', () => {
6144
expect( wasHandled ).to.be.true;
6245
} );
6346

64-
it( 'provides a callback which both preventDefault and stopPropagation', done => {
65-
const keyEvtData = getCtrlA();
66-
67-
Object.assign( keyEvtData, {
68-
preventDefault: sinon.spy(),
69-
stopPropagation: sinon.spy()
70-
} );
71-
72-
keystrokes.set( 'ctrl + A', ( data, cancel ) => {
73-
expect( data ).to.equal( keyEvtData );
74-
75-
sinon.assert.notCalled( keyEvtData.preventDefault );
76-
sinon.assert.notCalled( keyEvtData.stopPropagation );
77-
78-
cancel();
79-
80-
sinon.assert.calledOnce( keyEvtData.preventDefault );
81-
sinon.assert.calledOnce( keyEvtData.stopPropagation );
82-
83-
done();
84-
} );
85-
86-
emitter.fire( 'keydown', keyEvtData );
87-
} );
88-
8947
it( 'returns false when no handler', () => {
9048
const keyEvtData = getCtrlA();
9149

@@ -99,7 +57,7 @@ describe( 'KeystrokeHandler', () => {
9957
it( 'handles array format', () => {
10058
const spy = sinon.spy();
10159

102-
keystrokes.set( [ 'ctrl', 'A' ], spy );
60+
keystrokes.set( [ 'Ctrl', 'A' ], spy );
10361

10462
expect( keystrokes.press( getCtrlA() ) ).to.be.true;
10563
} );
@@ -108,14 +66,70 @@ describe( 'KeystrokeHandler', () => {
10866
const spy1 = sinon.spy();
10967
const spy2 = sinon.spy();
11068

111-
keystrokes.set( [ 'ctrl', 'A' ], spy1 );
112-
keystrokes.set( [ 'ctrl', 'A' ], spy2 );
69+
keystrokes.set( [ 'Ctrl', 'A' ], spy1 );
70+
keystrokes.set( [ 'Ctrl', 'A' ], spy2 );
11371

11472
keystrokes.press( getCtrlA() );
11573

11674
sinon.assert.calledOnce( spy1 );
11775
sinon.assert.calledOnce( spy2 );
11876
} );
77+
78+
it( 'supports priorities', () => {
79+
const spy1 = sinon.spy();
80+
const spy2 = sinon.spy();
81+
const spy3 = sinon.spy();
82+
const spy4 = sinon.spy();
83+
84+
keystrokes.set( [ 'Ctrl', 'A' ], spy1 );
85+
keystrokes.set( [ 'Ctrl', 'A' ], spy2, { priority: 'high' } );
86+
keystrokes.set( [ 'Ctrl', 'A' ], spy3, { priority: 'low' } );
87+
keystrokes.set( [ 'Ctrl', 'A' ], spy4 );
88+
89+
keystrokes.press( getCtrlA() );
90+
91+
sinon.assert.callOrder( spy2, spy1, spy4, spy3 );
92+
} );
93+
94+
it( 'provides a callback which causes preventDefault and stopPropagation in the DOM', done => {
95+
const keyEvtData = getCtrlA();
96+
97+
keystrokes.set( 'Ctrl+A', ( data, cancel ) => {
98+
expect( data ).to.equal( keyEvtData );
99+
100+
sinon.assert.notCalled( keyEvtData.preventDefault );
101+
sinon.assert.notCalled( keyEvtData.stopPropagation );
102+
103+
cancel();
104+
105+
sinon.assert.calledOnce( keyEvtData.preventDefault );
106+
sinon.assert.calledOnce( keyEvtData.stopPropagation );
107+
108+
done();
109+
} );
110+
111+
emitter.fire( 'keydown', keyEvtData );
112+
} );
113+
114+
it( 'provides a callback which stops the event and remaining callbacks in the keystroke handler', () => {
115+
const spy1 = sinon.spy();
116+
const spy2 = sinon.spy();
117+
const spy3 = sinon.spy();
118+
const spy4 = sinon.spy();
119+
120+
keystrokes.set( [ 'Ctrl', 'A' ], spy1 );
121+
keystrokes.set( [ 'Ctrl', 'A' ], spy2, { priority: 'high' } );
122+
keystrokes.set( [ 'Ctrl', 'A' ], spy3, { priority: 'low' } );
123+
keystrokes.set( [ 'Ctrl', 'A' ], ( keyEvtData, cancel ) => {
124+
spy4();
125+
cancel();
126+
} );
127+
128+
keystrokes.press( getCtrlA() );
129+
130+
sinon.assert.callOrder( spy2, spy1, spy4 );
131+
sinon.assert.notCalled( spy3 );
132+
} );
119133
} );
120134

121135
describe( 'destroy()', () => {
@@ -133,7 +147,7 @@ describe( 'KeystrokeHandler', () => {
133147
const spy = sinon.spy();
134148
const keystrokeHandler = keystrokes;
135149

136-
keystrokeHandler.set( 'ctrl + A', spy );
150+
keystrokeHandler.set( 'Ctrl+A', spy );
137151

138152
keystrokeHandler.destroy();
139153

@@ -146,5 +160,10 @@ describe( 'KeystrokeHandler', () => {
146160
} );
147161

148162
function getCtrlA() {
149-
return { keyCode: keyCodes.a, ctrlKey: true };
163+
return {
164+
keyCode: keyCodes.a,
165+
ctrlKey: true,
166+
preventDefault: sinon.spy(),
167+
stopPropagation: sinon.spy()
168+
};
150169
}

0 commit comments

Comments
 (0)