class PhotoUpload extends HTMLElement { // make element form-associated static formAssociated = true; constructor() { super(); this.attachShadow({ mode: "open" }); // Track all selected files this.selectedFiles = new Map(); this.fileCounter = 0; this.render(); this.fileInput = this.shadowRoot.getElementById("photos"); this.internals = this.attachInternals(); } connectedCallback() { setTimeout(() => this._initializeUploader(), 0); } _initializeUploader() { // Elements const photoInput = this.shadowRoot.querySelector("#photos"); // Handle photo selection photoInput.addEventListener("change", () => { this._handlePhotoSelection(); this._updateFormValue(); }); // Handle drag and drop const photoDropArea = this.shadowRoot.querySelector("#photoDropArea"); photoDropArea.addEventListener("dragover", (e) => { e.preventDefault(); photoDropArea.style.backgroundColor = "#e9ecef"; }); photoDropArea.addEventListener("dragleave", () => { photoDropArea.style.backgroundColor = "#f8f9fa"; }); photoDropArea.addEventListener("drop", (e) => { e.preventDefault(); photoDropArea.style.backgroundColor = "#f8f9fa"; if (e.dataTransfer.files.length) { this._handleFiles(e.dataTransfer.files); } }); } // Update form value with all selected files _updateFormValue() { const entries = new FormData(); for (const [fileId, file] of this.selectedFiles.entries()) { entries.append(`photo_${fileId}`, file); } this.internals.setFormValue(entries); } // Handle files from drag and drop _handleFiles(files) { // Set the files to the input element // (Not directly possible, but we can process them manually) Array.from(files).forEach((file) => { if (file.type.match("image.*")) { const fileId = this.fileCounter++; this.selectedFiles.set(fileId, file); this._createImagePreview(file, fileId); } }); this._updateFormValue(); } render() { const style = ` `; // Create the table let html = `
Take pictures of the mosquito problem area
`; // Set the shadow DOM content this.shadowRoot.innerHTML = style + html; this.shadowRoot.handleButtonClick = () => { const photoInput = this.shadowRoot.querySelector("#photos"); photoInput.click(); }; } /** * Create an image preview for a single file */ _createImagePreview(file, fileId) { const photoPreviewContainer = this.shadowRoot.querySelector( "#photoPreviewContainer", ); // Create preview container const previewContainer = document.createElement("div"); previewContainer.className = "position-relative m-1"; previewContainer.dataset.fileId = fileId; // Create image preview const img = document.createElement("img"); img.className = "img-thumbnail"; img.style.width = "100px"; img.style.height = "100px"; img.style.objectFit = "cover"; // Read file and set preview const reader = new FileReader(); reader.onload = (e) => { img.src = e.target.result; }; reader.readAsDataURL(file); // Create remove button const removeBtn = document.createElement("button"); removeBtn.type = "button"; removeBtn.className = "btn btn-sm btn-danger position-absolute top-0 end-0"; removeBtn.innerHTML = "×"; removeBtn.style.fontSize = "10px"; removeBtn.style.padding = "0 5px"; // Handle remove button click removeBtn.addEventListener("click", () => { // Remove this file from our collection this.selectedFiles.delete(parseInt(previewContainer.dataset.fileId)); // Update the form value this._updateFormValue(); // Remove the preview previewContainer.remove(); }); // Add elements to the preview container previewContainer.appendChild(img); previewContainer.appendChild(removeBtn); photoPreviewContainer.appendChild(previewContainer); } /** * Handle photo selection and preview */ _handlePhotoSelection() { const photoInput = this.shadowRoot.querySelector("#photos"); // Check if files were selected if (photoInput.files && photoInput.files.length > 0) { // Loop through selected files Array.from(photoInput.files).forEach((file) => { if (!file.type.match("image.*")) { console.log("Skipping non-image file", file.type); return; // Skip non-image files } // Add file to our collection with unique ID const fileId = this.fileCounter++; this.selectedFiles.set(fileId, file); // Create and add preview this._createImagePreview(file, fileId); }); } } } // Register the custom element customElements.define("photo-upload", PhotoUpload);