@tiptap/react 3.0.0-next.4 → 3.0.0-next.6
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/LICENSE.md +1 -1
- package/dist/index.cjs +178 -160
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -22
- package/dist/index.d.ts +35 -22
- package/dist/index.js +147 -131
- package/dist/index.js.map +1 -1
- package/dist/menus/index.cjs +142 -0
- package/dist/menus/index.cjs.map +1 -0
- package/dist/menus/index.d.cts +19 -0
- package/dist/menus/index.d.ts +19 -0
- package/dist/menus/index.js +104 -0
- package/dist/menus/index.js.map +1 -0
- package/package.json +17 -7
- package/src/EditorContent.tsx +2 -1
- package/src/ReactMarkViewRenderer.tsx +108 -0
- package/src/ReactNodeViewRenderer.tsx +1 -1
- package/src/index.ts +1 -2
- package/src/{BubbleMenu.tsx → menus/BubbleMenu.tsx} +2 -3
- package/src/{FloatingMenu.tsx → menus/FloatingMenu.tsx} +2 -3
- package/src/menus/index.ts +2 -0
- package/src/useEditor.ts +51 -6
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/menus/BubbleMenu.tsx
|
|
2
|
+
import { BubbleMenuPlugin } from "@tiptap/extension-bubble-menu";
|
|
3
|
+
import { useCurrentEditor } from "@tiptap/react";
|
|
4
|
+
import React, { useEffect, useRef } from "react";
|
|
5
|
+
import { createPortal } from "react-dom";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
var BubbleMenu = React.forwardRef(
|
|
8
|
+
({ pluginKey = "bubbleMenu", editor, updateDelay, resizeDelay, shouldShow = null, options, children, ...restProps }, ref) => {
|
|
9
|
+
const menuEl = useRef(document.createElement("div"));
|
|
10
|
+
if (typeof ref === "function") {
|
|
11
|
+
ref(menuEl.current);
|
|
12
|
+
} else if (ref) {
|
|
13
|
+
ref.current = menuEl.current;
|
|
14
|
+
}
|
|
15
|
+
const { editor: currentEditor } = useCurrentEditor();
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const bubbleMenuElement = menuEl.current;
|
|
18
|
+
bubbleMenuElement.style.visibility = "hidden";
|
|
19
|
+
bubbleMenuElement.style.position = "absolute";
|
|
20
|
+
if ((editor == null ? void 0 : editor.isDestroyed) || (currentEditor == null ? void 0 : currentEditor.isDestroyed)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const attachToEditor = editor || currentEditor;
|
|
24
|
+
if (!attachToEditor) {
|
|
25
|
+
console.warn("BubbleMenu component is not rendered inside of an editor component or does not have editor prop.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const plugin = BubbleMenuPlugin({
|
|
29
|
+
updateDelay,
|
|
30
|
+
resizeDelay,
|
|
31
|
+
editor: attachToEditor,
|
|
32
|
+
element: bubbleMenuElement,
|
|
33
|
+
pluginKey,
|
|
34
|
+
shouldShow,
|
|
35
|
+
options
|
|
36
|
+
});
|
|
37
|
+
attachToEditor.registerPlugin(plugin);
|
|
38
|
+
return () => {
|
|
39
|
+
attachToEditor.unregisterPlugin(pluginKey);
|
|
40
|
+
window.requestAnimationFrame(() => {
|
|
41
|
+
if (bubbleMenuElement.parentNode) {
|
|
42
|
+
bubbleMenuElement.parentNode.removeChild(bubbleMenuElement);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
}, [editor, currentEditor]);
|
|
47
|
+
return createPortal(/* @__PURE__ */ jsx("div", { ...restProps, children }), menuEl.current);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// src/menus/FloatingMenu.tsx
|
|
52
|
+
import { FloatingMenuPlugin } from "@tiptap/extension-floating-menu";
|
|
53
|
+
import { useCurrentEditor as useCurrentEditor2 } from "@tiptap/react";
|
|
54
|
+
import React2, { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
55
|
+
import { createPortal as createPortal2 } from "react-dom";
|
|
56
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
57
|
+
var FloatingMenu = React2.forwardRef(
|
|
58
|
+
({ pluginKey = "floatingMenu", editor, shouldShow = null, options, children, ...restProps }, ref) => {
|
|
59
|
+
const menuEl = useRef2(document.createElement("div"));
|
|
60
|
+
if (typeof ref === "function") {
|
|
61
|
+
ref(menuEl.current);
|
|
62
|
+
} else if (ref) {
|
|
63
|
+
ref.current = menuEl.current;
|
|
64
|
+
}
|
|
65
|
+
const { editor: currentEditor } = useCurrentEditor2();
|
|
66
|
+
useEffect2(() => {
|
|
67
|
+
const floatingMenuElement = menuEl.current;
|
|
68
|
+
floatingMenuElement.style.visibility = "hidden";
|
|
69
|
+
floatingMenuElement.style.position = "absolute";
|
|
70
|
+
if ((editor == null ? void 0 : editor.isDestroyed) || (currentEditor == null ? void 0 : currentEditor.isDestroyed)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const attachToEditor = editor || currentEditor;
|
|
74
|
+
if (!attachToEditor) {
|
|
75
|
+
console.warn(
|
|
76
|
+
"FloatingMenu component is not rendered inside of an editor component or does not have editor prop."
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const plugin = FloatingMenuPlugin({
|
|
81
|
+
editor: attachToEditor,
|
|
82
|
+
element: floatingMenuElement,
|
|
83
|
+
pluginKey,
|
|
84
|
+
shouldShow,
|
|
85
|
+
options
|
|
86
|
+
});
|
|
87
|
+
attachToEditor.registerPlugin(plugin);
|
|
88
|
+
return () => {
|
|
89
|
+
attachToEditor.unregisterPlugin(pluginKey);
|
|
90
|
+
window.requestAnimationFrame(() => {
|
|
91
|
+
if (floatingMenuElement.parentNode) {
|
|
92
|
+
floatingMenuElement.parentNode.removeChild(floatingMenuElement);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
}, [editor, currentEditor]);
|
|
97
|
+
return createPortal2(/* @__PURE__ */ jsx2("div", { ...restProps, children }), menuEl.current);
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
export {
|
|
101
|
+
BubbleMenu,
|
|
102
|
+
FloatingMenu
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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 { pluginKey = 'bubbleMenu', editor, updateDelay, resizeDelay, shouldShow = null, options, children, ...restProps },\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 bubbleMenuElement = menuEl.current\n\n bubbleMenuElement.style.visibility = 'hidden'\n bubbleMenuElement.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('BubbleMenu component is not rendered inside of an editor component or does not have editor prop.')\n return\n }\n\n const plugin = BubbleMenuPlugin({\n updateDelay,\n resizeDelay,\n editor: attachToEditor,\n element: bubbleMenuElement,\n pluginKey,\n shouldShow,\n options,\n })\n\n attachToEditor.registerPlugin(plugin)\n\n return () => {\n attachToEditor.unregisterPlugin(pluginKey)\n window.requestAnimationFrame(() => {\n if (bubbleMenuElement.parentNode) {\n bubbleMenuElement.parentNode.removeChild(bubbleMenuElement)\n }\n })\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editor, currentEditor])\n\n return createPortal(<div {...restProps}>{children}</div>, menuEl.current)\n },\n)\n","import { FloatingMenuPlugin, FloatingMenuPluginProps } 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 ({ pluginKey = 'floatingMenu', editor, shouldShow = null, options, children, ...restProps }, ref) => {\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 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])\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"]}
|
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.0.0-next.
|
|
4
|
+
"version": "3.0.0-next.6",
|
|
5
5
|
"homepage": "https://tiptap.dev",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tiptap",
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
},
|
|
21
21
|
"import": "./dist/index.js",
|
|
22
22
|
"require": "./dist/index.cjs"
|
|
23
|
+
},
|
|
24
|
+
"./menus": {
|
|
25
|
+
"types": {
|
|
26
|
+
"import": "./dist/menus/index.d.ts",
|
|
27
|
+
"require": "./dist/menus/index.d.cts"
|
|
28
|
+
},
|
|
29
|
+
"import": "./dist/menus/index.js",
|
|
30
|
+
"require": "./dist/menus/index.cjs"
|
|
23
31
|
}
|
|
24
32
|
},
|
|
25
33
|
"main": "dist/index.cjs",
|
|
@@ -31,23 +39,25 @@
|
|
|
31
39
|
"dist"
|
|
32
40
|
],
|
|
33
41
|
"dependencies": {
|
|
34
|
-
"@tiptap/extension-bubble-menu": "^3.0.0-next.4",
|
|
35
|
-
"@tiptap/extension-floating-menu": "^3.0.0-next.4",
|
|
36
42
|
"@types/use-sync-external-store": "^0.0.6",
|
|
37
43
|
"fast-deep-equal": "^3.1.3",
|
|
38
44
|
"use-sync-external-store": "^1.4.0"
|
|
39
45
|
},
|
|
40
46
|
"devDependencies": {
|
|
41
|
-
"@tiptap/core": "^3.0.0-next.
|
|
42
|
-
"@tiptap/pm": "^3.0.0-next.
|
|
47
|
+
"@tiptap/core": "^3.0.0-next.6",
|
|
48
|
+
"@tiptap/pm": "^3.0.0-next.6",
|
|
43
49
|
"@types/react": "^18.3.18",
|
|
44
50
|
"@types/react-dom": "^18.3.5",
|
|
45
51
|
"react": "^18.3.1",
|
|
46
52
|
"react-dom": "^18.3.1"
|
|
47
53
|
},
|
|
54
|
+
"optionalDependencies": {
|
|
55
|
+
"@tiptap/extension-bubble-menu": "^3.0.0-next.6",
|
|
56
|
+
"@tiptap/extension-floating-menu": "^3.0.0-next.6"
|
|
57
|
+
},
|
|
48
58
|
"peerDependencies": {
|
|
49
|
-
"@tiptap/core": "^3.0.0-next.
|
|
50
|
-
"@tiptap/pm": "^3.0.0-next.
|
|
59
|
+
"@tiptap/core": "^3.0.0-next.4",
|
|
60
|
+
"@tiptap/pm": "^3.0.0-next.4",
|
|
51
61
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
52
62
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
53
63
|
},
|
package/src/EditorContent.tsx
CHANGED
|
@@ -175,10 +175,11 @@ export class PureEditorContent extends React.Component<
|
|
|
175
175
|
|
|
176
176
|
editor.contentComponent = null
|
|
177
177
|
|
|
178
|
-
if (!editor.options.element
|
|
178
|
+
if (!editor.options.element?.firstChild) {
|
|
179
179
|
return
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
// TODO using the new editor.mount method might allow us to remove this
|
|
182
183
|
const newElement = document.createElement('div')
|
|
183
184
|
|
|
184
185
|
newElement.append(...editor.options.element.childNodes)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-shadow */
|
|
2
|
+
import { MarkView, MarkViewProps, MarkViewRenderer, MarkViewRendererOptions } from '@tiptap/core'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
// import { flushSync } from 'react-dom'
|
|
6
|
+
import { ReactRenderer } from './ReactRenderer.js'
|
|
7
|
+
|
|
8
|
+
export interface MarkViewContextProps {
|
|
9
|
+
markViewContentRef: (element: HTMLElement | null) => void
|
|
10
|
+
}
|
|
11
|
+
export const ReactMarkViewContext = React.createContext<MarkViewContextProps>({
|
|
12
|
+
markViewContentRef: () => {
|
|
13
|
+
// do nothing
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export type MarkViewContentProps<T extends keyof React.JSX.IntrinsicElements = 'span'> = {
|
|
18
|
+
as?: NoInfer<T>
|
|
19
|
+
} & React.ComponentProps<T>
|
|
20
|
+
|
|
21
|
+
export const MarkViewContent: React.FC<MarkViewContentProps> = props => {
|
|
22
|
+
const Tag = props.as || 'span'
|
|
23
|
+
const { markViewContentRef } = React.useContext(ReactMarkViewContext)
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
// @ts-ignore
|
|
27
|
+
<Tag {...props} ref={markViewContentRef} data-mark-view-content="" />
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ReactMarkViewRendererOptions extends MarkViewRendererOptions {
|
|
32
|
+
/**
|
|
33
|
+
* The tag name of the element wrapping the React component.
|
|
34
|
+
*/
|
|
35
|
+
as?: string
|
|
36
|
+
className?: string
|
|
37
|
+
attrs?: { [key: string]: string }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ReactMarkView extends MarkView<React.ComponentType<MarkViewProps>, ReactMarkViewRendererOptions> {
|
|
41
|
+
renderer: ReactRenderer
|
|
42
|
+
contentDOMElement: HTMLElement | null
|
|
43
|
+
didMountContentDomElement = false
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
component: React.ComponentType<MarkViewProps>,
|
|
47
|
+
props: MarkViewProps,
|
|
48
|
+
options?: Partial<ReactMarkViewRendererOptions>,
|
|
49
|
+
) {
|
|
50
|
+
super(component, props, options)
|
|
51
|
+
|
|
52
|
+
const { as = 'span', attrs, className = '' } = options || {}
|
|
53
|
+
const componentProps = props satisfies MarkViewProps
|
|
54
|
+
|
|
55
|
+
this.contentDOMElement = document.createElement('span')
|
|
56
|
+
|
|
57
|
+
const markViewContentRef: MarkViewContextProps['markViewContentRef'] = el => {
|
|
58
|
+
if (el && this.contentDOMElement && el.firstChild !== this.contentDOMElement) {
|
|
59
|
+
el.appendChild(this.contentDOMElement)
|
|
60
|
+
this.didMountContentDomElement = true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const context: MarkViewContextProps = {
|
|
64
|
+
markViewContentRef,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// For performance reasons, we memoize the provider component
|
|
68
|
+
// And all of the things it requires are declared outside of the component, so it doesn't need to re-render
|
|
69
|
+
const ReactMarkViewProvider: React.FunctionComponent<MarkViewProps> = React.memo(componentProps => {
|
|
70
|
+
return (
|
|
71
|
+
<ReactMarkViewContext.Provider value={context}>
|
|
72
|
+
{React.createElement(component, componentProps)}
|
|
73
|
+
</ReactMarkViewContext.Provider>
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
ReactMarkViewProvider.displayName = 'ReactNodeView'
|
|
78
|
+
|
|
79
|
+
this.renderer = new ReactRenderer(ReactMarkViewProvider, {
|
|
80
|
+
editor: props.editor,
|
|
81
|
+
props: componentProps,
|
|
82
|
+
as,
|
|
83
|
+
className: `mark-${props.mark.type.name} ${className}`.trim(),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
if (attrs) {
|
|
87
|
+
this.renderer.updateAttributes(attrs)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get dom() {
|
|
92
|
+
return this.renderer.element as HTMLElement
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get contentDOM() {
|
|
96
|
+
if (!this.didMountContentDomElement) {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
return this.contentDOMElement as HTMLElement
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function ReactMarkViewRenderer(
|
|
104
|
+
component: React.ComponentType<MarkViewProps>,
|
|
105
|
+
options: Partial<ReactMarkViewRendererOptions> = {},
|
|
106
|
+
): MarkViewRenderer {
|
|
107
|
+
return props => new ReactMarkView(component, props, options)
|
|
108
|
+
}
|
|
@@ -136,7 +136,6 @@ export class ReactNodeView<
|
|
|
136
136
|
const { className = '' } = this.options
|
|
137
137
|
|
|
138
138
|
this.handleSelectionUpdate = this.handleSelectionUpdate.bind(this)
|
|
139
|
-
this.editor.on('selectionUpdate', this.handleSelectionUpdate)
|
|
140
139
|
|
|
141
140
|
this.renderer = new ReactRenderer(ReactNodeViewProvider, {
|
|
142
141
|
editor: this.editor,
|
|
@@ -145,6 +144,7 @@ export class ReactNodeView<
|
|
|
145
144
|
className: `node-${this.node.type.name} ${className}`.trim(),
|
|
146
145
|
})
|
|
147
146
|
|
|
147
|
+
this.editor.on('selectionUpdate', this.handleSelectionUpdate)
|
|
148
148
|
this.updateElementAttributes()
|
|
149
149
|
}
|
|
150
150
|
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
export * from './BubbleMenu.js'
|
|
2
1
|
export * from './Context.js'
|
|
3
2
|
export * from './EditorContent.js'
|
|
4
|
-
export * from './FloatingMenu.js'
|
|
5
3
|
export * from './NodeViewContent.js'
|
|
6
4
|
export * from './NodeViewWrapper.js'
|
|
5
|
+
export * from './ReactMarkViewRenderer.js'
|
|
7
6
|
export * from './ReactNodeViewRenderer.js'
|
|
8
7
|
export * from './ReactRenderer.js'
|
|
9
8
|
export * from './useEditor.js'
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { type BubbleMenuPluginProps, BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'
|
|
2
|
+
import { useCurrentEditor } from '@tiptap/react'
|
|
2
3
|
import React, { useEffect, useRef } from 'react'
|
|
3
4
|
import { createPortal } from 'react-dom'
|
|
4
5
|
|
|
5
|
-
import { useCurrentEditor } from './Context.js'
|
|
6
|
-
|
|
7
6
|
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
|
|
8
7
|
|
|
9
8
|
export type BubbleMenuProps = Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> &
|
|
@@ -30,7 +29,7 @@ export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
|
|
|
30
29
|
bubbleMenuElement.style.visibility = 'hidden'
|
|
31
30
|
bubbleMenuElement.style.position = 'absolute'
|
|
32
31
|
|
|
33
|
-
if (editor?.isDestroyed || currentEditor?.isDestroyed) {
|
|
32
|
+
if (editor?.isDestroyed || (currentEditor as any)?.isDestroyed) {
|
|
34
33
|
return
|
|
35
34
|
}
|
|
36
35
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { FloatingMenuPlugin, FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'
|
|
2
|
+
import { useCurrentEditor } from '@tiptap/react'
|
|
2
3
|
import React, { useEffect, useRef } from 'react'
|
|
3
4
|
import { createPortal } from 'react-dom'
|
|
4
5
|
|
|
5
|
-
import { useCurrentEditor } from './Context.js'
|
|
6
|
-
|
|
7
6
|
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
|
|
8
7
|
|
|
9
8
|
export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element' | 'editor'> & {
|
|
@@ -29,7 +28,7 @@ export const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(
|
|
|
29
28
|
floatingMenuElement.style.visibility = 'hidden'
|
|
30
29
|
floatingMenuElement.style.position = 'absolute'
|
|
31
30
|
|
|
32
|
-
if (editor?.isDestroyed || currentEditor?.isDestroyed) {
|
|
31
|
+
if (editor?.isDestroyed || (currentEditor as any)?.isDestroyed) {
|
|
33
32
|
return
|
|
34
33
|
}
|
|
35
34
|
|
package/src/useEditor.ts
CHANGED
|
@@ -144,6 +144,7 @@ class EditorInstanceManager {
|
|
|
144
144
|
onContentError: (...args) => this.options.current.onContentError?.(...args),
|
|
145
145
|
onDrop: (...args) => this.options.current.onDrop?.(...args),
|
|
146
146
|
onPaste: (...args) => this.options.current.onPaste?.(...args),
|
|
147
|
+
onDelete: (...args) => this.options.current.onDelete?.(...args),
|
|
147
148
|
}
|
|
148
149
|
const editor = new Editor(optionsToApply)
|
|
149
150
|
|
|
@@ -177,6 +178,47 @@ class EditorInstanceManager {
|
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
static compareOptions(a: UseEditorOptions, b: UseEditorOptions) {
|
|
182
|
+
return (Object.keys(a) as (keyof UseEditorOptions)[]).every(key => {
|
|
183
|
+
if (
|
|
184
|
+
[
|
|
185
|
+
'onCreate',
|
|
186
|
+
'onBeforeCreate',
|
|
187
|
+
'onDestroy',
|
|
188
|
+
'onUpdate',
|
|
189
|
+
'onTransaction',
|
|
190
|
+
'onFocus',
|
|
191
|
+
'onBlur',
|
|
192
|
+
'onSelectionUpdate',
|
|
193
|
+
'onContentError',
|
|
194
|
+
'onDrop',
|
|
195
|
+
'onPaste',
|
|
196
|
+
].includes(key)
|
|
197
|
+
) {
|
|
198
|
+
// we don't want to compare callbacks, they are always different and only registered once
|
|
199
|
+
return true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// We often encourage putting extensions inlined in the options object, so we will do a slightly deeper comparison here
|
|
203
|
+
if (key === 'extensions' && a.extensions && b.extensions) {
|
|
204
|
+
if (a.extensions.length !== b.extensions.length) {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
return a.extensions.every((extension, index) => {
|
|
208
|
+
if (extension !== b.extensions?.[index]) {
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
return true
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
if (a[key] !== b[key]) {
|
|
215
|
+
// if any of the options have changed, we should update the editor options
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
return true
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
180
222
|
/**
|
|
181
223
|
* On each render, we will create, update, or destroy the editor instance.
|
|
182
224
|
* @param deps The dependencies to watch for changes
|
|
@@ -190,12 +232,15 @@ class EditorInstanceManager {
|
|
|
190
232
|
clearTimeout(this.scheduledDestructionTimeout)
|
|
191
233
|
|
|
192
234
|
if (this.editor && !this.editor.isDestroyed && deps.length === 0) {
|
|
193
|
-
// if the editor does exist & deps are empty, we don't need to re-initialize the editor
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
235
|
+
// if the editor does exist & deps are empty, we don't need to re-initialize the editor generally
|
|
236
|
+
if (!EditorInstanceManager.compareOptions(this.options.current, this.editor.options)) {
|
|
237
|
+
// But, the options are different, so we need to update the editor options
|
|
238
|
+
// Still, this is faster than re-creating the editor
|
|
239
|
+
this.editor.setOptions({
|
|
240
|
+
...this.options.current,
|
|
241
|
+
editable: this.editor.isEditable,
|
|
242
|
+
})
|
|
243
|
+
}
|
|
199
244
|
} else {
|
|
200
245
|
// When the editor:
|
|
201
246
|
// - does not yet exist
|