@tiptap/react 3.0.0-next.3 → 3.0.0-next.5

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":";AAAA,SAAqC,wBAAwB;AAC7D,SAAS,wBAAwB;AACjC,OAAO,SAAS,WAAW,cAAc;AACzC,SAAS,oBAAoB;AA8DL;AAvDjB,IAAM,aAAa,MAAM;AAAA,EAC9B,CACE,EAAE,YAAY,cAAc,QAAQ,aAAa,aAAa,aAAa,MAAM,SAAS,UAAU,GAAG,UAAU,GACjH,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;AAEnD,cAAU,MAAM;AACd,YAAM,oBAAoB,OAAO;AAEjC,wBAAkB,MAAM,aAAa;AACrC,wBAAkB,MAAM,WAAW;AAEnC,WAAI,iCAAQ,iBAAgB,+CAAuB,cAAa;AAC9D;AAAA,MACF;AAEA,YAAM,iBAAiB,UAAU;AAEjC,UAAI,CAAC,gBAAgB;AACnB,gBAAQ,KAAK,kGAAkG;AAC/G;AAAA,MACF;AAEA,YAAM,SAAS,iBAAiB;AAAA,QAC9B;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR,SAAS;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAED,qBAAe,eAAe,MAAM;AAEpC,aAAO,MAAM;AACX,uBAAe,iBAAiB,SAAS;AACzC,eAAO,sBAAsB,MAAM;AACjC,cAAI,kBAAkB,YAAY;AAChC,8BAAkB,WAAW,YAAY,iBAAiB;AAAA,UAC5D;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IAEF,GAAG,CAAC,QAAQ,aAAa,CAAC;AAE1B,WAAO,aAAa,oBAAC,SAAK,GAAG,WAAY,UAAS,GAAQ,OAAO,OAAO;AAAA,EAC1E;AACF;;;ACnEA,SAAS,0BAAmD;AAC5D,SAAS,oBAAAA,yBAAwB;AACjC,OAAOC,UAAS,aAAAC,YAAW,UAAAC,eAAc;AACzC,SAAS,gBAAAC,qBAAoB;AA6DL,gBAAAC,YAAA;AApDjB,IAAM,eAAeJ,OAAM;AAAA,EAChC,CAAC,EAAE,YAAY,gBAAgB,QAAQ,aAAa,MAAM,SAAS,UAAU,GAAG,UAAU,GAAG,QAAQ;AACnG,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;AAEnD,IAAAE,WAAU,MAAM;AACd,YAAM,sBAAsB,OAAO;AAEnC,0BAAoB,MAAM,aAAa;AACvC,0BAAoB,MAAM,WAAW;AAErC,WAAI,iCAAQ,iBAAgB,+CAAuB,cAAa;AAC9D;AAAA,MACF;AAEA,YAAM,iBAAiB,UAAU;AAEjC,UAAI,CAAC,gBAAgB;AACnB,gBAAQ;AAAA,UACN;AAAA,QACF;AACA;AAAA,MACF;AAEA,YAAM,SAAS,mBAAmB;AAAA,QAChC,QAAQ;AAAA,QACR,SAAS;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAED,qBAAe,eAAe,MAAM;AAEpC,aAAO,MAAM;AACX,uBAAe,iBAAiB,SAAS;AACzC,eAAO,sBAAsB,MAAM;AACjC,cAAI,oBAAoB,YAAY;AAClC,gCAAoB,WAAW,YAAY,mBAAmB;AAAA,UAChE;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IAEF,GAAG,CAAC,QAAQ,aAAa,CAAC;AAE1B,WAAOE,cAAa,gBAAAC,KAAC,SAAK,GAAG,WAAY,UAAS,GAAQ,OAAO,OAAO;AAAA,EAC1E;AACF;","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.3",
4
+ "version": "3.0.0-next.5",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -14,9 +14,20 @@
14
14
  },
15
15
  "exports": {
16
16
  ".": {
17
- "types": "./dist/index.d.ts",
17
+ "types": {
18
+ "import": "./dist/index.d.ts",
19
+ "require": "./dist/index.d.cts"
20
+ },
18
21
  "import": "./dist/index.js",
19
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"
20
31
  }
21
32
  },
