@tiptap/react 2.5.0-pre.13 → 2.5.0-pre.14
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 +676 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +677 -49
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +676 -47
- 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 +162 -49
- 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,67 @@ 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 [editor, setEditor] = useState(() => {
|
|
61
|
+
if (options.immediatelyRender === undefined) {
|
|
62
|
+
if (isSSR || isNext) {
|
|
63
|
+
// TODO in the next major release, we should throw an error here
|
|
64
|
+
if (isDev) {
|
|
65
|
+
/**
|
|
66
|
+
* Throw an error in development, to make sure the developer is aware that tiptap cannot be SSR'd
|
|
67
|
+
* and that they need to set `immediatelyRender` to `false` to avoid hydration mismatches.
|
|
68
|
+
*/
|
|
69
|
+
console.warn(
|
|
70
|
+
'Tiptap Error: SSR has been detected, please set `immediatelyRender` explicitly to `false` to avoid hydration mismatches.',
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Best faith effort in production, run the code in the legacy mode to avoid hydration mismatches and errors in production
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Default to immediately rendering when client-side rendering
|
|
79
|
+
return new Editor(options)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (options.immediatelyRender && isSSR && isDev) {
|
|
83
|
+
// Warn in development, to make sure the developer is aware that tiptap cannot be SSR'd, set `immediatelyRender` to `false` to avoid hydration mismatches.
|
|
84
|
+
throw new Error(
|
|
85
|
+
'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.',
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (options.immediatelyRender) {
|
|
90
|
+
return new Editor(options)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
useDebugValue(editor)
|
|
97
|
+
|
|
98
|
+
// This effect will handle creating/updating the editor instance
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
let editorInstance: Editor | null = editor
|
|
101
|
+
|
|
102
|
+
if (!editorInstance) {
|
|
103
|
+
editorInstance = new Editor(options)
|
|
104
|
+
// instantiate the editor if it doesn't exist
|
|
105
|
+
// for ssr, this is the first time the editor is created
|
|
106
|
+
setEditor(editorInstance)
|
|
107
|
+
} else {
|
|
108
|
+
// if the editor does exist, update the editor options accordingly
|
|
109
|
+
editorInstance.setOptions(options)
|
|
110
|
+
}
|
|
111
|
+
}, deps)
|
|
21
112
|
|
|
22
113
|
const {
|
|
23
114
|
onBeforeCreate,
|
|
@@ -44,96 +135,118 @@ export const useEditor = (options: Partial<EditorOptions> = {}, deps: Dependency
|
|
|
44
135
|
// This effect will handle updating the editor instance
|
|
45
136
|
// when the event handlers change.
|
|
46
137
|
useEffect(() => {
|
|
47
|
-
if (!
|
|
138
|
+
if (!editor) {
|
|
48
139
|
return
|
|
49
140
|
}
|
|
50
141
|
|
|
51
142
|
if (onBeforeCreate) {
|
|
52
|
-
|
|
53
|
-
|
|
143
|
+
editor.off('beforeCreate', onBeforeCreateRef.current)
|
|
144
|
+
editor.on('beforeCreate', onBeforeCreate)
|
|
54
145
|
|
|
55
146
|
onBeforeCreateRef.current = onBeforeCreate
|
|
56
147
|
}
|
|
57
148
|
|
|
58
149
|
if (onBlur) {
|
|
59
|
-
|
|
60
|
-
|
|
150
|
+
editor.off('blur', onBlurRef.current)
|
|
151
|
+
editor.on('blur', onBlur)
|
|
61
152
|
|
|
62
153
|
onBlurRef.current = onBlur
|
|
63
154
|
}
|
|
64
155
|
|
|
65
156
|
if (onCreate) {
|
|
66
|
-
|
|
67
|
-
|
|
157
|
+
editor.off('create', onCreateRef.current)
|
|
158
|
+
editor.on('create', onCreate)
|
|
68
159
|
|
|
69
160
|
onCreateRef.current = onCreate
|
|
70
161
|
}
|
|
71
162
|
|
|
72
163
|
if (onDestroy) {
|
|
73
|
-
|
|
74
|
-
|
|
164
|
+
editor.off('destroy', onDestroyRef.current)
|
|
165
|
+
editor.on('destroy', onDestroy)
|
|
75
166
|
|
|
76
167
|
onDestroyRef.current = onDestroy
|
|
77
168
|
}
|
|
78
169
|
|
|
79
170
|
if (onFocus) {
|
|
80
|
-
|
|
81
|
-
|
|
171
|
+
editor.off('focus', onFocusRef.current)
|
|
172
|
+
editor.on('focus', onFocus)
|
|
82
173
|
|
|
83
174
|
onFocusRef.current = onFocus
|
|
84
175
|
}
|
|
85
176
|
|
|
86
177
|
if (onSelectionUpdate) {
|
|
87
|
-
|
|
88
|
-
|
|
178
|
+
editor.off('selectionUpdate', onSelectionUpdateRef.current)
|
|
179
|
+
editor.on('selectionUpdate', onSelectionUpdate)
|
|
89
180
|
|
|
90
181
|
onSelectionUpdateRef.current = onSelectionUpdate
|
|
91
182
|
}
|
|
92
183
|
|
|
93
184
|
if (onTransaction) {
|
|
94
|
-
|
|
95
|
-
|
|
185
|
+
editor.off('transaction', onTransactionRef.current)
|
|
186
|
+
editor.on('transaction', onTransaction)
|
|
96
187
|
|
|
97
188
|
onTransactionRef.current = onTransaction
|
|
98
189
|
}
|
|
99
190
|
|
|
100
191
|
if (onUpdate) {
|
|
101
|
-
|
|
102
|
-
|
|
192
|
+
editor.off('update', onUpdateRef.current)
|
|
193
|
+
editor.on('update', onUpdate)
|
|
103
194
|
|
|
104
195
|
onUpdateRef.current = onUpdate
|
|
105
196
|
}
|
|
106
197
|
|
|
107
198
|
if (onContentError) {
|
|
108
|
-
|
|
109
|
-
|
|
199
|
+
editor.off('contentError', onContentErrorRef.current)
|
|
200
|
+
editor.on('contentError', onContentError)
|
|
110
201
|
|
|
111
202
|
onContentErrorRef.current = onContentError
|
|
112
203
|
}
|
|
113
|
-
}, [
|
|
114
|
-
|
|
204
|
+
}, [
|
|
205
|
+
onBeforeCreate,
|
|
206
|
+
onBlur,
|
|
207
|
+
onCreate,
|
|
208
|
+
onDestroy,
|
|
209
|
+
onFocus,
|
|
210
|
+
onSelectionUpdate,
|
|
211
|
+
onTransaction,
|
|
212
|
+
onUpdate,
|
|
213
|
+
onContentError,
|
|
214
|
+
editor,
|
|
215
|
+
])
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Destroy the editor instance when the component completely unmounts
|
|
219
|
+
* As opposed to the cleanup function in the effect above, this will
|
|
220
|
+
* only be called when the component is removed from the DOM, since it has no deps.
|
|
221
|
+
* */
|
|
115
222
|
useEffect(() => {
|
|
116
|
-
let isMounted = true
|
|
117
|
-
|
|
118
|
-
const editor = new Editor(options)
|
|
119
|
-
|
|
120
|
-
editorRef.current = editor
|
|
121
|
-
|
|
122
|
-
editorRef.current.on('transaction', () => {
|
|
123
|
-
requestAnimationFrame(() => {
|
|
124
|
-
requestAnimationFrame(() => {
|
|
125
|
-
if (isMounted) {
|
|
126
|
-
forceUpdate({})
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
})
|
|
131
|
-
|
|
132
223
|
return () => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}, deps)
|
|
224
|
+
if (editor) {
|
|
225
|
+
// We need to destroy the editor asynchronously to avoid memory leaks
|
|
226
|
+
// because the editor instance is still being used in the component.
|
|
137
227
|
|
|
138
|
-
|
|
228
|
+
setTimeout(() => (editor.isDestroyed ? null : editor.destroy()))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}, [])
|
|
232
|
+
|
|
233
|
+
// The default behavior is to re-render on each transaction
|
|
234
|
+
// This is legacy behavior that will be removed in future versions
|
|
235
|
+
useEditorState({
|
|
236
|
+
editor,
|
|
237
|
+
selector: ({ transactionNumber }) => {
|
|
238
|
+
if (options.shouldRerenderOnTransaction === false) {
|
|
239
|
+
// This will prevent the editor from re-rendering on each transaction
|
|
240
|
+
return null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// This will avoid re-rendering on the first transaction when `immediatelyRender` is set to `true`
|
|
244
|
+
if (options.immediatelyRender && transactionNumber === 0) {
|
|
245
|
+
return 0
|
|
246
|
+
}
|
|
247
|
+
return transactionNumber + 1
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
return editor
|
|
139
252
|
}
|
|
@@ -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 | null>
|
|
99
|
+
): TSelectorResult | null;
|
|
100
|
+
export function useEditorState<TSelectorResult>(
|
|
101
|
+
options: UseEditorStateOptions<TSelectorResult, Editor>
|
|
102
|
+
): TSelectorResult;
|
|
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
|
+
}
|