/*
* The client side script that cgit serves with every page. Its main job is
* to refresh the relative ages that cgit_print_age renders in
* source/ui-shared.c, so the thresholds, suffixes and class names tabulated
* below mirror the constants there and the two have to move together. Three
* smaller pieces follow, the selects that reload the page when they change,
* the highlight laid over the source line a URL fragment names, and the
* colour theme toggle. Each piece is wrapped in a function of its own so
* that nothing is left behind in the global scope.
*/
(function () {
// Written the way SECONDS_PER_* are derived in source/cgit.h, so that the
// bucket a browser picks is the bucket cgit_print_age already picked on the
// server. A month is a twelfth of a year rather than thirty days, which is
// the definition the server uses.
var MINUTE = 60;
var HOUR = 60 * MINUTE;
var DAY = 24 * HOUR;
var WEEK = 7 * DAY;
var YEAR = 365 * DAY;
var MONTH = YEAR / 12;
// The five arrays are indexed together by the bucket an age falls into. The
// search in render_age can stop one place past the last class, so the arrays
// it reads there repeat their final entry. Each limit is twice the next unit
// up, matching the thresholds cgit_print_age compares against.
var age_classes = [ "age-mins", "age-hours", "age-days", "age-weeks", "age-months", "age-years" ];
var age_suffix = [ "min.", "hours", "days", "weeks", "months", "years", "years" ];
var age_unit = [ MINUTE, HOUR, DAY, WEEK, MONTH, YEAR, YEAR ];
var age_limit = [ 2 * HOUR, 2 * DAY, 2 * WEEK, 2 * MONTH, 2 * YEAR, 2 * YEAR ];
var update_delay = [ 10, 5 * MINUTE, 30 * MINUTE, DAY, DAY, DAY, DAY ];
function render_age(element, age) {
var text, bucket;
for (bucket = 0; bucket < age_classes.length; bucket++)
if (age < age_limit[bucket])
break;
text = Math.round(age / age_unit[bucket]) + " " + age_suffix[bucket];
// Every age on the page is measured again on each pass, so most of
// them are already reading correctly and writing to them would cost a
// repaint for nothing.
if (element.textContent != text) {
element.textContent = text;
// An age past the last limit is still counted in years, but
// there is no class beyond age-years to put on it.
if (bucket == age_classes.length)
bucket--;
if (element.className != age_classes[bucket])
element.className = age_classes[bucket];
}
}
/*
* Measures every age on the page against the clock and books the next pass.
* There is no point coming back before the coarsest unit on the page could
* change, so a page already counted in hours is left alone for minutes and
* one counted in years for a day.
*/
function refresh_ages() {
var bucket, elements, i, age;
var delay = 24 * 3600;
var now = Math.round(new Date().getTime() / 1000);
for (bucket = 0; bucket < age_classes.length; bucket++) {
elements = document.getElementsByClassName(age_classes[bucket]);
if (elements.length && update_delay[bucket] < delay)
delay = update_delay[bucket];
for (i = 0; i < elements.length; i++) {
age = now - elements[i].getAttribute("data-ut");
// A commit dated ahead of the viewer's clock would
// otherwise show a negative age, and ui-shared.c
// clamps it the same way.
if (age < 0)
age = 0;
render_age(elements[i], age);
}
}
window.setTimeout(refresh_ages, delay * 1000);
}
document.addEventListener("DOMContentLoaded", function () {
// Nothing here depends on layout, so the first pass can run as soon
// as the document is parsed.
refresh_ages();
}, false);
})();
/*
* Selects marked data-autosubmit reload the page with the new setting. They
* are wired up from here rather than with an inline onchange handler because
* a strict Content-Security-Policy blocks those, and a reader without
* scripting still has the noscript reload button the forms carry.
*/
(function () {
document.addEventListener("DOMContentLoaded", function () {
var i, selects = document.querySelectorAll("select[data-autosubmit]");
for (i = 0; i < selects.length; i++)
selects[i].addEventListener("change", function () {
this.form.submit();
});
}, false);
})();
/*
* Washes a faded highlight over the source line a URL fragment names, either
* a single line as in #n231 or a range as in #n5-n12. The line anchors live
* in the number gutter, so the anchor is measured and a bar of the same
* height is laid across the full width of the code. Without scripting the
* CSS target rule still tints the line number itself.
*/
(function () {
var bar = null;
function place_bar() {
var match, first, last, table;
var table_box, first_box, last_box, top, bottom;
if (bar) {
bar.remove();
bar = null;
}
match = /^#n(\d+)(?:-n?(\d+))?$/.exec(location.hash);
if (!match)
return;
first = document.getElementById("n" + match[1]);
if (!first)
return;
last = (match[2] && document.getElementById("n" + match[2])) || first;
table = first.closest("table");
if (!table)
return;
table_box = table.getBoundingClientRect();
first_box = first.getBoundingClientRect();
last_box = last.getBoundingClientRect();
top = Math.min(first_box.top, last_box.top) - table_box.top;
bottom = Math.max(first_box.bottom, last_box.bottom) - table_box.top;
bar = document.createElement("div");
bar.className = "line-hl";
bar.style.top = top + "px";
bar.style.height = (bottom - top) + "px";
table.appendChild(bar);
// A browser only scrolls to a fragment that names a real id, which a
// range never does, so bring the start of one into view here.
if (match[2])
first.scrollIntoView({ block: "center" });
}
document.addEventListener("DOMContentLoaded", place_bar, false);
window.addEventListener("hashchange", place_bar, false);
})();
/*
* The colour theme toggle, which cycles from auto through light to dark and
* remembers the choice. Auto sets no override at all, leaving the page to
* follow the system preference the way the stylesheet does on its own.
*/
(function () {
var STORAGE_KEY = "cgit-theme";
var ORDER = [ "auto", "light", "dark" ];
var ICONS = {
auto: '',
light: '',
dark: ''
};
/*
* Local storage throws instead of answering when the browser has storage
* switched off for the site, and a colour preference is not worth breaking
* the page over, so both of these swallow the failure and the reader is left
* on the automatic theme.
*/
function saved() {
try {
return localStorage.getItem(STORAGE_KEY);
} catch (e) {
return null;
}
}
function persist(theme) {
try {
localStorage.setItem(STORAGE_KEY, theme);
} catch (e) { }
}
function apply(theme, button) {
var html = document.documentElement;
if (theme === "auto")
html.removeAttribute("data-theme");
else
html.setAttribute("data-theme", theme);
// The browser paints the page canvas and the scrollbars from
// color-scheme rather than from the stylesheet, so it has to be told
// the choice as well.
html.style.colorScheme = (theme === "auto") ? "" : theme;
if (button) {
button.innerHTML = ICONS[theme];
button.title = "Colour theme: " + theme;
}
}
var theme = saved();
if (ORDER.indexOf(theme) < 0)
theme = "auto";
// This runs while the file is being parsed rather than on DOMContentLoaded.
// The script is fetched from the head and so runs before the body exists,
// which lands the choice ahead of the first paint and stops the page showing
// the other theme for a moment. The marker goes on the same element for the
// same reason, since div#cgit has not been parsed yet, and cgit-js reserves
// the toggle's column so that revealing the button below does not shift the
// header sideways.
apply(theme, null);
document.documentElement.classList.add("cgit-js");
document.addEventListener("DOMContentLoaded", function () {
var root = document.getElementById("cgit");
if (!root)
return;
var button = root.querySelector(".theme-toggle");
if (!button)
return;
apply(theme, button);
// The button is served hidden so that a reader without scripting is
// never shown a control that cannot do anything.
button.hidden = false;
button.addEventListener("click", function () {
theme = ORDER[(ORDER.indexOf(theme) + 1) % ORDER.length];
persist(theme);
apply(theme, button);
});
}, false);
})();