@react-x11/components 0.7.1 → 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 +295 -51
- 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/color-picker/ColorPicker.d.ts.map +1 -1
- package/dist/color-picker/ColorPicker.js +43 -33
- package/dist/color-picker/ColorPicker.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 +33 -15
- package/dist/internal/scroll.js.map +1 -1
- package/dist/internal/units.d.ts +2 -0
- package/dist/internal/units.d.ts.map +1 -0
- package/dist/internal/units.js +35 -0
- package/dist/internal/units.js.map +1 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +4 -2
- package/dist/internal/window.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/table/index.d.ts.map +1 -1
- package/dist/table/index.js +14 -5
- package/dist/table/index.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/dist/terminal/vt/node.d.ts +56 -3
- package/dist/terminal/vt/node.d.ts.map +1 -1
- package/dist/terminal/vt/node.js +106 -26
- package/dist/terminal/vt/node.js.map +1 -1
- package/dist/terminal/vt/renderer.d.ts +4 -0
- package/dist/terminal/vt/renderer.d.ts.map +1 -1
- package/dist/terminal/vt/renderer.js +14 -5
- package/dist/terminal/vt/renderer.js.map +1 -1
- package/dist/tree/index.d.ts.map +1 -1
- package/dist/tree/index.js +6 -4
- package/dist/tree/index.js.map +1 -1
- package/package.json +23 -4
- package/src/charts/node.ts +6 -2
- package/src/color-picker/ColorPicker.ts +107 -81
- 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 +39 -18
- package/src/internal/units.ts +34 -0
- package/src/internal/window.ts +4 -2
- 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/table/index.ts +14 -5
- package/src/terminal/backends.ts +4 -1
- package/src/terminal/index.ts +43 -15
- package/src/terminal/vt/node.ts +128 -29
- package/src/terminal/vt/renderer.ts +27 -5
- package/src/tree/index.ts +6 -4
- 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,822 @@
|
|
|
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, { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
|
|
28
|
+
import { useApp, useClipboard, useTheme } from 'react-x11';
|
|
29
|
+
import { XK_ESCAPE, XK_KP_ENTER, XK_RETURN } from 'react-x11/keysyms';
|
|
30
|
+
import { tint } from 'react-x11/style';
|
|
31
|
+
import { toggleMark } from 'prosemirror-commands';
|
|
32
|
+
import { redo, undo } from 'prosemirror-history';
|
|
33
|
+
import { Fragment, Slice } from 'prosemirror-model';
|
|
34
|
+
import { EditorState, Selection } from 'prosemirror-state';
|
|
35
|
+
import { hx } from '../internal/hx.js';
|
|
36
|
+
import { registerRichText } from '../richtext/index.js';
|
|
37
|
+
import { isBlockActive, isMarkActive, markAttrs, setLink, toggleBlockType, toggleList, toggleWrap, } from './commands.js';
|
|
38
|
+
import { DROP_TYPES, SLICE_TYPE } from './drag.js';
|
|
39
|
+
import { docFromHTML, htmlFromContent } from './html.js';
|
|
40
|
+
import { defaultPlugins } from './keymap.js';
|
|
41
|
+
import { deriveLook } from './look.js';
|
|
42
|
+
import { docFromText, markdownCodec, textFromDoc } from './markdown.js';
|
|
43
|
+
import { registerEditorElements, ROOT_ELEMENT, TEXT_ELEMENT } from './nodes.js';
|
|
44
|
+
import { renderBlockRange, renderBlocks } from './render.js';
|
|
45
|
+
import { schema as defaultSchema } from './schema.js';
|
|
46
|
+
import { acceptSuggestion, SUGGESTION_PAGE, suggesterFor, suggestionState, suggestions as suggestionPlugin, } from './suggest.js';
|
|
47
|
+
import { DEFAULT_TOOLBAR, resolveToolbar, Toolbar, toolbarItems, } from './toolbar.js';
|
|
48
|
+
import { RichEditorView } from './view.js';
|
|
49
|
+
import { useBlockWindow } from './virtual.js';
|
|
50
|
+
registerRichText();
|
|
51
|
+
registerEditorElements();
|
|
52
|
+
const h = React.createElement;
|
|
53
|
+
/** What a drag out of the editor offers to do: an editable one moves or
|
|
54
|
+
* copies, a read-only one only copies. */
|
|
55
|
+
const MOVE_OR_COPY = ['move', 'copy'];
|
|
56
|
+
const COPY_ONLY = ['copy'];
|
|
57
|
+
function codecFor(schema, format, markdown) {
|
|
58
|
+
switch (format) {
|
|
59
|
+
case 'html':
|
|
60
|
+
return {
|
|
61
|
+
parse: (source) => docFromHTML(schema, source),
|
|
62
|
+
serialize: (doc) => htmlFromContent(schema, doc),
|
|
63
|
+
};
|
|
64
|
+
case 'text':
|
|
65
|
+
return {
|
|
66
|
+
parse: (source) => docFromText(schema, source),
|
|
67
|
+
serialize: textFromDoc,
|
|
68
|
+
};
|
|
69
|
+
default:
|
|
70
|
+
return markdown ?? markdownCodec(schema);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function replaceDocument(view, doc, addToHistory) {
|
|
74
|
+
const { state } = view;
|
|
75
|
+
const tr = state.tr.replaceWith(0, state.doc.content.size, doc.content);
|
|
76
|
+
// keep the caret about where it was, inside what is there now
|
|
77
|
+
tr.setSelection(Selection.near(tr.doc.resolve(Math.min(state.selection.head, tr.doc.content.size))));
|
|
78
|
+
if (!addToHistory)
|
|
79
|
+
tr.setMeta('addToHistory', false);
|
|
80
|
+
view.dispatch(tr);
|
|
81
|
+
}
|
|
82
|
+
function inListOrCode(state) {
|
|
83
|
+
const { $from } = state.selection;
|
|
84
|
+
if ($from.parent.type.spec.code)
|
|
85
|
+
return true;
|
|
86
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
87
|
+
if (/item$/i.test($from.node(d).type.name))
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
function linkAt(state, pos) {
|
|
93
|
+
const $pos = state.doc.resolve(pos);
|
|
94
|
+
const node = $pos.parent.childAfter($pos.parentOffset).node ?? $pos.nodeBefore;
|
|
95
|
+
const link = node?.marks.find((m) => m.type.name === 'link');
|
|
96
|
+
return link && typeof link.attrs.href === 'string' ? link.attrs.href : null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* ```tsx
|
|
100
|
+
* <RichTextEditor
|
|
101
|
+
* defaultValue={note.body}
|
|
102
|
+
* onChange={(ev) => save(ev.value)}
|
|
103
|
+
* placeholder="Write something…"
|
|
104
|
+
* toolbar
|
|
105
|
+
* style={{ flexGrow: 1 }}
|
|
106
|
+
* />
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function RichTextEditor(props) {
|
|
110
|
+
const theme = useTheme();
|
|
111
|
+
const clipboard = useClipboard();
|
|
112
|
+
const { placeholder, readOnly = false, disabled = false, autoFocus, toolbar, style, styles, } = props;
|
|
113
|
+
const format = props.format ?? 'markdown';
|
|
114
|
+
const schema = props.state?.schema ?? props.schema ?? defaultSchema;
|
|
115
|
+
const codec = useMemo(() => codecFor(schema, format, props.markdown), [schema, format, props.markdown]);
|
|
116
|
+
// The latest props, for the callbacks the view holds on to.
|
|
117
|
+
const latest = useRef(props);
|
|
118
|
+
latest.current = props;
|
|
119
|
+
const codecRef = useRef(codec);
|
|
120
|
+
codecRef.current = codec;
|
|
121
|
+
const look = useMemo(() => deriveLook(theme, {
|
|
122
|
+
fontSize: props.fontSize,
|
|
123
|
+
fontFamily: props.fontFamily,
|
|
124
|
+
monoFamily: props.monoFamily,
|
|
125
|
+
markStyles: props.markStyles,
|
|
126
|
+
resolveLanguage: props.resolveLanguage,
|
|
127
|
+
highlight: props.highlight,
|
|
128
|
+
dim: disabled,
|
|
129
|
+
}), [
|
|
130
|
+
theme,
|
|
131
|
+
props.fontSize,
|
|
132
|
+
props.fontFamily,
|
|
133
|
+
props.monoFamily,
|
|
134
|
+
props.markStyles,
|
|
135
|
+
props.resolveLanguage,
|
|
136
|
+
props.highlight,
|
|
137
|
+
disabled,
|
|
138
|
+
]);
|
|
139
|
+
// The editor's own plugins, made once per schema and option set — a
|
|
140
|
+
// plugin instance carries its state across a reconfigure, so the undo
|
|
141
|
+
// history survives an app swapping its own plugins.
|
|
142
|
+
const defaults = useMemo(() => defaultPlugins(schema, {
|
|
143
|
+
history: props.history,
|
|
144
|
+
inputRules: props.inputRules,
|
|
145
|
+
keymap: props.keymap,
|
|
146
|
+
typography: props.typography,
|
|
147
|
+
}), [schema, props.history, props.inputRules, props.keymap, props.typography]);
|
|
148
|
+
// The suggestion plugin, made once while there are suggesters: it reads
|
|
149
|
+
// the latest prop through `latest`, so an inline array is not a rebuild —
|
|
150
|
+
// a rebuild would drop the list that is open.
|
|
151
|
+
const suggesting = !!props.suggestions?.length;
|
|
152
|
+
const suggest = useMemo(() => suggesting
|
|
153
|
+
? suggestionPlugin(() => latest.current.suggestions ?? [])
|
|
154
|
+
: null, [suggesting]);
|
|
155
|
+
const plugins = useMemo(() => [
|
|
156
|
+
...(props.plugins ?? []),
|
|
157
|
+
// ahead of the defaults, so a row takes Enter before the keymap does
|
|
158
|
+
...(suggest ? [suggest] : []),
|
|
159
|
+
...defaults,
|
|
160
|
+
], [props.plugins, suggest, defaults]);
|
|
161
|
+
const listeners = useRef(new Set());
|
|
162
|
+
/** The value last reported through `onChange`, or accepted from `value`. */
|
|
163
|
+
const reported = useRef(null);
|
|
164
|
+
const handleRef = useRef(null);
|
|
165
|
+
const rootRef = useRef(null);
|
|
166
|
+
const [linkEdit, setLinkEdit] = useState(null);
|
|
167
|
+
// A suggestion list shows only while the editor has focus.
|
|
168
|
+
const [focused, setFocused] = useState(false);
|
|
169
|
+
// The trigger being typed is drawn in the accent colour, unless the app
|
|
170
|
+
// says what the `suggestion` class looks like. Memoized: a new object
|
|
171
|
+
// redraws every block's decorations (`configure`).
|
|
172
|
+
const decorationClasses = useMemo(() => ({ suggestion: { color: look.accent }, ...props.decorationClasses }), [look.accent, props.decorationClasses]);
|
|
173
|
+
const config = {
|
|
174
|
+
onRender: () => { },
|
|
175
|
+
onUpdate: (prev, trs) => onUpdate(prev, trs),
|
|
176
|
+
onFocusChange: setFocused,
|
|
177
|
+
clipboard,
|
|
178
|
+
...(placeholder ? { placeholder } : null),
|
|
179
|
+
colors: {
|
|
180
|
+
selection: look.selection,
|
|
181
|
+
selectionBlurred: look.selectionBlurred,
|
|
182
|
+
caret: look.caret,
|
|
183
|
+
placeholder: look.muted,
|
|
184
|
+
preedit: look.text,
|
|
185
|
+
},
|
|
186
|
+
decorationClasses,
|
|
187
|
+
};
|
|
188
|
+
const viewRef = useRef(null);
|
|
189
|
+
if (!viewRef.current) {
|
|
190
|
+
let state = props.state;
|
|
191
|
+
if (!state) {
|
|
192
|
+
const initial = props.value ?? props.defaultValue;
|
|
193
|
+
if (typeof initial === 'string')
|
|
194
|
+
reported.current = initial;
|
|
195
|
+
const doc = initial === undefined
|
|
196
|
+
? undefined
|
|
197
|
+
: typeof initial === 'string'
|
|
198
|
+
? codec.parse(initial)
|
|
199
|
+
: initial;
|
|
200
|
+
state = EditorState.create({ schema, doc, plugins });
|
|
201
|
+
}
|
|
202
|
+
const viewProps = {
|
|
203
|
+
...props.editorProps,
|
|
204
|
+
editable: () => !latest.current.readOnly && !latest.current.disabled,
|
|
205
|
+
handleClick: (view, pos, event) => {
|
|
206
|
+
const own = latest.current.editorProps?.handleClick;
|
|
207
|
+
if (own?.call(undefined, view, pos, event))
|
|
208
|
+
return true;
|
|
209
|
+
return openLinkAt(pos, event);
|
|
210
|
+
},
|
|
211
|
+
...(props.dispatchTransaction
|
|
212
|
+
? {
|
|
213
|
+
dispatchTransaction: (tr) => latest.current.dispatchTransaction?.(tr),
|
|
214
|
+
}
|
|
215
|
+
: null),
|
|
216
|
+
};
|
|
217
|
+
viewRef.current = new RichEditorView(state, viewProps, config);
|
|
218
|
+
}
|
|
219
|
+
const view = viewRef.current;
|
|
220
|
+
// Every state change re-renders this component; the document itself only
|
|
221
|
+
// re-renders the blocks whose nodes changed (./render.ts).
|
|
222
|
+
const subscribe = useCallback((fn) => {
|
|
223
|
+
listeners.current.add(fn);
|
|
224
|
+
return () => {
|
|
225
|
+
listeners.current.delete(fn);
|
|
226
|
+
};
|
|
227
|
+
}, []);
|
|
228
|
+
const state = useSyncExternalStore(subscribe, () => view.state, () => view.state);
|
|
229
|
+
function event(type) {
|
|
230
|
+
const at = view.state;
|
|
231
|
+
let cached = null;
|
|
232
|
+
return {
|
|
233
|
+
type,
|
|
234
|
+
get value() {
|
|
235
|
+
return (cached ??= codecRef.current.serialize(at.doc));
|
|
236
|
+
},
|
|
237
|
+
state: at,
|
|
238
|
+
name: latest.current.name,
|
|
239
|
+
target: handleRef.current,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function onUpdate(prev, trs) {
|
|
243
|
+
for (const fn of [...listeners.current])
|
|
244
|
+
fn();
|
|
245
|
+
const now = view.state;
|
|
246
|
+
const p = latest.current;
|
|
247
|
+
if (prev.doc !== now.doc && p.onChange) {
|
|
248
|
+
let cached = null;
|
|
249
|
+
p.onChange({
|
|
250
|
+
type: 'change',
|
|
251
|
+
get value() {
|
|
252
|
+
if (cached === null) {
|
|
253
|
+
cached = codecRef.current.serialize(now.doc);
|
|
254
|
+
reported.current = cached;
|
|
255
|
+
}
|
|
256
|
+
return cached;
|
|
257
|
+
},
|
|
258
|
+
doc: now.doc,
|
|
259
|
+
state: now,
|
|
260
|
+
transactions: trs,
|
|
261
|
+
name: p.name,
|
|
262
|
+
target: handleRef.current,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (!prev.selection.eq(now.selection))
|
|
266
|
+
p.onSelectionChange?.(event('selectionchange'));
|
|
267
|
+
}
|
|
268
|
+
function openLinkAt(pos, ev) {
|
|
269
|
+
const p = latest.current;
|
|
270
|
+
if (!p.onLink)
|
|
271
|
+
return false;
|
|
272
|
+
const editing = !p.readOnly && !p.disabled;
|
|
273
|
+
// editing, a plain click places the caret; Mod-click follows
|
|
274
|
+
if (editing && !(ev.ctrlKey || ev.metaKey))
|
|
275
|
+
return false;
|
|
276
|
+
const href = linkAt(view.state, pos);
|
|
277
|
+
if (!href)
|
|
278
|
+
return false;
|
|
279
|
+
p.onLink(href, ev);
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
const editLink = useCallback(() => {
|
|
283
|
+
const root = rootRef.current;
|
|
284
|
+
const frame = root?.getClientRects()[0];
|
|
285
|
+
if (!root || !frame || !view.state.schema.marks.link)
|
|
286
|
+
return;
|
|
287
|
+
const { from, to } = view.state.selection;
|
|
288
|
+
const a = view.coordsAtPos(from);
|
|
289
|
+
const b = view.coordsAtPos(to);
|
|
290
|
+
const attrs = markAttrs(view.state, view.state.schema.marks.link);
|
|
291
|
+
setLinkEdit({
|
|
292
|
+
at: {
|
|
293
|
+
x: a.left - frame.x,
|
|
294
|
+
y: Math.min(a.top, b.top) - frame.y,
|
|
295
|
+
width: Math.max(1, (a.top === b.top ? b.right : a.left) - a.left),
|
|
296
|
+
height: Math.max(a.bottom, b.bottom) - Math.min(a.top, b.top),
|
|
297
|
+
},
|
|
298
|
+
draft: typeof attrs?.href === 'string' ? attrs.href : '',
|
|
299
|
+
});
|
|
300
|
+
}, [view]);
|
|
301
|
+
const handle = useMemo(() => {
|
|
302
|
+
const nodeType = (name) => view.state.schema.nodes[name];
|
|
303
|
+
const markType = (name) => view.state.schema.marks[name];
|
|
304
|
+
const itemType = () => view.state.schema.nodes.list_item ?? view.state.schema.nodes.listItem;
|
|
305
|
+
return {
|
|
306
|
+
get view() {
|
|
307
|
+
return view;
|
|
308
|
+
},
|
|
309
|
+
get state() {
|
|
310
|
+
return view.state;
|
|
311
|
+
},
|
|
312
|
+
get schema() {
|
|
313
|
+
return view.state.schema;
|
|
314
|
+
},
|
|
315
|
+
getValue(fmt) {
|
|
316
|
+
const c = fmt
|
|
317
|
+
? codecFor(view.state.schema, fmt, latest.current.markdown)
|
|
318
|
+
: codecRef.current;
|
|
319
|
+
return c.serialize(view.state.doc);
|
|
320
|
+
},
|
|
321
|
+
setValue(value, options = {}) {
|
|
322
|
+
const c = options.format
|
|
323
|
+
? codecFor(view.state.schema, options.format, latest.current.markdown)
|
|
324
|
+
: codecRef.current;
|
|
325
|
+
const doc = typeof value === 'string' ? c.parse(value) : value;
|
|
326
|
+
if (typeof value === 'string' && !options.format)
|
|
327
|
+
reported.current = value;
|
|
328
|
+
replaceDocument(view, doc, options.addToHistory ?? false);
|
|
329
|
+
},
|
|
330
|
+
insertContent(value, fmt) {
|
|
331
|
+
const doc = typeof value === 'string'
|
|
332
|
+
? (fmt
|
|
333
|
+
? codecFor(view.state.schema, fmt, latest.current.markdown)
|
|
334
|
+
: codecRef.current).parse(value)
|
|
335
|
+
: value;
|
|
336
|
+
// a whole document goes in as its blocks, open at both ends so a
|
|
337
|
+
// single paragraph joins the one the caret is in
|
|
338
|
+
const content = doc.type === view.state.schema.topNodeType
|
|
339
|
+
? doc.content
|
|
340
|
+
: Fragment.from(doc);
|
|
341
|
+
view.dispatch(view.state.tr
|
|
342
|
+
.replaceSelection(Slice.maxOpen(content))
|
|
343
|
+
.scrollIntoView());
|
|
344
|
+
},
|
|
345
|
+
run: (command) => view.run(command),
|
|
346
|
+
can: (command) => command(view.state, undefined, view.asEditorView),
|
|
347
|
+
isActive(name, attrs) {
|
|
348
|
+
const mark = markType(name);
|
|
349
|
+
if (mark)
|
|
350
|
+
return isMarkActive(view.state, mark);
|
|
351
|
+
const node = nodeType(name);
|
|
352
|
+
return node ? isBlockActive(view.state, node, attrs) : false;
|
|
353
|
+
},
|
|
354
|
+
toggleMark(name, attrs) {
|
|
355
|
+
const type = markType(name);
|
|
356
|
+
return !!type && view.run(toggleMark(type, attrs));
|
|
357
|
+
},
|
|
358
|
+
setBlock(name, attrs) {
|
|
359
|
+
const type = nodeType(name);
|
|
360
|
+
return !!type && view.run(toggleBlockType(type, attrs));
|
|
361
|
+
},
|
|
362
|
+
toggleList(name) {
|
|
363
|
+
const type = nodeType(name);
|
|
364
|
+
const item = itemType();
|
|
365
|
+
return !!type && !!item && view.run(toggleList(type, item));
|
|
366
|
+
},
|
|
367
|
+
toggleWrap(name) {
|
|
368
|
+
const type = nodeType(name);
|
|
369
|
+
return !!type && view.run(toggleWrap(type));
|
|
370
|
+
},
|
|
371
|
+
undo: () => view.run(undo),
|
|
372
|
+
redo: () => view.run(redo),
|
|
373
|
+
editLink,
|
|
374
|
+
focus: () => view.focus(),
|
|
375
|
+
blur: () => {
|
|
376
|
+
view.dom?.blur?.();
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}, [view, editLink]);
|
|
380
|
+
handleRef.current = handle;
|
|
381
|
+
useImperativeHandle(props.ref, () => handle, [handle]);
|
|
382
|
+
// The component's half of the view, refreshed every render; the view
|
|
383
|
+
// acts only on what changed.
|
|
384
|
+
useLayoutEffect(() => {
|
|
385
|
+
view.configure(config);
|
|
386
|
+
});
|
|
387
|
+
// An app-owned state, handed back after each transaction.
|
|
388
|
+
useLayoutEffect(() => {
|
|
389
|
+
if (props.state && props.state !== view.state)
|
|
390
|
+
view.updateState(props.state);
|
|
391
|
+
}, [props.state, view]);
|
|
392
|
+
// A controlled value: anything but what the editor last reported replaces
|
|
393
|
+
// the document.
|
|
394
|
+
useLayoutEffect(() => {
|
|
395
|
+
const value = props.value;
|
|
396
|
+
if (value === undefined || props.state || value === reported.current)
|
|
397
|
+
return;
|
|
398
|
+
reported.current = value;
|
|
399
|
+
replaceDocument(view, codec.parse(value), false);
|
|
400
|
+
}, [props.value, props.state, codec, view]);
|
|
401
|
+
// New plugins (the app's, or the defaults' options): reconfigure, keeping
|
|
402
|
+
// every plugin instance that is still there — and its state.
|
|
403
|
+
const configured = useRef(plugins);
|
|
404
|
+
useLayoutEffect(() => {
|
|
405
|
+
if (props.state || plugins === configured.current)
|
|
406
|
+
return;
|
|
407
|
+
configured.current = plugins;
|
|
408
|
+
view.updateState(view.state.reconfigure({ plugins }));
|
|
409
|
+
}, [plugins, props.state, view]);
|
|
410
|
+
const editorProps = props.editorProps;
|
|
411
|
+
const firstProps = useRef(editorProps);
|
|
412
|
+
useLayoutEffect(() => {
|
|
413
|
+
if (editorProps === firstProps.current)
|
|
414
|
+
return;
|
|
415
|
+
firstProps.current = editorProps;
|
|
416
|
+
view.setProps({
|
|
417
|
+
...editorProps,
|
|
418
|
+
editable: view.props.editable,
|
|
419
|
+
handleClick: view.props.handleClick,
|
|
420
|
+
});
|
|
421
|
+
}, [editorProps, view]);
|
|
422
|
+
// the plugins' views, now that the root element exists for them to find
|
|
423
|
+
useLayoutEffect(() => {
|
|
424
|
+
view.mountPluginViews();
|
|
425
|
+
}, [view]);
|
|
426
|
+
useEffect(() => () => view.destroy(), [view]);
|
|
427
|
+
const editable = !readOnly && !disabled;
|
|
428
|
+
// what a table measures its columns with; none on the mock backend
|
|
429
|
+
const fonts = useApp()?.fonts ?? null;
|
|
430
|
+
const ctx = useMemo(() => ({
|
|
431
|
+
view,
|
|
432
|
+
look,
|
|
433
|
+
editable,
|
|
434
|
+
fonts,
|
|
435
|
+
...(props.nodeViews ? { nodeViews: props.nodeViews } : null),
|
|
436
|
+
...(props.renderImage ? { renderImage: props.renderImage } : null),
|
|
437
|
+
}), [view, look, editable, fonts, props.nodeViews, props.renderImage]);
|
|
438
|
+
// A long document draws only the blocks near the viewport, in a window
|
|
439
|
+
// the view asks about when what it needs is not drawn (./virtual.ts).
|
|
440
|
+
const windowed = useBlockWindow({
|
|
441
|
+
doc: state.doc,
|
|
442
|
+
keys: view.keys,
|
|
443
|
+
virtual: props.virtual ?? 'auto',
|
|
444
|
+
estimate: Math.round(look.size * 1.5) + look.blockGap,
|
|
445
|
+
gap: look.blockGap,
|
|
446
|
+
});
|
|
447
|
+
const { virtualizing, first, last, register } = windowed;
|
|
448
|
+
const blocks = useMemo(() => virtualizing
|
|
449
|
+
? renderBlockRange(state.doc, first, last, ctx, register)
|
|
450
|
+
: renderBlocks(state.doc, 0, ctx), [state.doc, ctx, virtualizing, first, last, register]);
|
|
451
|
+
useLayoutEffect(() => {
|
|
452
|
+
view.attachWindow(virtualizing ? windowed.window : null);
|
|
453
|
+
}, [view, virtualizing, windowed.window]);
|
|
454
|
+
// --- input ---------------------------------------------------------------
|
|
455
|
+
const onKeyDown = (ev) => {
|
|
456
|
+
const p = latest.current;
|
|
457
|
+
p.onKeyDown?.(ev);
|
|
458
|
+
if (ev.defaultPrevented || p.disabled)
|
|
459
|
+
return;
|
|
460
|
+
const mac = view.primaryModifier === 'meta';
|
|
461
|
+
const primary = mac ? ev.metaKey : ev.ctrlKey;
|
|
462
|
+
const k = ev.keysym;
|
|
463
|
+
if ((k === XK_RETURN || k === XK_KP_ENTER) && p.onSubmit) {
|
|
464
|
+
const plain = !ev.shiftKey && !primary && !ev.altKey;
|
|
465
|
+
// a plain Enter while a suggestion list shows rows takes a row: it is
|
|
466
|
+
// the list's plugin's, a step further on
|
|
467
|
+
const listing = plain && editable && !!suggestionState(view.state)?.items?.length;
|
|
468
|
+
const submit = !listing &&
|
|
469
|
+
(p.submitOnEnter
|
|
470
|
+
? plain && !inListOrCode(view.state)
|
|
471
|
+
: primary && !ev.shiftKey);
|
|
472
|
+
if (submit) {
|
|
473
|
+
ev.preventDefault();
|
|
474
|
+
p.onSubmit(event('submit'));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (primary &&
|
|
479
|
+
!ev.altKey &&
|
|
480
|
+
!ev.shiftKey &&
|
|
481
|
+
k === 0x6b /* k */ &&
|
|
482
|
+
editable) {
|
|
483
|
+
ev.preventDefault();
|
|
484
|
+
editLink();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (k === XK_ESCAPE && linkEdit)
|
|
488
|
+
setLinkEdit(null);
|
|
489
|
+
};
|
|
490
|
+
const onMouseDown = (ev) => {
|
|
491
|
+
latest.current.onMouseDown?.(ev);
|
|
492
|
+
if (ev.defaultPrevented || latest.current.disabled)
|
|
493
|
+
return;
|
|
494
|
+
if (view.mouseDown(ev))
|
|
495
|
+
ev.preventDefault();
|
|
496
|
+
};
|
|
497
|
+
const setRoot = useCallback((node) => {
|
|
498
|
+
rootRef.current = node ?? null;
|
|
499
|
+
}, []);
|
|
500
|
+
const pane = windowed.box;
|
|
501
|
+
const setScroller = useCallback((node) => {
|
|
502
|
+
pane.current = node ?? null;
|
|
503
|
+
view.attachScroller(pane.current);
|
|
504
|
+
}, [view, pane]);
|
|
505
|
+
// What a drag out of the editor offers, resolved when a drop asks for it:
|
|
506
|
+
// a copy's text and HTML, and the slice itself for a drop in this app.
|
|
507
|
+
const dragData = useMemo(() => ({
|
|
508
|
+
'text/plain': () => view.dragText(),
|
|
509
|
+
'text/html': () => view.dragHTML(),
|
|
510
|
+
[SLICE_TYPE]: () => view.dragPayload(),
|
|
511
|
+
}), [view]);
|
|
512
|
+
// --- chrome --------------------------------------------------------------
|
|
513
|
+
let chrome = null;
|
|
514
|
+
if (typeof toolbar === 'function') {
|
|
515
|
+
chrome = toolbar(handle);
|
|
516
|
+
}
|
|
517
|
+
else if (toolbar) {
|
|
518
|
+
const items = toolbarItems(schema, {
|
|
519
|
+
link: () => {
|
|
520
|
+
editLink();
|
|
521
|
+
return true;
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
chrome = h(Toolbar, {
|
|
525
|
+
entries: resolveToolbar(toolbar === true ? DEFAULT_TOOLBAR : toolbar, items),
|
|
526
|
+
state,
|
|
527
|
+
run: (command) => {
|
|
528
|
+
view.run(command);
|
|
529
|
+
view.focus();
|
|
530
|
+
},
|
|
531
|
+
look,
|
|
532
|
+
disabled: !editable,
|
|
533
|
+
...(styles?.toolbar ? { style: styles.toolbar } : null),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
const closeLink = () => {
|
|
537
|
+
setLinkEdit(null);
|
|
538
|
+
view.focus();
|
|
539
|
+
};
|
|
540
|
+
const applyLink = (href) => {
|
|
541
|
+
const target = href.trim();
|
|
542
|
+
view.run(setLink(target === '' ? null : target));
|
|
543
|
+
closeLink();
|
|
544
|
+
};
|
|
545
|
+
const linkPopup = linkEdit &&
|
|
546
|
+
hx('popup', {
|
|
547
|
+
theme: theme,
|
|
548
|
+
anchor: {
|
|
549
|
+
to: rootRef,
|
|
550
|
+
at: linkEdit.at,
|
|
551
|
+
placement: 'bottom',
|
|
552
|
+
},
|
|
553
|
+
onDismiss: closeLink,
|
|
554
|
+
}, hx('box', {
|
|
555
|
+
style: {
|
|
556
|
+
flexDirection: 'row',
|
|
557
|
+
alignItems: 'center',
|
|
558
|
+
gap: 6,
|
|
559
|
+
padding: 6,
|
|
560
|
+
borderWidth: 1,
|
|
561
|
+
borderColor: look.border,
|
|
562
|
+
borderRadius: look.radius,
|
|
563
|
+
backgroundColor: look.surface,
|
|
564
|
+
},
|
|
565
|
+
}, hx('textinput', {
|
|
566
|
+
value: linkEdit.draft,
|
|
567
|
+
placeholder: 'https://',
|
|
568
|
+
autoFocus: true,
|
|
569
|
+
style: { width: 260 },
|
|
570
|
+
onChange: (ev) => setLinkEdit((cur) => (cur ? { ...cur, draft: ev.value } : cur)),
|
|
571
|
+
onSubmit: (ev) => applyLink(ev.value),
|
|
572
|
+
onKeyDown: (ev) => {
|
|
573
|
+
if (ev.keysym === XK_ESCAPE) {
|
|
574
|
+
ev.preventDefault();
|
|
575
|
+
closeLink();
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
})));
|
|
579
|
+
// The suggestion list, drawn from the plugin's state and nothing else
|
|
580
|
+
// (./suggest.ts) — so an app that owns the EditorState gets it too. It hangs
|
|
581
|
+
// off the textblock the trigger is in, at the trigger, so it follows the
|
|
582
|
+
// text as the document scrolls and unmaps while the text is out of view.
|
|
583
|
+
// It never takes focus: a press on a row takes the row, and the caret
|
|
584
|
+
// stays in the editor.
|
|
585
|
+
const suggestion = suggestionState(state);
|
|
586
|
+
const firstRow = useRef(0);
|
|
587
|
+
const rowsShown = focused && editable && !!suggestion?.items?.length;
|
|
588
|
+
const anchor = rowsShown && suggestion ? view.anchorAt(suggestion.from) : null;
|
|
589
|
+
let suggestionPopup = null;
|
|
590
|
+
if (suggestion?.items && anchor) {
|
|
591
|
+
const { items, selected } = suggestion;
|
|
592
|
+
// the window of rows moves only as far as keeps the highlight in it
|
|
593
|
+
let first = Math.min(firstRow.current, Math.max(0, items.length - SUGGESTION_PAGE));
|
|
594
|
+
if (selected < first)
|
|
595
|
+
first = selected;
|
|
596
|
+
if (selected >= first + SUGGESTION_PAGE)
|
|
597
|
+
first = selected - SUGGESTION_PAGE + 1;
|
|
598
|
+
firstRow.current = first;
|
|
599
|
+
// a suggester may draw its rows itself; the row box — the highlight
|
|
600
|
+
// behind it, the press on it — stays the editor's
|
|
601
|
+
const custom = suggesterFor(state)?.renderItem;
|
|
602
|
+
const rowHeight = Math.round(look.size + 12);
|
|
603
|
+
const rows = items.slice(first, first + SUGGESTION_PAGE).map((item, i) => {
|
|
604
|
+
const index = first + i;
|
|
605
|
+
const on = index === selected;
|
|
606
|
+
if (custom) {
|
|
607
|
+
return hx('box', {
|
|
608
|
+
key: `${index} ${item.label}`,
|
|
609
|
+
style: {
|
|
610
|
+
flexDirection: 'row',
|
|
611
|
+
alignItems: 'center',
|
|
612
|
+
minHeight: rowHeight,
|
|
613
|
+
paddingLeft: 8,
|
|
614
|
+
paddingRight: 8,
|
|
615
|
+
borderRadius: Math.max(0, look.radius - 2),
|
|
616
|
+
backgroundColor: on ? look.accent : 'transparent',
|
|
617
|
+
...(on
|
|
618
|
+
? null
|
|
619
|
+
: { ':hover': { backgroundColor: tint(look.text, 0.08) } }),
|
|
620
|
+
},
|
|
621
|
+
onMouseDown: (ev) => {
|
|
622
|
+
ev.preventDefault();
|
|
623
|
+
view.run(acceptSuggestion(index));
|
|
624
|
+
},
|
|
625
|
+
}, custom(item, { selected: on, query: suggestion.query }));
|
|
626
|
+
}
|
|
627
|
+
return hx('box', {
|
|
628
|
+
key: `${index} ${item.label}`,
|
|
629
|
+
style: {
|
|
630
|
+
flexDirection: 'row',
|
|
631
|
+
alignItems: 'center',
|
|
632
|
+
gap: 12,
|
|
633
|
+
height: rowHeight,
|
|
634
|
+
paddingLeft: 8,
|
|
635
|
+
paddingRight: 8,
|
|
636
|
+
borderRadius: Math.max(0, look.radius - 2),
|
|
637
|
+
backgroundColor: on ? look.accent : 'transparent',
|
|
638
|
+
...(on
|
|
639
|
+
? null
|
|
640
|
+
: { ':hover': { backgroundColor: tint(look.text, 0.08) } }),
|
|
641
|
+
},
|
|
642
|
+
onMouseDown: (ev) => {
|
|
643
|
+
ev.preventDefault();
|
|
644
|
+
view.run(acceptSuggestion(index));
|
|
645
|
+
},
|
|
646
|
+
}, hx('text', {
|
|
647
|
+
style: {
|
|
648
|
+
flexGrow: 1,
|
|
649
|
+
fontFamily: look.family,
|
|
650
|
+
fontSize: look.size,
|
|
651
|
+
color: on ? look.accentText : look.text,
|
|
652
|
+
},
|
|
653
|
+
}, item.label), item.detail
|
|
654
|
+
? hx('text', {
|
|
655
|
+
style: {
|
|
656
|
+
fontFamily: look.family,
|
|
657
|
+
fontSize: Math.max(10, look.size - 2),
|
|
658
|
+
color: on ? look.accentText : look.muted,
|
|
659
|
+
},
|
|
660
|
+
}, item.detail)
|
|
661
|
+
: null);
|
|
662
|
+
});
|
|
663
|
+
suggestionPopup = hx('popup', {
|
|
664
|
+
theme: theme,
|
|
665
|
+
anchor: { to: anchor.node, at: anchor.at, placement: 'bottom' },
|
|
666
|
+
minWidth: 200,
|
|
667
|
+
maxWidth: 420,
|
|
668
|
+
}, hx('box', {
|
|
669
|
+
style: {
|
|
670
|
+
flexGrow: 1,
|
|
671
|
+
flexDirection: 'column',
|
|
672
|
+
padding: 4,
|
|
673
|
+
borderWidth: 1,
|
|
674
|
+
borderColor: look.border,
|
|
675
|
+
borderRadius: look.radius,
|
|
676
|
+
backgroundColor: look.surface,
|
|
677
|
+
},
|
|
678
|
+
}, ...rows));
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
firstRow.current = 0;
|
|
682
|
+
}
|
|
683
|
+
const frame = {
|
|
684
|
+
flexDirection: 'column',
|
|
685
|
+
borderWidth: 1,
|
|
686
|
+
borderColor: look.border,
|
|
687
|
+
borderRadius: look.radius,
|
|
688
|
+
backgroundColor: look.background,
|
|
689
|
+
overflow: 'hidden',
|
|
690
|
+
...(editable
|
|
691
|
+
? {
|
|
692
|
+
':focus-within': {
|
|
693
|
+
borderColor: String(theme.borderFocus ?? look.accent),
|
|
694
|
+
},
|
|
695
|
+
}
|
|
696
|
+
: null),
|
|
697
|
+
};
|
|
698
|
+
return h(ROOT_ELEMENT, {
|
|
699
|
+
ref: setRoot,
|
|
700
|
+
host: view,
|
|
701
|
+
focusable: !disabled,
|
|
702
|
+
disabled,
|
|
703
|
+
autoFocus,
|
|
704
|
+
'aria-label': props['aria-label'] ?? placeholder,
|
|
705
|
+
'data-testname': props['data-testname'],
|
|
706
|
+
style: style
|
|
707
|
+
? [frame, ...(Array.isArray(style) ? style : [style])]
|
|
708
|
+
: frame,
|
|
709
|
+
onKeyDown,
|
|
710
|
+
onMouseDown,
|
|
711
|
+
onMouseMove: (ev) => {
|
|
712
|
+
if (view.mouseMove(ev))
|
|
713
|
+
ev.preventDefault();
|
|
714
|
+
},
|
|
715
|
+
onMouseUp: () => view.mouseUp(),
|
|
716
|
+
onContextMenu: (ev) => {
|
|
717
|
+
if (ev.defaultPrevented || latest.current.disabled)
|
|
718
|
+
return;
|
|
719
|
+
view.contextMenu(ev);
|
|
720
|
+
ev.preventDefault();
|
|
721
|
+
},
|
|
722
|
+
// drag and drop (./drag.ts): the selection drags out, and a drop is
|
|
723
|
+
// read like a paste. A press on the selection is left to core, which
|
|
724
|
+
// arms the drag here; `onDragStart` cancels one that is not the
|
|
725
|
+
// selection's, and leaves a drag of something inside alone.
|
|
726
|
+
draggable: !disabled,
|
|
727
|
+
dragData,
|
|
728
|
+
dragActions: editable ? MOVE_OR_COPY : COPY_ONLY,
|
|
729
|
+
onDragStart: (ev) => {
|
|
730
|
+
if (ev.target !== rootRef.current)
|
|
731
|
+
return;
|
|
732
|
+
if (!view.dragStart())
|
|
733
|
+
ev.preventDefault();
|
|
734
|
+
},
|
|
735
|
+
onDrag: (ev) => {
|
|
736
|
+
if (ev.target === rootRef.current)
|
|
737
|
+
view.dragMoved(ev.ctrlKey, ev.altKey);
|
|
738
|
+
},
|
|
739
|
+
onDragEnd: (ev) => {
|
|
740
|
+
if (ev.target === rootRef.current)
|
|
741
|
+
view.dragEnd(ev.action, ev.dropped);
|
|
742
|
+
},
|
|
743
|
+
...(editable
|
|
744
|
+
? {
|
|
745
|
+
dropAccept: DROP_TYPES,
|
|
746
|
+
onDragOver: (ev) => {
|
|
747
|
+
if (!view.dragOver(ev.x, ev.y))
|
|
748
|
+
ev.reject();
|
|
749
|
+
},
|
|
750
|
+
onDragLeave: () => view.dragLeave(),
|
|
751
|
+
onDrop: (ev) => {
|
|
752
|
+
const settle = (answer) => {
|
|
753
|
+
if (answer)
|
|
754
|
+
ev.accept(answer);
|
|
755
|
+
else
|
|
756
|
+
ev.reject();
|
|
757
|
+
};
|
|
758
|
+
const answer = view.drop(ev);
|
|
759
|
+
if (answer instanceof Promise)
|
|
760
|
+
return answer.then(settle);
|
|
761
|
+
settle(answer);
|
|
762
|
+
return undefined;
|
|
763
|
+
},
|
|
764
|
+
}
|
|
765
|
+
: null),
|
|
766
|
+
onFocus: props.onFocus,
|
|
767
|
+
onBlur: props.onBlur,
|
|
768
|
+
}, chrome, hx('box', {
|
|
769
|
+
ref: setScroller,
|
|
770
|
+
// a press inside focuses the editor, not the viewport it scrolls in
|
|
771
|
+
focusable: false,
|
|
772
|
+
// `flexBasis: 'auto'` is load-bearing: a scroll box that grows
|
|
773
|
+
// takes a zero basis, so an editor nobody gave a height collapsed
|
|
774
|
+
// to its border. With it the viewport is its content's height — a
|
|
775
|
+
// composer grows as it is typed in, up to the frame's `maxHeight`,
|
|
776
|
+
// and past that it shrinks to fit and scrolls; given a height or a
|
|
777
|
+
// `flexGrow`, it fills.
|
|
778
|
+
style: {
|
|
779
|
+
overflow: 'scroll',
|
|
780
|
+
flexGrow: 1,
|
|
781
|
+
flexShrink: 1,
|
|
782
|
+
flexBasis: 'auto',
|
|
783
|
+
flexDirection: 'column',
|
|
784
|
+
},
|
|
785
|
+
...(virtualizing
|
|
786
|
+
? { onViewport: windowed.onViewport, onScroll: windowed.onScroll }
|
|
787
|
+
: null),
|
|
788
|
+
}, hx('box', {
|
|
789
|
+
style: {
|
|
790
|
+
flexDirection: 'column',
|
|
791
|
+
gap: look.blockGap,
|
|
792
|
+
padding: Math.round(look.size * 0.85),
|
|
793
|
+
flexGrow: 1,
|
|
794
|
+
...styles?.content,
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
// the blocks outside a long document's window, as space
|
|
798
|
+
windowed.above > 0
|
|
799
|
+
? hx('box', {
|
|
800
|
+
key: 'spacer:before',
|
|
801
|
+
style: { flexShrink: 0, height: windowed.above },
|
|
802
|
+
})
|
|
803
|
+
: null, ...blocks, windowed.below > 0
|
|
804
|
+
? hx('box', {
|
|
805
|
+
key: 'spacer:after',
|
|
806
|
+
style: { flexShrink: 0, height: windowed.below },
|
|
807
|
+
})
|
|
808
|
+
: null)), linkPopup, suggestionPopup);
|
|
809
|
+
}
|
|
810
|
+
// --- the rest of the surface -------------------------------------------------
|
|
811
|
+
export { ROOT_ELEMENT as RICH_EDITOR_ELEMENT, TEXT_ELEMENT as RICH_EDITOR_TEXT_ELEMENT, };
|
|
812
|
+
export { RichEditorView };
|
|
813
|
+
export { schema, nodes, marks } from './schema.js';
|
|
814
|
+
export { defaultPlugins, editingKeymap, markdownInputRules } from './keymap.js';
|
|
815
|
+
export { dedentCode, goToCell, indentCode, insertHorizontalRule, isBlockActive, isMarkActive, markAttrs, setLink, setTaskChecked, splitItem, toggleBlockType, toggleList, toggleTaskList, toggleWrap, } from './commands.js';
|
|
816
|
+
export { docFromMarkdown, markdownCodec, markdownFromDoc, docFromText, textFromDoc, } from './markdown.js';
|
|
817
|
+
export { docFromHTML, htmlFromContent } from './html.js';
|
|
818
|
+
export { DEFAULT_TOOLBAR, toolbarItems } from './toolbar.js';
|
|
819
|
+
export { acceptSuggestion, dismissSuggestion, filterSuggestions, selectSuggestion, suggesterFor, suggestionState, suggestions, } from './suggest.js';
|
|
820
|
+
export { addColumnAfter, addColumnBefore, addRowAfter, addRowBefore, columnAlign, deleteColumn, deleteRow, deleteTable, insertTable, isInTable, setColumnAlign, tableRepair, } from './tables.js';
|
|
821
|
+
export { remoteCaret } from './collab.js';
|
|
822
|
+
//# sourceMappingURL=index.js.map
|