@tiptap/react 3.0.0 → 3.0.1

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.
Files changed (215) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +5 -1
  3. package/dist/index.cjs +1030 -1163
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +350 -0
  6. package/dist/index.d.ts +350 -0
  7. package/dist/index.js +970 -1138
  8. package/dist/index.js.map +1 -1
  9. package/dist/menus/index.cjs +142 -0
  10. package/dist/menus/index.cjs.map +1 -0
  11. package/dist/menus/index.d.cts +19 -0
  12. package/dist/menus/index.d.ts +19 -0
  13. package/dist/menus/index.js +104 -0
  14. package/dist/menus/index.js.map +1 -0
  15. package/package.json +34 -21
  16. package/src/Context.tsx +18 -12
  17. package/src/Editor.ts +10 -11
  18. package/src/EditorContent.tsx +104 -64
  19. package/src/NodeViewContent.tsx +13 -8
  20. package/src/NodeViewWrapper.tsx +3 -2
  21. package/src/ReactMarkViewRenderer.tsx +111 -0
  22. package/src/ReactNodeViewRenderer.tsx +184 -67
  23. package/src/ReactRenderer.tsx +152 -51
  24. package/src/index.ts +2 -3
  25. package/src/menus/BubbleMenu.tsx +68 -0
  26. package/src/menus/FloatingMenu.tsx +68 -0
  27. package/src/menus/index.ts +2 -0
  28. package/src/types.ts +6 -0
  29. package/src/useEditor.ts +286 -166
  30. package/src/useEditorState.ts +133 -85
  31. package/src/useReactNodeView.ts +21 -5
  32. package/dist/index.umd.js +0 -1219
  33. package/dist/index.umd.js.map +0 -1
  34. package/dist/packages/core/src/CommandManager.d.ts +0 -20
  35. package/dist/packages/core/src/Editor.d.ts +0 -161
  36. package/dist/packages/core/src/EventEmitter.d.ts +0 -11
  37. package/dist/packages/core/src/Extension.d.ts +0 -343
  38. package/dist/packages/core/src/ExtensionManager.d.ts +0 -55
  39. package/dist/packages/core/src/InputRule.d.ts +0 -42
  40. package/dist/packages/core/src/Mark.d.ts +0 -451
  41. package/dist/packages/core/src/Node.d.ts +0 -611
  42. package/dist/packages/core/src/NodePos.d.ts +0 -44
  43. package/dist/packages/core/src/NodeView.d.ts +0 -31
  44. package/dist/packages/core/src/PasteRule.d.ts +0 -50
  45. package/dist/packages/core/src/Tracker.d.ts +0 -11
  46. package/dist/packages/core/src/commands/blur.d.ts +0 -13
  47. package/dist/packages/core/src/commands/clearContent.d.ts +0 -14
  48. package/dist/packages/core/src/commands/clearNodes.d.ts +0 -13
  49. package/dist/packages/core/src/commands/command.d.ts +0 -18
  50. package/dist/packages/core/src/commands/createParagraphNear.d.ts +0 -13
  51. package/dist/packages/core/src/commands/cut.d.ts +0 -20
  52. package/dist/packages/core/src/commands/deleteCurrentNode.d.ts +0 -13
  53. package/dist/packages/core/src/commands/deleteNode.d.ts +0 -15
  54. package/dist/packages/core/src/commands/deleteRange.d.ts +0 -14
  55. package/dist/packages/core/src/commands/deleteSelection.d.ts +0 -13
  56. package/dist/packages/core/src/commands/enter.d.ts +0 -13
  57. package/dist/packages/core/src/commands/exitCode.d.ts +0 -13
  58. package/dist/packages/core/src/commands/extendMarkRange.d.ts +0 -25
  59. package/dist/packages/core/src/commands/first.d.ts +0 -14
  60. package/dist/packages/core/src/commands/focus.d.ts +0 -27
  61. package/dist/packages/core/src/commands/forEach.d.ts +0 -14
  62. package/dist/packages/core/src/commands/index.d.ts +0 -55
  63. package/dist/packages/core/src/commands/insertContent.d.ts +0 -34
  64. package/dist/packages/core/src/commands/insertContentAt.d.ts +0 -47
  65. package/dist/packages/core/src/commands/join.d.ts +0 -41
  66. package/dist/packages/core/src/commands/joinItemBackward.d.ts +0 -13
  67. package/dist/packages/core/src/commands/joinItemForward.d.ts +0 -13
  68. package/dist/packages/core/src/commands/joinTextblockBackward.d.ts +0 -12
  69. package/dist/packages/core/src/commands/joinTextblockForward.d.ts +0 -12
  70. package/dist/packages/core/src/commands/keyboardShortcut.d.ts +0 -14
  71. package/dist/packages/core/src/commands/lift.d.ts +0 -17
  72. package/dist/packages/core/src/commands/liftEmptyBlock.d.ts +0 -13
  73. package/dist/packages/core/src/commands/liftListItem.d.ts +0 -15
  74. package/dist/packages/core/src/commands/newlineInCode.d.ts +0 -13
  75. package/dist/packages/core/src/commands/resetAttributes.d.ts +0 -16
  76. package/dist/packages/core/src/commands/scrollIntoView.d.ts +0 -13
  77. package/dist/packages/core/src/commands/selectAll.d.ts +0 -13
  78. package/dist/packages/core/src/commands/selectNodeBackward.d.ts +0 -13
  79. package/dist/packages/core/src/commands/selectNodeForward.d.ts +0 -13
  80. package/dist/packages/core/src/commands/selectParentNode.d.ts +0 -13
  81. package/dist/packages/core/src/commands/selectTextblockEnd.d.ts +0 -13
  82. package/dist/packages/core/src/commands/selectTextblockStart.d.ts +0 -13
  83. package/dist/packages/core/src/commands/setContent.d.ts +0 -40
  84. package/dist/packages/core/src/commands/setMark.d.ts +0 -15
  85. package/dist/packages/core/src/commands/setMeta.d.ts +0 -15
  86. package/dist/packages/core/src/commands/setNode.d.ts +0 -16
  87. package/dist/packages/core/src/commands/setNodeSelection.d.ts +0 -14
  88. package/dist/packages/core/src/commands/setTextSelection.d.ts +0 -14
  89. package/dist/packages/core/src/commands/sinkListItem.d.ts +0 -15
  90. package/dist/packages/core/src/commands/splitBlock.d.ts +0 -17
  91. package/dist/packages/core/src/commands/splitListItem.d.ts +0 -15
  92. package/dist/packages/core/src/commands/toggleList.d.ts +0 -18
  93. package/dist/packages/core/src/commands/toggleMark.d.ts +0 -30
  94. package/dist/packages/core/src/commands/toggleNode.d.ts +0 -17
  95. package/dist/packages/core/src/commands/toggleWrap.d.ts +0 -16
  96. package/dist/packages/core/src/commands/undoInputRule.d.ts +0 -13
  97. package/dist/packages/core/src/commands/unsetAllMarks.d.ts +0 -13
  98. package/dist/packages/core/src/commands/unsetMark.d.ts +0 -25
  99. package/dist/packages/core/src/commands/updateAttributes.d.ts +0 -24
  100. package/dist/packages/core/src/commands/wrapIn.d.ts +0 -16
  101. package/dist/packages/core/src/commands/wrapInList.d.ts +0 -16
  102. package/dist/packages/core/src/extensions/clipboardTextSerializer.d.ts +0 -5
  103. package/dist/packages/core/src/extensions/commands.d.ts +0 -3
  104. package/dist/packages/core/src/extensions/editable.d.ts +0 -2
  105. package/dist/packages/core/src/extensions/focusEvents.d.ts +0 -2
  106. package/dist/packages/core/src/extensions/index.d.ts +0 -6
  107. package/dist/packages/core/src/extensions/keymap.d.ts +0 -2
  108. package/dist/packages/core/src/extensions/tabindex.d.ts +0 -2
  109. package/dist/packages/core/src/helpers/combineTransactionSteps.d.ts +0 -10
  110. package/dist/packages/core/src/helpers/createChainableState.d.ts +0 -10
  111. package/dist/packages/core/src/helpers/createDocument.d.ts +0 -12
  112. package/dist/packages/core/src/helpers/createNodeFromContent.d.ts +0 -15
  113. package/dist/packages/core/src/helpers/defaultBlockAt.d.ts +0 -7
  114. package/dist/packages/core/src/helpers/findChildren.d.ts +0 -9
  115. package/dist/packages/core/src/helpers/findChildrenInRange.d.ts +0 -10
  116. package/dist/packages/core/src/helpers/findParentNode.d.ts +0 -16
  117. package/dist/packages/core/src/helpers/findParentNodeClosestToPos.d.ts +0 -17
  118. package/dist/packages/core/src/helpers/generateHTML.d.ts +0 -8
  119. package/dist/packages/core/src/helpers/generateJSON.d.ts +0 -8
  120. package/dist/packages/core/src/helpers/generateText.d.ts +0 -12
  121. package/dist/packages/core/src/helpers/getAttributes.d.ts +0 -9
  122. package/dist/packages/core/src/helpers/getAttributesFromExtensions.d.ts +0 -6
  123. package/dist/packages/core/src/helpers/getChangedRanges.d.ts +0 -11
  124. package/dist/packages/core/src/helpers/getDebugJSON.d.ts +0 -8
  125. package/dist/packages/core/src/helpers/getExtensionField.d.ts +0 -9
  126. package/dist/packages/core/src/helpers/getHTMLFromFragment.d.ts +0 -2
  127. package/dist/packages/core/src/helpers/getMarkAttributes.d.ts +0 -3
  128. package/dist/packages/core/src/helpers/getMarkRange.d.ts +0 -3
  129. package/dist/packages/core/src/helpers/getMarkType.d.ts +0 -2
  130. package/dist/packages/core/src/helpers/getMarksBetween.d.ts +0 -3
  131. package/dist/packages/core/src/helpers/getNodeAtPosition.d.ts +0 -11
  132. package/dist/packages/core/src/helpers/getNodeAttributes.d.ts +0 -3
  133. package/dist/packages/core/src/helpers/getNodeType.d.ts +0 -2
  134. package/dist/packages/core/src/helpers/getRenderedAttributes.d.ts +0 -3
  135. package/dist/packages/core/src/helpers/getSchema.d.ts +0 -4
  136. package/dist/packages/core/src/helpers/getSchemaByResolvedExtensions.d.ts +0 -10
  137. package/dist/packages/core/src/helpers/getSchemaTypeByName.d.ts +0 -8
  138. package/dist/packages/core/src/helpers/getSchemaTypeNameByName.d.ts +0 -8
  139. package/dist/packages/core/src/helpers/getSplittedAttributes.d.ts +0 -9
  140. package/dist/packages/core/src/helpers/getText.d.ts +0 -15
  141. package/dist/packages/core/src/helpers/getTextBetween.d.ts +0 -14
  142. package/dist/packages/core/src/helpers/getTextContentFromNodes.d.ts +0 -8
  143. package/dist/packages/core/src/helpers/getTextSerializersFromSchema.d.ts +0 -8
  144. package/dist/packages/core/src/helpers/index.d.ts +0 -50
  145. package/dist/packages/core/src/helpers/injectExtensionAttributesToParseRule.d.ts +0 -9
  146. package/dist/packages/core/src/helpers/isActive.d.ts +0 -2
  147. package/dist/packages/core/src/helpers/isAtEndOfNode.d.ts +0 -2
  148. package/dist/packages/core/src/helpers/isAtStartOfNode.d.ts +0 -2
  149. package/dist/packages/core/src/helpers/isExtensionRulesEnabled.d.ts +0 -2
  150. package/dist/packages/core/src/helpers/isList.d.ts +0 -2
  151. package/dist/packages/core/src/helpers/isMarkActive.d.ts +0 -3
  152. package/dist/packages/core/src/helpers/isNodeActive.d.ts +0 -3
  153. package/dist/packages/core/src/helpers/isNodeEmpty.d.ts +0 -2
  154. package/dist/packages/core/src/helpers/isNodeSelection.d.ts +0 -2
  155. package/dist/packages/core/src/helpers/isTextSelection.d.ts +0 -2
  156. package/dist/packages/core/src/helpers/posToDOMRect.d.ts +0 -2
  157. package/dist/packages/core/src/helpers/resolveFocusPosition.d.ts +0 -4
  158. package/dist/packages/core/src/helpers/selectionToInsertionEnd.d.ts +0 -2
  159. package/dist/packages/core/src/helpers/splitExtensions.d.ts +0 -9
  160. package/dist/packages/core/src/index.d.ts +0 -24
  161. package/dist/packages/core/src/inputRules/index.d.ts +0 -5
  162. package/dist/packages/core/src/inputRules/markInputRule.d.ts +0 -13
  163. package/dist/packages/core/src/inputRules/nodeInputRule.d.ts +0 -23
  164. package/dist/packages/core/src/inputRules/textInputRule.d.ts +0 -10
  165. package/dist/packages/core/src/inputRules/textblockTypeInputRule.d.ts +0 -15
  166. package/dist/packages/core/src/inputRules/wrappingInputRule.d.ts +0 -28
  167. package/dist/packages/core/src/pasteRules/index.d.ts +0 -3
  168. package/dist/packages/core/src/pasteRules/markPasteRule.d.ts +0 -13
  169. package/dist/packages/core/src/pasteRules/nodePasteRule.d.ts +0 -13
  170. package/dist/packages/core/src/pasteRules/textPasteRule.d.ts +0 -10
  171. package/dist/packages/core/src/style.d.ts +0 -1
  172. package/dist/packages/core/src/types.d.ts +0 -255
  173. package/dist/packages/core/src/utilities/callOrReturn.d.ts +0 -9
  174. package/dist/packages/core/src/utilities/createStyleTag.d.ts +0 -1
  175. package/dist/packages/core/src/utilities/deleteProps.d.ts +0 -6
  176. package/dist/packages/core/src/utilities/elementFromString.d.ts +0 -1
  177. package/dist/packages/core/src/utilities/escapeForRegEx.d.ts +0 -1
  178. package/dist/packages/core/src/utilities/findDuplicates.d.ts +0 -1
  179. package/dist/packages/core/src/utilities/fromString.d.ts +0 -1
  180. package/dist/packages/core/src/utilities/index.d.ts +0 -20
  181. package/dist/packages/core/src/utilities/isAndroid.d.ts +0 -1
  182. package/dist/packages/core/src/utilities/isEmptyObject.d.ts +0 -1
  183. package/dist/packages/core/src/utilities/isFunction.d.ts +0 -1
  184. package/dist/packages/core/src/utilities/isMacOS.d.ts +0 -1
  185. package/dist/packages/core/src/utilities/isNumber.d.ts +0 -1
  186. package/dist/packages/core/src/utilities/isPlainObject.d.ts +0 -1
  187. package/dist/packages/core/src/utilities/isRegExp.d.ts +0 -1
  188. package/dist/packages/core/src/utilities/isString.d.ts +0 -1
  189. package/dist/packages/core/src/utilities/isiOS.d.ts +0 -1
  190. package/dist/packages/core/src/utilities/mergeAttributes.d.ts +0 -1
  191. package/dist/packages/core/src/utilities/mergeDeep.d.ts +0 -1
  192. package/dist/packages/core/src/utilities/minMax.d.ts +0 -1
  193. package/dist/packages/core/src/utilities/objectIncludes.d.ts +0 -8
  194. package/dist/packages/core/src/utilities/removeDuplicates.d.ts +0 -8
  195. package/dist/packages/extension-bubble-menu/src/bubble-menu-plugin.d.ts +0 -76
  196. package/dist/packages/extension-bubble-menu/src/bubble-menu.d.ts +0 -15
  197. package/dist/packages/extension-bubble-menu/src/index.d.ts +0 -4
  198. package/dist/packages/extension-floating-menu/src/floating-menu-plugin.d.ts +0 -66
  199. package/dist/packages/extension-floating-menu/src/floating-menu.d.ts +0 -15
  200. package/dist/packages/extension-floating-menu/src/index.d.ts +0 -4
  201. package/dist/packages/react/src/BubbleMenu.d.ts +0 -11
  202. package/dist/packages/react/src/Context.d.ts +0 -23
  203. package/dist/packages/react/src/Editor.d.ts +0 -12
  204. package/dist/packages/react/src/EditorContent.d.ts +0 -24
  205. package/dist/packages/react/src/FloatingMenu.d.ts +0 -10
  206. package/dist/packages/react/src/NodeViewContent.d.ts +0 -6
  207. package/dist/packages/react/src/NodeViewWrapper.d.ts +0 -6
  208. package/dist/packages/react/src/ReactNodeViewRenderer.d.ts +0 -16
  209. package/dist/packages/react/src/ReactRenderer.d.ts +0 -62
  210. package/dist/packages/react/src/index.d.ts +0 -13
  211. package/dist/packages/react/src/useEditor.d.ts +0 -39
  212. package/dist/packages/react/src/useEditorState.d.ts +0 -22
  213. package/dist/packages/react/src/useReactNodeView.d.ts +0 -6
  214. package/src/BubbleMenu.tsx +0 -57
  215. package/src/FloatingMenu.tsx +0 -64
