feat: support multiple files upload
This commit is contained in:
@@ -49,12 +49,12 @@ export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerO
|
|||||||
view.dom.classList.remove(...DRAGOVER_CLASS_LIST)
|
view.dom.classList.remove(...DRAGOVER_CLASS_LIST)
|
||||||
|
|
||||||
const items = Array.from(event.dataTransfer?.files ?? [])
|
const items = Array.from(event.dataTransfer?.files ?? [])
|
||||||
const mediaFile = items.find(
|
const mediaFiles = items.filter(
|
||||||
(item) => item.type.includes('image') || item.type.includes('video')
|
(item) => item.type.includes('image') || item.type.includes('video')
|
||||||
)
|
)
|
||||||
if (!mediaFile) return false
|
if (!mediaFiles.length) return false
|
||||||
|
|
||||||
uploadFile(view, mediaFile, options)
|
uploadFile(view, mediaFiles, options)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerO
|
|||||||
) {
|
) {
|
||||||
const file = item.getAsFile()
|
const file = item.getAsFile()
|
||||||
if (file) {
|
if (file) {
|
||||||
uploadFile(view, file, options)
|
uploadFile(view, [file], options)
|
||||||
handled = true
|
handled = true
|
||||||
}
|
}
|
||||||
} else if (item.kind === 'string' && item.type === 'text/plain') {
|
} else if (item.kind === 'string' && item.type === 'text/plain') {
|
||||||
@@ -104,79 +104,85 @@ export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerO
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function uploadFile(view: EditorView, file: File, options: ClipboardAndDropHandlerOptions) {
|
async function uploadFile(
|
||||||
const name = file.name
|
view: EditorView,
|
||||||
|
files: File[],
|
||||||
|
options: ClipboardAndDropHandlerOptions
|
||||||
|
) {
|
||||||
|
for (const file of files) {
|
||||||
|
const name = file.name
|
||||||
|
|
||||||
options.onUploadStart?.(file)
|
options.onUploadStart?.(file)
|
||||||
|
|
||||||
const placeholder = `[Uploading "${name}"...]`
|
const placeholder = `[Uploading "${name}"...]`
|
||||||
const uploadingNode = view.state.schema.text(placeholder)
|
const uploadingNode = view.state.schema.text(placeholder)
|
||||||
const hardBreakNode = view.state.schema.nodes.hardBreak.create()
|
const hardBreakNode = view.state.schema.nodes.hardBreak.create()
|
||||||
let tr = view.state.tr.replaceSelectionWith(uploadingNode)
|
let tr = view.state.tr.replaceSelectionWith(uploadingNode)
|
||||||
tr = tr.insert(tr.selection.from, hardBreakNode)
|
tr = tr.insert(tr.selection.from, hardBreakNode)
|
||||||
view.dispatch(tr)
|
view.dispatch(tr)
|
||||||
|
|
||||||
mediaUpload
|
mediaUpload
|
||||||
.upload(file)
|
.upload(file)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
options.onUploadSuccess?.(file, result)
|
options.onUploadSuccess?.(file, result)
|
||||||
const urlNode = view.state.schema.text(result.url)
|
const urlNode = view.state.schema.text(result.url)
|
||||||
|
|
||||||
const tr = view.state.tr
|
const tr = view.state.tr
|
||||||
let didReplace = false
|
let didReplace = false
|
||||||
|
|
||||||
view.state.doc.descendants((node, pos) => {
|
view.state.doc.descendants((node, pos) => {
|
||||||
if (node.isText && node.text && node.text.includes(placeholder) && !didReplace) {
|
if (node.isText && node.text && node.text.includes(placeholder) && !didReplace) {
|
||||||
const startPos = node.text.indexOf(placeholder)
|
const startPos = node.text.indexOf(placeholder)
|
||||||
const from = pos + startPos
|
const from = pos + startPos
|
||||||
const to = from + placeholder.length
|
const to = from + placeholder.length
|
||||||
tr.replaceWith(from, to, urlNode)
|
tr.replaceWith(from, to, urlNode)
|
||||||
didReplace = true
|
didReplace = true
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (didReplace) {
|
||||||
|
view.dispatch(tr)
|
||||||
|
} else {
|
||||||
|
const endPos = view.state.doc.content.size
|
||||||
|
|
||||||
|
const paragraphNode = view.state.schema.nodes.paragraph.create(
|
||||||
|
null,
|
||||||
|
view.state.schema.text(result.url)
|
||||||
|
)
|
||||||
|
|
||||||
|
const insertTr = view.state.tr.insert(endPos, paragraphNode)
|
||||||
|
const newPos = endPos + 1 + result.url.length
|
||||||
|
insertTr.setSelection(TextSelection.near(insertTr.doc.resolve(newPos)))
|
||||||
|
view.dispatch(insertTr)
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
})
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Upload failed:', error)
|
||||||
|
options.onUploadError?.(file, error)
|
||||||
|
|
||||||
if (didReplace) {
|
const tr = view.state.tr
|
||||||
view.dispatch(tr)
|
let didReplace = false
|
||||||
} else {
|
|
||||||
const endPos = view.state.doc.content.size
|
|
||||||
|
|
||||||
const paragraphNode = view.state.schema.nodes.paragraph.create(
|
view.state.doc.descendants((node, pos) => {
|
||||||
null,
|
if (node.isText && node.text && node.text.includes(placeholder) && !didReplace) {
|
||||||
view.state.schema.text(result.url)
|
const startPos = node.text.indexOf(placeholder)
|
||||||
)
|
const from = pos + startPos
|
||||||
|
const to = from + placeholder.length
|
||||||
|
const errorNode = view.state.schema.text(`[Error uploading "${name}"]`)
|
||||||
|
tr.replaceWith(from, to, errorNode)
|
||||||
|
didReplace = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
const insertTr = view.state.tr.insert(endPos, paragraphNode)
|
if (didReplace) {
|
||||||
const newPos = endPos + 1 + result.url.length
|
view.dispatch(tr)
|
||||||
insertTr.setSelection(TextSelection.near(insertTr.doc.resolve(newPos)))
|
|
||||||
view.dispatch(insertTr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Upload failed:', error)
|
|
||||||
options.onUploadError?.(file, error)
|
|
||||||
|
|
||||||
const tr = view.state.tr
|
|
||||||
let didReplace = false
|
|
||||||
|
|
||||||
view.state.doc.descendants((node, pos) => {
|
|
||||||
if (node.isText && node.text && node.text.includes(placeholder) && !didReplace) {
|
|
||||||
const startPos = node.text.indexOf(placeholder)
|
|
||||||
const from = pos + startPos
|
|
||||||
const to = from + placeholder.length
|
|
||||||
const errorNode = view.state.schema.text(`[Error uploading "${name}"]`)
|
|
||||||
tr.replaceWith(from, to, errorNode)
|
|
||||||
didReplace = true
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
|
throw error
|
||||||
})
|
})
|
||||||
|
}
|
||||||
if (didReplace) {
|
|
||||||
view.dispatch(tr)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export default function Uploader({
|
|||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
accept={accept}
|
accept={accept}
|
||||||
|
multiple
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user