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

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 (146) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +5 -1
  3. package/dist/index.cjs +2627 -2651
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +2423 -2688
  6. package/dist/index.d.ts +2423 -2688
  7. package/dist/index.js +2639 -2684
  8. package/dist/index.js.map +1 -1
  9. package/dist/jsx-runtime/jsx-runtime.cjs +56 -0
  10. package/dist/jsx-runtime/jsx-runtime.cjs.map +1 -0
  11. package/dist/jsx-runtime/jsx-runtime.d.cts +22 -0
  12. package/dist/jsx-runtime/jsx-runtime.d.ts +22 -0
  13. package/dist/jsx-runtime/jsx-runtime.js +26 -0
  14. package/dist/jsx-runtime/jsx-runtime.js.map +1 -0
  15. package/jsx-runtime/index.cjs +1 -0
  16. package/jsx-runtime/index.d.cts +1 -0
  17. package/jsx-runtime/index.d.ts +1 -0
  18. package/jsx-runtime/index.js +1 -0
  19. package/package.json +27 -6
  20. package/src/CommandManager.ts +2 -9
  21. package/src/Editor.ts +191 -94
  22. package/src/EventEmitter.ts +7 -10
  23. package/src/Extendable.ts +483 -0
  24. package/src/Extension.ts +5 -496
  25. package/src/ExtensionManager.ts +81 -135
  26. package/src/InputRule.ts +35 -48
  27. package/src/Mark.ts +135 -623
  28. package/src/MarkView.ts +66 -0
  29. package/src/Node.ts +325 -829
  30. package/src/NodePos.ts +1 -3
  31. package/src/NodeView.ts +10 -20
  32. package/src/PasteRule.ts +43 -55
  33. package/src/Tracker.ts +7 -9
  34. package/src/commands/blur.ts +14 -12
  35. package/src/commands/clearContent.ts +12 -5
  36. package/src/commands/clearNodes.ts +32 -30
  37. package/src/commands/command.ts +1 -1
  38. package/src/commands/createParagraphNear.ts +5 -3
  39. package/src/commands/cut.ts +12 -10
  40. package/src/commands/deleteCurrentNode.ts +23 -21
  41. package/src/commands/deleteNode.ts +18 -16
  42. package/src/commands/deleteRange.ts +10 -8
  43. package/src/commands/deleteSelection.ts +5 -3
  44. package/src/commands/enter.ts +6 -4
  45. package/src/commands/exitCode.ts +5 -3
  46. package/src/commands/extendMarkRange.ts +14 -12
  47. package/src/commands/first.ts +2 -4
  48. package/src/commands/focus.ts +51 -48
  49. package/src/commands/forEach.ts +2 -2
  50. package/src/commands/insertContent.ts +12 -14
  51. package/src/commands/insertContentAt.ts +105 -98
  52. package/src/commands/join.ts +20 -12
  53. package/src/commands/joinItemBackward.ts +16 -18
  54. package/src/commands/joinItemForward.ts +16 -18
  55. package/src/commands/joinTextblockBackward.ts +5 -3
  56. package/src/commands/joinTextblockForward.ts +5 -3
  57. package/src/commands/keyboardShortcut.ts +29 -34
  58. package/src/commands/lift.ts +10 -8
  59. package/src/commands/liftEmptyBlock.ts +6 -4
  60. package/src/commands/liftListItem.ts +6 -4
  61. package/src/commands/newlineInCode.ts +5 -3
  62. package/src/commands/resetAttributes.ts +36 -41
  63. package/src/commands/scrollIntoView.ts +9 -7
  64. package/src/commands/selectAll.ts +10 -8
  65. package/src/commands/selectNodeBackward.ts +5 -3
  66. package/src/commands/selectNodeForward.ts +5 -3
  67. package/src/commands/selectParentNode.ts +5 -3
  68. package/src/commands/selectTextblockEnd.ts +5 -3
  69. package/src/commands/selectTextblockStart.ts +5 -3
  70. package/src/commands/setContent.ts +37 -36
  71. package/src/commands/setMark.ts +55 -57
  72. package/src/commands/setMeta.ts +7 -5
  73. package/src/commands/setNode.ts +32 -30
  74. package/src/commands/setNodeSelection.ts +11 -9
  75. package/src/commands/setTextSelection.ts +15 -13
  76. package/src/commands/sinkListItem.ts +6 -4
  77. package/src/commands/splitBlock.ts +67 -76
  78. package/src/commands/splitListItem.ts +93 -106
  79. package/src/commands/toggleList.ts +73 -71
  80. package/src/commands/toggleMark.ts +11 -9
  81. package/src/commands/toggleNode.ts +18 -16
  82. package/src/commands/toggleWrap.ts +10 -8
  83. package/src/commands/undoInputRule.ts +31 -29
  84. package/src/commands/unsetAllMarks.ts +16 -14
  85. package/src/commands/unsetMark.ts +27 -25
  86. package/src/commands/updateAttributes.ts +92 -100
  87. package/src/commands/wrapIn.ts +6 -4
  88. package/src/commands/wrapInList.ts +6 -4
  89. package/src/extensions/clipboardTextSerializer.ts +2 -4
  90. package/src/extensions/delete.ts +89 -0
  91. package/src/extensions/focusEvents.ts +2 -6
  92. package/src/extensions/index.ts +1 -0
  93. package/src/extensions/keymap.ts +58 -50
  94. package/src/extensions/paste.ts +0 -1
  95. package/src/extensions/tabindex.ts +1 -1
  96. package/src/helpers/combineTransactionSteps.ts +1 -4
  97. package/src/helpers/createChainableState.ts +1 -4
  98. package/src/helpers/createDocument.ts +1 -3
  99. package/src/helpers/createNodeFromContent.ts +4 -10
  100. package/src/helpers/findChildrenInRange.ts +1 -5
  101. package/src/helpers/findParentNode.ts +3 -1
  102. package/src/helpers/flattenExtensions.ts +30 -0
  103. package/src/helpers/getAttributes.ts +1 -4
  104. package/src/helpers/getAttributesFromExtensions.ts +28 -37
  105. package/src/helpers/getChangedRanges.ts +13 -11
  106. package/src/helpers/getExtensionField.ts +11 -11
  107. package/src/helpers/getMarkAttributes.ts +1 -4
  108. package/src/helpers/getMarkRange.ts +5 -15
  109. package/src/helpers/getMarkType.ts +1 -3
  110. package/src/helpers/getNodeAttributes.ts +1 -4
  111. package/src/helpers/getNodeType.ts +1 -3
  112. package/src/helpers/getRenderedAttributes.ts +1 -3
  113. package/src/helpers/getSchema.ts +2 -2
  114. package/src/helpers/getSchemaByResolvedExtensions.ts +45 -77
  115. package/src/helpers/getSplittedAttributes.ts +4 -4
  116. package/src/helpers/getTextContentFromNodes.ts +8 -11
  117. package/src/helpers/index.ts +4 -0
  118. package/src/helpers/injectExtensionAttributesToParseRule.ts +1 -1
  119. package/src/helpers/isActive.ts +1 -5
  120. package/src/helpers/isExtensionRulesEnabled.ts +1 -3
  121. package/src/helpers/isNodeEmpty.ts +2 -2
  122. package/src/helpers/resolveExtensions.ts +25 -0
  123. package/src/helpers/resolveFocusPosition.ts +3 -14
  124. package/src/helpers/rewriteUnknownContent.ts +149 -0
  125. package/src/helpers/sortExtensions.ts +26 -0
  126. package/src/index.ts +3 -7
  127. package/src/inputRules/markInputRule.ts +1 -5
  128. package/src/inputRules/nodeInputRule.ts +2 -9
  129. package/src/inputRules/textInputRule.ts +1 -4
  130. package/src/inputRules/textblockTypeInputRule.ts +2 -8
  131. package/src/inputRules/wrappingInputRule.ts +13 -19
  132. package/src/jsx-runtime.ts +64 -0
  133. package/src/pasteRules/markPasteRule.ts +1 -3
  134. package/src/pasteRules/nodePasteRule.ts +2 -8
  135. package/src/pasteRules/textPasteRule.ts +1 -4
  136. package/src/types.ts +529 -174
  137. package/src/utilities/createStyleTag.ts +3 -1
  138. package/src/utilities/deleteProps.ts +7 -11
  139. package/src/utilities/elementFromString.ts +3 -0
  140. package/src/utilities/findDuplicates.ts +4 -1
  141. package/src/utilities/index.ts +1 -0
  142. package/src/utilities/isFunction.ts +1 -0
  143. package/src/utilities/isMacOS.ts +1 -3
  144. package/src/utilities/isiOS.ts +5 -10
  145. package/src/utilities/mergeAttributes.ts +17 -7
  146. package/src/utilities/removeDuplicates.ts +1 -3
package/src/Editor.ts CHANGED
@@ -1,19 +1,20 @@
1
- import {
2
- MarkType,
3
- Node as ProseMirrorNode,
4
- NodeType,
5
- Schema,
6
- } from '@tiptap/pm/model'
7
- import {
8
- EditorState, Plugin, PluginKey, Transaction,
9
- } from '@tiptap/pm/state'
1
+ /* eslint-disable @typescript-eslint/no-empty-object-type */
2
+ import { MarkType, Node as ProseMirrorNode, NodeType, Schema } from '@tiptap/pm/model'
3
+ import { EditorState, Plugin, PluginKey, Transaction } from '@tiptap/pm/state'
10
4
  import { EditorView } from '@tiptap/pm/view'
11
5
 
12
6
  import { CommandManager } from './CommandManager.js'
13
7
  import { EventEmitter } from './EventEmitter.js'
14
8
  import { ExtensionManager } from './ExtensionManager.js'
15
9
  import {
16
- ClipboardTextSerializer, Commands, Drop, Editable, FocusEvents, Keymap, Paste,
10
+ ClipboardTextSerializer,
11
+ Commands,
12
+ Delete,
13
+ Drop,
14
+ Editable,
15
+ FocusEvents,
16
+ Keymap,
17
+ Paste,
17
18
  Tabindex,
18
19
  } from './extensions/index.js'
19
20
  import { createDocument } from './helpers/createDocument.js'
@@ -24,16 +25,19 @@ import { getTextSerializersFromSchema } from './helpers/getTextSerializersFromSc
24
25
  import { isActive } from './helpers/isActive.js'
25
26
  import { isNodeEmpty } from './helpers/isNodeEmpty.js'
26
27
  import { resolveFocusPosition } from './helpers/resolveFocusPosition.js'
28
+ import type { Storage } from './index.js'
27
29
  import { NodePos } from './NodePos.js'
28
30
  import { style } from './style.js'
29
31
  import {
30
32
  CanCommands,
31
33
  ChainedCommands,
34
+ DocumentType,
32
35
  EditorEvents,
33
36
  EditorOptions,
34
- JSONContent,
37
+ NodeType as TNodeType,
35
38
  SingleCommands,
36
39
  TextSerializer,
40
+ TextType as TTextType,
37
41
  } from './types.js'
38
42
  import { createStyleTag } from './utilities/createStyleTag.js'
39
43
  import { isFunction } from './utilities/isFunction.js'
@@ -54,16 +58,18 @@ export class Editor extends EventEmitter<EditorEvents> {
54
58
 
55
59
  public schema!: Schema
56
60
 
57
- public view!: EditorView
61
+ private editorView: EditorView | null = null
58
62
 
59
63
  public isFocused = false
60
64
 
65
+ private editorState!: EditorState
66
+
61
67
  /**
62
68
  * The editor is considered initialized after the `create` event has been emitted.
63
69
  */
64
70
  public isInitialized = false
65
71
 
66
- public extensionStorage: Record<string, any> = {}
72
+ public extensionStorage: Storage = {} as Storage
67
73
 
68
74
  /**
69
75
  * A unique ID for this editor instance.
@@ -71,7 +77,7 @@ export class Editor extends EventEmitter<EditorEvents> {
71
77
  public instanceId = Math.random().toString(36).slice(2, 9)
72
78
 
73
79
  public options: EditorOptions = {
74
- element: document.createElement('div'),
80
+ element: typeof document !== 'undefined' ? document.createElement('div') : null,
75
81
  content: '',
76
82
  injectCSS: true,
77
83
  injectNonce: undefined,
@@ -93,9 +99,12 @@ export class Editor extends EventEmitter<EditorEvents> {
93
99
  onFocus: () => null,
94
100
  onBlur: () => null,
95
101
  onDestroy: () => null,
96
- onContentError: ({ error }) => { throw error },
102
+ onContentError: ({ error }) => {
103
+ throw error
104
+ },
97
105
  onPaste: () => null,
98
106
  onDrop: () => null,
107
+ onDelete: () => null,
99
108
  }
100
109
 
101
110
  constructor(options: Partial<EditorOptions> = {}) {
@@ -107,8 +116,6 @@ export class Editor extends EventEmitter<EditorEvents> {
107
116
  this.on('beforeCreate', this.options.onBeforeCreate)
108
117
  this.emit('beforeCreate', { editor: this })
109
118
  this.on('contentError', this.options.onContentError)
110
- this.createView()
111
- this.injectCSS()
112
119
  this.on('create', this.options.onCreate)
113
120
  this.on('update', this.options.onUpdate)
114
121
  this.on('selectionUpdate', this.options.onSelectionUpdate)
@@ -118,6 +125,30 @@ export class Editor extends EventEmitter<EditorEvents> {
118
125
  this.on('destroy', this.options.onDestroy)
119
126
  this.on('drop', ({ event, slice, moved }) => this.options.onDrop(event, slice, moved))
120
127
  this.on('paste', ({ event, slice }) => this.options.onPaste(event, slice))
128
+ this.on('delete', this.options.onDelete)
129
+
130
+ const initialDoc = this.createDoc()
131
+ const selection = resolveFocusPosition(initialDoc, this.options.autofocus)
132
+
133
+ // Set editor state immediately, so that it's available independently from the view
134
+ this.editorState = EditorState.create({
135
+ doc: initialDoc,
136
+ schema: this.schema,
137
+ selection: selection || undefined,
138
+ })
139
+
140
+ if (this.options.element) {
141
+ this.mount(this.options.element)
142
+ }
143
+ }
144
+
145
+ public mount(el: NonNullable<EditorOptions['element']> & {}) {
146
+ if (typeof document === 'undefined') {
147
+ throw new Error(
148
+ `[tiptap error]: The editor cannot be mounted because there is no 'document' defined in this environment.`,
149
+ )
150
+ }
151
+ this.createView(el)
121
152
 
122
153
  window.setTimeout(() => {
123
154
  if (this.isDestroyed) {
@@ -133,7 +164,7 @@ export class Editor extends EventEmitter<EditorEvents> {
133
164
  /**
134
165
  * Returns the editor storage.
135
166
  */
136
- public get storage(): Record<string, any> {
167
+ public get storage(): Storage {
137
168
  return this.extensionStorage
138
169
  }
139
170
 
@@ -162,7 +193,7 @@ export class Editor extends EventEmitter<EditorEvents> {
162
193
  * Inject CSS styles.
163
194
  */
164
195
  private injectCSS(): void {
165
- if (this.options.injectCSS && document) {
196
+ if (this.options.injectCSS && typeof document !== 'undefined') {
166
197
  this.css = createStyleTag(style, this.options.injectNonce)
167
198
  }
168
199
  }
@@ -178,7 +209,7 @@ export class Editor extends EventEmitter<EditorEvents> {
178
209
  ...options,
179
210
  }
180
211
 
181
- if (!this.view || !this.state || this.isDestroyed) {
212
+ if (!this.editorView || !this.state || this.isDestroyed) {
182
213
  return
183
214
  }
184
215
 
@@ -196,7 +227,7 @@ export class Editor extends EventEmitter<EditorEvents> {
196
227
  this.setOptions({ editable })
197
228
 
198
229
  if (emitUpdate) {
199
- this.emit('update', { editor: this, transaction: this.state.tr })
230
+ this.emit('update', { editor: this, transaction: this.state.tr, appendedTransactions: [] })
200
231
  }
201
232
  }
202
233
 
@@ -210,11 +241,57 @@ export class Editor extends EventEmitter<EditorEvents> {
210
241
  return this.options.editable && this.view && this.view.editable
211
242
  }
212
243
 
244
+ /**
245
+ * Returns the editor state.
246
+ */
247
+ public get view(): EditorView {
248
+ if (this.editorView) {
249
+ return this.editorView
250
+ }
251
+
252
+ return new Proxy(
253
+ {
254
+ state: this.editorState,
255
+ updateState: (state: EditorState): ReturnType<EditorView['updateState']> => {
256
+ this.editorState = state
257
+ },
258
+ dispatch: (tr: Transaction): ReturnType<EditorView['dispatch']> => {
259
+ this.editorState = this.state.apply(tr)
260
+ },
261
+
262
+ // Stub some commonly accessed properties to prevent errors
263
+ composing: false,
264
+ dragging: null,
265
+ editable: true,
266
+ } as EditorView,
267
+ {
268
+ get: (obj, key) => {
269
+ // Specifically always return the most recent editorState
270
+ if (key === 'state') {
271
+ return this.editorState
272
+ }
273
+ if (key in obj) {
274
+ return Reflect.get(obj, key)
275
+ }
276
+
277
+ // We throw an error here, because we know the view is not available
278
+ throw new Error(
279
+ `[tiptap error]: The editor view is not available. Cannot access view['${key as string}']. The editor may not be mounted yet.`,
280
+ )
281
+ },
282
+ },
283
+ ) as EditorView
284
+ }
285
+
213
286
  /**
214
287
  * Returns the editor state.
215
288
  */
216
289
  public get state(): EditorState {
217
- return this.view.state
290
+ if (this.editorView) {
291
+ this.editorState = this.view.state
292
+ }
293
+
294
+ return this.editorState
218
295
  }
219
296
 
220
297
  /**
@@ -245,15 +322,17 @@ export class Editor extends EventEmitter<EditorEvents> {
245
322
  * @param nameOrPluginKeyToRemove The plugins name
246
323
  * @returns The new editor state or undefined if the editor is destroyed
247
324
  */
248
- public unregisterPlugin(nameOrPluginKeyToRemove: string | PluginKey | (string | PluginKey)[]): EditorState | undefined {
325
+ public unregisterPlugin(
326
+ nameOrPluginKeyToRemove: string | PluginKey | (string | PluginKey)[],
327
+ ): EditorState | undefined {
249
328
  if (this.isDestroyed) {
250
329
  return undefined
251
330
  }
252
331
 
253
332
  const prevPlugins = this.state.plugins
254
- let plugins = prevPlugins;
333
+ let plugins = prevPlugins
255
334
 
256
- ([] as (string | PluginKey)[]).concat(nameOrPluginKeyToRemove).forEach(nameOrPluginKey => {
335
+ ;([] as (string | PluginKey)[]).concat(nameOrPluginKeyToRemove).forEach(nameOrPluginKey => {
257
336
  // @ts-ignore
258
337
  const name = typeof nameOrPluginKey === 'string' ? `${nameOrPluginKey}$` : nameOrPluginKey.key
259
338
 
@@ -279,24 +358,28 @@ export class Editor extends EventEmitter<EditorEvents> {
279
358
  * Creates an extension manager.
280
359
  */
281
360
  private createExtensionManager(): void {
282
-
283
- const coreExtensions = this.options.enableCoreExtensions ? [
284
- Editable,
285
- ClipboardTextSerializer.configure({
286
- blockSeparator: this.options.coreExtensionOptions?.clipboardTextSerializer?.blockSeparator,
287
- }),
288
- Commands,
289
- FocusEvents,
290
- Keymap,
291
- Tabindex,
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
- }) : []
361
+ const coreExtensions = this.options.enableCoreExtensions
362
+ ? [
363
+ Editable,
364
+ ClipboardTextSerializer.configure({
365
+ blockSeparator: this.options.coreExtensionOptions?.clipboardTextSerializer?.blockSeparator,
366
+ }),
367
+ Commands,
368
+ FocusEvents,
369
+ Keymap,
370
+ Tabindex,
371
+ Drop,
372
+ Paste,
373
+ Delete,
374
+ ].filter(ext => {
375
+ if (typeof this.options.enableCoreExtensions === 'object') {
376
+ return (
377
+ this.options.enableCoreExtensions[ext.name as keyof typeof this.options.enableCoreExtensions] !== false
378
+ )
379
+ }
380
+ return true
381
+ })
382
+ : []
300
383
  const allExtensions = [...coreExtensions, ...this.options.extensions].filter(extension => {
301
384
  return ['extension', 'node', 'mark'].includes(extension?.type)
302
385
  })
@@ -321,20 +404,20 @@ export class Editor extends EventEmitter<EditorEvents> {
321
404
  }
322
405
 
323
406
  /**
324
- * Creates a ProseMirror view.
407
+ * Creates the initial document.
325
408
  */
326
- private createView(): void {
409
+ private createDoc(): ProseMirrorNode {
327
410
  let doc: ProseMirrorNode
328
411
 
329
412
  try {
330
- doc = createDocument(
331
- this.options.content,
332
- this.schema,
333
- this.options.parseOptions,
334
- { errorOnInvalidContent: this.options.enableContentCheck },
335
- )
413
+ doc = createDocument(this.options.content, this.schema, this.options.parseOptions, {
414
+ errorOnInvalidContent: this.options.enableContentCheck,
415
+ })
336
416
  } catch (e) {
337
- if (!(e instanceof Error) || !['[tiptap error]: Invalid JSON content', '[tiptap error]: Invalid HTML content'].includes(e.message)) {
417
+ if (
418
+ !(e instanceof Error) ||
419
+ !['[tiptap error]: Invalid JSON content', '[tiptap error]: Invalid HTML content'].includes(e.message)
420
+ ) {
338
421
  // Not the content error we were expecting
339
422
  throw e
340
423
  }
@@ -342,8 +425,12 @@ export class Editor extends EventEmitter<EditorEvents> {
342
425
  editor: this,
343
426
  error: e as Error,
344
427
  disableCollaboration: () => {
345
- if (this.storage.collaboration) {
346
- this.storage.collaboration.isDisabled = true
428
+ if (
429
+ 'collaboration' in this.storage &&
430
+ typeof this.storage.collaboration === 'object' &&
431
+ this.storage.collaboration
432
+ ) {
433
+ ;(this.storage.collaboration as any).isDisabled = true
347
434
  }
348
435
  // To avoid syncing back invalid content, reinitialize the extensions without the collaboration extension
349
436
  this.options.extensions = this.options.extensions.filter(extension => extension.name !== 'collaboration')
@@ -354,16 +441,18 @@ export class Editor extends EventEmitter<EditorEvents> {
354
441
  })
355
442
 
356
443
  // Content is invalid, but attempt to create it anyway, stripping out the invalid parts
357
- doc = createDocument(
358
- this.options.content,
359
- this.schema,
360
- this.options.parseOptions,
361
- { errorOnInvalidContent: false },
362
- )
444
+ doc = createDocument(this.options.content, this.schema, this.options.parseOptions, {
445
+ errorOnInvalidContent: false,
446
+ })
363
447
  }
364
- const selection = resolveFocusPosition(doc, this.options.autofocus)
448
+ return doc
449
+ }
365
450
 
366
- this.view = new EditorView(this.options.element, {
451
+ /**
452
+ * Creates a ProseMirror view.
453
+ */
454
+ private createView(element: NonNullable<EditorOptions['element']> & {}): void {
455
+ this.editorView = new EditorView(element, {
367
456
  ...this.options.editorProps,
368
457
  attributes: {
369
458
  // add `role="textbox"` to the editor element
@@ -371,10 +460,7 @@ export class Editor extends EventEmitter<EditorEvents> {
371
460
  ...this.options.editorProps?.attributes,
372
461
  },
373
462
  dispatchTransaction: this.dispatchTransaction.bind(this),
374
- state: EditorState.create({
375
- doc,
376
- selection: selection || undefined,
377
- }),
463
+ state: this.editorState,
378
464
  })
379
465
 
380
466
  // `editor.view` is not yet available at this time.
@@ -387,6 +473,7 @@ export class Editor extends EventEmitter<EditorEvents> {
387
473
 
388
474
  this.createNodeViews()
389
475
  this.prependClass()
476
+ this.injectCSS()
390
477
 
391
478
  // Let’s store the editor instance in the DOM element.
392
479
  // So we’ll have access to it for tests.
@@ -405,6 +492,7 @@ export class Editor extends EventEmitter<EditorEvents> {
405
492
  }
406
493
 
407
494
  this.view.setProps({
495
+ markViews: this.extensionManager.markViews,
408
496
  nodeViews: this.extensionManager.nodeViews,
409
497
  })
410
498
  }
@@ -420,7 +508,7 @@ export class Editor extends EventEmitter<EditorEvents> {
420
508
 
421
509
  private capturedTransaction: Transaction | null = null
422
510
 
423
- public captureTransaction(fn: Function) {
511
+ public captureTransaction(fn: () => void) {
424
512
  this.isCapturingTransaction = true
425
513
  fn()
426
514
  this.isCapturingTransaction = false
@@ -456,18 +544,30 @@ export class Editor extends EventEmitter<EditorEvents> {
456
544
  return
457
545
  }
458
546
 
459
- const state = this.state.apply(transaction)
547
+ // Apply transaction and get resulting state and transactions
548
+ const { state, transactions } = this.state.applyTransaction(transaction)
460
549
  const selectionHasChanged = !this.state.selection.eq(state.selection)
550
+ const rootTrWasApplied = transactions.includes(transaction)
551
+ const prevState = this.state
461
552
 
462
553
  this.emit('beforeTransaction', {
463
554
  editor: this,
464
555
  transaction,
465
556
  nextState: state,
466
557
  })
558
+
559
+ // If transaction was filtered out, we can return early
560
+ if (!rootTrWasApplied) {
561
+ return
562
+ }
563
+
467
564
  this.view.updateState(state)
565
+
566
+ // Emit transaction event with appended transactions info
468
567
  this.emit('transaction', {
469
568
  editor: this,
470
569
  transaction,
570
+ appendedTransactions: transactions.slice(1),
471
571
  })
472
572
 
473
573
  if (selectionHasChanged) {
@@ -477,14 +577,17 @@ export class Editor extends EventEmitter<EditorEvents> {
477
577
  })
478
578
  }
479
579
 
480
- const focus = transaction.getMeta('focus')
481
- const blur = transaction.getMeta('blur')
580
+ // Only emit the latest between focus and blur events
581
+ const mostRecentFocusTr = transactions.findLast(tr => tr.getMeta('focus') || tr.getMeta('blur'))
582
+ const focus = mostRecentFocusTr?.getMeta('focus')
583
+ const blur = mostRecentFocusTr?.getMeta('blur')
482
584
 
483
585
  if (focus) {
484
586
  this.emit('focus', {
485
587
  editor: this,
486
588
  event: focus.event,
487
- transaction,
589
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
590
+ transaction: mostRecentFocusTr!,
488
591
  })
489
592
  }
490
593
 
@@ -492,17 +595,24 @@ export class Editor extends EventEmitter<EditorEvents> {
492
595
  this.emit('blur', {
493
596
  editor: this,
494
597
  event: blur.event,
495
- transaction,
598
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
599
+ transaction: mostRecentFocusTr!,
496
600
  })
497
601
  }
498
602
 
499
- if (!transaction.docChanged || transaction.getMeta('preventUpdate')) {
603
+ // Compare states for update event
604
+ if (
605
+ transaction.getMeta('preventUpdate') ||
606
+ !transactions.some(tr => tr.docChanged) ||
607
+ prevState.doc.eq(state.doc)
608
+ ) {
500
609
  return
501
610
  }
502
611
 
503
612
  this.emit('update', {
504
613
  editor: this,
505
614
  transaction,
615
+ appendedTransactions: transactions.slice(1),
506
616
  })
507
617
  }
508
618
 
@@ -532,7 +642,10 @@ export class Editor extends EventEmitter<EditorEvents> {
532
642
  /**
533
643
  * Get the document as JSON.
534
644
  */
535
- public getJSON(): JSONContent {
645
+ public getJSON(): DocumentType<
646
+ Record<string, any> | undefined,
647
+ TNodeType<string, undefined | Record<string, any>, any, (TNodeType | TTextType)[]>[]
648
+ > {
536
649
  return this.state.doc.toJSON()
537
650
  }
538
651
 
@@ -546,10 +659,7 @@ export class Editor extends EventEmitter<EditorEvents> {
546
659
  /**
547
660
  * Get the document as text.
548
661
  */
549
- public getText(options?: {
550
- blockSeparator?: string
551
- textSerializers?: Record<string, TextSerializer>
552
- }): string {
662
+ public getText(options?: { blockSeparator?: string; textSerializers?: Record<string, TextSerializer> }): string {
553
663
  const { blockSeparator = '\n\n', textSerializers = {} } = options || {}
554
664
 
555
665
  return getText(this.state.doc, {
@@ -568,34 +678,21 @@ export class Editor extends EventEmitter<EditorEvents> {
568
678
  return isNodeEmpty(this.state.doc)
569
679
  }
570
680
 
571
- /**
572
- * Get the number of characters for the current document.
573
- *
574
- * @deprecated
575
- */
576
- public getCharacterCount(): number {
577
- console.warn(
578
- '[tiptap warn]: "editor.getCharacterCount()" is deprecated. Please use "editor.storage.characterCount.characters()" instead.',
579
- )
580
-
581
- return this.state.doc.content.size - 2
582
- }
583
-
584
681
  /**
585
682
  * Destroy the editor.
586
683
  */
587
684
  public destroy(): void {
588
685
  this.emit('destroy')
589
686
 
590
- if (this.view) {
687
+ if (this.editorView) {
591
688
  // Cleanup our reference to prevent circular references which caused memory leaks
592
689
  // @ts-ignore
593
- const dom = this.view.dom as TiptapEditorHTMLElement
690
+ const dom = this.editorView.dom as TiptapEditorHTMLElement
594
691
 
595
692
  if (dom && dom.editor) {
596
693
  delete dom.editor
597
694
  }
598
- this.view.destroy()
695
+ this.editorView.destroy()
599
696
  }
600
697
 
601
698
  this.removeAllListeners()
@@ -1,16 +1,13 @@
1
1
  type StringKeyOf<T> = Extract<keyof T, string>
2
- type CallbackType<
3
- T extends Record<string, any>,
4
- EventName extends StringKeyOf<T>,
5
- > = T[EventName] extends any[] ? T[EventName] : [T[EventName]]
6
- type CallbackFunction<
7
- T extends Record<string, any>,
8
- EventName extends StringKeyOf<T>,
9
- > = (...props: CallbackType<T, EventName>) => any
2
+ type CallbackType<T extends Record<string, any>, EventName extends StringKeyOf<T>> = T[EventName] extends any[]
3
+ ? T[EventName]
4
+ : [T[EventName]]
5
+ type CallbackFunction<T extends Record<string, any>, EventName extends StringKeyOf<T>> = (
6
+ ...props: CallbackType<T, EventName>
7
+ ) => any
10
8
 
11
9
  export class EventEmitter<T extends Record<string, any>> {
12
-
13
- private callbacks: { [key: string]: Function[] } = {}
10
+ private callbacks: { [key: string]: Array<(...args: any[]) => void> } = {}
14
11
 
15
12
  public on<EventName extends StringKeyOf<T>>(event: EventName, fn: CallbackFunction<T, EventName>): this {
16
13
  if (!this.callbacks[event]) {