package/src/useEditor.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { EditorOptions } from '@tiptap/core'
2
- import {
3
- DependencyList, useDebugValue, useEffect, useRef, useState,
4
- } from 'react'
1
+ import { type EditorOptions, Editor } from '@tiptap/core'
2
+ import type { DependencyList, MutableRefObject } from 'react'
3
+ import { useDebugValue, useEffect, useRef, useState } from 'react'
4
+ import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'
5
5
 
6
- import { Editor } from './Editor.js'
7
6
  import { useEditorState } from './useEditorState.js'
8
7
 
8
+ // @ts-ignore
9
9
  const isDev = process.env.NODE_ENV !== 'production'
10
10
  const isSSR = typeof window === 'undefined'
11
11
  const isNext = isSSR || Boolean(typeof window !== 'undefined' && (window as any).next)
@@ -20,54 +20,87 @@ export type UseEditorOptions = Partial<EditorOptions> & {
20
20
  * If server-side rendering, set this to `false`.
21
21
  * @default true
22
22
  */
23
- immediatelyRender?: boolean;
23
+ immediatelyRender?: boolean
24
24
  /**
25
25
  * Whether to re-render the editor on each transaction.
26
26
  * This is legacy behavior that will be removed in future versions.
27
- * @default true
27
+ * @default false
28
28
  */
29
- shouldRerenderOnTransaction?: boolean;
30
- };
29
+ shouldRerenderOnTransaction?: boolean
30
+ }
31
31
 
