class EventFileList extends Shadow { constructor(existingAttachments, onDeleteExisting = null, onDeleteNew = null, onPreview = null) { super(); this.existingAttachments = existingAttachments ?? []; this.newFiles = []; this.objectURLs = new Map(); this.onDeleteExisting = onDeleteExisting; this.onDeleteNew = onDeleteNew; this.onPreview = onPreview; } update(files) { this.newFiles.push(...Array.from(files)); this.rerender(); } removeExisting(fileId) { this.existingAttachments = this.existingAttachments.filter(f => f.id !== fileId); this.rerender(); } removeNew(index) { const file = this.newFiles[index]; if (this.objectURLs.has(file)) { URL.revokeObjectURL(this.objectURLs.get(file)); this.objectURLs.delete(file); } this.newFiles.splice(index, 1); this.rerender(); } commitNew(insertedFiles) { this.newFiles.forEach(file => { if (this.objectURLs.has(file)) { URL.revokeObjectURL(this.objectURLs.get(file)); this.objectURLs.delete(file); } }); this.newFiles = []; this.existingAttachments = [...this.existingAttachments, ...insertedFiles]; this.rerender(); } getObjectURL(file) { if (!this.objectURLs.has(file)) { this.objectURLs.set(file, URL.createObjectURL(file)); } return this.objectURLs.get(file); } render() { const hasFiles = this.existingAttachments.length > 0 || this.newFiles.length > 0; this.style.display = hasFiles ? "flex" : "none"; this.style.padding = "1em"; this.style.paddingTop = "0.5em"; this.style.boxSizing = "border-box"; VStack(() => { this.existingAttachments.forEach(file => { const isImage = file.type?.startsWith("image/"); const url = `${config.SERVER}/db/images/events/${file.name}` const row = HStack(() => { if (isImage) { img(`${config.UI}/db/images/events/${file.name}`, "1.5em", "1.5em") .objectFit("cover") .borderRadius(3, px) .flexShrink(0) } else { span("📎") } span(file.original_name ?? file.name) .overflow("hidden") .textOverflow("ellipsis") .whiteSpace("nowrap") .flex(1) span(file.size_bytes != null ? `${(file.size_bytes / 1024).toFixed(1)} KB` : "") .opacity(0.5) .flexShrink(0) .fontSize(0.85, rem) .width("5em") if (this.onDeleteExisting) { span("×") .color("var(--quillred)") .opacity(0.7) .fontSize(1.2, em) .fontWeight("600") .flexShrink(0) .cursor("pointer") .padding(0.5, em) .margin(-0.5, em) .onTap((e) => { e?.stopPropagation(); if (window.isMobile() === true) this.onDeleteExisting(file.id) }) .onClick((done, e) => { e?.stopPropagation(); if (done && window.isMobile() === false) this.onDeleteExisting(file.id) }) } }) .alignItems("center") .gap(0.5, em) if (this.onPreview) { row.cursor("pointer") .onClick((done) => { if (done) this.onPreview(file, url) }) } }) this.newFiles.forEach((file, index) => { const isImage = file.type.startsWith("image/"); const url = this.getObjectURL(file) const row = HStack(() => { if (isImage) { img(url, "1.5em", "1.5em") .objectFit("cover") .borderRadius(3, px) .flexShrink(0) } else { span("📎") } span(file.name) .overflow("hidden") .textOverflow("ellipsis") .whiteSpace("nowrap") .flex(1) span(`${(file.size / 1024).toFixed(1)} KB`) .opacity(0.5) .flexShrink(0) .fontSize(0.85, rem) .width("5em") if (this.onDeleteNew) { span("×") .color("var(--quillred)") .opacity(0.7) .fontSize(1.2, em) .fontWeight("600") .flexShrink(0) .cursor("pointer") .padding(0.5, em) .margin(-0.5, em) .onTap((e) => { e?.stopPropagation(); if (window.isMobile() === true) this.onDeleteNew(index) }) .onClick((done, e) => { e?.stopPropagation(); if (done && window.isMobile() === false) this.onDeleteNew(index) }) } }) .alignItems("center") .gap(0.5, em) if (this.onPreview) { row.cursor("pointer") .onClick((done) => { if (done) this.onPreview(file, url) }) } }) }) .gap(1, em) .color("var(--text)") .fontSize(0.85, rem) .fontFamily("Arial") } } register(EventFileList)