Contents
background
In front-end development, sometimes projects will encounter some shortcut key requirements, such as binding shortcut keys, displaying shortcut keys, editing shortcut keys, etc., especially for tool projects. If it is just a simple requirement to bind a few shortcut keys, we will usually
keydownimplement it by monitoring keyboard events (such as events). If it is a slightly more complex requirement, we will usually implement it by introducing a third-party shortcut key library. For example, several commonly used shortcut key librariesmousetrap,hotkey-jsetc.Next, I will
mousetrapconduct a simple analysis of the source code submitted for the first time to the shortcut key library, and then implement a simple shortcut key library.
prerequisite knowledge
First, we need to understand some basic knowledge about shortcut keys. For example, how to monitor keyboard events? How to monitor the keys pressed by the user? What are the keys on the keyboard? How is it classified? Only by knowing this can we better understand mousetrapthe idea of implementing this shortcut key library and better implement our own shortcut key library.
How to listen for keyboard events
To implement shortcut keys, you need to monitor the user’s behavior of pressing keyboard keys, so you need to use the keyboard event API .
Commonly used keyboard events include keydown, keyup, keypressevents. Generally speaking, we will determine whether to trigger the corresponding shortcut key behavior by monitoring the user’s key press behavior. Generally speaking, when the user presses a key , it will be determined whether there is a matching bound shortcut key, that is, keydownthe shortcut key is implemented by listening to the event.
How to monitor the keys pressed on the keyboard
We can monitor user keystrokes through keyboard events. So how do you know which keys/keys the user pressed?
For example, if the shortcut key bound to the user is s, how do you know what key is currently pressed s? We can use these properties on the keyboard event object keyboardEvent to determine the keys currently pressed by the user.codekeyCodekey
Keyboard key classification
Some keys affect the characters produced when other keys are pressed. For example, if the user presses shiftthe and /keys at the same time, the characters generated at this time are ?. However, in fact, if only shiftthe keys are pressed, no characters will be generated . /The characters generated by just pressing the keys should be /. The characters finally generated ?are because the keys are pressed at the same time. shiftCaused by key presses. The keys here shiftare keys that affect the characters produced when other keys are pressed. Such keys are called modifier keys . Similar modifier keys include ctrl, alt( option), command( meta).
In addition to these modifier keys, other keys are called non-modifier keys .
Shortcut key classification
Commonly used shortcut keys include single keys and key combinations. Some also use key sequences.
single key
As the name suggests, single keys are shortcuts that are triggered by pressing only one key. For example, commonly used audio and video switching play/pause shortcut keys Space, shortcut keys for controlling movement direction in games w, a, s, detc.
key combination
A key combination is usually a shortcut key consisting of one or more modifier keys and a non-modifier key. For example, commonly used shortcut keys for copying and pasting ctrl+c, ctrl+v, saving file shortcut keys ctrl+s, and creating a new (browser or other app) window shortcut key ctrl+shift+n( command+shift+n).
key sequence
Keys pressed in sequence are called key sequences. For example, a key sequence h e l l orequires pressing the h, e, l, keys in sequence to trigger.lo
mousetrap source code analysis
The following will mousetrapbe a simple analysis based on the first submitted source code. The source code link is as follows: https://bit.ly/3TdcK8u
To put it simply, the code only does two things, namely binding shortcut keys and listening for keyboard events .
Code design and initialization
First, windowa global property is added to the object , and IIFE (immediately executed function expression)Mousetrap is used to encapsulate the code.
This function exposes several public methods:
bind(keys, callback, action): Bind shortcut keystrigger(): Manually trigger the callback function corresponding to the bound shortcut key.
Finally, the function windowis executed immediately after loading init(), that is, the initialization logic is executed: adding keyboard event monitoring, etc.
//The following is the simplified code
window [ 'Mousetrap' ] = ( function () {
return {
/**
* Bind shortcut keys
* @param keys shortcut keys, supports binding multiple shortcut keys at one time.
* @param callback callback function after the shortcut key is triggered
* @param action behavior
*/
bind : function ( keys, callback, action ) {
action = action || '' ;
_bindMultiple (keys. split ( ',' ), callback, action);
_direct_map[keys + ':' + action] = callback;
},
/**
* Manually trigger the callback function corresponding to the shortcut key
* @param keys shortcut keys when binding
* @param action behavior
*/
trigger : function ( keys, action ) {
_direct_map[keys + ':' + (action || '' )]();
},
/**
* Add events to DOM objects, written for browser compatibility
* @param object
* @param type
* @param callback
*/
addEvent : function ( object, type, callback ) {
_addEvent (object, type, callback);
},
init : function () {
_addEvent ( document , 'keydown' , _handleKeyDown);
_addEvent ( document , 'keyup' , _handleKeyUp);
_addEvent ( window , 'focus' , _resetModifiers);
},
};
})();
Mousetrap . addEvent ( window , 'load' , Mousetrap . init );
Bind shortcut keys
Generally speaking, the shortcut key library will provide a function for binding shortcut keys, such as bind(key, callback). In mousetrap, we can Mousetrap.bind()implement shortcut key binding by calling functions.
Mousetrap.bind()We can analyze the function based on the writing method when calling . For example, we bound the shortcut keys ctrl+sand command+sas follows:Mousetrap.bind('ctrl+s, command+s', () => {console.log('保存成功')} )
bind(keys, callback, action)
Since bind()the function supports binding multiple shortcut keys at one time (multiple shortcut keys are separated by commas when binding), the function is internally encapsulated _bindMultiple()to handle the usage of binding multiple shortcut keys at one time.
window [ 'Mousetrap' ] = ( function () {
return {
bind : function ( keys, callback, action ) {
action = action || '' ;
_bindMultiple (keys. split ( ',' ), callback, action);
_direct_map[keys + ':' + action] = callback;
},
};
})();
_bindMultiple(combinations, callback, action)
This function just traverses the multiple shortcut keys passed in during binding, and then calls _bindSingle()the function to bind them in sequence.
/**
* binds multiple combinations to the same callback
*/
function _bindMultiple ( combinations, callback, action ) {
for ( var i = 0 ; i < combinations. length ; ++i) {
_bindSingle (combinations[i], callback, action);
}
}
_bindSingle(combination, callback, action)
This function is the core code for binding shortcut keys.
It is mainly divided into the following parts:
- Split the bound shortcut keys
combinationinto a single key array, and then collect the modifier keys into the modifier key arraymodifiers. - Using
key(key code) as the attribute name, save the currently bound shortcut keys and their corresponding callback functions to the callback function collection_callbacks. - If the same shortcut key has been bound before, call
_getMatch()the function to remove the previously bound shortcut key.
/**
*binds a single event
*/
function _bindSingle ( combination, callback, action ) {
var i,
key,
keys = combination. split ( '+' ),
// Modifier key list
modifiers = [];
// Collect modifier keys into the modifier key array
for (i = 0 ; i < keys. length ; ++i) {
if (keys[i] in _MODIFIERS) {
modifiers. push (_MODIFIERS[keys[i]]);
}
// Get the key code of the current key (modified key || special key || ordinary key (az, 0-9)). Note the usage of charCodeAt() here
key = _MODIFIERS[keys[i]] || _MAP[keys[ i]] || keys[i]. toUpperCase (). charCodeAt ( 0 );
}
// Use key code as the attribute name to save the callback function
if (!_callbacks[key]) {
_callbacks[key] = [];
}
// If the same shortcut key has been bound before, remove the previously bound shortcut
key_getMatch (key, modifiers, action, true );
// Save the callback function/modifier keys and other data of the currently bound shortcut keys to the callback function array_callbacks
[key]. push ({ callback : callback, modifiers : modifiers, action : action});
}
Pay attention to the data structure here _callbacks. Assume that the following shortcut keys are bound:
Mousetrap . bind ( 's' , e => {
console . log ( 'sss' )
})
Mousetrap . bind ( 'ctrl+s' , e => {
console . log ( 'ctrl+s' )
})
The values _callbacksare as follows:
{
// key code is used as the attribute name, and the attribute value is an array, used to save the currently bound modifier keys and callback functions.
"83" : [ // 83 corresponds to the key code of the character s
{
modifiers : [],
callback : e => { console . log ( 'sss' ) }
action : ""
},
{
modifiers : [ 17 ], // 17 corresponds to the key code of the modifier key ctrl
callback : e => { console . log ( 'ctrl+s' ) }
action : ""
}
]
}
_getMatch(code, modifiers, action, remove)
_callbacksGet/delete the callback function corresponding to the bound shortcut key from the shortcut key callback function collection callback.
function _getMatch ( code, modifiers, action, remove ) {
if (!_callbacks[code]) {
return ;
}
var i,
callback;
// loop through all callbacks for the key that was pressed
// and see if any of them match
for (i = 0 ; i < _callbacks[code]. length ; ++i) {
callback = _callbacks[code][i];
if (action == callback. action && _modifiersMatch (modifiers, callback. modifiers )) {
if (remove) {
_callbacks[code]. splice (i, 1 );
}
return callback;
}
}
}
Listen for keyboard events
Event listeners are registered init()for the object in the initialization logic function.documentkeydown
⚠: Only events are analyzed here keydown, keyupevents are similar.
_addEvent ( document , 'keydown' , _handleKeyDown);
_handleKeyDown(e)
First, a function is called to _stop(e)determine whether subsequent operations need to be stopped. Return directly if necessary.
eventSecondly, obtain the key corresponding to the currently pressed key according to the keyboard event object key code, and collect all modifier keys currently pressed key codeinto the modifier key list _active_modifiers.
Finally, call _fireCallback(code, modifers, action, e)the function to obtain the callback function corresponding to the currently matched shortcut key callbackand execute it.
function _handleKeyDown ( e ) {
if ( _stop (e)) {
return ;
}
var code = _keyCodeFromEvent (e);
if (_MODS[code]) {
_active_modifiers. push (code);
}
return _fireCallback (code, _active_modifiers, '' , e);
}
_stop(e)
If keydownthe target element when the current event is triggered is input/select/textareaan element, stop processing keydownthe event.
function _stop ( e ) {
var tag_name = (e. target || e. srcElement ). tagName ;
// stop for input, select, and textarea
return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' ;
}
_keyCodeFromEvent(e)
eventGet the corresponding key according to the keyboard event object key code.
Note that this is not used directly here event.keyCode. The reason is that some keys have inconsistent values in different browsers event.keyCodeand require special processing.
function _keyCodeFromEvent ( e ) {
var code = e.keyCode ;
// right command on webkit, command on gecko
if (code == 93 || code == 224 ) {
code = 91 ;
}
return code;
}
_fireCallback(code, modifiers, action, e)
Get the callback function corresponding to the currently matched shortcut key callbackand execute it.
function _fireCallback ( code, modifiers, action, e ) {
var callback = _getMatch (code, modifiers, action);
if (callback) {
return callback. callback (e);
}
}
_getMatch(code, modifiers, action)
Get the callback function corresponding to the currently matched shortcut key callback.
function _getMatch ( code, modifiers, action, remove ) {
if (!_callbacks[code]) {
return ;
}
var i,
callback;
// loop through all callbacks for the key that was pressed
// and see if any of them match
for (i = 0 ; i < _callbacks[code]. length ; ++i) {
callback = _callbacks[code][i];
if (action == callback. action && _modifiersMatch (modifiers, callback. modifiers )) {
if (remove) {
_callbacks[code]. splice (i, 1 );
}
return callback;
}
}
}
_modifiersMatch(modifiers1, modifiers2)
Determine whether the elements in the two modifier key arrays are exactly the same. eg: _modifiersMatch(['ctrl', 'shift'], ['shift', 'ctrl'])
function _modifiersMatch ( group1, group2 ) {
return group1. sort (). join ( ',' ) === group2. sort (). join ( ',' );
}
Implement a simple shortcut key library
Combining pre-knowledge and mousetrapanalysis of the source code, we can easily implement a simple shortcut key library.
Ideas
The general idea is mousetrapalmost exactly the same, except it does two things. That is, 1. Provide external bind()functions for binding shortcut keys, 2. Internally add keydownevents, monitor keyboard input, find the callback function that matches the corresponding shortcut key callback, and execute it.
The mousetrapdifference is that this time event.keythe attribute will be used to determine the specific key pressed by the user, which is also the attribute recommended by the specification/standard ( Authors SHOULD use the keyattribute instead of the charCodeand keyCodeattributes. ).
The code will use ES6 class syntax and provide external bind()functions for binding shortcut keys.
Function
Supports binding shortcut keys (single key, key combination).
accomplish
Since the implementation idea has been analyzed previously, I will not explain it in detail here. The complete source code is given directly below.
However, there are a few things to note about the code:
event.keyAffected byshiftkeystrokes. For example, the bound shortcut key isshift+/actually the value inkeydownthe event object , so the code maintains a mapping of this special character to determine whether the user pressed this type of special character.eventevent.key?_SHIFT_MAP- The characters ( ) generated by some special character keys
event.keyrequire special processing, such as the space keySpace. The actual characters ( ) generated after pressingevent.keyare' '. For details, see the function in the codecheckKeyMatch().
/**
* this is a mapping of keys that converts characters generated by pressing shift key
* at the same time to characters produced when the shift key is not pressed
*
* @type { Object }
*/
var _SHIFT_MAP = {
'~' : '`' ,
'!' : '1' ,
'@' : '2' ,
'#' : '3' ,
$ : '4' ,
'%' : '5 ' ,
'^' : '6' ,
'&' : '7' ,
'*' : '8' ,
'(' : '9' ,
')' : '0' ,
_ : '-' ,
'+ ' : '=' ,
':' : ';' ,
'"' : "'" ,
'<' : ',' ,
'>' : '.' ,
'?' : '/' ,
'|' : '\\' ,
};
/**
* get modifer key list by keyboard event
* @param { KeyboardEvent } event - keyboard event
* @returns { Array }
*/
const getModifierKeysByKeyboardEvent = ( event ) => {
const modifiers = [];
if (event. shiftKey ) {
modifiers. push ( 'shift' );
}
if (event. altKey ) {
modifiers. push ( 'alt' );
}
if (event.ctrlKey ) {
modifiers. push ( 'ctrl' );
}
if (event. metaKey ) {
modifiers. push ( 'command' );
}
return modifiers;
};
/**
* get non modifier key
* @param { string } shortcut
* @returns { string }
*/
function getNonModifierKeyByShortcut ( shortcut ) {
if ( typeof shortcut !== 'string' ) return '' ;
if (!shortcut. trim ()) return '' ;
const validModifierKeys = [ 'shift' , 'ctrl' , 'alt' , 'command' ];
return (
shortcut. split ( '+' ). filter ( ( key ) => !validModifierKeys. includes (key))[ 0 ] ||
''
);
}
/**
* check if two modifiers match
* @param { Array } modifers1
* @param { Array } modifers2
* @returns { boolean }
*/
function checkModifiersMatch ( modifers1, modifers2 ) {
return modifers1. sort (). join ( ',' ) === modifers2. sort (). join ( ',' );
}
/**
* check if key match
* @param { string } shortcutKey - shortcut key
* @param { string } eventKey - event.key
* @returns { boolean }
*/
function checkKeyMatch ( shortcutKey, eventKey ) {
if (shortcutKey === 'space' ) {
return eventKey === ' ' ;
}
return shortcutKey === (_SHIFT_MAP[eventKey] || eventKey);
}
/**
* shortcut binder class
*/
class ShortcutBinder {
constructor () {
/**
* shortcut list
*/
this . shortcuts = [];
this . init ();
}
/**
* init, add keyboard event listener
*/
init () {
this . _addKeydownEvent ();
}
/**
* add keydown event
*/
_addKeydownEvent () {
document . addEventListener ( 'keydown' , ( event ) => {
const modifers = getModifierKeysByKeyboardEvent (event);
const matchedShortcut = this . shortcuts . find (
( shortcut ) =>
checkKeyMatch ( shortcut. key , event. key . toLowerCase ( )) &&
checkModifiersMatch (shortcut. modifiers , modifers)
);
if (matchedShortcut) {
matchedShortcut. callback (event);
}
});
}
/**
* bind shortcut & callback
* @param { string } shortcut
* @param { Function } callback
*/
bind ( shortcut, callback ) {
this . _addShortcut (shortcut, callback);
}
/**
* add shortcut & callback to shortcut list
* @param { string } shortcut
* @param { Function } callback
*/
_addShortcut ( shortcut, callback ) {
this . shortcuts . push ({
shortcut,
callback,
key : this . _getKeyByShortcut (shortcut),
modifiers : this . _getModifiersByShortcut (shortcut),
});
}
/**
* get key (character/name) by shortcut
* @param { string } shortcut
* @returns { string }
*/
_getKeyByShortcut ( shortcut ) {
const key = getNonModifierKeyByShortcut (shortcut);
return key. toLowerCase ();
}
/**
* get modifier keys by shortcut
* @param { string } shortcut
* @returns { Array }
*/
_getModifiersByShortcut ( shortcut ) {
const keys = shortcut. split ( '+' ). map ( ( key ) => key. trim ());
const VALID_MODIFIERS = [ 'shift' , 'ctrl' , 'alt' , ' command' ];
let modifiers = [];
keys. forEach ( ( key ) => {
if ( VALID_MODIFIERS . includes (key)) {
modifiers. push (key);
}
});
return modifiers;
}
}
transfer
Calling methods and mousetrapsimilar. Only part of the test code is listed below. You can view the actual effect of the online sample test.
shortcutBinder. bind ( 'ctrl+s' , () => {
console . log ( 'ctrl+s' );
});
shortcutBinder. bind ( 'ctrl+shift+s' , () => {
console . log ( 'ctrl+shift+s' );
});
shortcutBinder. bind ( 'space' , ( e ) => {
e. preventDefault ();
console . log ( 'space' );
});
shortcutBinder.bind ( ' shift +5' , ( e ) => {
e. preventDefault ();
console . log ( 'shift+5' );
});
shortcutBinder.bind ( ` shift +\\` , ( e ) => {
e. preventDefault ();
console . log ( 'shift+\\' );
});
shortcutBinder.bind ( `f2` , ( e ) => {
e. preventDefault ();
console . log ( 'f2' );
});
TODO
So far, we have implemented a simple shortcut key library that can meet common business needs related to shortcut key bindings. Of course, compared with several currently popular shortcut key libraries, the shortcut key library we implemented is relatively simple, and there are still many functions and details to be implemented and improved. Listed below are several items to be completed. If you are interested, you can try to implement them.
- Support setting key sequence shortcut keys
- Support setting shortcut key scope
- Support unbinding a single shortcut key
- Supports resetting all bound shortcut keys
- Supports obtaining all bound shortcut key information
Summarize
By studying mousetrapthe source code and handwriting a simple shortcut key library, we can learn some knowledge about shortcut keys and keyboard events. The purpose is not to reinvent the wheel, but to drive us to understand the implementation ideas of the currently popular common shortcut key libraries through daily business needs, so that we can better understand and implement related business needs. If there is a need to display or modify shortcut keys or other shortcut keys in the future, we can be confident and draw inferences from one example.