32
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: [...] })
33
+ * This class handles the creation, destruction, and re-creation of the editor instance.
38
34
  */
39
- export function useEditor(
40
- options: UseEditorOptions & { immediatelyRender: true },
41
- deps?: DependencyList
42
- ): Editor;
35
+ class EditorInstanceManager {
36
+ /**
37
+ * The current editor instance.
38
+ */
39
+ private editor: Editor | null = null
43
40
 
44
- /**
45
- * This hook allows you to create an editor instance.
46
- * @param options The editor options
47
- * @param deps The dependencies to watch for changes
48
- * @returns The editor instance
49
- * @example const editor = useEditor({ extensions: [...] })
50
- */
51
- export function useEditor(
52
- options?: UseEditorOptions,
53
- deps?: DependencyList
54
- ): Editor | null;
41
+ /**
42
+ * The most recent options to apply to the editor.
43
+ */
44
+ private options: MutableRefObject<UseEditorOptions>
55
45
 
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) {
46
+ /**
47
+ * The subscriptions to notify when the editor instance
48
+ * has been created or destroyed.
49
+ */
50
+ private subscriptions = new Set<() => void>()
51
+
52
+ /**
53
+ * A timeout to destroy the editor if it was not mounted within a time frame.
54
+ */
55
+ private scheduledDestructionTimeout: ReturnType<typeof setTimeout> | undefined
56
+
57
+ /**
58
+ * Whether the editor has been mounted.
59
+ */
60
+ private isComponentMounted = false
61
+
62
+ /**
63
+ * The most recent dependencies array.
64
+ */
65
+ private previousDeps: DependencyList | null = null
66
+
67
+ /**
68
+ * The unique instance ID. This is used to identify the editor instance. And will be re-generated for each new instance.
69
+ */
70
+ public instanceId = ''
71
+
72
+ constructor(options: MutableRefObject<UseEditorOptions>) {
73
+ this.options = options
74
+ this.subscriptions = new Set<() => void>()
75
+ this.setEditor(this.getInitialEditor())
76
+ this.scheduleDestroy()
77
+
78
+ this.getEditor = this.getEditor.bind(this)
79
+ this.getServerSnapshot = this.getServerSnapshot.bind(this)
80
+ this.subscribe = this.subscribe.bind(this)
81
+ this.refreshEditorInstance = this.refreshEditorInstance.bind(this)
82
+ this.scheduleDestroy = this.scheduleDestroy.bind(this)
83
+ this.onRender = this.onRender.bind(this)
84
+ this.createEditor = this.createEditor.bind(this)
85
+ }
86
+
87
+ private setEditor(editor: Editor | null) {
88
+ this.editor = editor
89
+ this.instanceId = Math.random().toString(36).slice(2, 9)
90
+
91
+ // Notify all subscribers that the editor instance has been created
92
+ this.subscriptions.forEach(cb => cb())
93
+ }
94
+
95
+ private getInitialEditor() {
96
+ if (this.options.current.immediatelyRender === undefined) {
63
97
  if (isSSR || isNext) {
64
- // TODO in the next major release, we should throw an error here
65
98
  if (isDev) {
66
99
  /**
67
100
  * Throw an error in development, to make sure the developer is aware that tiptap cannot be SSR'd
68
101
  * and that they need to set `immediatelyRender` to `false` to avoid hydration mismatches.
69
102
  */
70
- console.warn(
103
+ throw new Error(
71
104
  'Tiptap Error: SSR has been detected, please set `immediatelyRender` explicitly to `false` to avoid hydration mismatches.',
72
105
  )
73
106
  }
@@ -77,175 +110,262 @@ export function useEditor(
77
110
  }
78
111
 
79
112
  // Default to immediately rendering when client-side rendering
80
- return new Editor(options)
113
+ return this.createEditor()
81
114
  }
82
115
 
83
- if (options.immediatelyRender && isSSR && isDev) {
116
+ if (this.options.current.immediatelyRender && isSSR && isDev) {
84
117
  // 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
118
  throw new Error(
86
119
  '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
120
  )
88
121
  }
89
122
 
90
- if (options.immediatelyRender) {
91
- return new Editor(options)
123
+ if (this.options.current.immediatelyRender) {
124
+ return this.createEditor()
92
125
  }
93
126
 
94
127
  return null
95
- })
96
-
97
- useDebugValue(editor)
128
+ }
98
129
 
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)
113
-
114
- const {
115
- onBeforeCreate,
116
- onBlur,
117
- onCreate,
118
- onDestroy,
119
- onFocus,
120
- onSelectionUpdate,
121
- onTransaction,
122
- onUpdate,
123
- onContentError,
124
- } = options
125
-
126
- const onBeforeCreateRef = useRef(onBeforeCreate)
127
- const onBlurRef = useRef(onBlur)
128
- const onCreateRef = useRef(onCreate)
129
- const onDestroyRef = useRef(onDestroy)
130
- const onFocusRef = useRef(onFocus)
131
- const onSelectionUpdateRef = useRef(onSelectionUpdate)
132
- const onTransactionRef = useRef(onTransaction)
133
- const onUpdateRef = useRef(onUpdate)
134
- const onContentErrorRef = useRef(onContentError)
135
-
136
- // This effect will handle updating the editor instance
137
- // when the event handlers change.
138
- useEffect(() => {
139
- if (!editor) {
140
- return
130
+ /**
131
+ * Create a new editor instance. And attach event listeners.
132
+ */
133
+ private createEditor(): Editor {
134
+ const optionsToApply: Partial<EditorOptions> = {
135
+ ...this.options.current,
136
+ // Always call the most recent version of the callback function by default
137
+ onBeforeCreate: (...args) => this.options.current.onBeforeCreate?.(...args),
138
+ onBlur: (...args) => this.options.current.onBlur?.(...args),
139
+ onCreate: (...args) => this.options.current.onCreate?.(...args),
140
+ onDestroy: (...args) => this.options.current.onDestroy?.(...args),
141
+ onFocus: (...args) => this.options.current.onFocus?.(...args),
142
+ onSelectionUpdate: (...args) => this.options.current.onSelectionUpdate?.(...args),
143
+ onTransaction: (...args) => this.options.current.onTransaction?.(...args),
144
+ onUpdate: (...args) => this.options.current.onUpdate?.(...args),
145
+ onContentError: (...args) => this.options.current.onContentError?.(...args),
146
+ onDrop: (...args) => this.options.current.onDrop?.(...args),
147
+ onPaste: (...args) => this.options.current.onPaste?.(...args),
148
+ onDelete: (...args) => this.options.current.onDelete?.(...args),
141
149
  }
150
+ const editor = new Editor(optionsToApply)
142
151
 
143
- if (onBeforeCreate) {
144
- editor.off('beforeCreate', onBeforeCreateRef.current)
145
- editor.on('beforeCreate', onBeforeCreate)
152
+ // no need to keep track of the event listeners, they will be removed when the editor is destroyed
146
153
 
147
- onBeforeCreateRef.current = onBeforeCreate
148
- }
154
+ return editor
155
+ }
149
156
 
150
- if (onBlur) {
151
- editor.off('blur', onBlurRef.current)
152
- editor.on('blur', onBlur)
157
+ /**
158
+ * Get the current editor instance.
159
+ */
160
+ getEditor(): Editor | null {
161
+ return this.editor
162
+ }
153
163
 
154
- onBlurRef.current = onBlur
155
- }
164
+ /**
165
+ * Always disable the editor on the server-side.
166
+ */
167
+ getServerSnapshot(): null {
168
+ return null
169
+ }
156
170
 
157
- if (onCreate) {
158
- editor.off('create', onCreateRef.current)
159
- editor.on('create', onCreate)
171
+ /**
172
+ * Subscribe to the editor instance's changes.
173
+ */
174
+ subscribe(onStoreChange: () => void) {
175
+ this.subscriptions.add(onStoreChange)
160
176
 
161
- onCreateRef.current = onCreate
177
+ return () => {
178
+ this.subscriptions.delete(onStoreChange)
162
179
  }
180
+ }
181
+
182
+ static compareOptions(a: UseEditorOptions, b: UseEditorOptions) {
183
+ return (Object.keys(a) as (keyof UseEditorOptions)[]).every(key => {
184
+ if (
185
+ [
186
+ 'onCreate',
187
+ 'onBeforeCreate',
188
+ 'onDestroy',
189
+ 'onUpdate',
190
+ 'onTransaction',
191
+ 'onFocus',
192
+ 'onBlur',
193
+ 'onSelectionUpdate',
194
+ 'onContentError',
195
+ 'onDrop',
196
+ 'onPaste',
197
+ ].includes(key)
198
+ ) {
199
+ // we don't want to compare callbacks, they are always different and only registered once
200
+ return true
201
+ }
202
+
203
+ // We often encourage putting extensions inlined in the options object, so we will do a slightly deeper comparison here
204
+ if (key === 'extensions' && a.extensions && b.extensions) {
205
+ if (a.extensions.length !== b.extensions.length) {
206
+ return false
207
+ }
208
+ return a.extensions.every((extension, index) => {
209
+ if (extension !== b.extensions?.[index]) {
210
+ return false
211
+ }
212
+ return true
213
+ })
214
+ }
215
+ if (a[key] !== b[key]) {
216
+ // if any of the options have changed, we should update the editor options
217
+ return false
218
+ }
219
+ return true
220
+ })
221
+ }
163
222
 
164
- if (onDestroy) {
165
- editor.off('destroy', onDestroyRef.current)
166
- editor.on('destroy', onDestroy)
223
+ /**
224
+ * On each render, we will create, update, or destroy the editor instance.
225
+ * @param deps The dependencies to watch for changes
226
+ * @returns A cleanup function
227
+ */
228
+ onRender(deps: DependencyList) {
229
+ // The returned callback will run on each render
230
+ return () => {
231
+ this.isComponentMounted = true
232
+ // Cleanup any scheduled destructions, since we are currently rendering
233
+ clearTimeout(this.scheduledDestructionTimeout)
234
+
235
+ if (this.editor && !this.editor.isDestroyed && deps.length === 0) {
236
+ // if the editor does exist & deps are empty, we don't need to re-initialize the editor generally
237
+ if (!EditorInstanceManager.compareOptions(this.options.current, this.editor.options)) {
238
+ // But, the options are different, so we need to update the editor options
239
+ // Still, this is faster than re-creating the editor
240
+ this.editor.setOptions({
241
+ ...this.options.current,
242
+ editable: this.editor.isEditable,
243
+ })
244
+ }
245
+ } else {
246
+ // When the editor:
247
+ // - does not yet exist
248
+ // - is destroyed
249
+ // - the deps array changes
250
+ // We need to destroy the editor instance and re-initialize it
251
+ this.refreshEditorInstance(deps)
252
+ }
167
253
 
168
- onDestroyRef.current = onDestroy
254
+ return () => {
255
+ this.isComponentMounted = false
256
+ this.scheduleDestroy()
257
+ }
169
258
  }
259
+ }
170
260
 
171
- if (onFocus) {
172
- editor.off('focus', onFocusRef.current)
173
- editor.on('focus', onFocus)
261
+ /**
262
+ * Recreate the editor instance if the dependencies have changed.
263
+ */
264
+ private refreshEditorInstance(deps: DependencyList) {
265
+ if (this.editor && !this.editor.isDestroyed) {
266
+ // Editor instance already exists
267
+ if (this.previousDeps === null) {
268
+ // If lastDeps has not yet been initialized, reuse the current editor instance
269
+ this.previousDeps = deps
270
+ return
271
+ }
272
+ const depsAreEqual =
273
+ this.previousDeps.length === deps.length && this.previousDeps.every((dep, index) => dep === deps[index])
174
274
 
175
- onFocusRef.current = onFocus
275
+ if (depsAreEqual) {
276
+ // deps exist and are equal, no need to recreate
277
+ return
278
+ }
176
279
  }
177
280
 
178
- if (onSelectionUpdate) {
179
- editor.off('selectionUpdate', onSelectionUpdateRef.current)
180
- editor.on('selectionUpdate', onSelectionUpdate)
181
-
182
- onSelectionUpdateRef.current = onSelectionUpdate
281
+ if (this.editor && !this.editor.isDestroyed) {
282
+ // Destroy the editor instance if it exists
283
+ this.editor.destroy()
183
284
  }
184
285
 
185
- if (onTransaction) {
186
- editor.off('transaction', onTransactionRef.current)
187
- editor.on('transaction', onTransaction)
286
+ this.setEditor(this.createEditor())
188
287
 
189
- onTransactionRef.current = onTransaction
190
- }
288
+ // Update the lastDeps to the current deps
289
+ this.previousDeps = deps
290
+ }
191
291
 
192
- if (onUpdate) {
193
- editor.off('update', onUpdateRef.current)
194
- editor.on('update', onUpdate)
292
+ /**
293
+ * Schedule the destruction of the editor instance.
294
+ * This will only destroy the editor if it was not mounted on the next tick.
295
+ * This is to avoid destroying the editor instance when it's actually still mounted.
296
+ */
297
+ private scheduleDestroy() {
298
+ const currentInstanceId = this.instanceId
299
+ const currentEditor = this.editor
300
+
301
+ // Wait two ticks to see if the component is still mounted
302
+ this.scheduledDestructionTimeout = setTimeout(() => {
303
+ if (this.isComponentMounted && this.instanceId === currentInstanceId) {
304
+ // If still mounted on the following tick, with the same instanceId, do not destroy the editor
305
+ if (currentEditor) {
306
+ // just re-apply options as they might have changed
307
+ currentEditor.setOptions(this.options.current)
308
+ }
309
+ return
310
+ }
311
+ if (currentEditor && !currentEditor.isDestroyed) {
312
+ currentEditor.destroy()
313
+ if (this.instanceId === currentInstanceId) {
314
+ this.setEditor(null)
315
+ }
316
+ }
317
+ // This allows the effect to run again between ticks
318
+ // which may save us from having to re-create the editor
319
+ }, 1)
320
+ }
321
+ }
195
322
 
196
- onUpdateRef.current = onUpdate
197
- }
323
+ /**
324
+ * This hook allows you to create an editor instance.
325
+ * @param options The editor options
326
+ * @param deps The dependencies to watch for changes
327
+ * @returns The editor instance
328
+ * @example const editor = useEditor({ extensions: [...] })
329
+ */
330
+ export function useEditor(
331
+ options: UseEditorOptions & { immediatelyRender: false },
332
+ deps?: DependencyList,
333
+ ): Editor | null
198
334
 
199
- if (onContentError) {
200
- editor.off('contentError', onContentErrorRef.current)
201
- editor.on('contentError', onContentError)
335
+ /**
336
+ * This hook allows you to create an editor instance.
337
+ * @param options The editor options
338
+ * @param deps The dependencies to watch for changes
339
+ * @returns The editor instance
340
+ * @example const editor = useEditor({ extensions: [...] })
341
+ */
342
+ export function useEditor(options: UseEditorOptions, deps?: DependencyList): Editor
202
343
 
203
- onContentErrorRef.current = onContentError
204
- }
205
- }, [
206
- onBeforeCreate,
207
- onBlur,
208
- onCreate,
209
- onDestroy,
210
- onFocus,
211
- onSelectionUpdate,
212
- onTransaction,
213
- onUpdate,
214
- onContentError,
215
- editor,
216
- ])
344
+ export function useEditor(options: UseEditorOptions = {}, deps: DependencyList = []): Editor | null {
345
+ const mostRecentOptions = useRef(options)
217
346
 
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
- * */
223
- useEffect(() => {
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.
230
-
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()
237
- }
238
- })
239
- }
240
- }
241
- }, [])
347
+ mostRecentOptions.current = options
348
+
349
+ const [instanceManager] = useState(() => new EditorInstanceManager(mostRecentOptions))
350
+
351
+ const editor = useSyncExternalStore(
352
+ instanceManager.subscribe,
353
+ instanceManager.getEditor,
354
+ instanceManager.getServerSnapshot,
355
+ )
356
+
357
+ useDebugValue(editor)
358
+
359
+ // This effect will handle creating/updating the editor instance
360
+ // eslint-disable-next-line react-hooks/exhaustive-deps
361
+ useEffect(instanceManager.onRender(deps))
242
362
 
243
363
  // The default behavior is to re-render on each transaction
244
364
  // This is legacy behavior that will be removed in future versions
245
365
  useEditorState({
246
366
  editor,
247
367
  selector: ({ transactionNumber }) => {
248
- if (options.shouldRerenderOnTransaction === false) {
368
+ if (options.shouldRerenderOnTransaction === false || options.shouldRerenderOnTransaction === undefined) {
249
369
  // This will prevent the editor from re-rendering on each transaction
250
370
  return null
251
371
  }