2026-01-30 22:35:18 +00:00
|
|
|
class PhotoUpload extends HTMLElement {
|
2026-01-31 02:18:22 +00:00
|
|
|
// make element form-associated
|
|
|
|
|
static formAssociated = true;
|
|
|
|
|
|
2026-01-30 22:35:18 +00:00
|
|
|
constructor() {
|
|
|
|
|
super();
|
2026-02-06 17:06:36 +00:00
|
|
|
this.attachShadow({ mode: "open" });
|
|
|
|
|
// Track all selected files
|
|
|
|
|
this.selectedFiles = new Map();
|
|
|
|
|
this.fileCounter = 0;
|
2026-01-30 22:35:18 +00:00
|
|
|
this.render();
|
2026-01-31 02:18:22 +00:00
|
|
|
this.fileInput = this.shadowRoot.getElementById("photos");
|
|
|
|
|
this.internals = this.attachInternals();
|
2026-01-30 22:35:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
|
setTimeout(() => this._initializeUploader(), 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_initializeUploader() {
|
|
|
|
|
// Elements
|
2026-02-06 17:06:36 +00:00
|
|
|
const photoInput = this.shadowRoot.querySelector("#photos");
|
2026-01-30 22:35:18 +00:00
|
|
|
|
|
|
|
|
// Handle photo selection
|
2026-02-06 17:06:36 +00:00
|
|
|
photoInput.addEventListener("change", () => {
|
|
|
|
|
this._handlePhotoSelection();
|
|
|
|
|
this._updateFormValue();
|
2026-01-31 02:18:22 +00:00
|
|
|
});
|
2026-02-06 17:06:36 +00:00
|
|
|
|
2026-01-30 22:35:18 +00:00
|
|
|
// Handle drag and drop
|
2026-02-06 17:06:36 +00:00
|
|
|
const photoDropArea = this.shadowRoot.querySelector("#photoDropArea");
|
|
|
|
|
|
|
|
|
|
photoDropArea.addEventListener("dragover", (e) => {
|
2026-01-30 22:35:18 +00:00
|
|
|
e.preventDefault();
|
2026-02-06 17:06:36 +00:00
|
|
|
photoDropArea.style.backgroundColor = "#e9ecef";
|
2026-01-30 22:35:18 +00:00
|
|
|
});
|
2026-02-06 17:06:36 +00:00
|
|
|
|
|
|
|
|
photoDropArea.addEventListener("dragleave", () => {
|
|
|
|
|
photoDropArea.style.backgroundColor = "#f8f9fa";
|
2026-01-30 22:35:18 +00:00
|
|
|
});
|
2026-02-06 17:06:36 +00:00
|
|
|
|
|
|
|
|
photoDropArea.addEventListener("drop", (e) => {
|
2026-01-30 22:35:18 +00:00
|
|
|
e.preventDefault();
|
2026-02-06 17:06:36 +00:00
|
|
|
photoDropArea.style.backgroundColor = "#f8f9fa";
|
|
|
|
|
|
2026-01-30 22:35:18 +00:00
|
|
|
if (e.dataTransfer.files.length) {
|
2026-02-06 17:06:36 +00:00
|
|
|
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);
|
2026-01-30 22:35:18 +00:00
|
|
|
}
|
|
|
|
|
});
|
2026-02-06 17:06:36 +00:00
|
|
|
|
|
|
|
|
this._updateFormValue();
|
2026-01-30 22:35:18 +00:00
|
|
|
}
|
2026-02-06 17:06:36 +00:00
|
|
|
|
2026-01-30 22:35:18 +00:00
|
|
|
render() {
|
|
|
|
|
const style = `
|
2026-02-06 17:06:36 +00:00
|
|
|
<link href="/static/css/bootstrap.css" rel="stylesheet" />
|
|
|
|
|
<style>
|
|
|
|
|
.photo-upload-area {
|
|
|
|
|
border: 2px dashed #ccc;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
|
}
|
2026-01-30 22:35:18 +00:00
|
|
|
|
2026-02-06 17:06:36 +00:00
|
|
|
.photo-preview {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
margin-top: 15px;
|
|
|
|
|
}
|
2026-01-30 22:35:18 +00:00
|
|
|
|
2026-02-06 17:06:36 +00:00
|
|
|
.photo-preview img {
|
|
|
|
|
width: 80px;
|
|
|
|
|
height: 80px;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
`;
|
2026-01-30 22:35:18 +00:00
|
|
|
|
|
|
|
|
// Create the table
|
|
|
|
|
let html = `
|
2026-02-06 17:06:36 +00:00
|
|
|
<div class="photo-upload-area">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-camera mb-2" viewBox="0 0 16 16">
|
|
|
|
|
<path d="M15 12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.172a3 3 0 0 0 2.12-.879l.83-.828A1 1 0 0 1 6.827 3h2.344a1 1 0 0 1 .707.293l.828.828A3 3 0 0 0 12.828 5H14a1 1 0 0 1 1 1v6zM2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4H2z"/>
|
|
|
|
|
<path d="M8 11a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm0 1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM3 6.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<div class="file-upload-container" id="photoDropArea">
|
2026-03-12 01:23:56 +00:00
|
|
|
<input type="file" id="photos" name="photos" class="d-none"
|
|
|
|
|
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp,image/bmp" multiple>
|
2026-02-06 17:06:36 +00:00
|
|
|
<button type="button" class="btn btn-outline-primary mb-2" onclick="this.getRootNode().handleButtonClick()">Add Photos</button>
|
|
|
|
|
</div>
|
|
|
|
|
<small class="d-block text-muted">Take pictures of the mosquito problem area</small>
|
|
|
|
|
|
|
|
|
|
<!-- Photo Preview Area -->
|
|
|
|
|
<div id="photoPreviewContainer" class="photo-preview mt-3 d-flex flex-wrap">
|
|
|
|
|
<!-- Image previews will be added here by JavaScript -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2026-01-30 22:35:18 +00:00
|
|
|
|
|
|
|
|
// Set the shadow DOM content
|
|
|
|
|
this.shadowRoot.innerHTML = style + html;
|
2026-01-30 22:39:43 +00:00
|
|
|
this.shadowRoot.handleButtonClick = () => {
|
2026-02-06 17:06:36 +00:00
|
|
|
const photoInput = this.shadowRoot.querySelector("#photos");
|
2026-01-30 22:39:43 +00:00
|
|
|
photoInput.click();
|
|
|
|
|
};
|
2026-01-30 22:35:18 +00:00
|
|
|
}
|
2026-02-06 17:06:36 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 22:35:18 +00:00
|
|
|
/**
|
|
|
|
|
* Handle photo selection and preview
|
|
|
|
|
*/
|
|
|
|
|
_handlePhotoSelection() {
|
2026-02-06 17:06:36 +00:00
|
|
|
const photoInput = this.shadowRoot.querySelector("#photos");
|
2026-01-30 22:35:18 +00:00
|
|
|
|
|
|
|
|
// Check if files were selected
|
|
|
|
|
if (photoInput.files && photoInput.files.length > 0) {
|
|
|
|
|
// Loop through selected files
|
2026-02-06 17:06:36 +00:00
|
|
|
Array.from(photoInput.files).forEach((file) => {
|
|
|
|
|
if (!file.type.match("image.*")) {
|
2026-01-30 22:35:18 +00:00
|
|
|
console.log("Skipping non-image file", file.type);
|
|
|
|
|
return; // Skip non-image files
|
|
|
|
|
}
|
2026-02-06 17:06:36 +00:00
|
|
|
|
|
|
|
|
// 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);
|
2026-01-30 22:35:18 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Register the custom element
|
2026-02-06 17:06:36 +00:00
|
|
|
customElements.define("photo-upload", PhotoUpload);
|