@stesura/react-wordgard 0.0.0-stage → 0.2.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.
Files changed (111) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE.txt +21 -0
  3. package/README.md +912 -2
  4. package/THIRD_PARTY_LICENSES.txt +93 -0
  5. package/dist/ReactWordgard.d.ts +13 -0
  6. package/dist/ReactWordgard.js +94 -0
  7. package/dist/changes.d.ts +14 -0
  8. package/dist/changes.js +163 -0
  9. package/dist/components/ChildNodeViews.d.ts +8 -0
  10. package/dist/components/ChildNodeViews.js +36 -0
  11. package/dist/components/DecorationRoot.d.ts +13 -0
  12. package/dist/components/DecorationRoot.js +31 -0
  13. package/dist/components/ErrorBoundary.d.ts +18 -0
  14. package/dist/components/ErrorBoundary.js +14 -0
  15. package/dist/components/SeparatorHackView.d.ts +2 -0
  16. package/dist/components/SeparatorHackView.js +4 -0
  17. package/dist/components/ShapeView.d.ts +12 -0
  18. package/dist/components/ShapeView.js +24 -0
  19. package/dist/components/TextNodeView.d.ts +5 -0
  20. package/dist/components/TextNodeView.js +4 -0
  21. package/dist/components/TrailingHackView.d.ts +2 -0
  22. package/dist/components/TrailingHackView.js +4 -0
  23. package/dist/components/WidgetView.d.ts +10 -0
  24. package/dist/components/WidgetView.js +32 -0
  25. package/dist/components/WordgardDoc.d.ts +9 -0
  26. package/dist/components/WordgardDoc.js +36 -0
  27. package/dist/components/WordgardEditor.d.ts +15 -0
  28. package/dist/components/WordgardEditor.js +17 -0
  29. package/dist/components/WordgardScope.d.ts +14 -0
  30. package/dist/components/WordgardScope.js +14 -0
  31. package/dist/components/WrapperView.d.ts +12 -0
  32. package/dist/components/WrapperView.js +23 -0
  33. package/dist/components/marks/MarkView.d.ts +11 -0
  34. package/dist/components/marks/MarkView.js +10 -0
  35. package/dist/components/marks/ReactMarkView.d.ts +10 -0
  36. package/dist/components/marks/ReactMarkView.js +30 -0
  37. package/dist/components/nodes/DefaultNodeView.d.ts +16 -0
  38. package/dist/components/nodes/DefaultNodeView.js +112 -0
  39. package/dist/components/nodes/DocNodeView.d.ts +7 -0
  40. package/dist/components/nodes/DocNodeView.js +5 -0
  41. package/dist/composition.d.ts +23 -0
  42. package/dist/composition.js +95 -0
  43. package/dist/contexts/DecorationContext.d.ts +15 -0
  44. package/dist/contexts/DecorationContext.js +35 -0
  45. package/dist/contexts/DecorationRootContext.d.ts +6 -0
  46. package/dist/contexts/DecorationRootContext.js +4 -0
  47. package/dist/contexts/EditorContext.d.ts +27 -0
  48. package/dist/contexts/EditorContext.js +87 -0
  49. package/dist/contexts/RenderStoreContext.d.ts +15 -0
  50. package/dist/contexts/RenderStoreContext.js +41 -0
  51. package/dist/contexts/editorEvents.d.ts +12 -0
  52. package/dist/contexts/editorEvents.js +92 -0
  53. package/dist/decorations/DecoIterator.d.ts +43 -0
  54. package/dist/decorations/DecoIterator.js +462 -0
  55. package/dist/decorations/ReactMarkType.d.ts +41 -0
  56. package/dist/decorations/ReactMarkType.js +47 -0
  57. package/dist/decorations/ReactShapeType.d.ts +65 -0
  58. package/dist/decorations/ReactShapeType.js +202 -0
  59. package/dist/decorations/ReactWidgetType.d.ts +29 -0
  60. package/dist/decorations/ReactWidgetType.js +67 -0
  61. package/dist/decorations/ReactWrapperType.d.ts +31 -0
  62. package/dist/decorations/ReactWrapperType.js +41 -0
  63. package/dist/decorations/iterDeco.d.ts +82 -0
  64. package/dist/decorations/iterDeco.js +406 -0
  65. package/dist/helpers/attributes.d.ts +2 -0
  66. package/dist/helpers/attributes.js +11 -0
  67. package/dist/helpers/index.d.ts +2 -0
  68. package/dist/helpers/index.js +2 -0
  69. package/dist/helpers/props.d.ts +8 -0
  70. package/dist/helpers/props.js +248 -0
  71. package/dist/helpers/refs.d.ts +6 -0
  72. package/dist/helpers/refs.js +20 -0
  73. package/dist/hooks/useEditorEffect.d.ts +10 -0
  74. package/dist/hooks/useEditorEffect.js +28 -0
  75. package/dist/hooks/useEditorEventCallback.d.ts +8 -0
  76. package/dist/hooks/useEditorEventCallback.js +17 -0
  77. package/dist/hooks/useEditorEventListener.d.ts +10 -0
  78. package/dist/hooks/useEditorEventListener.js +26 -0
  79. package/dist/hooks/useEditorLayoutEffect.d.ts +15 -0
  80. package/dist/hooks/useEditorLayoutEffect.js +34 -0
  81. package/dist/hooks/useEditorState.d.ts +6 -0
  82. package/dist/hooks/useEditorState.js +12 -0
  83. package/dist/hooks/useEditorStateSelector.d.ts +7 -0
  84. package/dist/hooks/useEditorStateSelector.js +11 -0
  85. package/dist/hooks/useIgnoreMutation.d.ts +6 -0
  86. package/dist/hooks/useIgnoreMutation.js +19 -0
  87. package/dist/hooks/useIsNodeSelected.d.ts +2 -0
  88. package/dist/hooks/useIsNodeSelected.js +5 -0
  89. package/dist/hooks/useMergedDOMRefs.d.ts +6 -0
  90. package/dist/hooks/useMergedDOMRefs.js +18 -0
  91. package/dist/hooks/useNodePos.d.ts +4 -0
  92. package/dist/hooks/useNodePos.js +7 -0
  93. package/dist/hooks/useSelectNode.d.ts +6 -0
  94. package/dist/hooks/useSelectNode.js +25 -0
  95. package/dist/hooks/useStopEvent.d.ts +8 -0
  96. package/dist/hooks/useStopEvent.js +21 -0
  97. package/dist/hooks/useWordgard.d.ts +2 -0
  98. package/dist/hooks/useWordgard.js +3 -0
  99. package/dist/index.d.ts +21 -0
  100. package/dist/index.js +21 -0
  101. package/dist/reactKeys.d.ts +39 -0
  102. package/dist/reactKeys.js +188 -0
  103. package/dist/tile.d.ts +32 -0
  104. package/dist/tile.js +1319 -0
  105. package/dist/viewRoots.d.ts +2 -0
  106. package/dist/viewRoots.js +7 -0
  107. package/dist/wordgardInternals.d.ts +211 -0
  108. package/dist/wordgardInternals.js +82 -0
  109. package/package.json +119 -4
  110. package/skills/react-wordgard/SKILL.md +525 -0
  111. package/skills/react-wordgard/references/wordgard.md +189 -0
@@ -0,0 +1,525 @@
1
+ ---
2
+ name: react-wordgard
3
+ description: Build rich text editors with Wordgard and react-wordgard in a React app. Use when creating or changing a Wordgard editor drawn by React - hosting it, drawing nodes, marks, wrappers or widgets as React components, reading the editor's state in components, positioning menus and popovers, handling the editor's events, or migrating react-prosemirror node views.
4
+ ---
5
+
6
+ # Building editors with Wordgard and react-wordgard
7
+
8
+ react-wordgard is a React view for [Wordgard](https://wordgard.net/docs/), a rich text editor.
9
+ Wordgard runs the editor (document, state, input, selection, IME composition, decorations,
10
+ commands); react-wordgard has React draw its document, inside Wordgard's own update, in the app's
11
+ React tree, so the app's contexts reach what it draws. **The API is Wordgard's**: a customisation
12
+ is a Wordgard extension or decoration, and react-wordgard adds functions that let a React
13
+ component be the decoration.
14
+
15
+ Wordgard's own API: its docs (https://wordgard.net/docs/) and its typings in
16
+ `node_modules/wordgard/dist/*.d.ts` (`wordgard/editor`, `wordgard/state`, `wordgard/doc`,
17
+ `wordgard/schema`, `wordgard/types`, `wordgard/command`). react-wordgard's: its README and
18
+ `node_modules/@stesura/react-wordgard/dist/index.d.ts`. Read them rather than guessing a signature.
19
+
20
+ **New to Wordgard, or porting ProseMirror code?** Read `references/wordgard.md`, next to this file:
21
+ Wordgard's model (plots, leaves, tags, marks on tags), changes, configuration, commands and the
22
+ editor, each against its ProseMirror counterpart, with the traps.
23
+
24
+ ## Setup
25
+
26
+ ```sh
27
+ npm install @stesura/react-wordgard wordgard react react-dom
28
+ ```
29
+
30
+ - Peers: `wordgard` `~0.6.2` (react-wordgard uses its internals: only its patches), React 18 or 19.
31
+ The app must have **one copy** of `wordgard`, or Wordgard's `instanceof` checks fail.
32
+ - TypeScript: Wordgard's typings need `skipLibCheck: true`.
33
+ - Name the editor for screen readers with `Wordgard.contentAttributes.of({ "aria-label": "…" })`
34
+ in the config (`Wordgard.label` names the wrong element in 0.6.2).
35
+
36
+ ## Hosting the editor
37
+
38
+ ```tsx
39
+ import { WordgardDoc, WordgardEditor } from "@stesura/react-wordgard";
40
+ import { basicSchema } from "wordgard/schema";
41
+
42
+ export const Editor = () => (
43
+ <WordgardEditor doc="<p>Hello</p>" config={[basicSchema()]}>
44
+ <WordgardDoc className="editor" />
45
+ </WordgardEditor>
46
+ );
47
+ ```
48
+
49
+ - `WordgardEditor` takes a `Wordgard.Spec` (`doc` as HTML, `config` as extensions, or a `state`),
50
+ **read once**: the editor is uncontrolled and owns its state. Change the document through
51
+ `wg.dispatch(…)`, the configuration through Wordgard's compartments or
52
+ `GardState.appendConfig`, never by re-rendering with new props.
53
+ - `WordgardDoc` puts the editor in the page; one per editor. Components inside `WordgardEditor`
54
+ (toolbars, menus, sidebars, next to `WordgardDoc`) can use the hooks: below. `WordgardEditor`
55
+ draws no DOM, so wrap as much of the page in it as needs the editor.
56
+ - Don't create editors with `Wordgard.create` for a React app: react-wordgard's editor is made by
57
+ `WordgardEditor`. In StrictMode (development) it creates the editor twice and drops one, which is
58
+ never put in the page.
59
+
60
+ ## Reading and changing the editor from components
61
+
62
+ ```tsx
63
+ import { Command, toggleStrong } from "wordgard/command";
64
+ import { useEditorEventCallback, useEditorStateSelector } from "@stesura/react-wordgard";
65
+
66
+ const Toolbar = () => {
67
+ const empty = useEditorStateSelector((state) => state.selection.empty);
68
+ const bold = useEditorEventCallback((wg) => Command.dispatch(wg, toggleStrong));
69
+ return (
70
+ <button
71
+ disabled={empty}
72
+ onMouseDown={(e) => {
73
+ e.preventDefault();
74
+ bold();
75
+ }}
76
+ >
77
+ Bold
78
+ </button>
79
+ );
80
+ };
81
+ ```
82
+
83
+ - `useWordgard()`: the editor (`wg`).
84
+ - `useEditorState()`, `useEditorStateSelector(selector, isEqual?)`: the state as of the editor's
85
+ last update (the one the DOM shows). Prefer the selector: the component re-renders only when its
86
+ result changes (`Object.is`, or `isEqual`).
87
+ - `useEditorEventCallback((wg, ...args) => …)`: a stable callback getting the editor, for event
88
+ handlers. Commands run through `Command.dispatch(wg, command, param?)`; plain changes through
89
+ `wg.dispatch({ changes: { from, to, insert }, selection })`.
90
+ - `useEditorEventListener(type, (wg, event) => handled)`: the editor's DOM events of `type`, before
91
+ Wordgard's handlers and keymaps; return `true` to handle the event (Wordgard then calls
92
+ `preventDefault`). Types: `keydown`, `keyup`, `keypress`, `beforeinput`, `input`, composition
93
+ events, `mousedown`, `mouseup`, `mouseover`, `mouseout`, `click`, `dblclick`, `contextmenu`,
94
+ `focus`, `blur`, `copy`, `cut`, `paste`, drag-and-drop events. Key events during an IME
95
+ composition don't come. Handling `input`, `beforeinput` or a composition event skips Wordgard's
96
+ own handling of it: leave typing to Wordgard. Touch, wheel, pointer events and `mousemove` aren't
97
+ offered (they'd make every editor's listener non-passive or busy): for those, add a
98
+ `Wordgard.domEventHandler` to `config`, or a listener on `wg.contentDOM` from `useEditorEffect`.
99
+ - `wg.state` in a callback is the latest state, dispatches not yet drawn included. To save the
100
+ document: `wg.state.doc.toJSON()`; to load it, `state.schema.docFromJSON(json)` as `doc`.
101
+
102
+ ### Reaching the editor from outside its `WordgardEditor`
103
+
104
+ The editor owns its state: there's no controlled `state` prop to lift into React, as with
105
+ react-prosemirror. The hooks throw outside `WordgardEditor`. Two ways:
106
+
107
+ 1. **Lift `WordgardEditor`** over everything that needs the editor (it draws no DOM; the document
108
+ goes where `WordgardDoc` is). Prefer this.
109
+ 2. **Share the editor through a store** where it can't wrap the component (another part of the
110
+ app, a portal): a component under `WordgardEditor` puts the editor in the store from
111
+ `useEditorEffect`, and the component outside reads it and uses `wg` directly (not the hooks).
112
+
113
+ ```tsx
114
+ import { atom, useAtomValue, useSetAtom } from "jotai";
115
+ import type { Wordgard } from "wordgard/editor";
116
+ import { basicSchema } from "wordgard/schema";
117
+ import { WordgardDoc, WordgardEditor, useEditorEffect } from "@stesura/react-wordgard";
118
+
119
+ const editorAtom = atom<Wordgard | null>(null);
120
+
121
+ const ShareEditor = () => {
122
+ const setEditor = useSetAtom(editorAtom);
123
+ useEditorEffect((wg) => {
124
+ setEditor(wg);
125
+ return () => setEditor(null);
126
+ }, []);
127
+ return null;
128
+ };
129
+
130
+ const SaveButton = () => {
131
+ const wg = useAtomValue(editorAtom);
132
+ const save = () => {
133
+ if (wg) localStorage.setItem("doc", JSON.stringify(wg.state.doc.toJSON()));
134
+ };
135
+ return (
136
+ <button onClick={save} disabled={!wg}>
137
+ Save
138
+ </button>
139
+ );
140
+ };
141
+
142
+ export const App = () => (
143
+ <>
144
+ <header>
145
+ <SaveButton />
146
+ </header>
147
+ <WordgardEditor doc="<p>Hello</p>" config={[basicSchema()]}>
148
+ <ShareEditor />
149
+ <WordgardDoc />
150
+ </WordgardEditor>
151
+ </>
152
+ );
153
+ ```
154
+
155
+ Any store works (Jotai here, Zustand, `useSyncExternalStore`). A sub-editor (a header's, a note's)
156
+ is its own `WordgardEditor`, often drawn inside a React shape of the main one; a toolbar acting on
157
+ whichever editor has focus keeps the focused one in such a store, set from
158
+ `useEditorEventListener("focus", …)` in each.
159
+
160
+ To react to changes without re-rendering (autosave, a debounced copy), add a
161
+ `Wordgard.updateListener.of((update) => …)` to `config`; `update.docChanged` and `update.state`
162
+ tell what changed. `config` is read once, so it should write to a store, not close over a
163
+ component's state.
164
+
165
+ ### Effects and layout
166
+
167
+ Wordgard drives the update and React draws inside it, so there are two effect hooks:
168
+
169
+ | Hook | Runs | May dispatch | What it sets shows | For |
170
+ | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------ | ------------------ | ---------------------------------------------------- |
171
+ | `useEditorEffect((wg) => cleanup?, deps)` | after the editor's update | yes | a frame later | focusing, follow-up dispatches, menus |
172
+ | `useEditorLayoutEffect({ read: (wg) => value, write: (value, wg) => … })` | inside every update: `read` in Wordgard's read phase, `write` in its write phase | **no** (Wordgard throws) | on the same paint | what moves with the text: margin markers, pagination |
173
+
174
+ - **Never read the editor's layout (`coordsAtPos`, `posAtCoords`) in a plain React `useEffect` or
175
+ `useLayoutEffect`**: it runs inside Wordgard's update, before the view of the new DOM exists, and
176
+ react-wordgard refuses the update. Use one of the two hooks above.
177
+ - `useEditorLayoutEffect`'s `read` runs on every update: return `undefined` when nothing changed,
178
+ which skips the write and, when no read asks for one, Wordgard's second measure. Reads run in
179
+ registration order; an engine with ordered parts (pagination) registers one effect and drives
180
+ them.
181
+ - Menus and popovers: position them with your popover library (Floating UI, Radix, Base UI) from a
182
+ virtual anchor built from `wg.coordsAtPos(pos)` in `useEditorEffect`; show or hide them from
183
+ `useEditorStateSelector`. Wordgard's own tooltips and panels also work, as plain DOM.
184
+
185
+ ## Drawing parts of the document with React
186
+
187
+ Prefer Wordgard's declarative shapes (`Elt` shapes, `Decoration.Tag.attribute`, mark shapes) for
188
+ anything that needs no code: React draws them all already. Use a React component where a part needs
189
+ state, interaction or the app's contexts. Each factory below is **called at module level**, with a
190
+ component defined there, and returns a Wordgard value you pass in `config`. `createShape`,
191
+ `createWrapper` and `createMarkShape` return the same value for the same component, so composing
192
+ the config again (per editor) is safe; a component created inside a render is new each time. The
193
+ components need no memoizing: React draws a node again only when it changed, or from the
194
+ component's own state, contexts and hooks.
195
+
196
+ | Need | Factory | Passed to |
197
+ | ------------------------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------- |
198
+ | A node, with content or an atom (callout, image, mention) | `createShape(component)` | `Decoration.Tag.shape(Type, shape)`; `shape.of(element, { atom? })` for `Point.shape` |
199
+ | An element around a node or part of its shape | `createWrapper(component, { element })` | `Decoration.Tag.wrapper(Type, wrapper, { target? })`, `Decoration.Point.wrapper` |
200
+ | A mark's element (link, comment highlight) | `createMarkShape(component, { element })` | `Decoration.Mark.shape(MarkType, shape)` |
201
+ | A widget at a position or beside a node (placeholder, badge) | `createWidgetType({ component, … })` | `Decoration.Point.widget(MyType.of(value), { side })`, `Decoration.Tag.widget` |
202
+
203
+ ```tsx
204
+ import { Decoration } from "wordgard/editor";
205
+ import { basicSchema, blockquote, image } from "wordgard/schema";
206
+ import { Blockquote, Image, Link } from "wordgard/types";
207
+ import { DecorationRoot, createMarkShape, createShape } from "@stesura/react-wordgard";
208
+
209
+ const CalloutShape = createShape(function Callout({ nodeProps: { contentDOMRef }, children }) {
210
+ return (
211
+ <DecorationRoot>
212
+ <span aria-hidden="true">!</span>
213
+ <div ref={contentDOMRef}>{children}</div>
214
+ </DecorationRoot>
215
+ );
216
+ });
217
+
218
+ const LinkMark = createMarkShape(
219
+ function LinkMark({ markProps: { mark, contentDOMRef }, children }) {
220
+ return (
221
+ <DecorationRoot href={String(mark.value)}>
222
+ <span ref={contentDOMRef}>{children}</span>
223
+ </DecorationRoot>
224
+ );
225
+ },
226
+ { element: "a" }
227
+ );
228
+
229
+ // An atom: no content, everything it draws is chrome.
230
+ const PictureShape = createShape(function Picture() {
231
+ return <DecorationRoot />;
232
+ });
233
+
234
+ export const config = [
235
+ basicSchema(),
236
+ blockquote(),
237
+ image(),
238
+ Decoration.Tag.shape(Blockquote, CalloutShape),
239
+ Decoration.Mark.shape(Link, LinkMark),
240
+ Decoration.Tag.shape(Image, PictureShape),
241
+ ];
242
+ ```
243
+
244
+ An image's `src` is its type's own attribute and its `alt` the `ImageAlt` mark's: `DecorationRoot`
245
+ puts both on the `<img>`. Check the parameter types of the node and mark types you use in
246
+ `wordgard/types`.
247
+
248
+ ### The component's contract
249
+
250
+ - **The root is the element the decoration says, and stays that element.** For a shape, that's
251
+ the node type's own outer element, per node (`h1` to `h6` for a heading); for `.of`,
252
+ `createWrapper` and `createMarkShape`, their `element` (`div` for wrappers, `span` for marks, by
253
+ default). Wordgard applies decorations to the shape before React draws (`target` selectors
254
+ match its tag), so the tag can't come from the component. Re-render freely, but don't swap the
255
+ root for another element.
256
+ - **Draw it with `<DecorationRoot>`**, once, at the component's root. It renders that tag with
257
+ Wordgard's attributes (the node type's own, the selection classes `wg-selected-node` and
258
+ `wg-selected-cell`, tag, point and mark attributes, `contenteditable` on an atom) and the root's
259
+ ref. Its own props win, except `className`, joined with Wordgard's, and `style`, merged (its
260
+ keys winning); its `ref` gets the root too. It works on React 18 and 19 without `forwardRef`.
261
+ Widgets use it too (see "React widgets").
262
+ - **Or draw the root by hand**, for more control over the decoration's attributes (drop or change
263
+ one, type the root as its element): the element the decoration says, `{...props}` (Wordgard's
264
+ attributes) and `ref` (`forwardRef` on React 18), never both ways at once. Join your classes with
265
+ `className` (Wordgard's, the selection's among them), and spread your style over `style`
266
+ (Wordgard's, as an object). On React 19, `ref` is a prop:
267
+
268
+ ```tsx
269
+ const ImageShape = createShape<HTMLImageElement>(function ImageShape({
270
+ nodeProps,
271
+ children,
272
+ className,
273
+ style,
274
+ ref,
275
+ ...props
276
+ }) {
277
+ return (
278
+ <img
279
+ {...props}
280
+ ref={ref}
281
+ className={["picture", className].join(" ").trim()}
282
+ style={{ ...style, outline: "1px solid" }}
283
+ />
284
+ );
285
+ });
286
+ ```
287
+
288
+ On React 18, with `forwardRef`:
289
+
290
+ ```tsx
291
+ const ImageShape = createShape(
292
+ forwardRef<HTMLImageElement, ShapeComponentProps<HTMLImageElement>>(function ImageShape(
293
+ { nodeProps, children, className, style, ...props },
294
+ ref
295
+ ) {
296
+ return (
297
+ <img
298
+ {...props}
299
+ ref={ref}
300
+ className={["picture", className].join(" ").trim()}
301
+ style={{ ...style, outline: "1px solid" }}
302
+ />
303
+ );
304
+ })
305
+ );
306
+ ```
307
+
308
+ - **Only the outer element comes from the node type.** The elements inside a structure shape (a
309
+ figure's `img`) are the component's to draw, from `nodeProps.node`; attributes aimed at them
310
+ arrive on the root (see "Should I use a shape, a wrapper or both?").
311
+ - **Content goes where `contentDOMRef` points**, else in the root. `children` is the content (a
312
+ wrapper's `children` is what it wraps; no `contentDOMRef` there).
313
+ - **An atom** (a leaf, or a plot whose shape has no hole) has no content: `children` is empty,
314
+ and a `contentDOMRef` throws. `.of(element, { atom: true })` has no hole: a leaf needs it (for
315
+ `Decoration.Point.shape` before a leaf), and it draws a plot as an atom.
316
+ - Everything else the component draws is **chrome**: made uneditable, never read as document
317
+ content, free to change from the component's state, a context or a store.
318
+ - In a textblock (a paragraph-like node) or a mark, the content element holds only the content.
319
+ Chrome beside a textblock's text (line numbers) is a React widget at its `"start"` or `"end"`:
320
+ `Decoration.Tag.widget(Paragraph, "start", LineNumber.of(null))` (see "React widgets").
321
+ - In a node holding blocks, chrome may sit before or after the content in its element, not
322
+ between the content's nodes.
323
+ - Moving `{children}` under another element remounts the content (a selection or IME
324
+ composition in it ends).
325
+ - Node parameters: `nodeProps.node.tag.param` for a node with content, `node.param` for a leaf;
326
+ `markProps.mark` is the document's mark (`mark.value`).
327
+ - `createWrapper` can't wrap a node a native widget draws (draw it with a React shape or widget), nor text.
328
+ - **Don't write attributes on document elements by hand.** On a React decoration's root they
329
+ survive only until React sets that attribute (a `classList.add` is lost when the selection
330
+ changes `className`); on elements Wordgard draws they're undone at the next update. Put the
331
+ state in the component (`data-resizing={resizing}`), or in a decoration from a state field.
332
+
333
+ ### React widgets
334
+
335
+ ```tsx
336
+ import { Decoration } from "wordgard/editor";
337
+ import { Paragraph } from "wordgard/types";
338
+ import {
339
+ DecorationRoot,
340
+ type WidgetComponentProps,
341
+ createWidgetType,
342
+ } from "@stesura/react-wordgard";
343
+
344
+ // The root is a DecorationRoot, a <span> unless the spec gives an `element`.
345
+ const Badge = createWidgetType({
346
+ component: function Badge({ value }: WidgetComponentProps<string>) {
347
+ return (
348
+ <DecorationRoot className="badge" aria-hidden="true">
349
+ {value}
350
+ </DecorationRoot>
351
+ );
352
+ },
353
+ });
354
+
355
+ export const badges = [
356
+ Decoration.Tag.widget(Paragraph, "start", Badge.of("¶")),
357
+ Decoration.Point.source.of(() =>
358
+ Decoration.Point.set([[Decoration.Point.widget(Badge.of("!")), 1]])
359
+ ),
360
+ ];
361
+ ```
362
+
363
+ - `createWidgetType({ component, element?, eq?, editable?, inFlow?, propagateEvent?, connect?,
364
+ disconnect? })` returns a real `Widget.Type`: its widgets are `Type.of(value)`. The component
365
+ gets `value`.
366
+ - **Draw its root with `<DecorationRoot>`**: `element` (`span` by default), the ref, and the
367
+ node's attributes for a widget drawing a node. Or by hand: any element, with the `ref` the
368
+ component gets (`forwardRef` on React 18), which it **must** put on its root.
369
+ - Widgets are uneditable unless their spec sets `editable`, as in Wordgard. A React widget
370
+ re-renders freely while it keeps its root element.
371
+ - A widget drawing a node (`Decoration.Tag.shape(Type, (tag) => W.of(tag.param))`):
372
+ `DecorationRoot` puts the node's attributes on its root; a root drawn by hand gets them as
373
+ props, to spread. A React shape is the usual way to draw a node.
374
+ - A widget's component may read a context a React shape around it provides (line numbers beside a
375
+ paragraph's text, from the paragraph's shape).
376
+
377
+ ### Should I use a shape, a wrapper or both?
378
+
379
+ A shape draws the node; a wrapper adds React around it. Unlike a ProseMirror node view, which picks
380
+ all its DOM, a node's outer element comes from its type's shape in the schema: Wordgard applies
381
+ marks and decorations to that shape (`target`, `preferTarget`) before React draws. A React shape
382
+ that draws more than its root gets the attributes aimed inside it on its root (a figure drawing its
383
+ own `img` gets `ImageAlt` and `ImageSize` on the `<figure>`). So the shape's root matches the node
384
+ type's element (`DecorationRoot`, or a root drawn by hand), and chrome around it is a wrapper:
385
+
386
+ ```tsx
387
+ import { Decoration } from "wordgard/editor";
388
+ import { Image } from "wordgard/types";
389
+ import { DecorationRoot, createShape, createWrapper, useStopEvent } from "@stesura/react-wordgard";
390
+
391
+ // The shape draws the <img>, which keeps its src, alt and width.
392
+ const ImageShape = createShape(function ImageShape() {
393
+ return <DecorationRoot className="image" />;
394
+ });
395
+
396
+ // The wrapper draws the resize handle around it.
397
+ const Resizer = createWrapper(
398
+ function Resizer({ children }) {
399
+ useStopEvent(() => true);
400
+ return (
401
+ <DecorationRoot className="resizer">
402
+ {children}
403
+ <span className="resize-handle" aria-hidden="true" />
404
+ </DecorationRoot>
405
+ );
406
+ },
407
+ { element: "span" } // an image is inline
408
+ );
409
+
410
+ export const imageConfig = [
411
+ Decoration.Tag.shape(Image, ImageShape),
412
+ Decoration.Tag.wrapper(Image, Resizer),
413
+ ];
414
+ ```
415
+
416
+ - A wrapper can also aim at an element inside the type's shape:
417
+ `Decoration.Tag.wrapper(Figure, createWrapper(Frame), { target: "img" })`.
418
+ - A shape and wrappers on the same type apply to all its nodes. A table: a React shape drawing
419
+ `table > tbody > {children}`, plus a wrapper for a scroll container around it.
420
+ - **One `Decoration.Tag.shape` per node type** draws it: where several features draw the same type
421
+ (react-prosemirror apps merging node views), fold their components into one in the app's code
422
+ and register that, or give each feature a wrapper or a tag widget of its own.
423
+ - Porting a react-prosemirror node view that draws chrome around the node's own element: keep the
424
+ type's shape (or a React shape for the element), and move the chrome into a wrapper.
425
+
426
+ ### Changing a node without remounting it
427
+
428
+ A component keeps its state while its node changes in place or moves within its parent; it
429
+ remounts, with everything inside, when the node is replaced whole, changes type or moves to another
430
+ parent.
431
+
432
+ - **Data an action changes in place** (size, alignment, a callout's kind, a table's widths): a mark
433
+ on the tag, `Mark.Type.define(name, { target: NodeType, … })`, read with
434
+ `node.tag.mark(Type)`, changed with `wg.dispatch({ changes: { from: pos, add: Type.of(v) } })`
435
+ or `{ from: pos, remove: Type.of(v) }`. It merges in collaboration and inverts in history.
436
+ - **What identifies the node** (an image's `src`, a heading's level): its parameter, changed by
437
+ retagging the opening token, `{ from: pos, to: pos + 1, insert: [Heading.of(2)] }`.
438
+ - **Never** replace the whole node (`{ from, to, insert: [node] }`) to change its data: that
439
+ remounts the component.
440
+ - ProseMirror's `setNodeAttribute` / `setNodeMarkup` port to one of the first two.
441
+ - **Moving a node**: delete it and insert the same node object elsewhere in the same parent, in one
442
+ transaction (`changes: [{ from, to: from + node.length }, { from: target, insert: [node] }]`).
443
+ A copy (a new object, or one inserted while the original stays) mounts anew. React moves the
444
+ DOM, so an `<iframe>` in a moved node reloads. Wordgard's drag-and-drop rebuilds what it drops
445
+ from HTML: those moves remount.
446
+
447
+ ### Hooks inside React decorations
448
+
449
+ - `useNodePos()`: a function returning the position of what the decoration draws (call it in
450
+ handlers and effects, not during render).
451
+ - `useIsNodeSelected()`, `useSelectNode(select, deselect?)`: the node selection.
452
+ - `useStopEvent((wg, event) => stop)`: asked for events in the decoration's chrome (inputs,
453
+ buttons in a title bar), on its own elements (its root and the elements around its content
454
+ element: an atom's root, a frame's padding; not a root that is itself the content element) and
455
+ in React widgets. Never for events in the content, unlike react-prosemirror. For an event it
456
+ stops, Wordgard's handlers and keymaps are skipped and the browser's default goes ahead.
457
+ - `useIgnoreMutation((wg, record) => ignore)`: Wordgard doesn't read the DOM mutations it claims
458
+ (typing aside: Wordgard takes it from `beforeinput`).
459
+ - `useMergedDOMRefs(...refs)`: one ref setting several (the forwarded `ref` and `contentDOMRef`
460
+ on one element, a measuring ref).
461
+
462
+ ## Accessibility
463
+
464
+ - The editable element has `role="textbox"` and `aria-multiline`; give it a name (Setup).
465
+ - Chrome is read inline with the document's text: `aria-hidden` on decorative parts, a name on
466
+ every control, controls focusable and wrapped in `useStopEvent` so Wordgard leaves their keys.
467
+
468
+ ## Migrating react-prosemirror node views
469
+
470
+ There are no node views: a node is drawn by decorations (its shape and wrappers), and a node view
471
+ component becomes one with `createShape` or `createWrapper`.
472
+
473
+ | react-prosemirror | react-wordgard |
474
+ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
475
+ | `<ProseMirror state dispatchTransaction>` | `<WordgardEditor doc config>`, uncontrolled; changes via `wg.dispatch` |
476
+ | `<ProseMirrorDoc />` | `<WordgardDoc />` |
477
+ | `nodeViews` | `Decoration.Tag.shape(Type, createShape(component))` in `config`, atoms included |
478
+ | `markViews` | `createMarkShape` with `Decoration.Mark.shape` |
479
+ | `NodeViewComponentProps` | `ShapeComponentProps` (same `nodeProps`, `contentDOMRef`, `children`, `ref`) |
480
+ | `node.attrs` | `node.tag.param` for identity, `node.tag.mark(Type)` for data changed in place |
481
+ | `view` in callbacks | `wg` |
482
+ | `ProseMirror-selectednode`, `selectedCell` | `wg-selected-node`, `wg-selected-cell` |
483
+ | `useNodePos()` returning a number | `useNodePos()` returning a function |
484
+ | `useEditorEffect` for same-paint layout | `useEditorLayoutEffect` |
485
+ | `useStopEvent`, `useIgnoreMutation`, `useSelectNode`, `useEditorEventListener` | the same names, the editor first in callbacks |
486
+ | `useIsNodeSelected`, `useMergedDOMRefs` | the same |
487
+ | `innerDecorations`, `useIsComposingIn`, `useIsEditorStatic` | none |
488
+
489
+ ProseMirror's model maps to Wordgard's in its migration guide:
490
+ https://wordgard.net/docs/prosemirror/
491
+
492
+ ## When something fails
493
+
494
+ - "…must draw a DecorationRoot, or forward its ref to its root element": the component's root is
495
+ neither a `DecorationRoot` nor given `ref` (on React 18, a hand-written root needs `forwardRef`).
496
+ - "…drew <x> where its shape says <y>": draw the element the shape says (the node type's, or
497
+ `element`), or pass the drawn one to the shape's `of`.
498
+ - "React's DOM has extra nodes" or "…chrome is between its content's nodes": chrome sits in a
499
+ content element where it can't (see the contract).
500
+ - "A React widget's component must forward its ref to its root element": a widget drawing its
501
+ root by hand needs `forwardRef` (React 18) and `ref` on its root; or use `DecorationRoot`.
502
+ - "DecorationRoot is the root of a React shape, wrapper, mark or widget: use it in their
503
+ component": a `DecorationRoot` outside those components (in a toolbar, say).
504
+ - "A React wrapper's component must draw its children": a wrapper drew nothing where it wraps.
505
+ - "A React shape drawing an atom has no content: drop its contentDOMRef": the node type draws an
506
+ atom (a leaf, a plot without a hole), so its React shape has no content element.
507
+ - "A React shape can't wrap: use createWrapper", "A React wrapper isn't a shape: use createShape":
508
+ a factory's value went to the other decoration.
509
+ - "…is called outside a React shape, wrapper, mark or widget": a decoration hook (`useNodePos`,
510
+ `useStopEvent`, `useIgnoreMutation`, `useSelectNode`, `useIsNodeSelected`) used elsewhere.
511
+ "Used outside a Wordgard editor": an editor hook outside `WordgardEditor`.
512
+ - "This React shape or wrapper isn't drawn" (or widget, or mark): `useNodePos()`'s function called
513
+ during render or after unmount; call it in handlers and effects.
514
+ - "Leaf nodes shapes shouldn't have a content hole": a React shape's `.of(element)` given to a
515
+ leaf; use `.of(element, { atom: true })`.
516
+ - "A React wrapper can't wrap a native widget: draw … with a React widget", "A React wrapper
517
+ can't wrap text": a wrapper around a node a native widget draws, or around text.
518
+ - "This editor is already rendered by another WordgardDoc": one `WordgardDoc` per editor.
519
+ - "react-wordgard can't render part of …", "…can't render the shape of … yet": a shape or
520
+ decoration react-wordgard doesn't draw yet; the message names the node type.
521
+ - "ReactWordgard can't update while React renders or commits": layout was read from a plain React
522
+ effect; use `useEditorEffect` or `useEditorLayoutEffect`.
523
+ - "Cannot dispatch new updates during the editor flush phase": a dispatch from
524
+ `useEditorLayoutEffect` or a render; dispatch from an event handler or `useEditorEffect`.
525
+ - Two copies of `wordgard` (`npm ls wordgard`) break `instanceof` checks in odd places.