Files
hoppscotch/helpers/keybindings.ts
2021-08-27 09:37:29 +05:30

172 lines
5.0 KiB
TypeScript

import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api"
import { HoppAction, invokeAction } from "./actions"
import { isAppleDevice } from "./platformutils"
import { isDOMElement, isTypableElement } from "./utils/dom"
/**
* This variable keeps track whether keybindings are being accepted
* true -> Keybindings are checked
* false -> Key presses are ignored (Keybindings are not checked)
*/
let keybindingsEnabled = true
/**
* Alt is also regarded as macOS OPTION (⌥) key
* Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!)
*/
type ModifierKeys = "ctrl" | "alt" | "ctrl-shift" | "alt-shift"
/* eslint-disable prettier/prettier */
// prettier-ignore
type Key =
| "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j"
| "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t"
| "u" | "v" | "w" | "x" | "y" | "z" | "0" | "1" | "2" | "3"
| "4" | "5" | "6" | "7" | "8" | "9" | "up" | "down" | "left"
| "right" | "/" | "?"
/* eslint-enable */
type ModifierBasedShortcutKey = `${ModifierKeys}-${Key}`
// Singular keybindings (these will be disabled when an input-ish area has been focused)
type SingleCharacterShortcutKey = `${Key}`
type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey
export const bindings: {
// eslint-disable-next-line no-unused-vars
[_ in ShortcutKey]?: HoppAction
} = {
"ctrl-g": "request.send-cancel",
"ctrl-i": "request.reset",
"ctrl-u": "request.copy-link",
"ctrl-s": "request.save",
"ctrl-shift-s": "request.save-as",
"alt-up": "request.method.next",
"alt-down": "request.method.prev",
"alt-g": "request.method.get",
"alt-h": "request.method.head",
"alt-p": "request.method.post",
"alt-u": "request.method.put",
"alt-x": "request.method.delete",
"ctrl-k": "flyouts.keybinds.toggle",
"/": "modals.search.toggle",
"?": "modals.support.toggle",
"ctrl-m": "modals.share.toggle",
"alt-r": "navigation.jump.rest",
"alt-q": "navigation.jump.graphql",
"alt-w": "navigation.jump.realtime",
"alt-d": "navigation.jump.documentation",
"alt-s": "navigation.jump.settings",
"ctrl-left": "navigation.jump.back",
"ctrl-right": "navigation.jump.forward",
}
/**
* A composable that hooks to the caller component's
* lifecycle and hooks to the keyboard events to fire
* the appropriate actions based on keybindings
*/
export function hookKeybindingsListener() {
onMounted(() => {
document.addEventListener("keydown", handleKeyDown)
})
onBeforeUnmount(() => {
document.removeEventListener("keydown", handleKeyDown)
})
}
function handleKeyDown(ev: KeyboardEvent) {
// Do not check keybinds if the mode is disabled
if (!keybindingsEnabled) return
const binding = generateKeybindingString(ev)
if (!binding) return
const boundAction = bindings[binding]
if (!boundAction) return
ev.preventDefault()
invokeAction(boundAction)
}
function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
// All our keybinds need to have one modifier pressed atleast
const modifierKey = getActiveModifier(ev)
const target = ev.target
if (!modifierKey && !(isDOMElement(target) && isTypableElement(target))) {
// Check if we are having singulars instead
const key = getPressedKey(ev)
if (!key) return null
else return `${key}` as ShortcutKey
}
const key = getPressedKey(ev)
if (!key) return null
return `${modifierKey}-${key}` as ShortcutKey
}
function getPressedKey(ev: KeyboardEvent): Key | null {
const val = ev.key.toLowerCase()
// Check arrow keys
if (val === "arrowup") return "up"
else if (val === "arrowdown") return "down"
else if (val === "arrowleft") return "left"
else if (val === "arrowright") return "right"
// Check letter keys
if (val.length === 1 && val.toUpperCase() !== val.toLowerCase())
return val as Key
// Check if number keys
if (val.length === 1 && !isNaN(val as any)) return val as Key
// Check if question mark
if (val === "?") return "?"
// Check if question mark
if (val === "/") return "/"
// If no other cases match, this is not a valid key
return null
}
function getActiveModifier(ev: KeyboardEvent): ModifierKeys | null {
const isShiftKey = ev.shiftKey
// We only allow one modifier key to be pressed (for now)
// Control key (+ Command) gets priority and if Alt is also pressed, it is ignored
if (isAppleDevice() && ev.metaKey) return isShiftKey ? "ctrl-shift" : "ctrl"
else if (!isAppleDevice() && ev.ctrlKey)
return isShiftKey ? "ctrl-shift" : "ctrl"
// Test for Alt key
if (ev.altKey) return isShiftKey ? "alt-shift" : "alt"
return null
}
/**
* This composable allows for the UI component to be disabled if the component in question is mounted
*/
export function useKeybindingDisabler() {
// TODO: Move to a lock based system that keeps the bindings disabled until all locks are lifted
const disableKeybindings = () => {
keybindingsEnabled = false
}
const enableKeybindings = () => {
keybindingsEnabled = true
}
return {
disableKeybindings,
enableKeybindings,
}
}