@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.
@@ -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",
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.4",
42
- "@tiptap/pm": "^3.0.0-next.4",
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.1",
50
- "@tiptap/pm": "^3.0.0-next.1",
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
  },
@@ -175,10 +175,11 @@ export class PureEditorContent extends React.Component<
175
175
 
176
176
  editor.contentComponent = null
177
177
 
178
- if (!editor.options.element.firstChild) {
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
 
@@ -0,0 +1,2 @@
1
+ export * from './BubbleMenu.js'
2
+ export * from './FloatingMenu.js'
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
- // we can fast-path to update the editor options on the existing instance
195
- this.editor.setOptions({
196
- ...this.options.current,
197
- editable: this.editor.isEditable,
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