diff --git a/html/static/js/photo-upload.js b/html/static/js/photo-upload.js index 2fa896cc..d3bb2ef3 100644 --- a/html/static/js/photo-upload.js +++ b/html/static/js/photo-upload.js @@ -4,7 +4,10 @@ class PhotoUpload extends HTMLElement { constructor() { super(); - this.attachShadow({ mode: 'open' }); + 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(); @@ -16,168 +19,193 @@ class PhotoUpload extends HTMLElement { _initializeUploader() { // Elements - const photoInput = this.shadowRoot.querySelector('#photos'); + const photoInput = this.shadowRoot.querySelector("#photos"); // Handle photo selection - photoInput.addEventListener('change', () => {this._handlePhotoSelection()}); - photoInput.addEventListener("input", (event) => { - let value = event.textContent; - const n = "photos"; - let i = 0 - const entries = new FormData(); - for (const file of event.target.files) { - entries.append(n + i, file); - i = i + 1 - } - this.internals.setFormValue(entries); + photoInput.addEventListener("change", () => { + this._handlePhotoSelection(); + this._updateFormValue(); }); + // Handle drag and drop - const photoDropArea = this.shadowRoot.querySelector('#photoDropArea'); - - photoDropArea.addEventListener('dragover', function(e) { + const photoDropArea = this.shadowRoot.querySelector("#photoDropArea"); + + photoDropArea.addEventListener("dragover", (e) => { e.preventDefault(); - photoDropArea.style.backgroundColor = '#e9ecef'; + photoDropArea.style.backgroundColor = "#e9ecef"; }); - - photoDropArea.addEventListener('dragleave', function() { - photoDropArea.style.backgroundColor = '#f8f9fa'; + + photoDropArea.addEventListener("dragleave", () => { + photoDropArea.style.backgroundColor = "#f8f9fa"; }); - - photoDropArea.addEventListener('drop', function(e) { + + photoDropArea.addEventListener("drop", (e) => { e.preventDefault(); - photoDropArea.style.backgroundColor = '#f8f9fa'; - + photoDropArea.style.backgroundColor = "#f8f9fa"; + if (e.dataTransfer.files.length) { - handleFiles(e.dataTransfer.files); + 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 = ` - - - `; + .photo-preview img { + width: 80px; + height: 80px; + object-fit: cover; + border-radius: 4px; + } + + `; // Create the table let html = ` -
- - - - -
- - -
- Take pictures of the mosquito problem area +
+ + + + +
+ + +
+ 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'); + 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'); - const photoPreviewContainer = this.shadowRoot.querySelector('#photoPreviewContainer'); - - // Clear previous previews - photoPreviewContainer.innerHTML = ''; + 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, index) => { - console.log("Handling", index, file); - if (!file.type.match('image.*')) { + Array.from(photoInput.files).forEach((file) => { + if (!file.type.match("image.*")) { console.log("Skipping non-image file", file.type); return; // Skip non-image files } - - // Create preview container - const previewContainer = document.createElement('div'); - previewContainer.className = 'position-relative m-1'; - - // 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', function() { - // Create a new FileList without this file - // Since FileList is immutable, we need to reset the input - // This is a bit tricky and requires recreating the input - previewContainer.remove(); - - // If this was the last image, clear the input entirely - if (photoPreviewContainer.children.length === 0) { - photoInput.value = ''; - } - // Note: Unfortunately, selectively removing files from a FileList isn't straightforward - // In a real implementation, we might track selected files in an array and recreate the input - }); - - // Add elements to the preview container - previewContainer.appendChild(img); - previewContainer.appendChild(removeBtn); - photoPreviewContainer.appendChild(previewContainer); + + // 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); +customElements.define("photo-upload", PhotoUpload);