// Based on https://codeberg.org/daudix/duckquill/issues/101#issuecomment-2377169 let searchSetup = false; let fuse; async function initIndex() { if (searchSetup) return; let data; if (typeof window.searchIndex !== "undefined") { data = window.searchIndex; } else { const response = await fetch( `/search_index.${document.documentElement.lang}.json`, ); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); data = await response.json(); } const options = { includeScore: false, includeMatches: true, ignoreLocation: true, threshold: 0.15, keys: [ { name: "title", weight: 3 }, { name: "description", weight: 2 }, { name: "body", weight: 1 }, ], }; fuse = new Fuse(data, options); searchSetup = true; console.log("Search index initialized successfully"); } function debounce(actual_fn, wait) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { actual_fn(...args); }, wait); }; } function initSearch() { const searchModal = document.getElementById("search-modal"); // Full-screen modal const searchModalContent = document.getElementById("search-modal-content"); // Actual modal box const searchInput = document.getElementById("search-input"); // Search input const searchResults = document.getElementById("search-results"); // Search results const searchButton = document.getElementById("search"); // Search button const MAX_ITEMS = 10; const MAX_RESULTS = 4; let currentTerm = ""; // Open search modal when clicking the search button if (searchButton) { searchButton.addEventListener("click", function () { searchModal.classList.add("active"); initIndex(); searchModal.addEventListener( "transitionend", function handler() { searchInput.focus(); searchModal.removeEventListener("transitionend", handler); }, { once: true }, ); }); } // Open search modal on "/" key press window.addEventListener("keydown", (event) => { if ( event.key === "/" && document.activeElement.tagName !== "INPUT" && document.activeElement.tagName !== "TEXTAREA" ) { event.preventDefault(); initIndex(); searchModal.classList.add("active"); searchModal.addEventListener( "transitionend", function handler() { searchInput.focus(); searchModal.removeEventListener("transitionend", handler); }, { once: true }, ); } }); // Close search modal on Escape key window.addEventListener("keydown", (event) => { if (event.key === "Escape") { searchModal.classList.remove("active"); } }); // Close search modal when clicking outside search-modal-content searchModal.addEventListener("click", function (e) { if (!searchModalContent.contains(e.target)) { searchModal.classList.remove("active"); } }); // Prevent clicks inside modal content from closing it searchModalContent.addEventListener("click", function (e) { e.stopPropagation(); // Stops event from reaching searchModal click handler }); // Search input event searchInput.addEventListener( "keyup", debounce(async function () { var term = searchInput.value.trim(); if (term === currentTerm) return; searchResults.style.display = term === "" ? "none" : "flex"; searchResults.innerHTML = ""; // Clear previous results currentTerm = term; if (term === "") return; await initIndex(); var results = fuse.search(term, { limit: MAX_ITEMS }); if (results.length === 0) { searchResults.style.display = "none"; return; } // Insert formatted search result items for (const result of results) { searchResults.innerHTML += makeTeaser(result, term); } }, 150), ); function makeTeaser(result, searchVal) { const TEASER_SIZE = 20; let output = `