// retoor <retoor@molodetz.nl>
const REFRESH_MS = 60000;
export class LocalTime {
constructor() {
this.localize(document);
this.observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) this.localize(node);
}
}
});
if (document.body) {
this.observer.observe(document.body, { childList: true, subtree: true });
}
window.setInterval(() => this.refreshRelative(), REFRESH_MS);
}
format(iso, mode) {
const date = new Date(iso);
if (isNaN(date.getTime())) return null;
if (mode === "ago") return this.relative(date);
if (mode === "date") return date.toLocaleDateString();
if (mode === "time") {
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}
return date.toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
}
relative(date) {
const seconds = (Date.now() - date.getTime()) / 1000;
if (seconds < 0) return "just now";
if (seconds < 60) return "just now";
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return minutes + "m ago";
const hours = Math.floor(minutes / 60);
if (hours < 24) return hours + "h ago";
const days = Math.floor(hours / 24);
if (days <= 30) return days + "d ago";
return date.toLocaleDateString();
}
apply(el) {
const iso = el.getAttribute("datetime");
if (!iso) return;
const mode = el.dataset.dtMode || "datetime";
const text = this.format(iso, mode);
if (text === null) return;
el.textContent = text;
const full = new Date(iso);
if (!isNaN(full.getTime())) {
el.title = full.toLocaleString([], { dateStyle: "full", timeStyle: "long" });
}
}
localize(root) {
if (root.matches && root.matches("[data-dt]")) this.apply(root);
if (!root.querySelectorAll) return;
root.querySelectorAll("[data-dt]").forEach((el) => this.apply(el));
}
refreshRelative() {
if (document.hidden) return;
const relative = document.querySelectorAll('[data-dt][data-dt-mode="ago"]');
if (!relative.length) return;
relative.forEach((el) => this.apply(el));
}
}