browser-shortcuts 0.1.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.
@@ -0,0 +1,58 @@
1
+ import * as preact from 'preact';
2
+ import { ComponentChildren } from 'preact';
3
+
4
+ declare const KEYBINDS: {
5
+ 'CTRL+SHIFT+F': "toggleFullScreen";
6
+ 'CMD+SHIFT+F': "toggleFullScreen";
7
+ 'CTRL+SHIFT+G': "toggleGrid";
8
+ 'CMD+SHIFT+G': "toggleGrid";
9
+ 'CTRL+SHIFT+H': "toggleHelp";
10
+ 'CMD+SHIFT+H': "toggleHelp";
11
+ ESCAPE: "closeOverlay";
12
+ 'CTRL+Z': "undo";
13
+ 'CMD+Z': "undo";
14
+ 'CTRL+SHIFT+Z': "redo";
15
+ 'CMD+SHIFT+Z': "redo";
16
+ 'CTRL+Y': "redo";
17
+ DELETE: "delete";
18
+ BACKSPACE: "delete";
19
+ 'CTRL+A': "selectAll";
20
+ 'CMD+A': "selectAll";
21
+ 'CTRL+C': "copy";
22
+ 'CMD+C': "copy";
23
+ 'CTRL+V': "paste";
24
+ 'CMD+V': "paste";
25
+ 'CTRL+S': "save";
26
+ 'CMD+S': "save";
27
+ DOUBLE_LEFT_BUTTON: "zoomIn";
28
+ LEFT_BUTTON: "leftMouseButton";
29
+ MIDDLE_BUTTON: "middleMouseButton";
30
+ RIGHT_BUTTON: "rightMouseButton";
31
+ };
32
+ /** A key combination string understood by {@link KEYBINDS}, e.g. `"CTRL+SHIFT+F"`. */
33
+ type Keybind = keyof typeof KEYBINDS;
34
+
35
+ /** Callback invoked when a bound shortcut fires. */
36
+ type ShortcutListener = (event: Event) => void;
37
+
38
+ interface ShortcutsContextProps {
39
+ /** Registers `callback` to run whenever `shortcut` is triggered. */
40
+ on: (shortcut: Keybind, callback: ShortcutListener) => void;
41
+ /** Removes a `callback` previously registered with {@link ShortcutsContextProps.on}. */
42
+ off: (shortcut: Keybind, callback: ShortcutListener) => void;
43
+ }
44
+ declare const ShortcutsContext: preact.Context<ShortcutsContextProps>;
45
+ /**
46
+ * Provides global keyboard, mouse and wheel shortcut handling to its
47
+ * subtree via {@link ShortcutsContext}.
48
+ */
49
+ declare function ShortcutsContextProvider({ children, }: {
50
+ children: ComponentChildren;
51
+ }): preact.JSX.Element;
52
+ /**
53
+ * Subscribes `callback` to `shortcut` for as long as the calling component
54
+ * is mounted. Must be used within a `ShortcutsContextProvider`.
55
+ */
56
+ declare function useShortcut(shortcut: Keybind, callback: (event: Event) => void): void;
57
+
58
+ export { type ShortcutListener, ShortcutsContext, type ShortcutsContextProps, ShortcutsContextProvider, useShortcut };
package/dist/preact.js ADDED
@@ -0,0 +1,257 @@
1
+ 'use client';
2
+
3
+ // src/config/shortcuts.ts
4
+ var KEYBINDS = {
5
+ "CTRL+SHIFT+F": "toggleFullScreen",
6
+ "CMD+SHIFT+F": "toggleFullScreen",
7
+ "CTRL+SHIFT+G": "toggleGrid",
8
+ "CMD+SHIFT+G": "toggleGrid",
9
+ "CTRL+SHIFT+H": "toggleHelp",
10
+ "CMD+SHIFT+H": "toggleHelp",
11
+ ESCAPE: "closeOverlay",
12
+ "CTRL+Z": "undo",
13
+ "CMD+Z": "undo",
14
+ "CTRL+SHIFT+Z": "redo",
15
+ "CMD+SHIFT+Z": "redo",
16
+ "CTRL+Y": "redo",
17
+ DELETE: "delete",
18
+ BACKSPACE: "delete",
19
+ "CTRL+A": "selectAll",
20
+ "CMD+A": "selectAll",
21
+ "CTRL+C": "copy",
22
+ "CMD+C": "copy",
23
+ "CTRL+V": "paste",
24
+ "CMD+V": "paste",
25
+ "CTRL+S": "save",
26
+ "CMD+S": "save",
27
+ DOUBLE_LEFT_BUTTON: "zoomIn",
28
+ LEFT_BUTTON: "leftMouseButton",
29
+ MIDDLE_BUTTON: "middleMouseButton",
30
+ RIGHT_BUTTON: "rightMouseButton"
31
+ };
32
+ var SHORTCUTS = {
33
+ toggleFullScreen: (event) => {
34
+ console.log("toggleFullScreen", event);
35
+ },
36
+ toggleGrid: (event) => {
37
+ console.log("toggleGrid", event);
38
+ },
39
+ toggleHelp: (event) => {
40
+ console.log("toggleHelp", event);
41
+ },
42
+ closeOverlay: (event) => {
43
+ console.log("closeOverlay", event);
44
+ },
45
+ undo: (event) => {
46
+ console.log("undo", event);
47
+ },
48
+ redo: (event) => {
49
+ console.log("redo", event);
50
+ },
51
+ delete: (event) => {
52
+ console.log("delete", event);
53
+ },
54
+ selectAll: (event) => {
55
+ console.log("selectAll", event);
56
+ },
57
+ copy: (event) => {
58
+ console.log("copy", event);
59
+ },
60
+ paste: (event) => {
61
+ console.log("paste", event);
62
+ },
63
+ save: (event) => {
64
+ console.log("save", event);
65
+ },
66
+ zoomIn: (event) => {
67
+ console.log("zoomIn", event);
68
+ },
69
+ leftMouseButton: (event) => {
70
+ console.log("leftMouseButton", event);
71
+ },
72
+ middleMouseButton: (event) => {
73
+ console.log("middleMouseButton", event);
74
+ },
75
+ rightMouseButton: (event) => {
76
+ console.log("rightMouseButton", event);
77
+ }
78
+ };
79
+ var shortcuts_default = KEYBINDS;
80
+
81
+ // src/utils/misc.ts
82
+ var IS_MAC = typeof navigator !== "undefined" && navigator.platform.toUpperCase().indexOf("MAC") >= 0;
83
+ var IS_NOT_TOUCH_DEVICE = (() => {
84
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
85
+ return false;
86
+ }
87
+ return !("ontouchstart" in window || navigator.maxTouchPoints > 0 || "msMaxTouchPoints" in navigator && navigator.msMaxTouchPoints > 0);
88
+ })();
89
+ var MOUSE_BUTTONS = ["LEFT_BUTTON", "MIDDLE_BUTTON", "RIGHT_BUTTON"];
90
+ function buildShortcutString(key, event) {
91
+ return [
92
+ IS_MAC && event.metaKey ? "CMD" : "",
93
+ event.ctrlKey ? "CTRL" : "",
94
+ event.altKey ? "ALT" : "",
95
+ event.shiftKey ? "SHIFT" : "",
96
+ key.toUpperCase()
97
+ ].filter(Boolean).join("+");
98
+ }
99
+
100
+ // src/core/shortcuts-manager.ts
101
+ var ShortcutsManager = class {
102
+ constructor() {
103
+ this.listeners = /* @__PURE__ */ new Map();
104
+ /** Registers `callback` to run whenever `shortcut` is triggered. */
105
+ this.on = (shortcut, callback) => {
106
+ var _a;
107
+ if (!this.listeners.has(shortcut)) {
108
+ this.listeners.set(shortcut, /* @__PURE__ */ new Set());
109
+ }
110
+ (_a = this.listeners.get(shortcut)) == null ? void 0 : _a.add(callback);
111
+ };
112
+ /** Removes a `callback` previously registered with {@link on}. */
113
+ this.off = (shortcut, callback) => {
114
+ var _a;
115
+ (_a = this.listeners.get(shortcut)) == null ? void 0 : _a.delete(callback);
116
+ };
117
+ this.emit = (shortcut, event) => {
118
+ var _a;
119
+ (_a = this.listeners.get(shortcut)) == null ? void 0 : _a.forEach((callback) => callback(event));
120
+ };
121
+ this.triggerShortcut = (shortcut, event) => {
122
+ var _a, _b;
123
+ this.emit(shortcut, event);
124
+ const action = shortcuts_default[shortcut];
125
+ if (action) {
126
+ event.preventDefault();
127
+ if (typeof action === "string") {
128
+ (_b = (_a = SHORTCUTS)[action]) == null ? void 0 : _b.call(_a, event);
129
+ } else {
130
+ action(event);
131
+ }
132
+ }
133
+ };
134
+ /**
135
+ * Starts listening on `target` (`window` by default). Returns a disposer
136
+ * that removes the listeners; safe to call multiple times.
137
+ */
138
+ this.attach = (target = window) => {
139
+ this.detach();
140
+ this.target = target;
141
+ const handleKeyDown = (event) => {
142
+ const keyboardEvent = event;
143
+ this.triggerShortcut(
144
+ buildShortcutString(keyboardEvent.key, keyboardEvent),
145
+ event
146
+ );
147
+ };
148
+ const handleKeyUp = (event) => {
149
+ const keyboardEvent = event;
150
+ this.triggerShortcut(
151
+ buildShortcutString(`${keyboardEvent.key}_UP`, keyboardEvent),
152
+ event
153
+ );
154
+ };
155
+ const handleMouseDown = (event) => {
156
+ const mouseEvent = event;
157
+ this.triggerShortcut(
158
+ buildShortcutString(MOUSE_BUTTONS[mouseEvent.button], mouseEvent),
159
+ event
160
+ );
161
+ };
162
+ const handleMouseUp = (event) => {
163
+ const mouseEvent = event;
164
+ this.triggerShortcut(
165
+ buildShortcutString(
166
+ `${MOUSE_BUTTONS[mouseEvent.button]}_UP`,
167
+ mouseEvent
168
+ ),
169
+ event
170
+ );
171
+ };
172
+ const handleDoubleClick = (event) => {
173
+ const mouseEvent = event;
174
+ this.triggerShortcut(
175
+ buildShortcutString(
176
+ `DOUBLE_${MOUSE_BUTTONS[mouseEvent.button]}`,
177
+ mouseEvent
178
+ ),
179
+ event
180
+ );
181
+ };
182
+ const handleContextMenu = (event) => {
183
+ this.triggerShortcut(
184
+ buildShortcutString("RIGHT_BUTTON", event),
185
+ event
186
+ );
187
+ };
188
+ const handleWheel = (event) => {
189
+ const wheelEvent = event;
190
+ const direction = wheelEvent.deltaY < 0 ? "WHEEL_UP" : "WHEEL_DOWN";
191
+ this.triggerShortcut(buildShortcutString(direction, wheelEvent), event);
192
+ };
193
+ target.addEventListener("keydown", handleKeyDown);
194
+ target.addEventListener("keyup", handleKeyUp);
195
+ target.addEventListener("mousedown", handleMouseDown);
196
+ target.addEventListener("mouseup", handleMouseUp);
197
+ target.addEventListener("dblclick", handleDoubleClick);
198
+ target.addEventListener("contextmenu", handleContextMenu);
199
+ target.addEventListener("wheel", handleWheel, { passive: true });
200
+ this.cleanup = () => {
201
+ target.removeEventListener("keydown", handleKeyDown);
202
+ target.removeEventListener("keyup", handleKeyUp);
203
+ target.removeEventListener("mousedown", handleMouseDown);
204
+ target.removeEventListener("mouseup", handleMouseUp);
205
+ target.removeEventListener("dblclick", handleDoubleClick);
206
+ target.removeEventListener("contextmenu", handleContextMenu);
207
+ target.removeEventListener("wheel", handleWheel);
208
+ this.target = void 0;
209
+ this.cleanup = void 0;
210
+ };
211
+ return this.cleanup;
212
+ };
213
+ /** Stops listening. Safe to call even if {@link attach} was never called. */
214
+ this.detach = () => {
215
+ var _a;
216
+ (_a = this.cleanup) == null ? void 0 : _a.call(this);
217
+ };
218
+ }
219
+ };
220
+
221
+ // src/preact/index.tsx
222
+ import { createContext } from "preact";
223
+ import {
224
+ useContext,
225
+ useEffect,
226
+ useLayoutEffect,
227
+ useState
228
+ } from "preact/hooks";
229
+ import { jsx } from "preact/jsx-runtime";
230
+ var ShortcutsContext = createContext(
231
+ {}
232
+ );
233
+ function ShortcutsContextProvider({
234
+ children
235
+ }) {
236
+ const [manager] = useState(() => new ShortcutsManager());
237
+ useLayoutEffect(() => {
238
+ return manager.attach();
239
+ }, [manager]);
240
+ const { on, off } = manager;
241
+ return /* @__PURE__ */ jsx(ShortcutsContext.Provider, { value: { on, off }, children });
242
+ }
243
+ function useShortcut(shortcut, callback) {
244
+ const { on, off } = useContext(ShortcutsContext);
245
+ useEffect(() => {
246
+ on(shortcut, callback);
247
+ return () => {
248
+ off(shortcut, callback);
249
+ };
250
+ }, [shortcut, callback, on, off]);
251
+ }
252
+ export {
253
+ ShortcutsContext,
254
+ ShortcutsContextProvider,
255
+ useShortcut
256
+ };
257
+ //# sourceMappingURL=preact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/shortcuts.ts","../src/utils/misc.ts","../src/core/shortcuts-manager.ts","../src/preact/index.tsx"],"sourcesContent":["/**\n * Maps a keybind string (e.g. `\"CTRL+SHIFT+F\"`) to either the name of a\n * {@link SHORTCUTS} entry or an inline handler.\n *\n * Prefer referencing a named entry in {@link SHORTCUTS} so the same action\n * can be triggered programmatically (e.g. from a command palette) using the\n * same {@link Shortcut} name.\n */\nexport type KeybindAction = Shortcut | ((event: Event) => void);\n\nconst KEYBINDS = {\n 'CTRL+SHIFT+F': 'toggleFullScreen',\n 'CMD+SHIFT+F': 'toggleFullScreen',\n 'CTRL+SHIFT+G': 'toggleGrid',\n 'CMD+SHIFT+G': 'toggleGrid',\n 'CTRL+SHIFT+H': 'toggleHelp',\n 'CMD+SHIFT+H': 'toggleHelp',\n ESCAPE: 'closeOverlay',\n 'CTRL+Z': 'undo',\n 'CMD+Z': 'undo',\n 'CTRL+SHIFT+Z': 'redo',\n 'CMD+SHIFT+Z': 'redo',\n 'CTRL+Y': 'redo',\n DELETE: 'delete',\n BACKSPACE: 'delete',\n 'CTRL+A': 'selectAll',\n 'CMD+A': 'selectAll',\n 'CTRL+C': 'copy',\n 'CMD+C': 'copy',\n 'CTRL+V': 'paste',\n 'CMD+V': 'paste',\n 'CTRL+S': 'save',\n 'CMD+S': 'save',\n DOUBLE_LEFT_BUTTON: 'zoomIn',\n LEFT_BUTTON: 'leftMouseButton',\n MIDDLE_BUTTON: 'middleMouseButton',\n RIGHT_BUTTON: 'rightMouseButton',\n} satisfies { [key: string]: KeybindAction };\n\n/**\n * Named shortcut handlers, keyed by {@link Shortcut} name. Referenced from\n * {@link KEYBINDS} by name so the same action can be triggered from\n * multiple bindings (or invoked directly, outside of any input event).\n */\nexport const SHORTCUTS = {\n toggleFullScreen: (event: Event) => {\n console.log('toggleFullScreen', event);\n },\n toggleGrid: (event: Event) => {\n console.log('toggleGrid', event);\n },\n toggleHelp: (event: Event) => {\n console.log('toggleHelp', event);\n },\n closeOverlay: (event: Event) => {\n console.log('closeOverlay', event);\n },\n undo: (event: Event) => {\n console.log('undo', event);\n },\n redo: (event: Event) => {\n console.log('redo', event);\n },\n delete: (event: Event) => {\n console.log('delete', event);\n },\n selectAll: (event: Event) => {\n console.log('selectAll', event);\n },\n copy: (event: Event) => {\n console.log('copy', event);\n },\n paste: (event: Event) => {\n console.log('paste', event);\n },\n save: (event: Event) => {\n console.log('save', event);\n },\n zoomIn: (event: Event) => {\n console.log('zoomIn', event);\n },\n leftMouseButton: (event: Event) => {\n console.log('leftMouseButton', event);\n },\n middleMouseButton: (event: Event) => {\n console.log('middleMouseButton', event);\n },\n rightMouseButton: (event: Event) => {\n console.log('rightMouseButton', event);\n },\n};\n\n/** A key combination string understood by {@link KEYBINDS}, e.g. `\"CTRL+SHIFT+F\"`. */\nexport type Keybind = keyof typeof KEYBINDS;\n\n/** The name of a handler registered in {@link SHORTCUTS}. */\nexport type Shortcut = keyof typeof SHORTCUTS;\n\nexport default KEYBINDS;\n","import type { Keybind } from '@/config';\n\n/** Whether the current platform reports itself as macOS (used to prefer `CMD` over `CTRL`). */\nexport const IS_MAC =\n typeof navigator !== 'undefined' &&\n navigator.platform.toUpperCase().indexOf('MAC') >= 0;\n\n/** Whether the current device supports mouse/keyboard input rather than touch only. */\nexport const IS_NOT_TOUCH_DEVICE = (() => {\n if (typeof window === 'undefined' || typeof navigator === 'undefined') {\n return false;\n }\n return !(\n 'ontouchstart' in window ||\n navigator.maxTouchPoints > 0 ||\n ('msMaxTouchPoints' in navigator &&\n (navigator.msMaxTouchPoints as number) > 0)\n );\n})();\n\n/** Names for `MouseEvent.button` values 0, 1 and 2, in that order. */\nexport const MOUSE_BUTTONS = ['LEFT_BUTTON', 'MIDDLE_BUTTON', 'RIGHT_BUTTON'];\n\n/**\n * Builds a {@link Keybind} string (e.g. `\"CTRL+SHIFT+F\"`) from a base key\n * name and the modifier keys held during the triggering event.\n *\n * @param key - The key or button name, e.g. `\"F\"` or `\"LEFT_BUTTON\"`.\n * @param event - The DOM event the key combination is derived from.\n */\nexport function buildShortcutString(\n key: string,\n event: KeyboardEvent | MouseEvent | WheelEvent,\n): Keybind {\n return [\n IS_MAC && event.metaKey ? 'CMD' : '',\n event.ctrlKey ? 'CTRL' : '',\n event.altKey ? 'ALT' : '',\n event.shiftKey ? 'SHIFT' : '',\n key.toUpperCase(),\n ]\n .filter(Boolean)\n .join('+') as Keybind;\n}\n","import KEYBINDS, {\n SHORTCUTS,\n type Keybind,\n type KeybindAction,\n type Shortcut,\n} from '@/config/shortcuts';\nimport { buildShortcutString, MOUSE_BUTTONS } from '@/utils/misc';\n\n/** Callback invoked when a bound shortcut fires. */\nexport type ShortcutListener = (event: Event) => void;\n\n/**\n * Framework-agnostic keyboard, mouse and wheel shortcut dispatcher.\n *\n * Listens for the most commonly used input events (`keydown`, `keyup`,\n * `mousedown`, `mouseup`, `dblclick`, `contextmenu` and `wheel`) on a target\n * (`window` by default), converts each into a {@link Keybind} string, and\n * dispatches it to any listeners registered with {@link on} as well as the\n * static bindings declared in `KEYBINDS`.\n *\n * Every framework adapter in this package (React, Vue, Svelte, Angular,\n * Solid, Preact) wraps a single instance of this class in that framework's\n * own idioms (context/provider, composable, store, service, etc.).\n */\nexport class ShortcutsManager {\n private listeners = new Map<Keybind, Set<ShortcutListener>>();\n private target: EventTarget | undefined;\n private cleanup: (() => void) | undefined;\n\n /** Registers `callback` to run whenever `shortcut` is triggered. */\n on = (shortcut: Keybind, callback: ShortcutListener): void => {\n if (!this.listeners.has(shortcut)) {\n this.listeners.set(shortcut, new Set());\n }\n this.listeners.get(shortcut)?.add(callback);\n };\n\n /** Removes a `callback` previously registered with {@link on}. */\n off = (shortcut: Keybind, callback: ShortcutListener): void => {\n this.listeners.get(shortcut)?.delete(callback);\n };\n\n private emit = (shortcut: Keybind, event: Event): void => {\n this.listeners.get(shortcut)?.forEach((callback) => callback(event));\n };\n\n private triggerShortcut = (shortcut: Keybind, event: Event): void => {\n this.emit(shortcut, event);\n\n const action = KEYBINDS[shortcut] as KeybindAction | undefined;\n if (action) {\n event.preventDefault();\n if (typeof action === 'string') {\n SHORTCUTS[action as Shortcut]?.(event);\n } else {\n action(event);\n }\n }\n };\n\n /**\n * Starts listening on `target` (`window` by default). Returns a disposer\n * that removes the listeners; safe to call multiple times.\n */\n attach = (target: EventTarget = window): (() => void) => {\n this.detach();\n this.target = target;\n\n const handleKeyDown = (event: Event) => {\n const keyboardEvent = event as KeyboardEvent;\n this.triggerShortcut(\n buildShortcutString(keyboardEvent.key, keyboardEvent),\n event,\n );\n };\n\n const handleKeyUp = (event: Event) => {\n const keyboardEvent = event as KeyboardEvent;\n this.triggerShortcut(\n buildShortcutString(`${keyboardEvent.key}_UP`, keyboardEvent),\n event,\n );\n };\n\n const handleMouseDown = (event: Event) => {\n const mouseEvent = event as MouseEvent;\n this.triggerShortcut(\n buildShortcutString(MOUSE_BUTTONS[mouseEvent.button], mouseEvent),\n event,\n );\n };\n\n const handleMouseUp = (event: Event) => {\n const mouseEvent = event as MouseEvent;\n this.triggerShortcut(\n buildShortcutString(\n `${MOUSE_BUTTONS[mouseEvent.button]}_UP`,\n mouseEvent,\n ),\n event,\n );\n };\n\n const handleDoubleClick = (event: Event) => {\n const mouseEvent = event as MouseEvent;\n this.triggerShortcut(\n buildShortcutString(\n `DOUBLE_${MOUSE_BUTTONS[mouseEvent.button]}`,\n mouseEvent,\n ),\n event,\n );\n };\n\n const handleContextMenu = (event: Event) => {\n this.triggerShortcut(\n buildShortcutString('RIGHT_BUTTON', event as MouseEvent),\n event,\n );\n };\n\n const handleWheel = (event: Event) => {\n const wheelEvent = event as WheelEvent;\n const direction = wheelEvent.deltaY < 0 ? 'WHEEL_UP' : 'WHEEL_DOWN';\n this.triggerShortcut(buildShortcutString(direction, wheelEvent), event);\n };\n\n target.addEventListener('keydown', handleKeyDown);\n target.addEventListener('keyup', handleKeyUp);\n target.addEventListener('mousedown', handleMouseDown);\n target.addEventListener('mouseup', handleMouseUp);\n target.addEventListener('dblclick', handleDoubleClick);\n target.addEventListener('contextmenu', handleContextMenu);\n target.addEventListener('wheel', handleWheel, { passive: true });\n\n this.cleanup = () => {\n target.removeEventListener('keydown', handleKeyDown);\n target.removeEventListener('keyup', handleKeyUp);\n target.removeEventListener('mousedown', handleMouseDown);\n target.removeEventListener('mouseup', handleMouseUp);\n target.removeEventListener('dblclick', handleDoubleClick);\n target.removeEventListener('contextmenu', handleContextMenu);\n target.removeEventListener('wheel', handleWheel);\n this.target = undefined;\n this.cleanup = undefined;\n };\n\n return this.cleanup;\n };\n\n /** Stops listening. Safe to call even if {@link attach} was never called. */\n detach = (): void => {\n this.cleanup?.();\n };\n}\n","/** @jsxImportSource preact */\nimport type { Keybind } from '@/config';\nimport { ShortcutsManager, type ShortcutListener } from '@/core/shortcuts-manager';\nimport { createContext, type ComponentChildren } from 'preact';\nimport {\n useContext,\n useEffect,\n useLayoutEffect,\n useState,\n} from 'preact/hooks';\n\nexport type { ShortcutListener } from '@/core/shortcuts-manager';\n\nexport interface ShortcutsContextProps {\n /** Registers `callback` to run whenever `shortcut` is triggered. */\n on: (shortcut: Keybind, callback: ShortcutListener) => void;\n /** Removes a `callback` previously registered with {@link ShortcutsContextProps.on}. */\n off: (shortcut: Keybind, callback: ShortcutListener) => void;\n}\n\nexport const ShortcutsContext = createContext<ShortcutsContextProps>(\n {} as ShortcutsContextProps,\n);\n\n/**\n * Provides global keyboard, mouse and wheel shortcut handling to its\n * subtree via {@link ShortcutsContext}.\n */\nexport function ShortcutsContextProvider({\n children,\n}: {\n children: ComponentChildren;\n}) {\n const [manager] = useState(() => new ShortcutsManager());\n\n useLayoutEffect(() => {\n return manager.attach();\n }, [manager]);\n\n const { on, off } = manager;\n\n return (\n <ShortcutsContext.Provider value={{ on, off }}>\n {children}\n </ShortcutsContext.Provider>\n );\n}\n\n/**\n * Subscribes `callback` to `shortcut` for as long as the calling component\n * is mounted. Must be used within a `ShortcutsContextProvider`.\n */\nexport function useShortcut(\n shortcut: Keybind,\n callback: (event: Event) => void,\n) {\n const { on, off } = useContext(ShortcutsContext);\n\n useEffect(() => {\n on(shortcut, callback);\n\n return () => {\n off(shortcut, callback);\n };\n }, [shortcut, callback, on, off]);\n}\n"],"mappings":"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","names":[]}
package/dist/react.cjs ADDED
@@ -0,0 +1,280 @@
1
+ 'use client';
2
+ "use strict";
3
+ "use client";
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+
22
+ // src/react/index.tsx
23
+ var react_exports = {};
24
+ __export(react_exports, {
25
+ ShortcutsContext: () => ShortcutsContext,
26
+ ShortcutsContextProvider: () => ShortcutsContextProvider,
27
+ useShortcut: () => useShortcut
28
+ });
29
+ module.exports = __toCommonJS(react_exports);
30
+
31
+ // src/config/shortcuts.ts
32
+ var KEYBINDS = {
33
+ "CTRL+SHIFT+F": "toggleFullScreen",
34
+ "CMD+SHIFT+F": "toggleFullScreen",
35
+ "CTRL+SHIFT+G": "toggleGrid",
36
+ "CMD+SHIFT+G": "toggleGrid",
37
+ "CTRL+SHIFT+H": "toggleHelp",
38
+ "CMD+SHIFT+H": "toggleHelp",
39
+ ESCAPE: "closeOverlay",
40
+ "CTRL+Z": "undo",
41
+ "CMD+Z": "undo",
42
+ "CTRL+SHIFT+Z": "redo",
43
+ "CMD+SHIFT+Z": "redo",
44
+ "CTRL+Y": "redo",
45
+ DELETE: "delete",
46
+ BACKSPACE: "delete",
47
+ "CTRL+A": "selectAll",
48
+ "CMD+A": "selectAll",
49
+ "CTRL+C": "copy",
50
+ "CMD+C": "copy",
51
+ "CTRL+V": "paste",
52
+ "CMD+V": "paste",
53
+ "CTRL+S": "save",
54
+ "CMD+S": "save",
55
+ DOUBLE_LEFT_BUTTON: "zoomIn",
56
+ LEFT_BUTTON: "leftMouseButton",
57
+ MIDDLE_BUTTON: "middleMouseButton",
58
+ RIGHT_BUTTON: "rightMouseButton"
59
+ };
60
+ var SHORTCUTS = {
61
+ toggleFullScreen: (event) => {
62
+ console.log("toggleFullScreen", event);
63
+ },
64
+ toggleGrid: (event) => {
65
+ console.log("toggleGrid", event);
66
+ },
67
+ toggleHelp: (event) => {
68
+ console.log("toggleHelp", event);
69
+ },
70
+ closeOverlay: (event) => {
71
+ console.log("closeOverlay", event);
72
+ },
73
+ undo: (event) => {
74
+ console.log("undo", event);
75
+ },
76
+ redo: (event) => {
77
+ console.log("redo", event);
78
+ },
79
+ delete: (event) => {
80
+ console.log("delete", event);
81
+ },
82
+ selectAll: (event) => {
83
+ console.log("selectAll", event);
84
+ },
85
+ copy: (event) => {
86
+ console.log("copy", event);
87
+ },
88
+ paste: (event) => {
89
+ console.log("paste", event);
90
+ },
91
+ save: (event) => {
92
+ console.log("save", event);
93
+ },
94
+ zoomIn: (event) => {
95
+ console.log("zoomIn", event);
96
+ },
97
+ leftMouseButton: (event) => {
98
+ console.log("leftMouseButton", event);
99
+ },
100
+ middleMouseButton: (event) => {
101
+ console.log("middleMouseButton", event);
102
+ },
103
+ rightMouseButton: (event) => {
104
+ console.log("rightMouseButton", event);
105
+ }
106
+ };
107
+ var shortcuts_default = KEYBINDS;
108
+
109
+ // src/utils/misc.ts
110
+ var IS_MAC = typeof navigator !== "undefined" && navigator.platform.toUpperCase().indexOf("MAC") >= 0;
111
+ var IS_NOT_TOUCH_DEVICE = (() => {
112
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
113
+ return false;
114
+ }
115
+ return !("ontouchstart" in window || navigator.maxTouchPoints > 0 || "msMaxTouchPoints" in navigator && navigator.msMaxTouchPoints > 0);
116
+ })();
117
+ var MOUSE_BUTTONS = ["LEFT_BUTTON", "MIDDLE_BUTTON", "RIGHT_BUTTON"];
118
+ function buildShortcutString(key, event) {
119
+ return [
120
+ IS_MAC && event.metaKey ? "CMD" : "",
121
+ event.ctrlKey ? "CTRL" : "",
122
+ event.altKey ? "ALT" : "",
123
+ event.shiftKey ? "SHIFT" : "",
124
+ key.toUpperCase()
125
+ ].filter(Boolean).join("+");
126
+ }
127
+
128
+ // src/core/shortcuts-manager.ts
129
+ var ShortcutsManager = class {
130
+ constructor() {
131
+ this.listeners = /* @__PURE__ */ new Map();
132
+ /** Registers `callback` to run whenever `shortcut` is triggered. */
133
+ this.on = (shortcut, callback) => {
134
+ var _a;
135
+ if (!this.listeners.has(shortcut)) {
136
+ this.listeners.set(shortcut, /* @__PURE__ */ new Set());
137
+ }
138
+ (_a = this.listeners.get(shortcut)) == null ? void 0 : _a.add(callback);
139
+ };
140
+ /** Removes a `callback` previously registered with {@link on}. */
141
+ this.off = (shortcut, callback) => {
142
+ var _a;
143
+ (_a = this.listeners.get(shortcut)) == null ? void 0 : _a.delete(callback);
144
+ };
145
+ this.emit = (shortcut, event) => {
146
+ var _a;
147
+ (_a = this.listeners.get(shortcut)) == null ? void 0 : _a.forEach((callback) => callback(event));
148
+ };
149
+ this.triggerShortcut = (shortcut, event) => {
150
+ var _a, _b;
151
+ this.emit(shortcut, event);
152
+ const action = shortcuts_default[shortcut];
153
+ if (action) {
154
+ event.preventDefault();
155
+ if (typeof action === "string") {
156
+ (_b = (_a = SHORTCUTS)[action]) == null ? void 0 : _b.call(_a, event);
157
+ } else {
158
+ action(event);
159
+ }
160
+ }
161
+ };
162
+ /**
163
+ * Starts listening on `target` (`window` by default). Returns a disposer
164
+ * that removes the listeners; safe to call multiple times.
165
+ */
166
+ this.attach = (target = window) => {
167
+ this.detach();
168
+ this.target = target;
169
+ const handleKeyDown = (event) => {
170
+ const keyboardEvent = event;
171
+ this.triggerShortcut(
172
+ buildShortcutString(keyboardEvent.key, keyboardEvent),
173
+ event
174
+ );
175
+ };
176
+ const handleKeyUp = (event) => {
177
+ const keyboardEvent = event;
178
+ this.triggerShortcut(
179
+ buildShortcutString(`${keyboardEvent.key}_UP`, keyboardEvent),
180
+ event
181
+ );
182
+ };
183
+ const handleMouseDown = (event) => {
184
+ const mouseEvent = event;
185
+ this.triggerShortcut(
186
+ buildShortcutString(MOUSE_BUTTONS[mouseEvent.button], mouseEvent),
187
+ event
188
+ );
189
+ };
190
+ const handleMouseUp = (event) => {
191
+ const mouseEvent = event;
192
+ this.triggerShortcut(
193
+ buildShortcutString(
194
+ `${MOUSE_BUTTONS[mouseEvent.button]}_UP`,
195
+ mouseEvent
196
+ ),
197
+ event
198
+ );
199
+ };
200
+ const handleDoubleClick = (event) => {
201
+ const mouseEvent = event;
202
+ this.triggerShortcut(
203
+ buildShortcutString(
204
+ `DOUBLE_${MOUSE_BUTTONS[mouseEvent.button]}`,
205
+ mouseEvent
206
+ ),
207
+ event
208
+ );
209
+ };
210
+ const handleContextMenu = (event) => {
211
+ this.triggerShortcut(
212
+ buildShortcutString("RIGHT_BUTTON", event),
213
+ event
214
+ );
215
+ };
216
+ const handleWheel = (event) => {
217
+ const wheelEvent = event;
218
+ const direction = wheelEvent.deltaY < 0 ? "WHEEL_UP" : "WHEEL_DOWN";
219
+ this.triggerShortcut(buildShortcutString(direction, wheelEvent), event);
220
+ };
221
+ target.addEventListener("keydown", handleKeyDown);
222
+ target.addEventListener("keyup", handleKeyUp);
223
+ target.addEventListener("mousedown", handleMouseDown);
224
+ target.addEventListener("mouseup", handleMouseUp);
225
+ target.addEventListener("dblclick", handleDoubleClick);
226
+ target.addEventListener("contextmenu", handleContextMenu);
227
+ target.addEventListener("wheel", handleWheel, { passive: true });
228
+ this.cleanup = () => {
229
+ target.removeEventListener("keydown", handleKeyDown);
230
+ target.removeEventListener("keyup", handleKeyUp);
231
+ target.removeEventListener("mousedown", handleMouseDown);
232
+ target.removeEventListener("mouseup", handleMouseUp);
233
+ target.removeEventListener("dblclick", handleDoubleClick);
234
+ target.removeEventListener("contextmenu", handleContextMenu);
235
+ target.removeEventListener("wheel", handleWheel);
236
+ this.target = void 0;
237
+ this.cleanup = void 0;
238
+ };
239
+ return this.cleanup;
240
+ };
241
+ /** Stops listening. Safe to call even if {@link attach} was never called. */
242
+ this.detach = () => {
243
+ var _a;
244
+ (_a = this.cleanup) == null ? void 0 : _a.call(this);
245
+ };
246
+ }
247
+ };
248
+
249
+ // src/react/index.tsx
250
+ var import_react = require("react");
251
+ var import_jsx_runtime = require("react/jsx-runtime");
252
+ var ShortcutsContext = (0, import_react.createContext)(
253
+ {}
254
+ );
255
+ function ShortcutsContextProvider({
256
+ children
257
+ }) {
258
+ const [manager] = (0, import_react.useState)(() => new ShortcutsManager());
259
+ (0, import_react.useLayoutEffect)(() => {
260
+ return manager.attach();
261
+ }, [manager]);
262
+ const { on, off } = manager;
263
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ShortcutsContext.Provider, { value: { on, off }, children });
264
+ }
265
+ function useShortcut(shortcut, callback) {
266
+ const { on, off } = (0, import_react.useContext)(ShortcutsContext);
267
+ (0, import_react.useEffect)(() => {
268
+ on(shortcut, callback);
269
+ return () => {
270
+ off(shortcut, callback);
271
+ };
272
+ }, [shortcut, callback, on, off]);
273
+ }
274
+ // Annotate the CommonJS export names for ESM import in node:
275
+ 0 && (module.exports = {
276
+ ShortcutsContext,
277
+ ShortcutsContextProvider,
278
+ useShortcut
279
+ });
280
+ //# sourceMappingURL=react.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/index.tsx","../src/config/shortcuts.ts","../src/utils/misc.ts","../src/core/shortcuts-manager.ts"],"sourcesContent":["'use client';\n\nimport type { Keybind } from '@/config';\nimport { ShortcutsManager, type ShortcutListener } from '@/core/shortcuts-manager';\nimport {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useState,\n} from 'react';\n\nexport type { ShortcutListener } from '@/core/shortcuts-manager';\n\nexport interface ShortcutsContextProps {\n /** Registers `callback` to run whenever `shortcut` is triggered. */\n on: (shortcut: Keybind, callback: ShortcutListener) => void;\n /** Removes a `callback` previously registered with {@link ShortcutsContextProps.on}. */\n off: (shortcut: Keybind, callback: ShortcutListener) => void;\n}\n\nexport const ShortcutsContext = createContext<ShortcutsContextProps>(\n {} as ShortcutsContextProps,\n);\n\n/**\n * Provides global keyboard, mouse and wheel shortcut handling to its\n * subtree via {@link ShortcutsContext}.\n */\nexport function ShortcutsContextProvider({\n children,\n}: {\n children: React.ReactNode;\n}) {\n const [manager] = useState(() => new ShortcutsManager());\n\n useLayoutEffect(() => {\n return manager.attach();\n }, [manager]);\n\n const { on, off } = manager;\n\n return (\n <ShortcutsContext.Provider value={{ on, off }}>\n {children}\n </ShortcutsContext.Provider>\n );\n}\n\n/**\n * Subscribes `callback` to `shortcut` for as long as the calling component\n * is mounted. Must be used within a `ShortcutsContextProvider`.\n *\n * @param shortcut - A key combination string, e.g. `\"CTRL+SHIFT+F\"`.\n * @param callback - Invoked with the originating `Event` when the shortcut fires.\n *\n * @example\n * ```tsx\n * useShortcut('CTRL+SHIFT+F', () => setFullScreen((prev) => !prev));\n * ```\n */\nexport function useShortcut(\n shortcut: Keybind,\n callback: (event: Event) => void,\n) {\n const { on, off } = useContext(ShortcutsContext);\n\n useEffect(() => {\n on(shortcut, callback);\n\n return () => {\n off(shortcut, callback);\n };\n }, [shortcut, callback, on, off]);\n}\n","/**\n * Maps a keybind string (e.g. `\"CTRL+SHIFT+F\"`) to either the name of a\n * {@link SHORTCUTS} entry or an inline handler.\n *\n * Prefer referencing a named entry in {@link SHORTCUTS} so the same action\n * can be triggered programmatically (e.g. from a command palette) using the\n * same {@link Shortcut} name.\n */\nexport type KeybindAction = Shortcut | ((event: Event) => void);\n\nconst KEYBINDS = {\n 'CTRL+SHIFT+F': 'toggleFullScreen',\n 'CMD+SHIFT+F': 'toggleFullScreen',\n 'CTRL+SHIFT+G': 'toggleGrid',\n 'CMD+SHIFT+G': 'toggleGrid',\n 'CTRL+SHIFT+H': 'toggleHelp',\n 'CMD+SHIFT+H': 'toggleHelp',\n ESCAPE: 'closeOverlay',\n 'CTRL+Z': 'undo',\n 'CMD+Z': 'undo',\n 'CTRL+SHIFT+Z': 'redo',\n 'CMD+SHIFT+Z': 'redo',\n 'CTRL+Y': 'redo',\n DELETE: 'delete',\n BACKSPACE: 'delete',\n 'CTRL+A': 'selectAll',\n 'CMD+A': 'selectAll',\n 'CTRL+C': 'copy',\n 'CMD+C': 'copy',\n 'CTRL+V': 'paste',\n 'CMD+V': 'paste',\n 'CTRL+S': 'save',\n 'CMD+S': 'save',\n DOUBLE_LEFT_BUTTON: 'zoomIn',\n LEFT_BUTTON: 'leftMouseButton',\n MIDDLE_BUTTON: 'middleMouseButton',\n RIGHT_BUTTON: 'rightMouseButton',\n} satisfies { [key: string]: KeybindAction };\n\n/**\n * Named shortcut handlers, keyed by {@link Shortcut} name. Referenced from\n * {@link KEYBINDS} by name so the same action can be triggered from\n * multiple bindings (or invoked directly, outside of any input event).\n */\nexport const SHORTCUTS = {\n toggleFullScreen: (event: Event) => {\n console.log('toggleFullScreen', event);\n },\n toggleGrid: (event: Event) => {\n console.log('toggleGrid', event);\n },\n toggleHelp: (event: Event) => {\n console.log('toggleHelp', event);\n },\n closeOverlay: (event: Event) => {\n console.log('closeOverlay', event);\n },\n undo: (event: Event) => {\n console.log('undo', event);\n },\n redo: (event: Event) => {\n console.log('redo', event);\n },\n delete: (event: Event) => {\n console.log('delete', event);\n },\n selectAll: (event: Event) => {\n console.log('selectAll', event);\n },\n copy: (event: Event) => {\n console.log('copy', event);\n },\n paste: (event: Event) => {\n console.log('paste', event);\n },\n save: (event: Event) => {\n console.log('save', event);\n },\n zoomIn: (event: Event) => {\n console.log('zoomIn', event);\n },\n leftMouseButton: (event: Event) => {\n console.log('leftMouseButton', event);\n },\n middleMouseButton: (event: Event) => {\n console.log('middleMouseButton', event);\n },\n rightMouseButton: (event: Event) => {\n console.log('rightMouseButton', event);\n },\n};\n\n/** A key combination string understood by {@link KEYBINDS}, e.g. `\"CTRL+SHIFT+F\"`. */\nexport type Keybind = keyof typeof KEYBINDS;\n\n/** The name of a handler registered in {@link SHORTCUTS}. */\nexport type Shortcut = keyof typeof SHORTCUTS;\n\nexport default KEYBINDS;\n","import type { Keybind } from '@/config';\n\n/** Whether the current platform reports itself as macOS (used to prefer `CMD` over `CTRL`). */\nexport const IS_MAC =\n typeof navigator !== 'undefined' &&\n navigator.platform.toUpperCase().indexOf('MAC') >= 0;\n\n/** Whether the current device supports mouse/keyboard input rather than touch only. */\nexport const IS_NOT_TOUCH_DEVICE = (() => {\n if (typeof window === 'undefined' || typeof navigator === 'undefined') {\n return false;\n }\n return !(\n 'ontouchstart' in window ||\n navigator.maxTouchPoints > 0 ||\n ('msMaxTouchPoints' in navigator &&\n (navigator.msMaxTouchPoints as number) > 0)\n );\n})();\n\n/** Names for `MouseEvent.button` values 0, 1 and 2, in that order. */\nexport const MOUSE_BUTTONS = ['LEFT_BUTTON', 'MIDDLE_BUTTON', 'RIGHT_BUTTON'];\n\n/**\n * Builds a {@link Keybind} string (e.g. `\"CTRL+SHIFT+F\"`) from a base key\n * name and the modifier keys held during the triggering event.\n *\n * @param key - The key or button name, e.g. `\"F\"` or `\"LEFT_BUTTON\"`.\n * @param event - The DOM event the key combination is derived from.\n */\nexport function buildShortcutString(\n key: string,\n event: KeyboardEvent | MouseEvent | WheelEvent,\n): Keybind {\n return [\n IS_MAC && event.metaKey ? 'CMD' : '',\n event.ctrlKey ? 'CTRL' : '',\n event.altKey ? 'ALT' : '',\n event.shiftKey ? 'SHIFT' : '',\n key.toUpperCase(),\n ]\n .filter(Boolean)\n .join('+') as Keybind;\n}\n","import KEYBINDS, {\n SHORTCUTS,\n type Keybind,\n type KeybindAction,\n type Shortcut,\n} from '@/config/shortcuts';\nimport { buildShortcutString, MOUSE_BUTTONS } from '@/utils/misc';\n\n/** Callback invoked when a bound shortcut fires. */\nexport type ShortcutListener = (event: Event) => void;\n\n/**\n * Framework-agnostic keyboard, mouse and wheel shortcut dispatcher.\n *\n * Listens for the most commonly used input events (`keydown`, `keyup`,\n * `mousedown`, `mouseup`, `dblclick`, `contextmenu` and `wheel`) on a target\n * (`window` by default), converts each into a {@link Keybind} string, and\n * dispatches it to any listeners registered with {@link on} as well as the\n * static bindings declared in `KEYBINDS`.\n *\n * Every framework adapter in this package (React, Vue, Svelte, Angular,\n * Solid, Preact) wraps a single instance of this class in that framework's\n * own idioms (context/provider, composable, store, service, etc.).\n */\nexport class ShortcutsManager {\n private listeners = new Map<Keybind, Set<ShortcutListener>>();\n private target: EventTarget | undefined;\n private cleanup: (() => void) | undefined;\n\n /** Registers `callback` to run whenever `shortcut` is triggered. */\n on = (shortcut: Keybind, callback: ShortcutListener): void => {\n if (!this.listeners.has(shortcut)) {\n this.listeners.set(shortcut, new Set());\n }\n this.listeners.get(shortcut)?.add(callback);\n };\n\n /** Removes a `callback` previously registered with {@link on}. */\n off = (shortcut: Keybind, callback: ShortcutListener): void => {\n this.listeners.get(shortcut)?.delete(callback);\n };\n\n private emit = (shortcut: Keybind, event: Event): void => {\n this.listeners.get(shortcut)?.forEach((callback) => callback(event));\n };\n\n private triggerShortcut = (shortcut: Keybind, event: Event): void => {\n this.emit(shortcut, event);\n\n const action = KEYBINDS[shortcut] as KeybindAction | undefined;\n if (action) {\n event.preventDefault();\n if (typeof action === 'string') {\n SHORTCUTS[action as Shortcut]?.(event);\n } else {\n action(event);\n }\n }\n };\n\n /**\n * Starts listening on `target` (`window` by default). Returns a disposer\n * that removes the listeners; safe to call multiple times.\n */\n attach = (target: EventTarget = window): (() => void) => {\n this.detach();\n this.target = target;\n\n const handleKeyDown = (event: Event) => {\n const keyboardEvent = event as KeyboardEvent;\n this.triggerShortcut(\n buildShortcutString(keyboardEvent.key, keyboardEvent),\n event,\n );\n };\n\n const handleKeyUp = (event: Event) => {\n const keyboardEvent = event as KeyboardEvent;\n this.triggerShortcut(\n buildShortcutString(`${keyboardEvent.key}_UP`, keyboardEvent),\n event,\n );\n };\n\n const handleMouseDown = (event: Event) => {\n const mouseEvent = event as MouseEvent;\n this.triggerShortcut(\n buildShortcutString(MOUSE_BUTTONS[mouseEvent.button], mouseEvent),\n event,\n );\n };\n\n const handleMouseUp = (event: Event) => {\n const mouseEvent = event as MouseEvent;\n this.triggerShortcut(\n buildShortcutString(\n `${MOUSE_BUTTONS[mouseEvent.button]}_UP`,\n mouseEvent,\n ),\n event,\n );\n };\n\n const handleDoubleClick = (event: Event) => {\n const mouseEvent = event as MouseEvent;\n this.triggerShortcut(\n buildShortcutString(\n `DOUBLE_${MOUSE_BUTTONS[mouseEvent.button]}`,\n mouseEvent,\n ),\n event,\n );\n };\n\n const handleContextMenu = (event: Event) => {\n this.triggerShortcut(\n buildShortcutString('RIGHT_BUTTON', event as MouseEvent),\n event,\n );\n };\n\n const handleWheel = (event: Event) => {\n const wheelEvent = event as WheelEvent;\n const direction = wheelEvent.deltaY < 0 ? 'WHEEL_UP' : 'WHEEL_DOWN';\n this.triggerShortcut(buildShortcutString(direction, wheelEvent), event);\n };\n\n target.addEventListener('keydown', handleKeyDown);\n target.addEventListener('keyup', handleKeyUp);\n target.addEventListener('mousedown', handleMouseDown);\n target.addEventListener('mouseup', handleMouseUp);\n target.addEventListener('dblclick', handleDoubleClick);\n target.addEventListener('contextmenu', handleContextMenu);\n target.addEventListener('wheel', handleWheel, { passive: true });\n\n this.cleanup = () => {\n target.removeEventListener('keydown', handleKeyDown);\n target.removeEventListener('keyup', handleKeyUp);\n target.removeEventListener('mousedown', handleMouseDown);\n target.removeEventListener('mouseup', handleMouseUp);\n target.removeEventListener('dblclick', handleDoubleClick);\n target.removeEventListener('contextmenu', handleContextMenu);\n target.removeEventListener('wheel', handleWheel);\n this.target = undefined;\n this.cleanup = undefined;\n };\n\n return this.cleanup;\n };\n\n /** Stops listening. Safe to call even if {@link attach} was never called. */\n detach = (): void => {\n this.cleanup?.();\n };\n}\n"],"mappings":"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","names":[]}