@tiptap/core 2.2.6 → 2.3.1
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/dist/index.cjs +110 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +110 -32
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +110 -32
- package/dist/index.umd.js.map +1 -1
- package/dist/packages/core/src/Editor.d.ts +1 -2
- package/dist/packages/core/src/commands/insertContent.d.ts +2 -0
- package/dist/packages/core/src/commands/insertContentAt.d.ts +2 -0
- package/dist/packages/core/src/extensions/clipboardTextSerializer.d.ts +4 -1
- package/dist/packages/core/src/types.d.ts +5 -0
- package/package.json +2 -2
- package/src/Editor.ts +16 -3
- package/src/InputRule.ts +20 -0
- package/src/NodePos.ts +2 -2
- package/src/PasteRule.ts +76 -27
- package/src/commands/insertContent.ts +2 -0
- package/src/commands/insertContentAt.ts +22 -4
- package/src/extensions/clipboardTextSerializer.ts +14 -1
- package/src/types.ts +5 -0
|
@@ -3,10 +3,9 @@ import { EditorState, Plugin, PluginKey, Transaction } from '@tiptap/pm/state';
|
|
|
3
3
|
import { EditorView } from '@tiptap/pm/view';
|
|
4
4
|
import { EventEmitter } from './EventEmitter.js';
|
|
5
5
|
import { ExtensionManager } from './ExtensionManager.js';
|
|
6
|
-
import * as extensions from './extensions/index.js';
|
|
7
6
|
import { NodePos } from './NodePos.js';
|
|
8
7
|
import { CanCommands, ChainedCommands, EditorEvents, EditorOptions, JSONContent, SingleCommands, TextSerializer } from './types.js';
|
|
9
|
-
export
|
|
8
|
+
export * as extensions from './extensions/index.js';
|
|
10
9
|
export interface HTMLElement {
|
|
11
10
|
editor?: Editor;
|
|
12
11
|
}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import { Extension } from '../Extension.js';
|
|
2
|
-
export declare
|
|
2
|
+
export declare type ClipboardTextSerializerOptions = {
|
|
3
|
+
blockSeparator?: string;
|
|
4
|
+
};
|
|
5
|
+
export declare const ClipboardTextSerializer: Extension<ClipboardTextSerializerOptions, any>;
|
|
@@ -58,6 +58,11 @@ export interface EditorOptions {
|
|
|
58
58
|
editable: boolean;
|
|
59
59
|
editorProps: EditorProps;
|
|
60
60
|
parseOptions: ParseOptions;
|
|
61
|
+
coreExtensionOptions?: {
|
|
62
|
+
clipboardTextSerializer?: {
|
|
63
|
+
blockSeparator?: string;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
61
66
|
enableInputRules: EnableRules;
|
|
62
67
|
enablePasteRules: EnableRules;
|
|
63
68
|
enableCoreExtensions: boolean;
|
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.
|
|
4
|
+
"version": "2.3.1",
|
|
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.
|
|
35
|
+
"@tiptap/pm": "^2.3.1"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@tiptap/pm": "^2.0.0"
|
package/src/Editor.ts
CHANGED
|
@@ -9,7 +9,9 @@ import { EditorView } from '@tiptap/pm/view'
|
|
|
9
9
|
import { CommandManager } from './CommandManager.js'
|
|
10
10
|
import { EventEmitter } from './EventEmitter.js'
|
|
11
11
|
import { ExtensionManager } from './ExtensionManager.js'
|
|
12
|
-
import
|
|
12
|
+
import {
|
|
13
|
+
ClipboardTextSerializer, Commands, Editable, FocusEvents, Keymap, Tabindex,
|
|
14
|
+
} from './extensions/index.js'
|
|
13
15
|
import { createDocument } from './helpers/createDocument.js'
|
|
14
16
|
import { getAttributes } from './helpers/getAttributes.js'
|
|
15
17
|
import { getHTMLFromFragment } from './helpers/getHTMLFromFragment.js'
|
|
@@ -32,7 +34,7 @@ import {
|
|
|
32
34
|
import { createStyleTag } from './utilities/createStyleTag.js'
|
|
33
35
|
import { isFunction } from './utilities/isFunction.js'
|
|
34
36
|
|
|
35
|
-
export
|
|
37
|
+
export * as extensions from './extensions/index.js'
|
|
36
38
|
|
|
37
39
|
export interface HTMLElement {
|
|
38
40
|
editor?: Editor
|
|
@@ -63,6 +65,7 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|
|
63
65
|
editable: true,
|
|
64
66
|
editorProps: {},
|
|
65
67
|
parseOptions: {},
|
|
68
|
+
coreExtensionOptions: {},
|
|
66
69
|
enableInputRules: true,
|
|
67
70
|
enablePasteRules: true,
|
|
68
71
|
enableCoreExtensions: true,
|
|
@@ -235,7 +238,17 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|
|
235
238
|
* Creates an extension manager.
|
|
236
239
|
*/
|
|
237
240
|
private createExtensionManager(): void {
|
|
238
|
-
|
|
241
|
+
|
|
242
|
+
const coreExtensions = this.options.enableCoreExtensions ? [
|
|
243
|
+
Editable,
|
|
244
|
+
ClipboardTextSerializer.configure({
|
|
245
|
+
blockSeparator: this.options.coreExtensionOptions?.clipboardTextSerializer?.blockSeparator,
|
|
246
|
+
}),
|
|
247
|
+
Commands,
|
|
248
|
+
FocusEvents,
|
|
249
|
+
Keymap,
|
|
250
|
+
Tabindex,
|
|
251
|
+
] : []
|
|
239
252
|
const allExtensions = [...coreExtensions, ...this.options.extensions].filter(extension => {
|
|
240
253
|
return ['extension', 'node', 'mark'].includes(extension?.type)
|
|
241
254
|
})
|
package/src/InputRule.ts
CHANGED
|
@@ -191,6 +191,26 @@ export function inputRulesPlugin(props: { editor: Editor; rules: InputRule[] }):
|
|
|
191
191
|
return stored
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// if InputRule is triggered by insertContent()
|
|
195
|
+
const simulatedInputMeta = tr.getMeta('applyInputRules')
|
|
196
|
+
const isSimulatedInput = !!simulatedInputMeta
|
|
197
|
+
|
|
198
|
+
if (isSimulatedInput) {
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
const { from, text } = simulatedInputMeta
|
|
201
|
+
const to = from + text.length
|
|
202
|
+
|
|
203
|
+
run({
|
|
204
|
+
editor,
|
|
205
|
+
from,
|
|
206
|
+
to,
|
|
207
|
+
text,
|
|
208
|
+
rules,
|
|
209
|
+
plugin,
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
194
214
|
return tr.selectionSet || tr.docChanged ? null : prev
|
|
195
215
|
},
|
|
196
216
|
},
|
package/src/NodePos.ts
CHANGED
|
@@ -136,7 +136,7 @@ export class NodePos {
|
|
|
136
136
|
this.node.content.forEach((node, offset) => {
|
|
137
137
|
const isBlock = node.isBlock && !node.isTextblock
|
|
138
138
|
|
|
139
|
-
const targetPos = this.pos + offset +
|
|
139
|
+
const targetPos = this.pos + offset + 1
|
|
140
140
|
const $pos = this.resolvedPos.doc.resolve(targetPos)
|
|
141
141
|
|
|
142
142
|
if (!isBlock && $pos.depth <= this.depth) {
|
|
@@ -201,7 +201,7 @@ export class NodePos {
|
|
|
201
201
|
let nodes: NodePos[] = []
|
|
202
202
|
|
|
203
203
|
// iterate through children recursively finding all nodes which match the selector with the node name
|
|
204
|
-
if (
|
|
204
|
+
if (!this.children || this.children.length === 0) {
|
|
205
205
|
return nodes
|
|
206
206
|
}
|
|
207
207
|
|
package/src/PasteRule.ts
CHANGED
|
@@ -154,6 +154,16 @@ function run(config: {
|
|
|
154
154
|
return success
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
const createClipboardPasteEvent = (text: string) => {
|
|
158
|
+
const event = new ClipboardEvent('paste', {
|
|
159
|
+
clipboardData: new DataTransfer(),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
event.clipboardData?.setData('text/html', text)
|
|
163
|
+
|
|
164
|
+
return event
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
/**
|
|
158
168
|
* Create an paste rules plugin. When enabled, it will cause pasted
|
|
159
169
|
* text that matches any of the given rules to trigger the rule’s
|
|
@@ -167,6 +177,45 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
|
|
|
167
177
|
let pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null
|
|
168
178
|
let dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
|
|
169
179
|
|
|
180
|
+
const processEvent = ({
|
|
181
|
+
state,
|
|
182
|
+
from,
|
|
183
|
+
to,
|
|
184
|
+
rule,
|
|
185
|
+
pasteEvt,
|
|
186
|
+
}: {
|
|
187
|
+
state: EditorState
|
|
188
|
+
from: number
|
|
189
|
+
to: { b: number }
|
|
190
|
+
rule: PasteRule
|
|
191
|
+
pasteEvt: ClipboardEvent | null
|
|
192
|
+
}) => {
|
|
193
|
+
const tr = state.tr
|
|
194
|
+
const chainableState = createChainableState({
|
|
195
|
+
state,
|
|
196
|
+
transaction: tr,
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const handler = run({
|
|
200
|
+
editor,
|
|
201
|
+
state: chainableState,
|
|
202
|
+
from: Math.max(from - 1, 0),
|
|
203
|
+
to: to.b - 1,
|
|
204
|
+
rule,
|
|
205
|
+
pasteEvent: pasteEvt,
|
|
206
|
+
dropEvent,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
if (!handler || !tr.steps.length) {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
|
|
214
|
+
pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null
|
|
215
|
+
|
|
216
|
+
return tr
|
|
217
|
+
}
|
|
218
|
+
|
|
170
219
|
const plugins = rules.map(rule => {
|
|
171
220
|
return new Plugin({
|
|
172
221
|
// we register a global drag handler to track the current drag source element
|
|
@@ -212,45 +261,45 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
|
|
|
212
261
|
const isPaste = transaction.getMeta('uiEvent') === 'paste' && !isPastedFromProseMirror
|
|
213
262
|
const isDrop = transaction.getMeta('uiEvent') === 'drop' && !isDroppedFromProseMirror
|
|
214
263
|
|
|
215
|
-
if
|
|
264
|
+
// if PasteRule is triggered by insertContent()
|
|
265
|
+
const simulatedPasteMeta = transaction.getMeta('applyPasteRules')
|
|
266
|
+
const isSimulatedPaste = !!simulatedPasteMeta
|
|
267
|
+
|
|
268
|
+
if (!isPaste && !isDrop && !isSimulatedPaste) {
|
|
216
269
|
return
|
|
217
270
|
}
|
|
218
271
|
|
|
219
|
-
//
|
|
272
|
+
// Handle simulated paste
|
|
273
|
+
if (isSimulatedPaste) {
|
|
274
|
+
const { from, text } = simulatedPasteMeta
|
|
275
|
+
const to = from + text.length
|
|
276
|
+
const pasteEvt = createClipboardPasteEvent(text)
|
|
277
|
+
|
|
278
|
+
return processEvent({
|
|
279
|
+
rule,
|
|
280
|
+
state,
|
|
281
|
+
from,
|
|
282
|
+
to: { b: to },
|
|
283
|
+
pasteEvt,
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// handle actual paste/drop
|
|
220
288
|
const from = oldState.doc.content.findDiffStart(state.doc.content)
|
|
221
289
|
const to = oldState.doc.content.findDiffEnd(state.doc.content)
|
|
222
290
|
|
|
291
|
+
// stop if there is no changed range
|
|
223
292
|
if (!isNumber(from) || !to || from === to.b) {
|
|
224
293
|
return
|
|
225
294
|
}
|
|
226
295
|
|
|
227
|
-
|
|
228
|
-
// so we can use a single transaction for all paste rules
|
|
229
|
-
const tr = state.tr
|
|
230
|
-
const chainableState = createChainableState({
|
|
231
|
-
state,
|
|
232
|
-
transaction: tr,
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
const handler = run({
|
|
236
|
-
editor,
|
|
237
|
-
state: chainableState,
|
|
238
|
-
from: Math.max(from - 1, 0),
|
|
239
|
-
to: to.b - 1,
|
|
296
|
+
return processEvent({
|
|
240
297
|
rule,
|
|
241
|
-
|
|
242
|
-
|
|
298
|
+
state,
|
|
299
|
+
from,
|
|
300
|
+
to,
|
|
301
|
+
pasteEvt: pasteEvent,
|
|
243
302
|
})
|
|
244
|
-
|
|
245
|
-
// stop if there are no changes
|
|
246
|
-
if (!handler || !tr.steps.length) {
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
|
|
251
|
-
pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null
|
|
252
|
-
|
|
253
|
-
return tr
|
|
254
303
|
},
|
|
255
304
|
})
|
|
256
305
|
})
|
|
@@ -16,6 +16,8 @@ declare module '@tiptap/core' {
|
|
|
16
16
|
options?: {
|
|
17
17
|
parseOptions?: ParseOptions
|
|
18
18
|
updateSelection?: boolean
|
|
19
|
+
applyInputRules?: boolean
|
|
20
|
+
applyPasteRules?: boolean
|
|
19
21
|
},
|
|
20
22
|
) => ReturnType
|
|
21
23
|
}
|
|
@@ -31,6 +33,8 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
|
|
|
31
33
|
options = {
|
|
32
34
|
parseOptions: {},
|
|
33
35
|
updateSelection: true,
|
|
36
|
+
applyInputRules: false,
|
|
37
|
+
applyPasteRules: false,
|
|
34
38
|
...options,
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -76,26 +80,40 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
|
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
let newContent
|
|
84
|
+
|
|
79
85
|
// if there is only plain text we have to use `insertText`
|
|
80
86
|
// because this will keep the current marks
|
|
81
87
|
if (isOnlyTextContent) {
|
|
82
88
|
// if value is string, we can use it directly
|
|
83
89
|
// otherwise if it is an array, we have to join it
|
|
84
90
|
if (Array.isArray(value)) {
|
|
85
|
-
|
|
91
|
+
newContent = value.map(v => v.text || '').join('')
|
|
86
92
|
} else if (typeof value === 'object' && !!value && !!value.text) {
|
|
87
|
-
|
|
93
|
+
newContent = value.text
|
|
88
94
|
} else {
|
|
89
|
-
|
|
95
|
+
newContent = value as string
|
|
90
96
|
}
|
|
97
|
+
|
|
98
|
+
tr.insertText(newContent, from, to)
|
|
91
99
|
} else {
|
|
92
|
-
|
|
100
|
+
newContent = content
|
|
101
|
+
|
|
102
|
+
tr.replaceWith(from, to, newContent)
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
// set cursor at end of inserted content
|
|
96
106
|
if (options.updateSelection) {
|
|
97
107
|
selectionToInsertionEnd(tr, tr.steps.length - 1, -1)
|
|
98
108
|
}
|
|
109
|
+
|
|
110
|
+
if (options.applyInputRules) {
|
|
111
|
+
tr.setMeta('applyInputRules', { from, text: newContent })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (options.applyPasteRules) {
|
|
115
|
+
tr.setMeta('applyPasteRules', { from, text: newContent })
|
|
116
|
+
}
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
return true
|
|
@@ -4,9 +4,19 @@ import { Extension } from '../Extension.js'
|
|
|
4
4
|
import { getTextBetween } from '../helpers/getTextBetween.js'
|
|
5
5
|
import { getTextSerializersFromSchema } from '../helpers/getTextSerializersFromSchema.js'
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export type ClipboardTextSerializerOptions = {
|
|
8
|
+
blockSeparator?: string,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ClipboardTextSerializer = Extension.create<ClipboardTextSerializerOptions>({
|
|
8
12
|
name: 'clipboardTextSerializer',
|
|
9
13
|
|
|
14
|
+
addOptions() {
|
|
15
|
+
return {
|
|
16
|
+
blockSeparator: undefined,
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
10
20
|
addProseMirrorPlugins() {
|
|
11
21
|
return [
|
|
12
22
|
new Plugin({
|
|
@@ -23,6 +33,9 @@ export const ClipboardTextSerializer = Extension.create({
|
|
|
23
33
|
const range = { from, to }
|
|
24
34
|
|
|
25
35
|
return getTextBetween(doc, range, {
|
|
36
|
+
...(this.options.blockSeparator !== undefined
|
|
37
|
+
? { blockSeparator: this.options.blockSeparator }
|
|
38
|
+
: {}),
|
|
26
39
|
textSerializers,
|
|
27
40
|
})
|
|
28
41
|
},
|
package/src/types.ts
CHANGED
|
@@ -59,6 +59,11 @@ export interface EditorOptions {
|
|
|
59
59
|
editable: boolean
|
|
60
60
|
editorProps: EditorProps
|
|
61
61
|
parseOptions: ParseOptions
|
|
62
|
+
coreExtensionOptions?: {
|
|
63
|
+
clipboardTextSerializer?: {
|
|
64
|
+
blockSeparator?: string
|
|
65
|
+
}
|
|
66
|
+
}
|
|
62
67
|
enableInputRules: EnableRules
|
|
63
68
|
enablePasteRules: EnableRules
|
|
64
69
|
enableCoreExtensions: boolean
|