@react-x11/components 0.8.0 → 0.9.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/README.md +95 -34
- package/dist/charts/node.d.ts +1 -1
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +6 -2
- package/dist/charts/node.js.map +1 -1
- package/dist/embed/client.d.ts +48 -3
- package/dist/embed/client.d.ts.map +1 -1
- package/dist/embed/client.js +79 -5
- package/dist/embed/client.js.map +1 -1
- package/dist/embed/index.d.ts +1 -1
- package/dist/embed/index.d.ts.map +1 -1
- package/dist/embed/index.js +1 -1
- package/dist/embed/index.js.map +1 -1
- package/dist/html/node.d.ts +3 -1
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +7 -5
- package/dist/html/node.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/{markdown → internal/markdown}/ast.d.ts +12 -0
- package/dist/internal/markdown/ast.d.ts.map +1 -0
- package/dist/internal/markdown/ast.js +22 -0
- package/dist/internal/markdown/ast.js.map +1 -0
- package/dist/internal/markdown/parse.d.ts.map +1 -0
- package/dist/{markdown → internal/markdown}/parse.js +112 -22
- package/dist/internal/markdown/parse.js.map +1 -0
- package/dist/internal/markdown/stringify.d.ts +4 -0
- package/dist/internal/markdown/stringify.d.ts.map +1 -0
- package/dist/internal/markdown/stringify.js +684 -0
- package/dist/internal/markdown/stringify.js.map +1 -0
- package/dist/internal/markdown/tags.d.ts.map +1 -0
- package/dist/internal/markdown/tags.js.map +1 -0
- package/dist/internal/scroll.d.ts +3 -2
- package/dist/internal/scroll.d.ts.map +1 -1
- package/dist/internal/scroll.js +1 -0
- package/dist/internal/scroll.js.map +1 -1
- package/dist/maps/anchors.d.ts +98 -0
- package/dist/maps/anchors.d.ts.map +1 -0
- package/dist/maps/anchors.js +531 -0
- package/dist/maps/anchors.js.map +1 -0
- package/dist/maps/controller.d.ts +283 -0
- package/dist/maps/controller.d.ts.map +1 -0
- package/dist/maps/controller.js +743 -0
- package/dist/maps/controller.js.map +1 -0
- package/dist/maps/gl/buckets.d.ts +95 -0
- package/dist/maps/gl/buckets.d.ts.map +1 -0
- package/dist/maps/gl/buckets.js +362 -0
- package/dist/maps/gl/buckets.js.map +1 -0
- package/dist/maps/gl/build-worker.d.ts +13 -0
- package/dist/maps/gl/build-worker.d.ts.map +1 -0
- package/dist/maps/gl/build-worker.js +44 -0
- package/dist/maps/gl/build-worker.js.map +1 -0
- package/dist/maps/gl/color.d.ts +8 -0
- package/dist/maps/gl/color.d.ts.map +1 -0
- package/dist/maps/gl/color.js +57 -0
- package/dist/maps/gl/color.js.map +1 -0
- package/dist/maps/gl/cover.d.ts +51 -0
- package/dist/maps/gl/cover.d.ts.map +1 -0
- package/dist/maps/gl/cover.js +124 -0
- package/dist/maps/gl/cover.js.map +1 -0
- package/dist/maps/gl/index.d.ts +3 -0
- package/dist/maps/gl/index.d.ts.map +1 -0
- package/dist/maps/gl/index.js +20 -0
- package/dist/maps/gl/index.js.map +1 -0
- package/dist/maps/gl/markers.d.ts +28 -0
- package/dist/maps/gl/markers.d.ts.map +1 -0
- package/dist/maps/gl/markers.js +60 -0
- package/dist/maps/gl/markers.js.map +1 -0
- package/dist/maps/gl/overlays.d.ts +68 -0
- package/dist/maps/gl/overlays.d.ts.map +1 -0
- package/dist/maps/gl/overlays.js +300 -0
- package/dist/maps/gl/overlays.js.map +1 -0
- package/dist/maps/gl/pane.d.ts +12 -0
- package/dist/maps/gl/pane.d.ts.map +1 -0
- package/dist/maps/gl/pane.js +37 -0
- package/dist/maps/gl/pane.js.map +1 -0
- package/dist/maps/gl/placement.d.ts +111 -0
- package/dist/maps/gl/placement.d.ts.map +1 -0
- package/dist/maps/gl/placement.js +567 -0
- package/dist/maps/gl/placement.js.map +1 -0
- package/dist/maps/gl/renderer.d.ts +352 -0
- package/dist/maps/gl/renderer.d.ts.map +1 -0
- package/dist/maps/gl/renderer.js +1127 -0
- package/dist/maps/gl/renderer.js.map +1 -0
- package/dist/maps/gl/shaders.d.ts +120 -0
- package/dist/maps/gl/shaders.d.ts.map +1 -0
- package/dist/maps/gl/shaders.js +451 -0
- package/dist/maps/gl/shaders.js.map +1 -0
- package/dist/maps/gl/store.d.ts +115 -0
- package/dist/maps/gl/store.d.ts.map +1 -0
- package/dist/maps/gl/store.js +469 -0
- package/dist/maps/gl/store.js.map +1 -0
- package/dist/maps/gl/text.d.ts +154 -0
- package/dist/maps/gl/text.d.ts.map +1 -0
- package/dist/maps/gl/text.js +382 -0
- package/dist/maps/gl/text.js.map +1 -0
- package/dist/maps/gl/view.d.ts +54 -0
- package/dist/maps/gl/view.d.ts.map +1 -0
- package/dist/maps/gl/view.js +974 -0
- package/dist/maps/gl/view.js.map +1 -0
- package/dist/maps/index.d.ts +1 -1
- package/dist/maps/index.d.ts.map +1 -1
- package/dist/maps/index.js +216 -52
- package/dist/maps/index.js.map +1 -1
- package/dist/maps/labels.d.ts +37 -16
- package/dist/maps/labels.d.ts.map +1 -1
- package/dist/maps/labels.js +188 -119
- package/dist/maps/labels.js.map +1 -1
- package/dist/maps/node.d.ts +162 -56
- package/dist/maps/node.d.ts.map +1 -1
- package/dist/maps/node.js +607 -515
- package/dist/maps/node.js.map +1 -1
- package/dist/maps/overlay.d.ts +72 -9
- package/dist/maps/overlay.d.ts.map +1 -1
- package/dist/maps/overlay.js +108 -28
- package/dist/maps/overlay.js.map +1 -1
- package/dist/maps/paint.d.ts +3 -0
- package/dist/maps/paint.d.ts.map +1 -1
- package/dist/maps/paint.js.map +1 -1
- package/dist/maps/proj.d.ts +14 -0
- package/dist/maps/proj.d.ts.map +1 -1
- package/dist/maps/proj.js +28 -0
- package/dist/maps/proj.js.map +1 -1
- package/dist/maps/renderer.d.ts +69 -0
- package/dist/maps/renderer.d.ts.map +1 -0
- package/dist/maps/renderer.js +66 -0
- package/dist/maps/renderer.js.map +1 -0
- package/dist/maps/sources.d.ts +25 -3
- package/dist/maps/sources.d.ts.map +1 -1
- package/dist/maps/sources.js +17 -0
- package/dist/maps/sources.js.map +1 -1
- package/dist/maps/theme.d.ts +19 -0
- package/dist/maps/theme.d.ts.map +1 -0
- package/dist/maps/theme.js +44 -0
- package/dist/maps/theme.js.map +1 -0
- package/dist/maps/tiles.d.ts +117 -17
- package/dist/maps/tiles.d.ts.map +1 -1
- package/dist/maps/tiles.js +163 -30
- package/dist/maps/tiles.js.map +1 -1
- package/dist/maps/types.d.ts +188 -20
- package/dist/maps/types.d.ts.map +1 -1
- package/dist/markdown/expressions.d.ts +1 -1
- package/dist/markdown/expressions.d.ts.map +1 -1
- package/dist/markdown/expressions.js.map +1 -1
- package/dist/markdown/index.d.ts +7 -6
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +9 -7
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/spans.d.ts +1 -1
- package/dist/markdown/spans.d.ts.map +1 -1
- package/dist/media-player/index.d.ts +11 -3
- package/dist/media-player/index.d.ts.map +1 -1
- package/dist/media-player/index.js +13 -2
- package/dist/media-player/index.js.map +1 -1
- package/dist/rich-text-editor/clipboard.d.ts +23 -0
- package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
- package/dist/rich-text-editor/clipboard.js +307 -0
- package/dist/rich-text-editor/clipboard.js.map +1 -0
- package/dist/rich-text-editor/collab.d.ts +32 -0
- package/dist/rich-text-editor/collab.d.ts.map +1 -0
- package/dist/rich-text-editor/collab.js +55 -0
- package/dist/rich-text-editor/collab.js.map +1 -0
- package/dist/rich-text-editor/commands.d.ts +50 -0
- package/dist/rich-text-editor/commands.d.ts.map +1 -0
- package/dist/rich-text-editor/commands.js +376 -0
- package/dist/rich-text-editor/commands.js.map +1 -0
- package/dist/rich-text-editor/drag.d.ts +68 -0
- package/dist/rich-text-editor/drag.d.ts.map +1 -0
- package/dist/rich-text-editor/drag.js +148 -0
- package/dist/rich-text-editor/drag.js.map +1 -0
- package/dist/rich-text-editor/html.d.ts +145 -0
- package/dist/rich-text-editor/html.d.ts.map +1 -0
- package/dist/rich-text-editor/html.js +516 -0
- package/dist/rich-text-editor/html.js.map +1 -0
- package/dist/rich-text-editor/index.d.ts +263 -0
- package/dist/rich-text-editor/index.d.ts.map +1 -0
- package/dist/rich-text-editor/index.js +822 -0
- package/dist/rich-text-editor/index.js.map +1 -0
- package/dist/rich-text-editor/inline.d.ts +94 -0
- package/dist/rich-text-editor/inline.d.ts.map +1 -0
- package/dist/rich-text-editor/inline.js +252 -0
- package/dist/rich-text-editor/inline.js.map +1 -0
- package/dist/rich-text-editor/keymap.d.ts +71 -0
- package/dist/rich-text-editor/keymap.d.ts.map +1 -0
- package/dist/rich-text-editor/keymap.js +393 -0
- package/dist/rich-text-editor/keymap.js.map +1 -0
- package/dist/rich-text-editor/keys.d.ts +29 -0
- package/dist/rich-text-editor/keys.d.ts.map +1 -0
- package/dist/rich-text-editor/keys.js +93 -0
- package/dist/rich-text-editor/keys.js.map +1 -0
- package/dist/rich-text-editor/look.d.ts +71 -0
- package/dist/rich-text-editor/look.d.ts.map +1 -0
- package/dist/rich-text-editor/look.js +342 -0
- package/dist/rich-text-editor/look.js.map +1 -0
- package/dist/rich-text-editor/markdown.d.ts +18 -0
- package/dist/rich-text-editor/markdown.d.ts.map +1 -0
- package/dist/rich-text-editor/markdown.js +496 -0
- package/dist/rich-text-editor/markdown.js.map +1 -0
- package/dist/rich-text-editor/nodes.d.ts +135 -0
- package/dist/rich-text-editor/nodes.d.ts.map +1 -0
- package/dist/rich-text-editor/nodes.js +397 -0
- package/dist/rich-text-editor/nodes.js.map +1 -0
- package/dist/rich-text-editor/render.d.ts +68 -0
- package/dist/rich-text-editor/render.d.ts.map +1 -0
- package/dist/rich-text-editor/render.js +500 -0
- package/dist/rich-text-editor/render.js.map +1 -0
- package/dist/rich-text-editor/schema.d.ts +16 -0
- package/dist/rich-text-editor/schema.d.ts.map +1 -0
- package/dist/rich-text-editor/schema.js +386 -0
- package/dist/rich-text-editor/schema.js.map +1 -0
- package/dist/rich-text-editor/store.d.ts +11 -0
- package/dist/rich-text-editor/store.d.ts.map +1 -0
- package/dist/rich-text-editor/store.js +43 -0
- package/dist/rich-text-editor/store.js.map +1 -0
- package/dist/rich-text-editor/suggest.d.ts +127 -0
- package/dist/rich-text-editor/suggest.d.ts.map +1 -0
- package/dist/rich-text-editor/suggest.js +0 -0
- package/dist/rich-text-editor/suggest.js.map +1 -0
- package/dist/rich-text-editor/tables.d.ts +41 -0
- package/dist/rich-text-editor/tables.d.ts.map +1 -0
- package/dist/rich-text-editor/tables.js +204 -0
- package/dist/rich-text-editor/tables.js.map +1 -0
- package/dist/rich-text-editor/toolbar.d.ts +58 -0
- package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
- package/dist/rich-text-editor/toolbar.js +548 -0
- package/dist/rich-text-editor/toolbar.js.map +1 -0
- package/dist/rich-text-editor/view.d.ts +368 -0
- package/dist/rich-text-editor/view.d.ts.map +1 -0
- package/dist/rich-text-editor/view.js +1968 -0
- package/dist/rich-text-editor/view.js.map +1 -0
- package/dist/rich-text-editor/virtual.d.ts +54 -0
- package/dist/rich-text-editor/virtual.d.ts.map +1 -0
- package/dist/rich-text-editor/virtual.js +262 -0
- package/dist/rich-text-editor/virtual.js.map +1 -0
- package/dist/richtext/node.d.ts +25 -2
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +46 -24
- package/dist/richtext/node.js.map +1 -1
- package/dist/terminal/backends.d.ts +4 -1
- package/dist/terminal/backends.d.ts.map +1 -1
- package/dist/terminal/backends.js.map +1 -1
- package/dist/terminal/index.d.ts +18 -7
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +28 -11
- package/dist/terminal/index.js.map +1 -1
- package/package.json +23 -4
- package/src/charts/node.ts +6 -2
- package/src/embed/client.ts +102 -8
- package/src/embed/index.ts +2 -0
- package/src/html/node.ts +7 -5
- package/src/index.ts +2 -0
- package/src/{markdown → internal/markdown}/ast.ts +24 -7
- package/src/{markdown → internal/markdown}/parse.ts +134 -24
- package/src/internal/markdown/stringify.ts +772 -0
- package/src/internal/scroll.ts +5 -3
- package/src/maps/anchors.ts +607 -0
- package/src/maps/controller.ts +999 -0
- package/src/maps/gl/buckets.ts +457 -0
- package/src/maps/gl/build-worker.ts +55 -0
- package/src/maps/gl/color.ts +62 -0
- package/src/maps/gl/cover.ts +170 -0
- package/src/maps/gl/index.ts +23 -0
- package/src/maps/gl/markers.ts +83 -0
- package/src/maps/gl/overlays.ts +379 -0
- package/src/maps/gl/pane.ts +42 -0
- package/src/maps/gl/placement.ts +816 -0
- package/src/maps/gl/renderer.ts +1576 -0
- package/src/maps/gl/shaders.ts +486 -0
- package/src/maps/gl/store.ts +566 -0
- package/src/maps/gl/text.ts +495 -0
- package/src/maps/gl/view.ts +1224 -0
- package/src/maps/index.ts +270 -81
- package/src/maps/labels.ts +214 -121
- package/src/maps/node.ts +763 -629
- package/src/maps/overlay.ts +148 -43
- package/src/maps/paint.ts +3 -0
- package/src/maps/proj.ts +31 -0
- package/src/maps/renderer.ts +155 -0
- package/src/maps/sources.ts +38 -3
- package/src/maps/theme.ts +51 -0
- package/src/maps/tiles.ts +191 -33
- package/src/maps/types.ts +188 -20
- package/src/markdown/expressions.ts +5 -1
- package/src/markdown/index.ts +14 -11
- package/src/markdown/spans.ts +1 -1
- package/src/media-player/index.ts +18 -4
- package/src/rich-text-editor/clipboard.ts +439 -0
- package/src/rich-text-editor/collab.ts +91 -0
- package/src/rich-text-editor/commands.ts +448 -0
- package/src/rich-text-editor/dom-globals.d.ts +23 -0
- package/src/rich-text-editor/drag.ts +211 -0
- package/src/rich-text-editor/html.ts +619 -0
- package/src/rich-text-editor/index.ts +1331 -0
- package/src/rich-text-editor/inline.ts +345 -0
- package/src/rich-text-editor/keymap.ts +510 -0
- package/src/rich-text-editor/keys.ts +119 -0
- package/src/rich-text-editor/look.ts +427 -0
- package/src/rich-text-editor/markdown.ts +653 -0
- package/src/rich-text-editor/nodes.ts +497 -0
- package/src/rich-text-editor/render.ts +800 -0
- package/src/rich-text-editor/schema.ts +431 -0
- package/src/rich-text-editor/store.ts +39 -0
- package/src/rich-text-editor/suggest.ts +597 -0
- package/src/rich-text-editor/tables.ts +272 -0
- package/src/rich-text-editor/toolbar.ts +724 -0
- package/src/rich-text-editor/view.ts +2287 -0
- package/src/rich-text-editor/virtual.ts +347 -0
- package/src/richtext/node.ts +59 -30
- package/src/terminal/backends.ts +4 -1
- package/src/terminal/index.ts +43 -15
- package/dist/markdown/ast.d.ts.map +0 -1
- package/dist/markdown/ast.js +0 -17
- package/dist/markdown/ast.js.map +0 -1
- package/dist/markdown/parse.d.ts.map +0 -1
- package/dist/markdown/parse.js.map +0 -1
- package/dist/markdown/tags.d.ts.map +0 -1
- package/dist/markdown/tags.js.map +0 -1
- /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
- /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
- /package/dist/{markdown → internal/markdown}/tags.js +0 -0
- /package/src/{markdown → internal/markdown}/tags.ts +0 -0
|
@@ -0,0 +1,1331 @@
|
|
|
1
|
+
// <RichTextEditor> — a WYSIWYG editor: ProseMirror's document model and
|
|
2
|
+
// plugin system, drawn and driven by react-x11.
|
|
3
|
+
//
|
|
4
|
+
// The ladder, one element all the way up (docs/prd-rich-text-editor.md):
|
|
5
|
+
//
|
|
6
|
+
// 1. `defaultValue` / `value` + `onChange` — markdown in and out, and a
|
|
7
|
+
// caller never meets ProseMirror;
|
|
8
|
+
// 2. `toolbar`, `placeholder`, `readOnly`, `onSubmit`, `suggestions` — the
|
|
9
|
+
// chrome and the behaviours an app would otherwise wire by hand;
|
|
10
|
+
// 3. `format="html" | "text"`, `markStyles`, `renderImage`, `nodeViews` —
|
|
11
|
+
// what the document is written in, and how its parts look;
|
|
12
|
+
// 4. `plugins`, `editorProps`, `schema` — ProseMirror's own seams, so the
|
|
13
|
+
// ecosystem's plugins and schemas plug in as they are;
|
|
14
|
+
// 5. `state` + `dispatchTransaction` — the app owns the EditorState outright.
|
|
15
|
+
//
|
|
16
|
+
// **Registration happens when this module is evaluated** — the editor's two
|
|
17
|
+
// elements, and `<richtext>` for the toolbar's labels — which is the one side
|
|
18
|
+
// effect the package's tree-shaking contract allows (AGENTS.md).
|
|
19
|
+
//
|
|
20
|
+
// **Not in the barrel.** `src/index.ts` re-exports every other component;
|
|
21
|
+
// this one is imported from `@react-x11/components/rich-text-editor`.
|
|
22
|
+
// ProseMirror's declarations name DOM globals (`dom-globals.d.ts` has the
|
|
23
|
+
// four this repository supplies for itself), and an app importing *anything*
|
|
24
|
+
// from the barrel loads every re-exported module's declarations — so every
|
|
25
|
+
// app would inherit that requirement to use a calendar. The subpath keeps it
|
|
26
|
+
// with the apps that use the editor.
|
|
27
|
+
import React, {
|
|
28
|
+
useCallback,
|
|
29
|
+
useEffect,
|
|
30
|
+
useImperativeHandle,
|
|
31
|
+
useLayoutEffect,
|
|
32
|
+
useMemo,
|
|
33
|
+
useRef,
|
|
34
|
+
useState,
|
|
35
|
+
useSyncExternalStore,
|
|
36
|
+
} from 'react';
|
|
37
|
+
import type { ComponentType, ReactElement, ReactNode, Ref } from 'react';
|
|
38
|
+
import { useApp, useClipboard, useTheme } from 'react-x11';
|
|
39
|
+
import type {
|
|
40
|
+
DragEndEvent,
|
|
41
|
+
DragEvent,
|
|
42
|
+
DragSourceEvent,
|
|
43
|
+
DrawnNode,
|
|
44
|
+
DropEvent,
|
|
45
|
+
FocusEvent,
|
|
46
|
+
KeyboardEvent,
|
|
47
|
+
MouseEvent as X11MouseEvent,
|
|
48
|
+
ScrollableNode,
|
|
49
|
+
} from 'react-x11';
|
|
50
|
+
import { XK_ESCAPE, XK_KP_ENTER, XK_RETURN } from 'react-x11/keysyms';
|
|
51
|
+
import { tint } from 'react-x11/style';
|
|
52
|
+
import type { Style } from 'react-x11/style';
|
|
53
|
+
import { toggleMark } from 'prosemirror-commands';
|
|
54
|
+
import { redo, undo } from 'prosemirror-history';
|
|
55
|
+
import { Fragment, Slice } from 'prosemirror-model';
|
|
56
|
+
import type { Node as PMNode, Schema } from 'prosemirror-model';
|
|
57
|
+
import { EditorState, Selection } from 'prosemirror-state';
|
|
58
|
+
import type { Command, Plugin, Transaction } from 'prosemirror-state';
|
|
59
|
+
import type { EditorProps, EditorView } from 'prosemirror-view';
|
|
60
|
+
|
|
61
|
+
// Loads the module the JSX augmentation at the bottom targets — nothing in
|
|
62
|
+
// `src/` writes JSX, so the build has no other reason to resolve it.
|
|
63
|
+
import type {} from 'react-x11/jsx-runtime';
|
|
64
|
+
|
|
65
|
+
import { hx } from '../internal/hx.js';
|
|
66
|
+
import { registerRichText } from '../richtext/index.js';
|
|
67
|
+
import type { TextRun } from '../richtext/index.js';
|
|
68
|
+
import type { Language } from '../code-language/index.js';
|
|
69
|
+
import {
|
|
70
|
+
isBlockActive,
|
|
71
|
+
isMarkActive,
|
|
72
|
+
markAttrs,
|
|
73
|
+
setLink,
|
|
74
|
+
toggleBlockType,
|
|
75
|
+
toggleList,
|
|
76
|
+
toggleWrap,
|
|
77
|
+
} from './commands.js';
|
|
78
|
+
import { DROP_TYPES, SLICE_TYPE } from './drag.js';
|
|
79
|
+
import type { DropAnswer } from './drag.js';
|
|
80
|
+
import { docFromHTML, htmlFromContent } from './html.js';
|
|
81
|
+
import { defaultPlugins } from './keymap.js';
|
|
82
|
+
import { deriveLook } from './look.js';
|
|
83
|
+
import type { MarkStyle } from './look.js';
|
|
84
|
+
import { docFromText, markdownCodec, textFromDoc } from './markdown.js';
|
|
85
|
+
import type { MarkdownCodec } from './markdown.js';
|
|
86
|
+
import { registerEditorElements, ROOT_ELEMENT, TEXT_ELEMENT } from './nodes.js';
|
|
87
|
+
import { renderBlockRange, renderBlocks } from './render.js';
|
|
88
|
+
import type { ImageInfo, NodeViewProps, RenderContext } from './render.js';
|
|
89
|
+
import type { RunStyle } from './inline.js';
|
|
90
|
+
import { schema as defaultSchema } from './schema.js';
|
|
91
|
+
import {
|
|
92
|
+
acceptSuggestion,
|
|
93
|
+
SUGGESTION_PAGE,
|
|
94
|
+
suggesterFor,
|
|
95
|
+
suggestionState,
|
|
96
|
+
suggestions as suggestionPlugin,
|
|
97
|
+
} from './suggest.js';
|
|
98
|
+
import type { Suggester } from './suggest.js';
|
|
99
|
+
import {
|
|
100
|
+
DEFAULT_TOOLBAR,
|
|
101
|
+
resolveToolbar,
|
|
102
|
+
Toolbar,
|
|
103
|
+
toolbarItems,
|
|
104
|
+
} from './toolbar.js';
|
|
105
|
+
import type { ToolbarEntry } from './toolbar.js';
|
|
106
|
+
import { RichEditorView } from './view.js';
|
|
107
|
+
import type { ViewConfig, ViewProps } from './view.js';
|
|
108
|
+
import { useBlockWindow } from './virtual.js';
|
|
109
|
+
|
|
110
|
+
registerRichText();
|
|
111
|
+
registerEditorElements();
|
|
112
|
+
|
|
113
|
+
const h = React.createElement;
|
|
114
|
+
|
|
115
|
+
/** What a drag out of the editor offers to do: an editable one moves or
|
|
116
|
+
* copies, a read-only one only copies. */
|
|
117
|
+
const MOVE_OR_COPY: Array<'move' | 'copy'> = ['move', 'copy'];
|
|
118
|
+
const COPY_ONLY: Array<'move' | 'copy'> = ['copy'];
|
|
119
|
+
|
|
120
|
+
// --- props -------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/** What `value`, `defaultValue` and `ev.value` are written in. */
|
|
123
|
+
export type EditorFormat = 'markdown' | 'html' | 'text';
|
|
124
|
+
|
|
125
|
+
/** What `onChange` hears. `value` is serialized when it is first read, so a
|
|
126
|
+
* listener that only wants to know *that* something changed pays nothing. */
|
|
127
|
+
export interface RichTextEditorChangeEvent {
|
|
128
|
+
type: 'change';
|
|
129
|
+
/** The document, in `format`. */
|
|
130
|
+
readonly value: string;
|
|
131
|
+
readonly doc: PMNode;
|
|
132
|
+
readonly state: EditorState;
|
|
133
|
+
readonly transactions: readonly Transaction[];
|
|
134
|
+
readonly name: string | undefined;
|
|
135
|
+
readonly target: RichTextEditorHandle;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** What `onSubmit` and `onSelectionChange` hear. */
|
|
139
|
+
export interface RichTextEditorEvent {
|
|
140
|
+
type: 'submit' | 'selectionchange';
|
|
141
|
+
readonly value: string;
|
|
142
|
+
readonly state: EditorState;
|
|
143
|
+
readonly name: string | undefined;
|
|
144
|
+
readonly target: RichTextEditorHandle;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The editor's imperative surface — the component's `ref`. */
|
|
148
|
+
export interface RichTextEditorHandle {
|
|
149
|
+
/** The EditorView-shaped view: `dispatch`, `state`, `someProp`,
|
|
150
|
+
* `posAtCoords`… — what a ProseMirror plugin or command is handed. */
|
|
151
|
+
readonly view: RichEditorView;
|
|
152
|
+
readonly state: EditorState;
|
|
153
|
+
readonly schema: Schema;
|
|
154
|
+
/** The document in `format` — the editor's own, when not given. */
|
|
155
|
+
getValue(format?: EditorFormat): string;
|
|
156
|
+
/** Replace the document. Not undoable, the way a form reset is not,
|
|
157
|
+
* unless `addToHistory` says so. */
|
|
158
|
+
setValue(
|
|
159
|
+
value: string | PMNode,
|
|
160
|
+
options?: { format?: EditorFormat; addToHistory?: boolean },
|
|
161
|
+
): void;
|
|
162
|
+
/** Insert content where the selection is — markdown, HTML, text or a node. */
|
|
163
|
+
insertContent(value: string | PMNode, format?: EditorFormat): void;
|
|
164
|
+
/** Run a ProseMirror command against the current state. */
|
|
165
|
+
run(command: Command): boolean;
|
|
166
|
+
/** Could `command` run now? — the question a toolbar greys a button on. */
|
|
167
|
+
can(command: Command): boolean;
|
|
168
|
+
/** A mark (by name) on the selection, or a node (by name, with `attrs`)
|
|
169
|
+
* around it. */
|
|
170
|
+
isActive(name: string, attrs?: Record<string, unknown>): boolean;
|
|
171
|
+
toggleMark(name: string, attrs?: Record<string, unknown>): boolean;
|
|
172
|
+
/** A textblock type — or back to a paragraph when it already is one. */
|
|
173
|
+
setBlock(name: string, attrs?: Record<string, unknown>): boolean;
|
|
174
|
+
toggleList(name: string): boolean;
|
|
175
|
+
toggleWrap(name: string): boolean;
|
|
176
|
+
undo(): boolean;
|
|
177
|
+
redo(): boolean;
|
|
178
|
+
/** Open the link editor on the selection — what Mod-K does. */
|
|
179
|
+
editLink(): void;
|
|
180
|
+
focus(): void;
|
|
181
|
+
blur(): void;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A document the app hands over as a value, not as ProseMirror state. */
|
|
185
|
+
interface ValueProps {
|
|
186
|
+
/** Uncontrolled initial document: a string in `format`, or a node. */
|
|
187
|
+
defaultValue?: string | PMNode;
|
|
188
|
+
/**
|
|
189
|
+
* The document, as a string in `format`. A value different from the one
|
|
190
|
+
* the editor last reported replaces the document — a reset, not undoable;
|
|
191
|
+
* handing back what `onChange` reported changes nothing.
|
|
192
|
+
*/
|
|
193
|
+
value?: string;
|
|
194
|
+
state?: undefined;
|
|
195
|
+
dispatchTransaction?: undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The top rung: the app owns the ProseMirror state. */
|
|
199
|
+
interface StateProps {
|
|
200
|
+
/** The whole EditorState — schema, plugins and all. The editor's default
|
|
201
|
+
* plugins are the app's to include: `defaultPlugins(schema)`. */
|
|
202
|
+
state: EditorState;
|
|
203
|
+
/** Called with every transaction; the app applies it and passes the new
|
|
204
|
+
* state back (ProseMirror's own contract). */
|
|
205
|
+
dispatchTransaction: (tr: Transaction) => void;
|
|
206
|
+
defaultValue?: never;
|
|
207
|
+
value?: never;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface CommonProps {
|
|
211
|
+
/** What `value`/`defaultValue`/`ev.value` are written in. Default
|
|
212
|
+
* `'markdown'`. */
|
|
213
|
+
format?: EditorFormat;
|
|
214
|
+
onChange?: (ev: RichTextEditorChangeEvent) => void;
|
|
215
|
+
onSelectionChange?: (ev: RichTextEditorEvent) => void;
|
|
216
|
+
/** Mod-Enter — or plain Enter, with `submitOnEnter`. */
|
|
217
|
+
onSubmit?: (ev: RichTextEditorEvent) => void;
|
|
218
|
+
/** Enter submits (Shift+Enter is a new line) — a chat composer. Not in a
|
|
219
|
+
* list or a code block, where Enter goes on editing. */
|
|
220
|
+
submitOnEnter?: boolean;
|
|
221
|
+
/** A link was activated: Mod-click while editing, a click when read-only.
|
|
222
|
+
* The editor never navigates by itself. */
|
|
223
|
+
onLink?: (href: string, ev: X11MouseEvent<DrawnNode>) => void;
|
|
224
|
+
/** Lists that open at a trigger character as its word is typed — `@` for
|
|
225
|
+
* people, `#` for issues, `/` for a block menu. A new array on every
|
|
226
|
+
* render is fine: the plugin reads the latest one. */
|
|
227
|
+
suggestions?: readonly Suggester[];
|
|
228
|
+
placeholder?: string;
|
|
229
|
+
/** Selectable and copyable, not editable. */
|
|
230
|
+
readOnly?: boolean;
|
|
231
|
+
/** Inert, out of the tab order, dimmed. */
|
|
232
|
+
disabled?: boolean;
|
|
233
|
+
autoFocus?: boolean;
|
|
234
|
+
/** Echoed on every event. */
|
|
235
|
+
name?: string;
|
|
236
|
+
|
|
237
|
+
/** The formatting toolbar: `true` for the default set, an array to pick
|
|
238
|
+
* and order (built-in names, `'|'`, items of your own), or a function
|
|
239
|
+
* rendering your own, handed the editor. */
|
|
240
|
+
toolbar?:
|
|
241
|
+
| boolean
|
|
242
|
+
| readonly ToolbarEntry[]
|
|
243
|
+
| ((editor: RichTextEditorHandle) => ReactNode);
|
|
244
|
+
|
|
245
|
+
/** Base text. Default: the theme's `fontSize`, and `sans-serif`. */
|
|
246
|
+
fontSize?: number;
|
|
247
|
+
fontFamily?: string;
|
|
248
|
+
/** Code, inline and fenced. Default `'monospace'`. */
|
|
249
|
+
monoFamily?: string;
|
|
250
|
+
/** How a mark looks, by mark name — over, or instead of, the built-in
|
|
251
|
+
* look. A function is handed the mark and the style under it. */
|
|
252
|
+
markStyles?: Readonly<Record<string, MarkStyle>>;
|
|
253
|
+
/** How a decoration's `class` looks: the one part of a plugin's DOM
|
|
254
|
+
* decoration this editor cannot read off the decoration itself. */
|
|
255
|
+
decorationClasses?: Readonly<Record<string, RunStyle>>;
|
|
256
|
+
/** Syntax colouring in code blocks. Default true. */
|
|
257
|
+
highlight?: boolean;
|
|
258
|
+
/** A `Language` for a fence tag the built-in tokenizers do not cover —
|
|
259
|
+
* `<Markdown>`'s seam, for the same fences. */
|
|
260
|
+
resolveLanguage?: (tag: string) => Language | null;
|
|
261
|
+
/** An image alone in its paragraph, drawn. The editor fetches nothing:
|
|
262
|
+
* without this an image is its alt text. */
|
|
263
|
+
renderImage?: (image: ImageInfo) => ReactNode;
|
|
264
|
+
|
|
265
|
+
/** The document model. Default: GFM (`schema` from this module). */
|
|
266
|
+
schema?: Schema;
|
|
267
|
+
/** A markdown codec for a schema the built-in one cannot read or write. */
|
|
268
|
+
markdown?: MarkdownCodec;
|
|
269
|
+
/** ProseMirror plugins, ahead of the editor's own — so a keymap here wins
|
|
270
|
+
* over the defaults. Give the array a stable identity. */
|
|
271
|
+
plugins?: readonly Plugin[];
|
|
272
|
+
/** The defaults — each can be turned off: undo history, markdown input
|
|
273
|
+
* rules, formatting shortcuts, and (off by default) typographic quotes. */
|
|
274
|
+
history?: boolean;
|
|
275
|
+
inputRules?: boolean;
|
|
276
|
+
keymap?: boolean;
|
|
277
|
+
typography?: boolean;
|
|
278
|
+
/** ProseMirror's view props — `handleKeyDown`, `handlePaste`,
|
|
279
|
+
* `decorations`, `transformPasted`… — without writing a plugin. */
|
|
280
|
+
editorProps?: EditorProps<unknown>;
|
|
281
|
+
/** Custom rendering by node type: a React component, handed the node, its
|
|
282
|
+
* rendered content and a way to update its attributes. */
|
|
283
|
+
nodeViews?: Readonly<Record<string, ComponentType<NodeViewProps>>>;
|
|
284
|
+
/**
|
|
285
|
+
* Draw only the top-level blocks near the viewport, for a long
|
|
286
|
+
* document. `'auto'`, the default, does once there are more than 200
|
|
287
|
+
* of them — so a composer never does. It matters only in an editor
|
|
288
|
+
* given a height to scroll in.
|
|
289
|
+
*/
|
|
290
|
+
virtual?: boolean | 'auto';
|
|
291
|
+
|
|
292
|
+
/** The frame: width, height, `flexGrow`, border, background. */
|
|
293
|
+
style?: Style | Style[];
|
|
294
|
+
/** The parts inside it. */
|
|
295
|
+
styles?: { toolbar?: Style; content?: Style };
|
|
296
|
+
ref?: Ref<RichTextEditorHandle | null>;
|
|
297
|
+
onKeyDown?: (ev: KeyboardEvent) => void;
|
|
298
|
+
onMouseDown?: (ev: X11MouseEvent) => void;
|
|
299
|
+
onFocus?: (ev: FocusEvent) => void;
|
|
300
|
+
onBlur?: (ev: FocusEvent) => void;
|
|
301
|
+
'aria-label'?: string;
|
|
302
|
+
'data-testname'?: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export type RichTextEditorProps = CommonProps & (ValueProps | StateProps);
|
|
306
|
+
|
|
307
|
+
// --- formats -----------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
interface Codec {
|
|
310
|
+
parse(source: string): PMNode;
|
|
311
|
+
serialize(doc: PMNode): string;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function codecFor(
|
|
315
|
+
schema: Schema,
|
|
316
|
+
format: EditorFormat,
|
|
317
|
+
markdown?: MarkdownCodec,
|
|
318
|
+
): Codec {
|
|
319
|
+
switch (format) {
|
|
320
|
+
case 'html':
|
|
321
|
+
return {
|
|
322
|
+
parse: (source) => docFromHTML(schema, source),
|
|
323
|
+
serialize: (doc) => htmlFromContent(schema, doc),
|
|
324
|
+
};
|
|
325
|
+
case 'text':
|
|
326
|
+
return {
|
|
327
|
+
parse: (source) => docFromText(schema, source),
|
|
328
|
+
serialize: textFromDoc,
|
|
329
|
+
};
|
|
330
|
+
default:
|
|
331
|
+
return markdown ?? markdownCodec(schema);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function replaceDocument(
|
|
336
|
+
view: RichEditorView,
|
|
337
|
+
doc: PMNode,
|
|
338
|
+
addToHistory: boolean,
|
|
339
|
+
): void {
|
|
340
|
+
const { state } = view;
|
|
341
|
+
const tr = state.tr.replaceWith(0, state.doc.content.size, doc.content);
|
|
342
|
+
// keep the caret about where it was, inside what is there now
|
|
343
|
+
tr.setSelection(
|
|
344
|
+
Selection.near(
|
|
345
|
+
tr.doc.resolve(Math.min(state.selection.head, tr.doc.content.size)),
|
|
346
|
+
),
|
|
347
|
+
);
|
|
348
|
+
if (!addToHistory) tr.setMeta('addToHistory', false);
|
|
349
|
+
view.dispatch(tr);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function inListOrCode(state: EditorState): boolean {
|
|
353
|
+
const { $from } = state.selection;
|
|
354
|
+
if ($from.parent.type.spec.code) return true;
|
|
355
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
356
|
+
if (/item$/i.test($from.node(d).type.name)) return true;
|
|
357
|
+
}
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function linkAt(state: EditorState, pos: number): string | null {
|
|
362
|
+
const $pos = state.doc.resolve(pos);
|
|
363
|
+
const node =
|
|
364
|
+
$pos.parent.childAfter($pos.parentOffset).node ?? $pos.nodeBefore;
|
|
365
|
+
const link = node?.marks.find((m) => m.type.name === 'link');
|
|
366
|
+
return link && typeof link.attrs.href === 'string' ? link.attrs.href : null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// --- the component -----------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
interface LinkEdit {
|
|
372
|
+
at: { x: number; y: number; width: number; height: number };
|
|
373
|
+
draft: string;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* ```tsx
|
|
378
|
+
* <RichTextEditor
|
|
379
|
+
* defaultValue={note.body}
|
|
380
|
+
* onChange={(ev) => save(ev.value)}
|
|
381
|
+
* placeholder="Write something…"
|
|
382
|
+
* toolbar
|
|
383
|
+
* style={{ flexGrow: 1 }}
|
|
384
|
+
* />
|
|
385
|
+
* ```
|
|
386
|
+
*/
|
|
387
|
+
export function RichTextEditor(props: RichTextEditorProps): ReactElement {
|
|
388
|
+
const theme = useTheme() as unknown as Record<string, unknown>;
|
|
389
|
+
const clipboard = useClipboard();
|
|
390
|
+
const {
|
|
391
|
+
placeholder,
|
|
392
|
+
readOnly = false,
|
|
393
|
+
disabled = false,
|
|
394
|
+
autoFocus,
|
|
395
|
+
toolbar,
|
|
396
|
+
style,
|
|
397
|
+
styles,
|
|
398
|
+
} = props;
|
|
399
|
+
const format = props.format ?? 'markdown';
|
|
400
|
+
const schema = props.state?.schema ?? props.schema ?? defaultSchema;
|
|
401
|
+
const codec = useMemo(
|
|
402
|
+
() => codecFor(schema, format, props.markdown),
|
|
403
|
+
[schema, format, props.markdown],
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// The latest props, for the callbacks the view holds on to.
|
|
407
|
+
const latest = useRef(props);
|
|
408
|
+
latest.current = props;
|
|
409
|
+
const codecRef = useRef(codec);
|
|
410
|
+
codecRef.current = codec;
|
|
411
|
+
|
|
412
|
+
const look = useMemo(
|
|
413
|
+
() =>
|
|
414
|
+
deriveLook(theme, {
|
|
415
|
+
fontSize: props.fontSize,
|
|
416
|
+
fontFamily: props.fontFamily,
|
|
417
|
+
monoFamily: props.monoFamily,
|
|
418
|
+
markStyles: props.markStyles,
|
|
419
|
+
resolveLanguage: props.resolveLanguage,
|
|
420
|
+
highlight: props.highlight,
|
|
421
|
+
dim: disabled,
|
|
422
|
+
}),
|
|
423
|
+
[
|
|
424
|
+
theme,
|
|
425
|
+
props.fontSize,
|
|
426
|
+
props.fontFamily,
|
|
427
|
+
props.monoFamily,
|
|
428
|
+
props.markStyles,
|
|
429
|
+
props.resolveLanguage,
|
|
430
|
+
props.highlight,
|
|
431
|
+
disabled,
|
|
432
|
+
],
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// The editor's own plugins, made once per schema and option set — a
|
|
436
|
+
// plugin instance carries its state across a reconfigure, so the undo
|
|
437
|
+
// history survives an app swapping its own plugins.
|
|
438
|
+
const defaults = useMemo(
|
|
439
|
+
() =>
|
|
440
|
+
defaultPlugins(schema, {
|
|
441
|
+
history: props.history,
|
|
442
|
+
inputRules: props.inputRules,
|
|
443
|
+
keymap: props.keymap,
|
|
444
|
+
typography: props.typography,
|
|
445
|
+
}),
|
|
446
|
+
[schema, props.history, props.inputRules, props.keymap, props.typography],
|
|
447
|
+
);
|
|
448
|
+
// The suggestion plugin, made once while there are suggesters: it reads
|
|
449
|
+
// the latest prop through `latest`, so an inline array is not a rebuild —
|
|
450
|
+
// a rebuild would drop the list that is open.
|
|
451
|
+
const suggesting = !!props.suggestions?.length;
|
|
452
|
+
const suggest = useMemo(
|
|
453
|
+
() =>
|
|
454
|
+
suggesting
|
|
455
|
+
? suggestionPlugin(() => latest.current.suggestions ?? [])
|
|
456
|
+
: null,
|
|
457
|
+
[suggesting],
|
|
458
|
+
);
|
|
459
|
+
const plugins = useMemo(
|
|
460
|
+
() => [
|
|
461
|
+
...(props.plugins ?? []),
|
|
462
|
+
// ahead of the defaults, so a row takes Enter before the keymap does
|
|
463
|
+
...(suggest ? [suggest] : []),
|
|
464
|
+
...defaults,
|
|
465
|
+
],
|
|
466
|
+
[props.plugins, suggest, defaults],
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const listeners = useRef(new Set<() => void>());
|
|
470
|
+
/** The value last reported through `onChange`, or accepted from `value`. */
|
|
471
|
+
const reported = useRef<string | null>(null);
|
|
472
|
+
const handleRef = useRef<RichTextEditorHandle | null>(null);
|
|
473
|
+
const rootRef = useRef<DrawnNode | null>(null);
|
|
474
|
+
const [linkEdit, setLinkEdit] = useState<LinkEdit | null>(null);
|
|
475
|
+
// A suggestion list shows only while the editor has focus.
|
|
476
|
+
const [focused, setFocused] = useState(false);
|
|
477
|
+
|
|
478
|
+
// The trigger being typed is drawn in the accent colour, unless the app
|
|
479
|
+
// says what the `suggestion` class looks like. Memoized: a new object
|
|
480
|
+
// redraws every block's decorations (`configure`).
|
|
481
|
+
const decorationClasses = useMemo(
|
|
482
|
+
() => ({ suggestion: { color: look.accent }, ...props.decorationClasses }),
|
|
483
|
+
[look.accent, props.decorationClasses],
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const config: ViewConfig = {
|
|
487
|
+
onRender: () => {},
|
|
488
|
+
onUpdate: (prev, trs) => onUpdate(prev, trs),
|
|
489
|
+
onFocusChange: setFocused,
|
|
490
|
+
clipboard,
|
|
491
|
+
...(placeholder ? { placeholder } : null),
|
|
492
|
+
colors: {
|
|
493
|
+
selection: look.selection,
|
|
494
|
+
selectionBlurred: look.selectionBlurred,
|
|
495
|
+
caret: look.caret,
|
|
496
|
+
placeholder: look.muted,
|
|
497
|
+
preedit: look.text,
|
|
498
|
+
},
|
|
499
|
+
decorationClasses,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const viewRef = useRef<RichEditorView | null>(null);
|
|
503
|
+
if (!viewRef.current) {
|
|
504
|
+
let state = props.state;
|
|
505
|
+
if (!state) {
|
|
506
|
+
const initial = props.value ?? props.defaultValue;
|
|
507
|
+
if (typeof initial === 'string') reported.current = initial;
|
|
508
|
+
const doc =
|
|
509
|
+
initial === undefined
|
|
510
|
+
? undefined
|
|
511
|
+
: typeof initial === 'string'
|
|
512
|
+
? codec.parse(initial)
|
|
513
|
+
: initial;
|
|
514
|
+
state = EditorState.create({ schema, doc, plugins });
|
|
515
|
+
}
|
|
516
|
+
const viewProps: ViewProps = {
|
|
517
|
+
...props.editorProps,
|
|
518
|
+
editable: () => !latest.current.readOnly && !latest.current.disabled,
|
|
519
|
+
handleClick: (view, pos, event) => {
|
|
520
|
+
const own = latest.current.editorProps?.handleClick;
|
|
521
|
+
if (own?.call(undefined, view, pos, event)) return true;
|
|
522
|
+
return openLinkAt(pos, event as unknown as X11MouseEvent<DrawnNode>);
|
|
523
|
+
},
|
|
524
|
+
...(props.dispatchTransaction
|
|
525
|
+
? {
|
|
526
|
+
dispatchTransaction: (tr: Transaction) =>
|
|
527
|
+
latest.current.dispatchTransaction?.(tr),
|
|
528
|
+
}
|
|
529
|
+
: null),
|
|
530
|
+
};
|
|
531
|
+
viewRef.current = new RichEditorView(state, viewProps, config);
|
|
532
|
+
}
|
|
533
|
+
const view = viewRef.current;
|
|
534
|
+
|
|
535
|
+
// Every state change re-renders this component; the document itself only
|
|
536
|
+
// re-renders the blocks whose nodes changed (./render.ts).
|
|
537
|
+
const subscribe = useCallback((fn: () => void) => {
|
|
538
|
+
listeners.current.add(fn);
|
|
539
|
+
return () => {
|
|
540
|
+
listeners.current.delete(fn);
|
|
541
|
+
};
|
|
542
|
+
}, []);
|
|
543
|
+
const state = useSyncExternalStore(
|
|
544
|
+
subscribe,
|
|
545
|
+
() => view.state,
|
|
546
|
+
() => view.state,
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
function event(type: RichTextEditorEvent['type']): RichTextEditorEvent {
|
|
550
|
+
const at = view.state;
|
|
551
|
+
let cached: string | null = null;
|
|
552
|
+
return {
|
|
553
|
+
type,
|
|
554
|
+
get value() {
|
|
555
|
+
return (cached ??= codecRef.current.serialize(at.doc));
|
|
556
|
+
},
|
|
557
|
+
state: at,
|
|
558
|
+
name: latest.current.name,
|
|
559
|
+
target: handleRef.current!,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function onUpdate(prev: EditorState, trs: readonly Transaction[]): void {
|
|
564
|
+
for (const fn of [...listeners.current]) fn();
|
|
565
|
+
const now = view.state;
|
|
566
|
+
const p = latest.current;
|
|
567
|
+
if (prev.doc !== now.doc && p.onChange) {
|
|
568
|
+
let cached: string | null = null;
|
|
569
|
+
p.onChange({
|
|
570
|
+
type: 'change',
|
|
571
|
+
get value() {
|
|
572
|
+
if (cached === null) {
|
|
573
|
+
cached = codecRef.current.serialize(now.doc);
|
|
574
|
+
reported.current = cached;
|
|
575
|
+
}
|
|
576
|
+
return cached;
|
|
577
|
+
},
|
|
578
|
+
doc: now.doc,
|
|
579
|
+
state: now,
|
|
580
|
+
transactions: trs,
|
|
581
|
+
name: p.name,
|
|
582
|
+
target: handleRef.current!,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
if (!prev.selection.eq(now.selection))
|
|
586
|
+
p.onSelectionChange?.(event('selectionchange'));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function openLinkAt(pos: number, ev: X11MouseEvent<DrawnNode>): boolean {
|
|
590
|
+
const p = latest.current;
|
|
591
|
+
if (!p.onLink) return false;
|
|
592
|
+
const editing = !p.readOnly && !p.disabled;
|
|
593
|
+
// editing, a plain click places the caret; Mod-click follows
|
|
594
|
+
if (editing && !(ev.ctrlKey || ev.metaKey)) return false;
|
|
595
|
+
const href = linkAt(view.state, pos);
|
|
596
|
+
if (!href) return false;
|
|
597
|
+
p.onLink(href, ev);
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const editLink = useCallback((): void => {
|
|
602
|
+
const root = rootRef.current;
|
|
603
|
+
const frame = root?.getClientRects()[0];
|
|
604
|
+
if (!root || !frame || !view.state.schema.marks.link) return;
|
|
605
|
+
const { from, to } = view.state.selection;
|
|
606
|
+
const a = view.coordsAtPos(from);
|
|
607
|
+
const b = view.coordsAtPos(to);
|
|
608
|
+
const attrs = markAttrs(view.state, view.state.schema.marks.link);
|
|
609
|
+
setLinkEdit({
|
|
610
|
+
at: {
|
|
611
|
+
x: a.left - frame.x,
|
|
612
|
+
y: Math.min(a.top, b.top) - frame.y,
|
|
613
|
+
width: Math.max(1, (a.top === b.top ? b.right : a.left) - a.left),
|
|
614
|
+
height: Math.max(a.bottom, b.bottom) - Math.min(a.top, b.top),
|
|
615
|
+
},
|
|
616
|
+
draft: typeof attrs?.href === 'string' ? attrs.href : '',
|
|
617
|
+
});
|
|
618
|
+
}, [view]);
|
|
619
|
+
|
|
620
|
+
const handle = useMemo<RichTextEditorHandle>(() => {
|
|
621
|
+
const nodeType = (name: string) => view.state.schema.nodes[name];
|
|
622
|
+
const markType = (name: string) => view.state.schema.marks[name];
|
|
623
|
+
const itemType = () =>
|
|
624
|
+
view.state.schema.nodes.list_item ?? view.state.schema.nodes.listItem;
|
|
625
|
+
return {
|
|
626
|
+
get view() {
|
|
627
|
+
return view;
|
|
628
|
+
},
|
|
629
|
+
get state() {
|
|
630
|
+
return view.state;
|
|
631
|
+
},
|
|
632
|
+
get schema() {
|
|
633
|
+
return view.state.schema;
|
|
634
|
+
},
|
|
635
|
+
getValue(fmt) {
|
|
636
|
+
const c = fmt
|
|
637
|
+
? codecFor(view.state.schema, fmt, latest.current.markdown)
|
|
638
|
+
: codecRef.current;
|
|
639
|
+
return c.serialize(view.state.doc);
|
|
640
|
+
},
|
|
641
|
+
setValue(value, options = {}) {
|
|
642
|
+
const c = options.format
|
|
643
|
+
? codecFor(view.state.schema, options.format, latest.current.markdown)
|
|
644
|
+
: codecRef.current;
|
|
645
|
+
const doc = typeof value === 'string' ? c.parse(value) : value;
|
|
646
|
+
if (typeof value === 'string' && !options.format)
|
|
647
|
+
reported.current = value;
|
|
648
|
+
replaceDocument(view, doc, options.addToHistory ?? false);
|
|
649
|
+
},
|
|
650
|
+
insertContent(value, fmt) {
|
|
651
|
+
const doc =
|
|
652
|
+
typeof value === 'string'
|
|
653
|
+
? (fmt
|
|
654
|
+
? codecFor(view.state.schema, fmt, latest.current.markdown)
|
|
655
|
+
: codecRef.current
|
|
656
|
+
).parse(value)
|
|
657
|
+
: value;
|
|
658
|
+
// a whole document goes in as its blocks, open at both ends so a
|
|
659
|
+
// single paragraph joins the one the caret is in
|
|
660
|
+
const content =
|
|
661
|
+
doc.type === view.state.schema.topNodeType
|
|
662
|
+
? doc.content
|
|
663
|
+
: Fragment.from(doc);
|
|
664
|
+
view.dispatch(
|
|
665
|
+
view.state.tr
|
|
666
|
+
.replaceSelection(Slice.maxOpen(content))
|
|
667
|
+
.scrollIntoView(),
|
|
668
|
+
);
|
|
669
|
+
},
|
|
670
|
+
run: (command) => view.run(command),
|
|
671
|
+
can: (command) => command(view.state, undefined, view.asEditorView),
|
|
672
|
+
isActive(name, attrs) {
|
|
673
|
+
const mark = markType(name);
|
|
674
|
+
if (mark) return isMarkActive(view.state, mark);
|
|
675
|
+
const node = nodeType(name);
|
|
676
|
+
return node ? isBlockActive(view.state, node, attrs) : false;
|
|
677
|
+
},
|
|
678
|
+
toggleMark(name, attrs) {
|
|
679
|
+
const type = markType(name);
|
|
680
|
+
return !!type && view.run(toggleMark(type, attrs));
|
|
681
|
+
},
|
|
682
|
+
setBlock(name, attrs) {
|
|
683
|
+
const type = nodeType(name);
|
|
684
|
+
return !!type && view.run(toggleBlockType(type, attrs));
|
|
685
|
+
},
|
|
686
|
+
toggleList(name) {
|
|
687
|
+
const type = nodeType(name);
|
|
688
|
+
const item = itemType();
|
|
689
|
+
return !!type && !!item && view.run(toggleList(type, item));
|
|
690
|
+
},
|
|
691
|
+
toggleWrap(name) {
|
|
692
|
+
const type = nodeType(name);
|
|
693
|
+
return !!type && view.run(toggleWrap(type));
|
|
694
|
+
},
|
|
695
|
+
undo: () => view.run(undo),
|
|
696
|
+
redo: () => view.run(redo),
|
|
697
|
+
editLink,
|
|
698
|
+
focus: () => view.focus(),
|
|
699
|
+
blur: () => {
|
|
700
|
+
(view.dom as unknown as { blur?: () => void } | null)?.blur?.();
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
}, [view, editLink]);
|
|
704
|
+
handleRef.current = handle;
|
|
705
|
+
useImperativeHandle(props.ref, () => handle, [handle]);
|
|
706
|
+
|
|
707
|
+
// The component's half of the view, refreshed every render; the view
|
|
708
|
+
// acts only on what changed.
|
|
709
|
+
useLayoutEffect(() => {
|
|
710
|
+
view.configure(config);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// An app-owned state, handed back after each transaction.
|
|
714
|
+
useLayoutEffect(() => {
|
|
715
|
+
if (props.state && props.state !== view.state)
|
|
716
|
+
view.updateState(props.state);
|
|
717
|
+
}, [props.state, view]);
|
|
718
|
+
|
|
719
|
+
// A controlled value: anything but what the editor last reported replaces
|
|
720
|
+
// the document.
|
|
721
|
+
useLayoutEffect(() => {
|
|
722
|
+
const value = props.value;
|
|
723
|
+
if (value === undefined || props.state || value === reported.current)
|
|
724
|
+
return;
|
|
725
|
+
reported.current = value;
|
|
726
|
+
replaceDocument(view, codec.parse(value), false);
|
|
727
|
+
}, [props.value, props.state, codec, view]);
|
|
728
|
+
|
|
729
|
+
// New plugins (the app's, or the defaults' options): reconfigure, keeping
|
|
730
|
+
// every plugin instance that is still there — and its state.
|
|
731
|
+
const configured = useRef(plugins);
|
|
732
|
+
useLayoutEffect(() => {
|
|
733
|
+
if (props.state || plugins === configured.current) return;
|
|
734
|
+
configured.current = plugins;
|
|
735
|
+
view.updateState(view.state.reconfigure({ plugins }));
|
|
736
|
+
}, [plugins, props.state, view]);
|
|
737
|
+
|
|
738
|
+
const editorProps = props.editorProps;
|
|
739
|
+
const firstProps = useRef(editorProps);
|
|
740
|
+
useLayoutEffect(() => {
|
|
741
|
+
if (editorProps === firstProps.current) return;
|
|
742
|
+
firstProps.current = editorProps;
|
|
743
|
+
view.setProps({
|
|
744
|
+
...editorProps,
|
|
745
|
+
editable: view.props.editable,
|
|
746
|
+
handleClick: view.props.handleClick,
|
|
747
|
+
});
|
|
748
|
+
}, [editorProps, view]);
|
|
749
|
+
|
|
750
|
+
// the plugins' views, now that the root element exists for them to find
|
|
751
|
+
useLayoutEffect(() => {
|
|
752
|
+
view.mountPluginViews();
|
|
753
|
+
}, [view]);
|
|
754
|
+
|
|
755
|
+
useEffect(() => () => view.destroy(), [view]);
|
|
756
|
+
|
|
757
|
+
const editable = !readOnly && !disabled;
|
|
758
|
+
// what a table measures its columns with; none on the mock backend
|
|
759
|
+
const fonts =
|
|
760
|
+
(useApp() as { fonts?: RenderContext['fonts'] } | null)?.fonts ?? null;
|
|
761
|
+
const ctx = useMemo<RenderContext>(
|
|
762
|
+
() => ({
|
|
763
|
+
view,
|
|
764
|
+
look,
|
|
765
|
+
editable,
|
|
766
|
+
fonts,
|
|
767
|
+
...(props.nodeViews ? { nodeViews: props.nodeViews } : null),
|
|
768
|
+
...(props.renderImage ? { renderImage: props.renderImage } : null),
|
|
769
|
+
}),
|
|
770
|
+
[view, look, editable, fonts, props.nodeViews, props.renderImage],
|
|
771
|
+
);
|
|
772
|
+
// A long document draws only the blocks near the viewport, in a window
|
|
773
|
+
// the view asks about when what it needs is not drawn (./virtual.ts).
|
|
774
|
+
const windowed = useBlockWindow({
|
|
775
|
+
doc: state.doc,
|
|
776
|
+
keys: view.keys,
|
|
777
|
+
virtual: props.virtual ?? 'auto',
|
|
778
|
+
estimate: Math.round(look.size * 1.5) + look.blockGap,
|
|
779
|
+
gap: look.blockGap,
|
|
780
|
+
});
|
|
781
|
+
const { virtualizing, first, last, register } = windowed;
|
|
782
|
+
const blocks = useMemo(
|
|
783
|
+
() =>
|
|
784
|
+
virtualizing
|
|
785
|
+
? renderBlockRange(state.doc, first, last, ctx, register)
|
|
786
|
+
: renderBlocks(state.doc, 0, ctx),
|
|
787
|
+
[state.doc, ctx, virtualizing, first, last, register],
|
|
788
|
+
);
|
|
789
|
+
useLayoutEffect(() => {
|
|
790
|
+
view.attachWindow(virtualizing ? windowed.window : null);
|
|
791
|
+
}, [view, virtualizing, windowed.window]);
|
|
792
|
+
|
|
793
|
+
// --- input ---------------------------------------------------------------
|
|
794
|
+
|
|
795
|
+
const onKeyDown = (ev: KeyboardEvent): void => {
|
|
796
|
+
const p = latest.current;
|
|
797
|
+
p.onKeyDown?.(ev);
|
|
798
|
+
if (ev.defaultPrevented || p.disabled) return;
|
|
799
|
+
const mac = view.primaryModifier === 'meta';
|
|
800
|
+
const primary = mac ? ev.metaKey : ev.ctrlKey;
|
|
801
|
+
const k = ev.keysym;
|
|
802
|
+
if ((k === XK_RETURN || k === XK_KP_ENTER) && p.onSubmit) {
|
|
803
|
+
const plain = !ev.shiftKey && !primary && !ev.altKey;
|
|
804
|
+
// a plain Enter while a suggestion list shows rows takes a row: it is
|
|
805
|
+
// the list's plugin's, a step further on
|
|
806
|
+
const listing =
|
|
807
|
+
plain && editable && !!suggestionState(view.state)?.items?.length;
|
|
808
|
+
const submit =
|
|
809
|
+
!listing &&
|
|
810
|
+
(p.submitOnEnter
|
|
811
|
+
? plain && !inListOrCode(view.state)
|
|
812
|
+
: primary && !ev.shiftKey);
|
|
813
|
+
if (submit) {
|
|
814
|
+
ev.preventDefault();
|
|
815
|
+
p.onSubmit(event('submit'));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (
|
|
820
|
+
primary &&
|
|
821
|
+
!ev.altKey &&
|
|
822
|
+
!ev.shiftKey &&
|
|
823
|
+
k === 0x6b /* k */ &&
|
|
824
|
+
editable
|
|
825
|
+
) {
|
|
826
|
+
ev.preventDefault();
|
|
827
|
+
editLink();
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (k === XK_ESCAPE && linkEdit) setLinkEdit(null);
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
const onMouseDown = (ev: X11MouseEvent): void => {
|
|
834
|
+
latest.current.onMouseDown?.(ev);
|
|
835
|
+
if (ev.defaultPrevented || latest.current.disabled) return;
|
|
836
|
+
if (view.mouseDown(ev)) ev.preventDefault();
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
const setRoot = useCallback((node: unknown) => {
|
|
840
|
+
rootRef.current = (node as DrawnNode | null) ?? null;
|
|
841
|
+
}, []);
|
|
842
|
+
const pane = windowed.box;
|
|
843
|
+
const setScroller = useCallback(
|
|
844
|
+
(node: unknown) => {
|
|
845
|
+
pane.current = (node as ScrollableNode | null) ?? null;
|
|
846
|
+
view.attachScroller(pane.current);
|
|
847
|
+
},
|
|
848
|
+
[view, pane],
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
// What a drag out of the editor offers, resolved when a drop asks for it:
|
|
852
|
+
// a copy's text and HTML, and the slice itself for a drop in this app.
|
|
853
|
+
const dragData = useMemo(
|
|
854
|
+
() => ({
|
|
855
|
+
'text/plain': () => view.dragText(),
|
|
856
|
+
'text/html': () => view.dragHTML(),
|
|
857
|
+
[SLICE_TYPE]: () => view.dragPayload(),
|
|
858
|
+
}),
|
|
859
|
+
[view],
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
// --- chrome --------------------------------------------------------------
|
|
863
|
+
|
|
864
|
+
let chrome: ReactNode = null;
|
|
865
|
+
if (typeof toolbar === 'function') {
|
|
866
|
+
chrome = toolbar(handle);
|
|
867
|
+
} else if (toolbar) {
|
|
868
|
+
const items = toolbarItems(schema, {
|
|
869
|
+
link: () => {
|
|
870
|
+
editLink();
|
|
871
|
+
return true;
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
chrome = h(Toolbar, {
|
|
875
|
+
entries: resolveToolbar(
|
|
876
|
+
toolbar === true ? DEFAULT_TOOLBAR : toolbar,
|
|
877
|
+
items,
|
|
878
|
+
),
|
|
879
|
+
state,
|
|
880
|
+
run: (command: Command) => {
|
|
881
|
+
view.run(command);
|
|
882
|
+
view.focus();
|
|
883
|
+
},
|
|
884
|
+
look,
|
|
885
|
+
disabled: !editable,
|
|
886
|
+
...(styles?.toolbar ? { style: styles.toolbar } : null),
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const closeLink = (): void => {
|
|
891
|
+
setLinkEdit(null);
|
|
892
|
+
view.focus();
|
|
893
|
+
};
|
|
894
|
+
const applyLink = (href: string): void => {
|
|
895
|
+
const target = href.trim();
|
|
896
|
+
view.run(setLink(target === '' ? null : target));
|
|
897
|
+
closeLink();
|
|
898
|
+
};
|
|
899
|
+
const linkPopup =
|
|
900
|
+
linkEdit &&
|
|
901
|
+
hx(
|
|
902
|
+
'popup',
|
|
903
|
+
{
|
|
904
|
+
theme: theme as Record<string, string | number>,
|
|
905
|
+
anchor: {
|
|
906
|
+
to: rootRef as unknown as React.RefObject<DrawnNode | null>,
|
|
907
|
+
at: linkEdit.at,
|
|
908
|
+
placement: 'bottom',
|
|
909
|
+
},
|
|
910
|
+
onDismiss: closeLink,
|
|
911
|
+
},
|
|
912
|
+
hx(
|
|
913
|
+
'box',
|
|
914
|
+
{
|
|
915
|
+
style: {
|
|
916
|
+
flexDirection: 'row',
|
|
917
|
+
alignItems: 'center',
|
|
918
|
+
gap: 6,
|
|
919
|
+
padding: 6,
|
|
920
|
+
borderWidth: 1,
|
|
921
|
+
borderColor: look.border,
|
|
922
|
+
borderRadius: look.radius,
|
|
923
|
+
backgroundColor: look.surface,
|
|
924
|
+
},
|
|
925
|
+
},
|
|
926
|
+
hx('textinput', {
|
|
927
|
+
value: linkEdit.draft,
|
|
928
|
+
placeholder: 'https://',
|
|
929
|
+
autoFocus: true,
|
|
930
|
+
style: { width: 260 },
|
|
931
|
+
onChange: (ev) =>
|
|
932
|
+
setLinkEdit((cur) => (cur ? { ...cur, draft: ev.value } : cur)),
|
|
933
|
+
onSubmit: (ev) => applyLink(ev.value),
|
|
934
|
+
onKeyDown: (ev) => {
|
|
935
|
+
if (ev.keysym === XK_ESCAPE) {
|
|
936
|
+
ev.preventDefault();
|
|
937
|
+
closeLink();
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
}),
|
|
941
|
+
),
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
// The suggestion list, drawn from the plugin's state and nothing else
|
|
945
|
+
// (./suggest.ts) — so an app that owns the EditorState gets it too. It hangs
|
|
946
|
+
// off the textblock the trigger is in, at the trigger, so it follows the
|
|
947
|
+
// text as the document scrolls and unmaps while the text is out of view.
|
|
948
|
+
// It never takes focus: a press on a row takes the row, and the caret
|
|
949
|
+
// stays in the editor.
|
|
950
|
+
const suggestion = suggestionState(state);
|
|
951
|
+
const firstRow = useRef(0);
|
|
952
|
+
const rowsShown = focused && editable && !!suggestion?.items?.length;
|
|
953
|
+
const anchor =
|
|
954
|
+
rowsShown && suggestion ? view.anchorAt(suggestion.from) : null;
|
|
955
|
+
let suggestionPopup: ReactNode = null;
|
|
956
|
+
if (suggestion?.items && anchor) {
|
|
957
|
+
const { items, selected } = suggestion;
|
|
958
|
+
// the window of rows moves only as far as keeps the highlight in it
|
|
959
|
+
let first = Math.min(
|
|
960
|
+
firstRow.current,
|
|
961
|
+
Math.max(0, items.length - SUGGESTION_PAGE),
|
|
962
|
+
);
|
|
963
|
+
if (selected < first) first = selected;
|
|
964
|
+
if (selected >= first + SUGGESTION_PAGE)
|
|
965
|
+
first = selected - SUGGESTION_PAGE + 1;
|
|
966
|
+
firstRow.current = first;
|
|
967
|
+
// a suggester may draw its rows itself; the row box — the highlight
|
|
968
|
+
// behind it, the press on it — stays the editor's
|
|
969
|
+
const custom = suggesterFor(state)?.renderItem;
|
|
970
|
+
const rowHeight = Math.round(look.size + 12);
|
|
971
|
+
const rows = items.slice(first, first + SUGGESTION_PAGE).map((item, i) => {
|
|
972
|
+
const index = first + i;
|
|
973
|
+
const on = index === selected;
|
|
974
|
+
if (custom) {
|
|
975
|
+
return hx(
|
|
976
|
+
'box',
|
|
977
|
+
{
|
|
978
|
+
key: `${index} ${item.label}`,
|
|
979
|
+
style: {
|
|
980
|
+
flexDirection: 'row',
|
|
981
|
+
alignItems: 'center',
|
|
982
|
+
minHeight: rowHeight,
|
|
983
|
+
paddingLeft: 8,
|
|
984
|
+
paddingRight: 8,
|
|
985
|
+
borderRadius: Math.max(0, look.radius - 2),
|
|
986
|
+
backgroundColor: on ? look.accent : 'transparent',
|
|
987
|
+
...(on
|
|
988
|
+
? null
|
|
989
|
+
: { ':hover': { backgroundColor: tint(look.text, 0.08) } }),
|
|
990
|
+
},
|
|
991
|
+
onMouseDown: (ev: X11MouseEvent) => {
|
|
992
|
+
ev.preventDefault();
|
|
993
|
+
view.run(acceptSuggestion(index));
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
custom(item, { selected: on, query: suggestion.query }),
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
return hx(
|
|
1000
|
+
'box',
|
|
1001
|
+
{
|
|
1002
|
+
key: `${index} ${item.label}`,
|
|
1003
|
+
style: {
|
|
1004
|
+
flexDirection: 'row',
|
|
1005
|
+
alignItems: 'center',
|
|
1006
|
+
gap: 12,
|
|
1007
|
+
height: rowHeight,
|
|
1008
|
+
paddingLeft: 8,
|
|
1009
|
+
paddingRight: 8,
|
|
1010
|
+
borderRadius: Math.max(0, look.radius - 2),
|
|
1011
|
+
backgroundColor: on ? look.accent : 'transparent',
|
|
1012
|
+
...(on
|
|
1013
|
+
? null
|
|
1014
|
+
: { ':hover': { backgroundColor: tint(look.text, 0.08) } }),
|
|
1015
|
+
},
|
|
1016
|
+
onMouseDown: (ev: X11MouseEvent) => {
|
|
1017
|
+
ev.preventDefault();
|
|
1018
|
+
view.run(acceptSuggestion(index));
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
hx(
|
|
1022
|
+
'text',
|
|
1023
|
+
{
|
|
1024
|
+
style: {
|
|
1025
|
+
flexGrow: 1,
|
|
1026
|
+
fontFamily: look.family,
|
|
1027
|
+
fontSize: look.size,
|
|
1028
|
+
color: on ? look.accentText : look.text,
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
item.label,
|
|
1032
|
+
),
|
|
1033
|
+
item.detail
|
|
1034
|
+
? hx(
|
|
1035
|
+
'text',
|
|
1036
|
+
{
|
|
1037
|
+
style: {
|
|
1038
|
+
fontFamily: look.family,
|
|
1039
|
+
fontSize: Math.max(10, look.size - 2),
|
|
1040
|
+
color: on ? look.accentText : look.muted,
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
item.detail,
|
|
1044
|
+
)
|
|
1045
|
+
: null,
|
|
1046
|
+
);
|
|
1047
|
+
});
|
|
1048
|
+
suggestionPopup = hx(
|
|
1049
|
+
'popup',
|
|
1050
|
+
{
|
|
1051
|
+
theme: theme as Record<string, string | number>,
|
|
1052
|
+
anchor: { to: anchor.node, at: anchor.at, placement: 'bottom' },
|
|
1053
|
+
minWidth: 200,
|
|
1054
|
+
maxWidth: 420,
|
|
1055
|
+
},
|
|
1056
|
+
hx(
|
|
1057
|
+
'box',
|
|
1058
|
+
{
|
|
1059
|
+
style: {
|
|
1060
|
+
flexGrow: 1,
|
|
1061
|
+
flexDirection: 'column',
|
|
1062
|
+
padding: 4,
|
|
1063
|
+
borderWidth: 1,
|
|
1064
|
+
borderColor: look.border,
|
|
1065
|
+
borderRadius: look.radius,
|
|
1066
|
+
backgroundColor: look.surface,
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
...rows,
|
|
1070
|
+
),
|
|
1071
|
+
);
|
|
1072
|
+
} else {
|
|
1073
|
+
firstRow.current = 0;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const frame: Style = {
|
|
1077
|
+
flexDirection: 'column',
|
|
1078
|
+
borderWidth: 1,
|
|
1079
|
+
borderColor: look.border,
|
|
1080
|
+
borderRadius: look.radius,
|
|
1081
|
+
backgroundColor: look.background,
|
|
1082
|
+
overflow: 'hidden',
|
|
1083
|
+
...(editable
|
|
1084
|
+
? {
|
|
1085
|
+
':focus-within': {
|
|
1086
|
+
borderColor: String(theme.borderFocus ?? look.accent),
|
|
1087
|
+
},
|
|
1088
|
+
}
|
|
1089
|
+
: null),
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
return h(
|
|
1093
|
+
ROOT_ELEMENT,
|
|
1094
|
+
{
|
|
1095
|
+
ref: setRoot,
|
|
1096
|
+
host: view,
|
|
1097
|
+
focusable: !disabled,
|
|
1098
|
+
disabled,
|
|
1099
|
+
autoFocus,
|
|
1100
|
+
'aria-label': props['aria-label'] ?? placeholder,
|
|
1101
|
+
'data-testname': props['data-testname'],
|
|
1102
|
+
style: style
|
|
1103
|
+
? [frame, ...(Array.isArray(style) ? style : [style])]
|
|
1104
|
+
: frame,
|
|
1105
|
+
onKeyDown,
|
|
1106
|
+
onMouseDown,
|
|
1107
|
+
onMouseMove: (ev: X11MouseEvent) => {
|
|
1108
|
+
if (view.mouseMove(ev)) ev.preventDefault();
|
|
1109
|
+
},
|
|
1110
|
+
onMouseUp: () => view.mouseUp(),
|
|
1111
|
+
onContextMenu: (ev: X11MouseEvent) => {
|
|
1112
|
+
if (ev.defaultPrevented || latest.current.disabled) return;
|
|
1113
|
+
view.contextMenu(ev);
|
|
1114
|
+
ev.preventDefault();
|
|
1115
|
+
},
|
|
1116
|
+
// drag and drop (./drag.ts): the selection drags out, and a drop is
|
|
1117
|
+
// read like a paste. A press on the selection is left to core, which
|
|
1118
|
+
// arms the drag here; `onDragStart` cancels one that is not the
|
|
1119
|
+
// selection's, and leaves a drag of something inside alone.
|
|
1120
|
+
draggable: !disabled,
|
|
1121
|
+
dragData,
|
|
1122
|
+
dragActions: editable ? MOVE_OR_COPY : COPY_ONLY,
|
|
1123
|
+
onDragStart: (ev: DragSourceEvent) => {
|
|
1124
|
+
if (ev.target !== rootRef.current) return;
|
|
1125
|
+
if (!view.dragStart()) ev.preventDefault();
|
|
1126
|
+
},
|
|
1127
|
+
onDrag: (ev: DragSourceEvent) => {
|
|
1128
|
+
if (ev.target === rootRef.current)
|
|
1129
|
+
view.dragMoved(ev.ctrlKey, ev.altKey);
|
|
1130
|
+
},
|
|
1131
|
+
onDragEnd: (ev: DragEndEvent) => {
|
|
1132
|
+
if (ev.target === rootRef.current) view.dragEnd(ev.action, ev.dropped);
|
|
1133
|
+
},
|
|
1134
|
+
...(editable
|
|
1135
|
+
? {
|
|
1136
|
+
dropAccept: DROP_TYPES,
|
|
1137
|
+
onDragOver: (ev: DragEvent) => {
|
|
1138
|
+
if (!view.dragOver(ev.x, ev.y)) ev.reject();
|
|
1139
|
+
},
|
|
1140
|
+
onDragLeave: () => view.dragLeave(),
|
|
1141
|
+
onDrop: (ev: DropEvent) => {
|
|
1142
|
+
const settle = (answer: DropAnswer): void => {
|
|
1143
|
+
if (answer) ev.accept(answer);
|
|
1144
|
+
else ev.reject();
|
|
1145
|
+
};
|
|
1146
|
+
const answer = view.drop(ev);
|
|
1147
|
+
if (answer instanceof Promise) return answer.then(settle);
|
|
1148
|
+
settle(answer);
|
|
1149
|
+
return undefined;
|
|
1150
|
+
},
|
|
1151
|
+
}
|
|
1152
|
+
: null),
|
|
1153
|
+
onFocus: props.onFocus,
|
|
1154
|
+
onBlur: props.onBlur,
|
|
1155
|
+
},
|
|
1156
|
+
chrome,
|
|
1157
|
+
hx(
|
|
1158
|
+
'box',
|
|
1159
|
+
{
|
|
1160
|
+
ref: setScroller,
|
|
1161
|
+
// a press inside focuses the editor, not the viewport it scrolls in
|
|
1162
|
+
focusable: false,
|
|
1163
|
+
// `flexBasis: 'auto'` is load-bearing: a scroll box that grows
|
|
1164
|
+
// takes a zero basis, so an editor nobody gave a height collapsed
|
|
1165
|
+
// to its border. With it the viewport is its content's height — a
|
|
1166
|
+
// composer grows as it is typed in, up to the frame's `maxHeight`,
|
|
1167
|
+
// and past that it shrinks to fit and scrolls; given a height or a
|
|
1168
|
+
// `flexGrow`, it fills.
|
|
1169
|
+
style: {
|
|
1170
|
+
overflow: 'scroll',
|
|
1171
|
+
flexGrow: 1,
|
|
1172
|
+
flexShrink: 1,
|
|
1173
|
+
flexBasis: 'auto',
|
|
1174
|
+
flexDirection: 'column',
|
|
1175
|
+
},
|
|
1176
|
+
...(virtualizing
|
|
1177
|
+
? { onViewport: windowed.onViewport, onScroll: windowed.onScroll }
|
|
1178
|
+
: null),
|
|
1179
|
+
},
|
|
1180
|
+
hx(
|
|
1181
|
+
'box',
|
|
1182
|
+
{
|
|
1183
|
+
style: {
|
|
1184
|
+
flexDirection: 'column',
|
|
1185
|
+
gap: look.blockGap,
|
|
1186
|
+
padding: Math.round(look.size * 0.85),
|
|
1187
|
+
flexGrow: 1,
|
|
1188
|
+
...styles?.content,
|
|
1189
|
+
},
|
|
1190
|
+
},
|
|
1191
|
+
// the blocks outside a long document's window, as space
|
|
1192
|
+
windowed.above > 0
|
|
1193
|
+
? hx('box', {
|
|
1194
|
+
key: 'spacer:before',
|
|
1195
|
+
style: { flexShrink: 0, height: windowed.above },
|
|
1196
|
+
})
|
|
1197
|
+
: null,
|
|
1198
|
+
...blocks,
|
|
1199
|
+
windowed.below > 0
|
|
1200
|
+
? hx('box', {
|
|
1201
|
+
key: 'spacer:after',
|
|
1202
|
+
style: { flexShrink: 0, height: windowed.below },
|
|
1203
|
+
})
|
|
1204
|
+
: null,
|
|
1205
|
+
),
|
|
1206
|
+
),
|
|
1207
|
+
linkPopup,
|
|
1208
|
+
suggestionPopup,
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// --- the elements, for JSX ---------------------------------------------------
|
|
1213
|
+
|
|
1214
|
+
/** The raw `<richeditor>`'s props. Structural on purpose — see the note on
|
|
1215
|
+
* `CodeEditorElementProps` in `../code-editor/index.ts`. */
|
|
1216
|
+
export interface RichEditorElementProps {
|
|
1217
|
+
host?: unknown;
|
|
1218
|
+
focusable?: boolean;
|
|
1219
|
+
autoFocus?: boolean;
|
|
1220
|
+
disabled?: boolean;
|
|
1221
|
+
style?: Style | Style[];
|
|
1222
|
+
ref?: Ref<unknown>;
|
|
1223
|
+
children?: ReactNode;
|
|
1224
|
+
onKeyDown?: (ev: KeyboardEvent) => void;
|
|
1225
|
+
onMouseDown?: (ev: X11MouseEvent) => void;
|
|
1226
|
+
onMouseMove?: (ev: X11MouseEvent) => void;
|
|
1227
|
+
onMouseUp?: (ev: X11MouseEvent) => void;
|
|
1228
|
+
onContextMenu?: (ev: X11MouseEvent) => void;
|
|
1229
|
+
onFocus?: (ev: FocusEvent) => void;
|
|
1230
|
+
onBlur?: (ev: FocusEvent) => void;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/** The raw `<richeditortext>`'s props. */
|
|
1234
|
+
export interface RichEditorTextElementProps {
|
|
1235
|
+
runs: TextRun[];
|
|
1236
|
+
map?: unknown;
|
|
1237
|
+
node?: unknown;
|
|
1238
|
+
blockKey?: string;
|
|
1239
|
+
host?: unknown;
|
|
1240
|
+
wrap?: boolean;
|
|
1241
|
+
style?: Style | Style[];
|
|
1242
|
+
ref?: Ref<unknown>;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
declare module 'react-x11/jsx-runtime' {
|
|
1246
|
+
namespace JSX {
|
|
1247
|
+
interface IntrinsicElements {
|
|
1248
|
+
richeditor: RichEditorElementProps;
|
|
1249
|
+
richeditortext: RichEditorTextElementProps;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// --- the rest of the surface -------------------------------------------------
|
|
1255
|
+
|
|
1256
|
+
export {
|
|
1257
|
+
ROOT_ELEMENT as RICH_EDITOR_ELEMENT,
|
|
1258
|
+
TEXT_ELEMENT as RICH_EDITOR_TEXT_ELEMENT,
|
|
1259
|
+
};
|
|
1260
|
+
export { RichEditorView };
|
|
1261
|
+
export type { EditorView };
|
|
1262
|
+
export type { ViewProps, BlockDecorations, MoveUnit } from './view.js';
|
|
1263
|
+
export { schema, nodes, marks } from './schema.js';
|
|
1264
|
+
export { defaultPlugins, editingKeymap, markdownInputRules } from './keymap.js';
|
|
1265
|
+
// What a plugin's `handleKeyDown` is really handed: ProseMirror's
|
|
1266
|
+
// declarations say DOM `KeyboardEvent`, which this platform has no values of.
|
|
1267
|
+
export type { DefaultPluginOptions, DomKeyEvent } from './keymap.js';
|
|
1268
|
+
export {
|
|
1269
|
+
dedentCode,
|
|
1270
|
+
goToCell,
|
|
1271
|
+
indentCode,
|
|
1272
|
+
insertHorizontalRule,
|
|
1273
|
+
isBlockActive,
|
|
1274
|
+
isMarkActive,
|
|
1275
|
+
markAttrs,
|
|
1276
|
+
setLink,
|
|
1277
|
+
setTaskChecked,
|
|
1278
|
+
splitItem,
|
|
1279
|
+
toggleBlockType,
|
|
1280
|
+
toggleList,
|
|
1281
|
+
toggleTaskList,
|
|
1282
|
+
toggleWrap,
|
|
1283
|
+
} from './commands.js';
|
|
1284
|
+
export {
|
|
1285
|
+
docFromMarkdown,
|
|
1286
|
+
markdownCodec,
|
|
1287
|
+
markdownFromDoc,
|
|
1288
|
+
docFromText,
|
|
1289
|
+
textFromDoc,
|
|
1290
|
+
} from './markdown.js';
|
|
1291
|
+
export type { MarkdownCodec } from './markdown.js';
|
|
1292
|
+
export { docFromHTML, htmlFromContent } from './html.js';
|
|
1293
|
+
export { DEFAULT_TOOLBAR, toolbarItems } from './toolbar.js';
|
|
1294
|
+
export type { ToolbarEntry, ToolbarItem, ToolbarItemName } from './toolbar.js';
|
|
1295
|
+
export {
|
|
1296
|
+
acceptSuggestion,
|
|
1297
|
+
dismissSuggestion,
|
|
1298
|
+
filterSuggestions,
|
|
1299
|
+
selectSuggestion,
|
|
1300
|
+
suggesterFor,
|
|
1301
|
+
suggestionState,
|
|
1302
|
+
suggestions,
|
|
1303
|
+
} from './suggest.js';
|
|
1304
|
+
export type {
|
|
1305
|
+
Suggester,
|
|
1306
|
+
SuggestionItem,
|
|
1307
|
+
SuggestionQuery,
|
|
1308
|
+
SuggestionRow,
|
|
1309
|
+
SuggestionState,
|
|
1310
|
+
} from './suggest.js';
|
|
1311
|
+
export {
|
|
1312
|
+
addColumnAfter,
|
|
1313
|
+
addColumnBefore,
|
|
1314
|
+
addRowAfter,
|
|
1315
|
+
addRowBefore,
|
|
1316
|
+
columnAlign,
|
|
1317
|
+
deleteColumn,
|
|
1318
|
+
deleteRow,
|
|
1319
|
+
deleteTable,
|
|
1320
|
+
insertTable,
|
|
1321
|
+
isInTable,
|
|
1322
|
+
setColumnAlign,
|
|
1323
|
+
tableRepair,
|
|
1324
|
+
} from './tables.js';
|
|
1325
|
+
export type { ColumnAlign } from './tables.js';
|
|
1326
|
+
export { remoteCaret } from './collab.js';
|
|
1327
|
+
export type { RemoteCaret } from './collab.js';
|
|
1328
|
+
export type { DomFocusEvent } from './nodes.js';
|
|
1329
|
+
export type { ImageInfo, NodeViewProps } from './render.js';
|
|
1330
|
+
export type { MarkStyle } from './look.js';
|
|
1331
|
+
export type { RunStyle } from './inline.js';
|