Source: utils/key.js

import Keycodes from './keycodes';
import Keys from './keys';
import { TAB } from 'mobiledoc-kit/utils/characters';

/**
 * @typedef Direction
 * @enum {number}
 * @property {number} FORWARD
 * @property {number} BACKWARD
 */
export const DIRECTION = {
  FORWARD: 1,
  BACKWARD: -1
};
import assert from './assert';

export const MODIFIERS = {
  META: 1, // also called "command" on OS X
  CTRL: 2,
  SHIFT: 4,
  ALT: 8   // also called "option" on OS X
};

export function modifierMask(event) {
  let {
    metaKey, shiftKey, ctrlKey, altKey
  } = event;
  let modVal = (val, modifier) => {
    return (val && modifier) || 0;
  };
  return modVal(metaKey,  MODIFIERS.META) +
         modVal(shiftKey, MODIFIERS.SHIFT) +
         modVal(ctrlKey,  MODIFIERS.CTRL) +
         modVal(altKey,   MODIFIERS.ALT);
}

const SPECIAL_KEYS = {
  BACKSPACE: Keycodes.BACKSPACE,
  TAB:       Keycodes.TAB,
  ENTER:     Keycodes.ENTER,
  ESC:       Keycodes.ESC,
  SPACE:     Keycodes.SPACE,
  PAGEUP:    Keycodes.PAGEUP,
  PAGEDOWN:  Keycodes.PAGEDOWN,
  END:       Keycodes.END,
  HOME:      Keycodes.HOME,
  LEFT:      Keycodes.LEFT,
  UP:        Keycodes.UP,
  RIGHT:     Keycodes.RIGHT,
  DOWN:      Keycodes.DOWN,
  INS:       Keycodes.INS,
  DEL:       Keycodes.DELETE
};

export function specialCharacterToCode(specialCharacter) {
  return SPECIAL_KEYS[specialCharacter];
}

// heuristic for determining if `event` is a key event
function isKeyEvent(event) {
  return /^key/.test(event.type);
}

/**
 * An abstraction around a KeyEvent
 * that key listeners in the editor can use
 * to determine what sort of key was pressed
 */
