@tiptap/react 2.5.8 → 2.6.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.
- package/dist/index.cjs +438 -272
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +438 -272
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +438 -272
- package/dist/index.umd.js.map +1 -1
- package/dist/packages/core/src/Editor.d.ts +4 -0
- package/dist/packages/core/src/Extension.d.ts +5 -4
- package/dist/packages/core/src/Mark.d.ts +5 -4
- package/dist/packages/core/src/Node.d.ts +5 -4
- package/dist/packages/core/src/commands/setMeta.d.ts +2 -1
- package/dist/packages/core/src/commands/splitListItem.d.ts +2 -1
- package/dist/packages/core/src/helpers/isNodeEmpty.d.ts +10 -4
- package/dist/packages/core/src/pasteRules/nodePasteRule.d.ts +2 -1
- package/dist/packages/react/src/Context.d.ts +1 -1
- package/dist/packages/react/src/Editor.d.ts +4 -2
- package/dist/packages/react/src/EditorContent.d.ts +4 -8
- package/dist/packages/react/src/useEditor.d.ts +1 -2
- package/dist/packages/react/src/useEditorState.d.ts +1 -1
- package/package.json +7 -7
- package/src/Context.tsx +3 -2
- package/src/Editor.ts +4 -2
- package/src/EditorContent.tsx +93 -47
- package/src/NodeViewWrapper.tsx +1 -0
- package/src/ReactNodeViewRenderer.tsx +14 -16
- package/src/ReactRenderer.tsx +11 -2
- package/src/useEditor.ts +243 -89
- package/src/useEditorState.ts +76 -62
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Editor as CoreEditor } from '@tiptap/core';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { EditorContentProps, EditorContentState } from './EditorContent.js';
|
|
4
3
|
import { ReactRenderer } from './ReactRenderer.js';
|
|
5
|
-
type ContentComponent =
|
|
4
|
+
type ContentComponent = {
|
|
6
5
|
setRenderer(id: string, renderer: ReactRenderer): void;
|
|
7
6
|
removeRenderer(id: string): void;
|
|
7
|
+
subscribe: (callback: () => void) => () => void;
|
|
8
|
+
getSnapshot: () => Record<string, React.ReactPortal>;
|
|
9
|
+
getServerSnapshot: () => Record<string, React.ReactPortal>;
|
|
8
10
|
};
|
|
9
11
|
export declare class Editor extends CoreEditor {
|
|
10
12
|
contentComponent: ContentComponent | null;
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import React, { ForwardedRef, HTMLProps } from 'react';
|
|
2
2
|
import { Editor } from './Editor.js';
|
|
3
|
-
import { ReactRenderer } from './ReactRenderer.js';
|
|
4
3
|
export interface EditorContentProps extends HTMLProps<HTMLDivElement> {
|
|
5
4
|
editor: Editor | null;
|
|
6
5
|
innerRef?: ForwardedRef<HTMLDivElement | null>;
|
|
7
6
|
}
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
export declare class PureEditorContent extends React.Component<EditorContentProps, EditorContentState> {
|
|
7
|
+
export declare class PureEditorContent extends React.Component<EditorContentProps, {
|
|
8
|
+
hasContentComponentInitialized: boolean;
|
|
9
|
+
}> {
|
|
12
10
|
editorContentRef: React.RefObject<any>;
|
|
13
11
|
initialized: boolean;
|
|
12
|
+
unsubscribeToContentComponent?: () => void;
|
|
14
13
|
constructor(props: EditorContentProps);
|
|
15
14
|
componentDidMount(): void;
|
|
16
15
|
componentDidUpdate(): void;
|
|
17
16
|
init(): void;
|
|
18
|
-
maybeFlushSync(fn: () => void): void;
|
|
19
|
-
setRenderer(id: string, renderer: ReactRenderer): void;
|
|
20
|
-
removeRenderer(id: string): void;
|
|
21
17
|
componentWillUnmount(): void;
|
|
22
18
|
render(): React.JSX.Element;
|
|
23
19
|
}
|
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.
|
|
4
|
+
"version": "2.6.0",
|
|
5
5
|
"homepage": "https://tiptap.dev",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tiptap",
|
|
@@ -29,22 +29,22 @@
|
|
|
29
29
|
"dist"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@tiptap/extension-bubble-menu": "^2.
|
|
33
|
-
"@tiptap/extension-floating-menu": "^2.
|
|
32
|
+
"@tiptap/extension-bubble-menu": "^2.6.0",
|
|
33
|
+
"@tiptap/extension-floating-menu": "^2.6.0",
|
|
34
34
|
"@types/use-sync-external-store": "^0.0.6",
|
|
35
35
|
"use-sync-external-store": "^1.2.2"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@tiptap/core": "^2.
|
|
39
|
-
"@tiptap/pm": "^2.
|
|
38
|
+
"@tiptap/core": "^2.6.0",
|
|
39
|
+
"@tiptap/pm": "^2.6.0",
|
|
40
40
|
"@types/react": "^18.2.14",
|
|
41
41
|
"@types/react-dom": "^18.2.6",
|
|
42
42
|
"react": "^18.0.0",
|
|
43
43
|
"react-dom": "^18.0.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@tiptap/core": "^2.
|
|
47
|
-
"@tiptap/pm": "^2.
|
|
46
|
+
"@tiptap/core": "^2.6.0",
|
|
47
|
+
"@tiptap/pm": "^2.6.0",
|
|
48
48
|
"react": "^17.0.0 || ^18.0.0",
|
|
49
49
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
50
50
|
},
|
package/src/Context.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { Editor } from '@tiptap/core'
|
|
1
2
|
import React, { createContext, ReactNode, useContext } from 'react'
|
|
2
3
|
|
|
3
|
-
import { Editor } from './Editor.js'
|
|
4
|
+
import { Editor as ReactEditor } from './Editor.js'
|
|
4
5
|
import { EditorContent } from './EditorContent.js'
|
|
5
6
|
import { useEditor, UseEditorOptions } from './useEditor.js'
|
|
6
7
|
|
|
@@ -44,7 +45,7 @@ export function EditorProvider({
|
|
|
44
45
|
{slotBefore}
|
|
45
46
|
<EditorConsumer>
|
|
46
47
|
{({ editor: currentEditor }) => (
|
|
47
|
-
<EditorContent editor={currentEditor} />
|
|
48
|
+
<EditorContent editor={currentEditor as ReactEditor} />
|
|
48
49
|
)}
|
|
49
50
|
</EditorConsumer>
|
|
50
51
|
{children}
|
package/src/Editor.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Editor as CoreEditor } from '@tiptap/core'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
|
-
import { EditorContentProps, EditorContentState } from './EditorContent.js'
|
|
5
4
|
import { ReactRenderer } from './ReactRenderer.js'
|
|
6
5
|
|
|
7
|
-
type ContentComponent =
|
|
6
|
+
type ContentComponent = {
|
|
8
7
|
setRenderer(id: string, renderer: ReactRenderer): void;
|
|
9
8
|
removeRenderer(id: string): void;
|
|
9
|
+
subscribe: (callback: () => void) => () => void;
|
|
10
|
+
getSnapshot: () => Record<string, React.ReactPortal>;
|
|
11
|
+
getServerSnapshot: () => Record<string, React.ReactPortal>;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export class Editor extends CoreEditor {
|
package/src/EditorContent.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, {
|
|
2
2
|
ForwardedRef, forwardRef, HTMLProps, LegacyRef, MutableRefObject,
|
|
3
3
|
} from 'react'
|
|
4
|
-
import ReactDOM
|
|
4
|
+
import ReactDOM from 'react-dom'
|
|
5
|
+
import { useSyncExternalStore } from 'use-sync-external-store/shim'
|
|
5
6
|
|
|
6
7
|
import { Editor } from './Editor.js'
|
|
7
8
|
import { ReactRenderer } from './ReactRenderer.js'
|
|
@@ -20,12 +21,23 @@ const mergeRefs = <T extends HTMLDivElement>(
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
/**
|
|
25
|
+
* This component renders all of the editor's node views.
|
|
26
|
+
*/
|
|
27
|
+
const Portals: React.FC<{ contentComponent: Exclude<Editor['contentComponent'], null> }> = ({
|
|
28
|
+
contentComponent,
|
|
29
|
+
}) => {
|
|
30
|
+
// For performance reasons, we render the node view portals on state changes only
|
|
31
|
+
const renderers = useSyncExternalStore(
|
|
32
|
+
contentComponent.subscribe,
|
|
33
|
+
contentComponent.getSnapshot,
|
|
34
|
+
contentComponent.getServerSnapshot,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// This allows us to directly render the portals without any additional wrapper
|
|
24
38
|
return (
|
|
25
39
|
<>
|
|
26
|
-
{Object.
|
|
27
|
-
return ReactDOM.createPortal(renderer.reactElement, renderer.element, key)
|
|
28
|
-
})}
|
|
40
|
+
{Object.values(renderers)}
|
|
29
41
|
</>
|
|
30
42
|
)
|
|
31
43
|
}
|
|
@@ -35,22 +47,67 @@ export interface EditorContentProps extends HTMLProps<HTMLDivElement> {
|
|
|
35
47
|
innerRef?: ForwardedRef<HTMLDivElement | null>;
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
function getInstance(): Exclude<Editor['contentComponent'], null> {
|
|
51
|
+
const subscribers = new Set<() => void>()
|
|
52
|
+
let renderers: Record<string, React.ReactPortal> = {}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
/**
|
|
56
|
+
* Subscribe to the editor instance's changes.
|
|
57
|
+
*/
|
|
58
|
+
subscribe(callback: () => void) {
|
|
59
|
+
subscribers.add(callback)
|
|
60
|
+
return () => {
|
|
61
|
+
subscribers.delete(callback)
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
getSnapshot() {
|
|
65
|
+
return renderers
|
|
66
|
+
},
|
|
67
|
+
getServerSnapshot() {
|
|
68
|
+
return renderers
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* Adds a new NodeView Renderer to the editor.
|
|
72
|
+
*/
|
|
73
|
+
setRenderer(id: string, renderer: ReactRenderer) {
|
|
74
|
+
renderers = {
|
|
75
|
+
...renderers,
|
|
76
|
+
[id]: ReactDOM.createPortal(renderer.reactElement, renderer.element, id),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
subscribers.forEach(subscriber => subscriber())
|
|
80
|
+
},
|
|
81
|
+
/**
|
|
82
|
+
* Removes a NodeView Renderer from the editor.
|
|
83
|
+
*/
|
|
84
|
+
removeRenderer(id: string) {
|
|
85
|
+
const nextRenderers = { ...renderers }
|
|
86
|
+
|
|
87
|
+
delete nextRenderers[id]
|
|
88
|
+
renderers = nextRenderers
|
|
89
|
+
subscribers.forEach(subscriber => subscriber())
|
|
90
|
+
},
|
|
91
|
+
}
|
|
40
92
|
}
|
|
41
93
|
|
|
42
|
-
export class PureEditorContent extends React.Component<
|
|
94
|
+
export class PureEditorContent extends React.Component<
|
|
95
|
+
EditorContentProps,
|
|
96
|
+
{ hasContentComponentInitialized: boolean }
|
|
97
|
+
> {
|
|
43
98
|
editorContentRef: React.RefObject<any>
|
|
44
99
|
|
|
45
100
|
initialized: boolean
|
|
46
101
|
|
|
102
|
+
unsubscribeToContentComponent?: () => void
|
|
103
|
+
|
|
47
104
|
constructor(props: EditorContentProps) {
|
|
48
105
|
super(props)
|
|
49
106
|
this.editorContentRef = React.createRef()
|
|
50
107
|
this.initialized = false
|
|
51
108
|
|
|
52
109
|
this.state = {
|
|
53
|
-
|
|
110
|
+
hasContentComponentInitialized: Boolean(props.editor?.contentComponent),
|
|
54
111
|
}
|
|
55
112
|
}
|
|
56
113
|
|
|
@@ -78,7 +135,27 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|
|
78
135
|
element,
|
|
79
136
|
})
|
|
80
137
|
|
|
81
|
-
editor.contentComponent =
|
|
138
|
+
editor.contentComponent = getInstance()
|
|
139
|
+
|
|
140
|
+
// Has the content component been initialized?
|
|
141
|
+
if (!this.state.hasContentComponentInitialized) {
|
|
142
|
+
// Subscribe to the content component
|
|
143
|
+
this.unsubscribeToContentComponent = editor.contentComponent.subscribe(() => {
|
|
144
|
+
this.setState(prevState => {
|
|
145
|
+
if (!prevState.hasContentComponentInitialized) {
|
|
146
|
+
return {
|
|
147
|
+
hasContentComponentInitialized: true,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return prevState
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Unsubscribe to previous content component
|
|
154
|
+
if (this.unsubscribeToContentComponent) {
|
|
155
|
+
this.unsubscribeToContentComponent()
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
}
|
|
82
159
|
|
|
83
160
|
editor.createNodeViews()
|
|
84
161
|
|
|
@@ -86,41 +163,6 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|
|
86
163
|
}
|
|
87
164
|
}
|
|
88
165
|
|
|
89
|
-
maybeFlushSync(fn: () => void) {
|
|
90
|
-
// Avoid calling flushSync until the editor is initialized.
|
|
91
|
-
// Initialization happens during the componentDidMount or componentDidUpdate
|
|
92
|
-
// lifecycle methods, and React doesn't allow calling flushSync from inside
|
|
93
|
-
// a lifecycle method.
|
|
94
|
-
if (this.initialized) {
|
|
95
|
-
flushSync(fn)
|
|
96
|
-
} else {
|
|
97
|
-
fn()
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
setRenderer(id: string, renderer: ReactRenderer) {
|
|
102
|
-
this.maybeFlushSync(() => {
|
|
103
|
-
this.setState(({ renderers }) => ({
|
|
104
|
-
renderers: {
|
|
105
|
-
...renderers,
|
|
106
|
-
[id]: renderer,
|
|
107
|
-
},
|
|
108
|
-
}))
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
removeRenderer(id: string) {
|
|
113
|
-
this.maybeFlushSync(() => {
|
|
114
|
-
this.setState(({ renderers }) => {
|
|
115
|
-
const nextRenderers = { ...renderers }
|
|
116
|
-
|
|
117
|
-
delete nextRenderers[id]
|
|
118
|
-
|
|
119
|
-
return { renderers: nextRenderers }
|
|
120
|
-
})
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
|
|
124
166
|
componentWillUnmount() {
|
|
125
167
|
const { editor } = this.props
|
|
126
168
|
|
|
@@ -136,6 +178,10 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|
|
136
178
|
})
|
|
137
179
|
}
|
|
138
180
|
|
|
181
|
+
if (this.unsubscribeToContentComponent) {
|
|
182
|
+
this.unsubscribeToContentComponent()
|
|
183
|
+
}
|
|
184
|
+
|
|
139
185
|
editor.contentComponent = null
|
|
140
186
|
|
|
141
187
|
if (!editor.options.element.firstChild) {
|
|
@@ -158,7 +204,7 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|
|
158
204
|
<>
|
|
159
205
|
<div ref={mergeRefs(innerRef, this.editorContentRef)} {...rest} />
|
|
160
206
|
{/* @ts-ignore */}
|
|
161
|
-
<Portals
|
|
207
|
+
{editor?.contentComponent && <Portals contentComponent={editor.contentComponent} />}
|
|
162
208
|
</>
|
|
163
209
|
)
|
|
164
210
|
}
|
|
@@ -168,7 +214,7 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
|
|
|
168
214
|
const EditorContentWithKey = forwardRef<HTMLDivElement, EditorContentProps>(
|
|
169
215
|
(props: Omit<EditorContentProps, 'innerRef'>, ref) => {
|
|
170
216
|
const key = React.useMemo(() => {
|
|
171
|
-
return Math.floor(Math.random() *
|
|
217
|
+
return Math.floor(Math.random() * 0xffffffff).toString()
|
|
172
218
|
}, [props.editor])
|
|
173
219
|
|
|
174
220
|
// Can't use JSX here because it conflicts with the type definition of Vue's JSX, so use createElement
|
package/src/NodeViewWrapper.tsx
CHANGED
|
@@ -58,25 +58,23 @@ class ReactNodeView extends NodeView<
|
|
|
58
58
|
this.component.displayName = capitalizeFirstChar(this.extension.name)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (element && this.contentDOMElement && element.firstChild !== this.contentDOMElement) {
|
|
66
|
-
element.appendChild(this.contentDOMElement)
|
|
67
|
-
}
|
|
61
|
+
const onDragStart = this.onDragStart.bind(this)
|
|
62
|
+
const nodeViewContentRef: ReactNodeViewContextProps['nodeViewContentRef'] = element => {
|
|
63
|
+
if (element && this.contentDOMElement && element.firstChild !== this.contentDOMElement) {
|
|
64
|
+
element.appendChild(this.contentDOMElement)
|
|
68
65
|
}
|
|
69
|
-
|
|
66
|
+
}
|
|
67
|
+
const context = { onDragStart, nodeViewContentRef }
|
|
68
|
+
const Component = this.component
|
|
69
|
+
// For performance reasons, we memoize the provider component
|
|
70
|
+
// And all of the things it requires are declared outside of the component, so it doesn't need to re-render
|
|
71
|
+
const ReactNodeViewProvider: React.FunctionComponent = React.memo(componentProps => {
|
|
70
72
|
return (
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
{/* @ts-ignore */}
|
|
75
|
-
<Component {...componentProps} />
|
|
76
|
-
</ReactNodeViewContext.Provider>
|
|
77
|
-
</>
|
|
73
|
+
<ReactNodeViewContext.Provider value={context}>
|
|
74
|
+
{React.createElement(Component, componentProps)}
|
|
75
|
+
</ReactNodeViewContext.Provider>
|
|
78
76
|
)
|
|
79
|
-
}
|
|
77
|
+
})
|
|
80
78
|
|
|
81
79
|
ReactNodeViewProvider.displayName = 'ReactNodeView'
|
|
82
80
|
|
package/src/ReactRenderer.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Editor } from '@tiptap/core'
|
|
2
2
|
import React from 'react'
|
|
3
|
+
import { flushSync } from 'react-dom'
|
|
3
4
|
|
|
4
5
|
import { Editor as ExtendedEditor } from './Editor.js'
|
|
5
6
|
|
|
@@ -121,7 +122,15 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|
|
121
122
|
})
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
this.
|
|
125
|
+
if (this.editor.isInitialized) {
|
|
126
|
+
// On first render, we need to flush the render synchronously
|
|
127
|
+
// Renders afterwards can be async, but this fixes a cursor positioning issue
|
|
128
|
+
flushSync(() => {
|
|
129
|
+
this.render()
|
|
130
|
+
})
|
|
131
|
+
} else {
|
|
132
|
+
this.render()
|
|
133
|
+
}
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
render(): void {
|
|
@@ -134,7 +143,7 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|
|
134
143
|
}
|
|
135
144
|
}
|
|
136
145
|
|
|
137
|
-
this.reactElement =
|
|
146
|
+
this.reactElement = React.createElement(Component, props)
|
|
138
147
|
|
|
139
148
|
this.editor?.contentComponent?.setRenderer(this.id, this)
|
|
140
149
|
}
|