import { post } from "./api.js"; import { h, replace, showAlert } from "./dom.js"; export type PendingImage = { file: Blob; url: string }; export const imageDrafts = new Map(); const imageTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"]; const maxImageBytes = 5 << 20; export function imageTray( drop: HTMLElement, input: HTMLTextAreaElement, key: string, onCount: (n: number) => void, ): { el: HTMLElement; take: () => PendingImage[]; restore: (imgs: PendingImage[]) => void } { const el = h("div", { class: "compose-images" }); const render = () => { const imgs = imageDrafts.get(key) ?? []; onCount(imgs.length); el.hidden = imgs.length === 0; replace( el, ...imgs.map((img) => { const remove = h("button", { type: "button", class: "remove", title: "Remove image" }, "×"); remove.onclick = () => { URL.revokeObjectURL(img.url); const rest = (imageDrafts.get(key) ?? []).filter((i) => i !== img); if (rest.length > 0) { imageDrafts.set(key, rest); } else { imageDrafts.delete(key); } render(); }; return h("div", { class: "thumb" }, h("img", { src: img.url, alt: "attached image" }), remove); }), ); }; const add = (files: Iterable) => { const added: PendingImage[] = []; for (const f of files) { if (!imageTypes.includes(f.type)) { void showAlert(`${f.name || "That file"} isn't a PNG, JPEG, GIF or WebP image.`); continue; } if (f.size > maxImageBytes) { void showAlert(`${f.name || "That image"} is over ${maxImageBytes >> 20} MiB.`); continue; } added.push({ file: f, url: URL.createObjectURL(f) }); } if (added.length === 0) { return false; } imageDrafts.set(key, [...(imageDrafts.get(key) ?? []), ...added]); render(); return true; }; input.addEventListener("paste", (e) => { const files = [...(e.clipboardData?.files ?? [])].filter((f) => f.type.startsWith("image/")); if (files.length > 0) { e.preventDefault(); add(files); } }); drop.addEventListener("dragover", (e) => { if (e.dataTransfer?.types.includes("Files")) { e.preventDefault(); drop.classList.add("dropping"); } }); drop.addEventListener("dragleave", (e) => { if (!drop.contains(e.relatedTarget as Node | null)) { drop.classList.remove("dropping"); } }); drop.addEventListener("drop", (e) => { drop.classList.remove("dropping"); const files = [...(e.dataTransfer?.files ?? [])]; if (files.length > 0) { e.preventDefault(); add(files); } }); render(); return { el, take: () => { const imgs = imageDrafts.get(key) ?? []; imageDrafts.delete(key); render(); return imgs; }, restore: (imgs) => { if (imgs.length > 0) { imageDrafts.set(key, [...imgs, ...(imageDrafts.get(key) ?? [])]); } render(); }, }; } export async function uploadImages(imgs: PendingImage[]): Promise { if (imgs.length === 0) { return undefined; } const ids: string[] = []; for (const img of imgs) { const blob = await post("/api/blobs", { body: img.file }); ids.push(blob.id); } return ids; } export function releaseImages(imgs: PendingImage[]): void { for (const img of imgs) { URL.revokeObjectURL(img.url); } }