@tanstack/hotkeys 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_event-target.js +1 -2
- package/dist/_keyboard-event.js +1 -2
- package/dist/_match.js +1 -2
- package/dist/_named-keys.d.ts +3 -5
- package/dist/_named-keys.js +1 -2
- package/dist/_recorder-chord.js +1 -2
- package/dist/_recording-guard.js +1 -2
- package/dist/_registration.js +1 -2
- package/dist/conflicts.d.ts +4 -7
- package/dist/conflicts.js +1 -2
- package/dist/constants.d.ts +14 -17
- package/dist/constants.js +8 -9
- package/dist/display-labels.d.ts +7 -10
- package/dist/display-labels.js +1 -2
- package/dist/format.d.ts +9 -12
- package/dist/format.js +9 -7
- package/dist/hint.d.ts +3 -6
- package/dist/hint.js +1 -2
- package/dist/hotkey-manager.d.ts +8 -10
- package/dist/hotkey-manager.js +21 -3
- package/dist/hotkey-recorder.d.ts +4 -7
- package/dist/hotkey-recorder.js +9 -6
- package/dist/hotkey-sequence-recorder.d.ts +5 -8
- package/dist/hotkey-sequence-recorder.js +6 -7
- package/dist/hotkey.types.d.ts +15 -18
- package/dist/key-state-tracker.d.ts +4 -7
- package/dist/key-state-tracker.js +6 -3
- package/dist/key.types.d.ts +17 -20
- package/dist/match.d.ts +8 -11
- package/dist/match.js +1 -2
- package/dist/parse.d.ts +11 -14
- package/dist/parse.js +5 -4
- package/dist/platform.d.ts +3 -6
- package/dist/platform.js +1 -2
- package/dist/recorder-options.d.ts +6 -9
- package/dist/sequence-manager.d.ts +11 -14
- package/dist/sequence-manager.js +12 -7
- package/dist/validate.d.ts +5 -8
- package/dist/validate.js +1 -2
- package/package.json +8 -13
- package/dist/_event-target.cjs +0 -73
- package/dist/_event-target.cjs.map +0 -1
- package/dist/_event-target.js.map +0 -1
- package/dist/_keyboard-event.cjs +0 -83
- package/dist/_keyboard-event.cjs.map +0 -1
- package/dist/_keyboard-event.js.map +0 -1
- package/dist/_match.cjs +0 -49
- package/dist/_match.cjs.map +0 -1
- package/dist/_match.js.map +0 -1
- package/dist/_named-keys.cjs +0 -68
- package/dist/_named-keys.cjs.map +0 -1
- package/dist/_named-keys.d.cts +0 -8
- package/dist/_named-keys.js.map +0 -1
- package/dist/_recorder-chord.cjs +0 -44
- package/dist/_recorder-chord.cjs.map +0 -1
- package/dist/_recorder-chord.js.map +0 -1
- package/dist/_recording-guard.cjs +0 -59
- package/dist/_recording-guard.cjs.map +0 -1
- package/dist/_recording-guard.js.map +0 -1
- package/dist/_registration.cjs +0 -54
- package/dist/_registration.cjs.map +0 -1
- package/dist/_registration.js.map +0 -1
- package/dist/conflicts.cjs +0 -55
- package/dist/conflicts.cjs.map +0 -1
- package/dist/conflicts.d.cts +0 -39
- package/dist/conflicts.js.map +0 -1
- package/dist/constants.cjs +0 -410
- package/dist/constants.cjs.map +0 -1
- package/dist/constants.d.cts +0 -165
- package/dist/constants.js.map +0 -1
- package/dist/display-labels.cjs +0 -119
- package/dist/display-labels.cjs.map +0 -1
- package/dist/display-labels.d.cts +0 -93
- package/dist/display-labels.js.map +0 -1
- package/dist/format.cjs +0 -85
- package/dist/format.cjs.map +0 -1
- package/dist/format.d.cts +0 -99
- package/dist/format.js.map +0 -1
- package/dist/hint.cjs +0 -24
- package/dist/hint.cjs.map +0 -1
- package/dist/hint.d.cts +0 -23
- package/dist/hint.js.map +0 -1
- package/dist/hotkey-manager.cjs +0 -414
- package/dist/hotkey-manager.cjs.map +0 -1
- package/dist/hotkey-manager.d.cts +0 -234
- package/dist/hotkey-manager.js.map +0 -1
- package/dist/hotkey-recorder.cjs +0 -235
- package/dist/hotkey-recorder.cjs.map +0 -1
- package/dist/hotkey-recorder.d.cts +0 -119
- package/dist/hotkey-recorder.js.map +0 -1
- package/dist/hotkey-sequence-recorder.cjs +0 -255
- package/dist/hotkey-sequence-recorder.cjs.map +0 -1
- package/dist/hotkey-sequence-recorder.d.cts +0 -86
- package/dist/hotkey-sequence-recorder.js.map +0 -1
- package/dist/hotkey.types.d.cts +0 -236
- package/dist/index.cjs +0 -71
- package/dist/index.d.cts +0 -18
- package/dist/key-state-tracker.cjs +0 -197
- package/dist/key-state-tracker.cjs.map +0 -1
- package/dist/key-state-tracker.d.cts +0 -107
- package/dist/key-state-tracker.js.map +0 -1
- package/dist/key.types.d.cts +0 -82
- package/dist/match.cjs +0 -144
- package/dist/match.cjs.map +0 -1
- package/dist/match.d.cts +0 -101
- package/dist/match.js.map +0 -1
- package/dist/parse.cjs +0 -245
- package/dist/parse.cjs.map +0 -1
- package/dist/parse.d.cts +0 -134
- package/dist/parse.js.map +0 -1
- package/dist/platform.cjs +0 -55
- package/dist/platform.cjs.map +0 -1
- package/dist/platform.d.cts +0 -44
- package/dist/platform.js.map +0 -1
- package/dist/recorder-options.d.cts +0 -34
- package/dist/sequence-manager.cjs +0 -441
- package/dist/sequence-manager.cjs.map +0 -1
- package/dist/sequence-manager.d.cts +0 -172
- package/dist/sequence-manager.js.map +0 -1
- package/dist/validate.cjs +0 -112
- package/dist/validate.cjs.map +0 -1
- package/dist/validate.d.cts +0 -67
- package/dist/validate.js.map +0 -1
- package/src/_event-target.ts +0 -157
- package/src/_keyboard-event.ts +0 -101
- package/src/_match.ts +0 -96
- package/src/_named-keys.ts +0 -62
- package/src/_recorder-chord.ts +0 -76
- package/src/_recording-guard.ts +0 -63
- package/src/_registration.ts +0 -79
- package/src/conflicts.ts +0 -125
- package/src/constants.ts +0 -455
- package/src/display-labels.ts +0 -123
- package/src/format.ts +0 -201
- package/src/hint.ts +0 -42
- package/src/hotkey-manager.ts +0 -814
- package/src/hotkey-recorder.ts +0 -348
- package/src/hotkey-sequence-recorder.ts +0 -410
- package/src/hotkey.types.ts +0 -269
- package/src/index.ts +0 -17
- package/src/key-state-tracker.ts +0 -261
- package/src/key.types.ts +0 -268
- package/src/match.ts +0 -210
- package/src/parse.ts +0 -369
- package/src/platform.ts +0 -66
- package/src/recorder-options.ts +0 -34
- package/src/sequence-manager.ts +0 -769
- package/src/validate.ts +0 -179
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sequence-manager.js","names":["#instance","#platform","#findConflictingSequence","#unregister","#registrations","#targetRegistrations","#ensureListenersForTarget","#removeListenersForTarget","#targetListeners","#createTargetKeyDownHandler","#createTargetKeyUpHandler","#processTargetEvent","#syncRegistrationView"],"sources":["../src/sequence-manager.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { isRecordingEvent } from './_recording-guard'\nimport { formatHotkeySequence } from './format'\nimport { detectPlatform } from './platform'\nimport { normalizeKeyName } from './constants'\nimport { isModifierKey, parseHotkey } from './parse'\nimport { areHotkeysEqual, matchesKeyboardEvent } from './match'\nimport { matchKeyboardEvent } from './_match'\nimport { normalizeKeyboardEvent } from './_keyboard-event'\nimport {\n defaultHotkeyOptions,\n getDefaultIgnoreInputs,\n handleConflict,\n optionsEqual,\n} from './_registration'\nimport { isEventForTarget, shouldIgnoreInputEvent } from './_event-target'\nimport type { HotkeyOptions } from './hotkey-manager'\nimport type {\n Hotkey,\n HotkeyCallback,\n HotkeyCallbackContext,\n ParsedHotkey,\n} from './hotkey.types'\n\nexport type Target = HTMLElement | Document | Window\n\n/**\n * Options for hotkey sequence matching.\n * Extends HotkeyOptions but excludes requireReset (not applicable to sequences).\n */\nexport interface SequenceOptions extends Omit<HotkeyOptions, 'requireReset'> {\n /** Timeout between keys in milliseconds. Default: 1000 */\n timeout?: number\n}\n\n/**\n * A sequence of hotkeys for Vim-style shortcuts.\n *\n * Each element is one step (a `Hotkey` string). Steps may include modifiers;\n * the same modifier can appear on consecutive steps (e.g. `Shift+R` then\n * `Shift+T`). Modifier-only key events do not advance or reset matching—see\n * `SequenceManager`. Automatic keydown repeats and IME composition are also ignored.\n *\n * @example\n * ```ts\n * const gotoTop: HotkeySequence = ['G', 'G'] // gg\n * const deleteLine: HotkeySequence = ['D', 'D'] // dd\n * const deleteWord: HotkeySequence = ['D', 'I', 'W'] // diw\n * const chainedShift: HotkeySequence = ['Shift+R', 'Shift+T']\n * ```\n */\nexport type HotkeySequence = Array<Hotkey>\n\n/**\n * Default timeout between keys in a sequence (in milliseconds).\n * Exported for consumers that display sequence state (e.g. devtools).\n */\nexport const DEFAULT_SEQUENCE_TIMEOUT = 1000\n\nlet sequenceIdCounter = 0\n\n/**\n * Generates a unique ID for sequence registrations.\n */\nfunction generateSequenceId(): string {\n return `sequence_${++sequenceIdCounter}`\n}\n\n/**\n * View of a sequence registration for devtools display.\n * Progress fields reflect an in-progress match (between first key and completion or timeout).\n */\nexport interface SequenceRegistrationView {\n id: string\n sequence: HotkeySequence\n options: SequenceOptions\n target: Target\n triggerCount: number\n /** Whether this sequence has been triggered at least once. */\n hasFired: boolean\n /** Steps matched in the current attempt (0 when idle or just completed). */\n matchedStepCount: number\n /** `Date.now()` when the last step in the current attempt matched; 0 if none. */\n partialMatchLastKeyTime: number\n}\n\n/**\n * Internal representation of a sequence registration.\n */\nexport interface SequenceRegistration {\n id: string\n sequence: HotkeySequence\n parsedSequence: Array<ParsedHotkey>\n callback: HotkeyCallback\n options: SequenceOptions\n target: Target\n currentIndex: number\n lastKeyTime: number\n triggerCount: number\n hasFired: boolean\n}\n\n/**\n * A handle returned from SequenceManager.register() that allows updating\n * the callback and options without re-registering the sequence.\n *\n * @example\n * ```ts\n * const handle = manager.register(['G', 'G'], callback, options)\n *\n * handle.callback = newCallback\n * handle.setOptions({ timeout: 500 })\n * handle.unregister()\n * ```\n */\nexport interface SequenceRegistrationHandle {\n readonly id: string\n readonly isActive: boolean\n callback: HotkeyCallback\n setOptions: (options: Partial<SequenceOptions>) => void\n unregister: () => void\n}\n\n/**\n * Manages keyboard sequence matching for Vim-style shortcuts.\n *\n * This class allows registering multi-key sequences like 'g g' or 'd d'\n * that trigger callbacks when the full sequence is pressed within\n * a configurable timeout.\n *\n * Modifier-only `keydown` / `keyup` events (Shift, Control, Alt, or Meta\n * alone) are not processed: they neither match a step nor reset progress.\n * That supports chained modifier chords (e.g. `Shift+R` then `Shift+T`) when\n * the user presses a modifier alone between steps.\n *\n * @example\n * ```ts\n * const matcher = SequenceManager.getInstance()\n *\n * // Register 'g g' to go to top\n * const unregister = matcher.register(['G', 'G'], (event, context) => {\n * scrollToTop()\n * }, { timeout: 500 })\n *\n * // Later, to unregister:\n * unregister()\n * ```\n */\n/**\n * Builds a devtools view from an internal registration.\n */\nfunction toRegistrationView(\n reg: SequenceRegistration,\n): SequenceRegistrationView {\n const len = reg.parsedSequence.length\n const inProgress = reg.currentIndex > 0 && reg.currentIndex < len\n return {\n id: reg.id,\n sequence: reg.sequence,\n options: reg.options,\n target: reg.target,\n triggerCount: reg.triggerCount,\n hasFired: reg.hasFired,\n matchedStepCount: inProgress ? reg.currentIndex : 0,\n partialMatchLastKeyTime: inProgress ? reg.lastKeyTime : 0,\n }\n}\n\nexport class SequenceManager {\n static #instance: SequenceManager | null = null\n\n /**\n * The TanStack Store containing sequence registration views for devtools.\n * Subscribe to this to observe registration changes.\n */\n readonly registrations: Store<Map<string, SequenceRegistrationView>> =\n new Store(new Map())\n\n #registrations: Map<string, SequenceRegistration> = new Map()\n #targetListeners: Map<\n Target,\n {\n keydown: (event: KeyboardEvent) => void\n keyup: (event: KeyboardEvent) => void\n }\n > = new Map()\n #targetRegistrations: Map<Target, Set<string>> = new Map()\n #platform: 'mac' | 'windows' | 'linux'\n\n private constructor() {\n this.#platform = detectPlatform()\n }\n\n /**\n * Gets the singleton instance of SequenceManager.\n */\n static getInstance(): SequenceManager {\n if (!SequenceManager.#instance) {\n SequenceManager.#instance = new SequenceManager()\n }\n return SequenceManager.#instance\n }\n\n /**\n * Resets the singleton instance. Useful for testing.\n */\n static resetInstance(): void {\n if (SequenceManager.#instance) {\n SequenceManager.#instance.destroy()\n SequenceManager.#instance = null\n }\n }\n\n #syncRegistrationView(registration: SequenceRegistration): void {\n this.registrations.setState((prev) =>\n new Map(prev).set(registration.id, toRegistrationView(registration)),\n )\n }\n\n /**\n * Registers a hotkey sequence handler.\n *\n * @param sequence - Array of hotkey strings that form the sequence\n * @param callback - Function to call when the sequence is completed\n * @param options - Options for the sequence behavior\n * @returns A handle to update or unregister the sequence\n */\n register(\n sequence: HotkeySequence,\n callback: HotkeyCallback,\n options: SequenceOptions = {},\n ): SequenceRegistrationHandle {\n if (sequence.length === 0) {\n throw new Error('Sequence must contain at least one hotkey')\n }\n\n const id = generateSequenceId()\n const platform = options.platform ?? this.#platform\n const parsedSequence = sequence.map((hotkey) =>\n parseHotkey(hotkey, platform),\n )\n\n // Resolve target: default to document if not provided or null\n const target: Target =\n options.target ??\n (typeof document !== 'undefined' ? document : ({} as Document))\n\n // Resolve conflict behavior\n const conflictBehavior = options.conflictBehavior ?? 'warn'\n\n // Check for existing registrations with the same sequence and target\n const conflictingRegistration = this.#findConflictingSequence(\n parsedSequence,\n target,\n )\n\n if (conflictingRegistration) {\n handleConflict(\n conflictingRegistration.id,\n formatHotkeySequence(sequence),\n conflictBehavior,\n (regId) => this.#unregister(regId),\n )\n }\n\n const firstStep = parsedSequence[0]!\n const resolvedIgnoreInputs =\n options.ignoreInputs ?? getDefaultIgnoreInputs(firstStep)\n\n const baseOptions = {\n ...defaultHotkeyOptions,\n timeout: DEFAULT_SEQUENCE_TIMEOUT,\n ...options,\n platform,\n ignoreInputs: resolvedIgnoreInputs,\n }\n\n const registration: SequenceRegistration = {\n id,\n sequence,\n parsedSequence,\n callback,\n options: baseOptions,\n target,\n currentIndex: 0,\n lastKeyTime: 0,\n triggerCount: 0,\n hasFired: false,\n }\n\n this.#registrations.set(id, registration)\n this.registrations.setState((prev) =>\n new Map(prev).set(id, toRegistrationView(registration)),\n )\n\n // Track registration for this target\n if (!this.#targetRegistrations.has(target)) {\n this.#targetRegistrations.set(target, new Set())\n }\n this.#targetRegistrations.get(target)!.add(id)\n\n // Ensure listeners are attached for this target\n this.#ensureListenersForTarget(target)\n\n const manager = this\n const handle: SequenceRegistrationHandle = {\n get id() {\n return id\n },\n get isActive() {\n return manager.#registrations.has(id)\n },\n get callback() {\n const reg = manager.#registrations.get(id)\n return reg?.callback ?? callback\n },\n set callback(newCallback: HotkeyCallback) {\n const reg = manager.#registrations.get(id)\n if (reg) {\n reg.callback = newCallback\n }\n },\n setOptions: (newOptions: Partial<SequenceOptions>) => {\n const reg = manager.#registrations.get(id)\n if (\n reg &&\n !optionsEqual(reg.options, { ...reg.options, ...newOptions })\n ) {\n reg.options = { ...reg.options, ...newOptions }\n manager.registrations.setState((prev) =>\n new Map(prev).set(id, toRegistrationView(reg)),\n )\n }\n },\n unregister: () => {\n manager.#unregister(id)\n },\n }\n\n return handle\n }\n\n /**\n * Unregisters a sequence by its registration ID.\n */\n #unregister(id: string): void {\n const registration = this.#registrations.get(id)\n if (!registration) {\n return\n }\n\n const target = registration.target\n\n this.#registrations.delete(id)\n this.registrations.setState((prev) => {\n const next = new Map(prev)\n next.delete(id)\n return next\n })\n\n // Remove from target registrations tracking\n const targetRegs = this.#targetRegistrations.get(target)\n if (targetRegs) {\n targetRegs.delete(id)\n if (targetRegs.size === 0) {\n this.#removeListenersForTarget(target)\n }\n }\n }\n\n /**\n * Ensures event listeners are attached for a specific target.\n */\n #ensureListenersForTarget(target: Target): void {\n if (typeof document === 'undefined') {\n return // SSR safety\n }\n\n if (this.#targetListeners.has(target)) {\n return\n }\n\n const keydownHandler = this.#createTargetKeyDownHandler(target)\n const keyupHandler = this.#createTargetKeyUpHandler(target)\n\n target.addEventListener('keydown', keydownHandler as EventListener)\n target.addEventListener('keyup', keyupHandler as EventListener)\n\n this.#targetListeners.set(target, {\n keydown: keydownHandler,\n keyup: keyupHandler,\n })\n }\n\n /**\n * Removes event listeners for a specific target.\n */\n #removeListenersForTarget(target: Target): void {\n if (typeof document === 'undefined') {\n return\n }\n\n const listeners = this.#targetListeners.get(target)\n if (!listeners) {\n return\n }\n\n target.removeEventListener('keydown', listeners.keydown as EventListener)\n target.removeEventListener('keyup', listeners.keyup as EventListener)\n\n this.#targetListeners.delete(target)\n this.#targetRegistrations.delete(target)\n }\n\n /**\n * Creates a keydown handler for a specific target.\n */\n #createTargetKeyDownHandler(target: Target): (event: KeyboardEvent) => void {\n return (event: KeyboardEvent) => {\n this.#processTargetEvent(event, target, 'keydown')\n }\n }\n\n /**\n * Creates a keyup handler for a specific target.\n */\n #createTargetKeyUpHandler(target: Target): (event: KeyboardEvent) => void {\n return (event: KeyboardEvent) => {\n this.#processTargetEvent(event, target, 'keyup')\n }\n }\n\n /**\n * Processes keyboard events for a specific target and event type.\n */\n #processTargetEvent(\n event: KeyboardEvent,\n target: Target,\n eventType: 'keydown' | 'keyup',\n ): void {\n if (isRecordingEvent(event)) {\n for (const id of this.#targetRegistrations.get(target) ?? []) {\n const registration = this.#registrations.get(id)\n if (registration && registration.currentIndex > 0) {\n registration.currentIndex = 0\n this.#syncRegistrationView(registration)\n }\n }\n return\n }\n if (\n (event.type === 'keydown' && event.repeat) ||\n normalizeKeyboardEvent(event).isComposing\n ) {\n return\n }\n // Skip modifier-only events so pressing e.g. Shift before Shift+C does not reset the sequence.\n if (isModifierKey(normalizeKeyName(event.key))) {\n return\n }\n\n const targetRegs = this.#targetRegistrations.get(target)\n if (!targetRegs) {\n return\n }\n\n const now = Date.now()\n // Score eligible next steps (or restarts) before advancing any sequence.\n const candidates = new Map<string, { score: number; nextIndex: number }>()\n let bestScore = 0\n\n registrationIds: for (const id of targetRegs) {\n const registration = this.#registrations.get(id)\n if (!registration) {\n continue\n }\n\n if (!isEventForTarget(event, target)) {\n continue\n }\n\n if (!registration.options.enabled) {\n continue\n }\n\n // Check if we should ignore input elements (defaults to true)\n if (registration.options.ignoreInputs !== false) {\n if (shouldIgnoreInputEvent(event, target, registration.target)) {\n continue registrationIds\n }\n }\n\n // Only process registrations that listen for this event type\n if (registration.options.eventType !== eventType) {\n continue\n }\n\n const timeout = registration.options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT\n\n // Check if sequence has timed out\n if (\n registration.currentIndex > 0 &&\n now - registration.lastKeyTime > timeout\n ) {\n registration.currentIndex = 0\n this.#syncRegistrationView(registration)\n }\n\n const expectedHotkey =\n registration.parsedSequence[registration.currentIndex]\n if (!expectedHotkey) {\n continue\n }\n\n let match = matchKeyboardEvent(\n event,\n expectedHotkey,\n registration.options.platform,\n )\n let nextIndex = registration.currentIndex + 1\n if (!match.matched && registration.currentIndex > 0) {\n match = matchKeyboardEvent(\n event,\n registration.parsedSequence[0]!,\n registration.options.platform,\n )\n nextIndex = 1\n }\n candidates.set(id, { score: match.score, nextIndex })\n bestScore = Math.max(bestScore, match.score)\n }\n\n for (const [id, candidate] of candidates) {\n const registration = this.#registrations.get(id)\n if (!registration || !registration.options.enabled) continue\n if (candidate.score > 0 && candidate.score === bestScore) {\n registration.lastKeyTime = now\n registration.currentIndex = candidate.nextIndex\n\n if (registration.currentIndex >= registration.parsedSequence.length) {\n if (registration.options.preventDefault) {\n event.preventDefault()\n }\n if (registration.options.stopPropagation) {\n event.stopPropagation()\n }\n\n const context: HotkeyCallbackContext = {\n hotkey: registration.sequence.join(' ') as Hotkey,\n parsedHotkey:\n registration.parsedSequence[\n registration.parsedSequence.length - 1\n ]!,\n }\n\n registration.triggerCount++\n registration.hasFired = true\n registration.currentIndex = 0\n\n this.#syncRegistrationView(registration)\n\n registration.callback(event, context)\n } else {\n this.#syncRegistrationView(registration)\n }\n } else if (registration.currentIndex > 0) {\n // A mismatched or weaker candidate cannot carry stale progress forward.\n registration.currentIndex = 0\n this.#syncRegistrationView(registration)\n }\n }\n }\n\n /**\n * Compares resolved step identities without changing the supplied sequence strings.\n */\n #findConflictingSequence(\n parsedSequence: Array<ParsedHotkey>,\n target: Target,\n ): SequenceRegistration | null {\n for (const registration of this.#registrations.values()) {\n if (\n registration.target === target &&\n registration.parsedSequence.length === parsedSequence.length &&\n parsedSequence.every((step, index) =>\n areHotkeysEqual(step, registration.parsedSequence[index]!),\n )\n ) {\n return registration\n }\n }\n return null\n }\n\n /**\n * Resets all sequence progress.\n */\n resetAll(): void {\n for (const registration of this.#registrations.values()) {\n registration.currentIndex = 0\n registration.lastKeyTime = 0\n this.#syncRegistrationView(registration)\n }\n }\n\n /**\n * Triggers a sequence's callback programmatically from devtools.\n * Creates a synthetic KeyboardEvent from the last key in the sequence.\n *\n * @param id - The registration ID to trigger\n * @returns True if the registration was found and triggered\n */\n triggerSequence(id: string): boolean {\n const registration = this.#registrations.get(id)\n if (!registration) {\n return false\n }\n\n const lastParsed =\n registration.parsedSequence[registration.parsedSequence.length - 1]\n if (!lastParsed) {\n return false\n }\n\n const syntheticEvent = new KeyboardEvent(\n registration.options.eventType ?? 'keydown',\n {\n ...(lastParsed.code !== undefined\n ? { code: lastParsed.code }\n : { key: lastParsed.key }),\n ctrlKey: lastParsed.ctrl,\n shiftKey: lastParsed.shift,\n altKey: lastParsed.alt,\n metaKey: lastParsed.meta,\n bubbles: true,\n cancelable: true,\n },\n )\n\n registration.triggerCount++\n registration.hasFired = true\n\n this.#syncRegistrationView(registration)\n\n const context: HotkeyCallbackContext = {\n hotkey: registration.sequence.join(' ') as Hotkey,\n parsedHotkey: lastParsed,\n }\n\n registration.callback(syntheticEvent, context)\n\n return true\n }\n\n /**\n * Gets the number of registered sequences.\n */\n getRegistrationCount(): number {\n return this.#registrations.size\n }\n\n /**\n * Destroys the manager and removes all listeners.\n */\n destroy(): void {\n for (const target of this.#targetListeners.keys()) {\n this.#removeListenersForTarget(target)\n }\n this.#registrations.clear()\n this.registrations.setState(() => new Map())\n }\n}\n\n/**\n * Gets the singleton SequenceManager instance.\n * Convenience function for accessing the manager.\n */\nexport function getSequenceManager(): SequenceManager {\n return SequenceManager.getInstance()\n}\n\n/**\n * Creates a simple sequence matcher for one-off use.\n *\n * This is a low-level helper that does not support ignoreInputs, target,\n * or other HotkeyOptions. Callers must handle input filtering themselves\n * if attaching to document.\n *\n * @param sequence - The sequence of hotkeys to match\n * @param options - Options including timeout\n * @returns An object with match() and reset() methods\n *\n * @example\n * ```ts\n * const matcher = createSequenceMatcher(['G', 'G'], { timeout: 500 })\n *\n * document.addEventListener('keydown', (event) => {\n * if (matcher.match(event)) {\n * console.log('Sequence matched!')\n * }\n * })\n * ```\n */\nexport function createSequenceMatcher(\n sequence: HotkeySequence,\n options: { timeout?: number; platform?: 'mac' | 'windows' | 'linux' } = {},\n): {\n match: (event: KeyboardEvent) => boolean\n reset: () => void\n getProgress: () => number\n} {\n const platform = options.platform ?? detectPlatform()\n const timeout = options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT\n const parsedSequence = sequence.map((hotkey) => parseHotkey(hotkey, platform))\n\n let currentIndex = 0\n let lastKeyTime = 0\n\n return {\n match(event: KeyboardEvent): boolean {\n // These events are not sequence steps and must not refresh the timeout.\n if (\n (event.type === 'keydown' && event.repeat) ||\n normalizeKeyboardEvent(event, platform).isComposing ||\n isModifierKey(normalizeKeyName(event.key))\n ) {\n return false\n }\n const now = Date.now()\n\n if (currentIndex > 0 && now - lastKeyTime > timeout) {\n currentIndex = 0\n }\n\n const expected = parsedSequence[currentIndex]\n if (!expected) {\n return false\n }\n\n if (matchesKeyboardEvent(event, expected, platform)) {\n lastKeyTime = now\n currentIndex++\n\n if (currentIndex >= parsedSequence.length) {\n currentIndex = 0\n return true\n }\n } else if (currentIndex > 0) {\n if (matchesKeyboardEvent(event, parsedSequence[0]!, platform)) {\n currentIndex = 1\n lastKeyTime = now\n } else {\n currentIndex = 0\n }\n }\n\n return false\n },\n\n reset(): void {\n currentIndex = 0\n lastKeyTime = 0\n },\n\n getProgress(): number {\n return currentIndex\n },\n }\n}\n"],"mappings":"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"}
|
package/dist/validate.cjs
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
const require_constants = require('./constants.cjs');
|
|
2
|
-
const require__keyboard_event = require('./_keyboard-event.cjs');
|
|
3
|
-
|
|
4
|
-
//#region src/validate.ts
|
|
5
|
-
/**
|
|
6
|
-
* Validates a hotkey string and returns any warnings or errors.
|
|
7
|
-
*
|
|
8
|
-
* Checks for:
|
|
9
|
-
* - Valid syntax (modifier+...+key format)
|
|
10
|
-
* - Known modifiers
|
|
11
|
-
* - Known keys
|
|
12
|
-
*
|
|
13
|
-
* @param hotkey - The hotkey string to validate
|
|
14
|
-
* @returns A ValidationResult with validity status, warnings, and errors
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```ts
|
|
18
|
-
* validateHotkey('Mod+S')
|
|
19
|
-
* // { valid: true, warnings: [], errors: [] }
|
|
20
|
-
*
|
|
21
|
-
* validateHotkey('')
|
|
22
|
-
* // { valid: false, warnings: [], errors: ['Hotkey cannot be empty'] }
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
function validateHotkey(hotkey) {
|
|
26
|
-
const warnings = [];
|
|
27
|
-
const errors = [];
|
|
28
|
-
if (!hotkey || hotkey.trim() === "") return {
|
|
29
|
-
valid: false,
|
|
30
|
-
warnings: [],
|
|
31
|
-
errors: ["Hotkey cannot be empty"]
|
|
32
|
-
};
|
|
33
|
-
const parts = require__keyboard_event.splitHotkeyParts(hotkey).map((p) => p.trim());
|
|
34
|
-
if (parts.length === 0 || parts.some((p) => p === "")) return {
|
|
35
|
-
valid: false,
|
|
36
|
-
warnings: [],
|
|
37
|
-
errors: ["Invalid hotkey format: empty parts detected"]
|
|
38
|
-
};
|
|
39
|
-
const modifierParts = parts.slice(0, -1);
|
|
40
|
-
const keyPart = parts[parts.length - 1];
|
|
41
|
-
for (const modifier of modifierParts) if (!(require_constants.MODIFIER_ALIASES[modifier] ?? require_constants.MODIFIER_ALIASES[modifier.toLowerCase()])) errors.push(`Unknown modifier: '${modifier}'`);
|
|
42
|
-
if (keyPart.length > 1 && (keyPart.startsWith("[") || keyPart.endsWith("]")) && !/^\[[A-Za-z][A-Za-z0-9]*\]$/.test(keyPart)) errors.push("Invalid physical key: use a bracketed KeyboardEvent.code, such as [KeyS]");
|
|
43
|
-
if (!isKnownKey(require_constants.normalizeKeyName(keyPart)) && !isKnownKey(keyPart)) warnings.push(`Unknown key: '${keyPart}'. This may still work but won't have type-safe autocomplete.`);
|
|
44
|
-
return {
|
|
45
|
-
valid: errors.length === 0,
|
|
46
|
-
warnings,
|
|
47
|
-
errors
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Checks if a key is in the known keys set.
|
|
52
|
-
*/
|
|
53
|
-
function isKnownKey(key) {
|
|
54
|
-
if (/^\[[A-Za-z][A-Za-z0-9]*\]$/.test(key)) return true;
|
|
55
|
-
if (Array.from(key).length === 1) return true;
|
|
56
|
-
if (require_constants.ALL_KEYS.has(key)) return true;
|
|
57
|
-
if (key.length === 1 && require_constants.ALL_KEYS.has(key.toUpperCase())) return true;
|
|
58
|
-
return key in {
|
|
59
|
-
Esc: true,
|
|
60
|
-
Return: true,
|
|
61
|
-
Space: true,
|
|
62
|
-
" ": true,
|
|
63
|
-
Del: true,
|
|
64
|
-
Up: true,
|
|
65
|
-
Down: true,
|
|
66
|
-
Left: true,
|
|
67
|
-
Right: true
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Validates a hotkey and throws an error if invalid.
|
|
72
|
-
* Useful for development-time validation.
|
|
73
|
-
*
|
|
74
|
-
* @param hotkey - The hotkey string to validate
|
|
75
|
-
* @throws Error if the hotkey is invalid
|
|
76
|
-
*
|
|
77
|
-
* @example
|
|
78
|
-
* ```ts
|
|
79
|
-
* assertValidHotkey('Mod+S') // OK
|
|
80
|
-
* assertValidHotkey('') // Throws Error: Invalid hotkey: Hotkey cannot be empty
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
|
-
function assertValidHotkey(hotkey) {
|
|
84
|
-
const result = validateHotkey(hotkey);
|
|
85
|
-
if (!result.valid) throw new Error(`Invalid hotkey '${hotkey}': ${result.errors.join(", ")}`);
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Validates a hotkey and logs warnings to the console.
|
|
89
|
-
* Useful for development-time feedback.
|
|
90
|
-
*
|
|
91
|
-
* @param hotkey - The hotkey string to validate
|
|
92
|
-
* @returns True if the hotkey is valid (may still have warnings)
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* ```ts
|
|
96
|
-
* checkHotkey('Alt+C')
|
|
97
|
-
* // Console: Warning: Alt+C may not work reliably on macOS...
|
|
98
|
-
* // Returns: true
|
|
99
|
-
* ```
|
|
100
|
-
*/
|
|
101
|
-
function checkHotkey(hotkey) {
|
|
102
|
-
const result = validateHotkey(hotkey);
|
|
103
|
-
if (result.errors.length > 0) console.error(`Hotkey '${hotkey}' errors:`, result.errors.join("; "));
|
|
104
|
-
if (result.warnings.length > 0) console.warn(`Hotkey '${hotkey}' warnings:`, result.warnings.join("; "));
|
|
105
|
-
return result.valid;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
//#endregion
|
|
109
|
-
exports.assertValidHotkey = assertValidHotkey;
|
|
110
|
-
exports.checkHotkey = checkHotkey;
|
|
111
|
-
exports.validateHotkey = validateHotkey;
|
|
112
|
-
//# sourceMappingURL=validate.cjs.map
|
package/dist/validate.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"validate.cjs","names":["splitHotkeyParts","MODIFIER_ALIASES","normalizeKeyName","ALL_KEYS"],"sources":["../src/validate.ts"],"sourcesContent":["import { ALL_KEYS, MODIFIER_ALIASES, normalizeKeyName } from './constants'\nimport { splitHotkeyParts } from './_keyboard-event'\nimport type { Hotkey } from './hotkey.types'\n\n/**\n * Validates a hotkey string and returns any warnings or errors.\n *\n * Checks for:\n * - Valid syntax (modifier+...+key format)\n * - Known modifiers\n * - Known keys\n *\n * @param hotkey - The hotkey string to validate\n * @returns A ValidationResult with validity status, warnings, and errors\n *\n * @example\n * ```ts\n * validateHotkey('Mod+S')\n * // { valid: true, warnings: [], errors: [] }\n *\n * validateHotkey('')\n * // { valid: false, warnings: [], errors: ['Hotkey cannot be empty'] }\n * ```\n */\nexport function validateHotkey(\n hotkey: Hotkey | (string & {}),\n): ValidationResult {\n const warnings: Array<string> = []\n const errors: Array<string> = []\n\n // Check for empty string\n if (!hotkey || hotkey.trim() === '') {\n return {\n valid: false,\n warnings: [],\n errors: ['Hotkey cannot be empty'],\n }\n }\n\n const parts = splitHotkeyParts(hotkey).map((p) => p.trim())\n\n // Must have at least one part (the key)\n if (parts.length === 0 || parts.some((p) => p === '')) {\n return {\n valid: false,\n warnings: [],\n errors: ['Invalid hotkey format: empty parts detected'],\n }\n }\n\n // Validate modifiers (all parts except the last)\n const modifierParts = parts.slice(0, -1)\n const keyPart = parts[parts.length - 1]!\n\n // Check for unknown modifiers\n for (const modifier of modifierParts) {\n const normalized =\n MODIFIER_ALIASES[modifier] ?? MODIFIER_ALIASES[modifier.toLowerCase()]\n if (!normalized) {\n errors.push(`Unknown modifier: '${modifier}'`)\n }\n }\n\n if (\n keyPart.length > 1 &&\n (keyPart.startsWith('[') || keyPart.endsWith(']')) &&\n !/^\\[[A-Za-z][A-Za-z0-9]*\\]$/.test(keyPart)\n ) {\n errors.push(\n 'Invalid physical key: use a bracketed KeyboardEvent.code, such as [KeyS]',\n )\n }\n\n // Check if key is known\n const normalizedKey = normalizeKeyName(keyPart)\n if (!isKnownKey(normalizedKey) && !isKnownKey(keyPart)) {\n warnings.push(\n `Unknown key: '${keyPart}'. This may still work but won't have type-safe autocomplete.`,\n )\n }\n\n return {\n valid: errors.length === 0,\n warnings,\n errors,\n }\n}\n\n/**\n * Checks if a key is in the known keys set.\n */\nfunction isKnownKey(key: string): boolean {\n if (/^\\[[A-Za-z][A-Za-z0-9]*\\]$/.test(key)) return true\n if (Array.from(key).length === 1) return true\n // Check direct match\n if (ALL_KEYS.has(key as any)) {\n return true\n }\n\n // Check uppercase version for letters\n if (key.length === 1 && ALL_KEYS.has(key.toUpperCase() as any)) {\n return true\n }\n\n // Check common aliases\n const aliases: Record<string, boolean> = {\n Esc: true,\n Return: true,\n Space: true,\n ' ': true,\n Del: true,\n Up: true,\n Down: true,\n Left: true,\n Right: true,\n }\n\n return key in aliases\n}\n\n/**\n * Validates a hotkey and throws an error if invalid.\n * Useful for development-time validation.\n *\n * @param hotkey - The hotkey string to validate\n * @throws Error if the hotkey is invalid\n *\n * @example\n * ```ts\n * assertValidHotkey('Mod+S') // OK\n * assertValidHotkey('') // Throws Error: Invalid hotkey: Hotkey cannot be empty\n * ```\n */\nexport function assertValidHotkey(hotkey: Hotkey | (string & {})): void {\n const result = validateHotkey(hotkey)\n if (!result.valid) {\n throw new Error(`Invalid hotkey '${hotkey}': ${result.errors.join(', ')}`)\n }\n}\n\n/**\n * Validates a hotkey and logs warnings to the console.\n * Useful for development-time feedback.\n *\n * @param hotkey - The hotkey string to validate\n * @returns True if the hotkey is valid (may still have warnings)\n *\n * @example\n * ```ts\n * checkHotkey('Alt+C')\n * // Console: Warning: Alt+C may not work reliably on macOS...\n * // Returns: true\n * ```\n */\nexport function checkHotkey(hotkey: Hotkey | (string & {})): boolean {\n const result = validateHotkey(hotkey)\n\n if (result.errors.length > 0) {\n console.error(`Hotkey '${hotkey}' errors:`, result.errors.join('; '))\n }\n\n if (result.warnings.length > 0) {\n console.warn(`Hotkey '${hotkey}' warnings:`, result.warnings.join('; '))\n }\n\n return result.valid\n}\n\n/**\n * Result of validating a hotkey string.\n */\nexport interface ValidationResult {\n /** Whether the hotkey is valid (can still have warnings) */\n valid: boolean\n /** Warning messages about potential issues */\n warnings: Array<string>\n /** Error messages about invalid syntax */\n errors: Array<string>\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,eACd,QACkB;CAClB,MAAM,WAA0B,EAAE;CAClC,MAAM,SAAwB,EAAE;AAGhC,KAAI,CAAC,UAAU,OAAO,MAAM,KAAK,GAC/B,QAAO;EACL,OAAO;EACP,UAAU,EAAE;EACZ,QAAQ,CAAC,yBAAyB;EACnC;CAGH,MAAM,QAAQA,yCAAiB,OAAO,CAAC,KAAK,MAAM,EAAE,MAAM,CAAC;AAG3D,KAAI,MAAM,WAAW,KAAK,MAAM,MAAM,MAAM,MAAM,GAAG,CACnD,QAAO;EACL,OAAO;EACP,UAAU,EAAE;EACZ,QAAQ,CAAC,8CAA8C;EACxD;CAIH,MAAM,gBAAgB,MAAM,MAAM,GAAG,GAAG;CACxC,MAAM,UAAU,MAAM,MAAM,SAAS;AAGrC,MAAK,MAAM,YAAY,cAGrB,KAAI,EADFC,mCAAiB,aAAaA,mCAAiB,SAAS,aAAa,GAErE,QAAO,KAAK,sBAAsB,SAAS,GAAG;AAIlD,KACE,QAAQ,SAAS,MAChB,QAAQ,WAAW,IAAI,IAAI,QAAQ,SAAS,IAAI,KACjD,CAAC,6BAA6B,KAAK,QAAQ,CAE3C,QAAO,KACL,2EACD;AAKH,KAAI,CAAC,WADiBC,mCAAiB,QAAQ,CACjB,IAAI,CAAC,WAAW,QAAQ,CACpD,UAAS,KACP,iBAAiB,QAAQ,+DAC1B;AAGH,QAAO;EACL,OAAO,OAAO,WAAW;EACzB;EACA;EACD;;;;;AAMH,SAAS,WAAW,KAAsB;AACxC,KAAI,6BAA6B,KAAK,IAAI,CAAE,QAAO;AACnD,KAAI,MAAM,KAAK,IAAI,CAAC,WAAW,EAAG,QAAO;AAEzC,KAAIC,2BAAS,IAAI,IAAW,CAC1B,QAAO;AAIT,KAAI,IAAI,WAAW,KAAKA,2BAAS,IAAI,IAAI,aAAa,CAAQ,CAC5D,QAAO;AAgBT,QAAO,OAZkC;EACvC,KAAK;EACL,QAAQ;EACR,OAAO;EACP,KAAK;EACL,KAAK;EACL,IAAI;EACJ,MAAM;EACN,MAAM;EACN,OAAO;EACR;;;;;;;;;;;;;;;AAkBH,SAAgB,kBAAkB,QAAsC;CACtE,MAAM,SAAS,eAAe,OAAO;AACrC,KAAI,CAAC,OAAO,MACV,OAAM,IAAI,MAAM,mBAAmB,OAAO,KAAK,OAAO,OAAO,KAAK,KAAK,GAAG;;;;;;;;;;;;;;;;AAkB9E,SAAgB,YAAY,QAAyC;CACnE,MAAM,SAAS,eAAe,OAAO;AAErC,KAAI,OAAO,OAAO,SAAS,EACzB,SAAQ,MAAM,WAAW,OAAO,YAAY,OAAO,OAAO,KAAK,KAAK,CAAC;AAGvE,KAAI,OAAO,SAAS,SAAS,EAC3B,SAAQ,KAAK,WAAW,OAAO,cAAc,OAAO,SAAS,KAAK,KAAK,CAAC;AAG1E,QAAO,OAAO"}
|
package/dist/validate.d.cts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { Hotkey } from "./hotkey.types.cjs";
|
|
2
|
-
|
|
3
|
-
//#region src/validate.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Validates a hotkey string and returns any warnings or errors.
|
|
6
|
-
*
|
|
7
|
-
* Checks for:
|
|
8
|
-
* - Valid syntax (modifier+...+key format)
|
|
9
|
-
* - Known modifiers
|
|
10
|
-
* - Known keys
|
|
11
|
-
*
|
|
12
|
-
* @param hotkey - The hotkey string to validate
|
|
13
|
-
* @returns A ValidationResult with validity status, warnings, and errors
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* validateHotkey('Mod+S')
|
|
18
|
-
* // { valid: true, warnings: [], errors: [] }
|
|
19
|
-
*
|
|
20
|
-
* validateHotkey('')
|
|
21
|
-
* // { valid: false, warnings: [], errors: ['Hotkey cannot be empty'] }
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
declare function validateHotkey(hotkey: Hotkey | (string & {})): ValidationResult;
|
|
25
|
-
/**
|
|
26
|
-
* Validates a hotkey and throws an error if invalid.
|
|
27
|
-
* Useful for development-time validation.
|
|
28
|
-
*
|
|
29
|
-
* @param hotkey - The hotkey string to validate
|
|
30
|
-
* @throws Error if the hotkey is invalid
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```ts
|
|
34
|
-
* assertValidHotkey('Mod+S') // OK
|
|
35
|
-
* assertValidHotkey('') // Throws Error: Invalid hotkey: Hotkey cannot be empty
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
declare function assertValidHotkey(hotkey: Hotkey | (string & {})): void;
|
|
39
|
-
/**
|
|
40
|
-
* Validates a hotkey and logs warnings to the console.
|
|
41
|
-
* Useful for development-time feedback.
|
|
42
|
-
*
|
|
43
|
-
* @param hotkey - The hotkey string to validate
|
|
44
|
-
* @returns True if the hotkey is valid (may still have warnings)
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```ts
|
|
48
|
-
* checkHotkey('Alt+C')
|
|
49
|
-
* // Console: Warning: Alt+C may not work reliably on macOS...
|
|
50
|
-
* // Returns: true
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
declare function checkHotkey(hotkey: Hotkey | (string & {})): boolean;
|
|
54
|
-
/**
|
|
55
|
-
* Result of validating a hotkey string.
|
|
56
|
-
*/
|
|
57
|
-
interface ValidationResult {
|
|
58
|
-
/** Whether the hotkey is valid (can still have warnings) */
|
|
59
|
-
valid: boolean;
|
|
60
|
-
/** Warning messages about potential issues */
|
|
61
|
-
warnings: Array<string>;
|
|
62
|
-
/** Error messages about invalid syntax */
|
|
63
|
-
errors: Array<string>;
|
|
64
|
-
}
|
|
65
|
-
//#endregion
|
|
66
|
-
export { ValidationResult, assertValidHotkey, checkHotkey, validateHotkey };
|
|
67
|
-
//# sourceMappingURL=validate.d.cts.map
|
package/dist/validate.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"validate.js","names":[],"sources":["../src/validate.ts"],"sourcesContent":["import { ALL_KEYS, MODIFIER_ALIASES, normalizeKeyName } from './constants'\nimport { splitHotkeyParts } from './_keyboard-event'\nimport type { Hotkey } from './hotkey.types'\n\n/**\n * Validates a hotkey string and returns any warnings or errors.\n *\n * Checks for:\n * - Valid syntax (modifier+...+key format)\n * - Known modifiers\n * - Known keys\n *\n * @param hotkey - The hotkey string to validate\n * @returns A ValidationResult with validity status, warnings, and errors\n *\n * @example\n * ```ts\n * validateHotkey('Mod+S')\n * // { valid: true, warnings: [], errors: [] }\n *\n * validateHotkey('')\n * // { valid: false, warnings: [], errors: ['Hotkey cannot be empty'] }\n * ```\n */\nexport function validateHotkey(\n hotkey: Hotkey | (string & {}),\n): ValidationResult {\n const warnings: Array<string> = []\n const errors: Array<string> = []\n\n // Check for empty string\n if (!hotkey || hotkey.trim() === '') {\n return {\n valid: false,\n warnings: [],\n errors: ['Hotkey cannot be empty'],\n }\n }\n\n const parts = splitHotkeyParts(hotkey).map((p) => p.trim())\n\n // Must have at least one part (the key)\n if (parts.length === 0 || parts.some((p) => p === '')) {\n return {\n valid: false,\n warnings: [],\n errors: ['Invalid hotkey format: empty parts detected'],\n }\n }\n\n // Validate modifiers (all parts except the last)\n const modifierParts = parts.slice(0, -1)\n const keyPart = parts[parts.length - 1]!\n\n // Check for unknown modifiers\n for (const modifier of modifierParts) {\n const normalized =\n MODIFIER_ALIASES[modifier] ?? MODIFIER_ALIASES[modifier.toLowerCase()]\n if (!normalized) {\n errors.push(`Unknown modifier: '${modifier}'`)\n }\n }\n\n if (\n keyPart.length > 1 &&\n (keyPart.startsWith('[') || keyPart.endsWith(']')) &&\n !/^\\[[A-Za-z][A-Za-z0-9]*\\]$/.test(keyPart)\n ) {\n errors.push(\n 'Invalid physical key: use a bracketed KeyboardEvent.code, such as [KeyS]',\n )\n }\n\n // Check if key is known\n const normalizedKey = normalizeKeyName(keyPart)\n if (!isKnownKey(normalizedKey) && !isKnownKey(keyPart)) {\n warnings.push(\n `Unknown key: '${keyPart}'. This may still work but won't have type-safe autocomplete.`,\n )\n }\n\n return {\n valid: errors.length === 0,\n warnings,\n errors,\n }\n}\n\n/**\n * Checks if a key is in the known keys set.\n */\nfunction isKnownKey(key: string): boolean {\n if (/^\\[[A-Za-z][A-Za-z0-9]*\\]$/.test(key)) return true\n if (Array.from(key).length === 1) return true\n // Check direct match\n if (ALL_KEYS.has(key as any)) {\n return true\n }\n\n // Check uppercase version for letters\n if (key.length === 1 && ALL_KEYS.has(key.toUpperCase() as any)) {\n return true\n }\n\n // Check common aliases\n const aliases: Record<string, boolean> = {\n Esc: true,\n Return: true,\n Space: true,\n ' ': true,\n Del: true,\n Up: true,\n Down: true,\n Left: true,\n Right: true,\n }\n\n return key in aliases\n}\n\n/**\n * Validates a hotkey and throws an error if invalid.\n * Useful for development-time validation.\n *\n * @param hotkey - The hotkey string to validate\n * @throws Error if the hotkey is invalid\n *\n * @example\n * ```ts\n * assertValidHotkey('Mod+S') // OK\n * assertValidHotkey('') // Throws Error: Invalid hotkey: Hotkey cannot be empty\n * ```\n */\nexport function assertValidHotkey(hotkey: Hotkey | (string & {})): void {\n const result = validateHotkey(hotkey)\n if (!result.valid) {\n throw new Error(`Invalid hotkey '${hotkey}': ${result.errors.join(', ')}`)\n }\n}\n\n/**\n * Validates a hotkey and logs warnings to the console.\n * Useful for development-time feedback.\n *\n * @param hotkey - The hotkey string to validate\n * @returns True if the hotkey is valid (may still have warnings)\n *\n * @example\n * ```ts\n * checkHotkey('Alt+C')\n * // Console: Warning: Alt+C may not work reliably on macOS...\n * // Returns: true\n * ```\n */\nexport function checkHotkey(hotkey: Hotkey | (string & {})): boolean {\n const result = validateHotkey(hotkey)\n\n if (result.errors.length > 0) {\n console.error(`Hotkey '${hotkey}' errors:`, result.errors.join('; '))\n }\n\n if (result.warnings.length > 0) {\n console.warn(`Hotkey '${hotkey}' warnings:`, result.warnings.join('; '))\n }\n\n return result.valid\n}\n\n/**\n * Result of validating a hotkey string.\n */\nexport interface ValidationResult {\n /** Whether the hotkey is valid (can still have warnings) */\n valid: boolean\n /** Warning messages about potential issues */\n warnings: Array<string>\n /** Error messages about invalid syntax */\n errors: Array<string>\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,eACd,QACkB;CAClB,MAAM,WAA0B,EAAE;CAClC,MAAM,SAAwB,EAAE;AAGhC,KAAI,CAAC,UAAU,OAAO,MAAM,KAAK,GAC/B,QAAO;EACL,OAAO;EACP,UAAU,EAAE;EACZ,QAAQ,CAAC,yBAAyB;EACnC;CAGH,MAAM,QAAQ,iBAAiB,OAAO,CAAC,KAAK,MAAM,EAAE,MAAM,CAAC;AAG3D,KAAI,MAAM,WAAW,KAAK,MAAM,MAAM,MAAM,MAAM,GAAG,CACnD,QAAO;EACL,OAAO;EACP,UAAU,EAAE;EACZ,QAAQ,CAAC,8CAA8C;EACxD;CAIH,MAAM,gBAAgB,MAAM,MAAM,GAAG,GAAG;CACxC,MAAM,UAAU,MAAM,MAAM,SAAS;AAGrC,MAAK,MAAM,YAAY,cAGrB,KAAI,EADF,iBAAiB,aAAa,iBAAiB,SAAS,aAAa,GAErE,QAAO,KAAK,sBAAsB,SAAS,GAAG;AAIlD,KACE,QAAQ,SAAS,MAChB,QAAQ,WAAW,IAAI,IAAI,QAAQ,SAAS,IAAI,KACjD,CAAC,6BAA6B,KAAK,QAAQ,CAE3C,QAAO,KACL,2EACD;AAKH,KAAI,CAAC,WADiB,iBAAiB,QAAQ,CACjB,IAAI,CAAC,WAAW,QAAQ,CACpD,UAAS,KACP,iBAAiB,QAAQ,+DAC1B;AAGH,QAAO;EACL,OAAO,OAAO,WAAW;EACzB;EACA;EACD;;;;;AAMH,SAAS,WAAW,KAAsB;AACxC,KAAI,6BAA6B,KAAK,IAAI,CAAE,QAAO;AACnD,KAAI,MAAM,KAAK,IAAI,CAAC,WAAW,EAAG,QAAO;AAEzC,KAAI,SAAS,IAAI,IAAW,CAC1B,QAAO;AAIT,KAAI,IAAI,WAAW,KAAK,SAAS,IAAI,IAAI,aAAa,CAAQ,CAC5D,QAAO;AAgBT,QAAO,OAZkC;EACvC,KAAK;EACL,QAAQ;EACR,OAAO;EACP,KAAK;EACL,KAAK;EACL,IAAI;EACJ,MAAM;EACN,MAAM;EACN,OAAO;EACR;;;;;;;;;;;;;;;AAkBH,SAAgB,kBAAkB,QAAsC;CACtE,MAAM,SAAS,eAAe,OAAO;AACrC,KAAI,CAAC,OAAO,MACV,OAAM,IAAI,MAAM,mBAAmB,OAAO,KAAK,OAAO,OAAO,KAAK,KAAK,GAAG;;;;;;;;;;;;;;;;AAkB9E,SAAgB,YAAY,QAAyC;CACnE,MAAM,SAAS,eAAe,OAAO;AAErC,KAAI,OAAO,OAAO,SAAS,EACzB,SAAQ,MAAM,WAAW,OAAO,YAAY,OAAO,OAAO,KAAK,KAAK,CAAC;AAGvE,KAAI,OAAO,SAAS,SAAS,EAC3B,SAAQ,KAAK,WAAW,OAAO,cAAc,OAAO,SAAS,KAAK,KAAK,CAAC;AAG1E,QAAO,OAAO"}
|
package/src/_event-target.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checks if an element is an input-like element that should be ignored for hotkeys.
|
|
3
|
-
*
|
|
4
|
-
* This includes:
|
|
5
|
-
* - HTMLInputElement (all input types except button, submit, reset)
|
|
6
|
-
* - HTMLTextAreaElement
|
|
7
|
-
* - HTMLSelectElement
|
|
8
|
-
* - Elements with contentEditable enabled
|
|
9
|
-
*
|
|
10
|
-
* Button-type inputs (button, submit, reset) are excluded so hotkeys like
|
|
11
|
-
* Mod+S and Escape fire when the user has tabbed to a form button.
|
|
12
|
-
*/
|
|
13
|
-
export function isInputElement(element: EventTarget | null): boolean {
|
|
14
|
-
if (!element) {
|
|
15
|
-
return false
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (element instanceof HTMLInputElement) {
|
|
19
|
-
const type = element.type.toLowerCase()
|
|
20
|
-
if (type === 'button' || type === 'submit' || type === 'reset') {
|
|
21
|
-
return false
|
|
22
|
-
}
|
|
23
|
-
return true
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
element instanceof HTMLTextAreaElement ||
|
|
28
|
-
element instanceof HTMLSelectElement
|
|
29
|
-
) {
|
|
30
|
-
return true
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Check for contenteditable elements (includes "true", "", "plaintext-only",
|
|
34
|
-
// and inherited contenteditable from ancestor elements)
|
|
35
|
-
if (element instanceof HTMLElement && element.isContentEditable) {
|
|
36
|
-
return true
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return false
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Returns the focused element for the document associated with where hotkey
|
|
44
|
-
* listeners are attached (listener root). Use this instead of the global
|
|
45
|
-
* `document.activeElement` so registrations scoped to an iframe (or another
|
|
46
|
-
* document) read focus from the correct tree.
|
|
47
|
-
*/
|
|
48
|
-
export function getActiveElementForListenerTarget(
|
|
49
|
-
target: HTMLElement | Document | Window,
|
|
50
|
-
): Element | null {
|
|
51
|
-
if (typeof document === 'undefined') {
|
|
52
|
-
return null
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Document first (nodeType covers environments where `document instanceof Document` is false)
|
|
56
|
-
if (
|
|
57
|
-
(typeof Document !== 'undefined' && target instanceof Document) ||
|
|
58
|
-
(typeof Node !== 'undefined' &&
|
|
59
|
-
(target as Node).nodeType === Node.DOCUMENT_NODE)
|
|
60
|
-
) {
|
|
61
|
-
return (target as Document).activeElement
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (typeof HTMLElement !== 'undefined' && target instanceof HTMLElement) {
|
|
65
|
-
return target.ownerDocument.activeElement ?? null
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Window (global or iframe): avoid relying on `instanceof Window` alone (test DOM quirks)
|
|
69
|
-
return (target as Window).document.activeElement ?? null
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Returns whether an event should be ignored because it originated from an
|
|
74
|
-
* input-like element other than the registration target.
|
|
75
|
-
*
|
|
76
|
-
* This checks:
|
|
77
|
-
* - the currently focused element for the listener target
|
|
78
|
-
* - the event's composed path (for shadow DOM)
|
|
79
|
-
* - the event target as a final fallback
|
|
80
|
-
*/
|
|
81
|
-
export function shouldIgnoreInputEvent(
|
|
82
|
-
event: KeyboardEvent,
|
|
83
|
-
listenerTarget: HTMLElement | Document | Window,
|
|
84
|
-
registrationTarget: HTMLElement | Document | Window,
|
|
85
|
-
): boolean {
|
|
86
|
-
const focused = getActiveElementForListenerTarget(listenerTarget)
|
|
87
|
-
if (focused && isInputElement(focused) && focused !== registrationTarget) {
|
|
88
|
-
return true
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
event
|
|
93
|
-
.composedPath()
|
|
94
|
-
.some(
|
|
95
|
-
(element) => isInputElement(element) && element !== registrationTarget,
|
|
96
|
-
)
|
|
97
|
-
) {
|
|
98
|
-
return true
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return isInputElement(event.target) && event.target !== registrationTarget
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Checks if an event is for the given target (originated from or bubbled to it).
|
|
106
|
-
*
|
|
107
|
-
* For document/window targets, also accepts document.documentElement as currentTarget
|
|
108
|
-
* to handle Brave and other browsers where currentTarget may be documentElement
|
|
109
|
-
* instead of document when listeners are attached to document.
|
|
110
|
-
*/
|
|
111
|
-
export function isEventForTarget(
|
|
112
|
-
event: KeyboardEvent,
|
|
113
|
-
target: HTMLElement | Document | Window,
|
|
114
|
-
): boolean {
|
|
115
|
-
// For Document and Window, verify that our handler was indeed called for this target.
|
|
116
|
-
//
|
|
117
|
-
// Browser compatibility note:
|
|
118
|
-
// Per the DOM spec, event.currentTarget should equal the element the listener was
|
|
119
|
-
// attached to. However, some Chromium-based browsers (notably Brave) exhibit
|
|
120
|
-
// non-standard behavior where event.currentTarget is set to document.documentElement
|
|
121
|
-
// (<html>) instead of document when a listener is attached to document.
|
|
122
|
-
// This may be related to privacy/fingerprinting protections.
|
|
123
|
-
//
|
|
124
|
-
// To ensure cross-browser compatibility, we accept both the expected target
|
|
125
|
-
// and document.documentElement as valid currentTarget values.
|
|
126
|
-
// See: https://dom.spec.whatwg.org/#dom-event-currenttarget
|
|
127
|
-
if (target === document || target === window) {
|
|
128
|
-
return (
|
|
129
|
-
event.currentTarget === target ||
|
|
130
|
-
event.currentTarget === document.documentElement
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// For Window, accept window, document, or document.documentElement (browser quirks)
|
|
135
|
-
if (target === window) {
|
|
136
|
-
return (
|
|
137
|
-
event.currentTarget === window ||
|
|
138
|
-
event.currentTarget === document ||
|
|
139
|
-
event.currentTarget === document.documentElement
|
|
140
|
-
)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// For HTMLElement, check if event originated from or bubbled to the element
|
|
144
|
-
if (target instanceof HTMLElement) {
|
|
145
|
-
// Check if the event's currentTarget is the target (capturing/bubbling)
|
|
146
|
-
if (event.currentTarget === target) {
|
|
147
|
-
return true
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Check if the event's target is a descendant of our target
|
|
151
|
-
if (event.target instanceof Node && target.contains(event.target)) {
|
|
152
|
-
return true
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return false
|
|
157
|
-
}
|
package/src/_keyboard-event.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { PUNCTUATION_CODE_MAP, normalizeKeyName } from './constants'
|
|
2
|
-
import type { NormalizedKeyboardEvent } from './key.types'
|
|
3
|
-
|
|
4
|
-
/** Normalizes Unicode key names and modifier flags while retaining physical code and IME state. */
|
|
5
|
-
export function normalizeKeyboardEvent(
|
|
6
|
-
event: KeyboardEvent,
|
|
7
|
-
platform?: 'mac' | 'windows' | 'linux',
|
|
8
|
-
): NormalizedKeyboardEvent {
|
|
9
|
-
let altGraph = false
|
|
10
|
-
try {
|
|
11
|
-
// WebKit reports AltGraph for Option on some macOS keyboard layouts. Option
|
|
12
|
-
// remains a normal Alt shortcut modifier there, so only use this signal on
|
|
13
|
-
// Windows/Linux where AltGraph synthesizes Control+Alt.
|
|
14
|
-
altGraph =
|
|
15
|
-
platform !== 'mac' &&
|
|
16
|
-
typeof event.getModifierState === 'function' &&
|
|
17
|
-
event.getModifierState('AltGraph')
|
|
18
|
-
} catch {
|
|
19
|
-
// Synthetic events and older browsers may not implement this reliably.
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const key = (event.key || '').normalize('NFC')
|
|
23
|
-
return {
|
|
24
|
-
key: normalizeKeyName(key),
|
|
25
|
-
code: event.code || '',
|
|
26
|
-
ctrl: Boolean(event.ctrlKey),
|
|
27
|
-
shift: Boolean(event.shiftKey),
|
|
28
|
-
alt: Boolean(event.altKey),
|
|
29
|
-
meta: Boolean(event.metaKey),
|
|
30
|
-
altGraph,
|
|
31
|
-
location: event.location,
|
|
32
|
-
isComposing:
|
|
33
|
-
Boolean(event.isComposing) || key === 'Process' || key === 'Unidentified',
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Maps familiar positions to compatibility fallback keys for the logical matcher.
|
|
39
|
-
* This assumes conventional positions, not the active layout; never use it to
|
|
40
|
-
* turn recorded physical bindings into logical bindings or display labels.
|
|
41
|
-
*/
|
|
42
|
-
export function codeToLogicalKey(code: string): string | undefined {
|
|
43
|
-
if (/^Key[A-Z]$/i.test(code)) return code.slice(3).toUpperCase()
|
|
44
|
-
if (/^Digit[0-9]$/i.test(code)) return code.slice(5)
|
|
45
|
-
if (/^Numpad[0-9]$/i.test(code)) return code.slice(6)
|
|
46
|
-
|
|
47
|
-
const punctuation = PUNCTUATION_CODE_MAP[code]
|
|
48
|
-
if (punctuation) return punctuation
|
|
49
|
-
|
|
50
|
-
const named: Record<string, string> = {
|
|
51
|
-
NumpadAdd: '+',
|
|
52
|
-
NumpadSubtract: '-',
|
|
53
|
-
NumpadMultiply: '*',
|
|
54
|
-
NumpadDivide: '/',
|
|
55
|
-
NumpadDecimal: '.',
|
|
56
|
-
NumpadEnter: 'Enter',
|
|
57
|
-
Space: 'Space',
|
|
58
|
-
}
|
|
59
|
-
return named[code]
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Tests for one Unicode code point, including characters represented by a surrogate pair. */
|
|
63
|
-
export function isPrintableKey(key: string): boolean {
|
|
64
|
-
return key !== '' && Array.from(key).length === 1
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Splits modifier tokens while retaining a trailing literal plus: `Control++` → `['Control', '+']`. */
|
|
68
|
-
export function splitHotkeyParts(hotkey: string): Array<string> {
|
|
69
|
-
if (!hotkey.endsWith('+')) return hotkey.split('+')
|
|
70
|
-
const parts = hotkey.slice(0, -1).split('+')
|
|
71
|
-
if (parts.at(-1) === '') parts.pop()
|
|
72
|
-
parts.push('+')
|
|
73
|
-
return parts
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Identifies ASCII letters whose layout-produced value takes precedence over positional fallbacks. */
|
|
77
|
-
export function isAsciiLetter(key: string): boolean {
|
|
78
|
-
return /^[A-Za-z]$/.test(key)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Identifies a single Latin-script character, including accented letters. */
|
|
82
|
-
export function isLatinLetter(key: string): boolean {
|
|
83
|
-
return /^\p{Script_Extensions=Latin}$/u.test(key)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Compares letters without case, preserving accents and avoiding expansions such as ß → SS. */
|
|
87
|
-
export function keysEqual(left: string, right: string): boolean {
|
|
88
|
-
if (/^\p{Letter}$/u.test(left) && /^\p{Letter}$/u.test(right)) {
|
|
89
|
-
return left.localeCompare(right, undefined, { sensitivity: 'accent' }) === 0
|
|
90
|
-
}
|
|
91
|
-
return left === right
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Rejects reserved physical-token syntax in logical object fields while allowing Unicode characters. */
|
|
95
|
-
export function assertLogicalKey(key: string): void {
|
|
96
|
-
if (/^\[.*\]$/s.test(key.trim())) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
'Logical key must not contain a bracketed physical code. Use the code field instead.',
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
}
|
package/src/_match.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { detectPlatform } from './platform'
|
|
2
|
-
import {
|
|
3
|
-
codeToLogicalKey,
|
|
4
|
-
isAsciiLetter,
|
|
5
|
-
isLatinLetter,
|
|
6
|
-
isPrintableKey,
|
|
7
|
-
keysEqual,
|
|
8
|
-
normalizeKeyboardEvent,
|
|
9
|
-
} from './_keyboard-event'
|
|
10
|
-
import { parseHotkey } from './parse'
|
|
11
|
-
import type { KeyboardEventMatch } from './match'
|
|
12
|
-
import type { Hotkey, ParsedHotkey } from './hotkey.types'
|
|
13
|
-
|
|
14
|
-
/** Matches an event and reports the winning candidate and release identity. */
|
|
15
|
-
export function matchKeyboardEvent(
|
|
16
|
-
event: KeyboardEvent,
|
|
17
|
-
hotkey: Hotkey | ParsedHotkey,
|
|
18
|
-
platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
|
|
19
|
-
): KeyboardEventMatch {
|
|
20
|
-
const parsed =
|
|
21
|
-
typeof hotkey === 'string' ? parseHotkey(hotkey, platform) : hotkey
|
|
22
|
-
const normalized = normalizeKeyboardEvent(event, platform)
|
|
23
|
-
const noMatch: KeyboardEventMatch = { matched: false, score: 0 }
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
normalized.isComposing &&
|
|
27
|
-
(parsed.code !== undefined || isPrintableKey(parsed.key))
|
|
28
|
-
)
|
|
29
|
-
return noMatch
|
|
30
|
-
if (normalized.altGraph && (parsed.ctrl || parsed.alt)) return noMatch
|
|
31
|
-
|
|
32
|
-
if (parsed.code !== undefined) {
|
|
33
|
-
if (!modifiersMatch(normalized, parsed, false)) return noMatch
|
|
34
|
-
return normalized.code === parsed.code ? matched('code', 3) : noMatch
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const direct = keysEqual(normalized.key, parsed.key)
|
|
38
|
-
const implicitShift =
|
|
39
|
-
direct &&
|
|
40
|
-
isPrintableKey(normalized.key) &&
|
|
41
|
-
!/^\p{Letter}$/u.test(normalized.key) &&
|
|
42
|
-
!parsed.shift
|
|
43
|
-
|
|
44
|
-
if (direct && modifiersMatch(normalized, parsed, implicitShift)) {
|
|
45
|
-
return matched('key', implicitShift ? 2 : 3)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ASCII alphabetic output is authoritative for Dvorak/Colemak/AZERTY.
|
|
49
|
-
// Native Latin letters are also authoritative on Key* positions; Alt is the
|
|
50
|
-
// exception because macOS Option commonly transforms a shortcut's glyph.
|
|
51
|
-
if (
|
|
52
|
-
isAsciiLetter(normalized.key) ||
|
|
53
|
-
(isLatinLetter(normalized.key) &&
|
|
54
|
-
normalized.code.startsWith('Key') &&
|
|
55
|
-
!normalized.alt)
|
|
56
|
-
) {
|
|
57
|
-
return noMatch
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!modifiersMatch(normalized, parsed, false)) return noMatch
|
|
61
|
-
const fallbackKey = codeToLogicalKey(normalized.code)
|
|
62
|
-
return fallbackKey !== undefined && keysEqual(fallbackKey, parsed.key)
|
|
63
|
-
? matched('fallback', 1)
|
|
64
|
-
: noMatch
|
|
65
|
-
|
|
66
|
-
// Preserve the triggering code so keyup can reset a shortcut even if its character changes.
|
|
67
|
-
function matched(
|
|
68
|
-
source: 'key' | 'code' | 'fallback',
|
|
69
|
-
score: 1 | 2 | 3,
|
|
70
|
-
): KeyboardEventMatch {
|
|
71
|
-
return {
|
|
72
|
-
matched: true,
|
|
73
|
-
score,
|
|
74
|
-
source,
|
|
75
|
-
identity: { key: normalized.key, code: normalized.code },
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Compares modifiers, excluding synthetic AltGr flags and optionally allowing Shift used to type a glyph. */
|
|
81
|
-
function modifiersMatch(
|
|
82
|
-
event: ReturnType<typeof normalizeKeyboardEvent>,
|
|
83
|
-
parsed: ParsedHotkey,
|
|
84
|
-
allowImplicitShift: boolean,
|
|
85
|
-
): boolean {
|
|
86
|
-
const ctrl = event.altGraph ? false : event.ctrl
|
|
87
|
-
const alt = event.altGraph ? false : event.alt
|
|
88
|
-
if (
|
|
89
|
-
ctrl !== parsed.ctrl ||
|
|
90
|
-
alt !== parsed.alt ||
|
|
91
|
-
event.meta !== parsed.meta
|
|
92
|
-
) {
|
|
93
|
-
return false
|
|
94
|
-
}
|
|
95
|
-
return event.shift === parsed.shift || (allowImplicitShift && event.shift)
|
|
96
|
-
}
|