Handwrite a simple shortcut library using JavaScript

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 libraries mousetraphotkey-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 keydownkeyupkeypressevents. 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 ctrlaltoption), commandmeta).

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 wasdetc.

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+cctrl+v, saving file shortcut keys ctrl+s, and creating a new (browser or other app) window shortcut key ctrl+shift+ncommand+shift+n).

key sequence

Keys pressed in sequence are called key sequences. For example, a key sequence h e l l orequires pressing the hel, 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 keys
  • trigger(): 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:

  1. Split the bound shortcut keys combinationinto a single key array, and then collect the modifier keys into the modifier key array modifiers.
  2. Using keykey code) as the attribute name, save the currently bound shortcut keys and their corresponding callback functions to the callback function collection _callbacks.
  3. 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)

_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 keydownkeyupevents 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:

  1. event.keyAffected by shiftkeystrokes. For example, the bound shortcut key is shift+/actually the value in keydownthe 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
  2. The characters ( ) generated by some special character keys event.keyrequire special processing, such as the space key Space. The actual characters ( ) generated after pressing event.keyare ' '. For details, see the function in the code checkKeyMatch().
/**
 * 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.