@puredesktop/platform-editor 1.0.0-beta.1 → 2.1.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 +1 -1
- package/src/CollapsePanel.tsx +35 -35
- package/src/DocumentDiffView.tsx +130 -130
- package/src/DocumentEditor.test.ts +118 -118
- package/src/DocumentEditor.tsx +2651 -2631
- package/src/alignedLineDiff.test.ts +52 -52
- package/src/alignedLineDiff.ts +67 -67
- package/src/collectionAssetSrc.ts +2 -2
- package/src/commentUtils.test.ts +350 -350
- package/src/commentUtils.ts +546 -546
- package/src/constants/toolKeys.ts +14 -14
- package/src/contentFormat.test.ts +27 -27
- package/src/contentFormat.ts +18 -18
- package/src/editorExtensions.ts +155 -155
- package/src/extensions/appliedChangeMark.ts +115 -115
- package/src/extensions/autoReviewPrompts.test.ts +529 -529
- package/src/extensions/autoReviewPrompts.ts +1442 -1442
- package/src/extensions/collectionImage.ts +26 -26
- package/src/extensions/collectionImagePaste.ts +215 -215
- package/src/extensions/commentMark.ts +223 -223
- package/src/extensions/documentAssetIdentity.ts +54 -54
- package/src/extensions/figure.ts +92 -92
- package/src/extensions/footnote.ts +186 -186
- package/src/extensions/indexMarker.ts +88 -88
- package/src/extensions/mathEditing.ts +239 -239
- package/src/extensions/mermaidBlock.tsx +297 -297
- package/src/extensions/slashCommands.test.ts +170 -170
- package/src/extensions/slashCommands.ts +746 -746
- package/src/extensions/smartTypography.test.ts +74 -74
- package/src/extensions/smartTypography.ts +120 -120
- package/src/index.ts +138 -137
- package/src/insertCollectionImage.test.ts +60 -60
- package/src/insertCollectionImage.ts +63 -63
- package/src/insertInlineAssetKind.test.ts +67 -67
- package/src/insertInlineAssetKind.ts +49 -49
- package/src/mermaidPreview.test.ts +54 -54
- package/src/mermaidPreview.ts +115 -115
- package/src/styled.ts +697 -676
- package/src/toolbar/ToolBtn.tsx +77 -63
- package/src/toolbar/Toolbar.test.tsx +325 -325
- package/src/toolbar/Toolbar.tsx +662 -624
- package/src/toolbar/groups/HeadingGroup.tsx +175 -153
- package/src/toolbar/overlayTypes.ts +28 -28
- package/src/useEditorExtensions.ts +22 -22
- package/src/utils/markdownUtils.ts +331 -331
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import Image from '@tiptap/extension-image'
|
|
2
|
-
import { COLLECTION_ASSET_SRC_ATTR } from '../collectionAssetSrc.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Collection document images: data URLs in the editor, relative `assets/…` paths on save
|
|
6
|
-
* via {@link COLLECTION_ASSET_SRC_ATTR} (see platform-ui `normalizeCollectionDocumentHtml`).
|
|
7
|
-
*/
|
|
8
|
-
export const CollectionImage = Image.extend({
|
|
9
|
-
addAttributes() {
|
|
10
|
-
return {
|
|
11
|
-
...this.parent?.(),
|
|
12
|
-
collectionAssetSrc: {
|
|
13
|
-
default: null,
|
|
14
|
-
parseHTML: element => element.getAttribute(COLLECTION_ASSET_SRC_ATTR),
|
|
15
|
-
renderHTML: attributes => {
|
|
16
|
-
const stored = attributes.collectionAssetSrc
|
|
17
|
-
if (!stored || typeof stored !== 'string') return {}
|
|
18
|
-
return { [COLLECTION_ASSET_SRC_ATTR]: stored }
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
}).configure({
|
|
24
|
-
inline: false,
|
|
25
|
-
allowBase64: true,
|
|
26
|
-
})
|
|
1
|
+
import Image from '@tiptap/extension-image'
|
|
2
|
+
import { COLLECTION_ASSET_SRC_ATTR } from '../collectionAssetSrc.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Collection document images: data URLs in the editor, relative `assets/…` paths on save
|
|
6
|
+
* via {@link COLLECTION_ASSET_SRC_ATTR} (see platform-ui `normalizeCollectionDocumentHtml`).
|
|
7
|
+
*/
|
|
8
|
+
export const CollectionImage = Image.extend({
|
|
9
|
+
addAttributes() {
|
|
10
|
+
return {
|
|
11
|
+
...this.parent?.(),
|
|
12
|
+
collectionAssetSrc: {
|
|
13
|
+
default: null,
|
|
14
|
+
parseHTML: element => element.getAttribute(COLLECTION_ASSET_SRC_ATTR),
|
|
15
|
+
renderHTML: attributes => {
|
|
16
|
+
const stored = attributes.collectionAssetSrc
|
|
17
|
+
if (!stored || typeof stored !== 'string') return {}
|
|
18
|
+
return { [COLLECTION_ASSET_SRC_ATTR]: stored }
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
}).configure({
|
|
24
|
+
inline: false,
|
|
25
|
+
allowBase64: true,
|
|
26
|
+
})
|
|
@@ -1,215 +1,215 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core'
|
|
2
|
-
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
|
3
|
-
|
|
4
|
-
const SUPPORTED_MIME = new Set([
|
|
5
|
-
'image/png',
|
|
6
|
-
'image/jpeg',
|
|
7
|
-
'image/gif',
|
|
8
|
-
'image/webp',
|
|
9
|
-
'image/svg+xml',
|
|
10
|
-
])
|
|
11
|
-
|
|
12
|
-
const EXT_FOR_MIME: Record<string, string> = {
|
|
13
|
-
'image/png': 'png',
|
|
14
|
-
'image/jpeg': 'jpg',
|
|
15
|
-
'image/gif': 'gif',
|
|
16
|
-
'image/webp': 'webp',
|
|
17
|
-
'image/svg+xml': 'svg',
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface CollectionImagePastePayload {
|
|
21
|
-
name: string
|
|
22
|
-
bytes: Uint8Array
|
|
23
|
-
mimeType: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface CollectionImagePasteExtensionOptions {
|
|
27
|
-
onError?: (message: string) => void
|
|
28
|
-
handleImage: (payload: CollectionImagePastePayload) => Promise<void>
|
|
29
|
-
/** Read bytes from an absolute path (external file:// drops). */
|
|
30
|
-
readBinary?: (absolutePath: string) => Promise<Uint8Array>
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function normalizeMimeType(
|
|
34
|
-
mimeType: string | undefined,
|
|
35
|
-
name: string,
|
|
36
|
-
): string | undefined {
|
|
37
|
-
const trimmed = mimeType?.trim().toLowerCase()
|
|
38
|
-
if (trimmed && SUPPORTED_MIME.has(trimmed)) return trimmed
|
|
39
|
-
const ext = name.split('.').pop()?.toLowerCase()
|
|
40
|
-
if (ext === 'png') return 'image/png'
|
|
41
|
-
if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg'
|
|
42
|
-
if (ext === 'gif') return 'image/gif'
|
|
43
|
-
if (ext === 'webp') return 'image/webp'
|
|
44
|
-
if (ext === 'svg') return 'image/svg+xml'
|
|
45
|
-
return undefined
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function decodeDataImage(dataUrl: string): CollectionImagePastePayload {
|
|
49
|
-
const match = dataUrl.match(/^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i)
|
|
50
|
-
if (!match) throw new Error('Unsupported data URL format.')
|
|
51
|
-
const [, mimeType, payload] = match
|
|
52
|
-
const binary = atob(payload)
|
|
53
|
-
const bytes = Uint8Array.from(binary, char => char.charCodeAt(0))
|
|
54
|
-
return {
|
|
55
|
-
bytes,
|
|
56
|
-
mimeType: mimeType.toLowerCase(),
|
|
57
|
-
name: `image.${EXT_FOR_MIME[mimeType.toLowerCase()] ?? 'png'}`,
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isSupportedImageFile(file: File): boolean {
|
|
62
|
-
return Boolean(normalizeMimeType(file.type, file.name))
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function collectImageFiles(dt: DataTransfer): File[] {
|
|
66
|
-
const seen = new Set<File>()
|
|
67
|
-
const add = (file: File | null) => {
|
|
68
|
-
if (file && isSupportedImageFile(file)) seen.add(file)
|
|
69
|
-
}
|
|
70
|
-
for (const file of Array.from(dt.files)) add(file)
|
|
71
|
-
for (const item of Array.from(dt.items)) {
|
|
72
|
-
if (item.kind === 'file') add(item.getAsFile())
|
|
73
|
-
}
|
|
74
|
-
return [...seen]
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function filePathFromDataTransfer(dt: DataTransfer): string | null {
|
|
78
|
-
const uri =
|
|
79
|
-
dt
|
|
80
|
-
.getData('text/uri-list')
|
|
81
|
-
?.split('\n')
|
|
82
|
-
.map(line => line.trim())
|
|
83
|
-
.find(line => line && !line.startsWith('#')) ?? null
|
|
84
|
-
if (!uri) return null
|
|
85
|
-
try {
|
|
86
|
-
if (uri.startsWith('file://')) {
|
|
87
|
-
return decodeURIComponent(new URL(uri).pathname)
|
|
88
|
-
}
|
|
89
|
-
if (uri.startsWith('/')) return uri
|
|
90
|
-
} catch {
|
|
91
|
-
return null
|
|
92
|
-
}
|
|
93
|
-
return null
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function nameFromLocalPath(filePath: string): string {
|
|
97
|
-
const normalized = filePath.replace(/\\/g, '/')
|
|
98
|
-
return normalized.split('/').pop() || 'image.png'
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** TipTap paste/drop hook — persistence is injected via `handleImage`. */
|
|
102
|
-
export function createCollectionImagePasteExtension(
|
|
103
|
-
options: CollectionImagePasteExtensionOptions,
|
|
104
|
-
): Extension {
|
|
105
|
-
const { onError, handleImage, readBinary } = options
|
|
106
|
-
|
|
107
|
-
return Extension.create({
|
|
108
|
-
name: 'collectionImagePaste',
|
|
109
|
-
|
|
110
|
-
addProseMirrorPlugins() {
|
|
111
|
-
const handleFiles = async (files: File[]): Promise<boolean> => {
|
|
112
|
-
const images = files.filter(isSupportedImageFile)
|
|
113
|
-
if (images.length === 0) {
|
|
114
|
-
if (files.length > 0) {
|
|
115
|
-
onError?.(
|
|
116
|
-
'Unsupported file type. Supported: PNG, JPEG, GIF, WebP, SVG.',
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
return false
|
|
120
|
-
}
|
|
121
|
-
for (const file of images) {
|
|
122
|
-
try {
|
|
123
|
-
await handleImage({
|
|
124
|
-
name: file.name,
|
|
125
|
-
bytes: new Uint8Array(await file.arrayBuffer()),
|
|
126
|
-
mimeType: normalizeMimeType(file.type, file.name) ?? 'image/png',
|
|
127
|
-
})
|
|
128
|
-
} catch (error) {
|
|
129
|
-
const message =
|
|
130
|
-
error instanceof Error ? error.message : String(error)
|
|
131
|
-
onError?.(`Could not save image: ${message}`)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return true
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const handleExternalFilePath = async (
|
|
138
|
-
absolutePath: string,
|
|
139
|
-
): Promise<boolean> => {
|
|
140
|
-
if (!readBinary) return false
|
|
141
|
-
const lower = absolutePath.toLowerCase()
|
|
142
|
-
if (!/\.(png|jpe?g|gif|webp|svg)$/.test(lower)) return false
|
|
143
|
-
try {
|
|
144
|
-
const bytes = await readBinary(absolutePath)
|
|
145
|
-
await handleImage({
|
|
146
|
-
name: nameFromLocalPath(absolutePath),
|
|
147
|
-
bytes,
|
|
148
|
-
mimeType: normalizeMimeType(undefined, absolutePath) ?? 'image/png',
|
|
149
|
-
})
|
|
150
|
-
return true
|
|
151
|
-
} catch (error) {
|
|
152
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
153
|
-
onError?.(`Could not import image: ${message}`)
|
|
154
|
-
return false
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return [
|
|
159
|
-
new Plugin({
|
|
160
|
-
key: new PluginKey('collectionImagePaste'),
|
|
161
|
-
props: {
|
|
162
|
-
handlePaste(_view, event) {
|
|
163
|
-
const dt = event.clipboardData
|
|
164
|
-
if (!dt) return false
|
|
165
|
-
|
|
166
|
-
const files = collectImageFiles(dt)
|
|
167
|
-
if (files.length > 0) {
|
|
168
|
-
event.preventDefault()
|
|
169
|
-
void handleFiles(files)
|
|
170
|
-
return true
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const text = dt.getData('text/plain')?.trim()
|
|
174
|
-
if (text?.startsWith('data:image/')) {
|
|
175
|
-
event.preventDefault()
|
|
176
|
-
void (async () => {
|
|
177
|
-
try {
|
|
178
|
-
await handleImage(decodeDataImage(text))
|
|
179
|
-
} catch (error) {
|
|
180
|
-
const message =
|
|
181
|
-
error instanceof Error ? error.message : String(error)
|
|
182
|
-
onError?.(`Could not save image: ${message}`)
|
|
183
|
-
}
|
|
184
|
-
})()
|
|
185
|
-
return true
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return false
|
|
189
|
-
},
|
|
190
|
-
handleDrop(_view, event) {
|
|
191
|
-
const dt = event.dataTransfer
|
|
192
|
-
if (!dt) return false
|
|
193
|
-
|
|
194
|
-
const files = collectImageFiles(dt)
|
|
195
|
-
if (files.length > 0) {
|
|
196
|
-
event.preventDefault()
|
|
197
|
-
void handleFiles(files)
|
|
198
|
-
return true
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const externalPath = filePathFromDataTransfer(dt)
|
|
202
|
-
if (externalPath) {
|
|
203
|
-
event.preventDefault()
|
|
204
|
-
void handleExternalFilePath(externalPath)
|
|
205
|
-
return true
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return false
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
}),
|
|
212
|
-
]
|
|
213
|
-
},
|
|
214
|
-
})
|
|
215
|
-
}
|
|
1
|
+
import { Extension } from '@tiptap/core'
|
|
2
|
+
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
|
3
|
+
|
|
4
|
+
const SUPPORTED_MIME = new Set([
|
|
5
|
+
'image/png',
|
|
6
|
+
'image/jpeg',
|
|
7
|
+
'image/gif',
|
|
8
|
+
'image/webp',
|
|
9
|
+
'image/svg+xml',
|
|
10
|
+
])
|
|
11
|
+
|
|
12
|
+
const EXT_FOR_MIME: Record<string, string> = {
|
|
13
|
+
'image/png': 'png',
|
|
14
|
+
'image/jpeg': 'jpg',
|
|
15
|
+
'image/gif': 'gif',
|
|
16
|
+
'image/webp': 'webp',
|
|
17
|
+
'image/svg+xml': 'svg',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CollectionImagePastePayload {
|
|
21
|
+
name: string
|
|
22
|
+
bytes: Uint8Array
|
|
23
|
+
mimeType: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CollectionImagePasteExtensionOptions {
|
|
27
|
+
onError?: (message: string) => void
|
|
28
|
+
handleImage: (payload: CollectionImagePastePayload) => Promise<void>
|
|
29
|
+
/** Read bytes from an absolute path (external file:// drops). */
|
|
30
|
+
readBinary?: (absolutePath: string) => Promise<Uint8Array>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeMimeType(
|
|
34
|
+
mimeType: string | undefined,
|
|
35
|
+
name: string,
|
|
36
|
+
): string | undefined {
|
|
37
|
+
const trimmed = mimeType?.trim().toLowerCase()
|
|
38
|
+
if (trimmed && SUPPORTED_MIME.has(trimmed)) return trimmed
|
|
39
|
+
const ext = name.split('.').pop()?.toLowerCase()
|
|
40
|
+
if (ext === 'png') return 'image/png'
|
|
41
|
+
if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg'
|
|
42
|
+
if (ext === 'gif') return 'image/gif'
|
|
43
|
+
if (ext === 'webp') return 'image/webp'
|
|
44
|
+
if (ext === 'svg') return 'image/svg+xml'
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function decodeDataImage(dataUrl: string): CollectionImagePastePayload {
|
|
49
|
+
const match = dataUrl.match(/^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i)
|
|
50
|
+
if (!match) throw new Error('Unsupported data URL format.')
|
|
51
|
+
const [, mimeType, payload] = match
|
|
52
|
+
const binary = atob(payload)
|
|
53
|
+
const bytes = Uint8Array.from(binary, char => char.charCodeAt(0))
|
|
54
|
+
return {
|
|
55
|
+
bytes,
|
|
56
|
+
mimeType: mimeType.toLowerCase(),
|
|
57
|
+
name: `image.${EXT_FOR_MIME[mimeType.toLowerCase()] ?? 'png'}`,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isSupportedImageFile(file: File): boolean {
|
|
62
|
+
return Boolean(normalizeMimeType(file.type, file.name))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collectImageFiles(dt: DataTransfer): File[] {
|
|
66
|
+
const seen = new Set<File>()
|
|
67
|
+
const add = (file: File | null) => {
|
|
68
|
+
if (file && isSupportedImageFile(file)) seen.add(file)
|
|
69
|
+
}
|
|
70
|
+
for (const file of Array.from(dt.files)) add(file)
|
|
71
|
+
for (const item of Array.from(dt.items)) {
|
|
72
|
+
if (item.kind === 'file') add(item.getAsFile())
|
|
73
|
+
}
|
|
74
|
+
return [...seen]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function filePathFromDataTransfer(dt: DataTransfer): string | null {
|
|
78
|
+
const uri =
|
|
79
|
+
dt
|
|
80
|
+
.getData('text/uri-list')
|
|
81
|
+
?.split('\n')
|
|
82
|
+
.map(line => line.trim())
|
|
83
|
+
.find(line => line && !line.startsWith('#')) ?? null
|
|
84
|
+
if (!uri) return null
|
|
85
|
+
try {
|
|
86
|
+
if (uri.startsWith('file://')) {
|
|
87
|
+
return decodeURIComponent(new URL(uri).pathname)
|
|
88
|
+
}
|
|
89
|
+
if (uri.startsWith('/')) return uri
|
|
90
|
+
} catch {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function nameFromLocalPath(filePath: string): string {
|
|
97
|
+
const normalized = filePath.replace(/\\/g, '/')
|
|
98
|
+
return normalized.split('/').pop() || 'image.png'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** TipTap paste/drop hook — persistence is injected via `handleImage`. */
|
|
102
|
+
export function createCollectionImagePasteExtension(
|
|
103
|
+
options: CollectionImagePasteExtensionOptions,
|
|
104
|
+
): Extension {
|
|
105
|
+
const { onError, handleImage, readBinary } = options
|
|
106
|
+
|
|
107
|
+
return Extension.create({
|
|
108
|
+
name: 'collectionImagePaste',
|
|
109
|
+
|
|
110
|
+
addProseMirrorPlugins() {
|
|
111
|
+
const handleFiles = async (files: File[]): Promise<boolean> => {
|
|
112
|
+
const images = files.filter(isSupportedImageFile)
|
|
113
|
+
if (images.length === 0) {
|
|
114
|
+
if (files.length > 0) {
|
|
115
|
+
onError?.(
|
|
116
|
+
'Unsupported file type. Supported: PNG, JPEG, GIF, WebP, SVG.',
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
for (const file of images) {
|
|
122
|
+
try {
|
|
123
|
+
await handleImage({
|
|
124
|
+
name: file.name,
|
|
125
|
+
bytes: new Uint8Array(await file.arrayBuffer()),
|
|
126
|
+
mimeType: normalizeMimeType(file.type, file.name) ?? 'image/png',
|
|
127
|
+
})
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const message =
|
|
130
|
+
error instanceof Error ? error.message : String(error)
|
|
131
|
+
onError?.(`Could not save image: ${message}`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const handleExternalFilePath = async (
|
|
138
|
+
absolutePath: string,
|
|
139
|
+
): Promise<boolean> => {
|
|
140
|
+
if (!readBinary) return false
|
|
141
|
+
const lower = absolutePath.toLowerCase()
|
|
142
|
+
if (!/\.(png|jpe?g|gif|webp|svg)$/.test(lower)) return false
|
|
143
|
+
try {
|
|
144
|
+
const bytes = await readBinary(absolutePath)
|
|
145
|
+
await handleImage({
|
|
146
|
+
name: nameFromLocalPath(absolutePath),
|
|
147
|
+
bytes,
|
|
148
|
+
mimeType: normalizeMimeType(undefined, absolutePath) ?? 'image/png',
|
|
149
|
+
})
|
|
150
|
+
return true
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
153
|
+
onError?.(`Could not import image: ${message}`)
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return [
|
|
159
|
+
new Plugin({
|
|
160
|
+
key: new PluginKey('collectionImagePaste'),
|
|
161
|
+
props: {
|
|
162
|
+
handlePaste(_view, event) {
|
|
163
|
+
const dt = event.clipboardData
|
|
164
|
+
if (!dt) return false
|
|
165
|
+
|
|
166
|
+
const files = collectImageFiles(dt)
|
|
167
|
+
if (files.length > 0) {
|
|
168
|
+
event.preventDefault()
|
|
169
|
+
void handleFiles(files)
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const text = dt.getData('text/plain')?.trim()
|
|
174
|
+
if (text?.startsWith('data:image/')) {
|
|
175
|
+
event.preventDefault()
|
|
176
|
+
void (async () => {
|
|
177
|
+
try {
|
|
178
|
+
await handleImage(decodeDataImage(text))
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const message =
|
|
181
|
+
error instanceof Error ? error.message : String(error)
|
|
182
|
+
onError?.(`Could not save image: ${message}`)
|
|
183
|
+
}
|
|
184
|
+
})()
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return false
|
|
189
|
+
},
|
|
190
|
+
handleDrop(_view, event) {
|
|
191
|
+
const dt = event.dataTransfer
|
|
192
|
+
if (!dt) return false
|
|
193
|
+
|
|
194
|
+
const files = collectImageFiles(dt)
|
|
195
|
+
if (files.length > 0) {
|
|
196
|
+
event.preventDefault()
|
|
197
|
+
void handleFiles(files)
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const externalPath = filePathFromDataTransfer(dt)
|
|
202
|
+
if (externalPath) {
|
|
203
|
+
event.preventDefault()
|
|
204
|
+
void handleExternalFilePath(externalPath)
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return false
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
}),
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
})
|
|
215
|
+
}
|