import { EditorView } from "prosemirror-view"; import uuidv1 from "uuid/v1"; import { EditorSchema } from "../schema"; import { DocPos } from "../types"; import { el, isInputElement } from "../util/dom"; import { FileSource, PendingItem, setPluginState, transitionPending, getPluginStateOrThrow } from "./AttachmentPlugin"; import { AttachmentService } from "./AttachmentService"; import { compose as at } from "./compose"; import { insertAttachment } from "./operations"; import { decodeAsImage, pixelRatio } from "./util/file"; /** * Asynchronously interact with the editor, attachment service, and browser to * insert files as attachments, including image previews. */ export function insertAttachmentsFromFiles( view: EditorView, pos: DocPos, files: FileList, attachmentService: AttachmentService, source: FileSource ) { const newPendingUploads: Array = []; for (let i = 0; i < files.length; i++) { const file = files[i]; const pendingId = uuidv1(); newPendingUploads.push({ id: pendingId, pos, source }); // Process the attachment asynchronously, but don't block execution of this // function. It's important that this function executions synchronously due // to the use of bookmarks. void uploadAndInsert(file, pendingId, attachmentService, view, source); } setPluginState(view, ({ pending, ...rest }) => ({ pending: transitionPending(pending).add({ items: newPendingUploads }), ...rest })); } // This has been pulled out into an async function to be more ergonomic, // specifically the use of `try {} finally {}` async function uploadAndInsert( file: File, pendingId: string, attachmentService: AttachmentService, view: EditorView, source: FileSource ) { const pendingImage = decodeAsImage(file).catch(() => null); const pendingUploadDescriptor = attachmentService.upload(file); try { const [image, uploadDescriptor] = await Promise.all([pendingImage, pendingUploadDescriptor]); attachmentService.seedPreview(uploadDescriptor.id, file); // Since this function is async, and some time may have passed, we need to // query the current position of the pending upload. It might seem simpler // to have just passed this though, but that is not possible. const { pending } = getPluginStateOrThrow(view.state); for (const pendingUpload of pending.items) { if (pendingUpload.id === pendingId) { const ratio = pixelRatio(file, source === FileSource.PASTE); const naturalSize = image !== null ? { width: image.naturalWidth / ratio, height: image.naturalHeight / ratio } : undefined; const tr = view.state.tr; insertAttachment( tr, pendingUpload.pos, at({ id: uploadDescriptor.id, type: file.type, name: file.name, naturalSize }).node ); view.dispatch(tr); break; } } } finally { setPluginState(view, ({ pending, ...rest }) => ({ pending: transitionPending(pending).remove({ id: pendingId }), ...rest })); } } export function pickFile(view: EditorView, attachmentService: AttachmentService, accept: string) { const fileInput = el("input"); fileInput.type = "file"; fileInput.accept = accept; fileInput.multiple = true; fileInput.addEventListener("change", e => { if (e.target !== null && isInputElement(e.target)) { if (e.target.files !== null) { insertAttachmentsFromFiles( view, view.state.selection.anchor as DocPos, e.target.files, attachmentService, FileSource.FILE_PICKER ); } } }); fileInput.click(); }