diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--assets/cgit.js252
1 file changed, 152 insertions, 100 deletions
diff --git a/assets/cgit.js b/assets/cgit.js
index f3fedc1..0fed45a 100644
--- a/assets/cgit.js
+++ b/assets/cgit.js
@@ -1,150 +1,182 @@
-/* cgit.js: javacript functions for cgit
- *
- * Copyright (C) 2006-2018 cgit Development Team <cgit@lists.zx2c4.com>
- *
- * Licensed under GNU General Public License v2
- * (see LICENSE.txt for full license text)
+/*
+ * 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 () {
-/* This follows the logic and suffixes used in ui-shared.c */
+// 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;
-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_next = [ 60, 3600, 24 * 3600, 7 * 24 * 3600, 30 * 24 * 3600, 365 * 24 * 3600, 365 * 24 * 3600 ];
-var age_limit = [ 7200, 24 * 7200, 7 * 24 * 7200, 30 * 24 * 7200, 365 * 25 * 7200, 365 * 25 * 7200 ];
-var update_next = [ 10, 5 * 60, 1800, 24 * 3600, 24 * 3600, 24 * 3600, 24 * 3600 ];
+// 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(e, age) {
- var t, n;
+function render_age(element, age) {
+ var text, bucket;
- for (n = 0; n < age_classes.length; n++)
- if (age < age_limit[n])
+ for (bucket = 0; bucket < age_classes.length; bucket++)
+ if (age < age_limit[bucket])
break;
- t = Math.round(age / age_next[n]) + " " + age_suffix[n];
+ text = Math.round(age / age_unit[bucket]) + " " + age_suffix[bucket];
- if (e.textContent != t) {
- e.textContent = t;
- if (n == age_classes.length)
- n--;
- if (e.className != age_classes[n])
- e.className = age_classes[n];
+ // 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];
}
}
-function aging() {
- var n, next = 24 * 3600,
- now_ut = Math.round((new Date().getTime() / 1000));
+/*
+ * 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 (n = 0; n < age_classes.length; n++) {
- var m, elems = document.getElementsByClassName(age_classes[n]);
+ for (bucket = 0; bucket < age_classes.length; bucket++) {
+ elements = document.getElementsByClassName(age_classes[bucket]);
- if (elems.length && update_next[n] < next)
- next = update_next[n];
+ if (elements.length && update_delay[bucket] < delay)
+ delay = update_delay[bucket];
- for (m = 0; m < elems.length; m++) {
- var age = now_ut - elems[m].getAttribute("data-ut");
+ 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 render as a negative age; ui-shared.c
- * clamps the same way. */
+ // 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(elems[m], age);
+ render_age(elements[i], age);
}
}
- /*
- * We only need to come back when the age might have changed.
- * Eg, if everything is counted in hours already, once per
- * 5 minutes is accurate enough.
- */
-
- window.setTimeout(aging, next * 1000);
+ window.setTimeout(refresh_ages, delay * 1000);
}
-document.addEventListener("DOMContentLoaded", function() {
- /* we can do the aging on DOM content load since no layout dependency */
- aging();
+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.
- * Wired here instead of inline onchange handlers, which a strict
- * Content-Security-Policy blocks. Without scripting the forms keep
- * their noscript reload button. */
+/*
+ * 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, els = document.querySelectorAll("select[data-autosubmit]");
+ var i, selects = document.querySelectorAll("select[data-autosubmit]");
- for (i = 0; i < els.length; i++)
- els[i].addEventListener("change", function () {
+ for (i = 0; i < selects.length; i++)
+ selects[i].addEventListener("change", function () {
this.form.submit();
});
}, false);
})();
-/* Wash a faded highlight over the source line targeted by the URL
- * fragment, #n231, or over a range like #n5-n12. The line anchors live
- * in the number gutter, so this measures the anchor and lays a
- * full-width bar across the code at the same height. Without JS the
- * CSS :target rule still tints the line number itself. */
+/*
+ * 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() {
- var m, a, b, table, box, ra, rb, top, bottom;
+function place_bar() {
+ var match, first, last, table;
+ var table_box, first_box, last_box, top, bottom;
if (bar) {
bar.remove();
bar = null;
}
- m = /^#n(\d+)(?:-n?(\d+))?$/.exec(location.hash);
- if (!m)
+ match = /^#n(\d+)(?:-n?(\d+))?$/.exec(location.hash);
+ if (!match)
return;
- a = document.getElementById("n" + m[1]);
- if (!a)
+ first = document.getElementById("n" + match[1]);
+ if (!first)
return;
- b = (m[2] && document.getElementById("n" + m[2])) || a;
- table = a.closest("table");
+ last = (match[2] && document.getElementById("n" + match[2])) || first;
+ table = first.closest("table");
if (!table)
return;
- box = table.getBoundingClientRect();
- ra = a.getBoundingClientRect();
- rb = b.getBoundingClientRect();
- top = Math.min(ra.top, rb.top) - box.top;
- bottom = Math.max(ra.bottom, rb.bottom) - box.top;
+ 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);
- /* The browser only scrolls to fragments that name a real id, so
- * bring ranges into view ourselves. */
- if (m[2])
- a.scrollIntoView({ block: "center" });
+
+ // 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, false);
-window.addEventListener("hashchange", place, false);
+document.addEventListener("DOMContentLoaded", place_bar, false);
+window.addEventListener("hashchange", place_bar, false);
})();
-/* Colour theme toggle: cycles auto -> light -> dark and remembers the
- * choice. "auto" leaves the page following the system preference via CSS. */
+/*
+ * 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 KEY = "cgit-theme";
+var STORAGE_KEY = "cgit-theme";
var ORDER = [ "auto", "light", "dark" ];
var ICONS = {
auto: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 0 0 18z" fill="currentColor" stroke="none"/></svg>',
@@ -152,25 +184,42 @@ var ICONS = {
dark: '<svg viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/></svg>'
};
+/*
+ * 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(KEY); } catch (e) { return null; }
+ try {
+ return localStorage.getItem(STORAGE_KEY);
+ } catch (e) {
+ return null;
+ }
}
-function persist(value) {
- try { localStorage.setItem(KEY, value); } catch (e) { }
+function persist(theme) {
+ try {
+ localStorage.setItem(STORAGE_KEY, theme);
+ } catch (e) { }
}
-function apply(theme, btn) {
+function apply(theme, button) {
var html = document.documentElement;
+
if (theme === "auto")
html.removeAttribute("data-theme");
else
html.setAttribute("data-theme", theme);
- /* Keep the standalone page background in sync with the choice. */
+
+ // 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 (btn) {
- btn.innerHTML = ICONS[theme];
- btn.title = "Colour theme: " + theme;
+
+ if (button) {
+ button.innerHTML = ICONS[theme];
+ button.title = "Colour theme: " + theme;
}
}
@@ -178,12 +227,13 @@ var theme = saved();
if (ORDER.indexOf(theme) < 0)
theme = "auto";
-/* Settled here rather than on DOMContentLoaded. This file is fetched from
- * <head> and runs before the body is parsed, so the choice lands on <html>
- * ahead of the first paint and the page never shows the other theme first.
- * The marker sits on <html> for the same reason, since div#cgit does not
- * exist yet. cgit-js reserves the toggle's column so revealing the button
- * below does not shift the header. */
+// 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");
@@ -191,16 +241,18 @@ document.addEventListener("DOMContentLoaded", function () {
var root = document.getElementById("cgit");
if (!root)
return;
- var btn = root.querySelector(".theme-toggle");
- if (!btn)
+ var button = root.querySelector(".theme-toggle");
+ if (!button)
return;
- apply(theme, btn);
- btn.hidden = false;
- btn.addEventListener("click", function () {
+ 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, btn);
+ apply(theme, button);
});
}, false);