简体   繁体   中英

Keyboard shortcuts / commands on non-Latin keyboards (JavaScript)

I'd like to make keyboard shortcuts work on as many keyboard layouts as possible, including non-Latin ones.

I understand that as far as Latin keyboards go, it's best to use KeyboardEvent.key property - so it's known that user has pressed a key that represents letter "L" for instance, regardless of the keyboard layout. This seems consistent with how OSes and other applications do it, as I just tested it by temporarily switching to Dvorak layout.

I'm pretty sure that this approach will not work with non-Latin keyboards, ie - Cyrillic.

What I'm looking for is a general way to handle other alphabets without necessarily diving deep into localization for each language.

So for instance, if I want to have an action for Ctrl+L (or Cmd+L for Macs), I'd like it to work on as many keyboard layouts as possible, even if those layouts don't have the letter L. Sort of get the character on that keyboard layout that would be equivalent to L.

I also want to respect the most basic OS commands: Ctrl+C, Ctrl+A, Ctrl+V, Ctrl+X - so I'm curious if operating systems do it the same way, ie on a Cyrillic keyboard, does paste action occur as Ctrl + (equivalent of V in Cyrillic) or does it depend on the locale?

It turns out that most non-Latin keyboards have 2 alphabets printed , Latin and its own - and the user can switch between layouts.

So if the user is in Latin layout mode, KeyboardEvent.key should work out of the box. The problem is that while the user is in non-Latin mode, the keyboard commands would not work, because KeyboardEvent.key will be a letter from that alphabet.

Fortunately, most of these keyboards follow the standard Qwerty layout, so KeyboardEvent.code can work as a fallback to reference Latin characters.

I created a function that given a KeyboardEvent.key and KeyboardEvent.code should fallback to absolute Qwerty code values if a non-Latin character is detected.

License: MIT

/**
 * Gets key associated with a Keyboard event with a fallback to use absolute code value for
 * non-Latin keyboard layouts.
 *
 * Most commonly non-Latin keyboards have 2 sets of alphabets printed and 2 modes to switch between
 * them. The Latin mode usually follows the standard Qwerty layout so by falling back to use key
 * codes, a keyboard command can work even though the layout is in non-Latin mode.
 *
 * Limitations:
 * - some non-Latin layouts have a symbol on KeyQ which makes it impossible to distinguish it
 * (without checking the entire layout) from Latin Dvorak layout, therefore KeyQ will not work for
 * those
 * - if the Latin layout mode is not Qwerty some of the mappings will not be correct
 *
 * @returns if `key` is a non-Latin letter (unicode >= 880) and `code` represents a letter or a
 * digit on a Qwerty layout, it will return the corresponding letter (uppercase) or digit on a
 * Qwerty layout. Otherwise it will return `key` (transformed to uppercase if it's a letter).
 *
 * License: MIT; Copyright 2021 Maciej Krawczyk
 */
function getLatinKey(key, code) {
  if (key.length !== 1) {
    return key;
  }

  const capitalHetaCode = 880;
  const isNonLatin = key.charCodeAt(0) >= capitalHetaCode;

  if (isNonLatin) {
    if (code.indexOf('Key') === 0 && code.length === 4) { // i.e. 'KeyW'
      return code.charAt(3);
    }

    if (code.indexOf('Digit') === 0 && code.length === 6) { // i.e. 'Digit7'
      return code.charAt(5);
    }
  }

  return key.toUpperCase();
}

Sample usage:

document.addEventListener('keydown', (e) => {
  if (e.ctrlKey && getLatinKey(e.key, e.code) === 'L') {
    alert('Ctrl+L');
  }
});

Test:

  describe('getLatinKey', () => {
    it('gets a Latin letter', () => {
      expect(getLatinKey('A', 'irrelevant')).toBe('A');
    });

    it('gets a digit', () => {
      expect(getLatinKey('0', 'irrelevant')).toBe('0');
    });

    it('transforms letters to uppercase', () => {
      expect(getLatinKey('a', 'irrelevant')).toBe('A');
    });

    it('converts non-Latin letters to code values if available', () => {
      expect(getLatinKey('β', 'KeyB')).toBe('B');
      expect(getLatinKey('я', 'KeyZ')).toBe('Z');

      // Not a real-world example, but it tests getting digits.
      expect(getLatinKey('я', 'Digit1')).toBe('1');
    });

    it('does not convert non-Latin letters on special keys', () => {
      expect(getLatinKey('ё', 'Backquote')).toBe('Ё');
    });

    it('does not convert Latin diacritics', () => {
      expect(getLatinKey('ś', 'KeyS')).toBe('Ś');
      expect(getLatinKey('ü', 'KeyU')).toBe('Ü');
      expect(getLatinKey('ž', 'KeyZ')).toBe('Ž');
    });
  });


Alternatives:

  1. It would be possible to use Keyboard.getLayoutMap() to determine if the layout is non-Latin, which can make KeyQ work as well. However, it's an experimental API and is not supported by Firefox and Safari (and it may never be, as Firefox currently rejected it on privacy terms - fingerprinting). It also would come as a drawback because that API is not synchronous and it would not be possible to call e.preventDefault() on the keyboard event if needed.
  2. In an Electron app, keyboard-layout npm module could allow for a more advanced implementation which does not suffer from the same limitations.

Other things

As for OS shortcuts for copy, paste, undo it seems to be where you'd expect it (at least this is the impression I got after switching keyboard layout on Mac OS).

As for some general tips, avoid using symbols for keyboard shortcuts. They are all over the place, even within Latin Qwerty layouts.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM