@tiptap/react 3.22.0 → 3.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiptap/react",
3
3
  "description": "React components for tiptap",
4
- "version": "3.22.0",
4
+ "version": "3.22.2",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -48,20 +48,20 @@
48
48
  "@types/react-dom": "^19.0.0",
49
49
  "react": "^19.0.0",
50
50
  "react-dom": "^19.0.0",
51
- "@tiptap/core": "^3.22.0",
52
- "@tiptap/pm": "^3.22.0"
51
+ "@tiptap/core": "^3.22.2",
52
+ "@tiptap/pm": "^3.22.2"
53
53
  },
54
54
  "optionalDependencies": {
55
- "@tiptap/extension-bubble-menu": "^3.22.0",
56
- "@tiptap/extension-floating-menu": "^3.22.0"
55
+ "@tiptap/extension-floating-menu": "^3.22.2",
56
+ "@tiptap/extension-bubble-menu": "^3.22.2"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
60
60
  "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
61
61
  "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
62
62
  "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
63
- "@tiptap/core": "^3.22.0",
64
- "@tiptap/pm": "^3.22.0"
63
+ "@tiptap/core": "^3.22.2",
64
+ "@tiptap/pm": "^3.22.2"
65
65
  },
66
66
  "repository": {
67
67
  "type": "git",
package/src/Editor.ts CHANGED
@@ -3,7 +3,10 @@ import type { ReactPortal } from 'react'
3
3
 
4
4
  import type { ReactRenderer } from './ReactRenderer.js'
5
5
 
6
- export type EditorWithContentComponent = Editor & { contentComponent?: ContentComponent | null }
6
+ export type EditorWithContentComponent = Editor & {
7
+ contentComponent?: ContentComponent | null
8
+ isEditorContentInitialized?: boolean
9
+ }
7
10
  export type ContentComponent = {
8
11
  setRenderer(id: string, renderer: ReactRenderer): void
9
12
  removeRenderer(id: string): void
@@ -89,18 +89,9 @@ export class PureEditorContent extends React.Component<
89
89
  > {
90
90
  editorContentRef: React.RefObject<any>
91
91
 
92
- initialized: boolean
93
-
94
- unsubscribeToContentComponent?: () => void
95
-
96
92
  constructor(props: EditorContentProps) {
97
93
  super(props)
98
94
  this.editorContentRef = React.createRef()
99
- this.initialized = false
100
-
101
- this.state = {
102
- hasContentComponentInitialized: Boolean((props.editor as EditorWithContentComponent | null)?.contentComponent),
103
- }
104
95
  }
105
96
 
106
97
  componentDidMount() {
@@ -129,29 +120,11 @@ export class PureEditorContent extends React.Component<
129
120
 
130
121
  editor.contentComponent = getInstance()
131
122
 
132
- // Has the content component been initialized?
133
- if (!this.state.hasContentComponentInitialized) {
134
- // Subscribe to the content component
135
- this.unsubscribeToContentComponent = editor.contentComponent.subscribe(() => {
136
- this.setState(prevState => {
137
- if (!prevState.hasContentComponentInitialized) {
138
- return {
139
- hasContentComponentInitialized: true,
140
- }
141
- }
142
- return prevState
143
- })
144
-
145
- // Unsubscribe to previous content component
146
- if (this.unsubscribeToContentComponent) {
147
- this.unsubscribeToContentComponent()
148
- }
149
- })
150
- }
151
-
152
123
  editor.createNodeViews()
153
124
 
154
- this.initialized = true
125
+ editor.isEditorContentInitialized = true
126
+
127
+ this.forceUpdate()
155
128
  }
156
129
  }
157
130
 
@@ -162,7 +135,7 @@ export class PureEditorContent extends React.Component<
162
135
  return
163
136
  }
164
137
 
165
- this.initialized = false
138
+ editor.isEditorContentInitialized = false
166
139
 
167
140
  if (!editor.isDestroyed) {
168
141
  editor.view.setProps({
@@ -170,10 +143,6 @@ export class PureEditorContent extends React.Component<
170
143
  })
171
144
  }
172
145
 
173
- if (this.unsubscribeToContentComponent) {
174
- this.unsubscribeToContentComponent()
175
- }
176
-
177
146
  editor.contentComponent = null
178
147
 
179
148
  // try to reset the editor element
@@ -5,7 +5,7 @@ import type {
5
5
  NodeViewRendererOptions,
6
6
  NodeViewRendererProps,
7
7
  } from '@tiptap/core'
8
- import { getRenderedAttributes, NodeView } from '@tiptap/core'
8
+ import { cancelPositionCheck, getRenderedAttributes, NodeView, schedulePositionCheck } from '@tiptap/core'
9
9
  import type { Node, Node as ProseMirrorNode } from '@tiptap/pm/model'
10
10
  import type { Decoration, DecorationSource, NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
11
11
  import type { ComponentType, NamedExoticComponent } from 'react'
@@ -72,6 +72,18 @@ export class ReactNodeView<
72
72
  */
73
73
  selectionRafId: number | null = null
74
74
 
75
+ /**
76
+ * The last known position of this node view, used to detect position-only
77
+ * changes that don't produce a new node object reference.
78
+ */
79
+ private currentPos: number | undefined
80
+
81
+ /**
82
+ * Callback registered with the per-editor position-update registry.
83
+ * Stored so it can be unregistered in destroy().
84
+ */
85
+ private positionCheckCallback: (() => void) | null = null
86
+
75
87
  constructor(component: Component, props: NodeViewRendererProps, options?: Partial<Options>) {
76
88
  super(component, props, options)
77
89
 
@@ -196,6 +208,26 @@ export class ReactNodeView<
196
208
 
197
209
  this.editor.on('selectionUpdate', this.handleSelectionUpdate)
198
210
  this.updateElementAttributes()
211
+ this.currentPos = this.getPos()
212
+
213
+ this.positionCheckCallback = () => {
214
+ const newPos = this.getPos()
215
+
216
+ if (typeof newPos !== 'number' || newPos === this.currentPos) {
217
+ return
218
+ }
219
+
220
+ this.currentPos = newPos
221
+
222
+ // Pass a fresh getPos reference so React's memo detects a prop change.
223
+ this.renderer.updateProps({ getPos: () => this.getPos() })
224
+
225
+ if (typeof this.options.attrs === 'function') {
226
+ this.updateElementAttributes()
227
+ }
228
+ }
229
+
230
+ schedulePositionCheck(this.editor, this.positionCheckCallback)
199
231
  }
200
232
 
201
233
  /**
@@ -238,7 +270,8 @@ export class ReactNodeView<
238
270
  this.selectionRafId = requestAnimationFrame(() => {
239
271
  this.selectionRafId = null
240
272
  const { from, to } = this.editor.state.selection
241
- const pos = this.getPos()
273
+ // Avoid resolving getPos() after ProseMirror has detached this node view.
274
+ const pos = this.currentPos
242
275
  if (typeof pos !== 'number') {
243
276
  return
244
277
  }
@@ -283,6 +316,7 @@ export class ReactNodeView<
283
316
  this.node = node
284
317
  this.decorations = decorations
285
318
  this.innerDecorations = innerDecorations
319
+ this.currentPos = this.getPos()
286
320
 
287
321
  return this.options.update({
288
322
  oldNode,
@@ -296,13 +330,31 @@ export class ReactNodeView<
296
330
  })
297
331
  }
298
332
 
333
+ const newPos = this.getPos()
334
+
299
335
  if (node === this.node && this.decorations === decorations && this.innerDecorations === innerDecorations) {
336
+ if (newPos === this.currentPos) {
337
+ return true
338
+ }
339
+
340
+ // Position changed without a content/decoration change — trigger re-render
341
+ // so the component receives an up-to-date value from getPos().
342
+ // Pass a fresh getPos reference so React's memo detects a prop change.
343
+ this.currentPos = newPos
344
+ rerenderComponent({
345
+ node,
346
+ decorations,
347
+ innerDecorations,
348
+ extension: this.extensionWithSyncedStorage,
349
+ getPos: () => this.getPos(),
350
+ })
300
351
  return true
301
352
  }
302
353
 
303
354
  this.node = node
304
355
  this.decorations = decorations
305
356
  this.innerDecorations = innerDecorations
357
+ this.currentPos = newPos
306
358
 
307
359
  rerenderComponent({ node, decorations, innerDecorations, extension: this.extensionWithSyncedStorage })
308
360
 
@@ -337,6 +389,12 @@ export class ReactNodeView<
337
389
  destroy() {
338
390
  this.renderer.destroy()
339
391
  this.editor.off('selectionUpdate', this.handleSelectionUpdate)
392
+
393
+ if (this.positionCheckCallback) {
394
+ cancelPositionCheck(this.editor, this.positionCheckCallback)
395
+ this.positionCheckCallback = null
396
+ }
397
+
340
398
  this.contentDOMElement = null
341
399
 
342
400
  if (this.selectionRafId) {
@@ -147,7 +147,7 @@ type ComponentType<R, P> =
147
147
  export class ReactRenderer<R = unknown, P extends Record<string, any> = object> {
148
148
  id: string
149
149
 
150
- editor: Editor
150
+ editor: EditorWithContentComponent
151
151
 
152
152
  component: any
153
153
 
@@ -173,7 +173,7 @@ export class ReactRenderer<R = unknown, P extends Record<string, any> = object>
173
173
  ) {
174
174
  this.id = Math.floor(Math.random() * 0xffffffff).toString()
175
175
  this.component = component
176
- this.editor = editor as EditorWithContentComponent
176
+ this.editor = editor
177
177
  this.props = props as P
178
178
  this.element = document.createElement(as)
179
179
  this.element.classList.add('react-renderer')
@@ -182,10 +182,9 @@ export class ReactRenderer<R = unknown, P extends Record<string, any> = object>
182
182
  this.element.classList.add(...className.split(' '))
183
183
  }
184
184
 
185
- // If the editor is already initialized, we will need to
186
- // synchronously render the component to ensure it renders
187
- // together with Prosemirror's rendering.
188
- if (this.editor.isInitialized) {
185
+ if (this.editor.isEditorContentInitialized) {
186
+ // If EditorContent is mounted, flush synchronously to maintain cursor positioning consistency.
187
+ // Subsequent renders can be async without affecting cursor behavior.
189
188
  flushSync(() => {
190
189
  this.render()
191
190
  })