@tiptap/react 3.17.0 → 3.18.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.
@@ -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
- const floatingMenuElement = menuEl.current;
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
- const attachToEditor = editor || currentEditor;
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
- editor: attachToEditor,
111
- element: floatingMenuElement,
112
- pluginKey,
113
- updateDelay,
114
- resizeDelay,
115
- appendTo,
116
- shouldShow,
117
- options
151
+ ...floatingMenuPluginPropsRef.current,
152
+ editor: pluginEditor,
153
+ element: floatingMenuElement
118
154
  });
119
- attachToEditor.registerPlugin(plugin);
155
+ pluginEditor.registerPlugin(plugin);
156
+ const createdPluginKey = floatingMenuPluginPropsRef.current.pluginKey;
157
+ skipFirstUpdateRef.current = true;
158
+ setPluginInitialized(true);
120
159
  return () => {
121
- attachToEditor.unregisterPlugin(pluginKey);
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
- }, [editor, currentEditor, appendTo, pluginKey, shouldShow, options, updateDelay, resizeDelay]);
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
  );
@@ -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":";AAAA,SAAqC,wBAAwB;AAC7D,SAAS,wBAAwB;AACjC,OAAO,SAAS,WAAW,QAAQ,gBAAgB;AACnD,SAAS,oBAAoB;AA2IL;AApIjB,IAAM,aAAa,MAAM;AAAA,EAC9B,CACE;AAAA,IACE,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,GACA,QACG;AACH,UAAM,SAAS,OAAO,SAAS,cAAc,KAAK,CAAC;AAEnD,QAAI,OAAO,QAAQ,YAAY;AAC7B,UAAI,OAAO,OAAO;AAAA,IACpB,WAAW,KAAK;AACd,UAAI,UAAU,OAAO;AAAA,IACvB;AAEA,UAAM,EAAE,QAAQ,cAAc,IAAI,iBAAiB;AAKnD,UAAM,eAAe,UAAU;AAI/B,UAAM,wBAA2E;AAAA,MAC/E;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAMA,UAAM,2BAA2B,OAAO,qBAAqB;AAC7D,6BAAyB,UAAU;AAMnC,UAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAS,KAAK;AAMhE,UAAM,qBAAqB,OAAO,IAAI;AAEtC,cAAU,MAAM;AACd,UAAI,6CAAc,aAAa;AAC7B;AAAA,MACF;AAEA,UAAI,CAAC,cAAc;AACjB,gBAAQ,KAAK,kGAAkG;AAC/G;AAAA,MACF;AAEA,YAAM,oBAAoB,OAAO;AACjC,wBAAkB,MAAM,aAAa;AACrC,wBAAkB,MAAM,WAAW;AAEnC,YAAM,SAAS,iBAAiB;AAAA,QAC9B,GAAG,yBAAyB;AAAA,QAC5B,QAAQ;AAAA,QACR,SAAS;AAAA,MACX,CAAC;AAED,mBAAa,eAAe,MAAM;AAElC,YAAM,mBAAmB,yBAAyB,QAAQ;AAE1D,yBAAmB,UAAU;AAC7B,2BAAqB,IAAI;AAEzB,aAAO,MAAM;AACX,6BAAqB,KAAK;AAC1B,qBAAa,iBAAiB,gBAAgB;AAC9C,eAAO,sBAAsB,MAAM;AACjC,cAAI,kBAAkB,YAAY;AAChC,8BAAkB,WAAW,YAAY,iBAAiB;AAAA,UAC5D;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,GAAG,CAAC,YAAY,CAAC;AAMjB,cAAU,MAAM;AACd,UAAI,CAAC,qBAAqB,CAAC,gBAAgB,aAAa,aAAa;AACnE;AAAA,MACF;AAGA,UAAI,mBAAmB,SAAS;AAC9B,2BAAmB,UAAU;AAC7B;AAAA,MACF;AAEA,mBAAa,KAAK;AAAA,QAChB,aAAa,MAAM,GAAG,QAAQ,cAAc;AAAA,UAC1C,MAAM;AAAA,UACN,SAAS,yBAAyB;AAAA,QACpC,CAAC;AAAA,MACH;AAAA,IACF,GAAG;AAAA,MACD;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,WAAO,aAAa,oBAAC,SAAK,GAAG,WAAY,UAAS,GAAQ,OAAO,OAAO;AAAA,EAC1E;AACF;;;AC/IA,SAAS,0BAA0B;AACnC,SAAS,oBAAAA,yBAAwB;AACjC,OAAOC,UAAS,aAAAC,YAAW,UAAAC,SAAQ,YAAAC,iBAAgB;AACnD,SAAS,gBAAAC,qBAAoB;AAqIL,gBAAAC,YAAA;AA5HjB,IAAM,eAAeL,OAAM;AAAA,EAChC,CACE;AAAA,IACE,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,GACA,QACG;AACH,UAAM,SAASE,QAAO,SAAS,cAAc,KAAK,CAAC;AAEnD,QAAI,OAAO,QAAQ,YAAY;AAC7B,UAAI,OAAO,OAAO;AAAA,IACpB,WAAW,KAAK;AACd,UAAI,UAAU,OAAO;AAAA,IACvB;AAEA,UAAM,EAAE,QAAQ,cAAc,IAAIH,kBAAiB;AAKnD,UAAM,eAAe,UAAU;AAI/B,UAAM,0BAA+E;AAAA,MACnF;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAOA,UAAM,6BAA6BG,QAAO,uBAAuB;AACjE,+BAA2B,UAAU;AAMrC,UAAM,CAAC,mBAAmB,oBAAoB,IAAIC,UAAS,KAAK;AAMhE,UAAM,qBAAqBD,QAAO,IAAI;AAEtC,IAAAD,WAAU,MAAM;AACd,UAAI,6CAAc,aAAa;AAC7B;AAAA,MACF;AAEA,UAAI,CAAC,cAAc;AACjB,gBAAQ;AAAA,UACN;AAAA,QACF;AACA;AAAA,MACF;AAEA,YAAM,sBAAsB,OAAO;AACnC,0BAAoB,MAAM,aAAa;AACvC,0BAAoB,MAAM,WAAW;AAErC,YAAM,SAAS,mBAAmB;AAAA,QAChC,GAAG,2BAA2B;AAAA,QAC9B,QAAQ;AAAA,QACR,SAAS;AAAA,MACX,CAAC;AAED,mBAAa,eAAe,MAAM;AAElC,YAAM,mBAAmB,2BAA2B,QAAQ;AAE5D,yBAAmB,UAAU;AAC7B,2BAAqB,IAAI;AAEzB,aAAO,MAAM;AACX,6BAAqB,KAAK;AAC1B,qBAAa,iBAAiB,gBAAgB;AAC9C,eAAO,sBAAsB,MAAM;AACjC,cAAI,oBAAoB,YAAY;AAClC,gCAAoB,WAAW,YAAY,mBAAmB;AAAA,UAChE;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,GAAG,CAAC,YAAY,CAAC;AAMjB,IAAAA,WAAU,MAAM;AACd,UAAI,CAAC,qBAAqB,CAAC,gBAAgB,aAAa,aAAa;AACnE;AAAA,MACF;AAGA,UAAI,mBAAmB,SAAS;AAC9B,2BAAmB,UAAU;AAC7B;AAAA,MACF;AAEA,mBAAa,KAAK;AAAA,QAChB,aAAa,MAAM,GAAG,QAAQ,gBAAgB;AAAA,UAC5C,MAAM;AAAA,UACN,SAAS,2BAA2B;AAAA,QACtC,CAAC;AAAA,MACH;AAAA,IACF,GAAG,CAAC,mBAAmB,cAAc,aAAa,aAAa,YAAY,SAAS,QAAQ,CAAC;AAE7F,WAAOG,cAAa,gBAAAC,KAAC,SAAK,GAAG,WAAY,UAAS,GAAQ,OAAO,OAAO;AAAA,EAC1E;AACF;","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.17.0",
4
+ "version": "3.18.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.17.0",
52
- "@tiptap/pm": "^3.17.0"
51
+ "@tiptap/core": "^3.18.0",
52
+ "@tiptap/pm": "^3.18.0"
53
53
  },
