UNPKG

@canalplus/readme.doc

Version:

Readme's an Extremely Accessible Documentation MakEr

1,194 lines (1,095 loc) 37.1 kB
let rootUrl = window.rootUrl; { // From relative to absolute URL const a = document.createElement("a"); a.href = rootUrl; rootUrl = a.href; if (rootUrl[rootUrl.length - 1] === "/") { rootUrl = rootUrl.substring(0, rootUrl.length - 1); } } /** Information on the initialization of the search feature. */ const searchState = { /** * Status of the search initialization (loading + building of the index). * Can be: * 1. "not-loaded": when... not loaded. * 2. "loading": when in the process of being loaded (e.g. the index request * is pending). * 3. "loaded": Search is initialized with success. This is the only state * under which search operations can be performed. * 4. "failed": Search initialization failed. * @type {string} */ initStatus: "not-loaded", /** * Promise resolving when search is initialized (in the "loaded" status) and * rejecting when search initialization fails (in the "failed" status). * Set to `null` if search initialization never began yet. */ promise: null, /** * Last searched text. * `null` if nothing was yet searched in the current searching session. * @type {string|null} */ lastSearch: null, }; /** * Absolute URL to the page that is currently wanted to be displayed. * `null` if we're not wanting to load any other page. */ let currentlyWantedPage = null; let fuse = null; /** * Contains the links to search results. * The elements of this array are in this same order than `Fuse.js`'s index. * @type {Array.<Object>} */ let searchIndexLinks = []; /** If `true` the header on the top of the page is currently visible. */ let isHeaderShown = true; /** Map linking the URL accessible through the sidebar to the sidebar element itself. */ let sidebarLinksToLinkElements = new Map(); /** Associate URLs accessible through the sidebar to the corresponding HTML content. */ const linksCache = []; /** * Set timeout after which we will automatically ask the browser to load a * given URL if "soft navigation" it takes too much time. */ let currentDisplayTimeout = null; /** * Initialize all dynamic behaviors on the page. * * The returned callback allows to free all reserved resources, mainly to allow * re-initialize the page from a clean state. */ let removeListeners = initializePage(); const spinnerSvg = `<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="spinner-svg" > <path d="M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z" class="spinner" /> </svg>`; const pageIndex = []; /** * Perform all actions and register all event handlers that should be done in * the page. */ function initializePage() { currentlyWantedPage = null; searchState.lastSearch = null; const deInitSearchIcons = initializeSearchIcons(); const deInitSearchBar = initializeSearchBar(); const deInitPageGroups = initializePageGroups(); const deInitHamburgerMenu = initializeHamburgerMenu(); initializeHeaderLinks(); const deInitSidebarLinks = initializeSideBarLinks(); const deInitContentLinks = initializeContentLinks(); performSearchInUrlIfOne(); scrollInSidebarToSeeActiveElement(); const deInitScrollBehavior = initializeScrollBehavior(); window.addEventListener("popstate", onPopState); // Work-arounds to make sure the header doesn't go on top // of a link if (window.location.hash !== "" && window.scrollY > 0) { hideHeader(); } return function () { deInitSearchIcons(); deInitSearchBar(); deInitPageGroups(); deInitHamburgerMenu(); deInitScrollBehavior(); deInitSidebarLinks(); deInitContentLinks(); window.removeEventListener("popstate", onPopState); }; } /** * Initialize behavior allowing to show or hide the header depending on the * scroll position and on anchors. * @returns {Function} - Function to remove all event listeners registered in * that function. */ function initializeScrollBehavior() { let prevScroll = window.scrollY; window.addEventListener("scroll", onScroll); window.addEventListener("hashchange", onHashChange); return function () { window.removeEventListener("scroll", onScroll); window.removeEventListener("hashchange", onHashChange); }; function onScroll() { const curScroll = window.scrollY; if (curScroll === 0) { showHeader(); return; } if (Math.abs(curScroll - prevScroll) < 5) { return; } if (curScroll > prevScroll) { hideHeader(); } else if (curScroll < prevScroll) { showHeader(); } prevScroll = curScroll; } function onHashChange() { if (window.scrollY !== 0) { hideHeader(); } prevScroll = window.scrollY; } } /** * Add or replace `onclick` handlers to the search icons on the page. * @returns {Function} - Function to remove all event listeners registered in * that function. */ function initializeSearchIcons() { const eventListenerRemovers = []; const searchParams = new URLSearchParams(window.location.search); const searchBarElt = getSearchBarElement(); const searchResultsElt = getSearchResultElement(); const searchIconElts = getSearchIconElements(); for (let i = searchIconElts.length - 1; i >= 0; i--) { searchIconElts[i].classList.remove("active"); } const searchWrapperElt = getSearchWrapperElement(); if (searchIconElts.length > 0) { for (let i = searchIconElts.length - 1; i >= 0; i--) { const searchIconElt = searchIconElts[i]; searchIconElt.addEventListener("click", onClick); eventListenerRemovers.push(function () { searchIconElt.removeEventListener("click", onClick); }); } } return function () { eventListenerRemovers.forEach((cb) => cb()); }; function onClick() { searchState.lastSearch = null; searchResultsElt.innerHTML = ""; searchBarElt.value = ""; if (searchWrapperElt.classList.contains("active")) { // Search already enabled: now disable searchParams.delete("search"); history.replaceState(null, null, window.location.href.split("?")[0]); searchWrapperElt.classList.remove("active"); for (let i = searchIconElts.length - 1; i >= 0; i--) { searchIconElts[i].classList.remove("active"); } } else { if (searchState.initStatus === "not-loaded") { initializeSearchEngine().then(function () { updateSearchResults(searchBarElt.value); }); } window.scrollTo(0, 0); searchWrapperElt.classList.add("active"); for (let i = searchIconElts.length - 1; i >= 0; i--) { searchIconElts[i].classList.add("active"); } searchBarElt.focus(); updateSearchResults(searchBarElt.value); } } } /** * Register callback to display search results when text is entered in the * search bar. * @returns {Function} - Function to remove all event listeners registered in * that function. */ function initializeSearchBar() { const searchParams = new URLSearchParams(window.location.search); const searchBarElt = getSearchBarElement(); if (searchBarElt === null) { return function () {}; } searchBarElt.addEventListener("input", onInput); return function () { searchBarElt.removeEventListener("input", onInput); }; function onInput() { searchParams.set("search", searchBarElt.value); history.replaceState(null, null, "?" + searchParams.toString()); updateSearchResults(searchBarElt.value); if (searchState.initStatus !== "loaded") { initializeSearchEngine().then(function () { updateSearchResults(searchBarElt.value); }); } } } /** * If the URL contains a search param, display results of the search in the * page. */ function performSearchInUrlIfOne() { const searchParams = new URLSearchParams(window.location.search); const searchBarElt = getSearchBarElement(); const searchIconElts = getSearchIconElements(); const searchWrapperElt = getSearchWrapperElement(); const initialSearch = searchParams.get("search"); if (initialSearch !== null) { searchWrapperElt.classList.add("active"); for (let i = searchIconElts.length - 1; i >= 0; i--) { searchIconElts[i].classList.add("active"); } searchBarElt.value = initialSearch; searchBarElt.focus(); searchBarElt.selectionStart = searchBarElt.selectionEnd = searchBarElt.value.length; updateSearchResults(searchBarElt.value); if (searchState.initStatus !== "loaded") { initializeSearchEngine().then(function () { updateSearchResults(searchBarElt.value); }); } } } /** * Open the active page group, closes the others and register listener to * smoothly open and close them on click. * @returns {Function} - Function to remove all event listeners registered in * that function. */ function initializePageGroups() { /** Elements corresponding each to a sidebar's group of pages */ const sidebarGroupElts = document.getElementsByClassName("sidebar-item-group"); const pageListGroupElts = document.getElementsByClassName("page-list-group"); const eventListenerRemovers = []; const groupElts = []; for (let i = 0; i < sidebarGroupElts.length; i++) { groupElts.push(sidebarGroupElts[i]); } for (let i = 0; i < pageListGroupElts.length; i++) { groupElts.push(pageListGroupElts[i]); } for (let groupElt of groupElts) { const wrapper = groupElt.parentElement; const ulElt = wrapper.getElementsByTagName("ul")[0]; const pageGroupElt = ulElt.previousSibling; let status = "closed"; let openingTimeout; let closingTimeout; const sidebarLinkElements = groupElt.parentElement.getElementsByClassName("sidebar-link"); for (const sidebarLinkElement of sidebarLinkElements) { if (sidebarLinkElement.classList.contains("active")) { pageGroupElt.classList.add("opened"); break; } } if (ulElt !== undefined) { if (pageGroupElt.classList.contains("opened")) { // already opened sidebar group status = "opened"; ulElt.style.display = "block"; ulElt.style.height = `auto`; } ulElt.addEventListener("transitionend", onTransitionEnd); eventListenerRemovers.push(function () { ulElt.removeEventListener("transitionend", onTransitionEnd); }); } groupElt.addEventListener("click", onClick); eventListenerRemovers.push(function () { groupElt.removeEventListener("click", onClick); }); function onClick() { if (pageGroupElt.classList.contains("opened")) { progressivelyHideElt(); } else { progressivelyDisplayElt(); } } function progressivelyDisplayElt() { clearTimeout(openingTimeout); clearTimeout(closingTimeout); pageGroupElt.classList.add("opened"); if (ulElt === undefined) { return; } ulElt.style.display = "block"; ulElt.style.height = "auto"; const height = ulElt.offsetHeight; ulElt.style.height = "0px"; const transitionDuration = getTransitionDuration(height); ulElt.style.transition = `height ${transitionDuration}ms ease-in-out 0s`; openingTimeout = setTimeout(function () { status = "opening"; ulElt.style.height = `${height}px`; }); } function progressivelyHideElt() { clearTimeout(openingTimeout); clearTimeout(closingTimeout); pageGroupElt.classList.remove("opened"); if (ulElt === undefined) { return; } const height = ulElt.offsetHeight; ulElt.style.height = `${height}px`; closingTimeout = setTimeout(function () { status = "closing"; const transitionDuration = getTransitionDuration(ulElt.offsetHeight); ulElt.style.transition = `height ${transitionDuration}ms ease-in-out 0s`; ulElt.style.height = "0px"; }); } function onTransitionEnd() { if (status === "closing") { ulElt.style.transition = ""; ulElt.style.display = "none"; ulElt.style.height = "auto"; status = "closed"; } else if (status === "opening") { status = "opened"; ulElt.style.height = "auto"; } } function getTransitionDuration(height) { return Math.max(300, height / 2); } } return function () { eventListenerRemovers.forEach((cb) => cb()); }; } /** * Loads the search engine's index and start initializing it. * Can be performed lazily to avoid loading loading the huge index if noone * searched anything. * * @returns {Promise} - Resolved when if the search engine has been initialized * with success and it is now possible to perform search. * Reject if it fails. */ function initializeSearchEngine() { if (searchState.promise !== null) { return searchState.promise; } searchState.initStatus = "loading"; searchState.promise = fetch(rootUrl + "/searchIndex.json") .then((res) => res.json()) .then((res) => { if (!Array.isArray(res)) { console.error( "Failed to initialize search: index has an invalid format.", ); return; } searchIndexLinks = []; let id = 0; for (let i = 0; i < res.length; i++) { for (let j = 0; j < res[i].index.length; j++) { const elt = res[i].index[j]; searchIndexLinks.push({ file: res[i].file, anchorH1: elt.anchorH1, anchorH2: elt.anchorH2, anchorH3: elt.anchorH3, }); pageIndex.push({ file: res[i].file, h1: elt.h1, h2: elt.h2, h3: elt.h3, body: elt.body, id: id, }); id++; } } fuse = new Fuse(pageIndex, { // includeScore: true, keys: [ { name: "h1", weight: 100, }, { name: "h2", weight: 80, }, { name: "h3", weight: 10, }, { name: "body", weight: 1, }, ], }); searchState.initStatus = "loaded"; }) .catch((err) => { searchState.initStatus = "failed"; console.error("Could not initialize search:", err); }); return searchState.promise; } // Symbol are guaranteed to be unique. // So there will be no overlap between this symbol or any string // when it's used as a key to access an object property. const __DEFAULT_SYMBOL__ = Symbol("__DEFAULT__"); /** * Update search result in the search result HTMLElement according to the given * value. * @param {string} value */ function updateSearchResults(value) { const searchResultsElt = getSearchResultElement(); if (value === searchState.lastSearch) { return; } if (value === "") { searchState.lastSearch = ""; searchResultsElt.innerHTML = '<div class="message">' + "Enter text to search in all documentation pages." + "</div>"; return; } if (searchState.initStatus === "not-loaded") { searchState.lastSearch = null; searchResultsElt.innerHTML = '<div class="message">' + "Loading the search index..." + "</div>"; return; } if (searchState.initStatus === "failed") { searchState.lastSearch = null; searchResultsElt.innerHTML = '<div class="message">' + "Error: an error happened while initializing the search index" + "</div>"; return; } searchState.lastSearch = value; const searchResults = fuse.search(value); if (searchResults.length === 0) { searchResultsElt.innerHTML = '<div class="message">' + "No result for that search." + "</div>"; return; } // only consider the first 30 results const searchResultsSliced = searchResults.slice(0, 29); const searchResultSorted = reorderSearchResults(searchResultsSliced); searchResultsElt.innerHTML = ""; let previousItem = null; for (const searchResult of searchResultSorted) { let displayFromLevel = previousItem === null ? 0 : getCommonAncestorLevel(searchResult, previousItem); previousItem = searchResult; const elems = createHeadingElements(searchResult, displayFromLevel); elems.forEach((elem) => searchResultsElt.appendChild(elem)); } } /** * Compares two search result items to determine the deepest common ancestor level. * * @param {Object} itemA - The first search result item. * @param {Object} itemB - The second search result item. * @returns {number} - Returns 0 if items does not share same file, 1 if h1 is different, etc... */ function getCommonAncestorLevel(searchResultItemA, searchResultItemB) { if (searchResultItemA.file !== searchResultItemB.file) { return 0; } if (searchResultItemA.h1 !== searchResultItemB.h1) { return 1; } if (searchResultItemA.h2 !== searchResultItemB.h2) { return 2; } return 3; } /** * Re-orders search results by grouping them according * to their shared section headings (h1, h2, h3). * @param {array} searchResults The array of search results to be re-ordered. * @returns An array re-ordered based on section headings. * * @example */ function reorderSearchResults(searchResults) { const groupedSearchResult = groupItems(searchResults); return flattenGroupedItems(groupedSearchResult); } /** * Groups items from the provided `results` array by their file, h1, h2, and h3 properties. * If a property is missing at any level, it defaults to using the `__DEFAULT__` symbol. * Items are nested based on their structure, creating keys for each * unique combination of `file`, `h1`, `h2`, and `h3`. * * @param {array} array The array to group. * @returns An object containing items grouped. */ function groupItems(results) { const groupedByFileAndHeader = {}; results.forEach((res) => { let item = res.item; if (item.file === undefined || item.h1 === undefined) { // item.file and item.h1 is required, if it's missing let's skip the search result. return; } if (!groupedByFileAndHeader[item.file]) { groupedByFileAndHeader[item.file] = {}; } if (!groupedByFileAndHeader[item.file][item.h1]) { groupedByFileAndHeader[item.file][item.h1] = {}; } // Create the h2 key, if exists if (item.h2) { if (!groupedByFileAndHeader[item.file][item.h1][item.h2]) { groupedByFileAndHeader[item.file][item.h1][item.h2] = {}; } // Create the h3 key, if exists if (item.h3) { if (!groupedByFileAndHeader[item.file][item.h1][item.h2][item.h3]) { groupedByFileAndHeader[item.file][item.h1][item.h2][item.h3] = []; } groupedByFileAndHeader[item.file][item.h1][item.h2][item.h3].push(item); } else { // If no h3, use the default symbol if ( !groupedByFileAndHeader[item.file][item.h1][item.h2][ __DEFAULT_SYMBOL__ ] ) { groupedByFileAndHeader[item.file][item.h1][item.h2][ __DEFAULT_SYMBOL__ ] = []; } groupedByFileAndHeader[item.file][item.h1][item.h2][ __DEFAULT_SYMBOL__ ].push(item); } } else { // If no h2, use the default symbol under h1 if (!groupedByFileAndHeader[item.file][item.h1][__DEFAULT_SYMBOL__]) { groupedByFileAndHeader[item.file][item.h1][__DEFAULT_SYMBOL__] = []; } groupedByFileAndHeader[item.file][item.h1][__DEFAULT_SYMBOL__].push(item); } }); return groupedByFileAndHeader; } /** * From a nested object contain search results returns an array with all * the search items. */ function flattenGroupedItems(groupedByFileAndHeader) { const flattenedItems = []; // Helper function to recursively traverse the nested structure function traverse(group) { // If the group is an array, it means there is no deeper level. if (Array.isArray(group)) { flattenedItems.push(...group); } else if (group !== null && typeof group === "object") { if (group[__DEFAULT_SYMBOL__] !== undefined) { traverse(group[__DEFAULT_SYMBOL__]); } // Object.keys() does not iterate through Symbol, so the __DEFAULT_SYMBOL__ // key will not be handled here. for (const key of Object.keys(group)) { traverse(group[key]); } } } // Start traversing from the top-level grouping for (const file in groupedByFileAndHeader) { for (const h1 in groupedByFileAndHeader[file]) { traverse(groupedByFileAndHeader[file][h1]); } } return flattenedItems; } /** * Scroll if necessary the sidebar so that the "active" link is visible. */ function scrollInSidebarToSeeActiveElement() { const sidebarLinkElements = document.getElementsByClassName("sidebar-link"); for (const sidebarLinkElement of sidebarLinkElements) { if (sidebarLinkElement.classList.contains("active")) { const activeRect = sidebarLinkElement.getBoundingClientRect(); if (activeRect.y + activeRect.height + 20 > window.innerHeight) { const sidebarWrapperElt = document.getElementsByClassName("sidebar-wrapper")[0]; const scrollYBy = activeRect.y + activeRect.height - window.innerHeight + 50; sidebarWrapperElt.scrollTo(0, sidebarWrapperElt.scrollTop + scrollYBy); } return; } } } /** * Hide the header on top of the page. */ function hideHeader() { const headerElt = document.getElementsByClassName("navbar-parent")[0]; if (isHeaderShown) { headerElt.classList.add("hidden"); isHeaderShown = false; } } /** * Show the header on top of the page. */ function showHeader() { const headerElt = document.getElementsByClassName("navbar-parent")[0]; if (!isHeaderShown) { headerElt.classList.remove("hidden"); isHeaderShown = true; } } /** * Initialize logic to open or close the sidebar overlay when the hamburger menu * is clicked (on smaller form factors). * @returns {Function} - Function to remove all event listeners registered in * that function. */ function initializeHamburgerMenu() { let opacityTimeout; const overlay = document.createElement("div"); overlay.className = "overlay"; const hamburgerOpenerElt = document.getElementsByClassName("hamburger-opener")[0]; const hamburgerBarElt = document.getElementsByClassName("hamburger-bar")[0]; const hamburgerCloserElt = document.getElementsByClassName( "hamburger-bar-closer", )[0]; hamburgerOpenerElt.addEventListener("click", openMenu); hamburgerCloserElt.addEventListener("click", closeMenu); let isOverlayVisible = false; overlay.addEventListener("click", onOverlayClick); function onOverlayClick() { if (isOverlayVisible) { closeMenu(); } } return function () { clearTimeout(opacityTimeout); hamburgerOpenerElt.removeEventListener("click", openMenu); hamburgerCloserElt.removeEventListener("click", closeMenu); overlay.removeEventListener("click", onOverlayClick); }; function openMenu() { clearTimeout(opacityTimeout); document.body.style.overflowY = "hidden"; document.body.appendChild(overlay); opacityTimeout = setTimeout(function () { overlay.style.opacity = "1"; isOverlayVisible = true; }); hamburgerBarElt.classList.add("opened"); } function closeMenu() { clearTimeout(opacityTimeout); document.body.style.overflowY = "auto"; overlay.style.opacity = "0"; isOverlayVisible = false; document.body.removeChild(overlay); hamburgerBarElt.classList.remove("opened"); } } /** * Links in the navigation bar usually are relative URL. * * Because we may navigate through paths here, we force them to be absolute URLs * only. */ function initializeHeaderLinks() { const headerElt = document.getElementsByClassName("navbar-parent")[0]; const headerLinks = headerElt?.getElementsByTagName("a") ?? []; for (const link of headerLinks) { // Transform from relative to absolute URL link.href = link.href; } } /** * Initialize the "soft navigation" of pages when clicking on one of the links in * the sidebar. * @returns {Function} - Function to remove all event listeners registered in * that function. */ function initializeSideBarLinks() { const sidebar = document.getElementsByClassName("sidebar-items")[0]; const eventListenerRemovers = []; sidebarLinksToLinkElements.clear(); const sidebarLinks = sidebar?.getElementsByTagName("a") ?? []; for (const link of sidebarLinks) { // Transform from relative to absolute URL link.href = link.href; sidebarLinksToLinkElements.set(link.href, link); link.addEventListener("click", onClick); link.addEventListener("mouseover", onMouseOver); eventListenerRemovers.push(function () { link.removeEventListener("click", onClick); link.removeEventListener("mouseover", onMouseOver); }); function onMouseOver() { loadSidebarLink(link, { display: false, updateURL: false }); } function onClick(evt) { evt.preventDefault(); loadSidebarLink(link, { display: true, updateURL: true }); } } return function () { eventListenerRemovers.forEach((cb) => cb()); }; } /** * Initialize the "soft navigation" of pages when clicking on one of the links in * the page referring to a link accessible through the sidebar. * @returns {Function} - Function to remove all event listeners registered in * that function. */ function initializeContentLinks() { const eventListenerRemovers = []; /** * Element containing the documentation page as well as potential search results * and the search bar. */ const currentContentElt = document.getElementsByClassName("content-wrapper")[0]; /** Every links contained both in the documentation page and in search result. */ const currentContentLinkElts = currentContentElt.getElementsByTagName("a"); for (const contentLink of currentContentLinkElts) { const correspondingElt = sidebarLinksToLinkElements.get(contentLink.href); if (correspondingElt !== undefined) { contentLink.addEventListener("click", onClick); contentLink.addEventListener("mouseover", onMouseOver); eventListenerRemovers.push(function () { contentLink.removeEventListener("click", onClick); contentLink.removeEventListener("mouseover", onMouseOver); }); function onClick(evt) { evt.preventDefault(); loadSidebarLink(correspondingElt, { display: true, updateURL: true, }); } function onMouseOver() { loadSidebarLink(correspondingElt, { display: false, updateURL: false, }); } } } return function () { eventListenerRemovers.forEach((cb) => cb()); }; } /** * Begin "soft navigation" of an URL linked to a sidebar link. * @param {HTMLLinkElement} link - The sidebar `HTMLLinkElement` whose link will * be loaded. * @param {Object} opt * @param {boolean} opt.display - If `true` the page will be displayed right * after being loaded. If `false`, it is just being pre-loaded for now. * @param {boolean} opt.updateURL - If `true` the page be added to the * history of the current window and the current URL will be replaced once the * page is loaded. * @returns {Promise} - Promise resolving when the page is loaded. */ function loadSidebarLink(link, { display, updateURL }) { for (const cached of linksCache) { if (cached[0] === link.href) { if (display) { currentlyWantedPage = link.href; setCurrentDisplayTimeout(link.href); } return cached[1].then(function (newContent) { if (updateURL) { history.pushState(null, null, link.href); } if (display) { prepareNextPage(link); displayContent(newContent, true); } }); } } if (display) { currentlyWantedPage = link.href; setCurrentDisplayTimeout(link.href); } return loadContentFrom(link, display, true); } function prepareNextPage(link) { const sidebar = document.getElementsByClassName("sidebar-items")[0]; const actives = sidebar.getElementsByClassName("active"); for (let i = actives.length - 1; i >= 0; i--) { actives[i].classList.remove("active"); } /** Elements corresponding each to a sidebar's group of pages */ const sidebarGroupElts = document.getElementsByClassName("sidebar-item-group"); link.classList.add("active"); let parentElement = link.parentElement; while ( parentElement != null && !parentElement.classList.contains("sidebar-items") ) { if (parentElement.classList.contains("sidebar-item-group")) { parentElement.classList.add("active"); parentElement.classList.add("opened"); } parentElement = parentElement.parentElement; } const pageListGroupElts = document.getElementsByClassName("page-list-group"); const groupElts = []; for (let i = 0; i < sidebarGroupElts.length; i++) { groupElts.push(sidebarGroupElts[i]); } for (let i = 0; i < pageListGroupElts.length; i++) { groupElts.push(pageListGroupElts[i]); } for (let groupElt of groupElts) { const wrapper = groupElt.parentElement; const ulElt = wrapper.getElementsByTagName("ul")[0]; const pageGroupElt = ulElt.previousSibling; if (ulElt !== undefined) { if (pageGroupElt.classList.contains("opened")) { // already opened sidebar group ulElt.style.display = "block"; ulElt.style.height = `auto`; } } } } /** * Setup a 1.5 seconds timeout after which a spinner is shown if the next page * isn't yet displayed. * @param {string} url */ function setCurrentDisplayTimeout(url) { if (currentDisplayTimeout !== null) { clearTimeout(currentDisplayTimeout); } currentDisplayTimeout = setTimeout(function () { const loadingParentDiv = document.createElement("div"); const loadingWrapperDiv = document.createElement("div"); loadingWrapperDiv.className = "loading-wrapper"; const pageTextSpan = document.createElement("span"); const pageTextUrl = document.createElement("a"); const spinnerDiv = document.createElement("div"); spinnerDiv.innerHTML = spinnerSvg; pageTextSpan.textContent = "Loading the next documentation page takes time, " + "you can also try to force browser navigation by going to the following link: "; pageTextUrl.href = url; pageTextUrl.textContent = url; loadingWrapperDiv.appendChild(spinnerDiv); loadingWrapperDiv.appendChild(pageTextSpan); loadingWrapperDiv.appendChild(pageTextUrl); loadingParentDiv.appendChild(loadingWrapperDiv); document.getElementsByClassName("content-wrapper")[0].innerHTML = loadingParentDiv.innerHTML; }, 1300); } /** * Soft-navigate to the content behind the given URL. * @param {HTMLLinkElement} link - The link whose URL we should soft-navigate to * @param {boolean} display - If `true` the page will be displayed right * after being loaded. If `false`, it is just being pre-loaded for now. * @param {boolean} scrollToTop - If `true` we will scroll to the top of the * page immediately after loading. Will only be considered if `display` is * also `true`. * @returns {Promise} - Promise resolving when the page is loaded. */ function loadContentFrom(link, display, scrollToTop) { const href = link.href; while (linksCache.length > 50) { linksCache.shift(); } const prom = fetch(href) .then((res) => { return res.text(); }) .then((body) => { const xml = new DOMParser().parseFromString(body, "text/html"); const newContent = xml.getElementsByClassName("content-wrapper")[0]; return newContent; }); linksCache.push([href, prom]); return prom .then(function (newContent) { if (display && currentlyWantedPage === href) { history.pushState(null, null, href); prepareNextPage(link); displayContent(newContent, scrollToTop); } }) .catch(function () { for (let i = 0; i < linksCache.length; i++) { if (linksCache[i][0] === href) { linksCache.splice(i, 1); } } if (currentlyWantedPage !== href) { return; } window.location.href = href; }); } /** * Display fetched content (end step of soft navigation). * @param {string} newContent - The content to display. * @param {boolean} scrollToTop - If `true` we will scroll to the top of the * page immediately after loading. */ function displayContent(newContent, scrollToTop) { if (currentDisplayTimeout !== null) { clearTimeout(currentDisplayTimeout); currentDisplayTimeout = null; } const content = newContent.innerHTML; document.getElementsByClassName("content-wrapper")[0].innerHTML = content; if (scrollToTop) { window.scrollTo(0, 0); } removeListeners(); removeListeners = initializePage(); showHeader(); } /** * Action to take when the `"popstate"` event arises. */ function onPopState() { const url = window.location.href; const linkElt = sidebarLinksToLinkElements.get(url); if (linkElt !== undefined) { loadSidebarLink(linkElt, { display: true, updateURL: false }); } else { window.location.href = url; } } /** * Returns the HTMLElement corresponding to the search input. * @returns {HTMLElement} */ function getSearchBarElement() { return document.getElementById("searchbar"); } /** * Returns the HTMLElement containing the search results. * @returns {HTMLElement} */ function getSearchResultElement() { return document.getElementById("search-results"); } /** * All search icon elements in the page (hopefully, there's only one). * @returns {HTMLElement} */ function getSearchIconElements() { return document.getElementsByClassName("search-icon"); } /** * All search icon elements in the page (hopefully, there's only one). * @returns {HTMLElement} */ function getSearchIconElements() { return document.getElementsByClassName("search-icon"); } /** * Parent element to search-related dynamic elements (input + results ...) * @returns {HTMLElement} */ function getSearchWrapperElement() { return document.getElementById("search-wrapper"); } /** * Creates an array of rendered elements based on the specified display level and item data. * * @param {Object} item - The item containing heading properties (h1, h2, h3). * @param {number} displayFromLevel - The level from which to start rendering headings (1 to 3). * @returns {Array} An array of elements created for the relevant heading levels. */ function createHeadingElements(item, displayFromLevel) { const headingElements = []; const headingLevelToRender = []; if (displayFromLevel <= 1 && item.h1) { headingLevelToRender.push("h1"); } if (displayFromLevel <= 2 && item.h2) { headingLevelToRender.push("h2"); } if (displayFromLevel <= 3 && item.h3) { headingLevelToRender.push("h3"); } for (const headingLevel of headingLevelToRender) { const element = createResultElement(item, headingLevel); headingElements.push(element); } return headingElements; } function createResultElement(item, itemLevel) { const links = searchIndexLinks[+item.id]; const contentDiv = document.createElement("div"); contentDiv.className = "search-result-item"; const locationDiv = document.createElement("div"); locationDiv.className = "search-result-location"; let href; let textContent; if (itemLevel === "h3") { contentDiv.classList.add("search-result-item-is-h3"); if (links.anchorH3 !== undefined) { href = rootUrl + "/" + links.file + "#" + links.anchorH3; } textContent = item.h3; } else if (itemLevel === "h2") { contentDiv.classList.add("search-result-item-is-h2"); if (links.anchorH2 !== undefined) { href = rootUrl + "/" + links.file + "#" + links.anchorH2; } textContent = item.h2; } else if (itemLevel === "h1") { contentDiv.classList.add("search-result-item-is-h1"); if (links.anchorH1 !== undefined) { href = rootUrl + "/" + links.file + "#" + links.anchorH1; } textContent = item.h1; } let anchorElement; if (href) { anchorElement = document.createElement("a"); anchorElement.href = href; } else { anchorElement = document.createElement("span"); } anchorElement.textContent = textContent; anchorElement.className = itemLevel; locationDiv.appendChild(anchorElement); const bodyDiv = document.createElement("div"); bodyDiv.className = "search-result-body"; let body = item.body ?? ""; if (body.length > 300) { body = body.substring(0, 300) + "..."; } bodyDiv.textContent = body; contentDiv.appendChild(locationDiv); contentDiv.appendChild(bodyDiv); return contentDiv; }