/
keystrokehandler.ts
144 lines (132 loc) · 5 KB
/
keystrokehandler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module utils/keystrokehandler
*/
import DomEmitterMixin, { type DomEmitter } from './dom/emittermixin.js';
import type { Emitter } from './emittermixin.js';
import { getCode, parseKeystroke, type KeystrokeInfo } from './keyboard.js';
import type { PriorityString } from './priorities.js';
/**
* Keystroke handler allows registering callbacks for given keystrokes.
*
* The most frequent use of this class is through the {@link module:core/editor/editor~Editor#keystrokes `editor.keystrokes`}
* property. It allows listening to keystrokes executed in the editing view:
*
* ```ts
* editor.keystrokes.set( 'Ctrl+A', ( keyEvtData, cancel ) => {
* console.log( 'Ctrl+A has been pressed' );
* cancel();
* } );
* ```
*
* However, this utility class can be used in various part of the UI. For instance, a certain {@link module:ui/view~View}
* can use it like this:
*
* ```ts
* class MyView extends View {
* constructor() {
* this.keystrokes = new KeystrokeHandler();
*
* this.keystrokes.set( 'tab', handleTabKey );
* }
*
* render() {
* super.render();
*
* this.keystrokes.listenTo( this.element );
* }
* }
* ```
*
* That keystroke handler will listen to `keydown` events fired in this view's main element.
*
*/
export default class KeystrokeHandler {
/**
* Listener used to listen to events for easier keystroke handler destruction.
*/
private readonly _listener: DomEmitter;
/**
* Creates an instance of the keystroke handler.
*/
constructor() {
this._listener = new ( DomEmitterMixin() )();
}
/**
* Starts listening for `keydown` events from a given emitter.
*/
public listenTo( emitter: Emitter | HTMLElement | Window ): void {
// The #_listener works here as a kind of dispatcher. It groups the events coming from the same
// keystroke so the listeners can be attached to them with different priorities.
//
// E.g. all the keystrokes with the `keyCode` of 42 coming from the `emitter` are propagated
// as a `_keydown:42` event by the `_listener`. If there's a callback created by the `set`
// method for this 42 keystroke, it listens to the `_listener#_keydown:42` event only and interacts
// only with other listeners of this particular event, thus making it possible to prioritize
// the listeners and safely cancel execution, when needed. Instead of duplicating the Emitter logic,
// the KeystrokeHandler re–uses it to do its job.
this._listener.listenTo( emitter as HTMLElement | Window, 'keydown', ( evt, keyEvtData ) => {
this._listener.fire( '_keydown:' + getCode( keyEvtData ), keyEvtData );
} );
}
/**
* Registers a handler for the specified keystroke.
*
* @param keystroke Keystroke defined in a format accepted by
* the {@link module:utils/keyboard~parseKeystroke} function.
* @param callback A function called with the
* {@link module:engine/view/observer/keyobserver~KeyEventData key event data} object and
* a helper function to call both `preventDefault()` and `stopPropagation()` on the underlying event.
* @param options Additional options.
* @param options.priority The priority of the keystroke
* callback. The higher the priority value the sooner the callback will be executed. Keystrokes having the same priority
* are called in the order they were added.
*/
public set(
keystroke: string | ReadonlyArray<string | number>,
callback: ( ev: KeyboardEvent, cancel: () => void ) => void,
options: { readonly priority?: PriorityString } = {}
): void {
const keyCode = parseKeystroke( keystroke );
const priority = options.priority;
// Execute the passed callback on KeystrokeHandler#_keydown.
// TODO: https://github.com/ckeditor/ckeditor5-utils/issues/144
this._listener.listenTo( this._listener, '_keydown:' + keyCode, ( evt, keyEvtData: KeyboardEvent ) => {
callback( keyEvtData, () => {
// Stop the event in the DOM: no listener in the web page
// will be triggered by this event.
keyEvtData.preventDefault();
keyEvtData.stopPropagation();
// Stop the event in the KeystrokeHandler: no more callbacks
// will be executed for this keystroke.
evt.stop();
} );
// Mark this keystroke as handled by the callback. See: #press.
evt.return = true;
}, { priority } );
}
/**
* Triggers a keystroke handler for a specified key combination, if such a keystroke was {@link #set defined}.
*
* @param keyEvtData Key event data.
* @returns Whether the keystroke was handled.
*/
public press( keyEvtData: Readonly<KeystrokeInfo> ): boolean {
return !!this._listener.fire( '_keydown:' + getCode( keyEvtData ), keyEvtData );
}
/**
* Stops listening to `keydown` events from the given emitter.
*/
public stopListening( emitter?: Emitter | HTMLElement | Window ): void {
this._listener.stopListening( emitter );
}
/**
* Destroys the keystroke handler.
*/
public destroy(): void {
this.stopListening();
}
}