@stesura/react-wordgard 0.1.0 → 0.2.1
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 +12 -0
- package/README.md +699 -237
- package/dist/ReactWordgard.js +2 -4
- package/dist/components/ChildNodeViews.js +2 -4
- package/dist/components/DecorationRoot.d.ts +6 -0
- package/dist/components/DecorationRoot.js +7 -4
- package/dist/components/ErrorBoundary.js +3 -5
- package/dist/components/SeparatorHackView.js +1 -2
- package/dist/components/ShapeView.js +2 -4
- package/dist/components/TextNodeView.js +2 -3
- package/dist/components/TrailingHackView.js +2 -3
- package/dist/components/WidgetView.js +2 -5
- package/dist/components/WordgardDoc.d.ts +5 -0
- package/dist/components/WordgardDoc.js +8 -6
- package/dist/components/WordgardEditor.d.ts +8 -0
- package/dist/components/WordgardEditor.js +10 -6
- package/dist/components/WordgardScope.d.ts +14 -0
- package/dist/components/WordgardScope.js +14 -0
- package/dist/components/WrapperView.js +1 -4
- package/dist/components/marks/MarkView.js +3 -6
- package/dist/components/marks/ReactMarkView.js +2 -4
- package/dist/components/nodes/DefaultNodeView.js +3 -7
- package/dist/components/nodes/DocNodeView.js +2 -3
- package/dist/composition.js +4 -6
- package/dist/contexts/DecorationContext.js +2 -5
- package/dist/contexts/DecorationRootContext.js +2 -4
- package/dist/contexts/EditorContext.d.ts +1 -0
- package/dist/contexts/EditorContext.js +3 -3
- package/dist/contexts/RenderStoreContext.js +3 -5
- package/dist/contexts/editorEvents.js +3 -6
- package/dist/decorations/ReactMarkType.d.ts +11 -0
- package/dist/decorations/ReactMarkType.js +6 -7
- package/dist/decorations/ReactShapeType.d.ts +16 -0
- package/dist/decorations/ReactShapeType.js +9 -11
- package/dist/decorations/ReactWidgetType.d.ts +10 -0
- package/dist/decorations/ReactWidgetType.js +6 -8
- package/dist/decorations/ReactWrapperType.d.ts +9 -0
- package/dist/decorations/ReactWrapperType.js +3 -6
- package/dist/decorations/iterDeco.js +3 -6
- package/dist/hooks/useEditorEffect.d.ts +7 -0
- package/dist/hooks/useEditorEffect.js +11 -7
- package/dist/hooks/useEditorEventCallback.d.ts +6 -0
- package/dist/hooks/useEditorEventCallback.js +7 -3
- package/dist/hooks/useEditorEventListener.d.ts +7 -0
- package/dist/hooks/useEditorEventListener.js +11 -11
- package/dist/hooks/useEditorLayoutEffect.d.ts +9 -0
- package/dist/hooks/useEditorLayoutEffect.js +13 -17
- package/dist/hooks/useEditorState.d.ts +5 -0
- package/dist/hooks/useEditorState.js +6 -4
- package/dist/hooks/useEditorStateSelector.d.ts +5 -0
- package/dist/hooks/useEditorStateSelector.js +5 -0
- package/dist/hooks/useIgnoreMutation.d.ts +5 -1
- package/dist/hooks/useIgnoreMutation.js +11 -10
- package/dist/hooks/useIsNodeSelected.d.ts +1 -0
- package/dist/hooks/useIsNodeSelected.js +3 -4
- package/dist/hooks/useMergedDOMRefs.d.ts +4 -0
- package/dist/hooks/useMergedDOMRefs.js +5 -4
- package/dist/hooks/useNodePos.d.ts +3 -0
- package/dist/hooks/useNodePos.js +5 -6
- package/dist/hooks/useSelectNode.d.ts +4 -0
- package/dist/hooks/useSelectNode.js +5 -4
- package/dist/hooks/useStopEvent.d.ts +6 -0
- package/dist/hooks/useStopEvent.js +8 -7
- package/dist/hooks/useWordgard.d.ts +1 -0
- package/dist/hooks/useWordgard.js +1 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/tile.js +3 -8
- package/dist/viewRoots.js +2 -3
- package/dist/wordgardInternals.js +2 -5
- package/package.json +1 -1
- package/skills/react-wordgard/SKILL.md +138 -10
|
@@ -51,7 +51,8 @@ export const Editor = () => (
|
|
|
51
51
|
`wg.dispatch(…)`, the configuration through Wordgard's compartments or
|
|
52
52
|
`GardState.appendConfig`, never by re-rendering with new props.
|
|
53
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.
|
|
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.
|
|
55
56
|
- Don't create editors with `Wordgard.create` for a React app: react-wordgard's editor is made by
|
|
56
57
|
`WordgardEditor`. In StrictMode (development) it creates the editor twice and drops one, which is
|
|
57
58
|
never put in the page.
|
|
@@ -67,6 +68,7 @@ const Toolbar = () => {
|
|
|
67
68
|
const bold = useEditorEventCallback((wg) => Command.dispatch(wg, toggleStrong));
|
|
68
69
|
return (
|
|
69
70
|
<button
|
|
71
|
+
disabled={empty}
|
|
70
72
|
onMouseDown={(e) => {
|
|
71
73
|
e.preventDefault();
|
|
72
74
|
bold();
|
|
@@ -91,7 +93,74 @@ const Toolbar = () => {
|
|
|
91
93
|
events, `mousedown`, `mouseup`, `mouseover`, `mouseout`, `click`, `dblclick`, `contextmenu`,
|
|
92
94
|
`focus`, `blur`, `copy`, `cut`, `paste`, drag-and-drop events. Key events during an IME
|
|
93
95
|
composition don't come. Handling `input`, `beforeinput` or a composition event skips Wordgard's
|
|
94
|
-
own handling of it: leave typing to Wordgard.
|
|
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.
|
|
95
164
|
|
|
96
165
|
### Effects and layout
|
|
97
166
|
|
|
@@ -194,7 +263,29 @@ puts both on the `<img>`. Check the parameter types of the node and mark types y
|
|
|
194
263
|
one, type the root as its element): the element the decoration says, `{...props}` (Wordgard's
|
|
195
264
|
attributes) and `ref` (`forwardRef` on React 18), never both ways at once. Join your classes with
|
|
196
265
|
`className` (Wordgard's, the selection's among them), and spread your style over `style`
|
|
197
|
-
(Wordgard's, as an object):
|
|
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`:
|
|
198
289
|
|
|
199
290
|
```tsx
|
|
200
291
|
const ImageShape = createShape(
|
|
@@ -216,7 +307,7 @@ puts both on the `<img>`. Check the parameter types of the node and mark types y
|
|
|
216
307
|
|
|
217
308
|
- **Only the outer element comes from the node type.** The elements inside a structure shape (a
|
|
218
309
|
figure's `img`) are the component's to draw, from `nodeProps.node`; attributes aimed at them
|
|
219
|
-
arrive on the root (see "
|
|
310
|
+
arrive on the root (see "Should I use a shape, a wrapper or both?").
|
|
220
311
|
- **Content goes where `contentDOMRef` points**, else in the root. `children` is the content (a
|
|
221
312
|
wrapper's `children` is what it wraps; no `contentDOMRef` there).
|
|
222
313
|
- **An atom** (a leaf, or a plot whose shape has no hole) has no content: `children` is empty,
|
|
@@ -283,13 +374,47 @@ disconnect? })` returns a real `Widget.Type`: its widgets are `Type.of(value)`.
|
|
|
283
374
|
- A widget's component may read a context a React shape around it provides (line numbers beside a
|
|
284
375
|
paragraph's text, from the paragraph's shape).
|
|
285
376
|
|
|
286
|
-
###
|
|
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
|
+
```
|
|
287
415
|
|
|
288
|
-
-
|
|
289
|
-
`
|
|
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}`.
|
|
416
|
+
- A wrapper can also aim at an element inside the type's shape:
|
|
417
|
+
`Decoration.Tag.wrapper(Figure, createWrapper(Frame), { target: "img" })`.
|
|
293
418
|
- A shape and wrappers on the same type apply to all its nodes. A table: a React shape drawing
|
|
294
419
|
`table > tbody > {children}`, plus a wrapper for a scroll container around it.
|
|
295
420
|
- **One `Decoration.Tag.shape` per node type** draws it: where several features draw the same type
|
|
@@ -342,6 +467,9 @@ parent.
|
|
|
342
467
|
|
|
343
468
|
## Migrating react-prosemirror node views
|
|
344
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
|
+
|
|
345
473
|
| react-prosemirror | react-wordgard |
|
|
346
474
|
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
|
|
347
475
|
| `<ProseMirror state dispatchTransaction>` | `<WordgardEditor doc config>`, uncontrolled; changes via `wg.dispatch` |
|