Last active
July 25, 2025 05:57
-
-
Save blinkinglight/6e991f0fdc512cacf68262337b792642 to your computer and use it in GitHub Desktop.
fancy file uploader vanilla js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Document</title> | |
<script src="script.js" defer type="module"></script> | |
</head> | |
<body> | |
<multi-file-uploader upload-url="/upload"></multi-file-uploader> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function generateMonthOptions(selectedValue = "") { | |
const select = document.createElement("select"); | |
select.id = "meta-month"; | |
const now = new Date(); | |
now.setDate(1); // stabilizuojam mėnesio pradžią | |
const monthNames = [ | |
"Sausis", "Vasaris", "Kovas", "Balandis", "Gegužė", "Birželis", | |
"Liepa", "Rugpjūtis", "Rugsėjis", "Spalis", "Lapkritis", "Gruodis" | |
]; | |
for (let offset = -6; offset <= 7; offset++) { | |
const date = new Date(now); | |
date.setMonth(now.getMonth() + offset); | |
const year = date.getFullYear(); | |
const month = String(date.getMonth() + 1).padStart(2, "0"); | |
const value = `${year}-${month}`; | |
const label = `${monthNames[date.getMonth()]} ${year}`; | |
const option = document.createElement("option"); | |
option.value = value; | |
option.textContent = label; | |
if (value === selectedValue) option.selected = true; | |
select.appendChild(option); | |
} | |
return select; | |
} | |
class MultiFileUploader extends HTMLElement { | |
constructor() { | |
super(); | |
this.attachShadow({ mode: 'open' }); | |
this.fileListContainer = document.createElement("div"); | |
this.fileListContainer.innerHTML = ` | |
<style> | |
.file-item { | |
margin-bottom: 6px; | |
font-family: sans-serif; | |
font-size: 13px; | |
} | |
.progress { | |
height: 6px; | |
background: #eee; | |
margin-top: 3px; | |
} | |
.progress-bar { | |
height: 6px; | |
width: 0%; | |
background: #4caf50; | |
} | |
#upload-status { | |
font-weight: bold; | |
margin-bottom: 10px; | |
position: sticky; | |
top: 0; | |
background: white; | |
padding: 5px 0; | |
z-index: 2; | |
} | |
</style> | |
<div> | |
<div id="upload-status">Laukiama</div> | |
<div id="list-inner"></div> | |
</div> | |
`; | |
this.fileListContainer.style.overflowY = "scroll"; // ne "auto", o "scroll" | |
this.fileListContainer.style.position = "fixed"; | |
this.fileListContainer.style.bottom = "20px"; | |
this.fileListContainer.style.right = "20px"; | |
this.fileListContainer.style.width = "300px"; | |
this.fileListContainer.style.maxHeight = "200px"; | |
this.fileListContainer.style.overflowY = "auto"; | |
this.fileListContainer.style.background = "#fff"; | |
this.fileListContainer.style.border = "1px solid #ccc"; | |
this.fileListContainer.style.boxShadow = "0 0 5px rgba(0,0,0,0.2)"; | |
this.fileListContainer.style.padding = "10px"; | |
this.fileListContainer.style.fontSize = "14px"; | |
this.fileListContainer.style.zIndex = "1000"; | |
this.fileListContainer.style.borderRadius = "6px"; | |
// this.fileListContainer.style.display = "flex"; | |
// this.fileListContainer.style.minHeight = "100px"; | |
this.fileListContainer.style.position = "fixed"; | |
document.body.appendChild(this.fileListContainer); | |
this.fileListContainer.appendChild(this.fileListContainer.querySelector("#list-inner")); | |
this.activeUploads = 0; | |
this.updateStatus = () => { | |
const statusEl = this.fileListContainer.querySelector("#upload-status"); | |
statusEl.textContent = this.activeUploads > 0 ? `Keliama... ${this.activeUploads}` : "Baigta"; | |
}; | |
} | |
connectedCallback() { | |
const uploadUrl = this.getAttribute('upload-url') || '/upload'; | |
const defaultMonth = this.getAttribute("default-month") || ""; | |
const defaultComment = this.getAttribute("default-comment") || ""; | |
this.shadowRoot.innerHTML = ` | |
<style> | |
#drop-area { | |
border: 2px dashed #ccc; | |
padding: 20px; | |
text-align: center; | |
margin-bottom: 20px; | |
min-height: 300px; | |
} | |
.file-item { | |
margin: 10px 0; | |
} | |
.progress { | |
height: 10px; | |
background: #eee; | |
margin-top: 5px; | |
} | |
.progress-bar { | |
height: 10px; | |
width: 0%; | |
background: #4caf50; | |
} | |
#upload-status { | |
font-weight: bold; | |
margin-bottom: 10px; | |
} | |
</style> | |
<div id="drop-area"> | |
<select id="meta-month"></select> | |
<input type="text" id="meta-comment" value="${defaultComment}" /> | |
<p>Užtempk failus čia</p> | |
<button id="select-button">Pasirink failus</button> | |
<input type="file" id="file-input" multiple style="display: none" /> | |
</div> | |
<div id="file-list"></div> | |
`; | |
const dropStyle = this.getAttribute("drop-style"); | |
if (dropStyle) { | |
const dropArea = this.shadowRoot.getElementById("drop-area"); | |
dropArea.style.cssText += dropStyle; | |
} | |
const selectMonth = this.shadowRoot.getElementById("meta-month"); | |
if (selectMonth) { | |
selectMonth.replaceWith(generateMonthOptions(defaultMonth)); | |
} | |
const dropArea = this.shadowRoot.getElementById("drop-area"); | |
// const fileList = this.shadowRoot.getElementById("file-list"); | |
const fileList = this.fileListContainer.querySelector("#list-inner"); | |
const selectButton = this.shadowRoot.getElementById("select-button"); | |
const fileInput = this.shadowRoot.getElementById("file-input"); | |
// Drag-n-drop | |
["dragenter", "dragover", "dragleave", "drop"].forEach(eventName => { | |
dropArea.addEventListener(eventName, e => { | |
e.preventDefault(); | |
e.stopPropagation(); | |
}); | |
}); | |
["dragenter", "dragover"].forEach(eventName => { | |
dropArea.addEventListener(eventName, () => | |
dropArea.classList.add("highlight") | |
); | |
}); | |
["dragleave", "drop"].forEach(eventName => { | |
dropArea.addEventListener(eventName, () => | |
dropArea.classList.remove("highlight") | |
); | |
}); | |
function addRetryButton(fileItem, file) { | |
const retryBtn = document.createElement("button"); | |
retryBtn.textContent = "Pakartoti"; | |
retryBtn.style.marginLeft = "10px"; | |
retryBtn.addEventListener("click", () => { | |
retryBtn.disabled = true; | |
retryBtn.textContent = "Bandau..."; | |
fileItem.remove(); // nuimam seną entry | |
uploadFile(file); // paleidžiam iš naujo | |
}); | |
fileItem.appendChild(retryBtn); | |
} | |
function autoRetry(fileItem, file, delaySeconds = 5) { | |
const retryInfo = document.createElement("span"); | |
retryInfo.style.marginLeft = "10px"; | |
fileItem.appendChild(retryInfo); | |
let secondsLeft = delaySeconds; | |
const countdown = () => { | |
retryInfo.textContent = `Bandys iš naujo po ${secondsLeft}s...`; | |
if (secondsLeft <= 0) { | |
fileItem.remove(); // pašalinam seną įrašą | |
uploadFile(file); // bandome vėl | |
} else { | |
secondsLeft--; | |
setTimeout(countdown, 1000); | |
} | |
}; | |
countdown(); | |
} | |
// File select | |
selectButton.addEventListener("click", () => fileInput.click()); | |
fileInput.addEventListener("change", e => { | |
const files = [...e.target.files]; | |
files.forEach(uploadFile); | |
}); | |
dropArea.addEventListener("drop", e => { | |
const files = [...e.dataTransfer.files]; | |
files.forEach(uploadFile); | |
}); | |
const uploadFile = (file) => { | |
this.activeUploads++; | |
this.updateStatus(); | |
const fileItem = document.createElement("div"); | |
fileItem.className = "file-item"; | |
fileItem.innerHTML = `<strong>${file.name}</strong> (${Math.round( | |
file.size / 1024 | |
)} KB) | |
<div class="progress"><div class="progress-bar"></div></div>`; | |
// fileList.appendChild(fileItem); | |
fileList.prepend(fileItem); | |
const progressBar = fileItem.querySelector(".progress-bar"); | |
const xhr = new XMLHttpRequest(); | |
xhr.open("POST", uploadUrl); | |
xhr.upload.addEventListener("progress", (e) => { | |
if (e.lengthComputable) { | |
const percent = (e.loaded / e.total) * 100; | |
progressBar.style.width = percent + "%"; | |
} | |
}); | |
xhr.onerror = () => { | |
progressBar.style.backgroundColor = "red"; | |
fileItem.style.color = "red"; // papildomai visas tekstas raudonas | |
autoRetry(fileItem, file, 5); | |
this.activeUploads--; | |
this.updateStatus(); | |
}; | |
xhr.ontimeout = () => { | |
progressBar.style.backgroundColor = "red"; | |
fileItem.style.color = "red"; | |
autoRetry(fileItem, file, 5); | |
this.activeUploads--; | |
this.updateStatus(); | |
}; | |
xhr.onload = () => { | |
if (xhr.status === 200) { | |
progressBar.style.backgroundColor = "#4caf50"; | |
} else { | |
progressBar.style.backgroundColor = "red"; | |
autoRetry(fileItem, file, 5); | |
} | |
this.activeUploads--; | |
this.updateStatus(); | |
}; | |
const formData = new FormData(); | |
formData.append("file", file); | |
const metaMonth = this.shadowRoot.getElementById("meta-month")?.value; | |
const metaComment = this.shadowRoot.getElementById("meta-comment")?.value; | |
if (metaMonth) formData.append("month", metaMonth); | |
if (metaComment) formData.append("comment", metaComment); | |
xhr.send(formData); | |
}; | |
} | |
} | |
customElements.define("multi-file-uploader", MultiFileUploader); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment