"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 '' + result + ''; }) 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', '
'); 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 = '' ); });