@inklu/keys 2.0.2 → 2.2.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/LICENSE +1 -1
- package/README.md +209 -66
- package/dist/attributes.d.mts +11 -2
- package/dist/attributes.mjs +22 -24
- package/dist/index-DRXMJmbc.d.mts +173 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -3
- package/dist/react.d.mts +27 -28
- package/dist/react.mjs +125 -130
- package/dist/src-TaZw4V61.mjs +1797 -0
- package/dist/types-D-hUKGoN.d.mts +357 -0
- package/package.json +5 -5
- package/dist/attributes-DotLr2MW.d.mts +0 -426
- package/dist/index-BTgscYdI.d.mts +0 -116
- package/dist/src-DgYjA1oS.mjs +0 -1697
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { C as SequenceBinding, D as Chord, T as SerializedKeymap, a as CommandDefinition, d as Conflict, g as KeymapStore, k as Platform, l as Commands, m as Json, p as DisplayOptions, r as Command, t as Binding, u as CommandsOptions } from "./types-D-hUKGoN.mjs";
|
|
2
|
+
//#region src/bindings.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Creates a sequence binding from ordered chords.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* keys: seq("G", "I")
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
declare function seq(...steps: Array<Chord>): SequenceBinding;
|
|
12
|
+
/**
|
|
13
|
+
* Whether every step is a printable character with no Control, Alt or Meta:
|
|
14
|
+
* the "character key shortcuts" WCAG 2.1.4 lets users turn off.
|
|
15
|
+
*/
|
|
16
|
+
declare function isSingleKeyBinding(binding: Binding): boolean;
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/display.d.ts
|
|
19
|
+
/**
|
|
20
|
+
* Formats a binding the way its platform writes shortcuts: `⇧⌘S` on a Mac,
|
|
21
|
+
* `Ctrl+Shift+S` elsewhere. The steps of a sequence are set apart by a space.
|
|
22
|
+
*/
|
|
23
|
+
declare function formatBinding(binding: Binding, options?: DisplayOptions): string;
|
|
24
|
+
/**
|
|
25
|
+
* Splits a binding into display tokens for rendering `<kbd>` elements: one
|
|
26
|
+
* per key of a chord, one per step of a sequence.
|
|
27
|
+
*/
|
|
28
|
+
declare function bindingTokens(binding: Binding, options?: DisplayOptions): Array<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Phrases a binding for a screen reader: `"Command K"` for a chord,
|
|
31
|
+
* `"G then I"` for a sequence, which `aria-keyshortcuts` cannot express.
|
|
32
|
+
*/
|
|
33
|
+
declare function spokenBinding(binding: Binding, platform?: Platform): string;
|
|
34
|
+
/**
|
|
35
|
+
* The `aria-keyshortcuts` value for a chord, as in `"Meta+K"`. Empty for a
|
|
36
|
+
* sequence and for a chord that cannot be read.
|
|
37
|
+
*/
|
|
38
|
+
declare function ariaKeyShortcuts(binding: Binding, platform?: Platform): string;
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/persist.d.ts
|
|
41
|
+
type WebStorage = Pick<Storage, "getItem" | "setItem" | "removeItem">;
|
|
42
|
+
/**
|
|
43
|
+
* Keeps user overrides in Web Storage under `key`, and follows edits made in
|
|
44
|
+
* other tabs. `storage` is only called in the browser; return
|
|
45
|
+
* `sessionStorage` or an object of your own to keep them somewhere else.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* createCommands(definitions, { persist: webStorage("my-app.keys") });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare function webStorage(key?: string, storage?: () => WebStorage | null): KeymapStore;
|
|
53
|
+
/** A registry's user overrides as JSON-safe data, for your own storage or another device. */
|
|
54
|
+
declare function exportKeymap<Id extends string>(commands: Pick<Commands<Id>, "keymap">): SerializedKeymap;
|
|
55
|
+
/**
|
|
56
|
+
* Replaces a registry's user overrides with exported data, and saves them.
|
|
57
|
+
* Data in another format changes nothing.
|
|
58
|
+
*/
|
|
59
|
+
declare function importKeymap<Id extends string>(commands: Pick<Commands<Id>, "keymap">, data: Json): void;
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/rebind.d.ts
|
|
62
|
+
/** Hotkey capture mode: single `"chord"` or multi-step `"sequence"`. */
|
|
63
|
+
type RebindMode = "chord" | "sequence";
|
|
64
|
+
/** Current state of a rebinding session. */
|
|
65
|
+
interface RebindState<Id extends string = string> {
|
|
66
|
+
/** Whether keys are actively being recorded. */
|
|
67
|
+
readonly recording: boolean;
|
|
68
|
+
/** Most recently captured binding. */
|
|
69
|
+
readonly binding: Binding | null;
|
|
70
|
+
/** Captured steps in the current sequence session. */
|
|
71
|
+
readonly steps: ReadonlyArray<Chord>;
|
|
72
|
+
/** What stands in the captured binding's way: other commands using it, or the platform reserving it. */
|
|
73
|
+
readonly conflicts: ReadonlyArray<Conflict<Id>>;
|
|
74
|
+
}
|
|
75
|
+
/** Options for creating a rebinder. */
|
|
76
|
+
interface RebindOptions<Id extends string = string> {
|
|
77
|
+
/** Capture mode. Defaults to `"chord"`. */
|
|
78
|
+
mode?: RebindMode;
|
|
79
|
+
/** Whether to write the recorded (or cleared) binding into the keymap. Defaults to `true`. */
|
|
80
|
+
apply?: boolean;
|
|
81
|
+
/** Whether to append rather than replace existing bindings. Defaults to `false`. */
|
|
82
|
+
add?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Prevent applying bindings that conflict with other commands. Defaults to
|
|
85
|
+
* `false`. A binding the browser or operating system reserves is never applied.
|
|
86
|
+
*/
|
|
87
|
+
rejectConflicts?: boolean;
|
|
88
|
+
/** Ignore key events from inputs while recording. Defaults to `true`. */
|
|
89
|
+
ignoreInputs?: boolean;
|
|
90
|
+
/** Inactivity timeout in milliseconds before a sequence commits. */
|
|
91
|
+
idleTimeout?: number;
|
|
92
|
+
/** Callback fired when a binding is recorded. */
|
|
93
|
+
onRecord?: (binding: Binding, conflicts: ReadonlyArray<Conflict<Id>>) => void;
|
|
94
|
+
/** Callback fired when recording is cancelled. */
|
|
95
|
+
onCancel?: () => void;
|
|
96
|
+
/** Callback fired when Backspace or Delete clears the binding while recording. */
|
|
97
|
+
onClear?: () => void;
|
|
98
|
+
}
|
|
99
|
+
/** Controller instance for managing a rebinding session. */
|
|
100
|
+
interface Rebinder<Id extends string = string> {
|
|
101
|
+
/** Begins capturing keys. */
|
|
102
|
+
start: () => void;
|
|
103
|
+
/** Stops capturing keys without recording. */
|
|
104
|
+
stop: () => void;
|
|
105
|
+
/** Stops capturing keys and reports cancellation. */
|
|
106
|
+
cancel: () => void;
|
|
107
|
+
/** Commits the currently captured sequence steps. */
|
|
108
|
+
commit: () => void;
|
|
109
|
+
/** Current rebinder state. */
|
|
110
|
+
readonly state: RebindState<Id>;
|
|
111
|
+
/** Subscribes to rebinder state changes. Returns an unsubscribe function. */
|
|
112
|
+
subscribe: (listener: () => void) => () => void;
|
|
113
|
+
/** Destroys the rebinder and cleans up listeners. */
|
|
114
|
+
destroy: () => void;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Creates a rebinder for recording a command's keys. While it records, no
|
|
118
|
+
* command runs. Escape cancels, Backspace removes the shortcut (or the last
|
|
119
|
+
* step of a sequence), and Enter finishes a sequence.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* const rebinder = createRebinder(commands, "palette.open", {
|
|
124
|
+
* onRecord: (binding, conflicts) => showConflicts(conflicts),
|
|
125
|
+
* });
|
|
126
|
+
* rebinder.start();
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
declare function createRebinder<Id extends string>(registry: Commands<Id>, id: Id, options?: RebindOptions<Id>): Rebinder<Id>;
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/registry.d.ts
|
|
132
|
+
/**
|
|
133
|
+
* Creates a command registry.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* export const commands = createCommands({
|
|
138
|
+
* "palette.open": { title: "Open command palette", keys: "Mod+K" },
|
|
139
|
+
* "nav.inbox": {
|
|
140
|
+
* title: "Go to inbox",
|
|
141
|
+
* keys: seq("G", "I"),
|
|
142
|
+
* run: () => router.navigate("/inbox"),
|
|
143
|
+
* },
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
declare function createCommands<Id extends string>(definitions: { [K in Id]: CommandDefinition<NoInfer<Id>>; }, options?: CommandsOptions<NoInfer<Id>>): Commands<Id>;
|
|
148
|
+
/**
|
|
149
|
+
* Extracts command ID union from a Commands registry type.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* export const commands = createCommands({ … });
|
|
154
|
+
* export type AppCommandId = CommandId<typeof commands>;
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
type CommandId<C> = C extends Commands<infer Id> ? Id : never;
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/search.d.ts
|
|
160
|
+
/**
|
|
161
|
+
* Filters commands for a search query, leaving out `hidden` ones. Every word
|
|
162
|
+
* must appear in the title, description, group, keywords, id or displayed
|
|
163
|
+
* shortcut. Titles starting with the query come first, then titles containing
|
|
164
|
+
* every word, then the rest. An empty query returns every visible command.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```ts
|
|
168
|
+
* const results = matchCommands(commands.list(), "go inbox");
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
declare function matchCommands<Id extends string>(commands: ReadonlyArray<Command<Id>>, query: string): Array<Command<Id>>;
|
|
172
|
+
//#endregion
|
|
173
|
+
export { seq as _, RebindOptions as a, createRebinder as c, webStorage as d, ariaKeyShortcuts as f, isSingleKeyBinding as g, spokenBinding as h, RebindMode as i, exportKeymap as l, formatBinding as m, CommandId as n, RebindState as o, bindingTokens as p, createCommands as r, Rebinder as s, matchCommands as t, importKeymap as u };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
export {
|
|
1
|
+
import { C as SequenceBinding, D as Chord, E as SerializedOverride, O as Key, S as ScopeOptions, T as SerializedKeymap, _ as Override, a as CommandDefinition, b as RunBlockReason, c as CommandSource, d as Conflict, f as ConflictOptions, g as KeymapStore, h as KeyCapture, i as CommandContext, k as Platform, l as Commands, m as Json, n as BindingInput, o as CommandHandler, p as DisplayOptions, r as Command, s as CommandKeymap, t as Binding, u as CommandsOptions, v as PendingSequence, w as SerializedBinding, x as RunOptions, y as PlatformBindings } from "./types-D-hUKGoN.mjs";
|
|
2
|
+
import { _ as seq, a as RebindOptions, c as createRebinder, d as webStorage, f as ariaKeyShortcuts, g as isSingleKeyBinding, h as spokenBinding, i as RebindMode, l as exportKeymap, m as formatBinding, n as CommandId, o as RebindState, p as bindingTokens, r as createCommands, s as Rebinder, t as matchCommands, u as importKeymap } from "./index-DRXMJmbc.mjs";
|
|
3
|
+
export { type Binding, type BindingInput, type Chord, type Command, type CommandContext, type CommandDefinition, type CommandHandler, type CommandId, type CommandKeymap, type CommandSource, type Commands, type CommandsOptions, type Conflict, type ConflictOptions, type DisplayOptions, type Json, type Key, type KeyCapture, type KeymapStore, type Override, type PendingSequence, type Platform, type PlatformBindings, type RebindMode, type RebindOptions, type RebindState, type Rebinder, type RunBlockReason, type RunOptions, type ScopeOptions, type SequenceBinding, type SerializedBinding, type SerializedKeymap, type SerializedOverride, ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, exportKeymap, formatBinding, importKeymap, isSingleKeyBinding, matchCommands, seq, spokenBinding, webStorage };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export { ALWAYS_SCOPE, ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, formatBinding, isSingleKeyBinding, matchCommands, observe, seq, spokenBinding, validateBinding };
|
|
1
|
+
import { a as importKeymap, c as bindingTokens, d as isSingleKeyBinding, f as seq, i as exportKeymap, l as formatBinding, n as createCommands, o as webStorage, r as createRebinder, s as ariaKeyShortcuts, t as matchCommands, u as spokenBinding } from "./src-TaZw4V61.mjs";
|
|
2
|
+
export { ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, exportKeymap, formatBinding, importKeymap, isSingleKeyBinding, matchCommands, seq, spokenBinding, webStorage };
|
package/dist/react.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
1
|
+
import { C as SequenceBinding, D as Chord, E as SerializedOverride, O as Key, S as ScopeOptions, T as SerializedKeymap, _ as Override, a as CommandDefinition, b as RunBlockReason, c as CommandSource, d as Conflict, f as ConflictOptions, g as KeymapStore, h as KeyCapture, i as CommandContext, k as Platform, l as Commands, m as Json, n as BindingInput, o as CommandHandler, p as DisplayOptions, r as Command, s as CommandKeymap, t as Binding, u as CommandsOptions, v as PendingSequence, w as SerializedBinding, x as RunOptions, y as PlatformBindings } from "./types-D-hUKGoN.mjs";
|
|
2
|
+
import { _ as seq, a as RebindOptions, c as createRebinder, d as webStorage, f as ariaKeyShortcuts, g as isSingleKeyBinding, h as spokenBinding, i as RebindMode, l as exportKeymap, m as formatBinding, n as CommandId, o as RebindState, p as bindingTokens, r as createCommands, s as Rebinder, t as matchCommands, u as importKeymap } from "./index-DRXMJmbc.mjs";
|
|
3
3
|
//#region src/react/hooks.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Mounts a commands registry for the lifecycle of the component.
|
|
@@ -13,9 +13,13 @@ import { a as RebindState, c as BindingValidation, d as validateBinding, i as Re
|
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
15
|
export declare function useMountCommands<Id extends string>(registry: Commands<Id>): void;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Returns all registered commands, re-rendering when the registry changes.
|
|
18
|
+
* Hydrates against `serverList()`, so a subtree that hydrates late still
|
|
19
|
+
* matches the server's HTML.
|
|
20
|
+
*/
|
|
17
21
|
export declare function useCommands<Id extends string>(registry: Commands<Id>): ReadonlyArray<Command<Id>>;
|
|
18
|
-
/** Returns
|
|
22
|
+
/** Returns one command's snapshot, re-rendering only when that command changes. */
|
|
19
23
|
export declare function useCommand<Id extends string>(registry: Commands<Id>, id: NoInfer<Id>): Command<Id> | undefined;
|
|
20
24
|
/**
|
|
21
25
|
* Returns display and accessibility shortcuts for a command's primary
|
|
@@ -29,7 +33,9 @@ export declare function useShortcut<Id extends string>(registry: Commands<Id>, i
|
|
|
29
33
|
spoken: string;
|
|
30
34
|
};
|
|
31
35
|
/**
|
|
32
|
-
* Activates a scope for the lifecycle of the component.
|
|
36
|
+
* Activates a scope for the lifecycle of the component. Scopes rank in the
|
|
37
|
+
* order React runs effects in: of a parent and a child opening in the same
|
|
38
|
+
* render, the child is pushed first.
|
|
33
39
|
*
|
|
34
40
|
* @example
|
|
35
41
|
* ```tsx
|
|
@@ -37,8 +43,19 @@ export declare function useShortcut<Id extends string>(registry: Commands<Id>, i
|
|
|
37
43
|
* ```
|
|
38
44
|
*/
|
|
39
45
|
export declare function useScope<Id extends string>(registry: Commands<Id>, name: string, active?: boolean, options?: ScopeOptions): void;
|
|
40
|
-
/**
|
|
41
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Attaches what a command does for the lifecycle of the component. The latest
|
|
48
|
+
* `run` is always the one called, and the handler attached last wins. While
|
|
49
|
+
* `enabled` is `false` the command falls to the handler attached before it,
|
|
50
|
+
* or cannot run if there is none.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* useCommandHandler(commands, "palette.open", () => setOpen(true));
|
|
55
|
+
* useCommandHandler(commands, "doc.save", save, dirty);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function useCommandHandler<Id extends string>(registry: Commands<Id>, id: NoInfer<Id>, run: CommandHandler<NoInfer<Id>>, enabled?: boolean): void;
|
|
42
59
|
/** State and actions for rebinding a command. */
|
|
43
60
|
interface UseRebindResult<Id extends string = string> extends RebindState<Id> {
|
|
44
61
|
/** Active display string for the shortcut. */
|
|
@@ -56,27 +73,9 @@ interface UseRebindResult<Id extends string = string> extends RebindState<Id> {
|
|
|
56
73
|
}
|
|
57
74
|
/** Hook for recording and configuring a command's keybindings. */
|
|
58
75
|
export declare function useRebind<Id extends string>(registry: Commands<Id>, id: NoInfer<Id>, options?: RebindOptions<Id>): UseRebindResult<Id>;
|
|
59
|
-
/** Returns
|
|
76
|
+
/** Returns the keys held down right now, by the name `KeyboardEvent.key` gives each. */
|
|
60
77
|
export declare function useHeldKeys(): ReadonlyArray<string>;
|
|
61
|
-
/**
|
|
62
|
-
* Checks whether a specific key is currently held down.
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* ```tsx
|
|
66
|
-
* const shift = useKeyHold("Shift");
|
|
67
|
-
* ```
|
|
68
|
-
*/
|
|
69
|
-
export declare function useKeyHold(key: string): boolean;
|
|
70
|
-
/** Returns a map of held key identifiers to physical event codes. */
|
|
71
|
-
export declare function useHeldKeyCodes(): Readonly<Record<string, string>>;
|
|
72
|
-
/**
|
|
73
|
-
* Returns currently active partially-typed sequences.
|
|
74
|
-
*
|
|
75
|
-
* @example
|
|
76
|
-
* ```tsx
|
|
77
|
-
* const pending = usePendingSequences(commands);
|
|
78
|
-
* ```
|
|
79
|
-
*/
|
|
78
|
+
/** Returns the half-typed sequences, for a hint such as "G …". */
|
|
80
79
|
export declare function usePendingSequences<Id extends string>(registry: Commands<Id>): ReadonlyArray<PendingSequence<Id>>;
|
|
81
80
|
//#endregion
|
|
82
|
-
export {
|
|
81
|
+
export { type Binding, type BindingInput, type Chord, type Command, type CommandContext, type CommandDefinition, type CommandHandler, type CommandId, type CommandKeymap, type CommandSource, type Commands, type CommandsOptions, type Conflict, type ConflictOptions, type DisplayOptions, type Json, type Key, type KeyCapture, type KeymapStore, type Override, type PendingSequence, type Platform, type PlatformBindings, type RebindMode, type RebindOptions, type RebindState, type Rebinder, type RunBlockReason, type RunOptions, type ScopeOptions, type SequenceBinding, type SerializedBinding, type SerializedKeymap, type SerializedOverride, type UseRebindResult, ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, exportKeymap, formatBinding, importKeymap, isSingleKeyBinding, matchCommands, seq, spokenBinding, webStorage };
|
package/dist/react.mjs
CHANGED
|
@@ -1,16 +1,66 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { a as importKeymap, c as bindingTokens, d as isSingleKeyBinding, f as seq, i as exportKeymap, l as formatBinding, m as keyName, n as createCommands, o as webStorage, p as isModifierKey, r as createRebinder, s as ariaKeyShortcuts, t as matchCommands, u as spokenBinding } from "./src-TaZw4V61.mjs";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
4
|
+
//#region src/held.ts
|
|
5
|
+
/** Which keys are down right now. Listens only while something is subscribed. */
|
|
6
|
+
const NONE = [];
|
|
7
|
+
/** Held keys by physical key, so two keys producing one character stay apart. */
|
|
8
|
+
const down = /* @__PURE__ */ new Map();
|
|
9
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
10
|
+
let held = NONE;
|
|
11
|
+
const identify = (event) => event.code || `key:${keyName(event.key)}`;
|
|
12
|
+
function publish() {
|
|
13
|
+
held = down.size === 0 ? NONE : [...new Set(down.values())];
|
|
14
|
+
for (const listener of listeners) listener();
|
|
15
|
+
}
|
|
16
|
+
function onKeyDown(event) {
|
|
17
|
+
if (!event.key || down.has(identify(event))) return;
|
|
18
|
+
down.set(identify(event), keyName(event.key));
|
|
19
|
+
publish();
|
|
20
|
+
}
|
|
21
|
+
function onKeyUp(event) {
|
|
22
|
+
down.delete(identify(event));
|
|
23
|
+
if (event.key === "Meta") {
|
|
24
|
+
for (const [identity, key] of down) if (!isModifierKey(key)) down.delete(identity);
|
|
25
|
+
}
|
|
26
|
+
publish();
|
|
27
|
+
}
|
|
28
|
+
/** Leaving the window or hiding the page swallows the keyups, so nothing is held any more. */
|
|
29
|
+
function onLeave() {
|
|
30
|
+
if (down.size === 0) return;
|
|
31
|
+
down.clear();
|
|
32
|
+
publish();
|
|
33
|
+
}
|
|
34
|
+
/** The keys held right now, by name. Empty unless something is subscribed. */
|
|
35
|
+
function heldKeys() {
|
|
36
|
+
return held;
|
|
37
|
+
}
|
|
38
|
+
/** Watches held keys until the returned function is called. */
|
|
39
|
+
function subscribeHeldKeys(listener) {
|
|
40
|
+
listeners.add(listener);
|
|
41
|
+
if (listeners.size === 1) {
|
|
42
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
43
|
+
document.addEventListener("keyup", onKeyUp, true);
|
|
44
|
+
document.addEventListener("visibilitychange", onLeave);
|
|
45
|
+
window.addEventListener("blur", onLeave);
|
|
46
|
+
}
|
|
47
|
+
return () => {
|
|
48
|
+
listeners.delete(listener);
|
|
49
|
+
if (listeners.size > 0) return;
|
|
50
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
51
|
+
document.removeEventListener("keyup", onKeyUp, true);
|
|
52
|
+
document.removeEventListener("visibilitychange", onLeave);
|
|
53
|
+
window.removeEventListener("blur", onLeave);
|
|
54
|
+
down.clear();
|
|
55
|
+
held = NONE;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
6
59
|
//#region src/react/hooks.ts
|
|
7
60
|
const EMPTY_TOKENS = [];
|
|
8
|
-
const EMPTY_CODES = {};
|
|
9
61
|
const EMPTY_PENDING = [];
|
|
10
62
|
const getEmptyTokens = () => EMPTY_TOKENS;
|
|
11
|
-
const getEmptyCodes = () => EMPTY_CODES;
|
|
12
63
|
const getEmptyPending = () => EMPTY_PENDING;
|
|
13
|
-
const getFalse = () => false;
|
|
14
64
|
/**
|
|
15
65
|
* Mounts a commands registry for the lifecycle of the component.
|
|
16
66
|
*
|
|
@@ -25,20 +75,23 @@ const getFalse = () => false;
|
|
|
25
75
|
function useMountCommands(registry) {
|
|
26
76
|
useEffect(() => registry.mount(), [registry]);
|
|
27
77
|
}
|
|
28
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Returns all registered commands, re-rendering when the registry changes.
|
|
80
|
+
* Hydrates against `serverList()`, so a subtree that hydrates late still
|
|
81
|
+
* matches the server's HTML.
|
|
82
|
+
*/
|
|
83
|
+
function useCommands(registry) {
|
|
29
84
|
const subscribe = useCallback((onChange) => registry.subscribe(onChange), [registry]);
|
|
30
85
|
const getSnapshot = useCallback(() => registry.list(), [registry]);
|
|
31
86
|
const getServerSnapshot = useCallback(() => registry.serverList(), [registry]);
|
|
32
87
|
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
33
88
|
}
|
|
34
|
-
/** Returns
|
|
35
|
-
function useCommands(registry) {
|
|
36
|
-
return useCommandList(registry);
|
|
37
|
-
}
|
|
38
|
-
/** Returns a specific command snapshot by ID. */
|
|
89
|
+
/** Returns one command's snapshot, re-rendering only when that command changes. */
|
|
39
90
|
function useCommand(registry, id) {
|
|
40
|
-
const
|
|
41
|
-
|
|
91
|
+
const subscribe = useCallback((onChange) => registry.subscribe(onChange), [registry]);
|
|
92
|
+
const getSnapshot = useCallback(() => registry.get(id), [registry, id]);
|
|
93
|
+
const getServerSnapshot = useCallback(() => registry.serverList().find((command) => command.id === id), [registry, id]);
|
|
94
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
42
95
|
}
|
|
43
96
|
/**
|
|
44
97
|
* Returns display and accessibility shortcuts for a command's primary
|
|
@@ -55,7 +108,9 @@ function useShortcut(registry, id) {
|
|
|
55
108
|
}), [command]);
|
|
56
109
|
}
|
|
57
110
|
/**
|
|
58
|
-
* Activates a scope for the lifecycle of the component.
|
|
111
|
+
* Activates a scope for the lifecycle of the component. Scopes rank in the
|
|
112
|
+
* order React runs effects in: of a parent and a child opening in the same
|
|
113
|
+
* render, the child is pushed first.
|
|
59
114
|
*
|
|
60
115
|
* @example
|
|
61
116
|
* ```tsx
|
|
@@ -64,113 +119,83 @@ function useShortcut(registry, id) {
|
|
|
64
119
|
*/
|
|
65
120
|
function useScope(registry, name, active = true, options = {}) {
|
|
66
121
|
const exclusive = options.exclusive === true;
|
|
67
|
-
const strict = options.strict === true;
|
|
68
122
|
useEffect(() => {
|
|
69
123
|
if (!active) return;
|
|
70
|
-
return registry.pushScope(name, {
|
|
71
|
-
exclusive,
|
|
72
|
-
strict
|
|
73
|
-
});
|
|
124
|
+
return registry.pushScope(name, { exclusive });
|
|
74
125
|
}, [
|
|
75
126
|
registry,
|
|
76
127
|
name,
|
|
77
128
|
active,
|
|
78
|
-
exclusive
|
|
79
|
-
strict
|
|
129
|
+
exclusive
|
|
80
130
|
]);
|
|
81
131
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Attaches what a command does for the lifecycle of the component. The latest
|
|
134
|
+
* `run` is always the one called, and the handler attached last wins. While
|
|
135
|
+
* `enabled` is `false` the command falls to the handler attached before it,
|
|
136
|
+
* or cannot run if there is none.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```tsx
|
|
140
|
+
* useCommandHandler(commands, "palette.open", () => setOpen(true));
|
|
141
|
+
* useCommandHandler(commands, "doc.save", save, dirty);
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
function useCommandHandler(registry, id, run, enabled = true) {
|
|
145
|
+
const latest = useRef(run);
|
|
146
|
+
latest.current = run;
|
|
87
147
|
useEffect(() => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
run: (context) => latest.current.run(context),
|
|
91
|
-
when: () => latest.current.when?.() !== false
|
|
92
|
-
});
|
|
93
|
-
return () => handle.dispose();
|
|
148
|
+
if (!enabled) return;
|
|
149
|
+
return registry.handle(id, (context) => latest.current(context));
|
|
94
150
|
}, [
|
|
95
151
|
registry,
|
|
96
152
|
id,
|
|
97
|
-
|
|
153
|
+
enabled
|
|
98
154
|
]);
|
|
99
155
|
}
|
|
100
|
-
function definitionSignature(definition) {
|
|
101
|
-
return [
|
|
102
|
-
toBindings(definition.keys).map((binding) => bindingSignature(binding)).join("|"),
|
|
103
|
-
definition.scope ?? "",
|
|
104
|
-
definition.title,
|
|
105
|
-
definition.description ?? "",
|
|
106
|
-
definition.group ?? "",
|
|
107
|
-
(definition.keywords ?? []).join(","),
|
|
108
|
-
definition.hidden === true ? "1" : "0",
|
|
109
|
-
definition.enabled === false ? "0" : "1",
|
|
110
|
-
definition.preventDefault === false ? "0" : "1",
|
|
111
|
-
definition.ignoreInputs === void 0 ? "" : String(definition.ignoreInputs),
|
|
112
|
-
definition.requireReset === true ? "1" : "0",
|
|
113
|
-
definition.sequenceTimeout === void 0 ? "" : String(definition.sequenceTimeout)
|
|
114
|
-
].join(" ");
|
|
115
|
-
}
|
|
116
|
-
const IDLE_REBIND = {
|
|
117
|
-
recording: false,
|
|
118
|
-
binding: null,
|
|
119
|
-
steps: [],
|
|
120
|
-
conflicts: []
|
|
121
|
-
};
|
|
122
156
|
/** Hook for recording and configuring a command's keybindings. */
|
|
123
157
|
function useRebind(registry, id, options = {}) {
|
|
124
158
|
const latest = useRef(options);
|
|
125
159
|
latest.current = options;
|
|
126
|
-
const command = useCommand(registry, id);
|
|
127
|
-
const [rebinder, setRebinder] = useState(null);
|
|
128
|
-
const [state, setState] = useState(IDLE_REBIND);
|
|
129
160
|
const mode = options.mode ?? "chord";
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const unsubscribe = instance.subscribe(() => setState(instance.state));
|
|
152
|
-
setRebinder(instance);
|
|
153
|
-
return () => {
|
|
154
|
-
unsubscribe();
|
|
155
|
-
instance.destroy();
|
|
156
|
-
setRebinder(null);
|
|
157
|
-
setState(IDLE_REBIND);
|
|
158
|
-
};
|
|
159
|
-
}, [
|
|
161
|
+
const rebinder = useMemo(() => createRebinder(registry, id, {
|
|
162
|
+
mode,
|
|
163
|
+
get apply() {
|
|
164
|
+
return latest.current.apply;
|
|
165
|
+
},
|
|
166
|
+
get add() {
|
|
167
|
+
return latest.current.add;
|
|
168
|
+
},
|
|
169
|
+
get rejectConflicts() {
|
|
170
|
+
return latest.current.rejectConflicts;
|
|
171
|
+
},
|
|
172
|
+
get ignoreInputs() {
|
|
173
|
+
return latest.current.ignoreInputs;
|
|
174
|
+
},
|
|
175
|
+
get idleTimeout() {
|
|
176
|
+
return latest.current.idleTimeout;
|
|
177
|
+
},
|
|
178
|
+
onRecord: (binding, conflicts) => latest.current.onRecord?.(binding, conflicts),
|
|
179
|
+
onCancel: () => latest.current.onCancel?.(),
|
|
180
|
+
onClear: () => latest.current.onClear?.()
|
|
181
|
+
}), [
|
|
160
182
|
registry,
|
|
161
183
|
id,
|
|
162
|
-
mode
|
|
163
|
-
idleTimeout
|
|
184
|
+
mode
|
|
164
185
|
]);
|
|
186
|
+
useEffect(() => rebinder.destroy, [rebinder]);
|
|
187
|
+
const getState = useCallback(() => rebinder.state, [rebinder]);
|
|
188
|
+
const state = useSyncExternalStore(rebinder.subscribe, getState, getState);
|
|
189
|
+
const command = useCommand(registry, id);
|
|
165
190
|
return useMemo(() => ({
|
|
166
191
|
...state,
|
|
167
192
|
display: command?.display[0] ?? "",
|
|
168
193
|
tokens: command?.tokens[0] ?? EMPTY_TOKENS,
|
|
169
194
|
overridden: command?.overridden ?? false,
|
|
170
|
-
start:
|
|
171
|
-
stop:
|
|
172
|
-
cancel:
|
|
173
|
-
commit:
|
|
195
|
+
start: rebinder.start,
|
|
196
|
+
stop: rebinder.stop,
|
|
197
|
+
cancel: rebinder.cancel,
|
|
198
|
+
commit: rebinder.commit,
|
|
174
199
|
clear: () => registry.keymap.set(id, null),
|
|
175
200
|
reset: () => registry.keymap.reset(id)
|
|
176
201
|
}), [
|
|
@@ -181,45 +206,15 @@ function useRebind(registry, id, options = {}) {
|
|
|
181
206
|
id
|
|
182
207
|
]);
|
|
183
208
|
}
|
|
184
|
-
|
|
185
|
-
return useCallback((onChange) => {
|
|
186
|
-
const subscription = getKeyStateTracker().store.subscribe(onChange);
|
|
187
|
-
return () => subscription.unsubscribe();
|
|
188
|
-
}, []);
|
|
189
|
-
}
|
|
190
|
-
/** Returns an array of keys currently held down. */
|
|
209
|
+
/** Returns the keys held down right now, by the name `KeyboardEvent.key` gives each. */
|
|
191
210
|
function useHeldKeys() {
|
|
192
|
-
return useSyncExternalStore(
|
|
211
|
+
return useSyncExternalStore(subscribeHeldKeys, heldKeys, getEmptyTokens);
|
|
193
212
|
}
|
|
194
|
-
/**
|
|
195
|
-
* Checks whether a specific key is currently held down.
|
|
196
|
-
*
|
|
197
|
-
* @example
|
|
198
|
-
* ```tsx
|
|
199
|
-
* const shift = useKeyHold("Shift");
|
|
200
|
-
* ```
|
|
201
|
-
*/
|
|
202
|
-
function useKeyHold(key) {
|
|
203
|
-
const subscribe = useKeyState();
|
|
204
|
-
const getSnapshot = useCallback(() => getKeyStateTracker().isKeyHeld(key), [key]);
|
|
205
|
-
return useSyncExternalStore(subscribe, getSnapshot, getFalse);
|
|
206
|
-
}
|
|
207
|
-
/** Returns a map of held key identifiers to physical event codes. */
|
|
208
|
-
function useHeldKeyCodes() {
|
|
209
|
-
return useSyncExternalStore(useKeyState(), () => getKeyStateTracker().store.state.heldCodes, getEmptyCodes);
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Returns currently active partially-typed sequences.
|
|
213
|
-
*
|
|
214
|
-
* @example
|
|
215
|
-
* ```tsx
|
|
216
|
-
* const pending = usePendingSequences(commands);
|
|
217
|
-
* ```
|
|
218
|
-
*/
|
|
213
|
+
/** Returns the half-typed sequences, for a hint such as "G …". */
|
|
219
214
|
function usePendingSequences(registry) {
|
|
220
215
|
const subscribe = useCallback((onChange) => registry.subscribePending(onChange), [registry]);
|
|
221
216
|
const getSnapshot = useCallback(() => registry.pending(), [registry]);
|
|
222
217
|
return useSyncExternalStore(subscribe, getSnapshot, getEmptyPending);
|
|
223
218
|
}
|
|
224
219
|
//#endregion
|
|
225
|
-
export {
|
|
220
|
+
export { ariaKeyShortcuts, bindingTokens, createCommands, createRebinder, exportKeymap, formatBinding, importKeymap, isSingleKeyBinding, matchCommands, seq, spokenBinding, useCommand, useCommandHandler, useCommands, useHeldKeys, useMountCommands, usePendingSequences, useRebind, useScope, useShortcut, webStorage };
|