-
Notifications
You must be signed in to change notification settings - Fork 63
Expand file tree
/
Copy pathplainTextClipboard.ts
More file actions
72 lines (65 loc) 路 3.13 KB
/
Copy pathplainTextClipboard.ts
File metadata and controls
72 lines (65 loc) 路 3.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import { Extension, getTextBetween, getTextSerializersFromSchema } from '@tiptap/core'
import { Fragment, Slice } from '@tiptap/pm/model'
import { Plugin, PluginKey } from '@tiptap/pm/state'
/**
* Clipboard handling for the plain text editor.
*
* Plain text files are stored as one paragraph per line (see the plain text strategy),
* so copying and pasting has to follow the same rule instead of ProseMirror's defaults.
*/
export function createPlainTextClipboardExtension() {
return Extension.create({
name: 'plainTextClipboard',
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('plainTextClipboard'),
props: {
handleDOMEvents: {
// ProseMirror prefers text/html over text/plain when both are present. Parsing
// the html into the reduced plain text schema strips everything it can't
// represent, e.g. markdown copied from the markdown editor loses its syntax.
// This runs as a DOM event handler because `handlePaste` only receives the
// already parsed html slice, and calling `pasteText` from there would re-enter it.
paste(view, event) {
// DOM event handlers run before ProseMirror's own editable check
if (!view.editable) {
return false
}
const text = event.clipboardData?.getData('text/plain')
if (!text || !view.pasteText(text, event)) {
// no text payload (e.g. html only): fall back to the default paste handling
return false
}
event.preventDefault()
return true
}
},
// Tiptap's core clipboard text serializer separates blocks with a blank line, which
// would double every line break when copying within the plain text editor. Use the
// same helpers with a single line break instead, matching how the file is saved.
// Tiptap orders plugins of custom extensions before its core ones, so this one wins.
clipboardTextSerializer(_slice, view) {
const { doc, selection, schema } = view.state
return getTextBetween(doc, selection, {
blockSeparator: '\n',
textSerializers: getTextSerializersFromSchema(schema)
})
},
// ProseMirror's default parser collapses consecutive line breaks, which would drop
// empty lines. Map every line to its own paragraph instead, keeping empty ones.
// The open slice (1, 1) lets the first and last line merge into the paragraph
// at the cursor, so pasting a single line inserts it inline.
clipboardTextParser(text, _context, _plain, view) {
const { schema } = view.state
const paragraphs = text
.split(/\r\n?|\n/)
.map((line) => schema.nodes.paragraph.create(null, line ? schema.text(line) : null))
return new Slice(Fragment.from(paragraphs), 1, 1)
}
}
})
]
}
})
}