UNPKG

@waradu/keyboard

Version:

A keyboard manager compatible with JavaScript, TypeScript and Nuxt

278 lines (274 loc) 7.8 kB
import { detectOsInBrowser } from './helper.mjs'; import { ANYKEY, platforms, SEPARATOR, keys } from './keys.mjs'; const readableMap = { // Special $any: "Any", $num: "Any Number", // Navigation "page-up": "Page Up", "page-down": "Page Down", "arrow-left": "Arrow Left", "arrow-up": "Arrow Up", "arrow-right": "Arrow Right", "arrow-down": "Arrow Down", "print-screen": "Print Screen", "context-menu": "Context Menu", // Lock keys "num-lock": "Num Lock", "scroll-lock": "Scroll Lock", // Media / audio "audio-volume-mute": "Volume Mute", "audio-volume-down": "Volume Down", "audio-volume-up": "Volume Up", "media-track-next": "Next Track", "media-track-previous": "Previous Track", "media-play-pause": "Play/Pause", "media-play": "Play", "media-pause": "Pause", "media-stop": "Stop", // Symbols / punctuation minus: "-", equal: "=", plus: "+", comma: ",", period: ".", star: "*", hash: "#", percent: "%", ampersand: "&", "single-quote": "'", "double-quote": '"', slash: "/", backslash: "\\", "angle-brackets-open": "<", "angle-brackets-close": ">", "square-brackets-open": "[", "square-brackets-close": "]", "curly-brackets-open": "{", "curly-brackets-close": "}", backquote: "`", tilde: "~", dollar: "$", "question-mark": "?", "exclamation-mark": "!" }; class Keybind { key; modifiers; platform; constructor(shape) { this.key = shape.key; this.modifiers = { ...shape.modifiers }; this.platform = shape.platform; } /** * Format this keybind as a canonical key string. * * @returns A valid `KeyString`. */ toString() { const parts = []; if (this.modifiers.meta) parts.push("meta"); if (this.modifiers.ctrl) parts.push("ctrl"); if (this.modifiers.ctrlCmd) parts.push("ctrl-cmd"); if (this.modifiers.alt) parts.push("alt"); if (this.modifiers.shift) parts.push("shift"); parts.push(this.key); const sequence = parts.join("+"); return this.platform ? `${this.platform}:${sequence}` : sequence; } /** * Format the main key for display. * * @returns A human-readable key label. */ toReadableKey() { const name = readableMap[this.key]; if (!name) { return this.key.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("-"); } return name; } /** * Format the platform constraint for display. * * @returns A human-readable platform label. */ toReadablePlatform() { switch (this.platform) { case "linux": return "Linux"; case "macos": return "macOS"; case "win": return "Windows"; case "no-linux": return "macOS & Windows"; case "no-macos": return "Linux & Windows"; case "no-win": return "Linux & macOS"; default: return "All Platforms"; } } /** * Format this keybind as display parts. * * The returned parts can be joined into a string or rendered as separate keycaps. * * @returns Human-readable modifier and key labels. */ toReadable() { const parts = []; if (this.modifiers.meta) parts.push("Meta"); if (this.modifiers.ctrl) parts.push("Ctrl"); if (this.modifiers.ctrlCmd) parts.push("Ctrl-Cmd"); if (this.modifiers.alt) parts.push("Alt"); if (this.modifiers.shift) parts.push("Shift"); parts.push(this.toReadableKey()); return parts; } /** * Format this keybind as display parts for a platform. * * When no platform is provided, the browser platform is detected. Passing a platform * is recommended for SSR and tests. * * @param config Optional platform override. * @returns Human-readable modifier and key labels. */ toLocalReadable(config = {}) { const parts = []; if (this.modifiers.meta) parts.push("Meta"); if (this.modifiers.ctrl) parts.push("Ctrl"); if (this.modifiers.ctrlCmd) { const browserPlatform = config.platform ?? detectOsInBrowser(); if (browserPlatform === "macos") parts.push("Cmd"); else parts.push("Ctrl"); } if (this.modifiers.alt) parts.push("Alt"); if (this.modifiers.shift) parts.push("Shift"); parts.push(this.toReadableKey()); return parts; } /** * Convert this keybind into a serializable object shape. * * @returns A `KeybindShape`. */ toShape() { return { key: this.key, modifiers: { ...this.modifiers }, platform: this.platform }; } /** * Compare two keybind inputs. * * @param a First keybind input. * @param b Second keybind input. * @returns Whether both inputs resolve to the same canonical key string. */ static equals(a, b) { const kbdA = Keybind.from(a); const kbdB = Keybind.from(b); return !!kbdA && !!kbdB && kbdA.toString() === kbdB.toString(); } /** * Compare this keybind with another keybind input. * * @param other Keybind input to compare against. * @returns Whether both inputs resolve to the same canonical key string. */ equals = (other) => Keybind.equals(this, other); /** * Parse a key string into a keybind. * * @param string Key string to parse. * @returns A `Keybind`, or `undefined` when the string is invalid. */ static fromString(string) { if (string === ANYKEY) { return Keybind.fromShape({ key: ANYKEY }); } let platformLabel; let keySequence = string; if (string.includes(":")) { const [platform, seq, ...rest] = string.split(":"); if (rest.length) return; if (!Object.values(platforms).includes(platform)) return; platformLabel = platform; keySequence = seq; } const parts = keySequence.split(SEPARATOR); if (parts.length === 0) return; const key = parts.pop(); const validKeyValues = new Set(Object.values(keys)); if (!validKeyValues.has(key)) return; const MOD_ORDER = ["meta", "ctrl", "ctrl-cmd", "alt", "shift"]; const modSet = new Set(MOD_ORDER); const modifiersOnly = parts; let lastIndex = -1; for (const mod of modifiersOnly) { if (!modSet.has(mod)) return; const idx = MOD_ORDER.indexOf(mod); if (idx === -1 || idx <= lastIndex) return; lastIndex = idx; } if (modifiersOnly.includes("ctrl-cmd")) { if (modifiersOnly.includes("meta") || modifiersOnly.includes("ctrl")) return; } const modifierMap = { alt: false, ctrl: false, ctrlCmd: false, meta: false, shift: false }; for (const mod of modifiersOnly) { if (mod === "ctrl-cmd") modifierMap.ctrlCmd = true; else modifierMap[mod] = true; } return new Keybind({ platform: platformLabel, modifiers: modifierMap, key }); } /** * Create a keybind from a plain object shape. * * Missing modifiers default to `false`. * * @param shape Keybind shape to normalize. * @returns A `Keybind`. */ static fromShape(shape) { const ctrlCmd = shape.modifiers?.ctrlCmd ?? false; return new Keybind({ key: shape.key, modifiers: { alt: shape.modifiers?.alt ?? false, ctrl: Boolean(!ctrlCmd && shape.modifiers?.ctrl), ctrlCmd, meta: Boolean(!ctrlCmd && shape.modifiers?.meta), shift: shape.modifiers?.shift ?? false }, platform: shape.platform }); } /** * Normalize any supported keybind input. * * @param keybind Key string, keybind shape, or existing `Keybind`. * @returns A `Keybind`, or `undefined` when the input is invalid. */ static from(keybind) { return keybind instanceof Keybind ? keybind : typeof keybind === "string" ? Keybind.fromString(keybind) : Keybind.fromShape(keybind); } } export { Keybind };