@tiptap/react 2.5.0-pre.8 → 3.0.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 +685 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +686 -48
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +685 -46
- package/dist/index.umd.js.map +1 -1
- package/dist/packages/react/src/Context.d.ts +11 -3
- package/dist/packages/react/src/index.d.ts +1 -0
- package/dist/packages/react/src/useEditor.d.ts +29 -1
- package/dist/packages/react/src/useEditorState.d.ts +22 -0
- package/package.json +9 -7
- package/src/Context.tsx +13 -6
- package/src/index.ts +1 -0
- package/src/useEditor.ts +168 -45
- package/src/useEditorState.ts +125 -0
package/src/useEditor.ts
CHANGED
|
@@ -1,12 +1,45 @@
|
|
|
1
1
|
import { EditorOptions } from '@tiptap/core'
|
|
2
2
|
import {
|
|
3
|
-
DependencyList,
|
|
4
|
-
useEffect,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
3
|
+
DependencyList, useDebugValue, useEffect, useRef, useState,
|
|
7
4
|
} from 'react'
|
|
8
5
|
|
|
9
6
|
import { Editor } from './Editor.js'
|
|
7
|
+
import { useEditorState } from './useEditorState.js'
|
|
8
|
+
|
|
9
|
+
const isDev = process.env.NODE_ENV !== 'production'
|
|
10
|
+
const isSSR = typeof window === 'undefined'
|
|
11
|
+
const isNext = isSSR || Boolean(typeof window !== 'undefined' && (window as any).next)
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The options for the `useEditor` hook.
|
|
15
|
+
*/
|
|
16
|
+
export type UseEditorOptions = Partial<EditorOptions> & {
|
|
17
|
+
/**
|
|
18
|
+
* Whether to render the editor on the first render.
|
|
19
|
+
* If client-side rendering, set this to `true`.
|
|
20
|
+
* If server-side rendering, set this to `false`.
|
|
21
|
+
* @default true
|
|
22
|
+
*/
|
|
23
|
+
immediatelyRender?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Whether to re-render the editor on each transaction.
|
|
26
|
+
* This is legacy behavior that will be removed in future versions.
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
shouldRerenderOnTransaction?: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* This hook allows you to create an editor instance.
|
|
34
|
+
* @param options The editor options
|
|
35
|
+
* @param deps The dependencies to watch for changes
|
|
36
|
+
* @returns The editor instance
|
|
37
|
+
* @example const editor = useEditor({ extensions: [...] })
|
|
38
|
+
*/
|
|
39
|
+
export function useEditor(
|
|
40
|
+
options: UseEditorOptions & { immediatelyRender: true },
|
|
41
|
+
deps?: DependencyList
|
|
42
|
+
): Editor;
|
|
10
43
|
|
|
11
44
|
/**
|
|
12
45
|
* This hook allows you to create an editor instance.
|
|
@@ -15,9 +48,68 @@ import { Editor } from './Editor.js'
|
|
|
15
48
|
* @returns The editor instance
|
|
16
49
|
* @example const editor = useEditor({ extensions: [...] })
|
|
17
50
|
*/
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
51
|
+
export function useEditor(
|
|
52
|
+
options?: UseEditorOptions,
|
|
53
|
+
deps?: DependencyList
|
|
54
|
+
): Editor | null;
|
|
55
|
+
|
|
56
|
+
export function useEditor(
|
|
57
|
+
options: UseEditorOptions = {},
|
|
58
|
+
deps: DependencyList = [],
|
|
59
|
+
): Editor | null {
|
|
60
|
+
const isMounted = useRef(false)
|
|
61
|
+
const [editor, setEditor] = useState(() => {
|
|
62
|
+
if (options.immediatelyRender === undefined) {
|
|
63
|
+
if (isSSR || isNext) {
|
|
64
|
+
// TODO in the next major release, we should throw an error here
|
|
65
|
+
if (isDev) {
|
|
66
|
+
/**
|
|
67
|
+
* Throw an error in development, to make sure the developer is aware that tiptap cannot be SSR'd
|
|
68
|
+
* and that they need to set `immediatelyRender` to `false` to avoid hydration mismatches.
|
|
69
|
+
*/
|
|
70
|
+
console.warn(
|
|
71
|
+
'Tiptap Error: SSR has been detected, please set `immediatelyRender` explicitly to `false` to avoid hydration mismatches.',
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Best faith effort in production, run the code in the legacy mode to avoid hydration mismatches and errors in production
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Default to immediately rendering when client-side rendering
|
|
80
|
+
return new Editor(options)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (options.immediatelyRender && isSSR && isDev) {
|
|
84
|
+
// Warn in development, to make sure the developer is aware that tiptap cannot be SSR'd, set `immediatelyRender` to `false` to avoid hydration mismatches.
|
|
85
|
+
throw new Error(
|
|
86
|
+
'Tiptap Error: SSR has been detected, and `immediatelyRender` has been set to `true` this is an unsupported configuration that may result in errors, explicitly set `immediatelyRender` to `false` to avoid hydration mismatches.',
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (options.immediatelyRender) {
|
|
91
|
+
return new Editor(options)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
useDebugValue(editor)
|
|
98
|
+
|
|
99
|
+
// This effect will handle creating/updating the editor instance
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
let editorInstance: Editor | null = editor
|
|
102
|
+
|
|
103
|
+
if (!editorInstance) {
|
|
104
|
+
editorInstance = new Editor(options)
|
|
105
|
+
// instantiate the editor if it doesn't exist
|
|
106
|
+
// for ssr, this is the first time the editor is created
|
|
107
|
+
setEditor(editorInstance)
|
|
108
|
+
} else {
|
|
109
|
+
// if the editor does exist, update the editor options accordingly
|
|
110
|
+
editorInstance.setOptions(options)
|
|
111
|
+
}
|
|
112
|
+
}, deps)
|
|
21
113
|
|
|
22
114
|
const {
|
|
23
115
|
onBeforeCreate,
|
|
@@ -44,96 +136,127 @@ export const useEditor = (options: Partial<EditorOptions> = {}, deps: Dependency
|
|
|
44
136
|
// This effect will handle updating the editor instance
|
|
45
137
|
// when the event handlers change.
|
|
46
138
|
useEffect(() => {
|
|
47
|
-
if (!
|
|
139
|
+
if (!editor) {
|
|
48
140
|
return
|
|
49
141
|
}
|
|
50
142
|
|
|
51
143
|
if (onBeforeCreate) {
|
|
52
|
-
|
|
53
|
-
|
|
144
|
+
editor.off('beforeCreate', onBeforeCreateRef.current)
|
|
145
|
+
editor.on('beforeCreate', onBeforeCreate)
|
|
54
146
|
|
|
55
147
|
onBeforeCreateRef.current = onBeforeCreate
|
|
56
148
|
}
|
|
57
149
|
|
|
58
150
|
if (onBlur) {
|
|
59
|
-
|
|
60
|
-
|
|
151
|
+
editor.off('blur', onBlurRef.current)
|
|
152
|
+
editor.on('blur', onBlur)
|
|
61
153
|
|
|
62
154
|
onBlurRef.current = onBlur
|
|
63
155
|
}
|
|
64
156
|
|
|
65
157
|
if (onCreate) {
|
|
66
|
-
|
|
67
|
-
|
|
158
|
+
editor.off('create', onCreateRef.current)
|
|
159
|
+
editor.on('create', onCreate)
|
|
68
160
|
|
|
69
161
|
onCreateRef.current = onCreate
|
|
70
162
|
}
|
|
71
163
|
|
|
72
164
|
if (onDestroy) {
|
|
73
|
-
|
|
74
|
-
|
|
165
|
+
editor.off('destroy', onDestroyRef.current)
|
|
166
|
+
editor.on('destroy', onDestroy)
|
|
75
167
|
|
|
76
168
|
onDestroyRef.current = onDestroy
|
|
77
169
|
}
|
|
78
170
|
|
|
79
171
|
if (onFocus) {
|
|
80
|
-
|
|
81
|
-
|
|
172
|
+
editor.off('focus', onFocusRef.current)
|
|
173
|
+
editor.on('focus', onFocus)
|
|
82
174
|
|
|
83
175
|
onFocusRef.current = onFocus
|
|
84
176
|
}
|
|
85
177
|
|
|
86
178
|
if (onSelectionUpdate) {
|
|
87
|
-
|
|
88
|
-
|
|
179
|
+
editor.off('selectionUpdate', onSelectionUpdateRef.current)
|
|
180
|
+
editor.on('selectionUpdate', onSelectionUpdate)
|
|
89
181
|
|
|
90
182
|
onSelectionUpdateRef.current = onSelectionUpdate
|
|
91
183
|
}
|
|
92
184
|
|
|
93
185
|
if (onTransaction) {
|
|
94
|
-
|
|
95
|
-
|
|
186
|
+
editor.off('transaction', onTransactionRef.current)
|
|
187
|
+
editor.on('transaction', onTransaction)
|
|
96
188
|
|
|
97
189
|
onTransactionRef.current = onTransaction
|
|
98
190
|
}
|
|
99
191
|
|
|
100
192
|
if (onUpdate) {
|
|
101
|
-
|
|
102
|
-
|
|
193
|
+
editor.off('update', onUpdateRef.current)
|
|
194
|
+
editor.on('update', onUpdate)
|
|
103
195
|
|
|
104
196
|
onUpdateRef.current = onUpdate
|
|
105
197
|
}
|
|
106
198
|
|
|
107
199
|
if (onContentError) {
|
|
108
|
-
|
|
109
|
-
|
|
200
|
+
editor.off('contentError', onContentErrorRef.current)
|
|
201
|
+
editor.on('contentError', onContentError)
|
|
110
202
|
|
|
111
203
|
onContentErrorRef.current = onContentError
|
|
112
204
|
}
|
|
113
|
-
}, [
|
|
205
|
+
}, [
|
|
206
|
+
onBeforeCreate,
|
|
207
|
+
onBlur,
|
|
208
|
+
onCreate,
|
|
209
|
+
onDestroy,
|
|
210
|
+
onFocus,
|
|
211
|
+
onSelectionUpdate,
|
|
212
|
+
onTransaction,
|
|
213
|
+
onUpdate,
|
|
214
|
+
onContentError,
|
|
215
|
+
editor,
|
|
216
|
+
])
|
|
114
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Destroy the editor instance when the component completely unmounts
|
|
220
|
+
* As opposed to the cleanup function in the effect above, this will
|
|
221
|
+
* only be called when the component is removed from the DOM, since it has no deps.
|
|
222
|
+
* */
|
|
115
223
|
useEffect(() => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
224
|
+
isMounted.current = true
|
|
225
|
+
return () => {
|
|
226
|
+
isMounted.current = false
|
|
227
|
+
if (editor) {
|
|
228
|
+
// We need to destroy the editor asynchronously to avoid memory leaks
|
|
229
|
+
// because the editor instance is still being used in the component.
|
|
121
230
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
// re-use the editor instance if it hasn't been destroyed yet
|
|
233
|
+
// and the component is still mounted
|
|
234
|
+
// otherwise, asynchronously destroy the editor instance
|
|
235
|
+
if (!isMounted.current && !editor.isDestroyed) {
|
|
236
|
+
editor.destroy()
|
|
127
237
|
}
|
|
128
238
|
})
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
return () => {
|
|
133
|
-
isMounted = false
|
|
134
|
-
editor.destroy()
|
|
239
|
+
}
|
|
135
240
|
}
|
|
136
|
-
},
|
|
241
|
+
}, [])
|
|
242
|
+
|
|
243
|
+
// The default behavior is to re-render on each transaction
|
|
244
|
+
// This is legacy behavior that will be removed in future versions
|
|
245
|
+
useEditorState({
|
|
246
|
+
editor,
|
|
247
|
+
selector: ({ transactionNumber }) => {
|
|
248
|
+
if (options.shouldRerenderOnTransaction === false) {
|
|
249
|
+
// This will prevent the editor from re-rendering on each transaction
|
|
250
|
+
return null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// This will avoid re-rendering on the first transaction when `immediatelyRender` is set to `true`
|
|
254
|
+
if (options.immediatelyRender && transactionNumber === 0) {
|
|
255
|
+
return 0
|
|
256
|
+
}
|
|
257
|
+
return transactionNumber + 1
|
|
258
|
+
},
|
|
259
|
+
})
|
|
137
260
|
|
|
138
|
-
return
|
|
261
|
+
return editor
|
|
139
262
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useDebugValue, useEffect, useState } from 'react'
|
|
2
|
+
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
|
|
3
|
+
|
|
4
|
+
import type { Editor } from './Editor.js'
|
|
5
|
+
|
|
6
|
+
export type EditorStateSnapshot<TEditor extends Editor | null = Editor | null> = {
|
|
7
|
+
editor: TEditor;
|
|
8
|
+
transactionNumber: number;
|
|
9
|
+
};
|
|
10
|
+
export type UseEditorStateOptions<
|
|
11
|
+
TSelectorResult,
|
|
12
|
+
TEditor extends Editor | null = Editor | null,
|
|
13
|
+
> = {
|
|
14
|
+
/**
|
|
15
|
+
* The editor instance.
|
|
16
|
+
*/
|
|
17
|
+
editor: TEditor;
|
|
18
|
+
/**
|
|
19
|
+
* A selector function to determine the value to compare for re-rendering.
|
|
20
|
+
*/
|
|
21
|
+
selector: (context: EditorStateSnapshot<TEditor>) => TSelectorResult;
|
|
22
|
+
/**
|
|
23
|
+
* A custom equality function to determine if the editor should re-render.
|
|
24
|
+
* @default `(a, b) => a === b`
|
|
25
|
+
*/
|
|
26
|
+
equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* To synchronize the editor instance with the component state,
|
|
31
|
+
* we need to create a separate instance that is not affected by the component re-renders.
|
|
32
|
+
*/
|
|
33
|
+
function makeEditorStateInstance<TEditor extends Editor | null = Editor | null>(initialEditor: TEditor) {
|
|
34
|
+
let transactionNumber = 0
|
|
35
|
+
let lastTransactionNumber = 0
|
|
36
|
+
let lastSnapshot: EditorStateSnapshot<TEditor> = { editor: initialEditor, transactionNumber: 0 }
|
|
37
|
+
let editor = initialEditor
|
|
38
|
+
const subscribers = new Set<() => void>()
|
|
39
|
+
|
|
40
|
+
const editorInstance = {
|
|
41
|
+
/**
|
|
42
|
+
* Get the current editor instance.
|
|
43
|
+
*/
|
|
44
|
+
getSnapshot(): EditorStateSnapshot<TEditor> {
|
|
45
|
+
if (transactionNumber === lastTransactionNumber) {
|
|
46
|
+
return lastSnapshot
|
|
47
|
+
}
|
|
48
|
+
lastTransactionNumber = transactionNumber
|
|
49
|
+
lastSnapshot = { editor, transactionNumber }
|
|
50
|
+
return lastSnapshot
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* Always disable the editor on the server-side.
|
|
54
|
+
*/
|
|
55
|
+
getServerSnapshot(): EditorStateSnapshot<null> {
|
|
56
|
+
return { editor: null, transactionNumber: 0 }
|
|
57
|
+
},
|
|
58
|
+
/**
|
|
59
|
+
* Subscribe to the editor instance's changes.
|
|
60
|
+
*/
|
|
61
|
+
subscribe(callback: () => void) {
|
|
62
|
+
subscribers.add(callback)
|
|
63
|
+
return () => {
|
|
64
|
+
subscribers.delete(callback)
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
/**
|
|
68
|
+
* Watch the editor instance for changes.
|
|
69
|
+
*/
|
|
70
|
+
watch(nextEditor: Editor | null) {
|
|
71
|
+
editor = nextEditor as TEditor
|
|
72
|
+
|
|
73
|
+
if (editor) {
|
|
74
|
+
/**
|
|
75
|
+
* This will force a re-render when the editor state changes.
|
|
76
|
+
* This is to support things like `editor.can().toggleBold()` in components that `useEditor`.
|
|
77
|
+
* This could be more efficient, but it's a good trade-off for now.
|
|
78
|
+
*/
|
|
79
|
+
const fn = () => {
|
|
80
|
+
transactionNumber += 1
|
|
81
|
+
subscribers.forEach(callback => callback())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const currentEditor = editor
|
|
85
|
+
|
|
86
|
+
currentEditor.on('transaction', fn)
|
|
87
|
+
return () => {
|
|
88
|
+
currentEditor.off('transaction', fn)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return editorInstance
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function useEditorState<TSelectorResult>(
|
|
98
|
+
options: UseEditorStateOptions<TSelectorResult, Editor>
|
|
99
|
+
): TSelectorResult;
|
|
100
|
+
export function useEditorState<TSelectorResult>(
|
|
101
|
+
options: UseEditorStateOptions<TSelectorResult, Editor | null>
|
|
102
|
+
): TSelectorResult | null;
|
|
103
|
+
|
|
104
|
+
export function useEditorState<TSelectorResult>(
|
|
105
|
+
options: UseEditorStateOptions<TSelectorResult, Editor> | UseEditorStateOptions<TSelectorResult, Editor | null>,
|
|
106
|
+
): TSelectorResult | null {
|
|
107
|
+
const [editorInstance] = useState(() => makeEditorStateInstance(options.editor))
|
|
108
|
+
|
|
109
|
+
// Using the `useSyncExternalStore` hook to sync the editor instance with the component state
|
|
110
|
+
const selectedState = useSyncExternalStoreWithSelector(
|
|
111
|
+
editorInstance.subscribe,
|
|
112
|
+
editorInstance.getSnapshot,
|
|
113
|
+
editorInstance.getServerSnapshot,
|
|
114
|
+
options.selector as UseEditorStateOptions<TSelectorResult, Editor | null>['selector'],
|
|
115
|
+
options.equalityFn,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
return editorInstance.watch(options.editor)
|
|
120
|
+
}, [options.editor])
|
|
121
|
+
|
|
122
|
+
useDebugValue(selectedState)
|
|
123
|
+
|
|
124
|
+
return selectedState
|
|
125
|
+
}
|