mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
fix: Rich editor pasting images within text (#19674)
This commit is contained in:
+32
-32
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { DOMParser as ProseMirrorDOMParser } from '@tiptap/pm/model'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
const dispatchFormEvent = (editorView, name, detail = {}) => {
|
||||
@@ -31,6 +32,14 @@ const validateFiles = ({
|
||||
return null
|
||||
}
|
||||
|
||||
const generateFileKey = () =>
|
||||
([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
|
||||
(
|
||||
c ^
|
||||
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
|
||||
).toString(16),
|
||||
)
|
||||
|
||||
const LocalFilesPlugin = ({
|
||||
editor,
|
||||
acceptedTypes,
|
||||
@@ -112,15 +121,7 @@ const LocalFilesPlugin = ({
|
||||
}),
|
||||
)
|
||||
|
||||
let fileKey = ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(
|
||||
/[018]/g,
|
||||
(c) =>
|
||||
(
|
||||
c ^
|
||||
(crypto.getRandomValues(new Uint8Array(1))[0] &
|
||||
(15 >> (c / 4)))
|
||||
).toString(16),
|
||||
)
|
||||
const fileKey = generateFileKey()
|
||||
|
||||
get$WireUsing().upload(
|
||||
`componentFileAttachments.${statePath}.${fileKey}`,
|
||||
@@ -170,121 +171,261 @@ const LocalFilesPlugin = ({
|
||||
return true
|
||||
},
|
||||
handlePaste(editorView, event) {
|
||||
if (!event.clipboardData?.files.length) {
|
||||
return false
|
||||
}
|
||||
const files = event.clipboardData?.files?.length
|
||||
? Array.from(event.clipboardData.files)
|
||||
: []
|
||||
const hasText = event.clipboardData?.getData('text')?.length > 0
|
||||
const html = event.clipboardData?.getData('text/html') || ''
|
||||
|
||||
if (event.clipboardData?.getData('text').length) {
|
||||
return false
|
||||
}
|
||||
if (files.length && !hasText) {
|
||||
const validationMessage = validateFiles({
|
||||
files,
|
||||
acceptedTypes,
|
||||
acceptedTypesValidationMessage,
|
||||
maxSize,
|
||||
maxSizeValidationMessage,
|
||||
})
|
||||
|
||||
const files = Array.from(event.clipboardData.files)
|
||||
if (validationMessage) {
|
||||
editorView.dom.dispatchEvent(
|
||||
new CustomEvent(
|
||||
'rich-editor-file-validation-message',
|
||||
{
|
||||
bubbles: true,
|
||||
detail: {
|
||||
key,
|
||||
livewireId: get$WireUsing().id,
|
||||
validationMessage,
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const validationMessage = validateFiles({
|
||||
files,
|
||||
acceptedTypes,
|
||||
acceptedTypesValidationMessage,
|
||||
maxSize,
|
||||
maxSizeValidationMessage,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (validationMessage) {
|
||||
editorView.dom.dispatchEvent(
|
||||
new CustomEvent('rich-editor-file-validation-message', {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
key,
|
||||
livewireId: get$WireUsing().id,
|
||||
validationMessage,
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
dispatchFormEvent(editorView, 'form-processing-started', {
|
||||
message: uploadingMessage,
|
||||
})
|
||||
|
||||
files.forEach((file, fileIndex) => {
|
||||
editor.setEditable(false)
|
||||
editorView.dom.dispatchEvent(
|
||||
new CustomEvent('rich-editor-uploading-file', {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
key,
|
||||
livewireId: get$WireUsing().id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const fileKey = generateFileKey()
|
||||
|
||||
get$WireUsing().upload(
|
||||
`componentFileAttachments.${statePath}.${fileKey}`,
|
||||
file,
|
||||
() => {
|
||||
getFileAttachmentUrl(fileKey).then((url) => {
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.insertContentAt(
|
||||
editor.state.selection.anchor,
|
||||
{
|
||||
type: 'image',
|
||||
attrs: {
|
||||
id: fileKey,
|
||||
src: url,
|
||||
},
|
||||
},
|
||||
)
|
||||
.run()
|
||||
|
||||
editor.setEditable(true)
|
||||
editorView.dom.dispatchEvent(
|
||||
new CustomEvent(
|
||||
'rich-editor-uploaded-file',
|
||||
{
|
||||
bubbles: true,
|
||||
detail: {
|
||||
key,
|
||||
livewireId:
|
||||
get$WireUsing().id,
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
if (fileIndex === files.length - 1) {
|
||||
dispatchFormEvent(
|
||||
editorView,
|
||||
'form-processing-finished',
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
if (!files.length) {
|
||||
return false
|
||||
}
|
||||
if (html) {
|
||||
const domParser = new DOMParser()
|
||||
const parsed = domParser.parseFromString(html, 'text/html')
|
||||
const images = parsed.querySelectorAll('img[src]')
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (images.length) {
|
||||
event.stopPropagation()
|
||||
|
||||
dispatchFormEvent(editorView, 'form-processing-started', {
|
||||
message: uploadingMessage,
|
||||
})
|
||||
const { from, to } = editor.state.selection
|
||||
|
||||
files.forEach((file, fileIndex) => {
|
||||
editor.setEditable(false)
|
||||
editorView.dom.dispatchEvent(
|
||||
new CustomEvent('rich-editor-uploading-file', {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
key,
|
||||
livewireId: get$WireUsing().id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
dispatchFormEvent(
|
||||
editorView,
|
||||
'form-processing-started',
|
||||
{ message: uploadingMessage },
|
||||
)
|
||||
|
||||
let fileKey = ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(
|
||||
/[018]/g,
|
||||
(c) =>
|
||||
(
|
||||
c ^
|
||||
(crypto.getRandomValues(new Uint8Array(1))[0] &
|
||||
(15 >> (c / 4)))
|
||||
).toString(16),
|
||||
)
|
||||
editor.setEditable(false)
|
||||
|
||||
get$WireUsing().upload(
|
||||
`componentFileAttachments.${statePath}.${fileKey}`,
|
||||
file,
|
||||
() => {
|
||||
getFileAttachmentUrl(fileKey).then((url) => {
|
||||
if (!url) {
|
||||
return
|
||||
editorView.dom.dispatchEvent(
|
||||
new CustomEvent('rich-editor-uploading-file', {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
key,
|
||||
livewireId: get$WireUsing().id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
;(async () => {
|
||||
for (const image of images) {
|
||||
const src = image.getAttribute('src')
|
||||
|
||||
if (!src.startsWith('data:image/')) {
|
||||
continue
|
||||
}
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.insertContentAt(
|
||||
editor.state.selection.anchor,
|
||||
{
|
||||
type: 'image',
|
||||
attrs: {
|
||||
id: fileKey,
|
||||
src: url,
|
||||
},
|
||||
},
|
||||
)
|
||||
.run()
|
||||
let blob
|
||||
|
||||
editor.setEditable(true)
|
||||
editorView.dom.dispatchEvent(
|
||||
new CustomEvent(
|
||||
'rich-editor-uploaded-file',
|
||||
{
|
||||
bubbles: true,
|
||||
detail: {
|
||||
key,
|
||||
livewireId: get$WireUsing().id,
|
||||
},
|
||||
},
|
||||
),
|
||||
try {
|
||||
const [header, base64] = src.split(',')
|
||||
const mimeType = header.match(/:(.*?);/)[1]
|
||||
const bytes = atob(base64)
|
||||
const array = new Uint8Array(bytes.length)
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
array[i] = bytes.charCodeAt(i)
|
||||
}
|
||||
|
||||
blob = new Blob([array], {
|
||||
type: mimeType,
|
||||
})
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
const extension =
|
||||
blob.type.split('/')[1]?.split('+')[0] ||
|
||||
'png'
|
||||
const file = new File(
|
||||
[blob],
|
||||
`image.${extension}`,
|
||||
{ type: blob.type },
|
||||
)
|
||||
|
||||
if (fileIndex === files.length - 1) {
|
||||
dispatchFormEvent(
|
||||
editorView,
|
||||
'form-processing-finished',
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
const validationMessage = validateFiles({
|
||||
files: [file],
|
||||
acceptedTypes,
|
||||
acceptedTypesValidationMessage,
|
||||
maxSize,
|
||||
maxSizeValidationMessage,
|
||||
})
|
||||
|
||||
return true
|
||||
if (validationMessage) {
|
||||
continue
|
||||
}
|
||||
|
||||
const fileKey = generateFileKey()
|
||||
|
||||
const url = await new Promise((resolve) => {
|
||||
get$WireUsing().upload(
|
||||
`componentFileAttachments.${statePath}.${fileKey}`,
|
||||
file,
|
||||
() => {
|
||||
getFileAttachmentUrl(fileKey).then(
|
||||
(uploadedUrl) =>
|
||||
resolve(
|
||||
uploadedUrl ?? null,
|
||||
),
|
||||
)
|
||||
},
|
||||
() => resolve(null),
|
||||
)
|
||||
})
|
||||
|
||||
if (url) {
|
||||
image.setAttribute('src', url)
|
||||
image.setAttribute('data-id', fileKey)
|
||||
}
|
||||
}
|
||||
|
||||
for (const image of Array.from(
|
||||
parsed.querySelectorAll('img[src]'),
|
||||
)) {
|
||||
const p = parsed.createElement('p')
|
||||
image.replaceWith(p)
|
||||
p.appendChild(image)
|
||||
}
|
||||
|
||||
let cleanedHtml = parsed.body.innerHTML
|
||||
|
||||
editor.view.someProp('transformPastedHTML', (f) => {
|
||||
cleanedHtml = f(cleanedHtml)
|
||||
})
|
||||
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.innerHTML = cleanedHtml
|
||||
|
||||
const slice = ProseMirrorDOMParser.fromSchema(
|
||||
editor.state.schema,
|
||||
).parseSlice(wrapper, {
|
||||
preserveWhitespace: false,
|
||||
})
|
||||
|
||||
editor.view.dispatch(
|
||||
editor.state.tr.replaceRange(from, to, slice),
|
||||
)
|
||||
|
||||
editor.setEditable(true)
|
||||
|
||||
editorView.dom.dispatchEvent(
|
||||
new CustomEvent('rich-editor-uploaded-file', {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
key,
|
||||
livewireId: get$WireUsing().id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
dispatchFormEvent(
|
||||
editorView,
|
||||
'form-processing-finished',
|
||||
)
|
||||
})()
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user