@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.
@@ -10,11 +10,7 @@ import { EditorWithContentComponent } from './Editor.js'
10
10
  * @returns {boolean}
11
11
  */
12
12
  function isClassComponent(Component: any) {
13
- return !!(
14
- typeof Component === 'function'
15
- && Component.prototype
16
- && Component.prototype.isReactComponent
17
- )
13
+ return !!(typeof Component === 'function' && Component.prototype && Component.prototype.isReactComponent)
18
14
  }
19
15
 
20
16
  /**
@@ -23,10 +19,7 @@ function isClassComponent(Component: any) {
23
19
  * @returns {boolean}
24
20
  */
25
21
  function isForwardRefComponent(Component: any) {
26
- return !!(
27
- typeof Component === 'object'
28
- && Component.$$typeof?.toString() === 'Symbol(react.forward_ref)'
29
- )
22
+ return !!(typeof Component === 'object' && Component.$$typeof?.toString() === 'Symbol(react.forward_ref)')
30
23
  }
31
24
 
32
25
  export interface ReactRendererOptions {
@@ -34,21 +27,21 @@ export interface ReactRendererOptions {
34
27
  * The editor instance.
35
28
  * @type {Editor}
36
29
  */
37
- editor: Editor,
30
+ editor: Editor
38
31
 
39
32
  /**
40
33
  * The props for the component.
41
34
  * @type {Record<string, any>}
42
35
  * @default {}
43
36
  */
44
- props?: Record<string, any>,
37
+ props?: Record<string, any>
45
38
 
46
39
  /**
47
40
  * The tag name of the element.
48
41
  * @type {string}
49
42
  * @default 'div'
50
43
  */
51
- as?: string,
44
+ as?: string
52
45
 
53
46
  /**
54
47
  * The class name of the element.
@@ -56,13 +49,13 @@ export interface ReactRendererOptions {
56
49
  * @default ''
57
50
  * @example 'foo bar'
58
51
  */
59
- className?: string,
52
+ className?: string
60
53
  }
61
54
 
62
55
  type ComponentType<R, P> =
63
- React.ComponentClass<P> |
64
- React.FunctionComponent<P> |
65
- React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<R>>;
56
+ | React.ComponentClass<P>
57
+ | React.FunctionComponent<P>
58
+ | React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<R>>
66
59
 
67
60
  /**
68
61
  * The ReactRenderer class. It's responsible for rendering React components inside the editor.
@@ -74,8 +67,8 @@ type ComponentType<R, P> =
74
67
  * },
75
68
  * as: 'span',
76
69
  * })
77
- */
78
- export class ReactRenderer<R = unknown, P extends Record<string, any> = {}> {
70
+ */
71
+ export class ReactRenderer<R = unknown, P extends Record<string, any> = object> {
79
72
  id: string
80
73
 
81
74
  editor: Editor
@@ -93,13 +86,11 @@ export class ReactRenderer<R = unknown, P extends Record<string, any> = {}> {
93
86
  /**
94
87
  * Immediately creates element and renders the provided React component.
95
88
  */
96
- constructor(component: ComponentType<R, P>, {
97
- editor,
98
- props = {},
99
- as = 'div',
100
- className = '',
101
- }: ReactRendererOptions) {
102
- this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
89
+ constructor(
90
+ component: ComponentType<R, P>,
91
+ { editor, props = {}, as = 'div', className = '' }: ReactRendererOptions,
92
+ ) {
93
+ this.id = Math.floor(Math.random() * 0xffffffff).toString()
103
94
  this.component = component
104
95
  this.editor = editor as EditorWithContentComponent
105
96
  this.props = props as P
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,33 +1,16 @@
1
- import { BubbleMenuPlugin, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'
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
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
6
7
 
7
- type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
8
-
9
- export type BubbleMenuProps = Omit<
10
- Optional<BubbleMenuPluginProps, 'pluginKey'>,
11
- 'element' | 'editor'
12
- > & {
13
- editor: BubbleMenuPluginProps['editor'] | null;
14
- updateDelay?: number;
15
- resizeDelay?: number;
16
- options?: BubbleMenuPluginProps['options'];
17
- } & React.HTMLAttributes<HTMLDivElement>;
8
+ export type BubbleMenuProps = Optional<Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>, 'editor'> &
9
+ React.HTMLAttributes<HTMLDivElement>
18
10
 
19
11
  export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
20
12
  (
21
- {
22
- pluginKey = 'bubbleMenu',
23
- editor,
24
- updateDelay,
25
- resizeDelay,
26
- shouldShow = null,
27
- options,
28
- children,
29
- ...restProps
30
- },
13
+ { pluginKey = 'bubbleMenu', editor, updateDelay, resizeDelay, shouldShow = null, options, children, ...restProps },
31
14
  ref,
32
15
  ) => {
33
16
  const menuEl = useRef(document.createElement('div'))
@@ -46,16 +29,14 @@ export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
46
29
  bubbleMenuElement.style.visibility = 'hidden'
47
30
  bubbleMenuElement.style.position = 'absolute'
48
31
 
49
- if (editor?.isDestroyed || currentEditor?.isDestroyed) {
32
+ if (editor?.isDestroyed || (currentEditor as any)?.isDestroyed) {
50
33
  return
51
34
  }
52
35
 
53
36
  const attachToEditor = editor || currentEditor
54
37
 
55
38
  if (!attachToEditor) {
56
- console.warn(
57
- 'BubbleMenu component is not rendered inside of an editor component or does not have editor prop.',
58
- )
39
+ console.warn('BubbleMenu component is not rendered inside of an editor component or does not have editor prop.')
59
40
  return
60
41
  }
61
42
 
@@ -82,13 +63,6 @@ export const BubbleMenu = React.forwardRef<HTMLDivElement, BubbleMenuProps>(
82
63
  // eslint-disable-next-line react-hooks/exhaustive-deps
83
64
  }, [editor, currentEditor])
84
65
 
85
- return createPortal(
86
- <div
87
- {...restProps}
88
- >
89
- {children}
90
- </div>,
91
- menuEl.current,
92
- )
66
+ return createPortal(<div {...restProps}>{children}</div>, menuEl.current)
93
67
  },
94
68
  )
@@ -0,0 +1,67 @@
1
+ import { FloatingMenuPlugin, FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'
2
+ import { useCurrentEditor } from '@tiptap/react'
3
+ import React, { useEffect, useRef } from 'react'
4
+ import { createPortal } from 'react-dom'
5
+
6
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
7
+
8
+ export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element' | 'editor'> & {
9
+ editor: FloatingMenuPluginProps['editor'] | null
10
+ options?: FloatingMenuPluginProps['options']
11
+ } & React.HTMLAttributes<HTMLDivElement>
12
+
13
+ export const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(
14
+ ({ pluginKey = 'floatingMenu', editor, shouldShow = null, options, children, ...restProps }, ref) => {
15
+ const menuEl = useRef(document.createElement('div'))
16
+
17
+ if (typeof ref === 'function') {
18
+ ref(menuEl.current)
19
+ } else if (ref) {
20
+ ref.current = menuEl.current
21
+ }
22
+
23
+ const { editor: currentEditor } = useCurrentEditor()
24
+
25
+ useEffect(() => {
26
+ const floatingMenuElement = menuEl.current
27
+
28
+ floatingMenuElement.style.visibility = 'hidden'
29
+ floatingMenuElement.style.position = 'absolute'
30
+
31
+ if (editor?.isDestroyed || (currentEditor as any)?.isDestroyed) {
32
+ return
33
+ }
34
+
35
+ const attachToEditor = editor || currentEditor
36
+
37
+ if (!attachToEditor) {
38
+ console.warn(
39
+ 'FloatingMenu component is not rendered inside of an editor component or does not have editor prop.',
40
+ )
41
+ return
42
+ }
43
+
44
+ const plugin = FloatingMenuPlugin({
45
+ editor: attachToEditor,
46
+ element: floatingMenuElement,
47
+ pluginKey,
48
+ shouldShow,
49
+ options,
50
+ })
51
+
52
+ attachToEditor.registerPlugin(plugin)
53
+
54
+ return () => {
55
+ attachToEditor.unregisterPlugin(pluginKey)
56
+ window.requestAnimationFrame(() => {
57
+ if (floatingMenuElement.parentNode) {
58
+ floatingMenuElement.parentNode.removeChild(floatingMenuElement)
59
+ }
60
+ })
61
+ }
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
63
+ }, [editor, currentEditor])
64
+
65
+ return createPortal(<div {...restProps}>{children}</div>, menuEl.current)
66
+ },
67
+ )
@@ -0,0 +1,2 @@
1
+ export * from './BubbleMenu.js'
2
+ export * from './FloatingMenu.js'
package/src/useEditor.ts CHANGED
@@ -1,16 +1,10 @@
1
1
  import { type EditorOptions, Editor } from '@tiptap/core'
2
- import {
3
- DependencyList,
4
- MutableRefObject,
5
- useDebugValue,
6
- useEffect,
7
- useRef,
8
- useState,
9
- } from 'react'
2
+ import { DependencyList, MutableRefObject, useDebugValue, useEffect, useRef, useState } from 'react'
10
3
  import { useSyncExternalStore } from 'use-sync-external-store/shim'
11
4
 
12
5
  import { useEditorState } from './useEditorState.js'
13
6
 
7
+ // @ts-ignore
14
8
  const isDev = process.env.NODE_ENV !== 'production'
15
9
  const isSSR = typeof window === 'undefined'
16
10
  const isNext = isSSR || Boolean(typeof window !== 'undefined' && (window as any).next)
@@ -25,14 +19,14 @@ export type UseEditorOptions = Partial<EditorOptions> & {
25
19
  * If server-side rendering, set this to `false`.
26
20
  * @default true
27
21
  */
28
- immediatelyRender?: boolean;
22
+ immediatelyRender?: boolean
29
23
  /**
30
24
  * Whether to re-render the editor on each transaction.
31
25
  * This is legacy behavior that will be removed in future versions.
32
26
  * @default false
33
27
  */
34
- shouldRerenderOnTransaction?: boolean;
35
- };
28
+ shouldRerenderOnTransaction?: boolean
29
+ }
36
30
 
37
31
  /**
38
32
  * This class handles the creation, destruction, and re-creation of the editor instance.
@@ -150,6 +144,7 @@ class EditorInstanceManager {
150
144
  onContentError: (...args) => this.options.current.onContentError?.(...args),
151
145
  onDrop: (...args) => this.options.current.onDrop?.(...args),
152
146
  onPaste: (...args) => this.options.current.onPaste?.(...args),
147
+ onDelete: (...args) => this.options.current.onDelete?.(...args),
153
148
  }
154
149
  const editor = new Editor(optionsToApply)
155
150
 
@@ -183,6 +178,47 @@ class EditorInstanceManager {
183
178
  }
184
179
  }
185
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
+
186
222
  /**
187
223
  * On each render, we will create, update, or destroy the editor instance.
188
224
  * @param deps The dependencies to watch for changes
@@ -196,12 +232,15 @@ class EditorInstanceManager {
196
232
  clearTimeout(this.scheduledDestructionTimeout)
197
233
 
198
234
  if (this.editor && !this.editor.isDestroyed && deps.length === 0) {
199
- // if the editor does exist & deps are empty, we don't need to re-initialize the editor
200
- // we can fast-path to update the editor options on the existing instance
201
- this.editor.setOptions({
202
- ...this.options.current,
203
- editable: this.editor.isEditable,
204
- })
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
+ }
205
244
  } else {
206
245
  // When the editor:
207
246
  // - does not yet exist
@@ -229,8 +268,8 @@ class EditorInstanceManager {
229
268
  this.previousDeps = deps
230
269
  return
231
270
  }
232
- const depsAreEqual = this.previousDeps.length === deps.length
233
- && this.previousDeps.every((dep, index) => dep === deps[index])
271
+ const depsAreEqual =
272
+ this.previousDeps.length === deps.length && this.previousDeps.every((dep, index) => dep === deps[index])
234
273
 
235
274
  if (depsAreEqual) {
236
275
  // deps exist and are equal, no need to recreate
@@ -289,8 +328,8 @@ class EditorInstanceManager {
289
328
  */
290
329
  export function useEditor(
291
330
  options: UseEditorOptions & { immediatelyRender: false },
292
- deps?: DependencyList
293
- ): Editor | null;
331
+ deps?: DependencyList,
332
+ ): Editor | null
294
333
 
295
334
  /**
296
335
  * This hook allows you to create an editor instance.
@@ -299,12 +338,9 @@ export function useEditor(
299
338
  * @returns The editor instance
300
339
  * @example const editor = useEditor({ extensions: [...] })
301
340
  */
302
- export function useEditor(options: UseEditorOptions, deps?: DependencyList): Editor;
341
+ export function useEditor(options: UseEditorOptions, deps?: DependencyList): Editor
303
342
 
304
- export function useEditor(
305
- options: UseEditorOptions = {},
306
- deps: DependencyList = [],
307
- ): Editor | null {
343
+ export function useEditor(options: UseEditorOptions = {}, deps: DependencyList = []): Editor | null {
308
344
  const mostRecentOptions = useRef(options)
309
345
 
310
346
  mostRecentOptions.current = options
@@ -1,35 +1,30 @@
1
1
  import type { Editor } from '@tiptap/core'
2
2
  import deepEqual from 'fast-deep-equal/es6/react'
3
- import {
4
- useDebugValue, useEffect, useLayoutEffect, useState,
5
- } from 'react'
3
+ import { useDebugValue, useEffect, useLayoutEffect, useState } from 'react'
6
4
  import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
7
5
 
8
6
  const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
9
7
 
10
8
  export type EditorStateSnapshot<TEditor extends Editor | null = Editor | null> = {
11
- editor: TEditor;
12
- transactionNumber: number;
13
- };
14
-
15
- export type UseEditorStateOptions<
16
- TSelectorResult,
17
- TEditor extends Editor | null = Editor | null,
18
- > = {
9
+ editor: TEditor
10
+ transactionNumber: number
11
+ }
12
+
13
+ export type UseEditorStateOptions<TSelectorResult, TEditor extends Editor | null = Editor | null> = {
19
14
  /**
20
15
  * The editor instance.
21
16
  */
22
- editor: TEditor;
17
+ editor: TEditor
23
18
  /**
24
19
  * A selector function to determine the value to compare for re-rendering.
25
20
  */
26
- selector: (context: EditorStateSnapshot<TEditor>) => TSelectorResult;
21
+ selector: (context: EditorStateSnapshot<TEditor>) => TSelectorResult
27
22
  /**
28
23
  * A custom equality function to determine if the editor should re-render.
29
24
  * @default `deepEqual` from `fast-deep-equal`
30
25
  */
31
- equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean;
32
- };
26
+ equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean
27
+ }
33
28
 
34
29
  /**
35
30
  * To synchronize the editor instance with the component state,
@@ -126,8 +121,8 @@ class EditorStateManager<TEditor extends Editor | null = Editor | null> {
126
121
  * })
127
122
  */
128
123
  export function useEditorState<TSelectorResult>(
129
- options: UseEditorStateOptions<TSelectorResult, Editor>
130
- ): TSelectorResult;
124
+ options: UseEditorStateOptions<TSelectorResult, Editor>,
125
+ ): TSelectorResult
131
126
  /**
132
127
  * This hook allows you to watch for changes on the editor instance.
133
128
  * It will allow you to select a part of the editor state and re-render the component when it changes.
@@ -140,8 +135,8 @@ export function useEditorState<TSelectorResult>(
140
135
  * })
141
136
  */
142
137
  export function useEditorState<TSelectorResult>(
143
- options: UseEditorStateOptions<TSelectorResult, Editor | null>
144
- ): TSelectorResult | null;
138
+ options: UseEditorStateOptions<TSelectorResult, Editor | null>,
139
+ ): TSelectorResult | null
145
140
 
146
141
  /**
147
142
  * This hook allows you to watch for changes on the editor instance.
@@ -1,12 +1,27 @@
1
- import { createContext, useContext } from 'react'
1
+ import { createContext, createElement, ReactNode, useContext } from 'react'
2
2
 
3
3
  export interface ReactNodeViewContextProps {
4
- onDragStart: (event: DragEvent) => void,
5
- nodeViewContentRef: (element: HTMLElement | null) => void,
4
+ onDragStart?: (event: DragEvent) => void
5
+ nodeViewContentRef?: (element: HTMLElement | null) => void
6
+ /**
7
+ * This allows you to add children into the NodeViewContent component.
8
+ * This is useful when statically rendering the content of a node view.
9
+ */
10
+ nodeViewContentChildren?: ReactNode
6
11
  }
7
12
 
8
- export const ReactNodeViewContext = createContext<Partial<ReactNodeViewContextProps>>({
9
- onDragStart: undefined,
13
+ export const ReactNodeViewContext = createContext<ReactNodeViewContextProps>({
14
+ onDragStart: () => {
15
+ // no-op
16
+ },
17
+ nodeViewContentChildren: undefined,
18
+ nodeViewContentRef: () => {
19
+ // no-op
20
+ },
10
21
  })
11
22
 
23
+ export const ReactNodeViewContentProvider = ({ children, content }: { children: ReactNode; content: ReactNode }) => {
24
+ return createElement(ReactNodeViewContext.Provider, { value: { nodeViewContentChildren: content } }, children)
25
+ }
26
+
12
27
  export const useReactNodeView = () => useContext(ReactNodeViewContext)
@@ -1,83 +0,0 @@
1
- import { FloatingMenuPlugin, FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'
2
- import React, { useEffect, useRef } from 'react'
3
- import { createPortal } from 'react-dom'
4
-
5
- import { useCurrentEditor } from './Context.js'
6
-
7
- type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
8
-
9
- export type FloatingMenuProps = Omit<
10
- Optional<FloatingMenuPluginProps, 'pluginKey'>,
11
- 'element' | 'editor'
12
- > & {
13
- editor: FloatingMenuPluginProps['editor'] | null;
14
- options?: FloatingMenuPluginProps['options'];
15
- } & React.HTMLAttributes<HTMLDivElement>;
16
-
17
- export const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(({
18
- pluginKey = 'floatingMenu',
19
- editor,
20
- shouldShow = null,
21
- options,
22
- children,
23
- ...restProps
24
- }, ref) => {
25
- const menuEl = useRef(document.createElement('div'))
26
-
27
- if (typeof ref === 'function') {
28
- ref(menuEl.current)
29
- } else if (ref) {
30
- ref.current = menuEl.current
31
- }
32
-
33
- const { editor: currentEditor } = useCurrentEditor()
34
-
35
- useEffect(() => {
36
- const floatingMenuElement = menuEl.current
37
-
38
- floatingMenuElement.style.visibility = 'hidden'
39
- floatingMenuElement.style.position = 'absolute'
40
-
41
- if (editor?.isDestroyed || currentEditor?.isDestroyed) {
42
- return
43
- }
44
-
45
- const attachToEditor = editor || currentEditor
46
-
47
- if (!attachToEditor) {
48
- console.warn(
49
- 'FloatingMenu component is not rendered inside of an editor component or does not have editor prop.',
50
- )
51
- return
52
- }
53
-
54
- const plugin = FloatingMenuPlugin({
55
- editor: attachToEditor,
56
- element: floatingMenuElement,
57
- pluginKey,
58
- shouldShow,
59
- options,
60
- })
61
-
62
- attachToEditor.registerPlugin(plugin)
63
-
64
- return () => {
65
- attachToEditor.unregisterPlugin(pluginKey)
66
- window.requestAnimationFrame(() => {
67
- if (floatingMenuElement.parentNode) {
68
- floatingMenuElement.parentNode.removeChild(floatingMenuElement)
69
- }
70
- })
71
- }
72
- // eslint-disable-next-line react-hooks/exhaustive-deps
73
- }, [editor, currentEditor])
74
-
75
- return createPortal(
76
- <div
77
- {...restProps}
78
- >
79
- {children}
80
- </div>,
81
- menuEl.current,
82
- )
83
- })