@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.
@@ -0,0 +1,6 @@
1
+ declare global {
2
+ interface Window {
3
+ gc?: () => void;
4
+ }
5
+ }
6
+ export {};
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Returns true while an IME composition is active inside the current node.
3
+ */
4
+ export declare function useIsComposingIn(): boolean;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handlewithcare/react-prosemirror",
3
- "version": "3.1.1",
3
+ "version": "3.1.2",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/cjs/index.js",
@@ -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