Browse Source

feat: support multiple files upload

imwald
codytseng 8 months ago
parent
commit
de09942124
  1. 154
      src/components/PostEditor/PostTextarea/ClipboardAndDropHandler.ts
  2. 1
      src/components/PostEditor/Uploader.tsx

154
src/components/PostEditor/PostTextarea/ClipboardAndDropHandler.ts

@ -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.onUploadStart?.(file) options: ClipboardAndDropHandlerOptions
) {
const placeholder = `[Uploading "${name}"...]` for (const file of files) {
const uploadingNode = view.state.schema.text(placeholder) const name = file.name
const hardBreakNode = view.state.schema.nodes.hardBreak.create()
let tr = view.state.tr.replaceSelectionWith(uploadingNode) options.onUploadStart?.(file)
tr = tr.insert(tr.selection.from, hardBreakNode)
view.dispatch(tr) const placeholder = `[Uploading "${name}"...]`
const uploadingNode = view.state.schema.text(placeholder)
mediaUpload const hardBreakNode = view.state.schema.nodes.hardBreak.create()
.upload(file) let tr = view.state.tr.replaceSelectionWith(uploadingNode)
.then((result) => { tr = tr.insert(tr.selection.from, hardBreakNode)
options.onUploadSuccess?.(file, result) view.dispatch(tr)
const urlNode = view.state.schema.text(result.url)
mediaUpload
const tr = view.state.tr .upload(file)
let didReplace = false .then((result) => {
options.onUploadSuccess?.(file, result)
view.state.doc.descendants((node, pos) => { const urlNode = view.state.schema.text(result.url)
if (node.isText && node.text && node.text.includes(placeholder) && !didReplace) {
const startPos = node.text.indexOf(placeholder) const tr = view.state.tr
const from = pos + startPos let didReplace = false
const to = from + placeholder.length
tr.replaceWith(from, to, urlNode) view.state.doc.descendants((node, pos) => {
didReplace = true if (node.isText && node.text && node.text.includes(placeholder) && !didReplace) {
return false const startPos = node.text.indexOf(placeholder)
const from = pos + startPos
const to = from + placeholder.length
tr.replaceWith(from, to, urlNode)
didReplace = true
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)
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
})
if (didReplace) { if (didReplace) {
view.dispatch(tr) 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)
}
})
.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
})
if (didReplace) { throw error
view.dispatch(tr) })
} }
throw error
})
} }

1
src/components/PostEditor/Uploader.tsx

@ -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>
) )

Loading…
Cancel
Save