/** * Clean URLs from anchors * @param {string} url Link URL * @returns Link URL cleaned */ const cleanURL = (url) => url.split("#")[0]; /** * Capitalize a text * @param {string} text Input text * @returns Capitalize text */ const capitalize = (text) => text.length === 0 ? text : text[0].toUpperCase() + text.substring(1); /** * Build the filetree * @param {HTMLElement} parent Root element of the filetree * @param {{name: string, is_dir: boolean, children: any[]}[]} data FileNode * @param {string} location Current location, used for links creation */ const buildFileTree = (parent, data, location) => { const fragment = document.createDocumentFragment(); const ul = document.createElement("ul"); fragment.appendChild(ul); data.forEach((item) => { const li = document.createElement("li"); li.classList.add(item.is_dir ? "directory" : "file"); if (item.is_dir) { // Directory li.textContent = capitalize(item.name); li.classList.add("collapsed"); // Toggle collapsing on click li.addEventListener( "click", (e) => { if (e.target === li) { li.classList.toggle("collapsed"); } }, { passive: true } ); } else { // File const url = cleanURL(window.location.href).split("?")[0]; const a = document.createElement("a"); a.text = capitalize( item.name.endsWith(".md") ? item.name.slice(0, -3) : item.name ); a.href = `${url}?q=${location}${item.name}`; li.appendChild(a); } ul.appendChild(li); if (item.children?.length) { buildFileTree( li, item.children, item.is_dir ? `${location}${item.name}/` : location ); } }); parent.appendChild(fragment); }; /** * Uncollapse elements from the deepest element * @param {HTMLLIElement} element Element to uncollapse */ const uncollapse = (element) => { if (element) { element.classList.remove("collapsed"); uncollapse(element.parentElement.closest("li")); } }; /** * Find the deepest opened directory * @param {string[]} path Current path we are looking at, init with fullpath * @param {NodeListOf} options Options we have, init with list root * @returns The deepest node */ const deepestNodeOpened = (path, options) => { if (path[0] === "") { return options[0].parentNode.parentNode; } // Iterate over possible options for (let i = 0; i < options.length; ++i) { // If the directory and the current path match if ( decodeURI(path[0]).toLowerCase() === options[i].firstChild.nodeValue.toLowerCase() ) { if (path.length === 1) { // We found it return options[i]; } // Continue the search return deepestNodeOpened( path.slice(1), options[i].querySelector("ul").childNodes ); } } }; /** * Search in the filetree, when nothing is aske, returns to initial state * @param {string} query Query * @param {HTMLElement} parent Filetree * @param {HTMLLIElement} currentFile Current file opened */ const searchFiles = (query, parent, currentFile) => { // Prevent blocking the main thread requestAnimationFrame(() => { const children = parent.querySelectorAll("li"); const normalizedQuery = query.toLowerCase().trim(); if (normalizedQuery === "") { children.forEach((item) => { item.style.display = ""; if ( item.classList.contains("directory") && !item.classList.contains("collapsed") ) { item.classList.add("collapsed"); } }); uncollapse(currentFile); return; } for (const item of children) { const matches = item.innerText.toLowerCase().includes(normalizedQuery); if (matches) { item.style.display = ""; uncollapse(item); continue; } item.style.display = "none"; } }); }; window.addEventListener("load", () => { // Build the filetree const fileTreeElement = document.getElementsByTagName("aside")[0]; const dataElement = fileTreeElement.getElementsByTagName("span")[0]; buildFileTree( fileTreeElement, JSON.parse(dataElement.getAttribute("data-json")).children, "" ); dataElement.remove(); // Open nested openeded directories const infoURL = cleanURL(window.location.href).split("?"); const fullpath = infoURL.length > 1 ? infoURL[1].substring(2) : "index.md"; const path = fullpath.substring(0, fullpath.lastIndexOf("/")); const currentlyOpen = deepestNodeOpened( path.split("/"), fileTreeElement.querySelector("ul").childNodes ); uncollapse(currentlyOpen); // Bold opened file const openedFile = decodeURI(fullpath.split("/").at(-1)); currentlyOpen.querySelector("ul").childNodes.forEach((el) => { const elementToCompare = decodeURI( el.firstChild.search ? el.firstChild.search.substring(3).split("/").at(-1) : el.firstChild.data ); if (elementToCompare === openedFile) { el.style.fontWeight = "bold"; } }); // Search bar hook document.getElementById("searchBar").addEventListener("input", (e) => { searchFiles(e.target.value, fileTreeElement, currentlyOpen); }); // Responsive menu let menuOpen = false; const button = document.getElementById("menu"); const content = document.getElementsByTagName("main")[0]; const initialButtonTextContent = button.textContent; const resetPage = () => { menuOpen = !menuOpen; if (menuOpen) { fileTreeElement.style.display = "block"; content.style.display = "none"; button.textContent = "Fermer le menu"; return; } fileTreeElement.style.display = ""; content.style.display = ""; button.textContent = initialButtonTextContent; }; button.addEventListener("click", resetPage); window.addEventListener("resize", () => { if (menuOpen && window.innerWidth > 640) { resetPage(); } }); });