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 = `
`;
// 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);