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