import { Node } from '@tiptap/core'
import { Plugin } from 'prosemirror-state'
import { NODE_TYPE, NODE_ATTR } from '../constants.js'
import { asyncCommand } from './helpers/async-command.js'
const getNodeType = (fileModule, mimetype, preview) => {
if (preview) {
if (fileModule.isVideoFile(mimetype)) {
return NODE_TYPE.FILE_VIDEO
}
if (fileModule.isImageFile(mimetype)) {
return NODE_TYPE.FILE_IMAGE
}
}
return NODE_TYPE.FILE_ANY
}
const handleFilesUpload = async (view, files, options) => {
try {
const { state, dispatch } = view
const { schema, doc } = state
const { fileModule, onUpload, onUploadError } = options
const loaderMap = new Map()
// Insert Loaders
for (const sourceFile of files) {
const loaderId = window.crypto.randomUUID()
const loaderNode = schema.nodes[NODE_TYPE.FILE_LOADER].create({ [NODE_ATTR.ID]: loaderId })
dispatch(view.state.tr.insert(doc.content.size, loaderNode))
loaderMap.set(sourceFile, loaderId)
}
for (let i = files.length - 1; i >= 0; i--) {
const sourceFile = files[i]
const result = await onUpload(sourceFile, onUploadError)
if (!result) continue
const { id, file } = result
// Replace Loader for a File Node
view.state.doc.descendants((node, pos) => {
if (node.type.name === NODE_TYPE.FILE_LOADER && node.attrs[NODE_ATTR.ID] === loaderMap.get(sourceFile)) {
const { url, type, name, dimensions, previews: preview } = file
const nodeType = getNodeType(fileModule, file.type, preview)
const newNode = schema.nodes[nodeType].create({
[NODE_ATTR.ID]: id,
src: url,
type,
name,
alt: name,
title: name,
dimensions,
preview
})
dispatch(view.state.tr.replaceWith(pos, pos + node.nodeSize, newNode))
}
})
view.focus()
}
} catch (error) {
options.onUploadError?.(error)
}
}
export const FileUploader = Node.create({
name: NODE_TYPE.FILE_UPLOADER,
addCommands () {
const { options } = this
return {
upload:
(files) =>
({ can, chain, commands, dispatch, editor, state, tr, view }) => {
asyncCommand(() => handleFilesUpload(view, files, options))
}
}
},
addProseMirrorPlugins () {
const { options } = this
return [
new Plugin({
view (view) {
const onDrop = (event) => {
if (options.dropPreventClassNames?.some(item => (
event.target.matches(item) ||
event.target.closest(item)
))) {
return
}
const files = []
const { items } = event.dataTransfer || { items: [] }
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (item.webkitGetAsEntry()?.isFile) {
files.push(item.getAsFile())
}
}
if (files.length === 0) {
return false
}
event.preventDefault()
handleFilesUpload(view, files, options)
}
document.addEventListener('drop', onDrop)
return {
destroy () {
document.removeEventListener('drop', onDrop)
}
}
},
props: {
handlePaste (view, event) {
const files = [...(event.clipboardData?.files || [])]
if (!files || files.length === 0) {
return false
}
event.preventDefault()
handleFilesUpload(view, files, options)
}
}
})
]
}
})