22
33
  "main": "dist/index.cjs",
@@ -28,23 +39,25 @@
28
39
  "dist"
29
40
  ],
30
41
  "dependencies": {
31
- "@tiptap/extension-bubble-menu": "^3.0.0-next.3",
32
- "@tiptap/extension-floating-menu": "^3.0.0-next.3",
33
42
  "@types/use-sync-external-store": "^0.0.6",
34
- "fast-deep-equal": "^3",
35
- "use-sync-external-store": "^1"
43
+ "fast-deep-equal": "^3.1.3",
44
+ "use-sync-external-store": "^1.4.0"
36
45
  },
37
46
  "devDependencies": {
38
- "@tiptap/core": "^3.0.0-next.3",
39
- "@tiptap/pm": "^3.0.0-next.3",
40
- "@types/react": "^18.2.14",
41
- "@types/react-dom": "^18.2.6",
42
- "react": "^18.0.0",
43
- "react-dom": "^18.0.0"
47
+ "@tiptap/core": "^3.0.0-next.5",
48
+ "@tiptap/pm": "^3.0.0-next.5",
49
+ "@types/react": "^18.3.18",
50
+ "@types/react-dom": "^18.3.5",
51
+ "react": "^18.3.1",
52
+ "react-dom": "^18.3.1"
53
+ },
54
+ "optionalDependencies": {
55
+ "@tiptap/extension-bubble-menu": "^3.0.0-next.5",
56
+ "@tiptap/extension-floating-menu": "^3.0.0-next.5"
44
57
  },
45
58
  "peerDependencies": {
46
- "@tiptap/core": "^3.0.0-next.1",
47
- "@tiptap/pm": "^3.0.0-next.1",
59
+ "@tiptap/core": "^3.0.0-next.4",
60
+ "@tiptap/pm": "^3.0.0-next.4",
48
61
  "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
49
62
  "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
50
63
  },
@@ -55,6 +68,7 @@
55
68
  },
56
69
  "sideEffects": false,
57
70
  "scripts": {
58
- "build": "tsup"
71
+ "build": "tsup",
72
+ "lint": "prettier ./src/ --check && eslint --cache --quiet --no-error-on-unmatched-pattern ./src/"
59
73
  }
60
- }
74
+ }
package/src/Context.tsx CHANGED
@@ -1,14 +1,12 @@
1
1
  import { Editor } from '@tiptap/core'
2
- import React, {
3
- createContext, HTMLAttributes, ReactNode, useContext, useMemo,
4
- } from 'react'
2
+ import React, { createContext, HTMLAttributes, ReactNode, useContext, useMemo } from 'react'
5
3
 
6
4
  import { EditorContent } from './EditorContent.js'
7
5
  import { useEditor, UseEditorOptions } from './useEditor.js'
8
6
 
9
7
  export type EditorContextValue = {
10
- editor: Editor | null;
11
- };
8
+ editor: Editor | null
9
+ }
12
10
 
