@tiptap/core 2.10.4 → 2.11.0

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/core",
3
3
  "description": "headless rich text editor",
4
- "version": "2.10.4",
4
+ "version": "2.11.0",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -32,7 +32,7 @@
32
32
  "dist"
33
33
  ],
34
34
  "devDependencies": {
35
- "@tiptap/pm": "^2.10.4"
35
+ "@tiptap/pm": "^2.11.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@tiptap/pm": "^2.7.0"
package/src/NodeView.ts CHANGED
@@ -151,11 +151,11 @@ export class NodeView<
151
151
  // ProseMirror tries to drag selectable nodes
152
152
  // even if `draggable` is set to `false`
153
153
  // this fix prevents that
154
- if (!isDraggable && isSelectable && isDragEvent) {
154
+ if (!isDraggable && isSelectable && isDragEvent && event.target === this.dom) {
155
155
  event.preventDefault()
156
156
  }
157
157
 
158
- if (isDraggable && isDragEvent && !isDragging) {
158
+ if (isDraggable && isDragEvent && !isDragging && event.target === this.dom) {
159
159
  event.preventDefault()
160
160
  return false
161
161
  }
package/src/PasteRule.ts CHANGED
@@ -162,6 +162,9 @@ function run(config: {
162
162
  return success
163
163
  }
164
164
 
165
+ // When dragging across editors, must get another editor instance to delete selection content.
166
+ let tiptapDragFromOtherEditor: Editor | null = null
167
+
165
168
  const createClipboardPasteEvent = (text: string) => {
166
169
  const event = new ClipboardEvent('paste', {
167
170
  clipboardData: new DataTransfer(),
@@ -242,13 +245,25 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
242
245
  dragSourceElement = view.dom.parentElement?.contains(event.target as Element)
243
246
  ? view.dom.parentElement
244
247
  : null
248
+
249
+ if (dragSourceElement) {
250
+ tiptapDragFromOtherEditor = editor
251
+ }
252
+ }
253
+
254
+ const handleDragend = () => {
255
+ if (tiptapDragFromOtherEditor) {
256
+ tiptapDragFromOtherEditor = null
257
+ }
245
258
  }
246
259
 
247
260
  window.addEventListener('dragstart', handleDragstart)
261
+ window.addEventListener('dragend', handleDragend)
248
262
 
249
263
  return {
250
264
  destroy() {
251
265
  window.removeEventListener('dragstart', handleDragstart)
266
+ window.removeEventListener('dragend', handleDragend)
252
267
  },
253
268
  }
254
269
  },
@@ -259,6 +274,20 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
259
274
  isDroppedFromProseMirror = dragSourceElement === view.dom.parentElement
260
275
  dropEvent = event as DragEvent
261
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
+ }
262
291
  return false
263
292
  },
264
293
 
@@ -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,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
  }
@@ -46,5 +46,6 @@ export * from './isNodeSelection.js'
46
46
  export * from './isTextSelection.js'
47
47
  export * from './posToDOMRect.js'
48
48
  export * from './resolveFocusPosition.js'
49
+ export * from './rewriteUnknownContent.js'
49
50
  export * from './selectionToInsertionEnd.js'
50
51
  export * from './splitExtensions.js'
@@ -0,0 +1,148 @@
1
+ import type { Schema } from '@tiptap/pm/model'
2
+
3
+ import type { JSONContent } from '../types.js'
4
+
5
+ type RewriteUnknownContentOptions = {
6
+ /**
7
+ * If true, unknown nodes will be treated as paragraphs
8
+ * @default true
9
+ */
10
+ fallbackToParagraph?: boolean;
11
+ };
12
+
13
+ type RewrittenContent = {
14
+ /**
15
+ * The original JSON content that was rewritten
16
+ */
17
+ original: JSONContent;
18
+ /**
19
+ * The name of the node or mark that was unsupported
20
+ */
21
+ unsupported: string;
22
+ }[];
23
+
24
+ /**
25
+ * The actual implementation of the rewriteUnknownContent function
26
+ */
27
+ function rewriteUnknownContentInner({
28
+ json,
29
+ validMarks,
30
+ validNodes,
31
+ options,
32
+ rewrittenContent = [],
33
+ }: {
34
+ json: JSONContent;
35
+ validMarks: Set<string>;
36
+ validNodes: Set<string>;
37
+ options?: RewriteUnknownContentOptions;
38
+ rewrittenContent?: RewrittenContent;
39
+ }): {
40
+ /**
41
+ * The cleaned JSON content
42
+ */
43
+ json: JSONContent | null;
44
+ /**
45
+ * The array of nodes and marks that were rewritten
46
+ */
47
+ rewrittenContent: RewrittenContent;
48
+ } {
49
+ if (json.marks && Array.isArray(json.marks)) {
50
+ json.marks = json.marks.filter(mark => {
51
+ const name = typeof mark === 'string' ? mark : mark.type
52
+
53
+ if (validMarks.has(name)) {
54
+ return true
55
+ }
56
+
57
+ rewrittenContent.push({
58
+ original: JSON.parse(JSON.stringify(mark)),
59
+ unsupported: name,
60
+ })
61
+ // Just ignore any unknown marks
62
+ return false
63
+ })
64
+ }
65
+
66
+ if (json.content && Array.isArray(json.content)) {
67
+ json.content = json.content
68
+ .map(
69
+ value => rewriteUnknownContentInner({
70
+ json: value,
71
+ validMarks,
72
+ validNodes,
73
+ options,
74
+ rewrittenContent,
75
+ }).json,
76
+ )
77
+ .filter(a => a !== null && a !== undefined)
78
+ }
79
+
80
+ if (json.type && !validNodes.has(json.type)) {
81
+ rewrittenContent.push({
82
+ original: JSON.parse(JSON.stringify(json)),
83
+ unsupported: json.type,
84
+ })
85
+
86
+ if (json.content && Array.isArray(json.content) && (options?.fallbackToParagraph !== false)) {
87
+ // Just treat it like a paragraph and hope for the best
88
+ json.type = 'paragraph'
89
+
90
+ return {
91
+ json,
92
+ rewrittenContent,
93
+ }
94
+ }
95
+
96
+ // or just omit it entirely
97
+ return {
98
+ json: null,
99
+ rewrittenContent,
100
+ }
101
+ }
102
+
103
+ return { json, rewrittenContent }
104
+ }
105
+
106
+ /**
107
+ * Rewrite unknown nodes and marks within JSON content
108
+ * Allowing for user within the editor
109
+ */
110
+ export function rewriteUnknownContent(
111
+ /**
112
+ * The JSON content to clean of unknown nodes and marks
113
+ */
114
+ json: JSONContent,
115
+ /**
116
+ * The schema to use for validation
117
+ */
118
+ schema: Schema,
119
+ /**
120
+ * Options for the cleaning process
121
+ */
122
+ options?: RewriteUnknownContentOptions,
123
+ ): {
124
+ /**
125
+ * The cleaned JSON content
126
+ */
127
+ json: JSONContent | null;
128
+ /**
129
+ * The array of nodes and marks that were rewritten
130
+ */
131
+ rewrittenContent: {
132
+ /**
133
+ * The original JSON content that was rewritten
134
+ */
135
+ original: JSONContent;
136
+ /**
137
+ * The name of the node or mark that was unsupported
138
+ */
139
+ unsupported: string;
140
+ }[];
141
+ } {
142
+ return rewriteUnknownContentInner({
143
+ json,
144
+ validNodes: new Set(Object.keys(schema.nodes)),
145
+ validMarks: new Set(Object.keys(schema.marks)),
146
+ options,
147
+ })
148
+ }