const Key = class Key {
  constructor(event) {
    this.key = event.key;
    this.keyCode = event.keyCode;
    this.charCode = event.charCode;
    this.event = event;
    this.modifierMask = modifierMask(event);
  }

  static fromEvent(event) {
    assert('Must pass a Key event to Key.fromEvent',
           event && isKeyEvent(event));
    return new Key(event);
  }

  toString() {
    if (this.isTab()) { return TAB; }
    return String.fromCharCode(this.charCode);
  }

  // See https://caniuse.com/#feat=keyboardevent-key for browser support.
  isKeySupported() {
    return this.key;
  }

  isKey(identifier) {
    if (this.isKeySupported()) {
      assert(`Must define Keys.${identifier}.`, Keys[identifier]);
      return this.key === Keys[identifier];
    } else {
      assert(`Must define Keycodes.${identifier}.`, Keycodes[identifier]);
      return this.keyCode === Keycodes[identifier];
    }
  }

  isEscape() {
    return this.isKey('ESC');
  }

  isDelete() {
    return this.isKey('BACKSPACE') || this.isForwardDelete();
  }

  isForwardDelete() {
    return this.isKey('DELETE');
  }

  isArrow() {
    return this.isHorizontalArrow() || this.isVerticalArrow();
  }

  isHorizontalArrow() {
    return this.isLeftArrow() || this.isRightArrow();
  }

  isHorizontalArrowWithoutModifiersOtherThanShift() {
    return this.isHorizontalArrow() &&
      !(this.ctrlKey || this.metaKey || this.altKey);
  }

  isVerticalArrow() {
    return this.isKey('UP') || this.isKey('DOWN');
  }

  isLeftArrow() {
    return this.isKey('LEFT');
  }

  isRightArrow() {
    return this.isKey('RIGHT');
  }

  isHome() {
    return this.isKey('HOME');
  }

  isEnd() {
    return this.isKey('END');
  }

  isPageUp() {
    return this.isKey('PAGEUP');
  }

  isPageDown() {
    return this.isKey('PAGEDOWN');
  }

  isInsert() {
    return this.isKey('INS');
  }

  isClear() {
    return this.isKey('CLEAR');
  }

  isPause() {
    return this.isKey('PAUSE');
  }

  isSpace() {
    return this.isKey('SPACE');
  }

  // In Firefox, pressing ctrl-TAB will switch to another open browser tab, but
  // it will also fire a keydown event for the tab+modifier (ctrl). This causes
  // Mobiledoc to erroneously insert a tab character before FF switches to the
  // new browser tab.  Chrome doesn't fire this event so the issue doesn't
  // arise there. Fix this by returning false when the TAB key event includes a
  // modifier.
  // See: https://github.com/bustle/mobiledoc-kit/issues/565
  isTab() {
    return !this.hasAnyModifier() && this.isKey('TAB');
  }

  isEnter() {
    return this.isKey('ENTER');
  }

  /*
   * If the key is the actual shift key. This is false when the shift key
   * is held down and the source `event` is not the shift key.
   * @see {isShift}
   * @return {bool}
   */
  isShiftKey() {
    return this.isKey('SHIFT');
  }

  /*
   * If the key is the actual alt key (aka "option" on mac). This is false when the alt key
   * is held down and the source `event` is not the alt key.
   * @return {bool}
   */
  isAltKey() {
    return this.isKey('ALT');
  }

  /*
   * If the key is the actual ctrl key. This is false when the ctrl key
   * is held down and the source `event` is not the ctrl key.
   * @return {bool}
   */
  isCtrlKey() {
    return this.isKey('CTRL');
  }

  isIME() {
    // FIXME the IME action seems to get lost when we issue an
    // `editor.deleteSelection` before it (in Chrome)
    return this.keyCode === Keycodes.IME;
  }

  get direction() {
    switch (true) {
      case this.isDelete():
        return this.isForwardDelete() ? DIRECTION.FORWARD : DIRECTION.BACKWARD;
      case this.isHorizontalArrow():
        return this.isRightArrow() ? DIRECTION.FORWARD : DIRECTION.BACKWARD;
    }
  }

  /**
   * If the shift key is depressed.
   * For example, while holding down meta+shift, pressing the "v"
   * key would result in an event whose `Key` had `isShift()` with a truthy value,
   * because the shift key is down when pressing the "v".
   * @see {isShiftKey} which checks if the key is actually the shift key itself.
   * @return {bool}
   */
  isShift() {
    return this.shiftKey;
  }

  hasModifier(modifier) {
    return modifier & this.modifierMask;
  }

  hasAnyModifier() {
    return !!this.modifierMask;
  }

  get ctrlKey() {
    return MODIFIERS.CTRL & this.modifierMask;
  }

  get metaKey() {
    return MODIFIERS.META & this.modifierMask;
  }

  get shiftKey() {
    return MODIFIERS.SHIFT & this.modifierMask;
  }

  get altKey() {
    return MODIFIERS.ALT & this.modifierMask;
  }

  isPrintableKey() {
    return !(
      this.isArrow() ||
      this.isHome() || this.isEnd() ||
      this.isPageUp() || this.isPageDown() ||
      this.isInsert() || this.isClear() || this.isPause() ||
      this.isEscape()
    );
  }

  isNumberKey() {
    if (this.isKeySupported()) {
      return this.key >= '0' && this.key <= '9';
    } else {
      const code = this.keyCode;
      return (code >= Keycodes['0'] && code <= Keycodes['9']) ||
        (code >= Keycodes.NUMPAD_0 && code <= Keycodes.NUMPAD_9); // numpad keys
    }
  }

  isLetterKey() {
    if (this.isKeySupported()) {
      const key = this.key;
      return (key >= 'a' && key <= 'z') ||
        (key >= 'A' && key <= 'Z');
    } else {
      const code = this.keyCode;
      return (code >= Keycodes.A && code <= Keycodes.Z) ||
        (code >= Keycodes.a && code <= Keycodes.z);
    }
  }

  isPunctuation() {
    if (this.isKeySupported()) {
      const key = this.key;
      return (key >= ';' && key <= '`') ||
        (key >= '[' && key <= '"');
    } else {
      const code = this.keyCode;
      return (code >= Keycodes[';'] && code <= Keycodes['`']) ||
      (code >= Keycodes['['] && code <= Keycodes['"']);
    }
  }

  /**
   * See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#Printable_keys_in_standard_position
   *   and http://stackoverflow.com/a/12467610/137784
   */
  isPrintable() {
    if (this.ctrlKey || this.metaKey) {
      return false;
    }

    // Firefox calls keypress events for some keys that should not be printable
    if (!this.isPrintableKey()) {
      return false;
    }

    return (
      this.keyCode !== 0 ||
      this.toString().length > 0 ||
      this.isNumberKey() ||
      this.isSpace() ||
      this.isTab()   ||
      this.isEnter() ||
      this.isLetterKey() ||
      this.isPunctuation() ||
      this.isIME()
    );
  }
};

export default Key;