13
11
  export const EditorContext = createContext<EditorContextValue>({
14
12
  editor: null,
@@ -22,11 +20,11 @@ export const EditorConsumer = EditorContext.Consumer
22
20
  export const useCurrentEditor = () => useContext(EditorContext)
23
21
 
24
22
  export type EditorProviderProps = {
25
- children?: ReactNode;
26
- slotBefore?: ReactNode;
27
- slotAfter?: ReactNode;
28
- editorContainerProps?: HTMLAttributes<HTMLDivElement>;
29
- } & UseEditorOptions;
23
+ children?: ReactNode
24
+ slotBefore?: ReactNode
25
+ slotAfter?: ReactNode
26
+ editorContainerProps?: HTMLAttributes<HTMLDivElement>
27
+ } & UseEditorOptions
30
28
 
31
29
  /**
32
30
  * This is the provider component for the editor.
@@ -51,9 +49,7 @@ export function EditorProvider({
51
49
  <EditorContext.Provider value={contextValue}>
52
50
  {slotBefore}
53
51
  <EditorConsumer>
54
- {({ editor: currentEditor }) => (
55
- <EditorContent editor={currentEditor} {...editorContainerProps} />
56
- )}
52
+ {({ editor: currentEditor }) => <EditorContent editor={currentEditor} {...editorContainerProps} />}
57
53
  </EditorConsumer>
58
54
  {children}
59
55
  {slotAfter}
package/src/Editor.ts CHANGED
@@ -5,9 +5,9 @@ import { ReactRenderer } from './ReactRenderer.js'
5
5
 
6
6
  export type EditorWithContentComponent = Editor & { contentComponent?: ContentComponent | null }
7
7
  export type ContentComponent = {
8
- setRenderer(id: string, renderer: ReactRenderer): void;
9
- removeRenderer(id: string): void;
10
- subscribe: (callback: () => void) => () => void;
11
- getSnapshot: () => Record<string, ReactPortal>;
12
- getServerSnapshot: () => Record<string, ReactPortal>;
8
+ setRenderer(id: string, renderer: ReactRenderer): void
9
+ removeRenderer(id: string): void
10
+ subscribe: (callback: () => void) => () => void
11
+ getSnapshot: () => Record<string, ReactPortal>
12
+ getServerSnapshot: () => Record<string, ReactPortal>
13
13
  }
@@ -1,22 +1,18 @@
1
1
  import { Editor } from '@tiptap/core'
2
- import React, {
3
- ForwardedRef, forwardRef, HTMLProps, LegacyRef, MutableRefObject,
4
- } from 'react'
2
+ import React, { ForwardedRef, forwardRef, HTMLProps, LegacyRef, MutableRefObject } from 'react'
5
3
  import ReactDOM from 'react-dom'
6
4
  import { useSyncExternalStore } from 'use-sync-external-store/shim'
7
5
 
8
6
  import { ContentComponent, EditorWithContentComponent } from './Editor.js'
9
7
  import { ReactRenderer } from './ReactRenderer.js'
10
8
 
11
- const mergeRefs = <T extends HTMLDivElement>(
12
- ...refs: Array<MutableRefObject<T> | LegacyRef<T> | undefined>
13
- ) => {
9
+ const mergeRefs = <T extends HTMLDivElement>(...refs: Array<MutableRefObject<T> | LegacyRef<T> | undefined>) => {
14
10
  return (node: T) => {
15
11
  refs.forEach(ref => {
16
12
  if (typeof ref === 'function') {
17
13
  ref(node)
18
14
  } else if (ref) {
19
- (ref as MutableRefObject<T | null>).current = node
15
+ ;(ref as MutableRefObject<T | null>).current = node
20
16
  }
21
17
  })
22
18
  }
@@ -25,9 +21,7 @@ const mergeRefs = <T extends HTMLDivElement>(
25
21
  /**
26
22
  * This component renders all of the editor's node views.
27
23
  */
28
- const Portals: React.FC<{ contentComponent: ContentComponent }> = ({
29
- contentComponent,
30
- }) => {
24
+ const Portals: React.FC<{ contentComponent: ContentComponent }> = ({ contentComponent }) => {
31
25
  // For performance reasons, we render the node view portals on state changes only
32
26
  const renderers = useSyncExternalStore(
33
27
  contentComponent.subscribe,
@@ -36,16 +30,12 @@ const Portals: React.FC<{ contentComponent: ContentComponent }> = ({
36
30
  )
37
31
 
38
32
  // This allows us to directly render the portals without any additional wrapper
39
- return (
40
- <>
41
- {Object.values(renderers)}
42
- </>
43
- )
33
+ return <>{Object.values(renderers)}</>
44
34
  }
45
35
 
46
36
  export interface EditorContentProps extends HTMLProps<HTMLDivElement> {
47
- editor: Editor | null;
48
- innerRef?: ForwardedRef<HTMLDivElement | null>;
37
+ editor: Editor | null
38
+ innerRef?: ForwardedRef<HTMLDivElement | null>
49
39
  }
50
40
 
51
41
  function getInstance(): ContentComponent {
@@ -185,10 +175,11 @@ export class PureEditorContent extends React.Component<
185
175
 
186
176
  editor.contentComponent = null
187
177
 
188
- if (!editor.options.element.firstChild) {
178
+ if (!editor.options.element?.firstChild) {
189
179
  return
190
180
  }
191
181
 
182
+ // TODO using the new editor.mount method might allow us to remove this
192
183
  const newElement = document.createElement('div')
193
184
 
194
185
  newElement.append(...editor.options.element.childNodes)
@@ -216,7 +207,7 @@ const EditorContentWithKey = forwardRef<HTMLDivElement, EditorContentProps>(
216
207
  (props: Omit<EditorContentProps, 'innerRef'>, ref) => {
217
208
  const key = React.useMemo(() => {
218
209
  return Math.floor(Math.random() * 0xffffffff).toString()
219
- // eslint-disable-next-line react-hooks/exhaustive-deps
210
+ // eslint-disable-next-line react-hooks/exhaustive-deps
220
211
  }, [props.editor])
221
212
 
222
213
  // Can't use JSX here because it conflicts with the type definition of Vue's JSX, so use createElement
@@ -1,15 +1,16 @@
1
- import React from 'react'
1
+ import React, { ComponentProps } from 'react'
2
2
 
3
3
  import { useReactNodeView } from './useReactNodeView.js'
4
4
 
5
- export interface NodeViewContentProps {
6
- [key: string]: any,
7
- as?: React.ElementType,
8
- }
5
+ export type NodeViewContentProps<T extends keyof React.JSX.IntrinsicElements = 'div'> = {
6
+ as?: NoInfer<T>
7
+ } & ComponentProps<T>
9
8
 
10
- export const NodeViewContent: React.FC<NodeViewContentProps> = props => {
11
- const Tag = props.as || 'div'
12
- const { nodeViewContentRef } = useReactNodeView()
9
+ export function NodeViewContent<T extends keyof React.JSX.IntrinsicElements = 'div'>({
10
+ as: Tag = 'div' as T,
11
+ ...props
12
+ }: NodeViewContentProps<T>) {
13
+ const { nodeViewContentRef, nodeViewContentChildren } = useReactNodeView()
13
14
 
14
15
  return (
15
16
  // @ts-ignore
@@ -21,6 +22,8 @@ export const NodeViewContent: React.FC<NodeViewContentProps> = props => {
21
22
  whiteSpace: 'pre-wrap',
22
23
  ...props.style,
23
24
  }}
24
- />
25
+ >
26
+ {nodeViewContentChildren}
27
+ </Tag>
25
28
  )
26
29
  }
@@ -3,8 +3,8 @@ import React from 'react'
3
3
  import { useReactNodeView } from './useReactNodeView.js'
4
4
 
5
5
  export interface NodeViewWrapperProps {
6
- [key: string]: any,
7
- as?: React.ElementType,
6
+ [key: string]: any
7
+ as?: React.ElementType
8
8
  }
9
9
 
10
10
  export const NodeViewWrapper: React.FC<NodeViewWrapperProps> = React.forwardRef((props, ref) => {
@@ -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
+ }
@@ -22,23 +22,23 @@ export interface ReactNodeViewRendererOptions extends NodeViewRendererOptions {
22
22
  */
23
23
  update:
24
24
  | ((props: {
25
- oldNode: ProseMirrorNode;
26
- oldDecorations: readonly Decoration[];
27
- oldInnerDecorations: DecorationSource;
28
- newNode: ProseMirrorNode;
29
- newDecorations: readonly Decoration[];
30
- innerDecorations: DecorationSource;
31
- updateProps: () => void;
25
+ oldNode: ProseMirrorNode
26
+ oldDecorations: readonly Decoration[]
27
+ oldInnerDecorations: DecorationSource
28
+ newNode: ProseMirrorNode
29
+ newDecorations: readonly Decoration[]
30
+ innerDecorations: DecorationSource
31
+ updateProps: () => void
32
32
  }) => boolean)
33
- | null;
33
+ | null
34
34
  /**
35
35
  * The tag name of the element wrapping the React component.
36
36
  */
37
- as?: string;
37
+ as?: string
38
38
  /**
39
39
  * The class name of the element wrapping the React component.
40
40
  */
41
- className?: string;
41
+ className?: string
42
42
  /**
43
43
  * Attributes that should be applied to the element wrapping the React component.
44
44
  * If this is a function, it will be called each time the node view is updated.
@@ -46,10 +46,7 @@ export interface ReactNodeViewRendererOptions extends NodeViewRendererOptions {
46
46
  */
47
47
  attrs?:
48
48
  | Record<string, string>
49
- | ((props: {
50
- node: ProseMirrorNode;
51
- HTMLAttributes: Record<string, any>;
52
- }) => Record<string, string>);
49
+ | ((props: { node: ProseMirrorNode; HTMLAttributes: Record<string, any> }) => Record<string, string>)
53
50
  }
54
51
 
55
52
  export class ReactNodeView<
@@ -104,15 +101,13 @@ export class ReactNodeView<
104
101
  const Component = this.component
105
102
  // For performance reasons, we memoize the provider component
106
103
  // And all of the things it requires are declared outside of the component, so it doesn't need to re-render
107
- const ReactNodeViewProvider: React.FunctionComponent<NodeViewProps> = React.memo(
108
- componentProps => {
109
- return (
110
- <ReactNodeViewContext.Provider value={context}>
111
- {React.createElement(Component, componentProps)}
112
- </ReactNodeViewContext.Provider>
113
- )
114
- },
115
- )
104
+ const ReactNodeViewProvider: React.FunctionComponent<NodeViewProps> = React.memo(componentProps => {
105
+ return (
106
+ <ReactNodeViewContext.Provider value={context}>
107
+ {React.createElement(Component, componentProps)}
108
+ </ReactNodeViewContext.Provider>
109
+ )
110
+ })
116
111
 
117
112
  ReactNodeViewProvider.displayName = 'ReactNodeView'
118
113
 
@@ -141,7 +136,6 @@ export class ReactNodeView<
141
136
  const { className = '' } = this.options
142
137
 
143
138
  this.handleSelectionUpdate = this.handleSelectionUpdate.bind(this)
144
- this.editor.on('selectionUpdate', this.handleSelectionUpdate)
145
139
 
146
140
  this.renderer = new ReactRenderer(ReactNodeViewProvider, {
147
141
  editor: this.editor,
@@ -150,6 +144,7 @@ export class ReactNodeView<
150
144
  className: `node-${this.node.type.name} ${className}`.trim(),
151
145
  })
152
146
 
147
+ this.editor.on('selectionUpdate', this.handleSelectionUpdate)
153
148
  this.updateElementAttributes()
154
149
  }
155
150
 
@@ -159,8 +154,8 @@ export class ReactNodeView<
159
154
  */
160
155
  get dom() {
161
156
  if (
162
- this.renderer.element.firstElementChild
163
- && !this.renderer.element.firstElementChild?.hasAttribute('data-node-view-wrapper')
157
+ this.renderer.element.firstElementChild &&
158
+ !this.renderer.element.firstElementChild?.hasAttribute('data-node-view-wrapper')
164
159
  ) {
165
160
  throw Error('Please use the NodeViewWrapper component for your node view.')
166
161
  }
@@ -211,11 +206,7 @@ export class ReactNodeView<
211
206
  * On update, update the React component.
212
207
  * To prevent unnecessary updates, the `update` option can be used.
213
208
  */
214
- update(
215
- node: Node,
216
- decorations: readonly Decoration[],
217
- innerDecorations: DecorationSource,
218
- ): boolean {
209
+ update(node: Node, decorations: readonly Decoration[], innerDecorations: DecorationSource): boolean {
219
210
  const rerenderComponent = (props?: Record<string, any>) => {
220
211
  this.renderer.updateProps(props)
221
212
  if (typeof this.options.attrs === 'function') {
@@ -247,11 +238,7 @@ export class ReactNodeView<
247
238
  })
248
239
  }
249
240
 
250
- if (
251
- node === this.node
252
- && this.decorations === decorations
253
- && this.innerDecorations === innerDecorations
254
- ) {
241
+ if (node === this.node && this.decorations === decorations && this.innerDecorations === innerDecorations) {
255
242
  return true
256
243
  }
257
244