@tiptap/react 3.17.1 → 3.19.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/dist/index.cjs +79 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +178 -1
- package/dist/index.d.ts +178 -1
- package/dist/index.js +73 -4
- package/dist/index.js.map +1 -1
- package/dist/menus/index.cjs +72 -17
- package/dist/menus/index.cjs.map +1 -1
- package/dist/menus/index.js +74 -19
- package/dist/menus/index.js.map +1 -1
- package/package.json +7 -7
- package/src/ReactNodeViewRenderer.tsx +29 -3
- package/src/Tiptap.tsx +224 -0
- package/src/index.ts +1 -0
- package/src/menus/BubbleMenu.tsx +49 -1
- package/src/menus/FloatingMenu.tsx +76 -20
package/dist/menus/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/menus/BubbleMenu.tsx
|
|
2
2
|
import { BubbleMenuPlugin } from "@tiptap/extension-bubble-menu";
|
|
3
3
|
import { useCurrentEditor } from "@tiptap/react";
|
|
4
|
-
import React, { useEffect, useRef } from "react";
|
|
4
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
6
|
import { jsx } from "react/jsx-runtime";
|
|
7
7
|
var BubbleMenu = React.forwardRef(
|
|
@@ -36,6 +36,8 @@ var BubbleMenu = React.forwardRef(
|
|
|
36
36
|
};
|
|
37
37
|
const bubbleMenuPluginPropsRef = useRef(bubbleMenuPluginProps);
|
|
38
38
|
bubbleMenuPluginPropsRef.current = bubbleMenuPluginProps;
|
|
39
|
+
const [pluginInitialized, setPluginInitialized] = useState(false);
|
|
40
|
+
const skipFirstUpdateRef = useRef(true);
|
|
39
41
|
useEffect(() => {
|
|
40
42
|
if (pluginEditor == null ? void 0 : pluginEditor.isDestroyed) {
|
|
41
43
|
return;
|
|
@@ -54,7 +56,10 @@ var BubbleMenu = React.forwardRef(
|
|
|
54
56
|
});
|
|
55
57
|
pluginEditor.registerPlugin(plugin);
|
|
56
58
|
const createdPluginKey = bubbleMenuPluginPropsRef.current.pluginKey;
|
|
59
|
+
skipFirstUpdateRef.current = true;
|
|
60
|
+
setPluginInitialized(true);
|
|
57
61
|
return () => {
|
|
62
|
+
setPluginInitialized(false);
|
|
58
63
|
pluginEditor.unregisterPlugin(createdPluginKey);
|
|
59
64
|
window.requestAnimationFrame(() => {
|
|
60
65
|
if (bubbleMenuElement.parentNode) {
|
|
@@ -63,6 +68,30 @@ var BubbleMenu = React.forwardRef(
|
|
|
63
68
|
});
|
|
64
69
|
};
|
|
65
70
|
}, [pluginEditor]);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (skipFirstUpdateRef.current) {
|
|
76
|
+
skipFirstUpdateRef.current = false;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
pluginEditor.view.dispatch(
|
|
80
|
+
pluginEditor.state.tr.setMeta("bubbleMenu", {
|
|
81
|
+
type: "updateOptions",
|
|
82
|
+
options: bubbleMenuPluginPropsRef.current
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
}, [
|
|
86
|
+
pluginInitialized,
|
|
87
|
+
pluginEditor,
|
|
88
|
+
updateDelay,
|
|
89
|
+
resizeDelay,
|
|
90
|
+
shouldShow,
|
|
91
|
+
options,
|
|
92
|
+
appendTo,
|
|
93
|
+
getReferencedVirtualElement
|
|
94
|
+
]);
|
|
66
95
|
return createPortal(/* @__PURE__ */ jsx("div", { ...restProps, children }), menuEl.current);
|
|
67
96
|
}
|
|
68
97
|
);
|
|
@@ -70,7 +99,7 @@ var BubbleMenu = React.forwardRef(
|
|
|
70
99
|
// src/menus/FloatingMenu.tsx
|
|
71
100
|
import { FloatingMenuPlugin } from "@tiptap/extension-floating-menu";
|
|
72
101
|
import { useCurrentEditor as useCurrentEditor2 } from "@tiptap/react";
|
|
73
|
-
import React2, { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
102
|
+
import React2, { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
74
103
|
import { createPortal as createPortal2 } from "react-dom";
|
|
75
104
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
76
105
|
var FloatingMenu = React2.forwardRef(
|
|
@@ -92,40 +121,66 @@ var FloatingMenu = React2.forwardRef(
|
|
|
92
121
|
ref.current = menuEl.current;
|
|
93
122
|
}
|
|
94
123
|
const { editor: currentEditor } = useCurrentEditor2();
|
|
124
|
+
const pluginEditor = editor || currentEditor;
|
|
125
|
+
const floatingMenuPluginProps = {
|
|
126
|
+
updateDelay,
|
|
127
|
+
resizeDelay,
|
|
128
|
+
appendTo,
|
|
129
|
+
pluginKey,
|
|
130
|
+
shouldShow,
|
|
131
|
+
options
|
|
132
|
+
};
|
|
133
|
+
const floatingMenuPluginPropsRef = useRef2(floatingMenuPluginProps);
|
|
134
|
+
floatingMenuPluginPropsRef.current = floatingMenuPluginProps;
|
|
135
|
+
const [pluginInitialized, setPluginInitialized] = useState2(false);
|
|
136
|
+
const skipFirstUpdateRef = useRef2(true);
|
|
95
137
|
useEffect2(() => {
|
|
96
|
-
|
|
97
|
-
floatingMenuElement.style.visibility = "hidden";
|
|
98
|
-
floatingMenuElement.style.position = "absolute";
|
|
99
|
-
if ((editor == null ? void 0 : editor.isDestroyed) || (currentEditor == null ? void 0 : currentEditor.isDestroyed)) {
|
|
138
|
+
if (pluginEditor == null ? void 0 : pluginEditor.isDestroyed) {
|
|
100
139
|
return;
|
|
101
140
|
}
|
|
102
|
-
|
|
103
|
-
if (!attachToEditor) {
|
|
141
|
+
if (!pluginEditor) {
|
|
104
142
|
console.warn(
|
|
105
143
|
"FloatingMenu component is not rendered inside of an editor component or does not have editor prop."
|
|
106
144
|
);
|
|
107
145
|
return;
|
|
108
146
|
}
|
|
147
|
+
const floatingMenuElement = menuEl.current;
|
|
148
|
+
floatingMenuElement.style.visibility = "hidden";
|
|
149
|
+
floatingMenuElement.style.position = "absolute";
|
|
109
150
|
const plugin = FloatingMenuPlugin({
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
updateDelay,
|
|
114
|
-
resizeDelay,
|
|
115
|
-
appendTo,
|
|
116
|
-
shouldShow,
|
|
117
|
-
options
|
|
151
|
+
...floatingMenuPluginPropsRef.current,
|
|
152
|
+
editor: pluginEditor,
|
|
153
|
+
element: floatingMenuElement
|
|
118
154
|
});
|
|
119
|
-
|
|
155
|
+
pluginEditor.registerPlugin(plugin);
|
|
156
|
+
const createdPluginKey = floatingMenuPluginPropsRef.current.pluginKey;
|
|
157
|
+
skipFirstUpdateRef.current = true;
|
|
158
|
+
setPluginInitialized(true);
|
|
120
159
|
return () => {
|
|
121
|
-
|
|
160
|
+
setPluginInitialized(false);
|
|
161
|
+
pluginEditor.unregisterPlugin(createdPluginKey);
|
|
122
162
|
window.requestAnimationFrame(() => {
|
|
123
163
|
if (floatingMenuElement.parentNode) {
|
|
124
164
|
floatingMenuElement.parentNode.removeChild(floatingMenuElement);
|
|
125
165
|
}
|
|
126
166
|
});
|
|
127
167
|
};
|
|
128
|
-
}, [
|
|
168
|
+
}, [pluginEditor]);
|
|
169
|
+
useEffect2(() => {
|
|
170
|
+
if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (skipFirstUpdateRef.current) {
|
|
174
|
+
skipFirstUpdateRef.current = false;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
pluginEditor.view.dispatch(
|
|
178
|
+
pluginEditor.state.tr.setMeta("floatingMenu", {
|
|
179
|
+
type: "updateOptions",
|
|
180
|
+
options: floatingMenuPluginPropsRef.current
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
}, [pluginInitialized, pluginEditor, updateDelay, resizeDelay, shouldShow, options, appendTo]);
|
|
129
184
|
return createPortal2(/* @__PURE__ */ jsx2("div", { ...restProps, children }), menuEl.current);
|
|
130
185
|
}
|
|
131
186
|
);
|
package/dist/menus/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/menus/BubbleMenu.tsx","../../src/menus/FloatingMenu.tsx"],"sourcesContent":["import { type BubbleMenuPluginProps, BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'\nimport { useCurrentEditor } from '@tiptap/react'\nimport React, { useEffect, useRef } from 'react'\nimport { createPortal } from 'react-dom'\n\ntype Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>\n\nexport type BubbleMenuProps = Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> &\n React.HTMLAttributes<HTMLDivElement>\n\nexport const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(\n (\n {\n pluginKey = 'bubbleMenu',\n editor,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow = null,\n getReferencedVirtualElement,\n options,\n children,\n ...restProps\n },\n ref,\n ) => {\n const menuEl = useRef(document.createElement('div'))\n\n if (typeof ref === 'function') {\n ref(menuEl.current)\n } else if (ref) {\n ref.current = menuEl.current\n }\n\n const { editor: currentEditor } = useCurrentEditor()\n\n /**\n * The editor instance where the bubble menu plugin will be registered.\n */\n const pluginEditor = editor || currentEditor\n\n // Creating a useMemo would be more computationally expensive than just\n // re-creating this object on every render.\n const bubbleMenuPluginProps: Omit<BubbleMenuPluginProps, 'editor' | 'element'> = {\n updateDelay,\n resizeDelay,\n appendTo,\n pluginKey,\n shouldShow,\n getReferencedVirtualElement,\n options,\n }\n /**\n * The props for the bubble menu plugin. They are accessed inside a ref to\n * avoid running the useEffect hook and re-registering the plugin when the\n * props change.\n */\n const bubbleMenuPluginPropsRef = useRef(bubbleMenuPluginProps)\n bubbleMenuPluginPropsRef.current = bubbleMenuPluginProps\n\n useEffect(() => {\n if (pluginEditor?.isDestroyed) {\n return\n }\n\n if (!pluginEditor) {\n console.warn('BubbleMenu component is not rendered inside of an editor component or does not have editor prop.')\n return\n }\n\n const bubbleMenuElement = menuEl.current\n bubbleMenuElement.style.visibility = 'hidden'\n bubbleMenuElement.style.position = 'absolute'\n\n const plugin = BubbleMenuPlugin({\n ...bubbleMenuPluginPropsRef.current,\n editor: pluginEditor,\n element: bubbleMenuElement,\n })\n\n pluginEditor.registerPlugin(plugin)\n\n const createdPluginKey = bubbleMenuPluginPropsRef.current.pluginKey\n\n return () => {\n pluginEditor.unregisterPlugin(createdPluginKey)\n window.requestAnimationFrame(() => {\n if (bubbleMenuElement.parentNode) {\n bubbleMenuElement.parentNode.removeChild(bubbleMenuElement)\n }\n })\n }\n }, [pluginEditor])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n","import type { FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'\nimport { FloatingMenuPlugin } from '@tiptap/extension-floating-menu'\nimport { useCurrentEditor } from '@tiptap/react'\nimport React, { useEffect, useRef } from 'react'\nimport { createPortal } from 'react-dom'\n\ntype Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>\n\nexport type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element' | 'editor'> & {\n editor: FloatingMenuPluginProps['editor'] | null\n options?: FloatingMenuPluginProps['options']\n} & React.HTMLAttributes<HTMLDivElement>\n\nexport const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(\n (\n {\n pluginKey = 'floatingMenu',\n editor,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow = null,\n options,\n children,\n ...restProps\n },\n ref,\n ) => {\n const menuEl = useRef(document.createElement('div'))\n\n if (typeof ref === 'function') {\n ref(menuEl.current)\n } else if (ref) {\n ref.current = menuEl.current\n }\n\n const { editor: currentEditor } = useCurrentEditor()\n\n useEffect(() => {\n const floatingMenuElement = menuEl.current\n\n floatingMenuElement.style.visibility = 'hidden'\n floatingMenuElement.style.position = 'absolute'\n\n if (editor?.isDestroyed || (currentEditor as any)?.isDestroyed) {\n return\n }\n\n const attachToEditor = editor || currentEditor\n\n if (!attachToEditor) {\n console.warn(\n 'FloatingMenu component is not rendered inside of an editor component or does not have editor prop.',\n )\n return\n }\n\n const plugin = FloatingMenuPlugin({\n editor: attachToEditor,\n element: floatingMenuElement,\n pluginKey,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow,\n options,\n })\n\n attachToEditor.registerPlugin(plugin)\n\n return () => {\n attachToEditor.unregisterPlugin(pluginKey)\n window.requestAnimationFrame(() => {\n if (floatingMenuElement.parentNode) {\n floatingMenuElement.parentNode.removeChild(floatingMenuElement)\n }\n })\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editor, currentEditor, appendTo, pluginKey, shouldShow, options, updateDelay, resizeDelay])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n"],"mappings":"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","names":["useCurrentEditor","React","useEffect","useRef","createPortal","jsx"]}
|
|
1
|
+
{"version":3,"sources":["../../src/menus/BubbleMenu.tsx","../../src/menus/FloatingMenu.tsx"],"sourcesContent":["import { type BubbleMenuPluginProps, BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'\nimport { useCurrentEditor } from '@tiptap/react'\nimport React, { useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\n\ntype Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>\n\nexport type BubbleMenuProps = Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> &\n React.HTMLAttributes<HTMLDivElement>\n\nexport const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(\n (\n {\n pluginKey = 'bubbleMenu',\n editor,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow = null,\n getReferencedVirtualElement,\n options,\n children,\n ...restProps\n },\n ref,\n ) => {\n const menuEl = useRef(document.createElement('div'))\n\n if (typeof ref === 'function') {\n ref(menuEl.current)\n } else if (ref) {\n ref.current = menuEl.current\n }\n\n const { editor: currentEditor } = useCurrentEditor()\n\n /**\n * The editor instance where the bubble menu plugin will be registered.\n */\n const pluginEditor = editor || currentEditor\n\n // Creating a useMemo would be more computationally expensive than just\n // re-creating this object on every render.\n const bubbleMenuPluginProps: Omit<BubbleMenuPluginProps, 'editor' | 'element'> = {\n updateDelay,\n resizeDelay,\n appendTo,\n pluginKey,\n shouldShow,\n getReferencedVirtualElement,\n options,\n }\n /**\n * The props for the bubble menu plugin. They are accessed inside a ref to\n * avoid running the useEffect hook and re-registering the plugin when the\n * props change.\n */\n const bubbleMenuPluginPropsRef = useRef(bubbleMenuPluginProps)\n bubbleMenuPluginPropsRef.current = bubbleMenuPluginProps\n\n /**\n * Track whether the plugin has been initialized, so we only send updates\n * after the initial registration.\n */\n const [pluginInitialized, setPluginInitialized] = useState(false)\n\n /**\n * Track whether we need to skip the first options update dispatch.\n * This prevents unnecessary updates right after plugin initialization.\n */\n const skipFirstUpdateRef = useRef(true)\n\n useEffect(() => {\n if (pluginEditor?.isDestroyed) {\n return\n }\n\n if (!pluginEditor) {\n console.warn('BubbleMenu component is not rendered inside of an editor component or does not have editor prop.')\n return\n }\n\n const bubbleMenuElement = menuEl.current\n bubbleMenuElement.style.visibility = 'hidden'\n bubbleMenuElement.style.position = 'absolute'\n\n const plugin = BubbleMenuPlugin({\n ...bubbleMenuPluginPropsRef.current,\n editor: pluginEditor,\n element: bubbleMenuElement,\n })\n\n pluginEditor.registerPlugin(plugin)\n\n const createdPluginKey = bubbleMenuPluginPropsRef.current.pluginKey\n\n skipFirstUpdateRef.current = true\n setPluginInitialized(true)\n\n return () => {\n setPluginInitialized(false)\n pluginEditor.unregisterPlugin(createdPluginKey)\n window.requestAnimationFrame(() => {\n if (bubbleMenuElement.parentNode) {\n bubbleMenuElement.parentNode.removeChild(bubbleMenuElement)\n }\n })\n }\n }, [pluginEditor])\n\n /**\n * Update the plugin options when props change after the plugin has been initialized.\n * This allows dynamic updates to options like scrollTarget without re-registering the entire plugin.\n */\n useEffect(() => {\n if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) {\n return\n }\n\n // Skip the first update right after initialization since the plugin was just created with these options\n if (skipFirstUpdateRef.current) {\n skipFirstUpdateRef.current = false\n return\n }\n\n pluginEditor.view.dispatch(\n pluginEditor.state.tr.setMeta('bubbleMenu', {\n type: 'updateOptions',\n options: bubbleMenuPluginPropsRef.current,\n }),\n )\n }, [\n pluginInitialized,\n pluginEditor,\n updateDelay,\n resizeDelay,\n shouldShow,\n options,\n appendTo,\n getReferencedVirtualElement,\n ])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n","import type { FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'\nimport { FloatingMenuPlugin } from '@tiptap/extension-floating-menu'\nimport { useCurrentEditor } from '@tiptap/react'\nimport React, { useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\n\ntype Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>\n\nexport type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element' | 'editor'> & {\n editor: FloatingMenuPluginProps['editor'] | null\n options?: FloatingMenuPluginProps['options']\n} & React.HTMLAttributes<HTMLDivElement>\n\nexport const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(\n (\n {\n pluginKey = 'floatingMenu',\n editor,\n updateDelay,\n resizeDelay,\n appendTo,\n shouldShow = null,\n options,\n children,\n ...restProps\n },\n ref,\n ) => {\n const menuEl = useRef(document.createElement('div'))\n\n if (typeof ref === 'function') {\n ref(menuEl.current)\n } else if (ref) {\n ref.current = menuEl.current\n }\n\n const { editor: currentEditor } = useCurrentEditor()\n\n /**\n * The editor instance where the floating menu plugin will be registered.\n */\n const pluginEditor = editor || currentEditor\n\n // Creating a useMemo would be more computationally expensive than just\n // re-creating this object on every render.\n const floatingMenuPluginProps: Omit<FloatingMenuPluginProps, 'editor' | 'element'> = {\n updateDelay,\n resizeDelay,\n appendTo,\n pluginKey,\n shouldShow,\n options,\n }\n\n /**\n * The props for the floating menu plugin. They are accessed inside a ref to\n * avoid running the useEffect hook and re-registering the plugin when the\n * props change.\n */\n const floatingMenuPluginPropsRef = useRef(floatingMenuPluginProps)\n floatingMenuPluginPropsRef.current = floatingMenuPluginProps\n\n /**\n * Track whether the plugin has been initialized, so we only send updates\n * after the initial registration.\n */\n const [pluginInitialized, setPluginInitialized] = useState(false)\n\n /**\n * Track whether we need to skip the first options update dispatch.\n * This prevents unnecessary updates right after plugin initialization.\n */\n const skipFirstUpdateRef = useRef(true)\n\n useEffect(() => {\n if (pluginEditor?.isDestroyed) {\n return\n }\n\n if (!pluginEditor) {\n console.warn(\n 'FloatingMenu component is not rendered inside of an editor component or does not have editor prop.',\n )\n return\n }\n\n const floatingMenuElement = menuEl.current\n floatingMenuElement.style.visibility = 'hidden'\n floatingMenuElement.style.position = 'absolute'\n\n const plugin = FloatingMenuPlugin({\n ...floatingMenuPluginPropsRef.current,\n editor: pluginEditor,\n element: floatingMenuElement,\n })\n\n pluginEditor.registerPlugin(plugin)\n\n const createdPluginKey = floatingMenuPluginPropsRef.current.pluginKey\n\n skipFirstUpdateRef.current = true\n setPluginInitialized(true)\n\n return () => {\n setPluginInitialized(false)\n pluginEditor.unregisterPlugin(createdPluginKey)\n window.requestAnimationFrame(() => {\n if (floatingMenuElement.parentNode) {\n floatingMenuElement.parentNode.removeChild(floatingMenuElement)\n }\n })\n }\n }, [pluginEditor])\n\n /**\n * Update the plugin options when props change after the plugin has been initialized.\n * This allows dynamic updates to options like scrollTarget without re-registering the entire plugin.\n */\n useEffect(() => {\n if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) {\n return\n }\n\n // Skip the first update right after initialization since the plugin was just created with these options\n if (skipFirstUpdateRef.current) {\n skipFirstUpdateRef.current = false\n return\n }\n\n pluginEditor.view.dispatch(\n pluginEditor.state.tr.setMeta('floatingMenu', {\n type: 'updateOptions',\n options: floatingMenuPluginPropsRef.current,\n }),\n )\n }, [pluginInitialized, pluginEditor, updateDelay, resizeDelay, shouldShow, options, appendTo])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n"],"mappings":"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","names":["useCurrentEditor","React","useEffect","useRef","useState","createPortal","jsx"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiptap/react",
|
|
3
3
|
"description": "React components for tiptap",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.19.0",
|
|
5
5
|
"homepage": "https://tiptap.dev",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tiptap",
|
|
@@ -48,20 +48,20 @@
|
|
|
48
48
|
"@types/react-dom": "^19.0.0",
|
|
49
49
|
"react": "^19.0.0",
|
|
50
50
|
"react-dom": "^19.0.0",
|
|
51
|
-
"@tiptap/core": "^3.
|
|
52
|
-
"@tiptap/pm": "^3.
|
|
51
|
+
"@tiptap/core": "^3.19.0",
|
|
52
|
+
"@tiptap/pm": "^3.19.0"
|
|
53
53
|
},
|
|
54
54
|
"optionalDependencies": {
|
|
55
|
-
"@tiptap/extension-bubble-menu": "^3.
|
|
56
|
-
"@tiptap/extension-floating-menu": "^3.
|
|
55
|
+
"@tiptap/extension-bubble-menu": "^3.19.0",
|
|
56
|
+
"@tiptap/extension-floating-menu": "^3.19.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
60
60
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
61
61
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
62
62
|
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
63
|
-
"@tiptap/
|
|
64
|
-
"@tiptap/
|
|
63
|
+
"@tiptap/core": "^3.19.0",
|
|
64
|
+
"@tiptap/pm": "^3.19.0"
|
|
65
65
|
},
|
|
66
66
|
"repository": {
|
|
67
67
|
"type": "git",
|
|
@@ -100,6 +100,31 @@ export class ReactNodeView<
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
private cachedExtensionWithSyncedStorage: NodeViewRendererProps['extension'] | null = null
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns a proxy of the extension that redirects storage access to the editor's mutable storage.
|
|
107
|
+
* This preserves the original prototype chain (instanceof checks, methods like configure/extend work).
|
|
108
|
+
* Cached to avoid proxy creation on every update.
|
|
109
|
+
*/
|
|
110
|
+
get extensionWithSyncedStorage(): NodeViewRendererProps['extension'] {
|
|
111
|
+
if (!this.cachedExtensionWithSyncedStorage) {
|
|
112
|
+
const editor = this.editor
|
|
113
|
+
const extension = this.extension
|
|
114
|
+
|
|
115
|
+
this.cachedExtensionWithSyncedStorage = new Proxy(extension, {
|
|
116
|
+
get(target, prop, receiver) {
|
|
117
|
+
if (prop === 'storage') {
|
|
118
|
+
return editor.storage[extension.name as keyof typeof editor.storage] ?? {}
|
|
119
|
+
}
|
|
120
|
+
return Reflect.get(target, prop, receiver)
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return this.cachedExtensionWithSyncedStorage
|
|
126
|
+
}
|
|
127
|
+
|
|
103
128
|
/**
|
|
104
129
|
* Setup the React component.
|
|
105
130
|
* Called on initialization.
|
|
@@ -112,7 +137,7 @@ export class ReactNodeView<
|
|
|
112
137
|
innerDecorations: this.innerDecorations,
|
|
113
138
|
view: this.view,
|
|
114
139
|
selected: false,
|
|
115
|
-
extension: this.
|
|
140
|
+
extension: this.extensionWithSyncedStorage,
|
|
116
141
|
HTMLAttributes: this.HTMLAttributes,
|
|
117
142
|
getPos: () => this.getPos(),
|
|
118
143
|
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
|
|
@@ -266,7 +291,8 @@ export class ReactNodeView<
|
|
|
266
291
|
newDecorations: decorations,
|
|
267
292
|
oldInnerDecorations,
|
|
268
293
|
innerDecorations,
|
|
269
|
-
updateProps: () =>
|
|
294
|
+
updateProps: () =>
|
|
295
|
+
rerenderComponent({ node, decorations, innerDecorations, extension: this.extensionWithSyncedStorage }),
|
|
270
296
|
})
|
|
271
297
|
}
|
|
272
298
|
|
|
@@ -278,7 +304,7 @@ export class ReactNodeView<
|
|
|
278
304
|
this.decorations = decorations
|
|
279
305
|
this.innerDecorations = innerDecorations
|
|
280
306
|
|
|
281
|
-
rerenderComponent({ node, decorations, innerDecorations })
|
|
307
|
+
rerenderComponent({ node, decorations, innerDecorations, extension: this.extensionWithSyncedStorage })
|
|
282
308
|
|
|
283
309
|
return true
|
|
284
310
|
}
|
package/src/Tiptap.tsx
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { createContext, useContext, useMemo } from 'react'
|
|
3
|
+
|
|
4
|
+
import { EditorContext } from './Context.js'
|
|
5
|
+
import type { Editor, EditorContentProps, EditorStateSnapshot } from './index.js'
|
|
6
|
+
import { EditorContent, useEditorState } from './index.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The shape of the React context used by the `<Tiptap />` components.
|
|
10
|
+
*
|
|
11
|
+
* The editor instance is always available when using the default `useEditor`
|
|
12
|
+
* configuration. For SSR scenarios where `immediatelyRender: false` is used,
|
|
13
|
+
* consider using the legacy `EditorProvider` pattern instead.
|
|
14
|
+
*/
|
|
15
|
+
export type TiptapContextType = {
|
|
16
|
+
/** The Tiptap editor instance. */
|
|
17
|
+
editor: Editor
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* React context that stores the current editor instance.
|
|
22
|
+
*
|
|
23
|
+
* Use `useTiptap()` to read from this context in child components.
|
|
24
|
+
*/
|
|
25
|
+
export const TiptapContext = createContext<TiptapContextType>({
|
|
26
|
+
get editor(): Editor {
|
|
27
|
+
throw new Error('useTiptap must be used within a <Tiptap> provider')
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
TiptapContext.displayName = 'TiptapContext'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Hook to read the Tiptap context and access the editor instance.
|
|
35
|
+
*
|
|
36
|
+
* This is a small convenience wrapper around `useContext(TiptapContext)`.
|
|
37
|
+
* The editor is always available when used within a `<Tiptap>` provider.
|
|
38
|
+
*
|
|
39
|
+
* @returns The current `TiptapContextType` value from the provider.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* import { useTiptap } from '@tiptap/react'
|
|
44
|
+
*
|
|
45
|
+
* function Toolbar() {
|
|
46
|
+
* const { editor } = useTiptap()
|
|
47
|
+
*
|
|
48
|
+
* return (
|
|
49
|
+
* <button onClick={() => editor.chain().focus().toggleBold().run()}>
|
|
50
|
+
* Bold
|
|
51
|
+
* </button>
|
|
52
|
+
* )
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export const useTiptap = () => useContext(TiptapContext)
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Select a slice of the editor state using the context-provided editor.
|
|
60
|
+
*
|
|
61
|
+
* This is a thin wrapper around `useEditorState` that reads the `editor`
|
|
62
|
+
* instance from `useTiptap()` so callers don't have to pass it manually.
|
|
63
|
+
*
|
|
64
|
+
* @typeParam TSelectorResult - The type returned by the selector.
|
|
65
|
+
* @param selector - Function that receives the editor state snapshot and
|
|
66
|
+
* returns the piece of state you want to subscribe to.
|
|
67
|
+
* @param equalityFn - Optional function to compare previous/next selected
|
|
68
|
+
* values and avoid unnecessary updates.
|
|
69
|
+
* @returns The selected slice of the editor state.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```tsx
|
|
73
|
+
* function WordCount() {
|
|
74
|
+
* const wordCount = useTiptapState(state => {
|
|
75
|
+
* const text = state.editor.state.doc.textContent
|
|
76
|
+
* return text.split(/\s+/).filter(Boolean).length
|
|
77
|
+
* })
|
|
78
|
+
*
|
|
79
|
+
* return <span>{wordCount} words</span>
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function useTiptapState<TSelectorResult>(
|
|
84
|
+
selector: (context: EditorStateSnapshot<Editor>) => TSelectorResult,
|
|
85
|
+
equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean,
|
|
86
|
+
) {
|
|
87
|
+
const { editor } = useTiptap()
|
|
88
|
+
|
|
89
|
+
return useEditorState({
|
|
90
|
+
editor,
|
|
91
|
+
selector,
|
|
92
|
+
equalityFn,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Props for the `Tiptap` root/provider component.
|
|
98
|
+
*/
|
|
99
|
+
export type TiptapWrapperProps = {
|
|
100
|
+
/**
|
|
101
|
+
* The editor instance to provide to child components.
|
|
102
|
+
* Use `useEditor()` to create this instance.
|
|
103
|
+
*/
|
|
104
|
+
editor?: Editor
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @deprecated Use `editor` instead. Will be removed in the next major version.
|
|
108
|
+
*/
|
|
109
|
+
instance?: Editor
|
|
110
|
+
|
|
111
|
+
children: ReactNode
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Top-level provider component that makes the editor instance available via
|
|
116
|
+
* React context to all child components.
|
|
117
|
+
*
|
|
118
|
+
* This component also provides backwards compatibility with the legacy
|
|
119
|
+
* `EditorContext`, so components using `useCurrentEditor()` will work
|
|
120
|
+
* inside a `<Tiptap>` provider.
|
|
121
|
+
*
|
|
122
|
+
* @param props - Component props.
|
|
123
|
+
* @returns A context provider element wrapping `children`.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```tsx
|
|
127
|
+
* import { Tiptap, useEditor } from '@tiptap/react'
|
|
128
|
+
*
|
|
129
|
+
* function App() {
|
|
130
|
+
* const editor = useEditor({ extensions: [...] })
|
|
131
|
+
*
|
|
132
|
+
* return (
|
|
133
|
+
* <Tiptap editor={editor}>
|
|
134
|
+
* <Toolbar />
|
|
135
|
+
* <Tiptap.Content />
|
|
136
|
+
* </Tiptap>
|
|
137
|
+
* )
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function TiptapWrapper({ editor, instance, children }: TiptapWrapperProps) {
|
|
142
|
+
const resolvedEditor = editor ?? instance
|
|
143
|
+
|
|
144
|
+
if (!resolvedEditor) {
|
|
145
|
+
throw new Error('Tiptap: An editor instance is required. Pass a non-null `editor` prop.')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const tiptapContextValue = useMemo<TiptapContextType>(() => ({ editor: resolvedEditor }), [resolvedEditor])
|
|
149
|
+
|
|
150
|
+
// Provide backwards compatibility with the legacy EditorContext
|
|
151
|
+
// so components using useCurrentEditor() work inside <Tiptap>
|
|
152
|
+
const legacyContextValue = useMemo(() => ({ editor: resolvedEditor }), [resolvedEditor])
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<EditorContext.Provider value={legacyContextValue}>
|
|
156
|
+
<TiptapContext.Provider value={tiptapContextValue}>{children}</TiptapContext.Provider>
|
|
157
|
+
</EditorContext.Provider>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
TiptapWrapper.displayName = 'Tiptap'
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Convenience component that renders `EditorContent` using the context-provided
|
|
165
|
+
* editor instance. Use this instead of manually passing the `editor` prop.
|
|
166
|
+
*
|
|
167
|
+
* @param props - All `EditorContent` props except `editor` and `ref`.
|
|
168
|
+
* @returns An `EditorContent` element bound to the context editor.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```tsx
|
|
172
|
+
* // inside a Tiptap provider
|
|
173
|
+
* <Tiptap.Content className="editor" />
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export function TiptapContent({ ...rest }: Omit<EditorContentProps, 'editor' | 'ref'>) {
|
|
177
|
+
const { editor } = useTiptap()
|
|
178
|
+
|
|
179
|
+
return <EditorContent editor={editor} {...rest} />
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
TiptapContent.displayName = 'Tiptap.Content'
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Root `Tiptap` component. Use it as the provider for all child components.
|
|
186
|
+
*
|
|
187
|
+
* The exported object includes the `Content` subcomponent for rendering the
|
|
188
|
+
* editor content area.
|
|
189
|
+
*
|
|
190
|
+
* This component provides both the new `TiptapContext` (accessed via `useTiptap()`)
|
|
191
|
+
* and the legacy `EditorContext` (accessed via `useCurrentEditor()`) for
|
|
192
|
+
* backwards compatibility.
|
|
193
|
+
*
|
|
194
|
+
* For bubble menus and floating menus, import them separately from
|
|
195
|
+
* `@tiptap/react/menus` to keep floating-ui as an optional dependency.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```tsx
|
|
199
|
+
* import { Tiptap, useEditor } from '@tiptap/react'
|
|
200
|
+
* import { BubbleMenu } from '@tiptap/react/menus'
|
|
201
|
+
*
|
|
202
|
+
* function App() {
|
|
203
|
+
* const editor = useEditor({ extensions: [...] })
|
|
204
|
+
*
|
|
205
|
+
* return (
|
|
206
|
+
* <Tiptap editor={editor}>
|
|
207
|
+
* <Tiptap.Content />
|
|
208
|
+
* <BubbleMenu>
|
|
209
|
+
* <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button>
|
|
210
|
+
* </BubbleMenu>
|
|
211
|
+
* </Tiptap>
|
|
212
|
+
* )
|
|
213
|
+
* }
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export const Tiptap = Object.assign(TiptapWrapper, {
|
|
217
|
+
/**
|
|
218
|
+
* The Tiptap Content component that renders the EditorContent with the editor instance from the context.
|
|
219
|
+
* @see TiptapContent
|
|
220
|
+
*/
|
|
221
|
+
Content: TiptapContent,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
export default Tiptap
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './NodeViewWrapper.js'
|
|
|
5
5
|
export * from './ReactMarkViewRenderer.js'
|
|
6
6
|
export * from './ReactNodeViewRenderer.js'
|
|
7
7
|
export * from './ReactRenderer.js'
|
|
8
|
+
export * from './Tiptap.js'
|
|
8
9
|
export * from './types.js'
|
|
9
10
|
export * from './useEditor.js'
|
|
10
11
|
export * from './useEditorState.js'
|
package/src/menus/BubbleMenu.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type BubbleMenuPluginProps, BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'
|
|
2
2
|
import { useCurrentEditor } from '@tiptap/react'
|
|
3
|
-
import React, { useEffect, useRef } from 'react'
|
|
3
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
4
4
|
import { createPortal } from 'react-dom'
|
|
5
5
|
|
|
6
6
|
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
|
|
@@ -58,6 +58,18 @@ export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
|
|
|
58
58
|
const bubbleMenuPluginPropsRef = useRef(bubbleMenuPluginProps)
|
|
59
59
|
bubbleMenuPluginPropsRef.current = bubbleMenuPluginProps
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Track whether the plugin has been initialized, so we only send updates
|
|
63
|
+
* after the initial registration.
|
|
64
|
+
*/
|
|
65
|
+
const [pluginInitialized, setPluginInitialized] = useState(false)
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Track whether we need to skip the first options update dispatch.
|
|
69
|
+
* This prevents unnecessary updates right after plugin initialization.
|
|
70
|
+
*/
|
|
71
|
+
const skipFirstUpdateRef = useRef(true)
|
|
72
|
+
|
|
61
73
|
useEffect(() => {
|
|
62
74
|
if (pluginEditor?.isDestroyed) {
|
|
63
75
|
return
|
|
@@ -82,7 +94,11 @@ export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
|
|
|
82
94
|
|
|
83
95
|
const createdPluginKey = bubbleMenuPluginPropsRef.current.pluginKey
|
|
84
96
|
|
|
97
|
+
skipFirstUpdateRef.current = true
|
|
98
|
+
setPluginInitialized(true)
|
|
99
|
+
|
|
85
100
|
return () => {
|
|
101
|
+
setPluginInitialized(false)
|
|
86
102
|
pluginEditor.unregisterPlugin(createdPluginKey)
|
|
87
103
|
window.requestAnimationFrame(() => {
|
|
88
104
|
if (bubbleMenuElement.parentNode) {
|
|
@@ -92,6 +108,38 @@ export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
|
|
|
92
108
|
}
|
|
93
109
|
}, [pluginEditor])
|
|
94
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Update the plugin options when props change after the plugin has been initialized.
|
|
113
|
+
* This allows dynamic updates to options like scrollTarget without re-registering the entire plugin.
|
|
114
|
+
*/
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!pluginInitialized || !pluginEditor || pluginEditor.isDestroyed) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Skip the first update right after initialization since the plugin was just created with these options
|
|
121
|
+
if (skipFirstUpdateRef.current) {
|
|
122
|
+
skipFirstUpdateRef.current = false
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
pluginEditor.view.dispatch(
|
|
127
|
+
pluginEditor.state.tr.setMeta('bubbleMenu', {
|
|
128
|
+
type: 'updateOptions',
|
|
129
|
+
options: bubbleMenuPluginPropsRef.current,
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
}, [
|
|
133
|
+
pluginInitialized,
|
|
134
|
+
pluginEditor,
|
|
135
|
+
updateDelay,
|
|
136
|
+
resizeDelay,
|
|
137
|
+
shouldShow,
|
|
138
|
+
options,
|
|
139
|
+
appendTo,
|
|
140
|
+
getReferencedVirtualElement,
|
|
141
|
+
])
|
|
142
|
+
|
|
95
143
|
return createPortal(<div {...restProps}>{children}</div>, menuEl.current)
|
|
96
144
|
},
|
|
97
145
|
)
|