@handlewithcare/react-prosemirror 3.1.1 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/cjs/components/ChildNodeViews.js +7 -5
- package/dist/cjs/hooks/useIsComposingIn.js +25 -0
- package/dist/cjs/index.js +4 -0
- package/dist/esm/components/ChildNodeViews.js +7 -5
- package/dist/esm/hooks/useIsComposingIn.js +17 -0
- package/dist/esm/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/components/__tests__/ProseMirror.memory-leak.test.d.ts +6 -0
- package/dist/types/hooks/useIsComposingIn.d.ts +4 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/tiptap/README.md +341 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { ProseMirror } from "./components/ProseMirror.js";
|
|
2
2
|
export { ProseMirrorDoc } from "./components/ProseMirrorDoc.js";
|
|
3
3
|
export { reorderSiblings } from "./commands/reorderSiblings.js";
|
|
4
|
+
export { useIsComposingIn } from "./hooks/useIsComposingIn.js";
|
|
4
5
|
export { useEditorEffect } from "./hooks/useEditorEffect.js";
|
|
5
6
|
export { useEditorEventCallback } from "./hooks/useEditorEventCallback.js";
|
|
6
7
|
export { useEditorEventListener } from "./hooks/useEditorEventListener.js";
|
package/package.json
CHANGED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# Tiptap Integration
|
|
2
|
+
|
|
3
|
+
A way to build a rich text editor with Tiptap while still safely rendering your
|
|
4
|
+
ProseMirror editor with React.
|
|
5
|
+
|
|
6
|
+
<!-- toc -->
|
|
7
|
+
|
|
8
|
+
- [API](#api)
|
|
9
|
+
- [`TiptapEditorView`](#tiptapeditorview)
|
|
10
|
+
- [`TiptapEditorContent`](#tiptapeditorcontent)
|
|
11
|
+
- [`tiptapNodeView`](#tiptapnodeview)
|
|
12
|
+
- [`useTiptapEditor`](#usetiptapeditor)
|
|
13
|
+
- [`useTiptapEditorEffect`](#usetiptapeditoreffect)
|
|
14
|
+
- [`useTiptapEditorEventCallback`](#usetiptapeditoreventcallback)
|
|
15
|
+
- [`useIsInReactProsemirror`](#useisinreactprosemirror)
|
|
16
|
+
|
|
17
|
+
<!-- tocstop -->
|
|
18
|
+
|
|
19
|
+
## The Problem
|
|
20
|
+
|
|
21
|
+
Tiptap has a first-party React integration, `@tiptap/react`, but it has some
|
|
22
|
+
downsides:
|
|
23
|
+
|
|
24
|
+
- Each React node view is rendered in a portal, and the portals are all direct
|
|
25
|
+
siblings. This means that context doesn’t flow from parent node views to their
|
|
26
|
+
children.
|
|
27
|
+
- ProseMirror View requires that a node view’s `dom` and `contentDOM` are
|
|
28
|
+
produced synchronously, but React renders asynchronously. The result is that
|
|
29
|
+
every node view needs to be wrapped in additional DOM nodes (which are not
|
|
30
|
+
controlled by React). Your `paragraph` node view will look like
|
|
31
|
+
`<div><p><span><span>text</span></span></p></div>`!
|
|
32
|
+
- There is a lot of state tearing. Tiptap executes side effects in render
|
|
33
|
+
functions, renders node view components in a second render pass, and exposes
|
|
34
|
+
the `Editor` in the render function and other unsafe locations. This means
|
|
35
|
+
that you can run into issues with data corruption and user experience issues
|
|
36
|
+
that are very challenging to pin down and resolve.
|
|
37
|
+
|
|
38
|
+
## The Solution
|
|
39
|
+
|
|
40
|
+
React ProseMirror has a React-based rendering system.
|
|
41
|
+
`@handlewithcare/react-prosemirror/tiptap` exposes an integration layer that
|
|
42
|
+
integrates that React-based ProseMirror renderer with Tiptap, allowing you to
|
|
43
|
+
keep your existing Tiptap extensions and commands, but giving you a safer React
|
|
44
|
+
integration. The React ProseMirror renderer also doesn’t require any wrapping
|
|
45
|
+
DOM nodes — your paragraph can just be `<p>text</p>`!
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### `useTiptapEditor`
|
|
50
|
+
|
|
51
|
+
To start, we’ll replace the usage of `@tiptap/react`’s `useEditor` hook with
|
|
52
|
+
React ProseMirror’s `useTiptapEditor`:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// import { useEditor } from "@tiptap/react";
|
|
56
|
+
import { useTiptapEditor } from "@handlewithcare/react-prosemirror/tiptap";
|
|
57
|
+
|
|
58
|
+
// const editor = useEditor({ extensions })
|
|
59
|
+
|
|
60
|
+
const editor = useTiptapEditor({ extensions });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `TiptapEditorView` and `TiptapEditorContent`
|
|
64
|
+
|
|
65
|
+
Next, we’ll replace `EditorContent` from `@tiptap/react` with
|
|
66
|
+
`TiptapEditorContent`. Like the plain React ProseMirror’s
|
|
67
|
+
[`ProseMirrorDoc`](../../README.md#prosemirrordoc), `TiptapEditorContent` must
|
|
68
|
+
be wrapped in a `TiptapEditorView`. Any components that are descendants of
|
|
69
|
+
`TiptapEditorView` can safely access the Tiptap Editor instance.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// import { EditorContent, useEditor } from '@tiptap/react';
|
|
73
|
+
import {
|
|
74
|
+
TiptapEditorView,
|
|
75
|
+
TiptapEditorContent,
|
|
76
|
+
useTiptapEditor,
|
|
77
|
+
} from "@handlewithcare/react-prosemirror/tiptap";
|
|
78
|
+
|
|
79
|
+
export function Editor() {
|
|
80
|
+
// const editor = useEditor({ extensions })
|
|
81
|
+
|
|
82
|
+
const editor = useTiptapEditor({ extensions });
|
|
83
|
+
|
|
84
|
+
// return <EditorContent editor={editor} />
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<TiptapEditorView editor={editor}>
|
|
88
|
+
<TiptapEditorContent editor={editor} />
|
|
89
|
+
</TiptapEditorView>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `useTiptapEditorEffect` and `useTiptapEditorEventCallback`
|
|
95
|
+
|
|
96
|
+
Then, any usages of `useEffect` or `useCallback` that make use of the Editor
|
|
97
|
+
instance should be replaced with React ProseMirror’s `useTiptapEditorEffect` and
|
|
98
|
+
`useTiptapEditorEventCallback`. These will ensure that Editor access is limited
|
|
99
|
+
to safe points in the React render cycle, when the DOM, ProseMirror state, and
|
|
100
|
+
React state are all in sync.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// import { useEffect } from 'react';
|
|
104
|
+
import { useTiptapEditorEffect } from "@handlewithcare/react-prosemirror/tiptap";
|
|
105
|
+
|
|
106
|
+
// useEffect(() => {
|
|
107
|
+
// editor.commands.focus();
|
|
108
|
+
// }, [editor])
|
|
109
|
+
|
|
110
|
+
useTiptapEditorEffect(
|
|
111
|
+
(editor) => {
|
|
112
|
+
editor.commands.focus();
|
|
113
|
+
},
|
|
114
|
+
[editor]
|
|
115
|
+
);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// import { useCallback } from 'react';
|
|
120
|
+
import { useTiptapEditorEventCallback } from "@handlewithcare/react-prosemirror/tiptap";
|
|
121
|
+
|
|
122
|
+
// const onClick = useCallback(() => {
|
|
123
|
+
// editor.commands.focus();
|
|
124
|
+
// }, [editor])
|
|
125
|
+
|
|
126
|
+
// NOTE: `useTiptapEditorEventCallback` doesn’t require a dependencies
|
|
127
|
+
// argument.
|
|
128
|
+
const onClick = useTiptapEditorEventCallback((editor) => {
|
|
129
|
+
editor.commands.focus();
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### `tiptapNodeView`
|
|
134
|
+
|
|
135
|
+
And finally, any custom node views that use Tiptap’s `ReactNodeViewRenderer` can
|
|
136
|
+
be migrated with React ProseMirror’s `tiptapNodeView` higher order component:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { tiptapNodeView } from "@handlewithcare/react-prosemirror/tiptap";
|
|
140
|
+
import { Node } from "@tiptap/core";
|
|
141
|
+
import { ReactNodeViewRenderer } from "@tiptap/react";
|
|
142
|
+
import Paragraph from "./ParagraphView.jsx";
|
|
143
|
+
|
|
144
|
+
const extension = Node.create({
|
|
145
|
+
name: "paragraph",
|
|
146
|
+
|
|
147
|
+
// NOTE: No need for addNodeView anymore!
|
|
148
|
+
// addNodeView() {
|
|
149
|
+
// return ReactNodeViewRenderer(Paragraph);
|
|
150
|
+
// },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
export default extension;
|
|
154
|
+
|
|
155
|
+
export const paragraph = tiptapNodeView({
|
|
156
|
+
extension,
|
|
157
|
+
component: Paragraph,
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
You’ll need to pass your new React ProseMirror node view component to the
|
|
162
|
+
`TiptapEditorView` as a prop:
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import {
|
|
166
|
+
TiptapEditorView,
|
|
167
|
+
TiptapEditorContent,
|
|
168
|
+
useTiptapEditor,
|
|
169
|
+
} from "@handlewithcare/react-prosemirror/tiptap";
|
|
170
|
+
|
|
171
|
+
import { paragraph } from "./extensions/Paragraph.js";
|
|
172
|
+
|
|
173
|
+
const nodeViewComponents = {
|
|
174
|
+
paragraph,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export function Editor() {
|
|
178
|
+
const editor = useTiptapEditor({ extensions });
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<TiptapEditorView editor={editor} nodeViewComponents={nodeViewComponents}>
|
|
182
|
+
<TiptapEditorContent editor={editor} />
|
|
183
|
+
</TiptapEditorView>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`tiptapNodeView` is a compatibility helper. It’s not required for React
|
|
189
|
+
ProseMirror’s Tiptap integration, it just allows you to migrate from
|
|
190
|
+
`@tiptap/react` without needing to rewrite all of your React node view
|
|
191
|
+
components. It preserves all of `@tiptap/react`’s functionality, including the
|
|
192
|
+
default drag start behavior, `ignoreMutation` and `stopEvent` handlers, and
|
|
193
|
+
wrapping DOM nodes.
|
|
194
|
+
|
|
195
|
+
If you want to move to using React ProseMirror node view components directly,
|
|
196
|
+
which allows you to drop the wrapping DOM nodes, follow the guide for writing
|
|
197
|
+
[React node view components](../../README.md#building-node-view-with-react).
|
|
198
|
+
|
|
199
|
+
## API
|
|
200
|
+
|
|
201
|
+
### `TiptapEditorView`
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
type TiptapEditorView = (props: {
|
|
205
|
+
editor: Editor;
|
|
206
|
+
nodeViewComponents?: Record<string, ComponentType<NodeViewComponentProps>;
|
|
207
|
+
markViewComponents: Record<string, ComponentType<MarkViewComponentProps>;
|
|
208
|
+
children?: ReactNode;
|
|
209
|
+
static?: boolean;
|
|
210
|
+
}) => JSX.Element;
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Render a Tiptap-compatible React ProseMirror editor.
|
|
214
|
+
|
|
215
|
+
### `TiptapEditorContent`
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
type TiptapEditorContent = HTMLProps<HTMLElement> & (props: {
|
|
219
|
+
editor: Editor;
|
|
220
|
+
as?: ElementType;
|
|
221
|
+
}) => JSX.Element;
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Renders the actual editable ProseMirror document.
|
|
225
|
+
|
|
226
|
+
This **must** be passed as a child to the `TiptapEditorView` component. It may
|
|
227
|
+
be wrapped in other components, and other childern may be passed before or
|
|
228
|
+
after. It must be passed the same `editor` as is passed to the
|
|
229
|
+
`TiptapEditorView`.
|
|
230
|
+
|
|
231
|
+
### `tiptapNodeView`
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
type tiptapNodeView = (options: {
|
|
235
|
+
component: ComponentType<ReactNodeViewProps>;
|
|
236
|
+
extension: ReactNodeViewProps["extension"];
|
|
237
|
+
className?: string | undefined;
|
|
238
|
+
attrs?:
|
|
239
|
+
| Record<string, string>
|
|
240
|
+
| ((props: {
|
|
241
|
+
node: ProseMirrorNode;
|
|
242
|
+
HTMLAttributes: Record<string, unknown>;
|
|
243
|
+
}) => Record<string, string>)
|
|
244
|
+
| undefined;
|
|
245
|
+
as?: ElementType | undefined;
|
|
246
|
+
stopEvent?:
|
|
247
|
+
| ((props: {
|
|
248
|
+
event: Event;
|
|
249
|
+
defaultStopEvent: (event: Event) => boolean;
|
|
250
|
+
}) => boolean)
|
|
251
|
+
| null;
|
|
252
|
+
ignoreMutation?:
|
|
253
|
+
| ((props: {
|
|
254
|
+
mutation: ViewMutationRecord;
|
|
255
|
+
defaultIgnoreMutation: (mutation: ViewMutationRecord) => boolean;
|
|
256
|
+
}) => boolean)
|
|
257
|
+
| null;
|
|
258
|
+
contentDOMElementTag?: ElementType | undefined;
|
|
259
|
+
}) => ComponentType<NodeViewComponentProps>;
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Convert a Tiptap node view component to a React ProseMirror node view component
|
|
263
|
+
Given a Tiptap-compatible React component and a Tiptap extension, returns a
|
|
264
|
+
React component that can be passed to React ProseMirror as a custom node view.
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
const nodeViews = {
|
|
270
|
+
codeBlock: nodeView({
|
|
271
|
+
component: function CodeBlock(nodeViewProps) {
|
|
272
|
+
return (
|
|
273
|
+
<pre>
|
|
274
|
+
<NodeViewContent as="code" />
|
|
275
|
+
</pre>
|
|
276
|
+
);
|
|
277
|
+
},
|
|
278
|
+
extension: CodeBlockExtension,
|
|
279
|
+
}),
|
|
280
|
+
};
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### `useTiptapEditor`
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
type useTiptapEditor(
|
|
287
|
+
options: Omit<Parameters<typeof useEditor[0], 'element'>,
|
|
288
|
+
deps?: DependencyList
|
|
289
|
+
) => Editor
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Create a React ProseMirror integrated Tiptap Editor instance. Use instead of
|
|
293
|
+
Tiptap’s `useEditor` hook.
|
|
294
|
+
|
|
295
|
+
### `useTiptapEditorEffect`
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
type useEditorEffect = (
|
|
299
|
+
effect: (editor: Editor | null) => void | (() => void),
|
|
300
|
+
dependencies?: React.DependencyList
|
|
301
|
+
) => void;
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Registers a layout effect to run after the EditorView has been updated with the
|
|
305
|
+
latest EditorState and Decorations.
|
|
306
|
+
|
|
307
|
+
Effects can take a Tiptap Editor instance as an argument. This hook should be
|
|
308
|
+
used to execute layout effects that depend on the Editor, such as for
|
|
309
|
+
positioning DOM nodes based on ProseMirror positions.
|
|
310
|
+
|
|
311
|
+
Layout effects registered with this hook still fire synchronously after all DOM
|
|
312
|
+
mutations, but they do so _after_ the Editor has been updated, even when the
|
|
313
|
+
Editor lives in an ancestor component.
|
|
314
|
+
|
|
315
|
+
This hook can only be used in a component that is mounted as a child of the
|
|
316
|
+
TiptapEditorView component, including React node view components.
|
|
317
|
+
|
|
318
|
+
### `useTiptapEditorEventCallback`
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
type useEditorEventCallback = <T extends unknown[]>(
|
|
322
|
+
callback: (editor: Editor | null, ...args: T) => void
|
|
323
|
+
) => void;
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Returns a stable function reference to be used as an event handler callback.
|
|
327
|
+
|
|
328
|
+
The callback will be called with the Tiptap Editor instance as its first
|
|
329
|
+
argument.
|
|
330
|
+
|
|
331
|
+
This hook can only be used in a component that is mounted as a child of the
|
|
332
|
+
TiptapEditorView component, including React node view components.
|
|
333
|
+
|
|
334
|
+
### `useIsInReactProsemirror`
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
type useIsInReactProseMirror = () => boolean;
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Returns true if the hook is called in a component that's a descendant of the
|
|
341
|
+
ProseMirror component
|