Skip to content

Instantly share code, notes, and snippets.

@blinkinglight
Last active July 25, 2025 05:57
Show Gist options
  • Save blinkinglight/6e991f0fdc512cacf68262337b792642 to your computer and use it in GitHub Desktop.
Save blinkinglight/6e991f0fdc512cacf68262337b792642 to your computer and use it in GitHub Desktop.
fancy file uploader vanilla js
<!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>
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