@tiptap/react 2.1.0-rc.8 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,11 @@
1
- export * from './BubbleMenu';
2
- export { Editor } from './Editor';
3
- export * from './EditorContent';
4
- export * from './FloatingMenu';
5
- export * from './NodeViewContent';
6
- export * from './NodeViewWrapper';
7
- export * from './ReactNodeViewRenderer';
8
- export * from './ReactRenderer';
9
- export * from './useEditor';
1
+ export * from './BubbleMenu.js';
2
+ export * from './Context.js';
3
+ export { Editor } from './Editor.js';
4
+ export * from './EditorContent.js';
5
+ export * from './FloatingMenu.js';
6
+ export * from './NodeViewContent.js';
7
+ export * from './NodeViewWrapper.js';
8
+ export * from './ReactNodeViewRenderer.js';
9
+ export * from './ReactRenderer.js';
10
+ export * from './useEditor.js';
10
11
  export * from '@tiptap/core';
@@ -1,4 +1,4 @@
1
1
  import { EditorOptions } from '@tiptap/core';
2
2
  import { DependencyList } from 'react';
3
- import { Editor } from './Editor';
3
+ import { Editor } from './Editor.js';
4
4
  export declare const useEditor: (options?: Partial<EditorOptions>, deps?: DependencyList) => Editor | null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiptap/react",
3
3
  "description": "React components for tiptap",
4
- "version": "2.1.0-rc.8",
4
+ "version": "2.1.0",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -29,14 +29,14 @@
29
29
  "dist"
30
30
  ],
31
31
  "dependencies": {
32
- "@tiptap/extension-bubble-menu": "^2.1.0-rc.8",
33
- "@tiptap/extension-floating-menu": "^2.1.0-rc.8"
32
+ "@tiptap/extension-bubble-menu": "^2.1.0",
33
+ "@tiptap/extension-floating-menu": "^2.1.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@tiptap/core": "^2.1.0-rc.8",
37
- "@tiptap/pm": "^2.1.0-rc.8",
38
- "@types/react": "^18.0.1",
39
- "@types/react-dom": "^18.0.0",
36
+ "@tiptap/core": "^2.1.0",
37
+ "@tiptap/pm": "^2.1.0",
38
+ "@types/react": "^18.2.14",
39
+ "@types/react-dom": "^18.2.6",
40
40
  "react": "^18.0.0",
41
41
  "react-dom": "^18.0.0"
42
42
  },
