We had an hardcoded mobile banner that was nice looking but couldn't deal with all the content for the various banners we want to display. Also, it was a separate element to the desktop one. Unify them as a single element that adapts (as it should) to desktop or mobile viewports.
391 lines
12 KiB
JavaScript
391 lines
12 KiB
JavaScript
"use strict";
|
|
|
|
/* Tags menu filter */
|
|
|
|
// Get a dictionary of tag name -> tag link from HTML
|
|
function getTags() {
|
|
const tags = {};
|
|
const list = document.querySelectorAll('.versions a');
|
|
for (const el of list) {
|
|
tags[el.innerText.trim()] = el.href;
|
|
}
|
|
return tags;
|
|
}
|
|
|
|
// Generate tag search results based on input
|
|
// filter: current filter input text
|
|
// tags: dictionary of tag name -> tag link
|
|
function generateResults(filter, tags) {
|
|
const searchResults = document.createDocumentFragment();
|
|
const filterRegex = new RegExp(filter, 'i');
|
|
|
|
for (let key in tags) {
|
|
if (tags.hasOwnProperty(key)) {
|
|
let tagFound = false;
|
|
const tagHighlight = key.replace(filterRegex, result => {
|
|
if (result) tagFound = true;
|
|
return '<strong>' + result + '</strong>';
|
|
})
|
|
|
|
if (tagFound) {
|
|
const tagLink = document.createElement('a');
|
|
tagLink.href = tags[key];
|
|
tagLink.innerHTML = tagHighlight;
|
|
searchResults.appendChild(tagLink);
|
|
}
|
|
}
|
|
}
|
|
|
|
return searchResults;
|
|
}
|
|
|
|
// Setup tags filter input
|
|
function setupVersionsFilter() {
|
|
const input = document.querySelector('.filter-input');
|
|
const results = document.querySelector('.filter-results');
|
|
const versions = document.querySelector('.versions');
|
|
const tags = getTags();
|
|
|
|
input.addEventListener('input', e => {
|
|
if (e.target.value === '') {
|
|
versions.classList.remove('hide');
|
|
results.innerHTML = '';
|
|
} else {
|
|
versions.classList.add('hide');
|
|
results.innerHTML = '';
|
|
results.appendChild(generateResults(e.target.value, tags));
|
|
}
|
|
});
|
|
}
|
|
|
|
// Setup expanding/collapsing versions tree on click
|
|
function setupVersionsTree() {
|
|
const versions = document.querySelector('.versions');
|
|
versions.addEventListener('click', e => {
|
|
if (e.target && e.target.nodeName == 'SPAN') {
|
|
e.target.classList.toggle('active')
|
|
}
|
|
});
|
|
}
|
|
|
|
function isWidescreen() {
|
|
return getComputedStyle(document.documentElement).getPropertyValue('--is-widescreen') === 'true';
|
|
}
|
|
|
|
// Toggles sidebar visibility, handles widescreen and mobile layouts
|
|
function toggleMenu() {
|
|
const isWidescreen = getComputedStyle(document.documentElement).getPropertyValue('--is-widescreen') === 'true';
|
|
if(isWidescreen) {
|
|
const hasShowMenu = document.documentElement.classList.contains('show-menu');
|
|
window.localStorage.setItem('show-sidebar', !hasShowMenu);
|
|
document.documentElement.classList.toggle('show-menu');
|
|
} else {
|
|
document.documentElement.classList.toggle('show-menu-mobile');
|
|
}
|
|
}
|
|
|
|
// Setup sidebar hamburger menu button, close button and mobile sidebar backdrop events
|
|
function setupSidebarSwitch() {
|
|
const tag = document.querySelector('.version em');
|
|
const openMenu = document.querySelector('.open-menu');
|
|
const sidebar = document.querySelector('.sidebar');
|
|
|
|
// toggle on hamburger menu click
|
|
openMenu.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
toggleMenu();
|
|
});
|
|
|
|
// toggle on footer tag icon click
|
|
tag.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
toggleMenu();
|
|
});
|
|
|
|
// close on close-menu/backdrop click
|
|
sidebar.addEventListener('click', e => {
|
|
if (e.target === sidebar && isWidescreen()) {
|
|
document.documentElement.classList.remove('show-menu');
|
|
window.localStorage.setItem('show-sidebar', false);
|
|
} else if (e.target === sidebar || e.target.classList.contains('close-menu')) {
|
|
document.documentElement.classList.remove('show-menu-mobile');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Parse and validate line identifier in format L${number}
|
|
function parseLineId(lineId) {
|
|
if (lineId[0] != "L") {
|
|
return;
|
|
}
|
|
|
|
let lineIdNum = parseInt(lineId.substring(1));
|
|
console.assert(!isNaN(lineIdNum), "Invalid line id");
|
|
|
|
let lineElement = document.getElementById(lineId);
|
|
if (lineElement === null || lineElement.tagName !== "A") {
|
|
return;
|
|
}
|
|
|
|
return lineIdNum;
|
|
}
|
|
|
|
// Parse and validate line range anchor in format #L${lineRangeStart}-L${lineRangeEnd}
|
|
function parseLineRangeAnchor(hashStr) {
|
|
const hash = hashStr.substring(1).split("-");
|
|
if (hash.length < 1 || hash.length > 2) {
|
|
return;
|
|
}
|
|
|
|
let firstLine = parseLineId(hash[0]);
|
|
let lastLine = hash.length === 2 ? parseLineId(hash[1]) : firstLine;
|
|
|
|
if (firstLine === undefined || lastLine === undefined) {
|
|
return;
|
|
}
|
|
|
|
// Swap line numbers to support "#L2-L1" format. Postel's law.
|
|
if (firstLine > lastLine) {
|
|
const lineTmp = lastLine;
|
|
lastLine = firstLine;
|
|
firstLine = lineTmp;
|
|
}
|
|
|
|
return [firstLine, lastLine];
|
|
}
|
|
|
|
// Highlights line number elements from firstLine to lastLine
|
|
function highlightFromTo(firstLine, lastLine) {
|
|
const firstLineElement = document.getElementById(`L${ firstLine }`);
|
|
const lastLineElement = document.getElementById(`L${ lastLine }`);
|
|
|
|
const firstCodeLine = document.getElementById(`codeline-${ firstLine }`);
|
|
const lastCodeLine = document.getElementById(`codeline-${ lastLine }`);
|
|
|
|
addClassToRangeOfElements(firstLineElement.parentNode, lastLineElement.parentNode, "line-highlight");
|
|
addClassToRangeOfElements(firstCodeLine, lastCodeLine, "line-highlight");
|
|
}
|
|
|
|
function clearRangeHighlight() {
|
|
const highlightElements = Array.from(document.getElementsByClassName("line-highlight"));
|
|
for (let el of highlightElements) {
|
|
el.classList.remove("line-highlight");
|
|
}
|
|
}
|
|
|
|
function addClassToRangeOfElements(first, last, class_name) {
|
|
let element = first;
|
|
const elementAfterLast = last !== null ? last.nextElementSibling : null;
|
|
while (element !== null && element != elementAfterLast) {
|
|
element.classList.add(class_name);
|
|
element = element.nextElementSibling;
|
|
}
|
|
}
|
|
|
|
// Sets up listeners on element that contains line numbers to handle
|
|
// shift-clicks for range highlighting
|
|
function setupLineRangeHandlers() {
|
|
// Check if page contains the element with line numbers
|
|
// If not, then likely script is not executed in context of the source page
|
|
const linenodiv = document.querySelector(".linenodiv");
|
|
if (linenodiv === null) {
|
|
return;
|
|
}
|
|
|
|
let rangeStartLine, rangeEndLine;
|
|
|
|
const parseFromHash = () => {
|
|
const highlightedRange = parseLineRangeAnchor(window.location.hash);
|
|
// Set range start/end to elements from hash
|
|
if (highlightedRange !== undefined) {
|
|
rangeStartLine = highlightedRange[0];
|
|
rangeEndLine = highlightedRange[1];
|
|
highlightFromTo(rangeStartLine, rangeEndLine);
|
|
document.getElementById(`L${rangeStartLine}`).scrollIntoView();
|
|
} else if (location.hash !== "" && location.hash[1] === "L") {
|
|
rangeStartLine = parseLineId(location.hash.substring(1));
|
|
}
|
|
}
|
|
|
|
window.addEventListener("hashchange", _ => {
|
|
clearRangeHighlight();
|
|
parseFromHash();
|
|
});
|
|
|
|
parseFromHash();
|
|
|
|
linenodiv.addEventListener("click", ev => {
|
|
if (ev.ctrlKey || ev.metaKey) {
|
|
return;
|
|
}
|
|
ev.preventDefault();
|
|
|
|
// Handler is set on the element that contains all line numbers, check if the
|
|
// event is directed at an actual line number element
|
|
const el = ev.target;
|
|
if (typeof(el.id) !== "string" || el.id[0] !== "L" || el.tagName !== "A") {
|
|
return;
|
|
}
|
|
|
|
clearRangeHighlight();
|
|
|
|
if (rangeStartLine === undefined || !ev.shiftKey) {
|
|
rangeStartLine = parseLineId(el.id);
|
|
rangeEndLine = undefined;
|
|
highlightFromTo(rangeStartLine, rangeStartLine);
|
|
window.location.hash = el.id;
|
|
} else if (ev.shiftKey) {
|
|
if (rangeEndLine === undefined) {
|
|
rangeEndLine = parseLineId(el.id);
|
|
}
|
|
|
|
const newLine = parseLineId(el.id);
|
|
console.assert(newLine !== undefined, "parseLineId for clicked line is undefined");
|
|
|
|
// Swap range elements if range end that was previously undefined is now
|
|
// before range start
|
|
if (rangeStartLine > rangeEndLine) {
|
|
const lineTmp = rangeStartLine;
|
|
rangeStartLine = rangeEndLine;
|
|
rangeEndLine = lineTmp;
|
|
}
|
|
|
|
if (newLine < rangeStartLine) {
|
|
// Expand if element above range
|
|
rangeStartLine = newLine;
|
|
} else if (newLine > rangeEndLine) {
|
|
// Expand if element below range
|
|
rangeEndLine = newLine;
|
|
} else {
|
|
// Shrink moving the edge that's closest to the selection.
|
|
// Move end if center was selected.
|
|
const distanceFromStart = Math.abs(rangeStartLine-newLine);
|
|
const distanceFromEnd = Math.abs(rangeEndLine-newLine);
|
|
if (distanceFromStart < distanceFromEnd) {
|
|
rangeStartLine = newLine;
|
|
} else {
|
|
rangeEndLine = newLine;
|
|
}
|
|
}
|
|
|
|
highlightFromTo(rangeStartLine, rangeEndLine);
|
|
window.location.hash = `L${rangeStartLine}-L${rangeEndLine}`;
|
|
}
|
|
});
|
|
}
|
|
|
|
/* Other fixes */
|
|
|
|
// prevent chrome from auto-scrolling to input elements
|
|
function setupAutoscrollingPrevention() {
|
|
const wrapper = document.querySelector('.wrapper');
|
|
Array.prototype.forEach.call(document.querySelectorAll('input'), el => {
|
|
el.addEventListener('keydown', _ => {
|
|
const before = wrapper.scrollTop;
|
|
const reset = () => wrapper.scrollTop = before;
|
|
window.requestAnimationFrame(reset);
|
|
setTimeout(reset, 0);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Scrolls the page after each anchor change to prevent selected line from
|
|
// hiding under the topbar after a line number click.
|
|
function setupAnchorOffsetHandler() {
|
|
const wrapper = document.querySelector('.wrapper');
|
|
|
|
const anchorChangeHandler = e => {
|
|
if (e && e.preventDefault) e.preventDefault();
|
|
if (location.hash.length !== 0) {
|
|
const el = document.querySelector(location.hash);
|
|
if (el) {
|
|
const offsetTop = el.offsetTop;
|
|
wrapper.scrollTop = offsetTop < 100 ? 200 : offsetTop + 100;
|
|
}
|
|
}
|
|
};
|
|
|
|
window.requestAnimationFrame(anchorChangeHandler);
|
|
window.addEventListener('hashchange', anchorChangeHandler);
|
|
}
|
|
|
|
function setupGoToTop() {
|
|
const wrapper = document.querySelector('.wrapper');
|
|
const goToTop = document.querySelector('.go-top');
|
|
|
|
goToTop.addEventListener('click', e => {
|
|
wrapper.scrollTop = 0;
|
|
wrapper.scrollLeft = 0;
|
|
});
|
|
}
|
|
|
|
function randomChoice(arr) {
|
|
return arr[Math.floor(Math.random() * arr.length)];
|
|
}
|
|
|
|
function updateBannerContents(msg) {
|
|
const banner = document.querySelector('.message-banner');
|
|
|
|
banner.querySelector('.title').innerText = msg.title;
|
|
banner.querySelector('.subtitle').innerHTML = msg.body.replace('\n', '<br/>');
|
|
document.querySelector('.message-link').href = msg.link;
|
|
|
|
const actionElement = banner.querySelector('.action');
|
|
const actionInner = actionElement.querySelector('.action-inner');
|
|
if (msg.action) {
|
|
actionInner.innerText = msg.action;
|
|
actionElement.classList.add("action-visible");
|
|
} else {
|
|
actionElement.classList.remove("action-visible");
|
|
}
|
|
}
|
|
|
|
function updateMessageBanner() {
|
|
fetch('/static/messages.json')
|
|
.then(r => r.json())
|
|
.then(messages => {
|
|
// TODO compatibility with old messages format, remove after ~march 2025
|
|
const pickedMsg = randomChoice(messages);
|
|
const msg = pickedMsg.desktop ? pickedMsg.desktop : pickedMsg;
|
|
|
|
updateBannerContents(msg);
|
|
});
|
|
}
|
|
|
|
function cycleBanner(delay=500) {
|
|
fetch('/static/messages.json')
|
|
.then(r => r.json())
|
|
.then(messages => {
|
|
cycleBannerWithData(messages, delay);
|
|
});
|
|
}
|
|
|
|
function sleep(duration) {
|
|
return new Promise(resolve => setTimeout(resolve, duration));
|
|
}
|
|
|
|
async function cycleBannerWithData(data, delay=500) {
|
|
for (const msg of data) {
|
|
updateBannerContents(msg);
|
|
await sleep(delay);
|
|
}
|
|
console.log("cycle finished");
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', _ => {
|
|
updateMessageBanner();
|
|
|
|
setupVersionsFilter();
|
|
setupVersionsTree();
|
|
setupSidebarSwitch();
|
|
|
|
setupLineRangeHandlers();
|
|
|
|
setupAutoscrollingPrevention();
|
|
setupAnchorOffsetHandler();
|
|
setupGoToTop();
|
|
|
|
document.getElementById('clear-search').addEventListener('click',
|
|
_ => document.getElementById('search-input').value = ''
|
|
);
|
|
});
|