@portabletext/editor
Version:
Portable Text Editor made in React
210 lines (186 loc) • 4.52 kB
text/typescript
export interface KeyboardEventLike {
key: string
keyCode?: number
altKey: boolean
ctrlKey: boolean
metaKey: boolean
shiftKey: boolean
}
interface HotKey {
keyCode?: number | undefined
key?: string | undefined
altKey: boolean | null
ctrlKey: boolean | null
metaKey: boolean | null
shiftKey: boolean | null
}
export const IS_MAC =
typeof window !== 'undefined' &&
/Mac|iPod|iPhone|iPad/.test(window.navigator.userAgent)
type Modifier = 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey'
const modifiers: Record<string, Modifier | undefined> = {
alt: 'altKey',
control: 'ctrlKey',
meta: 'metaKey',
shift: 'shiftKey',
}
const aliases: Record<string, string | undefined> = {
add: '+',
break: 'pause',
cmd: 'meta',
command: 'meta',
ctl: 'control',
ctrl: 'control',
del: 'delete',
down: 'arrowdown',
esc: 'escape',
ins: 'insert',
left: 'arrowleft',
mod: IS_MAC ? 'meta' : 'control',
opt: 'alt',
option: 'alt',
return: 'enter',
right: 'arrowright',
space: ' ',
spacebar: ' ',
up: 'arrowup',
win: 'meta',
windows: 'meta',
}
const keyCodes: Record<string, number | undefined> = {
'backspace': 8,
'tab': 9,
'enter': 13,
'shift': 16,
'control': 17,
'alt': 18,
'pause': 19,
'capslock': 20,
'escape': 27,
' ': 32,
'pageup': 33,
'pagedown': 34,
'end': 35,
'home': 36,
'arrowleft': 37,
'arrowup': 38,
'arrowright': 39,
'arrowdown': 40,
'insert': 45,
'delete': 46,
'meta': 91,
'numlock': 144,
'scrolllock': 145,
';': 186,
'=': 187,
',': 188,
'-': 189,
'.': 190,
'/': 191,
'`': 192,
'[': 219,
'\\': 220,
']': 221,
"'": 222,
'f1': 112,
'f2': 113,
'f3': 114,
'f4': 115,
'f5': 116,
'f6': 117,
'f7': 118,
'f8': 119,
'f9': 120,
'f10': 121,
'f11': 122,
'f12': 123,
'f13': 124,
'f14': 125,
'f15': 126,
'f16': 127,
'f17': 128,
'f18': 129,
'f19': 130,
'f20': 131,
}
export function isHotkey(hotkey: string, event: KeyboardEventLike): boolean {
return compareHotkey(parseHotkey(hotkey), event)
}
function parseHotkey(hotkey: string): HotKey {
// Ensure that all the modifiers are set to false unless the hotkey has them.
const parsedHotkey: HotKey = {
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
}
// Special case to handle the `+` key since we use it as a separator.
const hotkeySegments = hotkey.replace('++', '+add').split('+')
for (const rawHotkeySegment of hotkeySegments) {
const optional =
rawHotkeySegment.endsWith('?') && rawHotkeySegment.length > 1
const hotkeySegment = optional
? rawHotkeySegment.slice(0, -1)
: rawHotkeySegment
const keyName = toKeyName(hotkeySegment)
const modifier = modifiers[keyName]
const alias = aliases[hotkeySegment]
const code = keyCodes[keyName]
if (
hotkeySegment.length > 1 &&
modifier === undefined &&
alias === undefined &&
code === undefined
) {
throw new TypeError(`Unknown modifier: "${hotkeySegment}"`)
}
if (hotkeySegments.length === 1 || modifier === undefined) {
parsedHotkey.key = keyName
parsedHotkey.keyCode = toKeyCode(hotkeySegment)
}
if (modifier !== undefined) {
parsedHotkey[modifier] = optional ? null : true
}
}
return parsedHotkey
}
function compareHotkey(
parsedHotkey: HotKey,
event: KeyboardEventLike,
): boolean {
const matchingModifiers =
(parsedHotkey.altKey != null
? parsedHotkey.altKey === event.altKey
: true) &&
(parsedHotkey.ctrlKey != null
? parsedHotkey.ctrlKey === event.ctrlKey
: true) &&
(parsedHotkey.metaKey != null
? parsedHotkey.metaKey === event.metaKey
: true) &&
(parsedHotkey.shiftKey != null
? parsedHotkey.shiftKey === event.shiftKey
: true)
if (!matchingModifiers) {
return false
}
if (parsedHotkey.keyCode !== undefined && event.keyCode !== undefined) {
if (parsedHotkey.keyCode === 91 && event.keyCode === 93) {
return true
}
return parsedHotkey.keyCode === event.keyCode
}
return (
parsedHotkey.keyCode === event.keyCode ||
parsedHotkey.key === event.key.toLowerCase()
)
}
function toKeyCode(name: string): number {
const keyName = toKeyName(name)
const keyCode = keyCodes[keyName] ?? keyName.toUpperCase().charCodeAt(0)
return keyCode
}
function toKeyName(name: string): string {
const keyName = name.toLowerCase()
return aliases[keyName] ?? keyName
}