react-hotkeys-hook
Version:
React hook for handling keyboard shortcuts
247 lines (246 loc) • 10.3 kB
JavaScript
import { createContext as W, useContext as z, useState as A, useCallback as v, useRef as H, useLayoutEffect as _, useEffect as m } from "react";
import { jsx as q } from "react/jsx-runtime";
const G = ["shift", "alt", "meta", "mod", "ctrl", "control"], O = {
esc: "escape",
return: "enter",
left: "arrowleft",
right: "arrowright",
up: "arrowup",
down: "arrowdown",
ShiftLeft: "shift",
ShiftRight: "shift",
AltLeft: "alt",
AltRight: "alt",
MetaLeft: "meta",
MetaRight: "meta",
OSLeft: "meta",
OSRight: "meta",
ControlLeft: "ctrl",
ControlRight: "ctrl"
};
function S(t) {
return (O[t.trim()] || t.trim()).toLowerCase().replace(/key|digit|numpad/, "");
}
function J(t) {
return G.includes(t);
}
function b(t, r = ",") {
return t.toLowerCase().split(r);
}
function R(t, r = "+", n = ">", f = !1, l) {
let u = [], c = !1;
t.includes(n) ? (c = !0, u = t.toLocaleLowerCase().split(n).map((i) => S(i))) : u = t.toLocaleLowerCase().split(r).map((i) => S(i));
const d = {
alt: u.includes("alt"),
ctrl: u.includes("ctrl") || u.includes("control"),
shift: u.includes("shift"),
meta: u.includes("meta"),
mod: u.includes("mod"),
useKey: f
}, a = u.filter((i) => !G.includes(i));
return {
...d,
keys: a,
description: l,
isSequence: c
};
}
typeof document < "u" && (document.addEventListener("keydown", (t) => {
t.code !== void 0 && Q([S(t.code)]);
}), document.addEventListener("keyup", (t) => {
t.code !== void 0 && U([S(t.code)]);
})), typeof window < "u" && (window.addEventListener("blur", () => {
E.clear();
}), window.addEventListener("contextmenu", () => {
setTimeout(() => {
E.clear();
}, 0);
}));
const E = /* @__PURE__ */ new Set();
function B(t) {
return Array.isArray(t);
}
function ee(t, r = ",") {
return (B(t) ? t : t.split(r)).every((f) => E.has(f.trim().toLowerCase()));
}
function Q(t) {
const r = Array.isArray(t) ? t : [t];
E.has("meta") && E.forEach((n) => !J(n) && E.delete(n.toLowerCase())), r.forEach((n) => E.add(n.toLowerCase()));
}
function U(t) {
const r = Array.isArray(t) ? t : [t];
t === "meta" ? E.clear() : r.forEach((n) => E.delete(n.toLowerCase()));
}
function te(t, r, n) {
(typeof n == "function" && n(t, r) || n === !0) && t.preventDefault();
}
function re(t, r, n) {
return typeof n == "function" ? n(t, r) : n === !0 || n === void 0;
}
function ne(t) {
return V(t, ["input", "textarea", "select"]);
}
function V(t, r = !1) {
const { target: n, composed: f } = t;
let l;
return ce(n) && f ? l = t.composedPath()[0] && t.composedPath()[0].tagName : l = n && n.tagName, B(r) ? !!(l && r && r.some((u) => u.toLowerCase() === l.toLowerCase())) : !!(l && r && r);
}
function ce(t) {
return !!t.tagName && !t.tagName.startsWith("-") && t.tagName.includes("-");
}
function ue(t, r) {
return t.length === 0 && r ? (console.warn(
'A hotkey has the "scopes" option set, however no active scopes were found. If you want to use the global scopes feature, you need to wrap your app in a <HotkeysProvider>'
), !0) : r ? t.some((n) => r.includes(n)) || t.includes("*") : !0;
}
const oe = (t, r, n = !1) => {
const { alt: f, meta: l, mod: u, shift: c, ctrl: d, keys: a, useKey: i } = r, { code: w, key: e, ctrlKey: s, metaKey: y, shiftKey: k, altKey: K } = t, h = S(w);
if (i && (a == null ? void 0 : a.length) === 1 && a.includes(e))
return !0;
if (!(a != null && a.includes(h)) && !["ctrl", "control", "unknown", "meta", "alt", "shift", "os"].includes(h))
return !1;
if (!n) {
if (f !== K && h !== "alt" || c !== k && h !== "shift")
return !1;
if (u) {
if (!y && !s)
return !1;
} else if (l !== y && h !== "meta" && h !== "os" || d !== s && h !== "ctrl" && h !== "control")
return !1;
}
return a && a.length === 1 && a.includes(h) ? !0 : a ? ee(a) : !a;
}, X = W(void 0), ae = () => z(X);
function fe({ addHotkey: t, removeHotkey: r, children: n }) {
return /* @__PURE__ */ q(X.Provider, { value: { addHotkey: t, removeHotkey: r }, children: n });
}
function N(t, r) {
return t && r && typeof t == "object" && typeof r == "object" ? Object.keys(t).length === Object.keys(r).length && // @ts-expect-error TS7053
Object.keys(t).reduce((n, f) => n && N(t[f], r[f]), !0) : t === r;
}
const Y = W({
hotkeys: [],
activeScopes: [],
// This array has to be empty instead of containing '*' as default, to check if the provider is set or not
toggleScope: () => {
},
enableScope: () => {
},
disableScope: () => {
}
}), le = () => z(Y), he = ({ initiallyActiveScopes: t = ["*"], children: r }) => {
const [n, f] = A(t), [l, u] = A([]), c = v((e) => {
f((s) => s.includes("*") ? [e] : Array.from(/* @__PURE__ */ new Set([...s, e])));
}, []), d = v((e) => {
f((s) => s.filter((y) => y !== e));
}, []), a = v((e) => {
f((s) => s.includes(e) ? s.filter((y) => y !== e) : s.includes("*") ? [e] : Array.from(/* @__PURE__ */ new Set([...s, e])));
}, []), i = v((e) => {
u((s) => [...s, e]);
}, []), w = v((e) => {
u((s) => s.filter((y) => !N(y, e)));
}, []);
return /* @__PURE__ */ q(
Y.Provider,
{
value: { activeScopes: n, hotkeys: l, enableScope: c, disableScope: d, toggleScope: a },
children: /* @__PURE__ */ q(fe, { addHotkey: i, removeHotkey: w, children: r })
}
);
};
function se(t) {
const r = H(void 0);
return N(r.current, t) || (r.current = t), r.current;
}
const F = (t) => {
t.stopPropagation(), t.preventDefault(), t.stopImmediatePropagation();
}, ie = typeof window < "u" ? _ : m;
function we(t, r, n, f) {
const l = H(null), u = H(!1), c = n instanceof Array ? f instanceof Array ? void 0 : f : n, d = B(t) ? t.join(c == null ? void 0 : c.delimiter) : t, a = n instanceof Array ? n : f instanceof Array ? f : void 0, i = v(r, a ?? []), w = H(i);
a ? w.current = i : w.current = r;
const e = se(c), { activeScopes: s } = le(), y = ae();
return ie(() => {
if ((e == null ? void 0 : e.enabled) === !1 || !ue(s, e == null ? void 0 : e.scopes))
return;
let k = [], K;
const h = (o, M = !1) => {
var j;
if (!(ne(o) && !V(o, e == null ? void 0 : e.enableOnFormTags))) {
if (l.current !== null) {
const L = l.current.getRootNode();
if ((L instanceof Document || L instanceof ShadowRoot) && L.activeElement !== l.current && !l.current.contains(L.activeElement)) {
F(o);
return;
}
}
(j = o.target) != null && j.isContentEditable && !(e != null && e.enableOnContentEditable) || b(d, e == null ? void 0 : e.delimiter).forEach((L) => {
var D, I, p, $;
if (L.includes((e == null ? void 0 : e.splitKey) ?? "+") && L.includes((e == null ? void 0 : e.sequenceSplitKey) ?? ">")) {
console.warn(`Hotkey ${L} contains both ${(e == null ? void 0 : e.splitKey) ?? "+"} and ${(e == null ? void 0 : e.sequenceSplitKey) ?? ">"} which is not supported.`);
return;
}
const g = R(L, e == null ? void 0 : e.splitKey, e == null ? void 0 : e.sequenceSplitKey, e == null ? void 0 : e.useKey, e == null ? void 0 : e.description);
if (g.isSequence) {
K = setTimeout(() => {
k = [];
}, (e == null ? void 0 : e.sequenceTimeoutMs) ?? 1e3);
const P = g.useKey ? o.key : S(o.code);
if (J(P.toLowerCase()))
return;
k.push(P);
const Z = (D = g.keys) == null ? void 0 : D[k.length - 1];
if (P !== Z) {
k = [], K && clearTimeout(K);
return;
}
k.length === ((I = g.keys) == null ? void 0 : I.length) && (w.current(o, g), K && clearTimeout(K), k = []);
} else if (oe(o, g, e == null ? void 0 : e.ignoreModifiers) || (p = g.keys) != null && p.includes("*")) {
if (($ = e == null ? void 0 : e.ignoreEventWhen) != null && $.call(e, o) || M && u.current)
return;
if (te(o, g, e == null ? void 0 : e.preventDefault), !re(o, g, e == null ? void 0 : e.enabled)) {
F(o);
return;
}
w.current(o, g), M || (u.current = !0);
}
});
}
}, T = (o) => {
o.code !== void 0 && (Q(S(o.code)), ((e == null ? void 0 : e.keydown) === void 0 && (e == null ? void 0 : e.keyup) !== !0 || e != null && e.keydown) && h(o));
}, x = (o) => {
o.code !== void 0 && (U(S(o.code)), u.current = !1, e != null && e.keyup && h(o, !0));
}, C = l.current || (c == null ? void 0 : c.document) || document;
return C.addEventListener("keyup", x, c == null ? void 0 : c.eventListenerOptions), C.addEventListener("keydown", T, c == null ? void 0 : c.eventListenerOptions), y && b(d, e == null ? void 0 : e.delimiter).forEach(
(o) => y.addHotkey(
R(o, e == null ? void 0 : e.splitKey, e == null ? void 0 : e.sequenceSplitKey, e == null ? void 0 : e.useKey, e == null ? void 0 : e.description)
)
), () => {
C.removeEventListener("keyup", x, c == null ? void 0 : c.eventListenerOptions), C.removeEventListener("keydown", T, c == null ? void 0 : c.eventListenerOptions), y && b(d, e == null ? void 0 : e.delimiter).forEach(
(o) => y.removeHotkey(
R(o, e == null ? void 0 : e.splitKey, e == null ? void 0 : e.sequenceSplitKey, e == null ? void 0 : e.useKey, e == null ? void 0 : e.description)
)
), k = [], K && clearTimeout(K);
};
}, [d, e, s]), l;
}
function ge(t = !1) {
const [r, n] = A(/* @__PURE__ */ new Set()), [f, l] = A(!1), u = v((i) => {
i.code !== void 0 && (i.preventDefault(), i.stopPropagation(), n((w) => {
const e = new Set(w);
return e.add(S(t ? i.key : i.code)), e;
}));
}, [t]), c = v(() => {
typeof document < "u" && (document.removeEventListener("keydown", u), l(!1));
}, [u]), d = v(() => {
n(/* @__PURE__ */ new Set()), typeof document < "u" && (c(), document.addEventListener("keydown", u), l(!0));
}, [u, c]), a = v(() => {
n(/* @__PURE__ */ new Set());
}, []);
return [r, { start: d, stop: c, resetKeys: a, isRecording: f }];
}
export {
he as HotkeysProvider,
ee as isHotkeyPressed,
we as useHotkeys,
le as useHotkeysContext,
ge as useRecordHotkeys
};