feat: add scoped shortcuts
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import { keysStore } from "./store";
|
||||
import { keysStore, type ScopeKeys } from "./store";
|
||||
|
||||
export function ShortcutDebug() {
|
||||
const entries = useSyncExternalStore(
|
||||
@@ -19,14 +19,23 @@ export function ShortcutDebug() {
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "red", fontFamily: "mono" }}>Registered:</Text>
|
||||
<Text style={{ color: "red", fontFamily: "mono", textAlign: "right" }}>
|
||||
{entries
|
||||
.values()
|
||||
.map(([key, _]) => key)
|
||||
.toArray()
|
||||
.join(",")}
|
||||
</Text>
|
||||
<Text style={{ color: "red", fontFamily: "mono" }}>Scopes:</Text>
|
||||
{entries.map(([scope, keys]) => (
|
||||
<ScopeView key={scope} scope={scope} keys={keys} />
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function ScopeView({ scope, keys }: { scope: string; keys: ScopeKeys }) {
|
||||
return (
|
||||
<Text style={{ color: "red", fontFamily: "mono", textAlign: "right" }}>
|
||||
{scope}:
|
||||
{keys
|
||||
.entries()
|
||||
.map(([key, _]) => key)
|
||||
.toArray()
|
||||
.join(",")}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,16 +3,20 @@ import { keysStore } from "./store";
|
||||
import type { Key } from "./types";
|
||||
import { enforceKeyOptions } from "./util";
|
||||
|
||||
export const useShortcut = (key: Key, handler: () => void) => {
|
||||
export const useShortcut = (
|
||||
key: Key,
|
||||
handler: () => void,
|
||||
scope: string = "global",
|
||||
) => {
|
||||
const keyOptions = enforceKeyOptions(key);
|
||||
const keyName = keyOptions.name;
|
||||
const ref = useRef(handler);
|
||||
ref.current = handler;
|
||||
|
||||
useEffect(() => {
|
||||
keysStore.register(keyName, ref);
|
||||
keysStore.register(keyName, ref, scope);
|
||||
return () => {
|
||||
keysStore.deregister(keyName);
|
||||
keysStore.deregister(keyName, scope);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { type RefObject } from "react";
|
||||
|
||||
// internal map
|
||||
const keys = new Map<string, RefObject<() => void>>();
|
||||
export type ScopeKeys = Map<string, RefObject<() => void>>;
|
||||
|
||||
// cached snapshot (stable reference)
|
||||
let snapshot: [string, RefObject<() => void>][] = [];
|
||||
// outer reactive container
|
||||
const scopes = new Map<string, ScopeKeys>();
|
||||
|
||||
let listeners = new Set<() => void>();
|
||||
// stable snapshot for subscribers
|
||||
let snapshot: [string, ScopeKeys][] = [];
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function emit() {
|
||||
// refresh snapshot ONLY when keys actually change
|
||||
snapshot = Array.from(keys.entries());
|
||||
// replace identity so subscribers re-render
|
||||
snapshot = Array.from(scopes.entries());
|
||||
for (const fn of listeners) fn();
|
||||
}
|
||||
|
||||
@@ -21,20 +23,36 @@ export const keysStore = {
|
||||
},
|
||||
|
||||
getSnapshot() {
|
||||
return snapshot; // stable unless emit() ran
|
||||
return snapshot;
|
||||
},
|
||||
|
||||
register(key: string, ref: RefObject<() => void>) {
|
||||
keys.set(key, ref);
|
||||
register(key: string, ref: RefObject<() => void>, scope: string) {
|
||||
const prev = scopes.get(scope);
|
||||
const next = new Map(prev); // <-- important: new identity
|
||||
next.set(key, ref);
|
||||
|
||||
scopes.set(scope, next); // <-- outer identity also changes
|
||||
emit();
|
||||
},
|
||||
|
||||
deregister(key: string) {
|
||||
keys.delete(key);
|
||||
deregister(key: string, scope: string) {
|
||||
const prev = scopes.get(scope);
|
||||
if (!prev) return;
|
||||
|
||||
const next = new Map(prev);
|
||||
next.delete(key);
|
||||
|
||||
if (next.size === 0) {
|
||||
scopes.delete(scope);
|
||||
} else {
|
||||
scopes.set(scope, next);
|
||||
}
|
||||
emit();
|
||||
},
|
||||
|
||||
getHandler(key: string) {
|
||||
return keys.get(key)?.current;
|
||||
// last scope wins — clarify this logic as needed
|
||||
const last = Array.from(scopes.values()).at(-1);
|
||||
return last?.get(key)?.current;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user