123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 | // Small progressive enhancements: the light/dark toggle, the clone "Copy"
// button, fitting the tag list's frame to its contents, and dates shown as
// "2 days ago". The toggle and the button are hidden by CSS unless this
// script has marked the root, so without JavaScript the page follows the
// system colour scheme, the clone box is a plain read-only field, and dates
// stay as the page wrote them.
// timeAgo describes the time from then to now (both in milliseconds) the
// way people say it: "just now", "5 minutes ago", "yesterday", "3 weeks
// ago", "last year". A time in the future, from a skewed clock, is "just
// now".
var relativeFormat = null;
function timeAgo(then, now) {
"use strict";
var s = (now - then) / 1000;
var units = [
["year", 365.25 * 86400], ["month", 30.44 * 86400], ["week", 7 * 86400],
["day", 86400], ["hour", 3600], ["minute", 60],
];
// Days give way to weeks only after a week, weeks to months only after
// a month, and so on: each unit is used until the next one up reaches 1.
for (var i = 0; i < units.length; i++) {
var n = Math.floor(s / units[i][1]);
if (n < 1) continue;
// Built once: a listing has a date on every row.
if (!relativeFormat) relativeFormat = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
return relativeFormat.format(-n, units[i][0]);
}
return "just now";
}
if (typeof module !== "undefined" && module.exports) module.exports = { timeAgo: timeAgo };
(function () {
"use strict";
if (typeof document === "undefined") return;
var root = document.documentElement;
var KEY = "repo-render-theme";
root.classList.add("js");
function store(v) { try { localStorage.setItem(KEY, v); } catch (e) { /* private mode */ } }
var saved = null;
try { saved = localStorage.getItem(KEY); } catch (e) { /* ignore */ }
// Applied in <head>, before first paint, so there is no flash.
if (saved === "light" || saved === "dark") root.setAttribute("data-theme", saved);
// A toggle elsewhere (the page around the drawer's frame, or another
// tab) arrives as a storage event.
window.addEventListener("storage", function (e) {
if (e.key !== KEY) return;
if (e.newValue === "light" || e.newValue === "dark") root.setAttribute("data-theme", e.newValue);
else root.removeAttribute("data-theme");
});
function effective() {
var t = root.getAttribute("data-theme");
if (t) return t;
return matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
}
function ready() {
var toggles = document.querySelectorAll("[data-theme-toggle]");
for (var i = 0; i < toggles.length; i++) {
toggles[i].addEventListener("click", function () {
var next = effective() === "dark" ? "light" : "dark";
root.setAttribute("data-theme", next);
store(next);
});
}
var copies = document.querySelectorAll("[data-copy]");
Array.prototype.forEach.call(copies, function (btn) {
var box = btn.parentNode;
var input = box && box.querySelector("input");
if (!input) return;
var label = btn.textContent;
var timer;
btn.addEventListener("click", function () {
var done = function (ok) {
btn.textContent = ok ? "Copied" : "Press Ctrl+C";
clearTimeout(timer);
timer = setTimeout(function () { btn.textContent = label; }, 1500);
};
var fail = function () {
input.focus();
input.select();
done(false);
};
// The clipboard API exists only in secure contexts: over
// plain HTTP, select the text for the reader to copy.
if (!navigator.clipboard) return fail();
navigator.clipboard.writeText(input.value).then(function () { done(true); }, fail);
});
});
// Without script the frame keeps its CSS height and scrolls.
var frames = document.querySelectorAll("iframe[data-fit]");
Array.prototype.forEach.call(frames, function (frame) {
frame.addEventListener("load", function () {
var body = frame.contentDocument && frame.contentDocument.body;
if (body) frame.style.height = body.scrollHeight + "px";
});
});
}
// The download menu is a plain <details>; with script it also closes on
// Escape or a click elsewhere.
function closeMenus(except) {
var open = document.querySelectorAll("details.dl[open]");
for (var i = 0; i < open.length; i++) if (open[i] !== except) open[i].removeAttribute("open");
}
document.addEventListener("click", function (e) {
closeMenus(e.target.closest("details.dl"));
});
document.addEventListener("keydown", function (e) {
if (e.key !== "Escape") return;
// Focus inside a menu would be left on a hidden link: move it
// back to the menu's button.
var menu = document.activeElement && document.activeElement.closest("details.dl[open]");
closeMenus(null);
if (menu) menu.querySelector("summary").focus();
});
// Dates: each <time class="ago"> carries its full timestamp, and the
// page shows the date. Rewriting the text as the parser adds it, from
// this script in <head>, means the date is never painted first: mutation
// observers run before the browser next renders. The tooltip becomes
// the reader's local date and time.
var now = Date.now();
function relative(t) {
var when = Date.parse(t.getAttribute("datetime"));
if (isNaN(when)) return;
var text = timeAgo(when, now);
// The parser may add the date after the element: fix it again then,
// but leave text that is already right, or this would never settle.
if (t.textContent !== text) t.textContent = text;
var full = new Date(when).toLocaleString();
if (t.title !== full) t.title = full;
}
function relativeIn(node) {
if (node.nodeType !== 1) node = node.parentNode;
if (!node || !node.closest) return;
var t = node.closest("time.ago");
if (t) return relative(t);
var all = node.querySelectorAll("time.ago");
for (var i = 0; i < all.length; i++) relative(all[i]);
}
var watch = new MutationObserver(function (records) {
for (var i = 0; i < records.length; i++) {
for (var j = 0; j < records[i].addedNodes.length; j++) relativeIn(records[i].addedNodes[j]);
}
});
watch.observe(root, { childList: true, subtree: true });
document.addEventListener("DOMContentLoaded", function () { watch.disconnect(); });
function done() {
relativeIn(root);
ready();
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", done);
else done();
})();
|