54
54
  "optionalDependencies": {
55
- "@tiptap/extension-bubble-menu": "^3.17.0",
56
- "@tiptap/extension-floating-menu": "^3.17.0"
55
+ "@tiptap/extension-bubble-menu": "^3.18.0",
56
+ "@tiptap/extension-floating-menu": "^3.18.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/core": "^3.17.0",
64
- "@tiptap/pm": "^3.17.0"
63
+ "@tiptap/core": "^3.18.0",
64
+ "@tiptap/pm": "^3.18.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.extension,
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: () => rerenderComponent({ node, decorations, innerDecorations }),
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,355 @@
1
+ import type { ReactNode } from 'react'
2
+ import { createContext, useContext, useEffect, useMemo, useState } 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
+ import { type BubbleMenuProps, BubbleMenu } from './menus/BubbleMenu.js'
8
+ import { type FloatingMenuProps, FloatingMenu } from './menus/FloatingMenu.js'
9
+
10
+ /**
11
+ * The shape of the React context used by the `<Tiptap />` components.
12
+ *
13
+ * This object exposes the editor instance and a simple readiness flag.
14
+ */
15
+ export type TiptapContextType = {
16
+ /** The Tiptap editor instance. May be null during SSR or before initialization. */
17
+ editor: Editor | null
18
+
19
+ /** True when the editor has finished initializing and is ready for user interaction. */
20
+ isReady: boolean
21
+ }
22
+
23
+ /**
24
+ * React context that stores the current editor instance and readiness flag.
25
+ *
26
+ * Use `useTiptap()` to read from this context in child components.
27
+ */
28
+ export const TiptapContext = createContext<TiptapContextType>({
29
+ editor: null,
30
+ isReady: false,
31
+ })
32
+
33
+ TiptapContext.displayName = 'TiptapContext'
34
+
35
+ /**
36
+ * Hook to read the Tiptap context (`editor` + `isReady`).
37
+ *
38
+ * This is a small convenience wrapper around `useContext(TiptapContext)`.
39
+ *
40
+ * @returns The current `TiptapContextType` value from the provider.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * import { useTiptap } from '@tiptap/react'
45
+ *
46
+ * function Status() {
47
+ * const { isReady } = useTiptap()
48
+ * return <div>{isReady ? 'Editor ready' : 'Loading editor...'}</div>
49
+ * }
50
+ * ```
51
+ */
52
+ export const useTiptap = () => useContext(TiptapContext)
53
+
54
+ /**
55
+ * Select a slice of the editor state using the context-provided editor.
56
+ *
57
+ * This is a thin wrapper around `useEditorState` that reads the `editor`
58
+ * instance from `useTiptap()` so callers don't have to pass it manually.
59
+ *
60
+ * Important: This hook should only be used when the editor is available.
61
+ * Use the `isReady` flag from `useTiptap()` to guard against null editor,
62
+ * or ensure your component only renders after the editor is initialized.
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 { isReady } = useTiptap()
75
+ *
76
+ * // Only use useTiptapState when the editor is ready
77
+ * const wordCount = useTiptapState(state => {
78
+ * const text = state.editor.state.doc.textContent
79
+ * return text.split(/\s+/).filter(Boolean).length
80
+ * })
81
+ *
82
+ * if (!isReady) return null
83
+ *
84
+ * return <span>{wordCount} words</span>
85
+ * }
86
+ * ```
87
+ */
88
+ export function useTiptapState<TSelectorResult>(
89
+ selector: (context: EditorStateSnapshot<Editor>) => TSelectorResult,
90
+ equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean,
91
+ ) {
92
+ const { editor } = useTiptap()
93
+ return useEditorState({
94
+ editor: editor as Editor,
95
+ selector,
96
+ equalityFn,
97
+ })
98
+ }
99
+
100
+ /**
101
+ * Props for the `Tiptap` root/provider component.
102
+ */
103
+ export type TiptapWrapperProps = {
104
+ /**
105
+ * The editor instance to provide to child components.
106
+ * Can be null during SSR or before initialization.
107
+ */
108
+ instance: Editor | null
109
+ children: ReactNode
110
+ }
111
+
112
+ /**
113
+ * Top-level provider component that makes the editor instance available via
114
+ * React context and tracks when the editor becomes ready.
115
+ *
116
+ * The component listens to the editor's `create` event and flips the
117
+ * `isReady` flag once initialization completes.
118
+ *
119
+ * This component also provides backwards compatibility with the legacy
120
+ * `EditorContext`, so components using `useCurrentEditor()` will work
121
+ * inside a `<Tiptap>` provider.
122
+ *
123
+ * @param props - Component props.
124
+ * @returns A context provider element wrapping `children`.
125
+ *
126
+ * @example
127
+ * ```tsx
128
+ * import { Tiptap, useEditor } from '@tiptap/react'
129
+ *
130
+ * function App() {
131
+ * const editor = useEditor({ extensions: [...] })
132
+ *
133
+ * return (
134
+ * <Tiptap instance={editor}>
135
+ * <Toolbar />
136
+ * <Tiptap.Content />
137
+ * </Tiptap>
138
+ * )
139
+ * }
140
+ * ```
141
+ */
142
+ export function TiptapWrapper({ instance, children }: TiptapWrapperProps) {
143
+ const [isReady, setIsReady] = useState(instance?.isInitialized ?? false)
144
+
145
+ useEffect(() => {
146
+ if (!instance) {
147
+ setIsReady(false)
148
+ return
149
+ }
150
+
151
+ // If the editor is already initialized, set isReady to true
152
+ if (instance.isInitialized) {
153
+ setIsReady(true)
154
+ return
155
+ }
156
+
157
+ const handleCreate = () => {
158
+ setIsReady(true)
159
+ }
160
+
161
+ instance.on('create', handleCreate)
162
+
163
+ return () => {
164
+ instance.off('create', handleCreate)
165
+ }
166
+ }, [instance])
167
+
168
+ // Memoize context values to prevent unnecessary re-renders
169
+ const tiptapContextValue = useMemo<TiptapContextType>(() => ({ editor: instance, isReady }), [instance, isReady])
170
+
171
+ // Provide backwards compatibility with the legacy EditorContext
172
+ // so components using useCurrentEditor() work inside <Tiptap>
173
+ const legacyContextValue = useMemo(() => ({ editor: instance }), [instance])
174
+
175
+ return (
176
+ <EditorContext.Provider value={legacyContextValue}>
177
+ <TiptapContext.Provider value={tiptapContextValue}>{children}</TiptapContext.Provider>
178
+ </EditorContext.Provider>
179
+ )
180
+ }
181
+
182
+ TiptapWrapper.displayName = 'Tiptap'
183
+
184
+ /**
185
+ * Convenience component that renders `EditorContent` using the context-provided
186
+ * editor instance. Use this instead of manually passing the `editor` prop.
187
+ *
188
+ * @param props - All `EditorContent` props except `editor` and `ref`.
189
+ * @returns An `EditorContent` element bound to the context editor.
190
+ *
191
+ * @example
192
+ * ```tsx
193
+ * // inside a Tiptap provider
194
+ * <Tiptap.Content className="editor" />
195
+ * ```
196
+ */
197
+ export function TiptapContent({ ...rest }: Omit<EditorContentProps, 'editor' | 'ref'>) {
198
+ const { editor } = useTiptap()
199
+
200
+ return <EditorContent editor={editor} {...rest} />
201
+ }
202
+
203
+ TiptapContent.displayName = 'Tiptap.Content'
204
+
205
+ export type TiptapLoadingProps = {
206
+ children: ReactNode
207
+ }
208
+
209
+ /**
210
+ * Component that renders its children only when the editor is not ready.
211
+ *
212
+ * This is useful for displaying loading states or placeholders during
213
+ * editor initialization, especially with SSR.
214
+ *
215
+ * @param props - The props for the TiptapLoading component.
216
+ * @returns The children when editor is not ready, or null when ready.
217
+ *
218
+ * @example
219
+ * ```tsx
220
+ * <Tiptap instance={editor}>
221
+ * <Tiptap.Loading>
222
+ * <div className="skeleton">Loading editor...</div>
223
+ * </Tiptap.Loading>
224
+ * <Tiptap.Content />
225
+ * </Tiptap>
226
+ * ```
227
+ */
228
+ export function TiptapLoading({ children }: TiptapLoadingProps) {
229
+ const { isReady } = useTiptap()
230
+
231
+ if (isReady) {
232
+ return null
233
+ }
234
+
235
+ return children
236
+ }
237
+
238
+ TiptapLoading.displayName = 'Tiptap.Loading'
239
+
240
+ /**
241
+ * A wrapper around the library `BubbleMenu` that injects the editor from
242
+ * context so callers don't need to pass the `editor` prop.
243
+ *
244
+ * Returns `null` when the editor is not available (for example during SSR).
245
+ *
246
+ * @param props - Props for the underlying `BubbleMenu` (except `editor`).
247
+ * @returns A `BubbleMenu` bound to the context editor, or `null`.
248
+ *
249
+ * @example
250
+ * ```tsx
251
+ * <Tiptap.BubbleMenu tippyOptions={{ duration: 100 }}>
252
+ * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button>
253
+ * </Tiptap.BubbleMenu>
254
+ * ```
255
+ */
256
+ export function TiptapBubbleMenu({ children, ...rest }: { children: ReactNode } & Omit<BubbleMenuProps, 'editor'>) {
257
+ const { editor } = useTiptap()
258
+
259
+ if (!editor) {
260
+ return null
261
+ }
262
+
263
+ return (
264
+ <BubbleMenu editor={editor} {...rest}>
265
+ {children}
266
+ </BubbleMenu>
267
+ )
268
+ }
269
+
270
+ TiptapBubbleMenu.displayName = 'Tiptap.BubbleMenu'
271
+
272
+ /**
273
+ * A wrapper around the library `FloatingMenu` that injects the editor from
274
+ * context so callers don't need to pass the `editor` prop.
275
+ *
276
+ * Returns `null` when the editor is not available.
277
+ *
278
+ * @param props - Props for the underlying `FloatingMenu` (except `editor`).
279
+ * @returns A `FloatingMenu` bound to the context editor, or `null`.
280
+ *
281
+ * @example
282
+ * ```tsx
283
+ * <Tiptap.FloatingMenu placement="top">
284
+ * <button onClick={() => editor.chain().focus().toggleItalic().run()}>Italic</button>
285
+ * </Tiptap.FloatingMenu>
286
+ * ```
287
+ */
288
+ export function TiptapFloatingMenu({ children, ...rest }: { children: ReactNode } & Omit<FloatingMenuProps, 'editor'>) {
289
+ const { editor } = useTiptap()
290
+
291
+ if (!editor) {
292
+ return null
293
+ }
294
+
295
+ return (
296
+ <FloatingMenu {...rest} editor={editor}>
297
+ {children}
298
+ </FloatingMenu>
299
+ )
300
+ }
301
+
302
+ TiptapFloatingMenu.displayName = 'Tiptap.FloatingMenu'
303
+
304
+ /**
305
+ * Root `Tiptap` component. Use it as the provider for all child components.
306
+ *
307
+ * The exported object includes several helper subcomponents for common use
308
+ * cases: `Content`, `Loading`, `BubbleMenu`, and `FloatingMenu`.
309
+ *
310
+ * This component provides both the new `TiptapContext` (accessed via `useTiptap()`)
311
+ * and the legacy `EditorContext` (accessed via `useCurrentEditor()`) for
312
+ * backwards compatibility.
313
+ *
314
+ * @example
315
+ * ```tsx
316
+ * const editor = useEditor({ extensions: [...] })
317
+ *
318
+ * return (
319
+ * <Tiptap instance={editor}>
320
+ * <Tiptap.Loading>Initializing editor...</Tiptap.Loading>
321
+ * <Tiptap.Content />
322
+ * <Tiptap.BubbleMenu>
323
+ * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button>
324
+ * </Tiptap.BubbleMenu>
325
+ * </Tiptap>
326
+ * )
327
+ * ```
328
+ */
329
+ export const Tiptap = Object.assign(TiptapWrapper, {
330
+ /**
331
+ * The Tiptap Content component that renders the EditorContent with the editor instance from the context.
332
+ * @see TiptapContent
333
+ */
334
+ Content: TiptapContent,
335
+
336
+ /**
337
+ * The Tiptap Loading component that renders its children only when the editor is not ready.
338
+ * @see TiptapLoading
339
+ */
340
+ Loading: TiptapLoading,
341
+
342
+ /**
343
+ * The Tiptap BubbleMenu component that wraps the BubbleMenu from Tiptap and provides the editor instance from the context.
344
+ * @see TiptapBubbleMenu
345
+ */
346
+ BubbleMenu: TiptapBubbleMenu,
347
+
348
+ /**
349
+ * The Tiptap FloatingMenu component that wraps the FloatingMenu from Tiptap and provides the editor instance from the context.
350
+ * @see TiptapFloatingMenu
351
+ */
352
+ FloatingMenu: TiptapFloatingMenu,
353
+ })
354
+
355
+ 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'