diff --git a/docs/static/script/search.js b/docs/static/script/search.js index 618ec4a..a0284f9 100644 --- a/docs/static/script/search.js +++ b/docs/static/script/search.js @@ -2,38 +2,189 @@ document.addEventListener("DOMContentLoaded", () => { // The search widget should only be visible if we're in the options page. Else, we // want it hidden. if (window.location.pathname.endsWith("options.html")) { + console.log("Running script on options.html"); + + // Static const searchBar = document.createElement("div"); searchBar.id = "search-bar"; searchBar.innerHTML = ` - -
+ `; - document.body.prepend(searchBar); - const dtElements = document.querySelectorAll("dt"); - const ddElements = document.querySelectorAll("dd"); + // Floating + const searchPopup = document.createElement("div"); + searchPopup.id = "search-popup"; + searchPopup.innerHTML = ` +
+ + +
or
elements found. Ensure your HTML contains the correct structure.", - ); - } + // TODO: move this to the compiled stylesheet after development + const style = document.createElement("style"); + style.textContent = ` + #search-bar { + position: sticky; + top: 0; + background: #f9f9f9; + padding: 10px; + border-bottom: 1px solid #ccc; + z-index: 1000; + cursor: pointer; + } - // handle input and filter visible options - document - .getElementById("search-input") - .addEventListener("input", (event) => { - const query = event.target.value.toLowerCase(); - dtElements.forEach((dt, index) => { - const optionId = - dt.querySelector("a")?.id.toLowerCase() || ""; - const isMatch = optionId.includes(query); + #search-popup { + position: fixed; + top: 25%; + left: 50%; + transform: translate(-50%, -50%); + background: #fff; + border: 1px solid #ccc; + border-radius: 5px; + padding: 20px; + width: 450px; + max-height: 400px; /* Limit the height of the popup */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + z-index: 1001; + display: none; /* Initially hidden */ + overflow: hidden; + } - // toggle visibility based on the query match - dt.classList.toggle("hidden", !isMatch); - ddElements[index]?.classList.toggle("hidden", !isMatch); - }); + #search-popup-content { + display: flex; + flex-direction: column; + } + + #search-input-popup { + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + margin-bottom: 10px; + } + + #search-results-list { + list-style-type: none; + padding: 0; + max-height: 300px; /* Limit the number of visible entries */ + overflow-y: auto; /* Make it scrollable */ + } + + #search-results-list li { + margin: 5px 0; + cursor: pointer; + padding: 5px; + } + + #search-results-list li:hover { + background-color: #f0f0f0; + } + + .hidden { + display: none; + } + `; + document.head.appendChild(style); + + // Focus the input on pop-up widget + searchBar.addEventListener("click", (event) => { + event.stopPropagation(); // also cancel clicks to get rid of that annoying cursor flicker + console.log("Search bar clicked!"); + + // Visibility + if (searchPopup.style.display === "block") { + searchPopup.style.display = "none"; + } else { + searchPopup.style.display = "block"; + document.getElementById("search-input-popup").focus(); + } + }); + + // Ctrl+K opens the floating search widget + document.addEventListener("keydown", (event) => { + if (event.ctrlKey && event.key === "k") { + event.preventDefault(); + console.log("Ctrl+K pressed!"); + searchPopup.style.display = "block"; + document.getElementById("search-input-popup").focus(); + } + }); + + // Close the popup when clicking outside + document.addEventListener("click", (event) => { + if ( + !searchPopup.contains(event.target) && + event.target !== searchBar + ) { + console.log("Click outside, hiding popup"); + searchPopup.style.display = "none"; + } + }); + + // Close the popup with Esc key + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + console.log("Escape pressed!"); + searchPopup.style.display = "none"; + } + }); + + // Handle input + const searchInput = document.getElementById("search-input-popup"); + const searchResultsList = document.getElementById( + "search-results-list", + ); + const codeElements = document.querySelectorAll("code.option"); + + searchInput.addEventListener("input", (event) => { + const query = event.target.value.toLowerCase(); + searchResultsList.innerHTML = ""; // clear previous + + // Find matching + const matchingOptions = []; + codeElements.forEach((code) => { + const optionText = code.textContent; + if (optionText.toLowerCase().includes(query)) { + // Search should be case-insensitive, since chances are + // the user doesn't *actually* know what they're looking for. + const anchorId = code.closest("a").id; + matchingOptions.push({ text: optionText, id: anchorId }); + } }); + + // Limit the number of visible entries (e.g., show max 10 matches) + const maxVisibleResults = 10; + const resultsToShow = matchingOptions.slice(0, maxVisibleResults); + + // Display matching + resultsToShow.forEach(({ text, id }) => { + const li = document.createElement("li"); + li.textContent = text; + li.addEventListener("click", () => { + // Update the hash to the option name with a prefix + // e.g., #vim.enableEditorconfig -> #opt-vim.enableEditorconfig + const optionId = `opt-${text}`; + window.location.hash = `#${optionId}`; + searchPopup.style.display = "none"; + + const element = document.getElementById(id); + if (element) { + element.scrollIntoView({ behavior: "smooth" }); // doesn't really scroll very smoothly + } + }); + searchResultsList.appendChild(li); + }); + + // If there are items > maxVisibleResults, show a "See More" option + if (matchingOptions.length > maxVisibleResults) { + const li = document.createElement("li"); + li.textContent = `See more (${matchingOptions.length - maxVisibleResults} more)`; + li.style.fontStyle = "italic"; + searchResultsList.appendChild(li); + } + }); } });