@@ -1,9 +1,11 @@
1
1
  import { BubbleMenuPlugin, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'
2
2
  import React, { useEffect, useState } from 'react'
3
3
 
4
+ import { useCurrentEditor } from './Context.js'
5
+
4
6
  type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
5
7
 
6
- export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'> & {
8
+ export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey' | 'editor'>, 'element'> & {
7
9
  className?: string;
8
10
  children: React.ReactNode;
9
11
  updateDelay?: number;
@@ -11,13 +13,14 @@ export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>,
11
13
 
12
14
  export const BubbleMenu = (props: BubbleMenuProps) => {
13
15
  const [element, setElement] = useState<HTMLDivElement | null>(null)
16
+ const { editor: currentEditor } = useCurrentEditor()
14
17
 
15
18
  useEffect(() => {
16
19
  if (!element) {
17
20
  return
18
21
  }
19
22
 
20
- if (props.editor.isDestroyed) {
23
+ if (props.editor?.isDestroyed || currentEditor?.isDestroyed) {
21
24
  return
22
25
  }
23
26
 
@@ -25,18 +28,25 @@ export const BubbleMenu = (props: BubbleMenuProps) => {
25
28
  pluginKey = 'bubbleMenu', editor, tippyOptions = {}, updateDelay, shouldShow = null,
26
29
  } = props
27
30
 
31
+ const menuEditor = editor || currentEditor
32
+
33
+ if (!menuEditor) {
34
+ console.warn('BubbleMenu component is not rendered inside of an editor component or does not have editor prop.')
35
+ return
36
+ }
37
+
28
38
  const plugin = BubbleMenuPlugin({
29
39
  updateDelay,
30
- editor,
40
+ editor: menuEditor,
31
41
  element,
32
42
  pluginKey,
33
43
  shouldShow,
34
44
  tippyOptions,
35
45
  })
36
46
 
37
- editor.registerPlugin(plugin)
38
- return () => editor.unregisterPlugin(pluginKey)
39
- }, [props.editor, element])
47
+ menuEditor.registerPlugin(plugin)
48
+ return () => menuEditor.unregisterPlugin(pluginKey)
49
+ }, [props.editor, currentEditor, element])
40
50
 
41
51
  return (
42
52
  <div ref={setElement} className={props.className} style={{ visibility: 'hidden' }}>
@@ -0,0 +1,47 @@
1
+ import { EditorOptions } from '@tiptap/core'
2
+ import React, { createContext, ReactNode, useContext } from 'react'
3
+
4
+ import { Editor } from './Editor.js'
5
+ import { EditorContent } from './EditorContent.js'
6
+ import { useEditor } from './useEditor.js'
7
+
8
+ export type EditorContextValue = {
9
+ editor: Editor | null;
10
+ }
11
+
12
+ export const EditorContext = createContext<EditorContextValue>({
13
+ editor: null,
14
+ })
15
+
16
+ export const EditorConsumer = EditorContext.Consumer
17
+
18
+ export const useCurrentEditor = () => useContext(EditorContext)
19
+
20
+ export type EditorProviderProps = {
21
+ children: ReactNode;
22
+ slotBefore?: ReactNode;
23
+ slotAfter?: ReactNode;
24
+ } & Partial<EditorOptions>
25
+
26
+ export const EditorProvider = ({
27
+ children, slotAfter, slotBefore, ...editorOptions
28
+ }: EditorProviderProps) => {
29
+ const editor = useEditor(editorOptions)
30
+
31
+ if (!editor) {
32
+ return null
33
+ }
34
+
35
+ return (
36
+ <EditorContext.Provider value={{ editor }}>
37
+ {slotBefore}
38
+ <EditorConsumer>
39
+ {({ editor: currentEditor }) => (
40
+ <EditorContent editor={currentEditor} />
41
+ )}
42
+ </EditorConsumer>
43
+ {children}
44
+ {slotAfter}
45
+ </EditorContext.Provider>
46
+ )
47
+ }
package/src/Editor.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Editor as CoreEditor } from '@tiptap/core'
2
2
  import React from 'react'
3
3
 
4
- import { EditorContentProps, EditorContentState } from './EditorContent'
5
- import { ReactRenderer } from './ReactRenderer'
4
+ import { EditorContentProps, EditorContentState } from './EditorContent.js'
5
+ import { ReactRenderer } from './ReactRenderer.js'
6
6
 
7
7
  type ContentComponent = React.Component<EditorContentProps, EditorContentState> & {
8
8
  setRenderer(id: string, renderer: ReactRenderer): void;
@@ -1,8 +1,8 @@
1
1
  import React, { HTMLProps } from 'react'
2
2
  import ReactDOM, { flushSync } from 'react-dom'
3
3
 
4
- import { Editor } from './Editor'
5
- import { ReactRenderer } from './ReactRenderer'
4
+ import { Editor } from './Editor.js'
5
+ import { ReactRenderer } from './ReactRenderer.js'
6
6
 
7
7
  const Portals: React.FC<{ renderers: Record<string, ReactRenderer> }> = ({ renderers }) => {
8
8
  return (
@@ -147,4 +147,14 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
147
147
  }
148
148
  }
149
149
 
150
- export const EditorContent = React.memo(PureEditorContent)
150
+ // EditorContent should be re-created whenever the Editor instance changes
151
+ const EditorContentWithKey = (props: EditorContentProps) => {
152
+ const key = React.useMemo(() => {
153
+ return Math.floor(Math.random() * 0xFFFFFFFF).toString()
154
+ }, [props.editor])
155
+
156
+ // Can't use JSX here because it conflicts with the type definition of Vue's JSX, so use createElement
157
+ return React.createElement(PureEditorContent, { key, ...props })
158
+ }
159
+
160
+ export const EditorContent = React.memo(EditorContentWithKey)
@@ -3,22 +3,25 @@ import React, {
3
3
  useEffect, useState,
4
4
  } from 'react'
5
5
 
6
+ import { useCurrentEditor } from './Context.js'
7
+
6
8
  type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
7
9
 
8
- export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element'> & {
10
+ export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey' | 'editor'>, 'element'> & {
9
11
  className?: string,
10
12
  children: React.ReactNode
11
13
  }
12
14
 
13
15
  export const FloatingMenu = (props: FloatingMenuProps) => {
14
16
  const [element, setElement] = useState<HTMLDivElement | null>(null)
17
+ const { editor: currentEditor } = useCurrentEditor()
15
18
 
16
19
  useEffect(() => {
17
20
  if (!element) {
18
21
  return
19
22
  }
20
23
 
21
- if (props.editor.isDestroyed) {
24
+ if (props.editor?.isDestroyed || currentEditor?.isDestroyed) {
22
25
  return
23
26
  }
24
27
 
@@ -29,18 +32,26 @@ export const FloatingMenu = (props: FloatingMenuProps) => {
29
32
  shouldShow = null,
30
33
  } = props
31
34
 
35
+ const menuEditor = editor || currentEditor
36
+
37
+ if (!menuEditor) {
38
+ console.warn('FloatingMenu component is not rendered inside of an editor component or does not have editor prop.')
39
+ return
40
+ }
41
+
32
42
  const plugin = FloatingMenuPlugin({
33
43
  pluginKey,
34
- editor,
44
+ editor: menuEditor,
35
45
  element,
36
46
  tippyOptions,
37
47
  shouldShow,
38
48
  })
39
49
 
40
- editor.registerPlugin(plugin)
41
- return () => editor.unregisterPlugin(pluginKey)
50
+ menuEditor.registerPlugin(plugin)
51
+ return () => menuEditor.unregisterPlugin(pluginKey)
42
52
  }, [
43
53
  props.editor,
54
+ currentEditor,
44
55
  element,
45
56
  ])
46
57
 
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
 
3
- import { useReactNodeView } from './useReactNodeView'
3
+ import { useReactNodeView } from './useReactNodeView.js'
4
4
 
5
5
  export interface NodeViewContentProps {
6
6
  [key: string]: any,
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
 
3
- import { useReactNodeView } from './useReactNodeView'
3
+ import { useReactNodeView } from './useReactNodeView.js'
4
4
 
5
5
  export interface NodeViewWrapperProps {
6
6
  [key: string]: any,
@@ -10,9 +10,9 @@ import { Node as ProseMirrorNode } from '@tiptap/pm/model'
10
10
  import { Decoration, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
11
11
  import React from 'react'
12
12
 
13
- import { Editor } from './Editor'
14
- import { ReactRenderer } from './ReactRenderer'
15
- import { ReactNodeViewContext, ReactNodeViewContextProps } from './useReactNodeView'
13
+ import { Editor } from './Editor.js'
14
+ import { ReactRenderer } from './ReactRenderer.js'
15
+ import { ReactNodeViewContext, ReactNodeViewContextProps } from './useReactNodeView.js'
16
16
 
17
17
  export interface ReactNodeViewRendererOptions extends NodeViewRendererOptions {
18
18
  update:
@@ -99,6 +99,9 @@ class ReactNodeView extends NodeView<
99
99
 
100
100
  const { className = '' } = this.options
101
101
 
102
+ this.handleSelectionUpdate = this.handleSelectionUpdate.bind(this)
103
+ this.editor.on('selectionUpdate', this.handleSelectionUpdate)
104
+
102
105
  this.renderer = new ReactRenderer(ReactNodeViewProvider, {
103
106
  editor: this.editor,
104
107
  props,
@@ -127,6 +130,16 @@ class ReactNodeView extends NodeView<
127
130
  return this.contentDOMElement
128
131
  }
129
132
 
133
+ handleSelectionUpdate() {
134
+ const { from, to } = this.editor.state.selection
135
+
136
+ if (from <= this.getPos() && to >= this.getPos() + this.node.nodeSize) {
137
+ this.selectNode()
138
+ } else {
139
+ this.deselectNode()
140
+ }
141
+ }
142
+
130
143
  update(node: ProseMirrorNode, decorations: DecorationWithType[]) {
131
144
  const updateProps = (props?: Record<string, any>) => {
132
145
  this.renderer.updateProps(props)
@@ -178,6 +191,7 @@ class ReactNodeView extends NodeView<
178
191
 
179
192
  destroy() {
180
193
  this.renderer.destroy()
194
+ this.editor.off('selectionUpdate', this.handleSelectionUpdate)
181
195
  this.contentDOMElement = null
182
196
  }
183
197
  }
@@ -1,7 +1,7 @@
1
1
  import { Editor } from '@tiptap/core'
2
2
  import React from 'react'
3
3
 
4
- import { Editor as ExtendedEditor } from './Editor'
4
+ import { Editor as ExtendedEditor } from './Editor.js'
5
5
 
6
6
  function isClassComponent(Component: any) {
7
7
  return !!(
package/src/index.ts CHANGED
@@ -1,10 +1,11 @@
1
- export * from './BubbleMenu'
2
- export { Editor } from './Editor'
3
- export * from './EditorContent'
4
- export * from './FloatingMenu'
5
- export * from './NodeViewContent'
6
- export * from './NodeViewWrapper'
7
- export * from './ReactNodeViewRenderer'
8
- export * from './ReactRenderer'
9
- export * from './useEditor'
1
+ export * from './BubbleMenu.js'
2
+ export * from './Context.js'
3
+ export { Editor } from './Editor.js'
4
+ export * from './EditorContent.js'
5
+ export * from './FloatingMenu.js'
6
+ export * from './NodeViewContent.js'
7
+ export * from './NodeViewWrapper.js'
8
+ export * from './ReactNodeViewRenderer.js'
9
+ export * from './ReactRenderer.js'
10
+ export * from './useEditor.js'
10
11
  export * from '@tiptap/core'
package/src/useEditor.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  useState,
7
7
  } from 'react'
8
8
 
9
- import { Editor } from './Editor'
9
+ import { Editor } from './Editor.js'
10
10
 
11
11
  function useForceUpdate() {
12
12
  const [, setValue] = useState(0)
@@ -113,10 +113,15 @@ export const useEditor = (options: Partial<EditorOptions> = {}, deps: Dependency
113
113
  })
114
114
 
115
115
  return () => {
116
- instance.destroy()
117
116
  isMounted = false
118
117
  }
119
118
  }, deps)
120
119
 
120
+ useEffect(() => {
121
+ return () => {
122
+ editor?.destroy()
123
+ }
124
+ }, [editor])
125
+
121
126
  return editor
122
127
  }