@lofcz/edix 0.3.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 inokawa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,246 @@
1
+ # edix
2
+
3
+ ![npm](https://img.shields.io/npm/v/@lofcz/edix) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/@lofcz/edix) ![npm](https://img.shields.io/npm/dw/@lofcz/edix) [![check](https://github.com/lofcz/edix/actions/workflows/check.yml/badge.svg)](https://github.com/lofcz/edix/actions/workflows/check.yml) [![demo](https://github.com/lofcz/edix/actions/workflows/demo.yml/badge.svg)](https://github.com/lofcz/edix/actions/workflows/demo.yml)
4
+
5
+ > An experimental, framework agnostic, small (4kB+) [contenteditable](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable) state manager.
6
+
7
+ ## Motivation
8
+
9
+ Web editing is so hard even today. There are excellent libraries to make complex rich text editor, but they are too much for small purposes. Native [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) element is accessible and easy to use, but it's hardly customizable.
10
+
11
+ [contenteditable](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable) attribute is a primitive for rich text editing, [that was designed for Internet Explorer and copied by other browsers](https://blog.whatwg.org/the-road-to-html-5-contenteditable). As you may know it has [so many problems](https://github.com/grammarly/contenteditable). It has no clear spec, it has many edge case bugs, and has cross browser/OS/input device problems. And it doesn't work well with declarative frontend frameworks... However, at least the core of contenteditable is stable and it works in all browsers except the inconsistencies. This library aims to fill that gap, fix contenteditable to fit modern web development.
12
+
13
+ ## Demo
14
+
15
+ - [React Storybook](https://lofcz.github.io/edix/)
16
+ - [Framework examples](#other-examples)
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ npm install edix@npm:@lofcz/edix
22
+ ```
23
+
24
+ This uses an [npm alias](https://docs.npmjs.com/cli/commands/npm-install) so you can keep using `import { ... } from "edix"` in your code while installing the `@lofcz/edix` fork.
25
+
26
+ Alternatively, install directly and import from `@lofcz/edix`:
27
+
28
+ ```sh
29
+ npm install @lofcz/edix
30
+ ```
31
+
32
+ `typescript >=5.0` is recommended.
33
+
34
+ ### Supported browsers
35
+
36
+ Browser versions supporting [beforeinput event](https://developer.mozilla.org/en-US/docs/Web/API/Element/beforeinput_event#browser_compatibility) are supported.
37
+
38
+ Mobile browsers are also supported, but with some issues (https://github.com/lofcz/edix/issues/97).
39
+
40
+ ## Getting started
41
+
42
+ 1. Define your contents declaratively. There are rules you have to follow:
43
+ - You must render `<br/>` in empty row (limitation of contenteditable).
44
+ - If `singleline` option is
45
+ - `false` or undefined, direct children of the root are treated as rows. They must be elements, not text.
46
+ - `true`, direct children of the root are treated as inline nodes.
47
+ - (TODO)
48
+
49
+ 2. Initialize `Editor` with `createPlainEditor`/`createEditor`.
50
+ 3. Call `Editor.input` on mount, with `HTMLElement` which is the root of editable contents.
51
+ 4. Update your state with `onChange`, which will be called on edit.
52
+ 5. Call returned function from `Editor.input` on unmount for cleanup.
53
+
54
+ Here is an example for React.
55
+
56
+ ### Plain text
57
+
58
+ ```tsx
59
+ import { useState, useEffect, useRef } from "react";
60
+ import { createPlainEditor } from "edix";
61
+
62
+ export const App = () => {
63
+ const ref = useRef<HTMLDivElement>(null);
64
+ const [text, setText] = useState("Hello world.");
65
+
66
+ useEffect(() => {
67
+ // 2. init
68
+ const editor = createPlainEditor({
69
+ text: text,
70
+ onChange: (v) => {
71
+ // 4. update state
72
+ setText(v);
73
+ },
74
+ });
75
+ // 3. bind to DOM
76
+ const cleanup = editor.input(ref.current);
77
+ return () => {
78
+ // 5. cleanup DOM
79
+ cleanup();
80
+ };
81
+ }, []);
82
+
83
+ // 1. render contents from state
84
+ return (
85
+ <div
86
+ ref={ref}
87
+ style={{
88
+ backgroundColor: "white",
89
+ border: "solid 1px darkgray",
90
+ padding: 8,
91
+ }}
92
+ >
93
+ {text.split("\n").map((t, i) => (
94
+ <div key={i}>{t ? t : <br />}</div>
95
+ ))}
96
+ </div>
97
+ );
98
+ };
99
+ ```
100
+
101
+ ### Rich text
102
+
103
+ [Standard Schema](https://github.com/standard-schema/standard-schema) is supported.
104
+
105
+ ```tsx
106
+ import { useState, useEffect, useRef, useMemo } from "react";
107
+ import { createEditor, ToggleFormat } from "edix";
108
+ import * as v from "valibot";
109
+
110
+ const schema = v.strictObject({
111
+ children: v.array(
112
+ v.array(
113
+ v.strictObject({
114
+ text: v.string(),
115
+ bold: v.optional(v.boolean()),
116
+ italic: v.optional(v.boolean()),
117
+ }),
118
+ ),
119
+ ),
120
+ });
121
+
122
+ export const App = () => {
123
+ const ref = useRef<HTMLDivElement>(null);
124
+
125
+ type Doc = v.InferOutput<typeof schema>;
126
+ const [doc, setDoc] = useState<Doc>({
127
+ children: [
128
+ [
129
+ { text: "Hello", bold: true },
130
+ { text: " " },
131
+ { text: "World", italic: true },
132
+ { text: "." },
133
+ ],
134
+ ],
135
+ });
136
+
137
+ const editor = useMemo(
138
+ () =>
139
+ createEditor({
140
+ doc,
141
+ schema,
142
+ onChange: setDoc,
143
+ }),
144
+ [],
145
+ );
146
+
147
+ useEffect(() => {
148
+ return editor.input(ref.current);
149
+ }, []);
150
+
151
+ return (
152
+ <div>
153
+ <div>
154
+ <button
155
+ onClick={() => {
156
+ editor.apply(ToggleFormat, "bold");
157
+ }}
158
+ >
159
+ bold
160
+ </button>
161
+ <button
162
+ onClick={() => {
163
+ editor.apply(ToggleFormat, "italic");
164
+ }}
165
+ >
166
+ italic
167
+ </button>
168
+ </div>
169
+ <div
170
+ ref={ref}
171
+ style={{
172
+ backgroundColor: "white",
173
+ border: "solid 1px darkgray",
174
+ padding: 8,
175
+ }}
176
+ >
177
+ {doc.children.map((r, i) => (
178
+ <div key={i}>
179
+ {r.map((n, j) => (
180
+ <span
181
+ key={j}
182
+ style={{
183
+ fontWeight: n.bold ? "bold" : undefined,
184
+ fontStyle: n.italic ? "italic" : undefined,
185
+ }}
186
+ >
187
+ {n.text || <br />}
188
+ </span>
189
+ ))}
190
+ </div>
191
+ ))}
192
+ </div>
193
+ </div>
194
+ );
195
+ };
196
+ ```
197
+
198
+ ### Other examples
199
+
200
+ - React ([Demo](https://lofcz.github.io/edix/react), [Source](./examples/react))
201
+ - Vue ([Demo](https://lofcz.github.io/edix/vue), [Source](./examples/vue))
202
+ - Svelte ([Demo](https://lofcz.github.io/edix/svelte), [Source](./examples/svelte))
203
+ - Solid ([Demo](https://lofcz.github.io/edix/solid), [Source](./examples/solid))
204
+ - Angular ([Demo](https://lofcz.github.io/edix/angular), [Source](./examples/angular))
205
+ - Preact ([Demo](https://lofcz.github.io/edix/preact), [Source](./examples/preact))
206
+ - Vanilla ([Demo](https://lofcz.github.io/edix/vanilla), [Source](./examples/vanilla))
207
+
208
+ ...and more! Contribution welcome!
209
+
210
+ ## Documentation
211
+
212
+ - [API reference](./docs/API.md)
213
+ - [Storybook examples](./stories) for more usages
214
+
215
+ ## Contribute
216
+
217
+ All contributions are welcome.
218
+ If you find a problem, feel free to create an [issue](https://github.com/lofcz/edix/issues) or a [PR](https://github.com/lofcz/edix/pulls). If you have a question, ask in [discussions](https://github.com/lofcz/edix/discussions).
219
+
220
+ ### Making a Pull Request
221
+
222
+ 1. Fork this repo.
223
+ 2. Run `npm install`.
224
+ 3. Commit your fix.
225
+ 4. Make a PR and confirm all the CI checks passed.
226
+
227
+ ## Inspirations
228
+
229
+ - [ProseMirror](https://prosemirror.net/)
230
+ - [Slate](https://github.com/ianstormtaylor/slate)
231
+ - [Lexical](https://github.com/facebook/lexical)
232
+ - [Quill](https://github.com/slab/quill)
233
+ - [Tiptap](https://github.com/ueberdosis/tiptap)
234
+ - [Draft.js](https://github.com/facebookarchive/draft-js)
235
+ - [rich-textarea](https://github.com/inokawa/rich-textarea) (my early project)
236
+ - [use-editable](https://github.com/FormidableLabs/use-editable)
237
+ - [@react-libraries/markdown-editor](https://github.com/ReactLibraries/markdown-editor)
238
+ - [VS Code](https://github.com/microsoft/vscode)
239
+ - [Language Server Protocol](https://github.com/microsoft/language-server-protocol)
240
+ - [urql](https://github.com/urql-graphql/urql)
241
+ - [TanStack DB](https://github.com/tanstack/db)
242
+ - [Hono](https://github.com/honojs/hono)
243
+ - [Textbus](https://github.com/textbus/textbus)
244
+ - [vistree](https://github.com/mizchi/vistree)
245
+ - Proposed [EditContext API](https://github.com/w3c/edit-context)
246
+ - Proposed [Richer Text Fields](https://open-ui.org/components/richer-text-fields.explainer/) in [Open UI](https://open-ui.org/)
@@ -0,0 +1,41 @@
1
+ import type { Editor } from "./editor.js";
2
+ import type { DocNode, InferNode, Position, PositionRange, TextNode } from "./doc/types.js";
3
+ export type EditorCommand<A extends unknown[], T extends DocNode> = (this: Editor<T>, ...args: A) => void;
4
+ /**
5
+ * Delete content in the selection or specified range.
6
+ */
7
+ export declare function Delete(this: Editor, range?: PositionRange): void;
8
+ /**
9
+ * Insert text at the caret or specified position.
10
+ */
11
+ export declare function InsertText(this: Editor, text: string, position?: Position): void;
12
+ /**
13
+ * Insert node at the caret or specified position.
14
+ */
15
+ export declare function InsertNode<T extends DocNode>(this: Editor<T>, node: Exclude<InferNode<T>, TextNode>, position?: Position): void;
16
+ /**
17
+ * Insert multiple inline nodes as a single line fragment in one transaction.
18
+ * When `moveCaret` is true (default), the caret moves to the end of the
19
+ * inserted content.
20
+ */
21
+ export declare function InsertNodes<T extends DocNode>(this: Editor<T>, nodes: InferNode<T>[], position?: Position, moveCaret?: boolean): void;
22
+ /**
23
+ * Replace text in the selection or specified range.
24
+ */
25
+ export declare function ReplaceText(this: Editor, text: string): void;
26
+ /**
27
+ * Replace all content in the editor.
28
+ */
29
+ export declare function ReplaceAll(this: Editor, text: string): void;
30
+ type ToggleableKey<T> = {
31
+ [K in keyof T]-?: T[K] extends boolean | undefined ? K : never;
32
+ }[keyof T];
33
+ /**
34
+ * Format content in the selection or specified range.
35
+ */
36
+ export declare function Format<T extends DocNode, N extends Omit<InferNode<T>, "text">, K extends Extract<keyof N, string>>(this: Editor<T>, key: K, value: N[K], range?: PositionRange): void;
37
+ /**
38
+ * Toggle formatting in the selection or specified range.
39
+ */
40
+ export declare function ToggleFormat<T extends DocNode>(this: Editor<T>, key: Extract<ToggleableKey<Omit<InferNode<T>, "text">>, string>, range?: PositionRange): void;
41
+ export {};
@@ -0,0 +1,41 @@
1
+ import type { Fragment, Position, SelectionSnapshot } from "./types.js";
2
+ declare const TYPE_DELETE = "delete";
3
+ type DeleteOperation = Readonly<{
4
+ type: typeof TYPE_DELETE;
5
+ start: Position;
6
+ end: Position;
7
+ }>;
8
+ declare const TYPE_INSERT_TEXT = "insert_text";
9
+ type InsertOperation = Readonly<{
10
+ type: typeof TYPE_INSERT_TEXT;
11
+ at: Position;
12
+ text: string;
13
+ }>;
14
+ declare const TYPE_INSERT_NODE = "insert_node";
15
+ type InsertNodeOperation = Readonly<{
16
+ type: typeof TYPE_INSERT_NODE;
17
+ at: Position;
18
+ fragment: Fragment;
19
+ }>;
20
+ declare const TYPE_SET_ATTR = "set_attr";
21
+ type SetAttrOperation = Readonly<{
22
+ type: typeof TYPE_SET_ATTR;
23
+ start: Position;
24
+ end: Position;
25
+ key: string;
26
+ value: unknown;
27
+ }>;
28
+ export type Operation = DeleteOperation | InsertOperation | InsertNodeOperation | SetAttrOperation;
29
+ export declare class Transaction {
30
+ private readonly _ops;
31
+ selection?: SelectionSnapshot;
32
+ constructor(ops?: readonly Operation[]);
33
+ get ops(): readonly Operation[];
34
+ insertText(start: Position, text: string): this;
35
+ insertFragment(start: Position, fragment: Fragment): this;
36
+ delete(start: Position, end: Position): this;
37
+ attr(start: Position, end: Position, key: string, value: unknown): this;
38
+ transform(position: Position): Position;
39
+ }
40
+ export declare const rebasePosition: (position: Position, op: Operation) => Position;
41
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ export interface TextNode {
2
+ readonly text: string;
3
+ }
4
+ export interface VoidNode {
5
+ }
6
+ export type InlineNode = TextNode | VoidNode;
7
+ export interface DocNode {
8
+ readonly children: readonly (readonly InlineNode[])[];
9
+ }
10
+ export type Fragment = DocNode["children"];
11
+ export type InferNode<T extends DocNode> = T["children"][number][number];
12
+ export type Path = readonly [number?];
13
+ export type Position = readonly [path: Path, offset: number];
14
+ export type PositionRange = readonly [start: Position, end: Position];
15
+ export type SelectionSnapshot = readonly [anchor: Position, focus: Position];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export { defaultIsBlockNode, defaultIsVoidNode } from "./default.js";
@@ -0,0 +1,2 @@
1
+ export interface ParserConfig {
2
+ }
@@ -0,0 +1,102 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import type { DocNode, SelectionSnapshot } from "./doc/types.js";
3
+ import type { EditorCommand } from "./commands.js";
4
+ import { Transaction } from "./doc/edit.js";
5
+ import type { EditorPlugin } from "./plugins/types.js";
6
+ import { type CopyExtension, type PasteExtension } from "./extensions/index.js";
7
+ import { type KeyboardHandler } from "./hotkey.js";
8
+ /**
9
+ * Options of {@link createEditor}.
10
+ */
11
+ export interface EditorOptions<T extends DocNode, S extends StandardSchemaV1<T, T> | void = void> {
12
+ /**
13
+ * Optional [Standard Schema](https://github.com/standard-schema/standard-schema) to validate unsafe edits.
14
+ */
15
+ schema?: S;
16
+ /**
17
+ * Initial document.
18
+ */
19
+ doc: T;
20
+ /**
21
+ * The state editable or not.
22
+ */
23
+ readonly?: boolean;
24
+ /**
25
+ * TODO
26
+ */
27
+ plugins?: EditorPlugin[];
28
+ /**
29
+ * Functions to handle keyboard events.
30
+ *
31
+ * Return `true` if you want to stop propagation.
32
+ */
33
+ keyboard?: KeyboardHandler[];
34
+ /**
35
+ * Functions to handle copy events
36
+ * @default [plainCopy()]
37
+ */
38
+ copy?: [CopyExtension, ...rest: CopyExtension[]];
39
+ /**
40
+ * Functions to handle paste / drop events
41
+ * @default [plainPaste()]
42
+ */
43
+ paste?: [PasteExtension, ...rest: PasteExtension[]];
44
+ /**
45
+ * TODO
46
+ */
47
+ isBlock?: (node: HTMLElement) => boolean;
48
+ /**
49
+ * Automatically scroll the mounted element to keep the caret visible
50
+ * after document changes. Scroll is coalesced via rAF for zero
51
+ * synchronous layout cost during input handling.
52
+ */
53
+ autoScroll?: boolean;
54
+ /**
55
+ * Callback invoked when document changes.
56
+ */
57
+ onChange: (doc: T) => void;
58
+ /**
59
+ * Callback invoked when errors happen.
60
+ *
61
+ * @default console.error
62
+ */
63
+ onError?: (message: string) => void;
64
+ }
65
+ /**
66
+ * The editor instance.
67
+ */
68
+ export interface Editor<T extends DocNode = DocNode> {
69
+ readonly doc: T;
70
+ /**
71
+ * Whether the document is empty (no text content, no void nodes).
72
+ * Recomputed once per commit — O(1) read.
73
+ */
74
+ readonly isEmpty: boolean;
75
+ selection: SelectionSnapshot;
76
+ /**
77
+ * The getter/setter for the editor's read-only state.
78
+ * `true` to read-only. `false` to editable.
79
+ */
80
+ readonly: boolean;
81
+ /**
82
+ * Enable/disable auto-scroll after document changes.
83
+ */
84
+ autoScroll: boolean;
85
+ /**
86
+ * Dispatches editing operations.
87
+ * @param tr {@link Transaction} or {@link EditorCommand}
88
+ * @param args arguments of {@link EditorCommand}
89
+ * @param immediate If true, flushes queued operations immediately.
90
+ */
91
+ apply(tr: Transaction, immediate?: boolean): this;
92
+ apply<A extends unknown[]>(fn: EditorCommand<A, T>, ...args: A): this;
93
+ /**
94
+ * A function to make DOM editable.
95
+ * @returns A function to stop subscribing DOM changes and restores previous DOM state.
96
+ */
97
+ input: (element: HTMLElement) => () => void;
98
+ }
99
+ /**
100
+ * A function to initialize {@link Editor}.
101
+ */
102
+ export declare const createEditor: <T extends DocNode, S extends StandardSchemaV1<T, T> | void = void>({ doc, readonly, schema, plugins, keyboard, copy: copyExtensions, paste: pasteExtensions, isBlock, autoScroll: _autoScroll, onChange, onError, }: EditorOptions<T, S>) => Editor<T>;
@@ -0,0 +1,5 @@
1
+ import type { CopyExtension } from "./types.js";
2
+ /**
3
+ * An extension to handle copying to HTML.
4
+ */
5
+ export declare const htmlCopy: () => CopyExtension;
@@ -0,0 +1,4 @@
1
+ export { plainCopy } from "./plain.js";
2
+ export { htmlCopy } from "./html.js";
3
+ export { internalCopy } from "./internal.js";
4
+ export type { CopyExtension } from "./types.js";
@@ -0,0 +1,7 @@
1
+ import type { CopyExtension } from "./types.js";
2
+ /**
3
+ * An extension to handle copying to edix editor instance.
4
+ */
5
+ export declare const internalCopy: ({ key, }?: {
6
+ key?: string;
7
+ }) => CopyExtension;
@@ -0,0 +1,6 @@
1
+ import type { DocNode, InferNode } from "../../doc/types.js";
2
+ import type { CopyExtension } from "./types.js";
3
+ /**
4
+ * An extension to handle copying to plain text.
5
+ */
6
+ export declare const plainCopy: <T extends DocNode>(serializer?: (node: InferNode<T>) => string) => CopyExtension;
@@ -0,0 +1,2 @@
1
+ import type { Fragment } from "../../doc/types.js";
2
+ export type CopyExtension = (dataTransfer: DataTransfer, doc: Fragment, element: Element) => void;
@@ -0,0 +1,2 @@
1
+ export * from "./copy/index.js";
2
+ export * from "./paste/index.js";
@@ -0,0 +1,6 @@
1
+ import type { InlineNode } from "../../doc/types.js";
2
+ import type { PasteExtension } from "./types.js";
3
+ /**
4
+ * An extension to handle pasting / dropping from File.
5
+ */
6
+ export declare const filePaste: (handlerByMime: Record<string, (file: File) => InlineNode>) => PasteExtension;
@@ -0,0 +1,6 @@
1
+ import type { DocNode, InferNode, TextNode } from "../../doc/types.js";
2
+ import type { PasteExtension } from "./types.js";
3
+ /**
4
+ * An extension to handle pasting / dropping from HTML.
5
+ */
6
+ export declare const htmlPaste: <T extends DocNode>(serializeText: (t: string) => Extract<InferNode<T>, TextNode>, serializers?: ((node: HTMLElement) => Exclude<InferNode<T>, TextNode> | void)[]) => PasteExtension;
@@ -0,0 +1,5 @@
1
+ export { filePaste } from "./file.js";
2
+ export { plainPaste } from "./plain.js";
3
+ export { htmlPaste } from "./html.js";
4
+ export { internalPaste } from "./internal.js";
5
+ export type { PasteExtension } from "./types.js";
@@ -0,0 +1,7 @@
1
+ import type { PasteExtension } from "./types.js";
2
+ /**
3
+ * An extension to handle pasting / dropping from edix editor instance.
4
+ */
5
+ export declare const internalPaste: ({ key, }?: {
6
+ key?: string;
7
+ }) => PasteExtension;
@@ -0,0 +1,5 @@
1
+ import type { PasteExtension } from "./types.js";
2
+ /**
3
+ * An extension to handle pasting / dropping from plain text.
4
+ */
5
+ export declare const plainPaste: () => PasteExtension;
@@ -0,0 +1,3 @@
1
+ import type { Fragment } from "../../doc/types.js";
2
+ import type { ParserConfig } from "../../dom/parser.js";
3
+ export type PasteExtension = (dataTransfer: DataTransfer, config: ParserConfig) => string | Fragment | null;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ export type KeyboardHandler = (keyboard: KeyboardEvent) => boolean | void;
2
+ /**
3
+ * TODO
4
+ */
5
+ export declare const hotkey: (key: string, cb: (e: KeyboardEvent) => void, { mod, shift, alt, }?: {
6
+ mod?: boolean;
7
+ shift?: boolean;
8
+ alt?: boolean;
9
+ }) => KeyboardHandler;