@tiptap/react 3.0.0-next.1 → 3.0.0-next.2
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 +255 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +155 -42
- package/dist/index.d.ts +155 -42
- package/dist/index.js +244 -113
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
- package/src/BubbleMenu.tsx +70 -50
- package/src/Context.tsx +14 -6
- package/src/FloatingMenu.tsx +51 -45
- package/src/ReactNodeViewRenderer.tsx +152 -41
- package/src/ReactRenderer.tsx +26 -19
- package/src/useEditor.ts +17 -9
- package/src/useEditorState.ts +49 -10
package/src/FloatingMenu.tsx
CHANGED
|
@@ -1,77 +1,83 @@
|
|
|
1
1
|
import { FloatingMenuPlugin, FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'
|
|
2
|
-
import React, {
|
|
3
|
-
useEffect, useRef,
|
|
4
|
-
} from 'react'
|
|
2
|
+
import React, { useEffect, useRef } from 'react'
|
|
5
3
|
import { createPortal } from 'react-dom'
|
|
6
4
|
|
|
7
5
|
import { useCurrentEditor } from './Context.js'
|
|
8
6
|
|
|
9
|
-
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K
|
|
7
|
+
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
|
10
8
|
|
|
11
|
-
export type FloatingMenuProps = Omit<
|
|
9
|
+
export type FloatingMenuProps = Omit<
|
|
10
|
+
Optional<FloatingMenuPluginProps, 'pluginKey'>,
|
|
11
|
+
'element' | 'editor'
|
|
12
|
+
> & {
|
|
12
13
|
editor: FloatingMenuPluginProps['editor'] | null;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
options?: FloatingMenuPluginProps['options']
|
|
16
|
-
}
|
|
14
|
+
options?: FloatingMenuPluginProps['options'];
|
|
15
|
+
} & React.HTMLAttributes<HTMLDivElement>;
|
|
17
16
|
|
|
18
|
-
export const FloatingMenu =
|
|
17
|
+
export const FloatingMenu = React.forwardRef<HTMLDivElement, FloatingMenuProps>(({
|
|
18
|
+
pluginKey = 'floatingMenu',
|
|
19
|
+
editor,
|
|
20
|
+
shouldShow = null,
|
|
21
|
+
options,
|
|
22
|
+
children,
|
|
23
|
+
...restProps
|
|
24
|
+
}, ref) => {
|
|
19
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
|
+
|
|
20
33
|
const { editor: currentEditor } = useCurrentEditor()
|
|
21
34
|
|
|
22
35
|
useEffect(() => {
|
|
23
|
-
menuEl.current
|
|
24
|
-
|
|
36
|
+
const floatingMenuElement = menuEl.current
|
|
37
|
+
|
|
38
|
+
floatingMenuElement.style.visibility = 'hidden'
|
|
39
|
+
floatingMenuElement.style.position = 'absolute'
|
|
25
40
|
|
|
26
|
-
if (
|
|
41
|
+
if (editor?.isDestroyed || currentEditor?.isDestroyed) {
|
|
27
42
|
return
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
const
|
|
31
|
-
pluginKey = 'floatingMenu',
|
|
32
|
-
editor,
|
|
33
|
-
options,
|
|
34
|
-
shouldShow = null,
|
|
35
|
-
} = props
|
|
36
|
-
|
|
37
|
-
const menuEditor = editor || currentEditor
|
|
45
|
+
const attachToEditor = editor || currentEditor
|
|
38
46
|
|
|
39
|
-
if (!
|
|
40
|
-
console.warn(
|
|
47
|
+
if (!attachToEditor) {
|
|
48
|
+
console.warn(
|
|
49
|
+
'FloatingMenu component is not rendered inside of an editor component or does not have editor prop.',
|
|
50
|
+
)
|
|
41
51
|
return
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
const plugin = FloatingMenuPlugin({
|
|
55
|
+
editor: attachToEditor,
|
|
56
|
+
element: floatingMenuElement,
|
|
45
57
|
pluginKey,
|
|
46
|
-
editor: menuEditor,
|
|
47
|
-
element: menuEl.current,
|
|
48
|
-
options,
|
|
49
58
|
shouldShow,
|
|
59
|
+
options,
|
|
50
60
|
})
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
attachToEditor.registerPlugin(plugin)
|
|
63
|
+
|
|
53
64
|
return () => {
|
|
54
|
-
|
|
65
|
+
attachToEditor.unregisterPlugin(pluginKey)
|
|
55
66
|
window.requestAnimationFrame(() => {
|
|
56
|
-
if (
|
|
57
|
-
|
|
67
|
+
if (floatingMenuElement.parentNode) {
|
|
68
|
+
floatingMenuElement.parentNode.removeChild(floatingMenuElement)
|
|
58
69
|
}
|
|
59
70
|
})
|
|
60
71
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
currentEditor,
|
|
64
|
-
])
|
|
65
|
-
|
|
66
|
-
const portal = createPortal(
|
|
67
|
-
(
|
|
68
|
-
<div className={props.className}>
|
|
69
|
-
{props.children}
|
|
70
|
-
</div>
|
|
71
|
-
), menuEl.current,
|
|
72
|
-
)
|
|
72
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
+
}, [editor, currentEditor])
|
|
73
74
|
|
|
74
|
-
return (
|
|
75
|
-
|
|
75
|
+
return createPortal(
|
|
76
|
+
<div
|
|
77
|
+
{...restProps}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</div>,
|
|
81
|
+
menuEl.current,
|
|
76
82
|
)
|
|
77
|
-
}
|
|
83
|
+
})
|
|
@@ -1,55 +1,90 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DecorationWithType,
|
|
3
3
|
Editor,
|
|
4
|
+
getRenderedAttributes,
|
|
4
5
|
NodeView,
|
|
5
6
|
NodeViewProps,
|
|
6
7
|
NodeViewRenderer,
|
|
7
8
|
NodeViewRendererOptions,
|
|
8
|
-
NodeViewRendererProps,
|
|
9
9
|
} from '@tiptap/core'
|
|
10
|
-
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
|
11
|
-
import { Decoration, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
|
|
12
|
-
import React from 'react'
|
|
10
|
+
import { Node, Node as ProseMirrorNode } from '@tiptap/pm/model'
|
|
11
|
+
import { Decoration, DecorationSource, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
|
|
12
|
+
import React, { ComponentType } from 'react'
|
|
13
13
|
|
|
14
14
|
import { EditorWithContentComponent } from './Editor.js'
|
|
15
15
|
import { ReactRenderer } from './ReactRenderer.js'
|
|
16
16
|
import { ReactNodeViewContext, ReactNodeViewContextProps } from './useReactNodeView.js'
|
|
17
17
|
|
|
18
18
|
export interface ReactNodeViewRendererOptions extends NodeViewRendererOptions {
|
|
19
|
+
/**
|
|
20
|
+
* This function is called when the node view is updated.
|
|
21
|
+
* It allows you to compare the old node with the new node and decide if the component should update.
|
|
22
|
+
*/
|
|
19
23
|
update:
|
|
20
24
|
| ((props: {
|
|
21
|
-
oldNode: ProseMirrorNode
|
|
22
|
-
oldDecorations: Decoration[]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
oldNode: ProseMirrorNode;
|
|
26
|
+
oldDecorations: readonly Decoration[];
|
|
27
|
+
oldInnerDecorations: DecorationSource;
|
|
28
|
+
newNode: ProseMirrorNode;
|
|
29
|
+
newDecorations: readonly Decoration[];
|
|
30
|
+
innerDecorations: DecorationSource;
|
|
31
|
+
updateProps: () => void;
|
|
26
32
|
}) => boolean)
|
|
27
|
-
| null
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
| null;
|
|
34
|
+
/**
|
|
35
|
+
* The tag name of the element wrapping the React component.
|
|
36
|
+
*/
|
|
37
|
+
as?: string;
|
|
38
|
+
/**
|
|
39
|
+
* The class name of the element wrapping the React component.
|
|
40
|
+
*/
|
|
41
|
+
className?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Attributes that should be applied to the element wrapping the React component.
|
|
44
|
+
* If this is a function, it will be called each time the node view is updated.
|
|
45
|
+
* If this is an object, it will be applied once when the node view is mounted.
|
|
46
|
+
*/
|
|
47
|
+
attrs?:
|
|
48
|
+
| Record<string, string>
|
|
49
|
+
| ((props: {
|
|
50
|
+
node: ProseMirrorNode;
|
|
51
|
+
HTMLAttributes: Record<string, any>;
|
|
52
|
+
}) => Record<string, string>);
|
|
31
53
|
}
|
|
32
54
|
|
|
33
|
-
class ReactNodeView
|
|
34
|
-
|
|
35
|
-
Editor,
|
|
36
|
-
ReactNodeViewRendererOptions
|
|
37
|
-
> {
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
export class ReactNodeView<
|
|
56
|
+
Component extends ComponentType<NodeViewProps> = ComponentType<NodeViewProps>,
|
|
57
|
+
NodeEditor extends Editor = Editor,
|
|
58
|
+
Options extends ReactNodeViewRendererOptions = ReactNodeViewRendererOptions,
|
|
59
|
+
> extends NodeView<Component, NodeEditor, Options> {
|
|
60
|
+
/**
|
|
61
|
+
* The renderer instance.
|
|
62
|
+
*/
|
|
63
|
+
renderer!: ReactRenderer<unknown, NodeViewProps>
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The element that holds the rich-text content of the node.
|
|
67
|
+
*/
|
|
40
68
|
contentDOMElement!: HTMLElement | null
|
|
41
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Setup the React component.
|
|
72
|
+
* Called on initialization.
|
|
73
|
+
*/
|
|
42
74
|
mount() {
|
|
43
|
-
const props
|
|
75
|
+
const props = {
|
|
44
76
|
editor: this.editor,
|
|
45
77
|
node: this.node,
|
|
46
|
-
decorations: this.decorations,
|
|
78
|
+
decorations: this.decorations as DecorationWithType[],
|
|
79
|
+
innerDecorations: this.innerDecorations,
|
|
80
|
+
view: this.view,
|
|
47
81
|
selected: false,
|
|
48
82
|
extension: this.extension,
|
|
83
|
+
HTMLAttributes: this.HTMLAttributes,
|
|
49
84
|
getPos: () => this.getPos(),
|
|
50
85
|
updateAttributes: (attributes = {}) => this.updateAttributes(attributes),
|
|
51
86
|
deleteNode: () => this.deleteNode(),
|
|
52
|
-
}
|
|
87
|
+
} satisfies NodeViewProps
|
|
53
88
|
|
|
54
89
|
if (!(this.component as any).displayName) {
|
|
55
90
|
const capitalizeFirstChar = (string: string): string => {
|
|
@@ -69,13 +104,15 @@ class ReactNodeView extends NodeView<
|
|
|
69
104
|
const Component = this.component
|
|
70
105
|
// For performance reasons, we memoize the provider component
|
|
71
106
|
// And all of the things it requires are declared outside of the component, so it doesn't need to re-render
|
|
72
|
-
const ReactNodeViewProvider: React.FunctionComponent = React.memo(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
)
|
|
79
116
|
|
|
80
117
|
ReactNodeViewProvider.displayName = 'ReactNodeView'
|
|
81
118
|
|
|
@@ -88,6 +125,7 @@ class ReactNodeView extends NodeView<
|
|
|
88
125
|
}
|
|
89
126
|
|
|
90
127
|
if (this.contentDOMElement) {
|
|
128
|
+
this.contentDOMElement.dataset.nodeViewContentReact = ''
|
|
91
129
|
// For some reason the whiteSpace prop is not inherited properly in Chrome and Safari
|
|
92
130
|
// With this fix it seems to work fine
|
|
93
131
|
// See: https://github.com/ueberdosis/tiptap/issues/1197
|
|
@@ -110,10 +148,15 @@ class ReactNodeView extends NodeView<
|
|
|
110
148
|
props,
|
|
111
149
|
as,
|
|
112
150
|
className: `node-${this.node.type.name} ${className}`.trim(),
|
|
113
|
-
attrs: this.options.attrs,
|
|
114
151
|
})
|
|
152
|
+
|
|
153
|
+
this.updateElementAttributes()
|
|
115
154
|
}
|
|
116
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Return the DOM element.
|
|
158
|
+
* This is the element that will be used to display the node view.
|
|
159
|
+
*/
|
|
117
160
|
get dom() {
|
|
118
161
|
if (
|
|
119
162
|
this.renderer.element.firstElementChild
|
|
@@ -125,6 +168,10 @@ class ReactNodeView extends NodeView<
|
|
|
125
168
|
return this.renderer.element as HTMLElement
|
|
126
169
|
}
|
|
127
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Return the content DOM element.
|
|
173
|
+
* This is the element that will be used to display the rich-text content of the node.
|
|
174
|
+
*/
|
|
128
175
|
get contentDOM() {
|
|
129
176
|
if (this.node.isLeaf) {
|
|
130
177
|
return null
|
|
@@ -133,10 +180,19 @@ class ReactNodeView extends NodeView<
|
|
|
133
180
|
return this.contentDOMElement
|
|
134
181
|
}
|
|
135
182
|
|
|
183
|
+
/**
|
|
184
|
+
* On editor selection update, check if the node is selected.
|
|
185
|
+
* If it is, call `selectNode`, otherwise call `deselectNode`.
|
|
186
|
+
*/
|
|
136
187
|
handleSelectionUpdate() {
|
|
137
188
|
const { from, to } = this.editor.state.selection
|
|
189
|
+
const pos = this.getPos()
|
|
190
|
+
|
|
191
|
+
if (typeof pos !== 'number') {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
138
194
|
|
|
139
|
-
if (from <=
|
|
195
|
+
if (from <= pos && to >= pos + this.node.nodeSize) {
|
|
140
196
|
if (this.renderer.props.selected) {
|
|
141
197
|
return
|
|
142
198
|
}
|
|
@@ -151,9 +207,20 @@ class ReactNodeView extends NodeView<
|
|
|
151
207
|
}
|
|
152
208
|
}
|
|
153
209
|
|
|
154
|
-
|
|
155
|
-
|
|
210
|
+
/**
|
|
211
|
+
* On update, update the React component.
|
|
212
|
+
* To prevent unnecessary updates, the `update` option can be used.
|
|
213
|
+
*/
|
|
214
|
+
update(
|
|
215
|
+
node: Node,
|
|
216
|
+
decorations: readonly Decoration[],
|
|
217
|
+
innerDecorations: DecorationSource,
|
|
218
|
+
): boolean {
|
|
219
|
+
const rerenderComponent = (props?: Record<string, any>) => {
|
|
156
220
|
this.renderer.updateProps(props)
|
|
221
|
+
if (typeof this.options.attrs === 'function') {
|
|
222
|
+
this.updateElementAttributes()
|
|
223
|
+
}
|
|
157
224
|
}
|
|
158
225
|
|
|
159
226
|
if (node.type !== this.node.type) {
|
|
@@ -163,31 +230,44 @@ class ReactNodeView extends NodeView<
|
|
|
163
230
|
if (typeof this.options.update === 'function') {
|
|
164
231
|
const oldNode = this.node
|
|
165
232
|
const oldDecorations = this.decorations
|
|
233
|
+
const oldInnerDecorations = this.innerDecorations
|
|
166
234
|
|
|
167
235
|
this.node = node
|
|
168
236
|
this.decorations = decorations
|
|
237
|
+
this.innerDecorations = innerDecorations
|
|
169
238
|
|
|
170
239
|
return this.options.update({
|
|
171
240
|
oldNode,
|
|
172
241
|
oldDecorations,
|
|
173
242
|
newNode: node,
|
|
174
243
|
newDecorations: decorations,
|
|
175
|
-
|
|
244
|
+
oldInnerDecorations,
|
|
245
|
+
innerDecorations,
|
|
246
|
+
updateProps: () => rerenderComponent({ node, decorations, innerDecorations }),
|
|
176
247
|
})
|
|
177
248
|
}
|
|
178
249
|
|
|
179
|
-
if (
|
|
250
|
+
if (
|
|
251
|
+
node === this.node
|
|
252
|
+
&& this.decorations === decorations
|
|
253
|
+
&& this.innerDecorations === innerDecorations
|
|
254
|
+
) {
|
|
180
255
|
return true
|
|
181
256
|
}
|
|
182
257
|
|
|
183
258
|
this.node = node
|
|
184
259
|
this.decorations = decorations
|
|
260
|
+
this.innerDecorations = innerDecorations
|
|
185
261
|
|
|
186
|
-
|
|
262
|
+
rerenderComponent({ node, decorations, innerDecorations })
|
|
187
263
|
|
|
188
264
|
return true
|
|
189
265
|
}
|
|
190
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Select the node.
|
|
269
|
+
* Add the `selected` prop and the `ProseMirror-selectednode` class.
|
|
270
|
+
*/
|
|
191
271
|
selectNode() {
|
|
192
272
|
this.renderer.updateProps({
|
|
193
273
|
selected: true,
|
|
@@ -195,6 +275,10 @@ class ReactNodeView extends NodeView<
|
|
|
195
275
|
this.renderer.element.classList.add('ProseMirror-selectednode')
|
|
196
276
|
}
|
|
197
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Deselect the node.
|
|
280
|
+
* Remove the `selected` prop and the `ProseMirror-selectednode` class.
|
|
281
|
+
*/
|
|
198
282
|
deselectNode() {
|
|
199
283
|
this.renderer.updateProps({
|
|
200
284
|
selected: false,
|
|
@@ -202,25 +286,52 @@ class ReactNodeView extends NodeView<
|
|
|
202
286
|
this.renderer.element.classList.remove('ProseMirror-selectednode')
|
|
203
287
|
}
|
|
204
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Destroy the React component instance.
|
|
291
|
+
*/
|
|
205
292
|
destroy() {
|
|
206
293
|
this.renderer.destroy()
|
|
207
294
|
this.editor.off('selectionUpdate', this.handleSelectionUpdate)
|
|
208
295
|
this.contentDOMElement = null
|
|
209
296
|
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Update the attributes of the top-level element that holds the React component.
|
|
300
|
+
* Applying the attributes defined in the `attrs` option.
|
|
301
|
+
*/
|
|
302
|
+
updateElementAttributes() {
|
|
303
|
+
if (this.options.attrs) {
|
|
304
|
+
let attrsObj: Record<string, string> = {}
|
|
305
|
+
|
|
306
|
+
if (typeof this.options.attrs === 'function') {
|
|
307
|
+
const extensionAttributes = this.editor.extensionManager.attributes
|
|
308
|
+
const HTMLAttributes = getRenderedAttributes(this.node, extensionAttributes)
|
|
309
|
+
|
|
310
|
+
attrsObj = this.options.attrs({ node: this.node, HTMLAttributes })
|
|
311
|
+
} else {
|
|
312
|
+
attrsObj = this.options.attrs
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.renderer.updateAttributes(attrsObj)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
210
318
|
}
|
|
211
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Create a React node view renderer.
|
|
322
|
+
*/
|
|
212
323
|
export function ReactNodeViewRenderer(
|
|
213
|
-
component:
|
|
324
|
+
component: ComponentType<NodeViewProps>,
|
|
214
325
|
options?: Partial<ReactNodeViewRendererOptions>,
|
|
215
326
|
): NodeViewRenderer {
|
|
216
|
-
return
|
|
327
|
+
return props => {
|
|
217
328
|
// try to get the parent component
|
|
218
329
|
// this is important for vue devtools to show the component hierarchy correctly
|
|
219
330
|
// maybe it’s `undefined` because <editor-content> isn’t rendered yet
|
|
220
331
|
if (!(props.editor as EditorWithContentComponent).contentComponent) {
|
|
221
|
-
return {}
|
|
332
|
+
return {} as unknown as ProseMirrorNodeView
|
|
222
333
|
}
|
|
223
334
|
|
|
224
|
-
return new ReactNodeView(component, props, options)
|
|
335
|
+
return new ReactNodeView(component, props, options)
|
|
225
336
|
}
|
|
226
337
|
}
|
package/src/ReactRenderer.tsx
CHANGED
|
@@ -57,14 +57,6 @@ export interface ReactRendererOptions {
|
|
|
57
57
|
* @example 'foo bar'
|
|
58
58
|
*/
|
|
59
59
|
className?: string,
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* The attributes of the element.
|
|
63
|
-
* @type {Record<string, string>}
|
|
64
|
-
* @default {}
|
|
65
|
-
* @example { 'data-foo': 'bar' }
|
|
66
|
-
*/
|
|
67
|
-
attrs?: Record<string, string>,
|
|
68
60
|
}
|
|
69
61
|
|
|
70
62
|
type ComponentType<R, P> =
|
|
@@ -83,7 +75,7 @@ type ComponentType<R, P> =
|
|
|
83
75
|
* as: 'span',
|
|
84
76
|
* })
|
|
85
77
|
*/
|
|
86
|
-
export class ReactRenderer<R = unknown, P =
|
|
78
|
+
export class ReactRenderer<R = unknown, P extends Record<string, any> = {}> {
|
|
87
79
|
id: string
|
|
88
80
|
|
|
89
81
|
editor: Editor
|
|
@@ -92,23 +84,25 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|
|
92
84
|
|
|
93
85
|
element: Element
|
|
94
86
|
|
|
95
|
-
props:
|
|
87
|
+
props: P
|
|
96
88
|
|
|
97
89
|
reactElement: React.ReactNode
|
|
98
90
|
|
|
99
91
|
ref: R | null = null
|
|
100
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Immediately creates element and renders the provided React component.
|
|
95
|
+
*/
|
|
101
96
|
constructor(component: ComponentType<R, P>, {
|
|
102
97
|
editor,
|
|
103
98
|
props = {},
|
|
104
99
|
as = 'div',
|
|
105
100
|
className = '',
|
|
106
|
-
attrs,
|
|
107
101
|
}: ReactRendererOptions) {
|
|
108
102
|
this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString()
|
|
109
103
|
this.component = component
|
|
110
104
|
this.editor = editor as EditorWithContentComponent
|
|
111
|
-
this.props = props
|
|
105
|
+
this.props = props as P
|
|
112
106
|
this.element = document.createElement(as)
|
|
113
107
|
this.element.classList.add('react-renderer')
|
|
114
108
|
|
|
@@ -116,12 +110,6 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|
|
116
110
|
this.element.classList.add(...className.split(' '))
|
|
117
111
|
}
|
|
118
112
|
|
|
119
|
-
if (attrs) {
|
|
120
|
-
Object.keys(attrs).forEach(key => {
|
|
121
|
-
this.element.setAttribute(key, attrs[key])
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
|
|
125
113
|
if (this.editor.isInitialized) {
|
|
126
114
|
// On first render, we need to flush the render synchronously
|
|
127
115
|
// Renders afterwards can be async, but this fixes a cursor positioning issue
|
|
@@ -133,22 +121,29 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|
|
133
121
|
}
|
|
134
122
|
}
|
|
135
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Render the React component.
|
|
126
|
+
*/
|
|
136
127
|
render(): void {
|
|
137
128
|
const Component = this.component
|
|
138
129
|
const props = this.props
|
|
139
130
|
const editor = this.editor as EditorWithContentComponent
|
|
140
131
|
|
|
141
132
|
if (isClassComponent(Component) || isForwardRefComponent(Component)) {
|
|
133
|
+
// @ts-ignore This is a hack to make the ref work
|
|
142
134
|
props.ref = (ref: R) => {
|
|
143
135
|
this.ref = ref
|
|
144
136
|
}
|
|
145
137
|
}
|
|
146
138
|
|
|
147
|
-
this.reactElement =
|
|
139
|
+
this.reactElement = <Component {...props} />
|
|
148
140
|
|
|
149
141
|
editor?.contentComponent?.setRenderer(this.id, this)
|
|
150
142
|
}
|
|
151
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Re-renders the React component with new props.
|
|
146
|
+
*/
|
|
152
147
|
updateProps(props: Record<string, any> = {}): void {
|
|
153
148
|
this.props = {
|
|
154
149
|
...this.props,
|
|
@@ -158,9 +153,21 @@ export class ReactRenderer<R = unknown, P = unknown> {
|
|
|
158
153
|
this.render()
|
|
159
154
|
}
|
|
160
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Destroy the React component.
|
|
158
|
+
*/
|
|
161
159
|
destroy(): void {
|
|
162
160
|
const editor = this.editor as EditorWithContentComponent
|
|
163
161
|
|
|
164
162
|
editor?.contentComponent?.removeRenderer(this.id)
|
|
165
163
|
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Update the attributes of the element that holds the React component.
|
|
167
|
+
*/
|
|
168
|
+
updateAttributes(attributes: Record<string, string>): void {
|
|
169
|
+
Object.keys(attributes).forEach(key => {
|
|
170
|
+
this.element.setAttribute(key, attributes[key])
|
|
171
|
+
})
|
|
172
|
+
}
|
|
166
173
|
}
|
package/src/useEditor.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type UseEditorOptions = Partial<EditorOptions> & {
|
|
|
29
29
|
/**
|
|
30
30
|
* Whether to re-render the editor on each transaction.
|
|
31
31
|
* This is legacy behavior that will be removed in future versions.
|
|
32
|
-
* @default
|
|
32
|
+
* @default false
|
|
33
33
|
*/
|
|
34
34
|
shouldRerenderOnTransaction?: boolean;
|
|
35
35
|
};
|
|
@@ -78,6 +78,7 @@ class EditorInstanceManager {
|
|
|
78
78
|
this.options = options
|
|
79
79
|
this.subscriptions = new Set<() => void>()
|
|
80
80
|
this.setEditor(this.getInitialEditor())
|
|
81
|
+
this.scheduleDestroy()
|
|
81
82
|
|
|
82
83
|
this.getEditor = this.getEditor.bind(this)
|
|
83
84
|
this.getServerSnapshot = this.getServerSnapshot.bind(this)
|
|
@@ -147,6 +148,8 @@ class EditorInstanceManager {
|
|
|
147
148
|
onTransaction: (...args) => this.options.current.onTransaction?.(...args),
|
|
148
149
|
onUpdate: (...args) => this.options.current.onUpdate?.(...args),
|
|
149
150
|
onContentError: (...args) => this.options.current.onContentError?.(...args),
|
|
151
|
+
onDrop: (...args) => this.options.current.onDrop?.(...args),
|
|
152
|
+
onPaste: (...args) => this.options.current.onPaste?.(...args),
|
|
150
153
|
}
|
|
151
154
|
const editor = new Editor(optionsToApply)
|
|
152
155
|
|
|
@@ -195,7 +198,10 @@ class EditorInstanceManager {
|
|
|
195
198
|
if (this.editor && !this.editor.isDestroyed && deps.length === 0) {
|
|
196
199
|
// if the editor does exist & deps are empty, we don't need to re-initialize the editor
|
|
197
200
|
// we can fast-path to update the editor options on the existing instance
|
|
198
|
-
this.editor.setOptions(
|
|
201
|
+
this.editor.setOptions({
|
|
202
|
+
...this.options.current,
|
|
203
|
+
editable: this.editor.isEditable,
|
|
204
|
+
})
|
|
199
205
|
} else {
|
|
200
206
|
// When the editor:
|
|
201
207
|
// - does not yet exist
|
|
@@ -252,10 +258,10 @@ class EditorInstanceManager {
|
|
|
252
258
|
const currentInstanceId = this.instanceId
|
|
253
259
|
const currentEditor = this.editor
|
|
254
260
|
|
|
255
|
-
// Wait
|
|
261
|
+
// Wait two ticks to see if the component is still mounted
|
|
256
262
|
this.scheduledDestructionTimeout = setTimeout(() => {
|
|
257
263
|
if (this.isComponentMounted && this.instanceId === currentInstanceId) {
|
|
258
|
-
// If still mounted on the
|
|
264
|
+
// If still mounted on the following tick, with the same instanceId, do not destroy the editor
|
|
259
265
|
if (currentEditor) {
|
|
260
266
|
// just re-apply options as they might have changed
|
|
261
267
|
currentEditor.setOptions(this.options.current)
|
|
@@ -268,7 +274,9 @@ class EditorInstanceManager {
|
|
|
268
274
|
this.setEditor(null)
|
|
269
275
|
}
|
|
270
276
|
}
|
|
271
|
-
|
|
277
|
+
// This allows the effect to run again between ticks
|
|
278
|
+
// which may save us from having to re-create the editor
|
|
279
|
+
}, 1)
|
|
272
280
|
}
|
|
273
281
|
}
|
|
274
282
|
|
|
@@ -280,9 +288,9 @@ class EditorInstanceManager {
|
|
|
280
288
|
* @example const editor = useEditor({ extensions: [...] })
|
|
281
289
|
*/
|
|
282
290
|
export function useEditor(
|
|
283
|
-
options: UseEditorOptions & { immediatelyRender:
|
|
291
|
+
options: UseEditorOptions & { immediatelyRender: false },
|
|
284
292
|
deps?: DependencyList
|
|
285
|
-
): Editor;
|
|
293
|
+
): Editor | null;
|
|
286
294
|
|
|
287
295
|
/**
|
|
288
296
|
* This hook allows you to create an editor instance.
|
|
@@ -291,7 +299,7 @@ export function useEditor(
|
|
|
291
299
|
* @returns The editor instance
|
|
292
300
|
* @example const editor = useEditor({ extensions: [...] })
|
|
293
301
|
*/
|
|
294
|
-
export function useEditor(options
|
|
302
|
+
export function useEditor(options: UseEditorOptions, deps?: DependencyList): Editor;
|
|
295
303
|
|
|
296
304
|
export function useEditor(
|
|
297
305
|
options: UseEditorOptions = {},
|
|
@@ -320,7 +328,7 @@ export function useEditor(
|
|
|
320
328
|
useEditorState({
|
|
321
329
|
editor,
|
|
322
330
|
selector: ({ transactionNumber }) => {
|
|
323
|
-
if (options.shouldRerenderOnTransaction === false) {
|
|
331
|
+
if (options.shouldRerenderOnTransaction === false || options.shouldRerenderOnTransaction === undefined) {
|
|
324
332
|
// This will prevent the editor from re-rendering on each transaction
|
|
325
333
|
return null
|
|
326
334
|
}
|