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);
+ }
+ });
}
});