(() => { const formSelector = "form[data-submit-once]"; const pickerSelector = "[data-image-picker]"; const allowedImageTypes = new Set(["image/jpeg", "image/png", "image/webp"]); const maxImageBytes = 5 * 1024 * 1024; const pickerStates = new WeakMap(); function progressIndicator() { return document.getElementById("submit-progress"); } function resetForm(form) { form.removeAttribute("aria-busy"); delete form.dataset.submitting; const button = form.querySelector("[data-submit-button]"); if (!button) { return; } button.disabled = false; button.removeAttribute("aria-disabled"); if (button.dataset.idleLabel) { button.textContent = button.dataset.idleLabel; delete button.dataset.idleLabel; } } function existingImageCount(picker) { return picker.querySelectorAll("[data-existing-image]:not([hidden])").length; } function updateImageCount(picker, state) { const count = existingImageCount(picker) + state.entries.length; const status = picker.querySelector("[data-image-count]"); if (status) { status.textContent = `${count} of ${state.max}`; } } function showImageError(picker, message) { const error = picker.querySelector("[data-image-error]"); if (!error) { return; } error.textContent = message; error.hidden = !message; } function imageFileAllowed(file) { if (allowedImageTypes.has(file.type)) { return true; } if (file.type) { return false; } return /\.(jpe?g|png|webp)$/i.test(file.name); } function sameImageFile(left, right) { return left.name === right.name && left.size === right.size && left.lastModified === right.lastModified; } function syncImageInput(state) { const transfer = new DataTransfer(); state.entries.forEach((entry) => transfer.items.add(entry.file)); state.input.files = transfer.files; } function removeNewImage(picker, state, entry) { const index = state.entries.indexOf(entry); if (index === -1) { return; } state.entries.splice(index, 1); URL.revokeObjectURL(entry.previewURL); entry.card.remove(); syncImageInput(state); showImageError(picker, ""); updateImageCount(picker, state); } function addImageFiles(picker, state, files) { showImageError(picker, ""); const uniqueFiles = files.filter((file) => !state.entries.some((entry) => sameImageFile(entry.file, file)) ); const available = state.max - existingImageCount(picker) - state.entries.length; if (uniqueFiles.length > available) { showImageError( picker, available > 0 ? `You can add ${available} more ${available === 1 ? "image" : "images"}.` : "You already have 4 images selected." ); syncImageInput(state); return; } for (const file of uniqueFiles) { if (!imageFileAllowed(file)) { showImageError(picker, "Images must be JPEG, PNG, or WebP."); syncImageInput(state); return; } if (file.size > maxImageBytes) { showImageError(picker, `${file.name} is larger than 5 MB.`); syncImageInput(state); return; } } uniqueFiles.forEach((file) => { const fragment = state.template.content.cloneNode(true); const card = fragment.querySelector("[data-new-image]"); const preview = fragment.querySelector("[data-image-preview]"); const name = fragment.querySelector("[data-image-name]"); const previewURL = URL.createObjectURL(file); preview.src = previewURL; if (name) { name.textContent = file.name; } const entry = { file, card, previewURL }; const removeButton = card.querySelector("[data-remove-image]"); removeButton.setAttribute("aria-label", `Remove selected image: ${file.name}`); removeButton.addEventListener("click", () => { removeNewImage(picker, state, entry); }); state.list.appendChild(fragment); state.entries.push(entry); }); syncImageInput(state); updateImageCount(picker, state); } function resetImagePicker(picker) { const state = pickerStates.get(picker); if (!state) { return; } state.entries.forEach((entry) => { URL.revokeObjectURL(entry.previewURL); entry.card.remove(); }); state.entries = []; state.input.value = ""; picker.querySelectorAll("[data-existing-image]").forEach((card) => { card.hidden = false; card.querySelectorAll("input").forEach((input) => { input.disabled = false; }); }); showImageError(picker, ""); updateImageCount(picker, state); } function initializeImagePicker(picker) { if ( pickerStates.has(picker) || typeof DataTransfer === "undefined" || typeof URL.createObjectURL !== "function" ) { return; } const input = picker.querySelector("[data-image-input]"); const dropzone = picker.querySelector("[data-image-dropzone]"); const list = picker.querySelector("[data-image-list]"); const template = picker.querySelector("[data-image-template]"); if (!input || !dropzone || !list || !template) { return; } const state = { input, list, template, entries: [], max: Number.parseInt(picker.dataset.maxImages, 10) || 4, }; pickerStates.set(picker, state); updateImageCount(picker, state); input.addEventListener("change", () => { addImageFiles(picker, state, Array.from(input.files)); }); picker.querySelectorAll("[data-existing-image]").forEach((card) => { card.querySelector("[data-remove-image]").addEventListener("click", () => { card.hidden = true; card.querySelectorAll("input").forEach((existingInput) => { existingInput.disabled = true; }); showImageError(picker, ""); updateImageCount(picker, state); }); }); ["dragenter", "dragover"].forEach((eventName) => { dropzone.addEventListener(eventName, (event) => { event.preventDefault(); dropzone.classList.add("is-dragging"); }); }); ["dragleave", "drop"].forEach((eventName) => { dropzone.addEventListener(eventName, (event) => { event.preventDefault(); dropzone.classList.remove("is-dragging"); }); }); dropzone.addEventListener("drop", (event) => { addImageFiles(picker, state, Array.from(event.dataTransfer.files)); }); picker.closest("form")?.addEventListener("reset", () => { window.setTimeout(() => resetImagePicker(picker), 0); }); } document.addEventListener("submit", (event) => { const form = event.target.closest(formSelector); if (!form) { return; } if (form.dataset.submitting === "true") { event.preventDefault(); return; } form.dataset.submitting = "true"; form.setAttribute("aria-busy", "true"); const button = event.submitter || form.querySelector("[data-submit-button]"); if (button) { button.dataset.idleLabel = button.textContent; button.textContent = form.dataset.submittingLabel || "Posting…"; button.disabled = true; button.setAttribute("aria-disabled", "true"); } const progress = progressIndicator(); if (progress) { progress.hidden = false; } }); window.addEventListener("pageshow", () => { document.querySelectorAll(formSelector).forEach(resetForm); document.querySelectorAll(pickerSelector).forEach(resetImagePicker); const progress = progressIndicator(); if (progress) { progress.hidden = true; } }); document.querySelectorAll(pickerSelector).forEach(initializeImagePicker); })();