feat: Clock
This commit is contained in:
109
backend/static/countdown.js
Normal file
109
backend/static/countdown.js
Normal file
@@ -0,0 +1,109 @@
|
||||
(() => {
|
||||
function pad2(value) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
function splitCountdown(ms) {
|
||||
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
||||
const days = Math.floor(totalSeconds / 86400);
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return { days, hours, minutes, seconds };
|
||||
}
|
||||
|
||||
function setAnimatedValue(node, nextValue) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (node.textContent === nextValue) {
|
||||
return;
|
||||
}
|
||||
node.textContent = nextValue;
|
||||
node.classList.remove("is-updated");
|
||||
void node.offsetWidth;
|
||||
node.classList.add("is-updated");
|
||||
}
|
||||
|
||||
function initTimer(widget) {
|
||||
const targetIso = widget.dataset.countdownTarget;
|
||||
const startedLabel = widget.dataset.countdownStarted || "";
|
||||
const toggle = widget.querySelector("[data-countdown-toggle]");
|
||||
const popover = widget.querySelector("[data-countdown-popover]");
|
||||
const subline = widget.querySelector("[data-countdown-subline]");
|
||||
const daysNode = widget.querySelector("[data-countdown-days]");
|
||||
const hoursNode = widget.querySelector("[data-countdown-hours]");
|
||||
const minutesNode = widget.querySelector("[data-countdown-minutes]");
|
||||
const secondsNode = widget.querySelector("[data-countdown-seconds]");
|
||||
|
||||
if (!targetIso || !toggle || !popover || !subline || !daysNode || !hoursNode || !minutesNode || !secondsNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetMs = Date.parse(targetIso);
|
||||
if (Number.isNaN(targetMs)) {
|
||||
subline.textContent = "--";
|
||||
return;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
const now = Date.now();
|
||||
const delta = targetMs - now;
|
||||
|
||||
if (delta <= 0) {
|
||||
setAnimatedValue(daysNode, "0");
|
||||
setAnimatedValue(hoursNode, "00");
|
||||
setAnimatedValue(minutesNode, "00");
|
||||
setAnimatedValue(secondsNode, "00");
|
||||
subline.textContent = startedLabel;
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = splitCountdown(delta);
|
||||
setAnimatedValue(daysNode, String(parts.days));
|
||||
setAnimatedValue(hoursNode, pad2(parts.hours));
|
||||
setAnimatedValue(minutesNode, pad2(parts.minutes));
|
||||
setAnimatedValue(secondsNode, pad2(parts.seconds));
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
popover.hidden = true;
|
||||
toggle.setAttribute("aria-expanded", "false");
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
popover.hidden = false;
|
||||
toggle.setAttribute("aria-expanded", "true");
|
||||
update();
|
||||
};
|
||||
|
||||
toggle.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
if (popover.hidden) {
|
||||
open();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!widget.contains(event.target)) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
update();
|
||||
window.setInterval(update, 1000);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const widgets = document.querySelectorAll(".toolbar-timer");
|
||||
widgets.forEach(initTimer);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user