@stesura/react-wordgard 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/LICENSE.txt +21 -0
- package/README.md +450 -2
- package/THIRD_PARTY_LICENSES.txt +93 -0
- package/dist/ReactWordgard.d.ts +13 -0
- package/dist/ReactWordgard.js +96 -0
- package/dist/changes.d.ts +14 -0
- package/dist/changes.js +163 -0
- package/dist/components/ChildNodeViews.d.ts +8 -0
- package/dist/components/ChildNodeViews.js +38 -0
- package/dist/components/DecorationRoot.d.ts +7 -0
- package/dist/components/DecorationRoot.js +28 -0
- package/dist/components/ErrorBoundary.d.ts +18 -0
- package/dist/components/ErrorBoundary.js +16 -0
- package/dist/components/SeparatorHackView.d.ts +2 -0
- package/dist/components/SeparatorHackView.js +5 -0
- package/dist/components/ShapeView.d.ts +12 -0
- package/dist/components/ShapeView.js +26 -0
- package/dist/components/TextNodeView.d.ts +5 -0
- package/dist/components/TextNodeView.js +5 -0
- package/dist/components/TrailingHackView.d.ts +2 -0
- package/dist/components/TrailingHackView.js +5 -0
- package/dist/components/WidgetView.d.ts +10 -0
- package/dist/components/WidgetView.js +35 -0
- package/dist/components/WordgardDoc.d.ts +4 -0
- package/dist/components/WordgardDoc.js +34 -0
- package/dist/components/WordgardEditor.d.ts +7 -0
- package/dist/components/WordgardEditor.js +13 -0
- package/dist/components/WrapperView.d.ts +12 -0
- package/dist/components/WrapperView.js +26 -0
- package/dist/components/marks/MarkView.d.ts +11 -0
- package/dist/components/marks/MarkView.js +13 -0
- package/dist/components/marks/ReactMarkView.d.ts +10 -0
- package/dist/components/marks/ReactMarkView.js +32 -0
- package/dist/components/nodes/DefaultNodeView.d.ts +16 -0
- package/dist/components/nodes/DefaultNodeView.js +116 -0
- package/dist/components/nodes/DocNodeView.d.ts +7 -0
- package/dist/components/nodes/DocNodeView.js +6 -0
- package/dist/composition.d.ts +23 -0
- package/dist/composition.js +97 -0
- package/dist/contexts/DecorationContext.d.ts +15 -0
- package/dist/contexts/DecorationContext.js +38 -0
- package/dist/contexts/DecorationRootContext.d.ts +6 -0
- package/dist/contexts/DecorationRootContext.js +6 -0
- package/dist/contexts/EditorContext.d.ts +26 -0
- package/dist/contexts/EditorContext.js +87 -0
- package/dist/contexts/RenderStoreContext.d.ts +15 -0
- package/dist/contexts/RenderStoreContext.js +43 -0
- package/dist/contexts/editorEvents.d.ts +12 -0
- package/dist/contexts/editorEvents.js +95 -0
- package/dist/decorations/DecoIterator.d.ts +43 -0
- package/dist/decorations/DecoIterator.js +462 -0
- package/dist/decorations/ReactMarkType.d.ts +30 -0
- package/dist/decorations/ReactMarkType.js +48 -0
- package/dist/decorations/ReactShapeType.d.ts +49 -0
- package/dist/decorations/ReactShapeType.js +204 -0
- package/dist/decorations/ReactWidgetType.d.ts +19 -0
- package/dist/decorations/ReactWidgetType.js +69 -0
- package/dist/decorations/ReactWrapperType.d.ts +22 -0
- package/dist/decorations/ReactWrapperType.js +44 -0
- package/dist/decorations/iterDeco.d.ts +82 -0
- package/dist/decorations/iterDeco.js +409 -0
- package/dist/helpers/attributes.d.ts +2 -0
- package/dist/helpers/attributes.js +11 -0
- package/dist/helpers/index.d.ts +2 -0
- package/dist/helpers/index.js +2 -0
- package/dist/helpers/props.d.ts +8 -0
- package/dist/helpers/props.js +248 -0
- package/dist/helpers/refs.d.ts +6 -0
- package/dist/helpers/refs.js +20 -0
- package/dist/hooks/useEditorEffect.d.ts +3 -0
- package/dist/hooks/useEditorEffect.js +24 -0
- package/dist/hooks/useEditorEventCallback.d.ts +2 -0
- package/dist/hooks/useEditorEventCallback.js +13 -0
- package/dist/hooks/useEditorEventListener.d.ts +3 -0
- package/dist/hooks/useEditorEventListener.js +26 -0
- package/dist/hooks/useEditorLayoutEffect.d.ts +6 -0
- package/dist/hooks/useEditorLayoutEffect.js +38 -0
- package/dist/hooks/useEditorState.d.ts +1 -0
- package/dist/hooks/useEditorState.js +10 -0
- package/dist/hooks/useEditorStateSelector.d.ts +2 -0
- package/dist/hooks/useEditorStateSelector.js +6 -0
- package/dist/hooks/useIgnoreMutation.d.ts +2 -0
- package/dist/hooks/useIgnoreMutation.js +18 -0
- package/dist/hooks/useIsNodeSelected.d.ts +1 -0
- package/dist/hooks/useIsNodeSelected.js +6 -0
- package/dist/hooks/useMergedDOMRefs.d.ts +2 -0
- package/dist/hooks/useMergedDOMRefs.js +17 -0
- package/dist/hooks/useNodePos.d.ts +1 -0
- package/dist/hooks/useNodePos.js +8 -0
- package/dist/hooks/useSelectNode.d.ts +2 -0
- package/dist/hooks/useSelectNode.js +24 -0
- package/dist/hooks/useStopEvent.d.ts +2 -0
- package/dist/hooks/useStopEvent.js +20 -0
- package/dist/hooks/useWordgard.d.ts +1 -0
- package/dist/hooks/useWordgard.js +4 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/reactKeys.d.ts +39 -0
- package/dist/reactKeys.js +188 -0
- package/dist/tile.d.ts +32 -0
- package/dist/tile.js +1324 -0
- package/dist/viewRoots.d.ts +2 -0
- package/dist/viewRoots.js +8 -0
- package/dist/wordgardInternals.d.ts +211 -0
- package/dist/wordgardInternals.js +85 -0
- package/package.json +119 -4
- package/skills/react-wordgard/SKILL.md +397 -0
- package/skills/react-wordgard/references/wordgard.md +189 -0
|
@@ -0,0 +1,397 @@
|
|
|
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.
|
|
55
|
+
- Don't create editors with `Wordgard.create` for a React app: react-wordgard's editor is made by
|
|
56
|
+
`WordgardEditor`. In StrictMode (development) it creates the editor twice and drops one, which is
|
|
57
|
+
never put in the page.
|
|
58
|
+
|
|
59
|
+
## Reading and changing the editor from components
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { Command, toggleStrong } from "wordgard/command";
|
|
63
|
+
import { useEditorEventCallback, useEditorStateSelector } from "@stesura/react-wordgard";
|
|
64
|
+
|
|
65
|
+
const Toolbar = () => {
|
|
66
|
+
const empty = useEditorStateSelector((state) => state.selection.empty);
|
|
67
|
+
const bold = useEditorEventCallback((wg) => Command.dispatch(wg, toggleStrong));
|
|
68
|
+
return (
|
|
69
|
+
<button
|
|
70
|
+
onMouseDown={(e) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
bold();
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
Bold
|
|
76
|
+
</button>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- `useWordgard()`: the editor (`wg`).
|
|
82
|
+
- `useEditorState()`, `useEditorStateSelector(selector, isEqual?)`: the state as of the editor's
|
|
83
|
+
last update (the one the DOM shows). Prefer the selector: the component re-renders only when its
|
|
84
|
+
result changes (`Object.is`, or `isEqual`).
|
|
85
|
+
- `useEditorEventCallback((wg, ...args) => …)`: a stable callback getting the editor, for event
|
|
86
|
+
handlers. Commands run through `Command.dispatch(wg, command, param?)`; plain changes through
|
|
87
|
+
`wg.dispatch({ changes: { from, to, insert }, selection })`.
|
|
88
|
+
- `useEditorEventListener(type, (wg, event) => handled)`: the editor's DOM events of `type`, before
|
|
89
|
+
Wordgard's handlers and keymaps; return `true` to handle the event (Wordgard then calls
|
|
90
|
+
`preventDefault`). Types: `keydown`, `keyup`, `keypress`, `beforeinput`, `input`, composition
|
|
91
|
+
events, `mousedown`, `mouseup`, `mouseover`, `mouseout`, `click`, `dblclick`, `contextmenu`,
|
|
92
|
+
`focus`, `blur`, `copy`, `cut`, `paste`, drag-and-drop events. Key events during an IME
|
|
93
|
+
composition don't come. Handling `input`, `beforeinput` or a composition event skips Wordgard's
|
|
94
|
+
own handling of it: leave typing to Wordgard.
|
|
95
|
+
|
|
96
|
+
### Effects and layout
|
|
97
|
+
|
|
98
|
+
Wordgard drives the update and React draws inside it, so there are two effect hooks:
|
|
99
|
+
|
|
100
|
+
| Hook | Runs | May dispatch | What it sets shows | For |
|
|
101
|
+
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------ | ------------------ | ---------------------------------------------------- |
|
|
102
|
+
| `useEditorEffect((wg) => cleanup?, deps)` | after the editor's update | yes | a frame later | focusing, follow-up dispatches, menus |
|
|
103
|
+
| `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 |
|
|
104
|
+
|
|
105
|
+
- **Never read the editor's layout (`coordsAtPos`, `posAtCoords`) in a plain React `useEffect` or
|
|
106
|
+
`useLayoutEffect`**: it runs inside Wordgard's update, before the view of the new DOM exists, and
|
|
107
|
+
react-wordgard refuses the update. Use one of the two hooks above.
|
|
108
|
+
- `useEditorLayoutEffect`'s `read` runs on every update: return `undefined` when nothing changed,
|
|
109
|
+
which skips the write and, when no read asks for one, Wordgard's second measure. Reads run in
|
|
110
|
+
registration order; an engine with ordered parts (pagination) registers one effect and drives
|
|
111
|
+
them.
|
|
112
|
+
- Menus and popovers: position them with your popover library (Floating UI, Radix, Base UI) from a
|
|
113
|
+
virtual anchor built from `wg.coordsAtPos(pos)` in `useEditorEffect`; show or hide them from
|
|
114
|
+
`useEditorStateSelector`. Wordgard's own tooltips and panels also work, as plain DOM.
|
|
115
|
+
|
|
116
|
+
## Drawing parts of the document with React
|
|
117
|
+
|
|
118
|
+
Prefer Wordgard's declarative shapes (`Elt` shapes, `Decoration.Tag.attribute`, mark shapes) for
|
|
119
|
+
anything that needs no code: React draws them all already. Use a React component where a part needs
|
|
120
|
+
state, interaction or the app's contexts. Each factory below is **called at module level**, with a
|
|
121
|
+
component defined there, and returns a Wordgard value you pass in `config`. `createShape`,
|
|
122
|
+
`createWrapper` and `createMarkShape` return the same value for the same component, so composing
|
|
123
|
+
the config again (per editor) is safe; a component created inside a render is new each time. The
|
|
124
|
+
components need no memoizing: React draws a node again only when it changed, or from the
|
|
125
|
+
component's own state, contexts and hooks.
|
|
126
|
+
|
|
127
|
+
| Need | Factory | Passed to |
|
|
128
|
+
| ------------------------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
129
|
+
| A node, with content or an atom (callout, image, mention) | `createShape(component)` | `Decoration.Tag.shape(Type, shape)`; `shape.of(element, { atom? })` for `Point.shape` |
|
|
130
|
+
| An element around a node or part of its shape | `createWrapper(component, { element })` | `Decoration.Tag.wrapper(Type, wrapper, { target? })`, `Decoration.Point.wrapper` |
|
|
131
|
+
| A mark's element (link, comment highlight) | `createMarkShape(component, { element })` | `Decoration.Mark.shape(MarkType, shape)` |
|
|
132
|
+
| A widget at a position or beside a node (placeholder, badge) | `createWidgetType({ component, … })` | `Decoration.Point.widget(MyType.of(value), { side })`, `Decoration.Tag.widget` |
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { Decoration } from "wordgard/editor";
|
|
136
|
+
import { basicSchema, blockquote, image } from "wordgard/schema";
|
|
137
|
+
import { Blockquote, Image, Link } from "wordgard/types";
|
|
138
|
+
import { DecorationRoot, createMarkShape, createShape } from "@stesura/react-wordgard";
|
|
139
|
+
|
|
140
|
+
const CalloutShape = createShape(function Callout({ nodeProps: { contentDOMRef }, children }) {
|
|
141
|
+
return (
|
|
142
|
+
<DecorationRoot>
|
|
143
|
+
<span aria-hidden="true">!</span>
|
|
144
|
+
<div ref={contentDOMRef}>{children}</div>
|
|
145
|
+
</DecorationRoot>
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const LinkMark = createMarkShape(
|
|
150
|
+
function LinkMark({ markProps: { mark, contentDOMRef }, children }) {
|
|
151
|
+
return (
|
|
152
|
+
<DecorationRoot href={String(mark.value)}>
|
|
153
|
+
<span ref={contentDOMRef}>{children}</span>
|
|
154
|
+
</DecorationRoot>
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
{ element: "a" }
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// An atom: no content, everything it draws is chrome.
|
|
161
|
+
const PictureShape = createShape(function Picture() {
|
|
162
|
+
return <DecorationRoot />;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export const config = [
|
|
166
|
+
basicSchema(),
|
|
167
|
+
blockquote(),
|
|
168
|
+
image(),
|
|
169
|
+
Decoration.Tag.shape(Blockquote, CalloutShape),
|
|
170
|
+
Decoration.Mark.shape(Link, LinkMark),
|
|
171
|
+
Decoration.Tag.shape(Image, PictureShape),
|
|
172
|
+
];
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
An image's `src` is its type's own attribute and its `alt` the `ImageAlt` mark's: `DecorationRoot`
|
|
176
|
+
puts both on the `<img>`. Check the parameter types of the node and mark types you use in
|
|
177
|
+
`wordgard/types`.
|
|
178
|
+
|
|
179
|
+
### The component's contract
|
|
180
|
+
|
|
181
|
+
- **The root is the element the decoration says, and stays that element.** For a shape, that's
|
|
182
|
+
the node type's own outer element, per node (`h1` to `h6` for a heading); for `.of`,
|
|
183
|
+
`createWrapper` and `createMarkShape`, their `element` (`div` for wrappers, `span` for marks, by
|
|
184
|
+
default). Wordgard applies decorations to the shape before React draws (`target` selectors
|
|
185
|
+
match its tag), so the tag can't come from the component. Re-render freely, but don't swap the
|
|
186
|
+
root for another element.
|
|
187
|
+
- **Draw it with `<DecorationRoot>`**, once, at the component's root. It renders that tag with
|
|
188
|
+
Wordgard's attributes (the node type's own, the selection classes `wg-selected-node` and
|
|
189
|
+
`wg-selected-cell`, tag, point and mark attributes, `contenteditable` on an atom) and the root's
|
|
190
|
+
ref. Its own props win, except `className`, joined with Wordgard's, and `style`, merged (its
|
|
191
|
+
keys winning); its `ref` gets the root too. It works on React 18 and 19 without `forwardRef`.
|
|
192
|
+
Widgets use it too (see "React widgets").
|
|
193
|
+
- **Or draw the root by hand**, for more control over the decoration's attributes (drop or change
|
|
194
|
+
one, type the root as its element): the element the decoration says, `{...props}` (Wordgard's
|
|
195
|
+
attributes) and `ref` (`forwardRef` on React 18), never both ways at once. Join your classes with
|
|
196
|
+
`className` (Wordgard's, the selection's among them), and spread your style over `style`
|
|
197
|
+
(Wordgard's, as an object):
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
const ImageShape = createShape(
|
|
201
|
+
forwardRef<HTMLImageElement, ShapeComponentProps<HTMLImageElement>>(function ImageShape(
|
|
202
|
+
{ nodeProps, children, className, style, ...props },
|
|
203
|
+
ref
|
|
204
|
+
) {
|
|
205
|
+
return (
|
|
206
|
+
<img
|
|
207
|
+
{...props}
|
|
208
|
+
ref={ref}
|
|
209
|
+
className={["picture", className].join(" ").trim()}
|
|
210
|
+
style={{ ...style, outline: "1px solid" }}
|
|
211
|
+
/>
|
|
212
|
+
);
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- **Only the outer element comes from the node type.** The elements inside a structure shape (a
|
|
218
|
+
figure's `img`) are the component's to draw, from `nodeProps.node`; attributes aimed at them
|
|
219
|
+
arrive on the root (see "A shape, a wrapper, or both").
|
|
220
|
+
- **Content goes where `contentDOMRef` points**, else in the root. `children` is the content (a
|
|
221
|
+
wrapper's `children` is what it wraps; no `contentDOMRef` there).
|
|
222
|
+
- **An atom** (a leaf, or a plot whose shape has no hole) has no content: `children` is empty,
|
|
223
|
+
and a `contentDOMRef` throws. `.of(element, { atom: true })` has no hole: a leaf needs it (for
|
|
224
|
+
`Decoration.Point.shape` before a leaf), and it draws a plot as an atom.
|
|
225
|
+
- Everything else the component draws is **chrome**: made uneditable, never read as document
|
|
226
|
+
content, free to change from the component's state, a context or a store.
|
|
227
|
+
- In a textblock (a paragraph-like node) or a mark, the content element holds only the content.
|
|
228
|
+
Chrome beside a textblock's text (line numbers) is a React widget at its `"start"` or `"end"`:
|
|
229
|
+
`Decoration.Tag.widget(Paragraph, "start", LineNumber.of(null))` (see "React widgets").
|
|
230
|
+
- In a node holding blocks, chrome may sit before or after the content in its element, not
|
|
231
|
+
between the content's nodes.
|
|
232
|
+
- Moving `{children}` under another element remounts the content (a selection or IME
|
|
233
|
+
composition in it ends).
|
|
234
|
+
- Node parameters: `nodeProps.node.tag.param` for a node with content, `node.param` for a leaf;
|
|
235
|
+
`markProps.mark` is the document's mark (`mark.value`).
|
|
236
|
+
- `createWrapper` can't wrap a node a native widget draws (draw it with a React shape or widget), nor text.
|
|
237
|
+
- **Don't write attributes on document elements by hand.** On a React decoration's root they
|
|
238
|
+
survive only until React sets that attribute (a `classList.add` is lost when the selection
|
|
239
|
+
changes `className`); on elements Wordgard draws they're undone at the next update. Put the
|
|
240
|
+
state in the component (`data-resizing={resizing}`), or in a decoration from a state field.
|
|
241
|
+
|
|
242
|
+
### React widgets
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
import { Decoration } from "wordgard/editor";
|
|
246
|
+
import { Paragraph } from "wordgard/types";
|
|
247
|
+
import {
|
|
248
|
+
DecorationRoot,
|
|
249
|
+
type WidgetComponentProps,
|
|
250
|
+
createWidgetType,
|
|
251
|
+
} from "@stesura/react-wordgard";
|
|
252
|
+
|
|
253
|
+
// The root is a DecorationRoot, a <span> unless the spec gives an `element`.
|
|
254
|
+
const Badge = createWidgetType({
|
|
255
|
+
component: function Badge({ value }: WidgetComponentProps<string>) {
|
|
256
|
+
return (
|
|
257
|
+
<DecorationRoot className="badge" aria-hidden="true">
|
|
258
|
+
{value}
|
|
259
|
+
</DecorationRoot>
|
|
260
|
+
);
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
export const badges = [
|
|
265
|
+
Decoration.Tag.widget(Paragraph, "start", Badge.of("¶")),
|
|
266
|
+
Decoration.Point.source.of(() =>
|
|
267
|
+
Decoration.Point.set([[Decoration.Point.widget(Badge.of("!")), 1]])
|
|
268
|
+
),
|
|
269
|
+
];
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- `createWidgetType({ component, element?, eq?, editable?, inFlow?, propagateEvent?, connect?,
|
|
273
|
+
disconnect? })` returns a real `Widget.Type`: its widgets are `Type.of(value)`. The component
|
|
274
|
+
gets `value`.
|
|
275
|
+
- **Draw its root with `<DecorationRoot>`**: `element` (`span` by default), the ref, and the
|
|
276
|
+
node's attributes for a widget drawing a node. Or by hand: any element, with the `ref` the
|
|
277
|
+
component gets (`forwardRef` on React 18), which it **must** put on its root.
|
|
278
|
+
- Widgets are uneditable unless their spec sets `editable`, as in Wordgard. A React widget
|
|
279
|
+
re-renders freely while it keeps its root element.
|
|
280
|
+
- A widget drawing a node (`Decoration.Tag.shape(Type, (tag) => W.of(tag.param))`):
|
|
281
|
+
`DecorationRoot` puts the node's attributes on its root; a root drawn by hand gets them as
|
|
282
|
+
props, to spread. A React shape is the usual way to draw a node.
|
|
283
|
+
- A widget's component may read a context a React shape around it provides (line numbers beside a
|
|
284
|
+
paragraph's text, from the paragraph's shape).
|
|
285
|
+
|
|
286
|
+
### A shape, a wrapper, or both
|
|
287
|
+
|
|
288
|
+
- The node type's shape decides where attributes go (`target`, `preferTarget`: `ImageAlt` and
|
|
289
|
+
`ImageSize` land on a figure's `img`). Don't replace a shape just to add chrome.
|
|
290
|
+
- React chrome around a node, or an element in its shape, is a **wrapper**:
|
|
291
|
+
`Decoration.Tag.wrapper(Figure, createWrapper(Frame), { target: "img" })`. The `img` keeps its
|
|
292
|
+
`src`, `alt` and width; `Frame` draws a resize handle beside `{children}`.
|
|
293
|
+
- A shape and wrappers on the same type apply to all its nodes. A table: a React shape drawing
|
|
294
|
+
`table > tbody > {children}`, plus a wrapper for a scroll container around it.
|
|
295
|
+
- **One `Decoration.Tag.shape` per node type** draws it: where several features draw the same type
|
|
296
|
+
(react-prosemirror apps merging node views), fold their components into one in the app's code
|
|
297
|
+
and register that, or give each feature a wrapper or a tag widget of its own.
|
|
298
|
+
- Porting a react-prosemirror node view that draws chrome around the node's own element: keep the
|
|
299
|
+
type's shape (or a React shape for the element), and move the chrome into a wrapper.
|
|
300
|
+
|
|
301
|
+
### Changing a node without remounting it
|
|
302
|
+
|
|
303
|
+
A component keeps its state while its node changes in place or moves within its parent; it
|
|
304
|
+
remounts, with everything inside, when the node is replaced whole, changes type or moves to another
|
|
305
|
+
parent.
|
|
306
|
+
|
|
307
|
+
- **Data an action changes in place** (size, alignment, a callout's kind, a table's widths): a mark
|
|
308
|
+
on the tag, `Mark.Type.define(name, { target: NodeType, … })`, read with
|
|
309
|
+
`node.tag.mark(Type)`, changed with `wg.dispatch({ changes: { from: pos, add: Type.of(v) } })`
|
|
310
|
+
or `{ from: pos, remove: Type.of(v) }`. It merges in collaboration and inverts in history.
|
|
311
|
+
- **What identifies the node** (an image's `src`, a heading's level): its parameter, changed by
|
|
312
|
+
retagging the opening token, `{ from: pos, to: pos + 1, insert: [Heading.of(2)] }`.
|
|
313
|
+
- **Never** replace the whole node (`{ from, to, insert: [node] }`) to change its data: that
|
|
314
|
+
remounts the component.
|
|
315
|
+
- ProseMirror's `setNodeAttribute` / `setNodeMarkup` port to one of the first two.
|
|
316
|
+
- **Moving a node**: delete it and insert the same node object elsewhere in the same parent, in one
|
|
317
|
+
transaction (`changes: [{ from, to: from + node.length }, { from: target, insert: [node] }]`).
|
|
318
|
+
A copy (a new object, or one inserted while the original stays) mounts anew. React moves the
|
|
319
|
+
DOM, so an `<iframe>` in a moved node reloads. Wordgard's drag-and-drop rebuilds what it drops
|
|
320
|
+
from HTML: those moves remount.
|
|
321
|
+
|
|
322
|
+
### Hooks inside React decorations
|
|
323
|
+
|
|
324
|
+
- `useNodePos()`: a function returning the position of what the decoration draws (call it in
|
|
325
|
+
handlers and effects, not during render).
|
|
326
|
+
- `useIsNodeSelected()`, `useSelectNode(select, deselect?)`: the node selection.
|
|
327
|
+
- `useStopEvent((wg, event) => stop)`: asked for events in the decoration's chrome (inputs,
|
|
328
|
+
buttons in a title bar), on its own elements (its root and the elements around its content
|
|
329
|
+
element: an atom's root, a frame's padding; not a root that is itself the content element) and
|
|
330
|
+
in React widgets. Never for events in the content, unlike react-prosemirror. For an event it
|
|
331
|
+
stops, Wordgard's handlers and keymaps are skipped and the browser's default goes ahead.
|
|
332
|
+
- `useIgnoreMutation((wg, record) => ignore)`: Wordgard doesn't read the DOM mutations it claims
|
|
333
|
+
(typing aside: Wordgard takes it from `beforeinput`).
|
|
334
|
+
- `useMergedDOMRefs(...refs)`: one ref setting several (the forwarded `ref` and `contentDOMRef`
|
|
335
|
+
on one element, a measuring ref).
|
|
336
|
+
|
|
337
|
+
## Accessibility
|
|
338
|
+
|
|
339
|
+
- The editable element has `role="textbox"` and `aria-multiline`; give it a name (Setup).
|
|
340
|
+
- Chrome is read inline with the document's text: `aria-hidden` on decorative parts, a name on
|
|
341
|
+
every control, controls focusable and wrapped in `useStopEvent` so Wordgard leaves their keys.
|
|
342
|
+
|
|
343
|
+
## Migrating react-prosemirror node views
|
|
344
|
+
|
|
345
|
+
| react-prosemirror | react-wordgard |
|
|
346
|
+
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
|
|
347
|
+
| `<ProseMirror state dispatchTransaction>` | `<WordgardEditor doc config>`, uncontrolled; changes via `wg.dispatch` |
|
|
348
|
+
| `<ProseMirrorDoc />` | `<WordgardDoc />` |
|
|
349
|
+
| `nodeViews` | `Decoration.Tag.shape(Type, createShape(component))` in `config`, atoms included |
|
|
350
|
+
| `markViews` | `createMarkShape` with `Decoration.Mark.shape` |
|
|
351
|
+
| `NodeViewComponentProps` | `ShapeComponentProps` (same `nodeProps`, `contentDOMRef`, `children`, `ref`) |
|
|
352
|
+
| `node.attrs` | `node.tag.param` for identity, `node.tag.mark(Type)` for data changed in place |
|
|
353
|
+
| `view` in callbacks | `wg` |
|
|
354
|
+
| `ProseMirror-selectednode`, `selectedCell` | `wg-selected-node`, `wg-selected-cell` |
|
|
355
|
+
| `useNodePos()` returning a number | `useNodePos()` returning a function |
|
|
356
|
+
| `useEditorEffect` for same-paint layout | `useEditorLayoutEffect` |
|
|
357
|
+
| `useStopEvent`, `useIgnoreMutation`, `useSelectNode`, `useEditorEventListener` | the same names, the editor first in callbacks |
|
|
358
|
+
| `useIsNodeSelected`, `useMergedDOMRefs` | the same |
|
|
359
|
+
| `innerDecorations`, `useIsComposingIn`, `useIsEditorStatic` | none |
|
|
360
|
+
|
|
361
|
+
ProseMirror's model maps to Wordgard's in its migration guide:
|
|
362
|
+
https://wordgard.net/docs/prosemirror/
|
|
363
|
+
|
|
364
|
+
## When something fails
|
|
365
|
+
|
|
366
|
+
- "…must draw a DecorationRoot, or forward its ref to its root element": the component's root is
|
|
367
|
+
neither a `DecorationRoot` nor given `ref` (on React 18, a hand-written root needs `forwardRef`).
|
|
368
|
+
- "…drew <x> where its shape says <y>": draw the element the shape says (the node type's, or
|
|
369
|
+
`element`), or pass the drawn one to the shape's `of`.
|
|
370
|
+
- "React's DOM has extra nodes" or "…chrome is between its content's nodes": chrome sits in a
|
|
371
|
+
content element where it can't (see the contract).
|
|
372
|
+
- "A React widget's component must forward its ref to its root element": a widget drawing its
|
|
373
|
+
root by hand needs `forwardRef` (React 18) and `ref` on its root; or use `DecorationRoot`.
|
|
374
|
+
- "DecorationRoot is the root of a React shape, wrapper, mark or widget: use it in their
|
|
375
|
+
component": a `DecorationRoot` outside those components (in a toolbar, say).
|
|
376
|
+
- "A React wrapper's component must draw its children": a wrapper drew nothing where it wraps.
|
|
377
|
+
- "A React shape drawing an atom has no content: drop its contentDOMRef": the node type draws an
|
|
378
|
+
atom (a leaf, a plot without a hole), so its React shape has no content element.
|
|
379
|
+
- "A React shape can't wrap: use createWrapper", "A React wrapper isn't a shape: use createShape":
|
|
380
|
+
a factory's value went to the other decoration.
|
|
381
|
+
- "…is called outside a React shape, wrapper, mark or widget": a decoration hook (`useNodePos`,
|
|
382
|
+
`useStopEvent`, `useIgnoreMutation`, `useSelectNode`, `useIsNodeSelected`) used elsewhere.
|
|
383
|
+
"Used outside a Wordgard editor": an editor hook outside `WordgardEditor`.
|
|
384
|
+
- "This React shape or wrapper isn't drawn" (or widget, or mark): `useNodePos()`'s function called
|
|
385
|
+
during render or after unmount; call it in handlers and effects.
|
|
386
|
+
- "Leaf nodes shapes shouldn't have a content hole": a React shape's `.of(element)` given to a
|
|
387
|
+
leaf; use `.of(element, { atom: true })`.
|
|
388
|
+
- "A React wrapper can't wrap a native widget: draw … with a React widget", "A React wrapper
|
|
389
|
+
can't wrap text": a wrapper around a node a native widget draws, or around text.
|
|
390
|
+
- "This editor is already rendered by another WordgardDoc": one `WordgardDoc` per editor.
|
|
391
|
+
- "react-wordgard can't render part of …", "…can't render the shape of … yet": a shape or
|
|
392
|
+
decoration react-wordgard doesn't draw yet; the message names the node type.
|
|
393
|
+
- "ReactWordgard can't update while React renders or commits": layout was read from a plain React
|
|
394
|
+
effect; use `useEditorEffect` or `useEditorLayoutEffect`.
|
|
395
|
+
- "Cannot dispatch new updates during the editor flush phase": a dispatch from
|
|
396
|
+
`useEditorLayoutEffect` or a render; dispatch from an event handler or `useEditorEffect`.
|
|
397
|
+
- Two copies of `wordgard` (`npm ls wordgard`) break `instanceof` checks in odd places.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Wordgard, for ProseMirror developers
|
|
2
|
+
|
|
3
|
+
Read this when you're new to Wordgard or porting ProseMirror code (react-prosemirror apps
|
|
4
|
+
included). It covers the model and the traps; for exact signatures, read the typings in
|
|
5
|
+
`node_modules/wordgard/dist/*.d.ts`, whose doc comments are Wordgard's reference manual. Wordgard's
|
|
6
|
+
own guides: https://wordgard.net/docs/guide/ and, for ProseMirror users,
|
|
7
|
+
https://wordgard.net/docs/prosemirror/. Written for Wordgard 0.6.
|
|
8
|
+
|
|
9
|
+
## Modules
|
|
10
|
+
|
|
11
|
+
One package, `wordgard`, in submodules:
|
|
12
|
+
|
|
13
|
+
| Wordgard | Replaces |
|
|
14
|
+
| ------------------ | ----------------------------------------------------------------------------------- |
|
|
15
|
+
| `wordgard/doc` | `prosemirror-model`, `prosemirror-transform` |
|
|
16
|
+
| `wordgard/state` | `prosemirror-state` |
|
|
17
|
+
| `wordgard/editor` | `prosemirror-view`, `prosemirror-keymap`, `prosemirror-menu`, input rules |
|
|
18
|
+
| `wordgard/command` | `prosemirror-commands` |
|
|
19
|
+
| `wordgard/history` | `prosemirror-history` |
|
|
20
|
+
| `wordgard/collab` | `prosemirror-collab` |
|
|
21
|
+
| `wordgard/types` | `prosemirror-schema-basic`, `prosemirror-schema-list`: the node and mark types |
|
|
22
|
+
| `wordgard/schema` | extensions adding those types with their commands, keys and menus (`basicSchema()`) |
|
|
23
|
+
|
|
24
|
+
## The document
|
|
25
|
+
|
|
26
|
+
- **A node is a plot or a leaf.** A `Plot` has content (`plot.content`, an array); a `Leaf` has
|
|
27
|
+
none. Text is a leaf (`Leaf.Text`, its parameter the string), and so is a line break.
|
|
28
|
+
- **A tag is a node's type, parameter and marks.** A leaf is its own tag; a plot has one
|
|
29
|
+
(`plot.tag`). Read a parameter as `plot.tag.param` or `leaf.param`, a mark as
|
|
30
|
+
`node.tag.mark(Type)` (the value, or `undefined`).
|
|
31
|
+
- **One parameter, no `attrs`.** What identifies a node (a heading's level, an image's `src`) is
|
|
32
|
+
its parameter. Anything else (alignment, size, a callout's kind) is a mark on its tag: a mark
|
|
33
|
+
type with a `target`, added or removed without touching the node.
|
|
34
|
+
- **Marks live on tags**, a plot's as a leaf's, and a mark type says which nodes it may go on
|
|
35
|
+
(`target`), not the parent which content it allows.
|
|
36
|
+
- **No content expressions.** A plot type says what it holds (`blockContent: Node.Group.Content`,
|
|
37
|
+
or `inlineContent: true`); finer rules go above the document (corrections, `wordgard/state`).
|
|
38
|
+
- **Positions count as in ProseMirror**: a character or leaf is 1, a plot 2 plus its content.
|
|
39
|
+
`node.length` is ProseMirror's `nodeSize`; the document's excludes its own tokens.
|
|
40
|
+
- **Walking it:** `doc.iterate(from, to, (node, pos, parent, index) => …)` for `descendants` and
|
|
41
|
+
`nodesBetween` (return `false` to skip a node's content), `doc.nodeAt(pos)`, `doc.plotAt(pos)`,
|
|
42
|
+
`doc.resolve(pos)` (a `Pos`, ProseMirror's `ResolvedPos`), `plot.textContent({ blockSeparator })`.
|
|
43
|
+
- **Types are objects, not names.** Import them (`Heading`, `Paragraph` from `wordgard/types`) and
|
|
44
|
+
compare with `node.type == Heading` or `node.is(Heading)`; names are for JSON and debugging.
|
|
45
|
+
|
|
46
|
+
### Defining types
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { Leaf, Mark, Node, Plot } from "wordgard/doc";
|
|
50
|
+
import { GardState } from "wordgard/state";
|
|
51
|
+
|
|
52
|
+
// A plot without a parameter: `define` gives its tag.
|
|
53
|
+
export const Callout = Plot.define("Callout", {
|
|
54
|
+
blockContent: Node.Group.Content,
|
|
55
|
+
group: Node.Group.Content,
|
|
56
|
+
shape: { element: "aside", selector: "aside.callout", attributes: { class: "callout" } },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Data an editing action changes is a mark on the tag, not a parameter.
|
|
60
|
+
export const CalloutKind = Mark.Type.define<string>("CalloutKind", {
|
|
61
|
+
target: Callout,
|
|
62
|
+
validate: "string",
|
|
63
|
+
shape: { attribute: "data-kind", value: 0 },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// A leaf with a parameter: `Type.define`, and nodes are `Mention.of(param)`.
|
|
67
|
+
export const Mention = Leaf.Type.define<string>("Mention", {
|
|
68
|
+
inline: true,
|
|
69
|
+
validate: "string",
|
|
70
|
+
shape: {
|
|
71
|
+
element: "span",
|
|
72
|
+
selector: "span[data-mention]",
|
|
73
|
+
attributes: (id) => ({ "data-mention": id }),
|
|
74
|
+
readElement: (elt) => elt.getAttribute("data-mention") ?? "",
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// The schema is assembled from the configuration.
|
|
79
|
+
export const schemaPart = GardState.schemaElement.of([Callout, CalloutKind, Mention]);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `Plot.define` / `Leaf.define` / `Mark.define` for types without a parameter;
|
|
83
|
+
`Plot.Type.define<P>` / `Leaf.Type.define<P>` / `Mark.Type.define<P>` with one (`Heading.of(2)`).
|
|
84
|
+
- `shape` is both `toDOM` and `parseDOM`: an element (with `selector` and `readElement` for
|
|
85
|
+
parsing) or, for a mark, an attribute merged onto its node's element. A `structure` shape draws
|
|
86
|
+
inner elements; `0` marks where the content goes.
|
|
87
|
+
|
|
88
|
+
## Changes
|
|
89
|
+
|
|
90
|
+
ProseMirror's steps are gone. A transaction is a spec, every position in it counts in the
|
|
91
|
+
document before it, and its changes are one `ChangeSet`, which also maps positions.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import type { Node } from "wordgard/doc";
|
|
95
|
+
import type { Wordgard } from "wordgard/editor";
|
|
96
|
+
import { Heading, Strong } from "wordgard/types";
|
|
97
|
+
|
|
98
|
+
export const examples = (wg: Wordgard, pos: number, node: Node, target: number) => {
|
|
99
|
+
// Insert, mark a range, delete: all in the original document's positions.
|
|
100
|
+
wg.dispatch({
|
|
101
|
+
changes: [
|
|
102
|
+
{ from: 1, insert: [node] },
|
|
103
|
+
{ from: 8, to: 10, add: Strong },
|
|
104
|
+
{ from: 20, to: 30 },
|
|
105
|
+
],
|
|
106
|
+
selection: { anchor: 2 },
|
|
107
|
+
});
|
|
108
|
+
// A parameter change: replace the node's opening token by one of the same type.
|
|
109
|
+
wg.dispatch({ changes: { from: pos, to: pos + 1, insert: [Heading.of(2)] } });
|
|
110
|
+
// A move: delete the node and insert the same object elsewhere.
|
|
111
|
+
wg.dispatch({
|
|
112
|
+
changes: [
|
|
113
|
+
{ from: pos, to: pos + node.length },
|
|
114
|
+
{ from: target, insert: [node] },
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| ProseMirror | Wordgard |
|
|
121
|
+
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
122
|
+
| `tr.insert`, `tr.replaceWith`, `tr.delete` | `changes: { from, to?, insert? }`; `insert` is an array of tokens |
|
|
123
|
+
| `tr.addMark`, `tr.removeMark` | `{ from, to, add: Mark }`, `{ from, to, remove: Mark }` |
|
|
124
|
+
| `tr.setNodeMarkup`, `tr.setNodeAttribute` | a retag (`{ from: pos, to: pos + 1, insert: [Type.of(param)] }`) or a mark on the tag (`{ from: pos, add: Type.of(value) }`) |
|
|
125
|
+
| `tr.mapping.map(pos)` | `tr.changes.mapPos(pos, assoc?)` |
|
|
126
|
+
| `tr.setMeta` | `Transaction.Annotation` (about the transaction), `Transaction.Effect` (a change to state, mapped through changes) |
|
|
127
|
+
| `tr.steps`, `tr.docChanged` | `tr.changes`, `tr.docChanged` |
|
|
128
|
+
| `state.apply(tr)` | `tr.state` (`state.update(spec)` builds the transaction) |
|
|
129
|
+
| `Slice` with open depths | `Slice`, a flat run of tokens: nodes, `Plot.Tag` (an opening token), `Plot.End` |
|
|
130
|
+
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
- **Extensions, not plugins.** The configuration is a tree of extensions (CodeMirror 6's system):
|
|
134
|
+
facets (`GardState.Facet`), state fields, precedence (`GardState.prec.highest(…)`), and
|
|
135
|
+
compartments (`new GardState.Compartment()`, `.of(…)`, `.reconfigure(…)` in an effect) or
|
|
136
|
+
`GardState.appendConfig` to change it later.
|
|
137
|
+
- **Plugin state** is a field: `GardState.Field.define({ create, update: (value, tr) => …, provide })`.
|
|
138
|
+
- **Plugin views** are `Wordgard.Plugin.define((wg) => ({ update(update) {}, docUpdate(wg) {} }))`.
|
|
139
|
+
- **Props** are facets: `Wordgard.domEventHandler(type, (event, wg) => handled)`,
|
|
140
|
+
`Wordgard.contentAttributes.of({…})`, `Wordgard.updateListener.of((update) => …)`, decorations.
|
|
141
|
+
- **Keymaps:** `KeyBinding.of({ key: "Mod-b", run: command })`, each binding an extension.
|
|
142
|
+
- **Commands** are `(wg, param) => boolean | Transaction.Spec`, run with
|
|
143
|
+
`Command.dispatch(wg, command, param?)`. `Command.Pure` takes `{ state }` and returns a spec or
|
|
144
|
+
`false`; `Command.bind(command, param)` fixes the parameter for menus and keys;
|
|
145
|
+
`Command.handler(command, other)` lets an extension change what a command does.
|
|
146
|
+
- **History:** `history()` from `wordgard/history`, with the `undo` and `redo` commands.
|
|
147
|
+
- **HTML and JSON:** `parse(schema, element)` and `serialize(doc).toHTML()` from `wordgard/doc`;
|
|
148
|
+
`node.toJSON()` and `state.schema.nodeFromJSON(json)`. A spec's `doc` may be an HTML string.
|
|
149
|
+
|
|
150
|
+
## Selection
|
|
151
|
+
|
|
152
|
+
- `state.selection` (`GardSelection`) holds raw positions (`anchor`, `head`, `from`, `to`,
|
|
153
|
+
`empty`); `state.sel` the resolved ones (`sel.head.parent.node`).
|
|
154
|
+
- Set it in a spec, `selection: { anchor, head? }`, or with `GardSelection.node(pos, node)` for a
|
|
155
|
+
node selection.
|
|
156
|
+
|
|
157
|
+
## The editor
|
|
158
|
+
|
|
159
|
+
- **`Wordgard`** is the view. It updates the DOM asynchronously, on the next frame (its _flush_):
|
|
160
|
+
read layout with `coordsAtPos` and the like, which flush first, or after `wg.flush()`. A
|
|
161
|
+
dispatch during the flush throws.
|
|
162
|
+
- **It draws its own cursor** and handles selection keys itself; bind commands to them.
|
|
163
|
+
- **Decorations** come as point decorations (`Decoration.Point`: widgets, attributes, a node's
|
|
164
|
+
shape or wrapper, at a position), range decorations (`Decoration.Range`: wrappers and attributes
|
|
165
|
+
over a range) and tag decorations (`Decoration.Tag`: for every node of a type, in place of node
|
|
166
|
+
views). Point and range sources are functions of the state called on every update: keep large
|
|
167
|
+
sets in a state field and map them (`set.map(tr.changes)`). `decoratePlots` decorates each plot
|
|
168
|
+
of a type from its own content.
|
|
169
|
+
- **No node views.** A node's DOM is its shape; interactive parts are widgets in it, beside its
|
|
170
|
+
content. With react-wordgard, a React component can be the shape: see `SKILL.md`.
|
|
171
|
+
|
|
172
|
+
## Traps
|
|
173
|
+
|
|
174
|
+
- **Don't replace a node to change its data.** `{ from, to, insert: [newNode] }` makes a new node:
|
|
175
|
+
views remount, and history and collaboration see a replacement. Retag or change a mark.
|
|
176
|
+
- **Don't carry ProseMirror's `attrs` over as one object parameter.** Split identity (the
|
|
177
|
+
parameter) from data that changes (marks with a `target`).
|
|
178
|
+
- **Positions in one spec all count in the original document**: don't adjust the later ones for
|
|
179
|
+
the earlier changes, as with ProseMirror's step-by-step mapping.
|
|
180
|
+
- **Refer to types by object.** `node.type.name == "heading"` breaks when the name differs and
|
|
181
|
+
can't tell two types with one name apart.
|
|
182
|
+
- **Don't read layout right after `dispatch`** without letting the editor flush; and never
|
|
183
|
+
dispatch from inside an update (a plugin's `update`, a field's `update`).
|
|
184
|
+
- **Point and range sources run on every update**: computing a big set there costs every
|
|
185
|
+
keystroke.
|
|
186
|
+
- **Drag-and-drop inside the editor rebuilds what it moves from HTML** (Wordgard 0.6): moved nodes
|
|
187
|
+
are new objects, and whatever their HTML doesn't carry is lost.
|
|
188
|
+
- **`Wordgard.label` names the outer element in 0.6.2**, not the textbox: use
|
|
189
|
+
`Wordgard.contentAttributes.of({ "aria-label": … })`.
|