Handling keystrokes aka shortcuts and debugging

Hello! It will be about hotkeys in WEBAPI + JavaScript, we will consider their ways of organizing and the problems that arise primarily in large applications.

Consider how to handle keys on a specific task.


Imagine that in an existing project you need to implement keyboard input processing. At the same time, the project interface and its controllers, for purely historical reasons, such as they are. And they are the following:

ParentControllerin which there are two components with their own states and state. Controller1and an element that uses CTRL+SHIFT+Fto search the site, and Controller2with its DOM element, which is a local area, in the presence of which the search is performed inside it. At the same time they can be on the screen at the same time. Below are several ways to solve this problem.

1. “ KeyboardEvent and its manual processing”

KeyboardEvent objects describe the keyboard user experience. Each event describes a key; The event type (keydown, keypress, or keyup) determines the type of action produced.

Sounds great doesn't it? Let's take a closer look.
Consider keystroke capture CTRL+SHIFT+F, usually corresponding to a global search call.

element.addEventListener('keypress', (event) => {
  const keyName = event.key;
  // Приведение к нижнему регистру имени клавиши обязательно
  // т.к. при нажатии вместе с SHIFT оно будет в верхнем регистре
  if (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 't') {
      alert('CTRL+SHIFT+T pressed');

Now, we can apply to our problem in two ways (for example)

Perform key interception in controllers 1 and 2 separately

This will lead to the fact that, depending on the order in the DOM, you may need useCaptureto ensure the order of processing Controller2 and then Controller1. So you get isolated logic, but if the application is complex and there are many such controllers, this solution is not good. some may be on the screen at the same time and they may have their own strict processing order, which does not depend on their position in the DOM tree. (see bubbling and capturing )

Perform key interception in CommonController

An alternative solution may be to handle clicks in a common parent controller, which knows exactly when to show its children, controlled by the first and second controllers. This, while increasing the child controllers, will not cause difficulties in catching events and making decisions on which controller to handle the keys. However, there will be another problem - a thick one appears in the parent controller if, which handles all possible cases. For large applications, this solution is not suitable, because at some point another one may appear Controllerthat is not a child; ParentControllerthen you have to move the handler to a higher level, to their common parent, and so on ... Until one of the controllers starts to know too much about the elements inside it.

In fact, only 80% of browsers can work with KeboardEvent.key, in all others you will need to operate KeboardEvent.keyCode: Numberkey codes. Which greatly complicates life. Here it is worth going to the description of the disadvantages of this approach.


  • The organization of the code is not entirely convenient, a map of character codes and their text equivalent and other utilities reducing the amount of code in handlers are required.
  • 80% Browser support for working with characters without using their codes is still not enough.
  • Overlapping with the help of useCapturesome other handlers.
  • With interceptions with useCapturenested elements with the same handlers,
    debugging is difficult.
  • Poor scalability.

But natively, there are no unnecessary dependencies and libraries.

Then we will discuss two libraries, one of which was designed to solve their own similar problems.

2. “Using the HotKeys library ”

Three thousand stars on a githaba, modest size and no dependencies. The Chinese manufacturer promises us a solution that will suit everyone. However, we will not hurry. Let's try to solve our problem with its help.

// обработчик клавиш
hotkeys('ctrl+shift+f', function(event, handler){
  alert('CTRL+SHIFT+T pressed');

The syntax is already much shorter, and the main chip for solving the problem will be the direct display of the components of controllers 1 and 2 on the screen. A little digging through the library code makes it easy to see that handlers form a stack that fills or clears as they are registered on the screen (Assume an element with a handler that appeared later than an existing one will have priority in the hot key processing queue).

Often it happens so that the element that should intercept processing appears later. In this case, we can safely spread the logic of handling handles to each of the controllers. And other type of chips, help us to separate one stream of clicks from another. But in the case when порядок появления на экране ≠ приоритету обработки нажатий- the same problems arise as with the native eventListener's. We'll have to make everything in a common parent controller.

In addition, it often happens that you need to block the default behavior, but the event is not considered processed (in other words, there is no unambiguous understanding of whether the event is processed or not, if we received it) or must be processed by two controllers simultaneously. One of which will cause a reaction to the behavior, and the other will just take into account what the event was.

Total advantages:

  • Scope allows to separate streams.
  • The syntax is clear and short.
  • The order determines the appearance of the element, not the position in the DOM.
  • Size and no dependencies.


  • You can process only one scoop at a time.
  • Debugging is still difficult because of the function calls in the loop, it may not be known on which handlergot lost the event was processed
  • The statement that the event is processed if it has the defaultPrevented flag and its distribution is aborted - not true.
  • Global functions of calling registration and unsubscribing from events

Suitable for solving typical tasks, but with the writing of a trading terminal or a large admin panel there will be problems with debugging for sure.

3. “Using stack-shortcuts library ”

As a result of many rakes and attempts to use other people's decisions, I had to make my own bicycle A library that helps to debug first of all will retain all the best properties of the popular ones and contribute something new.

What tasks were solved during the creation?

  • Reactive working principle
  • Simple debugging handlers
  • Unambiguous processing status of the event
  • Cross Platform
  • Convenience of import and lack of global functions
  • No direct access to the window when connecting
  • No need to call preventDefaultorstopPropagation

// подписка
this.shortcuts = shortcuts({
    'CMD+SHIFT+F': function (event, next) {
      alert('CMD+SHIFT+F pressed');
// утилизация

Applicable to our problem, the solution completely coincides with the previous library. There is still no complete separation of processing logic without undue knowledge of each other, but much has become simpler and clearer. Thanks to the following:

  • The binding to the DOM is still absent (with the exception of one listener) and the stack of handlers is filled depending on the order of their registration.
  • scopeThey immediately refused to use the insulation. it is not clear what tasks it solves and it seems that it only complicates the architecture.
  • Debugging and the next function is probably worth more.
  • Mutations in the data events that it carries in event.detail

Debugging Handlers are arranged in such a way that before a call is formed callstackfrom them. It allows you to see in the console the entire chain of events passing from the first handler to the next.

next () - The function call means that the event has not been processed and will be passed to the next handler. A fairly familiar contract that applies to intermediate processors or middlewares in express. So you will always know whether the event is processed or simply mutated or "taken into account".

This is how the call stack looks like if you put a breakpoint in one of them.

Well, about the cons:

  • There are no taipings for TypeScript
  • No skoupov - splitskrin site not to do)
  • One combination at registration (no CMD+F,CMD+V,Tcomma will understand this)

Also popular now: