@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/src/PasteRule.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import { Fragment, Node as ProseMirrorNode } from '@tiptap/pm/model'
1
2
  import { EditorState, Plugin } 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 {
7
9
  CanCommands,
8
10
  ChainedCommands,
@@ -14,45 +16,47 @@ import { isNumber } from './utilities/isNumber.js'
14
16
  import { isRegExp } from './utilities/isRegExp.js'
15
17
 
16
18
  export type PasteRuleMatch = {
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 PasteRuleFinder = RegExp | ((text: string, event?: ClipboardEvent | null) => PasteRuleMatch[] | null | undefined)
26
+ export type PasteRuleFinder =
27
+ | RegExp
28
+ | ((text: string, event?: ClipboardEvent | null) => PasteRuleMatch[] | null | undefined);
25
29
 
26
30
  /**
27
31
  * Paste rules are used to react to pasted content.
28
- * @see https://tiptap.dev/guide/custom-extensions/#paste-rules
32
+ * @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#paste-rules
29
33
  */
30
34
  export class PasteRule {
31
35
  find: PasteRuleFinder
32
36
 
33
37
  handler: (props: {
34
- state: EditorState
35
- range: Range
36
- match: ExtendedRegExpMatchArray
37
- commands: SingleCommands
38
- chain: () => ChainedCommands
39
- can: () => CanCommands
40
- pasteEvent: ClipboardEvent | null
41
- dropEvent: DragEvent | null
38
+ state: EditorState;
39
+ range: Range;
40
+ match: ExtendedRegExpMatchArray;
41
+ commands: SingleCommands;
42
+ chain: () => ChainedCommands;
43
+ can: () => CanCommands;
44
+ pasteEvent: ClipboardEvent | null;
45
+ dropEvent: DragEvent | null;
42
46
  }) => void | null
43
47
 
44
48
  constructor(config: {
45
- find: PasteRuleFinder
49
+ find: PasteRuleFinder;
46
50
  handler: (props: {
47
- can: () => CanCommands
48
- chain: () => ChainedCommands
49
- commands: SingleCommands
50
- dropEvent: DragEvent | null
51
- match: ExtendedRegExpMatchArray
52
- pasteEvent: ClipboardEvent | null
53
- range: Range
54
- state: EditorState
55
- }) => void | null
51
+ can: () => CanCommands;
52
+ chain: () => ChainedCommands;
53
+ commands: SingleCommands;
54
+ dropEvent: DragEvent | null;
55
+ match: ExtendedRegExpMatchArray;
56
+ pasteEvent: ClipboardEvent | null;
57
+ range: Range;
58
+ state: EditorState;
59
+ }) => void | null;
56
60
  }) {
57
61
  this.find = config.find
58
62
  this.handler = config.handler
@@ -96,13 +100,13 @@ const pasteRuleMatcherHandler = (
96
100
  }
97
101
 
98
102
  function run(config: {
99
- editor: Editor
100
- state: EditorState
101
- from: number
102
- to: number
103
- rule: PasteRule
104
- pasteEvent: ClipboardEvent | null
105
- dropEvent: DragEvent | null
103
+ editor: Editor;
104
+ state: EditorState;
105
+ from: number;
106
+ to: number;
107
+ rule: PasteRule;
108
+ pasteEvent: ClipboardEvent | null;
109
+ dropEvent: DragEvent | null;
106
110
  }): boolean {
107
111
  const {
108
112
  editor, state, from, to, rule, pasteEvent, dropEvent,
@@ -158,6 +162,9 @@ function run(config: {
158
162
  return success
159
163
  }
160
164
 
165
+ // When dragging across editors, must get another editor instance to delete selection content.
166
+ let tiptapDragFromOtherEditor: Editor | null = null
167
+
161
168
  const createClipboardPasteEvent = (text: string) => {
162
169
  const event = new ClipboardEvent('paste', {
163
170
  clipboardData: new DataTransfer(),
@@ -179,7 +186,13 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
179
186
  let isPastedFromProseMirror = false
180
187
  let isDroppedFromProseMirror = false
181
188
  let pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null
182
- let dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
189
+ let dropEvent: DragEvent | null
190
+
191
+ try {
192
+ dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
193
+ } catch (e) {
194
+ dropEvent = null
195
+ }
183
196
 
184
197
  const processEvent = ({
185
198
  state,
@@ -188,11 +201,11 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
188
201
  rule,
189
202
  pasteEvt,
190
203
  }: {
191
- state: EditorState
192
- from: number
193
- to: { b: number }
194
- rule: PasteRule
195
- pasteEvt: ClipboardEvent | null
204
+ state: EditorState;
205
+ from: number;
206
+ to: { b: number };
207
+ rule: PasteRule;
208
+ pasteEvt: ClipboardEvent | null;
196
209
  }) => {
197
210
  const tr = state.tr
198
211
  const chainableState = createChainableState({
@@ -214,7 +227,11 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
214
227
  return
215
228
  }
216
229
 
217
- dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
230
+ try {
231
+ dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
232
+ } catch (e) {
233
+ dropEvent = null
234
+ }
218
235
  pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null
219
236
 
220
237
  return tr
@@ -228,13 +245,25 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
228
245
  dragSourceElement = view.dom.parentElement?.contains(event.target as Element)
229
246
  ? view.dom.parentElement
230
247
  : null
248
+
249
+ if (dragSourceElement) {
250
+ tiptapDragFromOtherEditor = editor
251
+ }
252
+ }
253
+
254
+ const handleDragend = () => {
255
+ if (tiptapDragFromOtherEditor) {
256
+ tiptapDragFromOtherEditor = null
257
+ }
231
258
  }
232
259
 
233
260
  window.addEventListener('dragstart', handleDragstart)
261
+ window.addEventListener('dragend', handleDragend)
234
262
 
235
263
  return {
236
264
  destroy() {
237
265
  window.removeEventListener('dragstart', handleDragstart)
266
+ window.removeEventListener('dragend', handleDragend)
238
267
  },
239
268
  }
240
269
  },
@@ -245,6 +274,20 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
245
274
  isDroppedFromProseMirror = dragSourceElement === view.dom.parentElement
246
275
  dropEvent = event as DragEvent
247
276
 
277
+ if (!isDroppedFromProseMirror) {
278
+ const dragFromOtherEditor = tiptapDragFromOtherEditor
279
+
280
+ if (dragFromOtherEditor) {
281
+ // setTimeout to avoid the wrong content after drop, timeout arg can't be empty or 0
282
+ setTimeout(() => {
283
+ const selection = dragFromOtherEditor.state.selection
284
+
285
+ if (selection) {
286
+ dragFromOtherEditor.commands.deleteRange({ from: selection.from, to: selection.to })
287
+ }
288
+ }, 10)
289
+ }
290
+ }
248
291
  return false
249
292
  },
250
293
 
@@ -266,7 +309,9 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
266
309
  const isDrop = transaction.getMeta('uiEvent') === 'drop' && !isDroppedFromProseMirror
267
310
 
268
311
  // if PasteRule is triggered by insertContent()
269
- const simulatedPasteMeta = transaction.getMeta('applyPasteRules')
312
+ const simulatedPasteMeta = transaction.getMeta('applyPasteRules') as
313
+ | undefined
314
+ | { from: number; text: string | ProseMirrorNode | Fragment }
270
315
  const isSimulatedPaste = !!simulatedPasteMeta
271
316
 
272
317
  if (!isPaste && !isDrop && !isSimulatedPaste) {
@@ -275,8 +320,17 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
275
320
 
276
321
  // Handle simulated paste
277
322
  if (isSimulatedPaste) {
278
- const { from, text } = simulatedPasteMeta
323
+ let { text } = simulatedPasteMeta
324
+
325
+ if (typeof text === 'string') {
326
+ text = text as string
327
+ } else {
328
+ text = getHTMLFromFragment(Fragment.from(text), state.schema)
329
+ }
330
+
331
+ const { from } = simulatedPasteMeta
279
332
  const to = from + text.length
333
+
280
334
  const pasteEvt = createClipboardPasteEvent(text)
281
335
 
282
336
  return processEvent({
@@ -1,7 +1,6 @@
1
1
  import { isTextSelection } from '../helpers/isTextSelection.js'
2
2
  import { resolveFocusPosition } from '../helpers/resolveFocusPosition.js'
3
3
  import { FocusPosition, RawCommands } from '../types.js'
4
- import { isiOS } from '../utilities/isiOS.js'
5
4
 
6
5
  declare module '@tiptap/core' {
7
6
  interface Commands<ReturnType> {
@@ -43,11 +42,7 @@ export const focus: RawCommands['focus'] = (position = null, options = {}) => ({
43
42
  }
44
43
 
45
44
  const delayedFocus = () => {
46
- // focus within `requestAnimationFrame` breaks focus on iOS
47
- // so we have to call this
48
- if (isiOS()) {
49
- (view.dom as HTMLElement).focus()
50
- }
45
+ (view.dom as HTMLElement).focus()
51
46
 
52
47
  // For React we have to focus asynchronously. Otherwise wild things happen.
53
48
  // see: https://github.com/ueberdosis/tiptap/issues/1520
@@ -1,4 +1,4 @@
1
- import { ParseOptions } from '@tiptap/pm/model'
1
+ import { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm/model'
2
2
 
3
3
  import { Content, RawCommands } from '../types.js'
4
4
 
@@ -14,7 +14,7 @@ declare module '@tiptap/core' {
14
14
  /**
15
15
  * The ProseMirror content to insert.
16
16
  */
17
- value: Content,
17
+ value: Content | ProseMirrorNode | Fragment,
18
18
 
19
19
  /**
20
20
  * Optional options
@@ -23,17 +23,17 @@ declare module '@tiptap/core' {
23
23
  /**
24
24
  * Options for parsing the content.
25
25
  */
26
- parseOptions?: ParseOptions
26
+ parseOptions?: ParseOptions;
27
27
 
28
28
  /**
29
29
  * Whether to update the selection after inserting the content.
30
30
  */
31
- updateSelection?: boolean
32
- applyInputRules?: boolean
33
- applyPasteRules?: boolean
34
- },
35
- ) => ReturnType
36
- }
31
+ updateSelection?: boolean;
32
+ applyInputRules?: boolean;
33
+ applyPasteRules?: boolean;
34
+ }
35
+ ) => ReturnType;
36
+ };
37
37
  }
38
38
  }
39
39
 
@@ -20,7 +20,7 @@ declare module '@tiptap/core' {
20
20
  /**
21
21
  * The ProseMirror content to insert.
22
22
  */
23
- value: Content,
23
+ value: Content | ProseMirrorNode | Fragment,
24
24
 
25
25
  /**
26
26
  * Optional options
@@ -63,7 +63,7 @@ const isFragment = (nodeOrFragment: ProseMirrorNode | Fragment): nodeOrFragment
63
63
  export const insertContentAt: RawCommands['insertContentAt'] = (position, value, options) => ({ tr, dispatch, editor }) => {
64
64
  if (dispatch) {
65
65
  options = {
66
- parseOptions: {},
66
+ parseOptions: editor.options.parseOptions,
67
67
  updateSelection: true,
68
68
  applyInputRules: false,
69
69
  applyPasteRules: false,
@@ -71,6 +71,7 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
71
71
  }
72
72
 
73
73
  let content: Fragment | ProseMirrorNode
74
+ const { selection } = editor.state
74
75
 
75
76
  try {
76
77
  content = createNodeFromContent(value, editor.schema, {
@@ -85,7 +86,9 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
85
86
  editor,
86
87
  error: e as Error,
87
88
  disableCollaboration: () => {
88
- console.error('[tiptap error]: Unable to disable collaboration at this point in time')
89
+ if (editor.storage.collaboration) {
90
+ editor.storage.collaboration.isDisabled = true
91
+ }
89
92
  },
90
93
  })
91
94
  return false
@@ -130,6 +133,16 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
130
133
  // otherwise if it is an array, we have to join it
131
134
  if (Array.isArray(value)) {
132
135
  newContent = value.map(v => v.text || '').join('')
136
+ } else if (value instanceof Fragment) {
137
+ let text = ''
138
+
139
+ value.forEach(node => {
140
+ if (node.text) {
141
+ text += node.text
142
+ }
143
+ })
144
+
145
+ newContent = text
133
146
  } else if (typeof value === 'object' && !!value && !!value.text) {
134
147
  newContent = value.text
135
148
  } else {
@@ -140,6 +153,13 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
140
153
  } else {
141
154
  newContent = content
142
155
 
156
+ const fromSelectionAtStart = selection.$from.parentOffset === 0
157
+ const isTextSelection = selection.$from.node().isText || selection.$from.node().isTextblock
158
+
159
+ if (fromSelectionAtStart && isTextSelection) {
160
+ from = Math.max(0, from - 1)
161
+ }
162
+
143
163
  tr.replaceWith(from, to, newContent)
144
164
  }
145
165
 
@@ -1,3 +1,5 @@
1
+ import { AllSelection } from '@tiptap/pm/state'
2
+
1
3
  import { RawCommands } from '../types.js'
2
4
 
3
5
  declare module '@tiptap/core' {
@@ -12,9 +14,12 @@ declare module '@tiptap/core' {
12
14
  }
13
15
  }
14
16
 
15
- export const selectAll: RawCommands['selectAll'] = () => ({ tr, commands }) => {
16
- return commands.setTextSelection({
17
- from: 0,
18
- to: tr.doc.content.size,
19
- })
17
+ export const selectAll: RawCommands['selectAll'] = () => ({ tr, dispatch }) => {
18
+ if (dispatch) {
19
+ const selection = new AllSelection(tr.doc)
20
+
21
+ tr.setSelection(selection)
22
+ }
23
+
24
+ return true
20
25
  }
@@ -1,4 +1,4 @@
1
- import { ParseOptions } from '@tiptap/pm/model'
1
+ import { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm/model'
2
2
 
3
3
  import { createDocument } from '../helpers/createDocument.js'
4
4
  import { Content, RawCommands } from '../types.js'
@@ -17,7 +17,7 @@ declare module '@tiptap/core' {
17
17
  /**
18
18
  * The new content.
19
19
  */
20
- content: Content,
20
+ content: Content | Fragment | ProseMirrorNode,
21
21
 
22
22
  /**
23
23
  * Whether to emit an update event.
@@ -37,10 +37,10 @@ declare module '@tiptap/core' {
37
37
  /**
38
38
  * Whether to throw an error if the content is invalid.
39
39
  */
40
- errorOnInvalidContent?: boolean
41
- },
42
- ) => ReturnType
43
- }
40
+ errorOnInvalidContent?: boolean;
41
+ }
42
+ ) => ReturnType;
43
+ };
44
44
  }
45
45
  }
46
46
 
@@ -66,12 +66,8 @@ export const setContent: RawCommands['setContent'] = (content, emitUpdate = fals
66
66
  tr.setMeta('preventUpdate', !emitUpdate)
67
67
  }
68
68
 
69
- return commands.insertContentAt(
70
- { from: 0, to: doc.content.size },
71
- content,
72
- {
73
- parseOptions,
74
- errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck,
75
- },
76
- )
69
+ return commands.insertContentAt({ from: 0, to: doc.content.size }, content, {
70
+ parseOptions,
71
+ errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck,
72
+ })
77
73
  }
@@ -21,6 +21,13 @@ declare module '@tiptap/core' {
21
21
  export const setNode: RawCommands['setNode'] = (typeOrName, attributes = {}) => ({ state, dispatch, chain }) => {
22
22
  const type = getNodeType(typeOrName, state.schema)
23
23
 
24
+ let attributesToCopy: Record<string, any> | undefined
25
+
26
+ if (state.selection.$anchor.sameParent(state.selection.$head)) {
27
+ // only copy attributes if the selection is pointing to a node of the same type
28
+ attributesToCopy = state.selection.$anchor.parent.attrs
29
+ }
30
+
24
31
  // TODO: use a fallback like insertContent?
25
32
  if (!type.isTextblock) {
26
33
  console.warn('[tiptap warn]: Currently "setNode()" only supports text block nodes.')
@@ -32,7 +39,7 @@ export const setNode: RawCommands['setNode'] = (typeOrName, attributes = {}) =>
32
39
  chain()
33
40
  // try to convert node to default node if needed
34
41
  .command(({ commands }) => {
35
- const canSetBlock = setBlockType(type, attributes)(state)
42
+ const canSetBlock = setBlockType(type, { ...attributesToCopy, ...attributes })(state)
36
43
 
37
44
  if (canSetBlock) {
38
45
  return true
@@ -41,7 +48,7 @@ export const setNode: RawCommands['setNode'] = (typeOrName, attributes = {}) =>
41
48
  return commands.clearNodes()
42
49
  })
43
50
  .command(({ state: updatedState }) => {
44
- return setBlockType(type, attributes)(updatedState, dispatch)
51
+ return setBlockType(type, { ...attributesToCopy, ...attributes })(updatedState, dispatch)
45
52
  })
46
53
  .run()
47
54
  )
@@ -28,9 +28,18 @@ export const toggleNode: RawCommands['toggleNode'] = (typeOrName, toggleTypeOrNa
28
28
  const toggleType = getNodeType(toggleTypeOrName, state.schema)
29
29
  const isActive = isNodeActive(state, type, attributes)
30
30
 
31
+ let attributesToCopy: Record<string, any> | undefined
32
+
33
+ if (state.selection.$anchor.sameParent(state.selection.$head)) {
34
+ // only copy attributes if the selection is pointing to a node of the same type
35
+ attributesToCopy = state.selection.$anchor.parent.attrs
36
+ }
37
+
31
38
  if (isActive) {
32
- return commands.setNode(toggleType)
39
+ return commands.setNode(toggleType, attributesToCopy)
33
40
  }
34
41
 
35
- return commands.setNode(type, attributes)
42
+ // If the node is not active, we want to set the new node type with the given attributes
43
+ // Copying over the attributes from the current node if the selection is pointing to a node of the same type
44
+ return commands.setNode(type, { ...attributesToCopy, ...attributes })
36
45
  }
@@ -1,4 +1,7 @@
1
- import { MarkType, NodeType } from '@tiptap/pm/model'
1
+ import {
2
+ Mark, MarkType, Node, NodeType,
3
+ } from '@tiptap/pm/model'
4
+ import { SelectionRange } from '@tiptap/pm/state'
2
5
 
3
6
  import { getMarkType } from '../helpers/getMarkType.js'
4
7
  import { getNodeType } from '../helpers/getNodeType.js'
@@ -30,6 +33,7 @@ declare module '@tiptap/core' {
30
33
  }
31
34
 
32
35
  export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, attributes = {}) => ({ tr, state, dispatch }) => {
36
+
33
37
  let nodeType: NodeType | null = null
34
38
  let markType: MarkType | null = null
35
39
 
@@ -51,24 +55,80 @@ export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, at
51
55
  }
52
56
 
53
57
  if (dispatch) {
54
- tr.selection.ranges.forEach(range => {
58
+ tr.selection.ranges.forEach((range: SelectionRange) => {
59
+
55
60
  const from = range.$from.pos
56
61
  const to = range.$to.pos
57
62
 
58
- state.doc.nodesBetween(from, to, (node, pos) => {
59
- if (nodeType && nodeType === node.type) {
60
- tr.setNodeMarkup(pos, undefined, {
61
- ...node.attrs,
63
+ let lastPos: number | undefined
64
+ let lastNode: Node | undefined
65
+ let trimmedFrom: number
66
+ let trimmedTo: number
67
+
68
+ if (tr.selection.empty) {
69
+ state.doc.nodesBetween(from, to, (node: Node, pos: number) => {
70
+
71
+ if (nodeType && nodeType === node.type) {
72
+ trimmedFrom = Math.max(pos, from)
73
+ trimmedTo = Math.min(pos + node.nodeSize, to)
74
+ lastPos = pos
75
+ lastNode = node
76
+ }
77
+ })
78
+ } else {
79
+ state.doc.nodesBetween(from, to, (node: Node, pos: number) => {
80
+
81
+ if (pos < from && nodeType && nodeType === node.type) {
82
+ trimmedFrom = Math.max(pos, from)
83
+ trimmedTo = Math.min(pos + node.nodeSize, to)
84
+ lastPos = pos
85
+ lastNode = node
86
+ }
87
+
88
+ if (pos >= from && pos <= to) {
89
+
90
+ if (nodeType && nodeType === node.type) {
91
+ tr.setNodeMarkup(pos, undefined, {
92
+ ...node.attrs,
93
+ ...attributes,
94
+ })
95
+ }
96
+
97
+ if (markType && node.marks.length) {
98
+ node.marks.forEach((mark: Mark) => {
99
+
100
+ if (markType === mark.type) {
101
+ const trimmedFrom2 = Math.max(pos, from)
102
+ const trimmedTo2 = Math.min(pos + node.nodeSize, to)
103
+
104
+ tr.addMark(
105
+ trimmedFrom2,
106
+ trimmedTo2,
107
+ markType.create({
108
+ ...mark.attrs,
109
+ ...attributes,
110
+ }),
111
+ )
112
+ }
113
+ })
114
+ }
115
+ }
116
+ })
117
+ }
118
+
119
+ if (lastNode) {
120
+
121
+ if (lastPos !== undefined) {
122
+ tr.setNodeMarkup(lastPos, undefined, {
123
+ ...lastNode.attrs,
62
124
  ...attributes,
63
125
  })
64
126
  }
65
127
 
66
- if (markType && node.marks.length) {
67
- node.marks.forEach(mark => {
68
- if (markType === mark.type) {
69
- const trimmedFrom = Math.max(pos, from)
70
- const trimmedTo = Math.min(pos + node.nodeSize, to)
128
+ if (markType && lastNode.marks.length) {
129
+ lastNode.marks.forEach((mark: Mark) => {
71
130
 
131
+ if (markType === mark.type) {
72
132
  tr.addMark(
73
133
  trimmedFrom,
74
134
  trimmedTo,
@@ -80,7 +140,7 @@ export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, at
80
140
  }
81
141
  })
82
142
  }
83
- })
143
+ }
84
144
  })
85
145
  }
86
146
 
@@ -0,0 +1,26 @@
1
+ import { Plugin, PluginKey } from '@tiptap/pm/state'
2
+
3
+ import { Extension } from '../Extension.js'
4
+
5
+ export const Drop = Extension.create({
6
+ name: 'drop',
7
+
8
+ addProseMirrorPlugins() {
9
+ return [
10
+ new Plugin({
11
+ key: new PluginKey('tiptapDrop'),
12
+
13
+ props: {
14
+ handleDrop: (_, e, slice, moved) => {
15
+ this.editor.emit('drop', {
16
+ editor: this.editor,
17
+ event: e,
18
+ slice,
19
+ moved,
20
+ })
21
+ },
22
+ },
23
+ }),
24
+ ]
25
+ },
26
+ })
@@ -1,6 +1,8 @@
1
1
  export { ClipboardTextSerializer } from './clipboardTextSerializer.js'
2
2
  export { Commands } from './commands.js'
3
+ export { Drop } from './drop.js'
3
4
  export { Editable } from './editable.js'
4
5
  export { FocusEvents } from './focusEvents.js'
5
6
  export { Keymap } from './keymap.js'
7
+ export { Paste } from './paste.js'
6
8
  export { Tabindex } from './tabindex.js'
@@ -3,6 +3,7 @@ import { Plugin, PluginKey, Selection } from '@tiptap/pm/state'
3
3
  import { CommandManager } from '../CommandManager.js'
4
4
  import { Extension } from '../Extension.js'
5
5
  import { createChainableState } from '../helpers/createChainableState.js'
6
+ import { isNodeEmpty } from '../helpers/isNodeEmpty.js'
6
7
  import { isiOS } from '../utilities/isiOS.js'
7
8
  import { isMacOS } from '../utilities/isMacOS.js'
8
9
 
@@ -106,7 +107,9 @@ export const Keymap = Extension.create({
106
107
  const docChanges = transactions.some(transaction => transaction.docChanged)
107
108
  && !oldState.doc.eq(newState.doc)
108
109
 
109
- if (!docChanges) {
110
+ const ignoreTr = transactions.some(transaction => transaction.getMeta('preventClearDocument'))
111
+
112
+ if (!docChanges || ignoreTr) {
110
113
  return
111
114
  }
112
115
 
@@ -119,7 +122,7 @@ export const Keymap = Extension.create({
119
122
  return
120
123
  }
121
124
 
122
- const isEmpty = newState.doc.textBetween(0, newState.doc.content.size, ' ', ' ').length === 0
125
+ const isEmpty = isNodeEmpty(newState.doc)
123
126
 
124
127
  if (!isEmpty) {
125
128
  return