@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
package/src/_named-keys.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/** Names defined for both KeyboardEvent.key and KeyboardEvent.code, beyond the basic key groups. */
|
|
2
|
-
export const SHARED_NAMED_KEYS = [
|
|
3
|
-
'CapsLock',
|
|
4
|
-
'ContextMenu',
|
|
5
|
-
'Convert',
|
|
6
|
-
'KanaMode',
|
|
7
|
-
'NonConvert',
|
|
8
|
-
'Help',
|
|
9
|
-
'Insert',
|
|
10
|
-
'NumLock',
|
|
11
|
-
'Fn',
|
|
12
|
-
'FnLock',
|
|
13
|
-
'PrintScreen',
|
|
14
|
-
'ScrollLock',
|
|
15
|
-
'Pause',
|
|
16
|
-
'BrowserBack',
|
|
17
|
-
'BrowserFavorites',
|
|
18
|
-
'BrowserForward',
|
|
19
|
-
'BrowserHome',
|
|
20
|
-
'BrowserRefresh',
|
|
21
|
-
'BrowserSearch',
|
|
22
|
-
'BrowserStop',
|
|
23
|
-
'Eject',
|
|
24
|
-
'LaunchMail',
|
|
25
|
-
'MediaPlayPause',
|
|
26
|
-
'MediaStop',
|
|
27
|
-
'MediaTrackNext',
|
|
28
|
-
'MediaTrackPrevious',
|
|
29
|
-
'Power',
|
|
30
|
-
'AudioVolumeDown',
|
|
31
|
-
'AudioVolumeMute',
|
|
32
|
-
'AudioVolumeUp',
|
|
33
|
-
'WakeUp',
|
|
34
|
-
'Hyper',
|
|
35
|
-
'Super',
|
|
36
|
-
'Again',
|
|
37
|
-
'Copy',
|
|
38
|
-
'Cut',
|
|
39
|
-
'Find',
|
|
40
|
-
'Open',
|
|
41
|
-
'Paste',
|
|
42
|
-
'Props',
|
|
43
|
-
'Select',
|
|
44
|
-
'Undo',
|
|
45
|
-
'Hiragana',
|
|
46
|
-
'Katakana',
|
|
47
|
-
] as const
|
|
48
|
-
|
|
49
|
-
/** Logical counterparts whose physical positions use different names (for example Sleep and Lang1). */
|
|
50
|
-
export const LOGICAL_ONLY_NAMED_KEYS = [
|
|
51
|
-
'Clear',
|
|
52
|
-
'Cancel',
|
|
53
|
-
'Standby',
|
|
54
|
-
'LaunchApplication1',
|
|
55
|
-
'LaunchApplication2',
|
|
56
|
-
'LaunchMediaPlayer',
|
|
57
|
-
'HangulMode',
|
|
58
|
-
'HanjaMode',
|
|
59
|
-
'Zenkaku',
|
|
60
|
-
'Hankaku',
|
|
61
|
-
'ZenkakuHankaku',
|
|
62
|
-
] as const
|
package/src/_recorder-chord.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { normalizeKeyName } from './constants'
|
|
2
|
-
import { normalizeKeyboardEvent } from './_keyboard-event'
|
|
3
|
-
import {
|
|
4
|
-
isModifierKey,
|
|
5
|
-
normalizeHotkeyFromParsed,
|
|
6
|
-
parseKeyboardEvent,
|
|
7
|
-
} from './parse'
|
|
8
|
-
import type {
|
|
9
|
-
RecorderKeyMode,
|
|
10
|
-
RecorderOptions,
|
|
11
|
-
RecorderRejection,
|
|
12
|
-
} from './recorder-options'
|
|
13
|
-
import type { Hotkey } from './hotkey.types'
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Converts a keydown to a replayable chord, preserving physical codes by default.
|
|
17
|
-
* Returns null for composition, modifier-only input, or unusable physical input.
|
|
18
|
-
* Key mode retains the produced character; neither mode guesses layout letters.
|
|
19
|
-
*/
|
|
20
|
-
export function hotkeyChordFromKeydown(
|
|
21
|
-
event: KeyboardEvent,
|
|
22
|
-
platform: 'mac' | 'windows' | 'linux',
|
|
23
|
-
recordBy: RecorderKeyMode = 'code',
|
|
24
|
-
): Hotkey | null {
|
|
25
|
-
const normalized = normalizeKeyboardEvent(event, platform)
|
|
26
|
-
if (
|
|
27
|
-
normalized.isComposing ||
|
|
28
|
-
isModifierKey(normalizeKeyName(event.key)) ||
|
|
29
|
-
event.key === 'AltGraph'
|
|
30
|
-
)
|
|
31
|
-
return null
|
|
32
|
-
if (
|
|
33
|
-
recordBy === 'code' &&
|
|
34
|
-
(!/^[A-Za-z][A-Za-z0-9]*$/.test(event.code) ||
|
|
35
|
-
event.code === 'Unidentified' ||
|
|
36
|
-
normalized.altGraph)
|
|
37
|
-
)
|
|
38
|
-
return null
|
|
39
|
-
const parsed = parseKeyboardEvent(event, platform)
|
|
40
|
-
if (recordBy === 'code') {
|
|
41
|
-
// Remove logical identity rather than placing a physical code in the key field.
|
|
42
|
-
const { key: _key, code: _code, ...modifiers } = parsed
|
|
43
|
-
return normalizeHotkeyFromParsed(
|
|
44
|
-
{ ...modifiers, code: event.code },
|
|
45
|
-
platform,
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
return parsed.key ? normalizeHotkeyFromParsed(parsed, platform) : null
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Explains why a physical chord cannot be recorded before syntax/policy checks.
|
|
53
|
-
* Logical mode deliberately accepts produced characters instead of requiring a code.
|
|
54
|
-
*/
|
|
55
|
-
export function chordRejection(
|
|
56
|
-
event: KeyboardEvent,
|
|
57
|
-
options: RecorderOptions,
|
|
58
|
-
): RecorderRejection | undefined {
|
|
59
|
-
if (options.recordBy === 'key') return undefined
|
|
60
|
-
// AltGr is character entry, often reported as synthetic Ctrl+Alt; macOS Option is handled separately.
|
|
61
|
-
if (normalizeKeyboardEvent(event, options.platform).altGraph)
|
|
62
|
-
return {
|
|
63
|
-
reason: 'alt-graph',
|
|
64
|
-
message:
|
|
65
|
-
'AltGraph character entry cannot be recorded as a physical shortcut. Use key mode for characters.',
|
|
66
|
-
}
|
|
67
|
-
if (
|
|
68
|
-
!/^[A-Za-z][A-Za-z0-9]*$/.test(event.code) ||
|
|
69
|
-
event.code === 'Unidentified'
|
|
70
|
-
)
|
|
71
|
-
return {
|
|
72
|
-
reason: 'missing-code',
|
|
73
|
-
message: 'This event has no usable physical key code.',
|
|
74
|
-
}
|
|
75
|
-
return undefined
|
|
76
|
-
}
|
package/src/_recording-guard.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { MODIFIER_KEYS, normalizeKeyName } from './constants'
|
|
2
|
-
|
|
3
|
-
// Shared only by recorders and dispatchers. Does not own bindings or preferences.
|
|
4
|
-
// Multiple recorder instances may be active; dispatch resumes after the last one ends.
|
|
5
|
-
const sessions = new Set<object>()
|
|
6
|
-
// Remember consumed event objects through the remainder of their DOM dispatch.
|
|
7
|
-
const captured = new WeakSet<KeyboardEvent>()
|
|
8
|
-
// Captured physical identities stay blocked through repeats and their eventual keyup.
|
|
9
|
-
const pending = new Map<string, string>()
|
|
10
|
-
let listening = false
|
|
11
|
-
// Prefer the stable physical identity when modifier release changes event.key.
|
|
12
|
-
const identity = (event: KeyboardEvent) => event.code || `key:${event.key}`
|
|
13
|
-
|
|
14
|
-
/** Consumes recorded key releases and removes guards for completed chords. */
|
|
15
|
-
function release(event: KeyboardEvent): void {
|
|
16
|
-
if (pending.delete(identity(event))) captured.add(event)
|
|
17
|
-
// macOS can swallow the main key's keyup while Command is held.
|
|
18
|
-
// Releasing a modifier ends that chord, as in KeyStateTracker.
|
|
19
|
-
if (MODIFIER_KEYS.has(normalizeKeyName(event.key))) {
|
|
20
|
-
for (const [id, key] of pending) {
|
|
21
|
-
if (!MODIFIER_KEYS.has(key)) pending.delete(id)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
cleanup()
|
|
25
|
-
}
|
|
26
|
-
/** Clears pending releases when focus loss prevents the browser from delivering them. */
|
|
27
|
-
function blur(): void {
|
|
28
|
-
pending.clear()
|
|
29
|
-
cleanup()
|
|
30
|
-
}
|
|
31
|
-
/** Detaches shared listeners once neither active sessions nor pending releases need them. */
|
|
32
|
-
function cleanup(): void {
|
|
33
|
-
if (listening && sessions.size === 0 && pending.size === 0) {
|
|
34
|
-
document.removeEventListener('keyup', release, true)
|
|
35
|
-
window.removeEventListener('blur', blur)
|
|
36
|
-
listening = false
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/** Pauses application shortcut dispatch and starts tracking releases for this recorder. */
|
|
40
|
-
export function beginRecording(owner: object): void {
|
|
41
|
-
sessions.add(owner)
|
|
42
|
-
if (!listening && typeof document !== 'undefined') {
|
|
43
|
-
document.addEventListener('keyup', release, true)
|
|
44
|
-
window.addEventListener('blur', blur)
|
|
45
|
-
listening = true
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/** Ends a session while retaining guards for keys still held after commit or cancellation. */
|
|
49
|
-
export function endRecording(owner: object): void {
|
|
50
|
-
sessions.delete(owner)
|
|
51
|
-
cleanup()
|
|
52
|
-
}
|
|
53
|
-
/** Marks a consumed keydown and guards subsequent repeats and its release. */
|
|
54
|
-
export function captureRecordingEvent(event: KeyboardEvent): void {
|
|
55
|
-
captured.add(event)
|
|
56
|
-
pending.set(identity(event), normalizeKeyName(event.key))
|
|
57
|
-
}
|
|
58
|
-
/** Whether managers should suppress callbacks for this recording session or captured chord. */
|
|
59
|
-
export function isRecordingEvent(event: KeyboardEvent): boolean {
|
|
60
|
-
return (
|
|
61
|
-
sessions.size > 0 || captured.has(event) || pending.has(identity(event))
|
|
62
|
-
)
|
|
63
|
-
}
|
package/src/_registration.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import type { ConflictBehavior, ParsedHotkey } from './hotkey.types'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Default options for hotkey/sequence registration.
|
|
5
|
-
* Omitted: platform, target (resolved at registration), requireReset (HotkeyManager only).
|
|
6
|
-
*/
|
|
7
|
-
export const defaultHotkeyOptions = {
|
|
8
|
-
preventDefault: true,
|
|
9
|
-
stopPropagation: true,
|
|
10
|
-
eventType: 'keydown' as const,
|
|
11
|
-
enabled: true,
|
|
12
|
-
ignoreInputs: true,
|
|
13
|
-
conflictBehavior: 'warn' as ConflictBehavior,
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Computes the default ignoreInputs value based on the hotkey.
|
|
18
|
-
* Ctrl/Meta shortcuts and Escape fire in inputs; single keys and Shift/Alt combos are ignored.
|
|
19
|
-
*/
|
|
20
|
-
export function getDefaultIgnoreInputs(parsedHotkey: ParsedHotkey): boolean {
|
|
21
|
-
if (parsedHotkey.ctrl || parsedHotkey.meta) return false // Mod+S, Ctrl+C, etc.
|
|
22
|
-
if (parsedHotkey.key === 'Escape' || parsedHotkey.code === 'Escape')
|
|
23
|
-
return false // Close modal, etc.
|
|
24
|
-
return true // Single keys, Shift+key, Alt+key
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Handles conflicts between registrations based on conflict behavior.
|
|
29
|
-
*
|
|
30
|
-
* @param conflictingId - The ID of the conflicting registration
|
|
31
|
-
* @param keyDisplay - Display string for the conflicting key/sequence (for error messages)
|
|
32
|
-
* @param conflictBehavior - How to handle the conflict
|
|
33
|
-
* @param unregister - Function to unregister by ID
|
|
34
|
-
*/
|
|
35
|
-
export function handleConflict(
|
|
36
|
-
conflictingId: string,
|
|
37
|
-
keyDisplay: string,
|
|
38
|
-
conflictBehavior: ConflictBehavior,
|
|
39
|
-
unregister: (id: string) => void,
|
|
40
|
-
): void {
|
|
41
|
-
if (conflictBehavior === 'allow') {
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (conflictBehavior === 'warn') {
|
|
46
|
-
console.warn(
|
|
47
|
-
`'${keyDisplay}' is already registered. Multiple handlers will be triggered. ` +
|
|
48
|
-
`Use conflictBehavior: 'replace' to replace the existing handler, ` +
|
|
49
|
-
`or conflictBehavior: 'allow' to suppress this warning.`,
|
|
50
|
-
)
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (conflictBehavior === 'error') {
|
|
55
|
-
throw new Error(
|
|
56
|
-
`'${keyDisplay}' is already registered. ` +
|
|
57
|
-
`Use conflictBehavior: 'replace' to replace the existing handler, ` +
|
|
58
|
-
`or conflictBehavior: 'allow' to allow multiple registrations.`,
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// At this point, conflictBehavior must be 'replace'
|
|
63
|
-
unregister(conflictingId)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Compare updates without publishing unchanged options, including fresh metadata objects. */
|
|
67
|
-
export function optionsEqual(left: object, right: object): boolean {
|
|
68
|
-
const a = left as Record<string, unknown>
|
|
69
|
-
const b = right as Record<string, unknown>
|
|
70
|
-
const keys = Object.keys(a)
|
|
71
|
-
return (
|
|
72
|
-
keys.length === Object.keys(b).length &&
|
|
73
|
-
keys.every((key) =>
|
|
74
|
-
key === 'meta' && a[key] && b[key]
|
|
75
|
-
? optionsEqual(a[key] as object, b[key] as object)
|
|
76
|
-
: Object.is(a[key], b[key]),
|
|
77
|
-
)
|
|
78
|
-
)
|
|
79
|
-
}
|
package/src/conflicts.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { detectPlatform } from './platform'
|
|
2
|
-
import { parseRegisterableHotkey } from './parse'
|
|
3
|
-
import { keysEqual } from './_keyboard-event'
|
|
4
|
-
import { matchesKeyboardEvent } from './match'
|
|
5
|
-
import { getHotkeyManager, toHotkeyRegistrationView } from './hotkey-manager'
|
|
6
|
-
import { getSequenceManager } from './sequence-manager'
|
|
7
|
-
import type { HotkeyRegistrationView } from './hotkey-manager'
|
|
8
|
-
import type { SequenceRegistrationView } from './sequence-manager'
|
|
9
|
-
import type { RegisterableHotkey } from './hotkey.types'
|
|
10
|
-
|
|
11
|
-
export type HotkeyConflict =
|
|
12
|
-
| { type: 'hotkey'; registration: HotkeyRegistrationView }
|
|
13
|
-
| { type: 'sequence'; registration: SequenceRegistrationView }
|
|
14
|
-
|
|
15
|
-
export interface HotkeyConflictOptions {
|
|
16
|
-
/** Intended registration target. Defaults to document; disjoint targets are excluded. */
|
|
17
|
-
target?: HTMLElement | Document | Window
|
|
18
|
-
/** Check all targets instead of overlapping targets. Default: overlapping. */
|
|
19
|
-
scope?: 'overlapping' | 'all'
|
|
20
|
-
/** Intended event type. Default: keydown. */
|
|
21
|
-
eventType?: 'keydown' | 'keyup'
|
|
22
|
-
/** Include disabled registrations. Default: false. */
|
|
23
|
-
includeDisabled?: boolean
|
|
24
|
-
/** IDs of registrations being edited. */
|
|
25
|
-
excludeIds?: ReadonlyArray<string>
|
|
26
|
-
/** Additional app-specific exclusions, such as all instances of one action. */
|
|
27
|
-
exclude?: (
|
|
28
|
-
registration: HotkeyRegistrationView | SequenceRegistrationView,
|
|
29
|
-
) => boolean
|
|
30
|
-
/** Platform used to resolve Mod in the candidate. Registrations retain their own platform. */
|
|
31
|
-
platform?: 'mac' | 'windows' | 'linux'
|
|
32
|
-
/** Source events allow detecting physical/logical overlap on the recorded layout. */
|
|
33
|
-
events?: ReadonlyArray<KeyboardEvent>
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Whether two targets share a document and one contains the other (or is its window). */
|
|
37
|
-
function targetsOverlap(
|
|
38
|
-
a: HTMLElement | Document | Window,
|
|
39
|
-
b: HTMLElement | Document | Window,
|
|
40
|
-
): boolean {
|
|
41
|
-
if (a === b) return true
|
|
42
|
-
// Resolve the owning document without relying on cross-frame instanceof checks.
|
|
43
|
-
const documentOf = (target: typeof a): Document | null => {
|
|
44
|
-
if ('document' in target) return target.document
|
|
45
|
-
if (target.nodeType === 9) return target as Document
|
|
46
|
-
return target.ownerDocument
|
|
47
|
-
}
|
|
48
|
-
if (documentOf(a) !== documentOf(b)) return false
|
|
49
|
-
if (!('contains' in a) || !('contains' in b)) return true
|
|
50
|
-
return a.contains(b as Node) || b.contains(a as Node)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Find equivalent bindings and sequence-prefix conflicts in the live registry.
|
|
55
|
-
* With source events, also checks observed logical/physical overlap. Does not
|
|
56
|
-
* discover unmounted handlers, external listeners, or other keyboard layouts.
|
|
57
|
-
*/
|
|
58
|
-
export function findHotkeyConflicts(
|
|
59
|
-
candidate: RegisterableHotkey | ReadonlyArray<RegisterableHotkey>,
|
|
60
|
-
options: HotkeyConflictOptions = {},
|
|
61
|
-
): Array<HotkeyConflict> {
|
|
62
|
-
const steps: ReadonlyArray<RegisterableHotkey> = Array.isArray(candidate)
|
|
63
|
-
? candidate
|
|
64
|
-
: [candidate as RegisterableHotkey]
|
|
65
|
-
if (steps.length === 0) return []
|
|
66
|
-
const platform = options.platform ?? detectPlatform()
|
|
67
|
-
const parsed = steps.map((step) => parseRegisterableHotkey(step, platform))
|
|
68
|
-
const target =
|
|
69
|
-
options.target ?? (typeof document === 'undefined' ? undefined : document)
|
|
70
|
-
const conflicts: Array<HotkeyConflict> = []
|
|
71
|
-
// Apply scope policy before comparing equivalent bindings or observed event overlap.
|
|
72
|
-
const matches = (
|
|
73
|
-
registration: HotkeyRegistrationView | SequenceRegistrationView,
|
|
74
|
-
bindings: ReadonlyArray<RegisterableHotkey>,
|
|
75
|
-
) => {
|
|
76
|
-
if (
|
|
77
|
-
(!options.includeDisabled && registration.options.enabled === false) ||
|
|
78
|
-
(registration.options.eventType ?? 'keydown') !==
|
|
79
|
-
(options.eventType ?? 'keydown') ||
|
|
80
|
-
options.excludeIds?.includes(registration.id) ||
|
|
81
|
-
options.exclude?.(registration) ||
|
|
82
|
-
(options.scope !== 'all' &&
|
|
83
|
-
target &&
|
|
84
|
-
!targetsOverlap(target, registration.target))
|
|
85
|
-
)
|
|
86
|
-
return false
|
|
87
|
-
// Compare only the common prefix: a shorter sequence can collide with a longer one.
|
|
88
|
-
return parsed
|
|
89
|
-
.slice(0, Math.min(parsed.length, bindings.length))
|
|
90
|
-
.every((a, index) => {
|
|
91
|
-
const b = parseRegisterableHotkey(
|
|
92
|
-
bindings[index]!,
|
|
93
|
-
registration.options.platform,
|
|
94
|
-
)
|
|
95
|
-
const same =
|
|
96
|
-
a.ctrl === b.ctrl &&
|
|
97
|
-
a.alt === b.alt &&
|
|
98
|
-
a.shift === b.shift &&
|
|
99
|
-
a.meta === b.meta &&
|
|
100
|
-
(a.code !== undefined || b.code !== undefined
|
|
101
|
-
? a.code === b.code
|
|
102
|
-
: keysEqual(a.key, b.key))
|
|
103
|
-
// A source event can establish code/key overlap without guessing its layout.
|
|
104
|
-
const event = options.events?.[index]
|
|
105
|
-
return (
|
|
106
|
-
same ||
|
|
107
|
-
(event !== undefined &&
|
|
108
|
-
matchesKeyboardEvent(event, a, platform) &&
|
|
109
|
-
matchesKeyboardEvent(event, b, registration.options.platform))
|
|
110
|
-
)
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
for (const registration of getHotkeyManager().registrations.state.values()) {
|
|
114
|
-
if (matches(registration, [registration.hotkey]))
|
|
115
|
-
conflicts.push({
|
|
116
|
-
type: 'hotkey',
|
|
117
|
-
registration: toHotkeyRegistrationView(registration),
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
for (const registration of getSequenceManager().registrations.state.values()) {
|
|
121
|
-
if (matches(registration, registration.sequence))
|
|
122
|
-
conflicts.push({ type: 'sequence', registration })
|
|
123
|
-
}
|
|
124
|
-
return conflicts
|
|
125
|
-
}
|