/* * 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); })();