chromium/chrome/browser/resources/extensions/shortcut_util.ts

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assertNotReached} from 'chrome://resources/js/assert.js';
import {isChromeOS, isMac} from 'chrome://resources/js/platform.js';


export enum Key {
  COMMA = 188,
  DEL = 46,
  DOWN = 40,
  END = 35,
  ESCAPE = 27,
  HOME = 36,
  INS = 45,
  LEFT = 37,
  MEDIA_NEXT_TRACK = 176,
  MEDIA_PLAY_PAUSE = 179,
  MEDIA_PREV_TRACK = 177,
  MEDIA_STOP = 178,
  PAGE_DOWN = 34,
  PAGE_UP = 33,
  PERIOD = 190,
  RIGHT = 39,
  SPACE = 32,
  TAB = 9,
  UP = 38,
}

/**
 * Enum for whether we require modifiers of a keycode.
 */
enum ModifierPolicy {
  NOT_ALLOWED = 0,
  REQUIRED = 1
}

/**
 * Gets the ModifierPolicy. Currently only "MediaNextTrack", "MediaPrevTrack",
 * "MediaStop", "MediaPlayPause" are required to be used without any modifier.
 */
function getModifierPolicy(keyCode: number): ModifierPolicy {
  switch (keyCode) {
    case Key.MEDIA_NEXT_TRACK:
    case Key.MEDIA_PLAY_PAUSE:
    case Key.MEDIA_PREV_TRACK:
    case Key.MEDIA_STOP:
      return ModifierPolicy.NOT_ALLOWED;
    default:
      return ModifierPolicy.REQUIRED;
  }
}

/**
 * Returns whether the keyboard event has a key modifier, which could affect
 * how it's handled.
 * @param countShiftAsModifier Whether the 'Shift' key should be counted as
 *     modifier.
 * @return Whether the event has any modifiers.
 */
function hasModifier(e: KeyboardEvent, countShiftAsModifier: boolean): boolean {
  return e.ctrlKey || e.altKey ||
      // Meta key is only relevant on Mac and CrOS, where we treat Command
      // and Search (respectively) as modifiers.
      (isMac && e.metaKey) || (isChromeOS && e.metaKey) ||
      (countShiftAsModifier && e.shiftKey);
}

/**
 * Checks whether the passed in |keyCode| is a valid extension command key.
 * @return Whether the key is valid.
 */
export function isValidKeyCode(keyCode: number): boolean {
  if (keyCode === Key.ESCAPE) {
    return false;
  }
  for (const k in Key) {
    if (Key[k as keyof typeof Key] === keyCode) {
      return true;
    }
  }
  return (keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) ||
      (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0));
}

/**
 * Converts a keystroke event to string form, ignoring invalid extension
 * commands.
 */
export function keystrokeToString(e: KeyboardEvent): string {
  const output = [];
  // TODO(devlin): Should this be i18n'd?
  if (isMac && e.metaKey) {
    output.push('Command');
  }
  if (isChromeOS && e.metaKey) {
    output.push('Search');
  }
  if (e.ctrlKey) {
    output.push('Ctrl');
  }
  if (!e.ctrlKey && e.altKey) {
    output.push('Alt');
  }
  if (e.shiftKey) {
    output.push('Shift');
  }

  const keyCode = e.keyCode;
  if (isValidKeyCode(keyCode)) {
    if ((keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) ||
        (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0))) {
      output.push(String.fromCharCode(keyCode));
    } else {
      switch (keyCode) {
        case Key.COMMA:
          output.push('Comma');
          break;
        case Key.DEL:
          output.push('Delete');
          break;
        case Key.DOWN:
          output.push('Down');
          break;
        case Key.END:
          output.push('End');
          break;
        case Key.HOME:
          output.push('Home');
          break;
        case Key.INS:
          output.push('Insert');
          break;
        case Key.LEFT:
          output.push('Left');
          break;
        case Key.MEDIA_NEXT_TRACK:
          output.push('MediaNextTrack');
          break;
        case Key.MEDIA_PLAY_PAUSE:
          output.push('MediaPlayPause');
          break;
        case Key.MEDIA_PREV_TRACK:
          output.push('MediaPrevTrack');
          break;
        case Key.MEDIA_STOP:
          output.push('MediaStop');
          break;
        case Key.PAGE_DOWN:
          output.push('PageDown');
          break;
        case Key.PAGE_UP:
          output.push('PageUp');
          break;
        case Key.PERIOD:
          output.push('Period');
          break;
        case Key.RIGHT:
          output.push('Right');
          break;
        case Key.SPACE:
          output.push('Space');
          break;
        case Key.TAB:
          output.push('Tab');
          break;
        case Key.UP:
          output.push('Up');
          break;
      }
    }
  }

  return output.join('+');
}

/**
 * Returns true if the event has valid modifiers.
 * @param e The keyboard event to consider.
 * @return Wether the event is valid.
 */
export function hasValidModifiers(e: KeyboardEvent): boolean {
  switch (getModifierPolicy(e.keyCode)) {
    case ModifierPolicy.REQUIRED:
      return hasModifier(e, false);
    case ModifierPolicy.NOT_ALLOWED:
      return !hasModifier(e, true);
    default:
      assertNotReached();
  }
}