@tiptap/core 3.0.0-next.1 → 3.0.0-next.3

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 (42) hide show
  1. package/dist/index.cjs +403 -137
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +177 -53
  4. package/dist/index.d.ts +177 -53
  5. package/dist/index.js +375 -108
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/Editor.ts +60 -10
  9. package/src/EventEmitter.ts +9 -0
  10. package/src/ExtensionManager.ts +16 -11
  11. package/src/InputRule.ts +45 -30
  12. package/src/Node.ts +19 -0
  13. package/src/NodePos.ts +9 -4
  14. package/src/NodeView.ts +43 -12
  15. package/src/PasteRule.ts +96 -42
  16. package/src/commands/focus.ts +1 -6
  17. package/src/commands/insertContent.ts +9 -9
  18. package/src/commands/insertContentAt.ts +23 -3
  19. package/src/commands/selectAll.ts +10 -5
  20. package/src/commands/setContent.ts +10 -14
  21. package/src/commands/setNode.ts +9 -2
  22. package/src/commands/toggleNode.ts +11 -2
  23. package/src/commands/updateAttributes.ts +72 -12
  24. package/src/extensions/drop.ts +26 -0
  25. package/src/extensions/index.ts +2 -0
  26. package/src/extensions/keymap.ts +5 -2
  27. package/src/extensions/paste.ts +26 -0
  28. package/src/helpers/createDocument.ts +4 -2
  29. package/src/helpers/createNodeFromContent.ts +11 -2
  30. package/src/helpers/getMarkRange.ts +35 -8
  31. package/src/helpers/getRenderedAttributes.ts +3 -0
  32. package/src/helpers/getSchemaByResolvedExtensions.ts +2 -1
  33. package/src/inputRules/markInputRule.ts +1 -1
  34. package/src/inputRules/nodeInputRule.ts +1 -1
  35. package/src/inputRules/textInputRule.ts +1 -1
  36. package/src/inputRules/textblockTypeInputRule.ts +1 -1
  37. package/src/inputRules/wrappingInputRule.ts +1 -1
  38. package/src/pasteRules/markPasteRule.ts +1 -1
  39. package/src/pasteRules/nodePasteRule.ts +1 -1
  40. package/src/pasteRules/textPasteRule.ts +1 -1
  41. package/src/types.ts +107 -19
  42. package/src/utilities/mergeAttributes.ts +18 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiptap/core",
3
3
  "description": "headless rich text editor",
4
- "version": "3.0.0-next.1",
4
+ "version": "3.0.0-next.3",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -31,7 +31,7 @@
31
31
  "dist"
32
32
  ],
33
33
  "devDependencies": {
34
- "@tiptap/pm": "^3.0.0-next.1"
34
+ "@tiptap/pm": "^3.0.0-next.3"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@tiptap/pm": "^3.0.0-next.1"
package/src/Editor.ts CHANGED
@@ -13,7 +13,8 @@ import { CommandManager } from './CommandManager.js'
13
13
  import { EventEmitter } from './EventEmitter.js'
14
14
  import { ExtensionManager } from './ExtensionManager.js'
15
15
  import {
16
- ClipboardTextSerializer, Commands, Editable, FocusEvents, Keymap, Tabindex,
16
+ ClipboardTextSerializer, Commands, Drop, Editable, FocusEvents, Keymap, Paste,
17
+ Tabindex,
17
18
  } from './extensions/index.js'
18
19
  import { createDocument } from './helpers/createDocument.js'
19
20
  import { getAttributes } from './helpers/getAttributes.js'
@@ -64,6 +65,11 @@ export class Editor extends EventEmitter<EditorEvents> {
64
65
 
65
66
  public extensionStorage: Record<string, any> = {}
66
67
 
68
+ /**
69
+ * A unique ID for this editor instance.
70
+ */
71
+ public instanceId = Math.random().toString(36).slice(2, 9)
72
+
67
73
  public options: EditorOptions = {
68
74
  element: document.createElement('div'),
69
75
  content: '',
@@ -88,6 +94,8 @@ export class Editor extends EventEmitter<EditorEvents> {
88
94
  onBlur: () => null,
89
95
  onDestroy: () => null,
90
96
  onContentError: ({ error }) => { throw error },
97
+ onPaste: () => null,
98
+ onDrop: () => null,
91
99
  }
92
100
 
93
101
  constructor(options: Partial<EditorOptions> = {}) {
@@ -108,6 +116,8 @@ export class Editor extends EventEmitter<EditorEvents> {
108
116
  this.on('focus', this.options.onFocus)
109
117
  this.on('blur', this.options.onBlur)
110
118
  this.on('destroy', this.options.onDestroy)
119
+ this.on('drop', ({ event, slice, moved }) => this.options.onDrop(event, slice, moved))
120
+ this.on('paste', ({ event, slice }) => this.options.onPaste(event, slice))
111
121
 
112
122
  window.setTimeout(() => {
113
123
  if (this.isDestroyed) {
@@ -212,11 +222,12 @@ export class Editor extends EventEmitter<EditorEvents> {
212
222
  *
213
223
  * @param plugin A ProseMirror plugin
214
224
  * @param handlePlugins Control how to merge the plugin into the existing plugins.
225
+ * @returns The new editor state
215
226
  */
216
227
  public registerPlugin(
217
228
  plugin: Plugin,
218
229
  handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[],
219
- ): void {
230
+ ): EditorState {
220
231
  const plugins = isFunction(handlePlugins)
221
232
  ? handlePlugins(plugin, [...this.state.plugins])
222
233
  : [...this.state.plugins, plugin]
@@ -224,27 +235,44 @@ export class Editor extends EventEmitter<EditorEvents> {
224
235
  const state = this.state.reconfigure({ plugins })
225
236
 
226
237
  this.view.updateState(state)
238
+
239
+ return state
227
240
  }
228
241
 
229
242
  /**
230
243
  * Unregister a ProseMirror plugin.
231
244
  *
232
- * @param nameOrPluginKey The plugins name
245
+ * @param nameOrPluginKeyToRemove The plugins name
246
+ * @returns The new editor state or undefined if the editor is destroyed
233
247
  */
234
- public unregisterPlugin(nameOrPluginKey: string | PluginKey): void {
248
+ public unregisterPlugin(nameOrPluginKeyToRemove: string | PluginKey | (string | PluginKey)[]): EditorState | undefined {
235
249
  if (this.isDestroyed) {
236
- return
250
+ return undefined
237
251
  }
238
252
 
239
- // @ts-ignore
240
- const name = typeof nameOrPluginKey === 'string' ? `${nameOrPluginKey}$` : nameOrPluginKey.key
253
+ const prevPlugins = this.state.plugins
254
+ let plugins = prevPlugins;
241
255
 
242
- const state = this.state.reconfigure({
256
+ ([] as (string | PluginKey)[]).concat(nameOrPluginKeyToRemove).forEach(nameOrPluginKey => {
243
257
  // @ts-ignore
244
- plugins: this.state.plugins.filter(plugin => !plugin.key.startsWith(name)),
258
+ const name = typeof nameOrPluginKey === 'string' ? `${nameOrPluginKey}$` : nameOrPluginKey.key
259
+
260
+ // @ts-ignore
261
+ plugins = prevPlugins.filter(plugin => !plugin.key.startsWith(name))
262
+ })
263
+
264
+ if (prevPlugins.length === plugins.length) {
265
+ // No plugin was removed, so we don’t need to update the state
266
+ return undefined
267
+ }
268
+
269
+ const state = this.state.reconfigure({
270
+ plugins,
245
271
  })
246
272
 
247
273
  this.view.updateState(state)
274
+
275
+ return state
248
276
  }
249
277
 
250
278
  /**
@@ -261,7 +289,14 @@ export class Editor extends EventEmitter<EditorEvents> {
261
289
  FocusEvents,
262
290
  Keymap,
263
291
  Tabindex,
264
- ] : []
292
+ Drop,
293
+ Paste,
294
+ ].filter(ext => {
295
+ if (typeof this.options.enableCoreExtensions === 'object') {
296
+ return this.options.enableCoreExtensions[ext.name as keyof typeof this.options.enableCoreExtensions] !== false
297
+ }
298
+ return true
299
+ }) : []
265
300
  const allExtensions = [...coreExtensions, ...this.options.extensions].filter(extension => {
266
301
  return ['extension', 'node', 'mark'].includes(extension?.type)
267
302
  })
@@ -307,6 +342,9 @@ export class Editor extends EventEmitter<EditorEvents> {
307
342
  editor: this,
308
343
  error: e as Error,
309
344
  disableCollaboration: () => {
345
+ if (this.storage.collaboration) {
346
+ this.storage.collaboration.isDisabled = true
347
+ }
310
348
  // To avoid syncing back invalid content, reinitialize the extensions without the collaboration extension
311
349
  this.options.extensions = this.options.extensions.filter(extension => extension.name !== 'collaboration')
312
350
 
@@ -327,6 +365,11 @@ export class Editor extends EventEmitter<EditorEvents> {
327
365
 
328
366
  this.view = new EditorView(this.options.element, {
329
367
  ...this.options.editorProps,
368
+ attributes: {
369
+ // add `role="textbox"` to the editor element
370
+ role: 'textbox',
371
+ ...this.options.editorProps?.attributes,
372
+ },
330
373
  dispatchTransaction: this.dispatchTransaction.bind(this),
331
374
  state: EditorState.create({
332
375
  doc,
@@ -545,6 +588,13 @@ export class Editor extends EventEmitter<EditorEvents> {
545
588
  this.emit('destroy')
546
589
 
547
590
  if (this.view) {
591
+ // Cleanup our reference to prevent circular references which caused memory leaks
592
+ // @ts-ignore
593
+ const dom = this.view.dom as TiptapEditorHTMLElement
594
+
595
+ if (dom && dom.editor) {
596
+ delete dom.editor
597
+ }
548
598
  this.view.destroy()
549
599
  }
550
600
 
@@ -46,6 +46,15 @@ export class EventEmitter<T extends Record<string, any>> {
46
46
  return this
47
47
  }
48
48
 
49
+ public once<EventName extends StringKeyOf<T>>(event: EventName, fn: CallbackFunction<T, EventName>): this {
50
+ const onceFn = (...args: CallbackType<T, EventName>) => {
51
+ this.off(event, onceFn)
52
+ fn.apply(this, args)
53
+ }
54
+
55
+ return this.on(event, onceFn)
56
+ }
57
+
49
58
  public removeAllListeners(): void {
50
59
  this.callbacks = {}
51
60
  }
@@ -1,7 +1,7 @@
1
1
  import { keymap } from '@tiptap/pm/keymap'
2
- import { Node as ProsemirrorNode, Schema } from '@tiptap/pm/model'
2
+ import { Schema } from '@tiptap/pm/model'
3
3
  import { Plugin } from '@tiptap/pm/state'
4
- import { Decoration, EditorView } from '@tiptap/pm/view'
4
+ import { NodeViewConstructor } from '@tiptap/pm/view'
5
5
 
6
6
  import type { Editor } from './Editor.js'
7
7
  import { getAttributesFromExtensions } from './helpers/getAttributesFromExtensions.js'
@@ -261,7 +261,7 @@ export class ExtensionManager {
261
261
  * Get all node views from the extensions.
262
262
  * @returns An object with all node views where the key is the node name and the value is the node view function
263
263
  */
264
- get nodeViews() {
264
+ get nodeViews(): Record<string, NodeViewConstructor> {
265
265
  const { editor } = this
266
266
  const { nodeExtensions } = splitExtensions(this.extensions)
267
267
 
@@ -289,21 +289,26 @@ export class ExtensionManager {
289
289
  return []
290
290
  }
291
291
 
292
- const nodeview = (
293
- node: ProsemirrorNode,
294
- view: EditorView,
295
- getPos: (() => number) | boolean,
296
- decorations: Decoration[],
292
+ const nodeview: NodeViewConstructor = (
293
+ node,
294
+ view,
295
+ getPos,
296
+ decorations,
297
+ innerDecorations,
297
298
  ) => {
298
299
  const HTMLAttributes = getRenderedAttributes(node, extensionAttributes)
299
300
 
300
301
  return addNodeView()({
301
- editor,
302
+ // pass-through
302
303
  node,
303
- getPos,
304
+ view,
305
+ getPos: getPos as () => number,
304
306
  decorations,
305
- HTMLAttributes,
307
+ innerDecorations,
308
+ // tiptap-specific
309
+ editor,
306
310
  extension,
311
+ HTMLAttributes,
307
312
  })
308
313
  }
309
314
 
package/src/InputRule.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import { Fragment, Node as ProseMirrorNode } from '@tiptap/pm/model'
1
2
  import { EditorState, Plugin, TextSelection } from '@tiptap/pm/state'
2
3
 
3
4
  import { CommandManager } from './CommandManager.js'
4
5
  import { Editor } from './Editor.js'
5
6
  import { createChainableState } from './helpers/createChainableState.js'
7
+ import { getHTMLFromFragment } from './helpers/getHTMLFromFragment.js'
6
8
  import { getTextContentFromNodes } from './helpers/getTextContentFromNodes.js'
7
9
  import {
8
10
  CanCommands,
@@ -14,37 +16,37 @@ import {
14
16
  import { isRegExp } from './utilities/isRegExp.js'
15
17
 
16
18
  export type InputRuleMatch = {
17
- index: number
18
- text: string
19
- replaceWith?: string
20
- match?: RegExpMatchArray
21
- data?: Record<string, any>
22
- }
19
+ index: number;
20
+ text: string;
21
+ replaceWith?: string;
22
+ match?: RegExpMatchArray;
23
+ data?: Record<string, any>;
24
+ };
23
25
 
24
- export type InputRuleFinder = RegExp | ((text: string) => InputRuleMatch | null)
26
+ export type InputRuleFinder = RegExp | ((text: string) => InputRuleMatch | null);
25
27
 
26
28
  export class InputRule {
27
29
  find: InputRuleFinder
28
30
 
29
31
  handler: (props: {
30
- state: EditorState
31
- range: Range
32
- match: ExtendedRegExpMatchArray
33
- commands: SingleCommands
34
- chain: () => ChainedCommands
35
- can: () => CanCommands
32
+ state: EditorState;
33
+ range: Range;
34
+ match: ExtendedRegExpMatchArray;
35
+ commands: SingleCommands;
36
+ chain: () => ChainedCommands;
37
+ can: () => CanCommands;
36
38
  }) => void | null
37
39
 
38
40
  constructor(config: {
39
- find: InputRuleFinder
41
+ find: InputRuleFinder;
40
42
  handler: (props: {
41
- state: EditorState
42
- range: Range
43
- match: ExtendedRegExpMatchArray
44
- commands: SingleCommands
45
- chain: () => ChainedCommands
46
- can: () => CanCommands
47
- }) => void | null
43
+ state: EditorState;
44
+ range: Range;
45
+ match: ExtendedRegExpMatchArray;
46
+ commands: SingleCommands;
47
+ chain: () => ChainedCommands;
48
+ can: () => CanCommands;
49
+ }) => void | null;
48
50
  }) {
49
51
  this.find = config.find
50
52
  this.handler = config.handler
@@ -85,12 +87,12 @@ const inputRuleMatcherHandler = (
85
87
  }
86
88
 
87
89
  function run(config: {
88
- editor: Editor
89
- from: number
90
- to: number
91
- text: string
92
- rules: InputRule[]
93
- plugin: Plugin
90
+ editor: Editor;
91
+ from: number;
92
+ to: number;
93
+ text: string;
94
+ rules: InputRule[];
95
+ plugin: Plugin;
94
96
  }): boolean {
95
97
  const {
96
98
  editor, from, to, text, rules, plugin,
@@ -184,7 +186,7 @@ export function inputRulesPlugin(props: { editor: Editor; rules: InputRule[] }):
184
186
  init() {
185
187
  return null
186
188
  },
187
- apply(tr, prev) {
189
+ apply(tr, prev, state) {
188
190
  const stored = tr.getMeta(plugin)
189
191
 
190
192
  if (stored) {
@@ -192,12 +194,25 @@ export function inputRulesPlugin(props: { editor: Editor; rules: InputRule[] }):
192
194
  }
193
195
 
194
196
  // if InputRule is triggered by insertContent()
195
- const simulatedInputMeta = tr.getMeta('applyInputRules')
197
+ const simulatedInputMeta = tr.getMeta('applyInputRules') as
198
+ | undefined
199
+ | {
200
+ from: number;
201
+ text: string | ProseMirrorNode | Fragment;
202
+ }
196
203
  const isSimulatedInput = !!simulatedInputMeta
197
204
 
198
205
  if (isSimulatedInput) {
199
206
  setTimeout(() => {
200
- const { from, text } = simulatedInputMeta
207
+ let { text } = simulatedInputMeta
208
+
209
+ if (typeof text === 'string') {
210
+ text = text as string
211
+ } else {
212
+ text = getHTMLFromFragment(Fragment.from(text), state.schema)
213
+ }
214
+
215
+ const { from } = simulatedInputMeta
201
216
  const to = from + text.length
202
217
 
203
218
  run({
package/src/Node.ts CHANGED
@@ -595,6 +595,25 @@ declare module '@tiptap/core' {
595
595
  editor?: Editor
596
596
  }) => NodeSpec['whitespace'])
597
597
 
598
+ /**
599
+ * Allows a **single** node to be set as linebreak equivalent (e.g. hardBreak).
600
+ * When converting between block types that have whitespace set to "pre"
601
+ * and don't support the linebreak node (e.g. codeBlock) and other block types
602
+ * that do support the linebreak node (e.g. paragraphs) - this node will be used
603
+ * as the linebreak instead of stripping the newline.
604
+ *
605
+ * See [linebreakReplacement](https://prosemirror.net/docs/ref/#model.NodeSpec.linebreakReplacement).
606
+ */
607
+ linebreakReplacement?:
608
+ | NodeSpec['linebreakReplacement']
609
+ | ((this: {
610
+ name: string
611
+ options: Options
612
+ storage: Storage
613
+ parent: ParentConfig<NodeConfig<Options, Storage>>['linebreakReplacement']
614
+ editor?: Editor
615
+ }) => NodeSpec['linebreakReplacement'])
616
+
598
617
  /**
599
618
  * When enabled, enables both
600
619
  * [`definingAsContext`](https://prosemirror.net/docs/ref/#model.NodeSpec.definingAsContext) and
package/src/NodePos.ts CHANGED
@@ -135,8 +135,9 @@ export class NodePos {
135
135
 
136
136
  this.node.content.forEach((node, offset) => {
137
137
  const isBlock = node.isBlock && !node.isTextblock
138
+ const isNonTextAtom = node.isAtom && !node.isText
138
139
 
139
- const targetPos = this.pos + offset + 1
140
+ const targetPos = this.pos + offset + (isNonTextAtom ? 0 : 1)
140
141
  const $pos = this.resolvedPos.doc.resolve(targetPos)
141
142
 
142
143
  if (!isBlock && $pos.depth <= this.depth) {
@@ -235,9 +236,13 @@ export class NodePos {
235
236
  }
236
237
 
237
238
  setAttribute(attributes: { [key: string]: any }) {
238
- const oldSelection = this.editor.state.selection
239
+ const { tr } = this.editor.state
239
240
 
240
- this.editor.chain().setTextSelection(this.from).updateAttributes(this.node.type.name, attributes).setTextSelection(oldSelection.from)
241
- .run()
241
+ tr.setNodeMarkup(this.from, undefined, {
242
+ ...this.node.attrs,
243
+ ...attributes,
244
+ })
245
+
246
+ this.editor.view.dispatch(tr)
242
247
  }
243
248
  }
package/src/NodeView.ts CHANGED
@@ -1,9 +1,7 @@
1
- import { Node as ProseMirrorNode } from '@tiptap/pm/model'
2
1
  import { NodeSelection } from '@tiptap/pm/state'
3
- import { NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
2
+ import { NodeView as ProseMirrorNodeView, ViewMutationRecord } from '@tiptap/pm/view'
4
3
 
5
4
  import { Editor as CoreEditor } from './Editor.js'
6
- import { Node } from './Node.js'
7
5
  import { DecorationWithType, NodeViewRendererOptions, NodeViewRendererProps } from './types.js'
8
6
  import { isAndroid } from './utilities/isAndroid.js'
9
7
  import { isiOS } from './utilities/isiOS.js'
@@ -23,13 +21,19 @@ export class NodeView<
23
21
 
24
22
  options: Options
25
23
 
26
- extension: Node
24
+ extension: NodeViewRendererProps['extension']
27
25
 
28
- node: ProseMirrorNode
26
+ node: NodeViewRendererProps['node']
29
27
 
30
- decorations: DecorationWithType[]
28
+ decorations: NodeViewRendererProps['decorations']
31
29
 
32
- getPos: any
30
+ innerDecorations: NodeViewRendererProps['innerDecorations']
31
+
32
+ view: NodeViewRendererProps['view']
33
+
34
+ getPos: NodeViewRendererProps['getPos']
35
+
36
+ HTMLAttributes: NodeViewRendererProps['HTMLAttributes']
33
37
 
34
38
  isDragging = false
35
39
 
@@ -44,6 +48,9 @@ export class NodeView<
44
48
  this.extension = props.extension
45
49
  this.node = props.node
46
50
  this.decorations = props.decorations as DecorationWithType[]
51
+ this.innerDecorations = props.innerDecorations
52
+ this.view = props.view
53
+ this.HTMLAttributes = props.HTMLAttributes
47
54
  this.getPos = props.getPos
48
55
  this.mount()
49
56
  }
@@ -93,9 +100,14 @@ export class NodeView<
93
100
 
94
101
  event.dataTransfer?.setDragImage(this.dom, x, y)
95
102
 
103
+ const pos = this.getPos()
104
+
105
+ if (typeof pos !== 'number') {
106
+ return
107
+ }
96
108
  // we need to tell ProseMirror that we want to move the whole node
97
109
  // so we create a NodeSelection
98
- const selection = NodeSelection.create(view.state.doc, this.getPos())
110
+ const selection = NodeSelection.create(view.state.doc, pos)
99
111
  const transaction = view.state.tr.setSelection(selection)
100
112
 
101
113
  view.dispatch(transaction)
@@ -139,11 +151,11 @@ export class NodeView<
139
151
  // ProseMirror tries to drag selectable nodes
140
152
  // even if `draggable` is set to `false`
141
153
  // this fix prevents that
142
- if (!isDraggable && isSelectable && isDragEvent) {
154
+ if (!isDraggable && isSelectable && isDragEvent && event.target === this.dom) {
143
155
  event.preventDefault()
144
156
  }
145
157
 
146
- if (isDraggable && isDragEvent && !isDragging) {
158
+ if (isDraggable && isDragEvent && !isDragging && event.target === this.dom) {
147
159
  event.preventDefault()
148
160
  return false
149
161
  }
@@ -197,7 +209,12 @@ export class NodeView<
197
209
  return true
198
210
  }
199
211
 
200
- ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) {
212
+ /**
213
+ * Called when a DOM [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) or a selection change happens within the view.
214
+ * @return `false` if the editor should re-read the selection or re-parse the range around the mutation
215
+ * @return `true` if it can safely be ignored.
216
+ */
217
+ ignoreMutation(mutation: ViewMutationRecord) {
201
218
  if (!this.dom || !this.contentDOM) {
202
219
  return true
203
220
  }
@@ -254,10 +271,17 @@ export class NodeView<
254
271
  return true
255
272
  }
256
273
 
257
- updateAttributes(attributes: {}) {
274
+ /**
275
+ * Update the attributes of the prosemirror node.
276
+ */
277
+ updateAttributes(attributes: Record<string, any>): void {
258
278
  this.editor.commands.command(({ tr }) => {
259
279
  const pos = this.getPos()
260
280
 
281
+ if (typeof pos !== 'number') {
282
+ return false
283
+ }
284
+
261
285
  tr.setNodeMarkup(pos, undefined, {
262
286
  ...this.node.attrs,
263
287
  ...attributes,
@@ -267,8 +291,15 @@ export class NodeView<
267
291
  })
268
292
  }
269
293
 
294
+ /**
295
+ * Delete the node.
296
+ */
270
297
  deleteNode(): void {
271
298
  const from = this.getPos()
299
+
300
+ if (typeof from !== 'number') {
301
+ return
302
+ }
272
303
  const to = from + this.node.nodeSize
273
304
 
274
305
  this.editor.commands.deleteRange({ from, to })