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
keydown
implement 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-js
etc.Next, I will
mousetrap
conduct 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 mousetrap
the 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
, keypress
events. 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, keydown
the 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.code
keyCode
key
Keyboard key classification
Some keys affect the characters produced when other keys are pressed. For example, if the user presses shift
the and /
keys at the same time, the characters generated at this time are ?
. However, in fact, if only shift
the 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. shift
Caused by key presses. The keys here shift
are 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
, d
etc.
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 o
requires pressing the h
, e
, l
, keys in sequence to trigger.l
o
mousetrap source code analysis
The following will mousetrap
be 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, window
a 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 window
is 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+s
and command+s
as 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
combination
into 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 _callbacks
are 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)
_callbacks
Get/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.document
keydown
⚠: Only events are analyzed here keydown
, keyup
events 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.
event
Secondly, 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 code
into 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 callback
and 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 keydown
the target element when the current event is triggered is input/select/textarea
an element, stop processing keydown
the 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)
event
Get 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.keyCode
and 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 callback
and 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 mousetrap
analysis of the source code, we can easily implement a simple shortcut key library.
Ideas
The general idea is mousetrap
almost exactly the same, except it does two things. That is, 1. Provide external bind()
functions for binding shortcut keys, 2. Internally add keydown
events, monitor keyboard input, find the callback function that matches the corresponding shortcut key callback
, and execute it.
The mousetrap
difference is that this time event.key
the 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 key
attribute instead of the charCode
and keyCode
attributes. ).
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.key
Affected byshift
keystrokes. For example, the bound shortcut key isshift+/
actually the value inkeydown
the event object , so the code maintains a mapping of this special character to determine whether the user pressed this type of special character.event
event.key
?
_SHIFT_MAP
- The characters ( ) generated by some special character keys
event.key
require special processing, such as the space keySpace
. The actual characters ( ) generated after pressingevent.key
are' '
. 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 mousetrap
similar. 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 mousetrap
the 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.