diff --git a/doc/changelog.d/798.added.md b/doc/changelog.d/798.added.md new file mode 100644 index 000000000..3000c75e8 --- /dev/null +++ b/doc/changelog.d/798.added.md @@ -0,0 +1 @@ +Cleanup \`\`js\`\` diff --git a/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/components/announcement_version.html b/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/components/announcement_version.html index c35ede7ce..e43b34778 100644 --- a/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/components/announcement_version.html +++ b/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/components/announcement_version.html @@ -1,5 +1,11 @@ -
+ + +
diff --git a/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/components/cheatsheet_sidebar.html b/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/components/cheatsheet_sidebar.html index 634a7884c..badd7cd1c 100644 --- a/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/components/cheatsheet_sidebar.html +++ b/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/components/cheatsheet_sidebar.html @@ -1,13 +1,19 @@ + +{# + Cheatsheet Sidebar + Displays a cheatsheet link and thumbnail in the sidebar if configured. + Uses
for compatibility as requested. +#} {% if theme_cheatsheet %} {% set theme_cheatsheet_title = theme_cheatsheet.get('title', 'Cheatsheet') %} - + + + {% endif %} diff --git a/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/static/js/search-main.js b/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/static/js/search-main.js index 0bc29a61f..f7f5e1af7 100644 --- a/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/static/js/search-main.js +++ b/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/static/js/search-main.js @@ -1,40 +1,42 @@ +/** + * search-main.js + * + * Main search logic for the documentation site. Handles search index loading, filtering, and UI updates. + * Uses Fuse.js for fuzzy searching and IndexedDB for caching search data. + * + */ + +// DOM Elements const SEARCH_BAR = document.getElementById("search-bar"); const SEARCH_INPUT = SEARCH_BAR.querySelector(".bd-search input.form-control"); +// Configure RequireJS for Fuse.js require.config({ paths: { fuse: "https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min", }, }); -/* IndexDB functions */ /** - * Open or create an IndexedDB database. + * Open or create an IndexedDB database for caching search indexes. * @param {string} name - Name of the database. * @param {number} version - Version of the database. - * @returns {Promise} - A promise that resolves with the database instance. + * @returns {Promise} Promise resolving to the database instance. */ function openDB(name = "search-cache", version = 1) { return new Promise((resolve, reject) => { - console.log(`Opening IndexedDB: ${name}, version: ${version}`); - const request = indexedDB.open(name, version); - request.onerror = () => { console.error("IndexedDB open error:", request.error); reject(request.error); }; - request.onsuccess = () => { - console.log("IndexedDB opened successfully"); resolve(request.result); }; - request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains("indexes")) { db.createObjectStore("indexes"); - console.log("Created object store: indexes"); } }; }); @@ -43,23 +45,16 @@ function openDB(name = "search-cache", version = 1) { /** * Retrieve a value from IndexedDB by key. * @param {string} key - The key to look up in the object store. - * @returns {Promise} - A promise that resolves with the retrieved value. + * @returns {Promise} Promise resolving to the retrieved value. */ async function getFromIDB(key) { const db = await openDB(); - return new Promise((resolve, reject) => { const transaction = db.transaction("indexes", "readonly"); const store = transaction.objectStore("indexes"); const request = store.get(key); - - request.onsuccess = () => { - resolve(request.result); - }; - - request.onerror = () => { - reject(request.error); - }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); }); } @@ -67,68 +62,68 @@ async function getFromIDB(key) { * Save a key-value pair to IndexedDB. * @param {string} key - The key to store the value under. * @param {any} value - The value to store. - * @returns {Promise} - A promise that resolves when saving is complete. + * @returns {Promise} Promise resolving when saving is complete. */ async function saveToIDB(key, value) { const db = await openDB(); - return new Promise((resolve, reject) => { const transaction = db.transaction("indexes", "readwrite"); const store = transaction.objectStore("indexes"); const request = store.put(value, key); - - request.onsuccess = () => { - resolve(true); - }; - + request.onsuccess = () => resolve(true); request.onerror = () => { console.error("Failed to save to IndexedDB:", request.error); reject(request.error); }; }); } + /** - * Initializes the search system by loading the document search index. + * Main search logic, loaded after Fuse.js is available. */ - require(["fuse"], function (Fuse) { let fuse; let searchData = []; let selectedObjectIDs = []; let selectedLibraries = []; const libSearchData = {}; - let selectedFilter = new Set(); - function debounce(func, delay) { + + /** + * Debounce utility to limit function execution rate. + * @param {Function} func + * @param {number} delay + * @returns {Function} + */ + const debounce = (func, delay) => { let timeout; - return function (...args) { + return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; - } + }; /** - * Initializes the search system by loading the document search index. + * Initialize the search system by loading the document search index and library indexes. + * Sets up filter dropdowns and triggers initial search if needed. */ async function initializeSearch() { try { const cacheKey = "main-search-index"; let data = await getFromIDB(cacheKey); - if (!data) { const response = await fetch(SEARCH_FILE); data = await response.json(); await saveToIDB(cacheKey, data); } - searchData = data; fuse = new Fuse(searchData, SEARCH_OPTIONS); + // Load library search data const allLibs = Object.keys(EXTRA_SOURCES); for (const lib of allLibs) { const cacheKey = `lib-search-${lib}`; let libData = await getFromIDB(cacheKey); - if (!libData) { const libPath = EXTRA_SOURCES[lib]; const url = `${libPath}/_static/search.json`; @@ -136,10 +131,8 @@ require(["fuse"], function (Fuse) { libData = await res.json(); await saveToIDB(cacheKey, libData); } - - libSearchData[lib] = libData; // Save in memory + libSearchData[lib] = libData; } - setupFilterDropdown(); showObjectIdDropdown(); showLibraryDropdown(); @@ -149,11 +142,10 @@ require(["fuse"], function (Fuse) { } /** - * Sets up the filter dropdown and its toggle interactions. + * Sets up the filter dropdown and its toggle interactions in the sidebar. */ function setupFilterDropdown() { const dropdownContainer = document.getElementById("search-sidebar"); - const filters = [ { name: "Documents", @@ -166,40 +158,32 @@ require(["fuse"], function (Fuse) { callback: showLibraryDropdown, }, ]; - - // remove the library filter if no libraries + // Remove the library filter if no libraries if (Object.keys(EXTRA_SOURCES).length === 0) { filters.splice(1, 1); } - filters.forEach(({ name, dropdownId, callback }) => { const toggleDiv = document.createElement("div"); toggleDiv.className = "search-page-sidebar toggle-section"; toggleDiv.dataset.target = dropdownId; - const icon = document.createElement("span"); icon.className = "toggle-icon"; icon.textContent = "▼"; icon.style.fontSize = "12px"; - const label = document.createElement("span"); label.className = "toggle-label"; label.textContent = name; - toggleDiv.append(icon, label); - const dropdown = document.createElement("div"); dropdown.id = dropdownId; dropdown.className = "dropdown-menu show"; dropdown.style.display = "block"; dropdown.style.marginTop = "10px"; - // Add event listener to toggle the dropdown toggleDiv.addEventListener("click", () => { const isVisible = dropdown.style.display === "block"; dropdown.style.display = isVisible ? "none" : "block"; icon.textContent = isVisible ? "▶" : "▼"; - if (isVisible) { selectedFilter.delete(name); toggleDiv.classList.remove("active"); @@ -208,22 +192,21 @@ require(["fuse"], function (Fuse) { toggleDiv.classList.add("active"); callback?.(); } - performSearch(); }); - dropdownContainer.append(toggleDiv, dropdown); }); } + /** + * Show the object ID dropdown for document filtering. + */ function showObjectIdDropdown() { const dropdown = document.getElementById("objectid-dropdown"); dropdown.innerHTML = ""; - const objectIDs = [ ...new Set(searchData.map((item) => item.objectID)), ].filter(Boolean); - objectIDs.forEach((id) => { const checkbox = createCheckboxItem(id, selectedObjectIDs, () => { renderSelectedChips(); @@ -231,15 +214,16 @@ require(["fuse"], function (Fuse) { }); dropdown.appendChild(checkbox); }); - dropdown.style.display = "block"; renderSelectedChips(); } + /** + * Show the library dropdown for library filtering. + */ function showLibraryDropdown() { const dropdown = document.getElementById("library-dropdown"); dropdown.innerHTML = ""; - for (const lib in EXTRA_SOURCES) { const checkbox = createCheckboxItem(lib, selectedLibraries, () => { renderSelectedChips(); @@ -247,15 +231,20 @@ require(["fuse"], function (Fuse) { }); dropdown.appendChild(checkbox); } - dropdown.style.display = "block"; renderSelectedChips(); } - function createCheckboxItem(value, selectedArray, onchnage) { + /** + * Create a checkbox item for a filter dropdown. + * @param {string} value - Value for the checkbox. + * @param {Array} selectedArray - Array of selected values. + * @param {Function} onChange - Callback on change. + * @returns {HTMLDivElement} Checkbox item div. + */ + function createCheckboxItem(value, selectedArray, onChange) { const div = document.createElement("div"); div.className = "checkbox-item"; - const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.value = value; @@ -266,13 +255,10 @@ require(["fuse"], function (Fuse) { selectedArray.push(value); } else { const index = selectedArray.indexOf(value); - if (index > -1) { - selectedArray.splice(index, 1); - } + if (index > -1) selectedArray.splice(index, 1); } - onchnage(); + onChange(); }; - const label = document.createElement("label"); label.textContent = value; div.appendChild(checkbox); @@ -281,17 +267,15 @@ require(["fuse"], function (Fuse) { } /** - * Renders chips for selected filters and binds remove logic. + * Render chips for selected filters and bind remove logic. */ function renderSelectedChips() { const container = document.getElementById("selected-chips"); container.innerHTML = ""; - const renderChip = (value, type, selectedArray) => { const chip = document.createElement("div"); chip.className = "chip"; chip.textContent = `${value} (${type})`; - const removeBtn = document.createElement("button"); removeBtn.className = "remove-btn"; removeBtn.innerHTML = "×"; @@ -303,11 +287,9 @@ require(["fuse"], function (Fuse) { if (type === "Library") showLibraryDropdown(); performSearch(); }; - chip.appendChild(removeBtn); container.appendChild(chip); }; - selectedObjectIDs.forEach((id) => renderChip(id, "Documents", selectedObjectIDs), ); @@ -316,92 +298,82 @@ require(["fuse"], function (Fuse) { ); } + /** + * Perform the search and update the results UI. + */ async function performSearch() { const query = document.getElementById("search-input").value.trim(); if (!fuse) return; - const resultsContainer = document.getElementById("search-results"); resultsContainer.innerHTML = "Searching..."; - let docResults = []; let libResults = []; - const resultLimit = getSelectedResultLimit(); - - // === Search in internal documents === + // Search in internal documents if (selectedFilter.size === 0 || selectedFilter.has("Documents")) { docResults = fuse .search(query, { limit: resultLimit }) .map((r) => r.item); - if (selectedObjectIDs.length > 0) { docResults = docResults.filter((item) => selectedObjectIDs.includes(item.objectID), ); } } - + // Search in selected libraries for (const lib of selectedLibraries) { const libBaseUrl = EXTRA_SOURCES[lib]; const cacheKey = `lib-search-${lib}`; - try { - const data = await getFromIDB(cacheKey); // Use cached data - + const data = await getFromIDB(cacheKey); if (data) { const enrichedEntries = data.map((entry) => ({ title: entry.title, text: entry.text, - section: entry.section, // if used in keys + section: entry.section, link: `${libBaseUrl}${entry.href}`, source: lib, })); - - // Create a separate Fuse instance for this library const libFuse = new Fuse(enrichedEntries, SEARCH_OPTIONS); - - // Search and add to results (append instead of overwrite) const results = libFuse .search(query, { limit: resultLimit }) .map((r) => r.item); - libResults.push(...results); } } catch (err) { console.error(`Error accessing cache for ${lib}:`, err); } } - - // === Merge and show results === + // Merge and show results const mergedResults = [...docResults, ...libResults]; - if (mergedResults.length === 0) { resultsContainer.innerHTML = "

No results found.

"; return; } - const highlightedResults = highlightResults(mergedResults, query); displayResults(highlightedResults); } + /** + * Highlight search query in the results. + * @param {Array} results - Array of result objects. + * @param {string} query - Search query. + * @returns {Array} Highlighted results. + */ function highlightResults(results, query) { const regex = new RegExp(`(${query})`, "gi"); - return results .map((result) => { const matchIndex = result.text .toLowerCase() .indexOf(query.toLowerCase()); if (matchIndex === -1) return null; - const contextLength = 100; const start = Math.max(0, matchIndex - contextLength); const end = Math.min(result.text.length, matchIndex + contextLength); let snippet = result.text.slice(start, end); - if (start > 0) snippet = "…" + snippet; if (end < result.text.length) snippet += "…"; - return { ...result, title: result.title.replace( @@ -418,47 +390,51 @@ require(["fuse"], function (Fuse) { } /** - * Displays the final search results on the UI. + * Display the final search results on the UI. + * @param {Array} results - Array of result objects. */ function displayResults(results) { const container = document.getElementById("search-results"); container.innerHTML = ""; - results.forEach((item) => { const div = document.createElement("div"); div.className = "result-item"; - const title = document.createElement("a"); title.href = item.href || item.link || "#"; title.target = "_blank"; title.innerHTML = item.title || "Untitled"; title.className = "result-title"; - div.appendChild(title); - if (item.text) { const text = document.createElement("p"); text.innerHTML = item.text; text.className = "result-text"; div.appendChild(text); } - if (item.source) { const source = document.createElement("p"); source.className = "checkmark"; source.textContent = `Source: ${item.source}`; div.appendChild(source); } - container.appendChild(div); }); } + /** + * Get the selected result limit from the dropdown. + * @returns {number} Result limit. + */ function getSelectedResultLimit() { const select = document.getElementById("result-limit"); - return parseInt(select.value, 10) || 10; // default to 10 if not set + return parseInt(select.value, 10) || 10; } + // --- Event Handlers and Initialization --- + + /** + * Debounced handler for search input changes. + */ const handleSearchInput = debounce( () => { const query = document.getElementById("search-input").value.trim(); @@ -469,7 +445,11 @@ require(["fuse"], function (Fuse) { parseInt(SEARCH_OPTIONS.delay) || 300, ); - // Utility to get element by ID + /** + * Utility to get element by ID. + * @param {string} id - Element ID. + * @returns {HTMLElement} DOM element. + */ const $ = (id) => document.getElementById(id); // Elements @@ -483,7 +463,9 @@ require(["fuse"], function (Fuse) { searchInput.value = initialQuery; } - // Unified search trigger + /** + * Unified search trigger for input events. + */ const triggerSearch = () => { const query = searchInput.value.trim(); if (query) { diff --git a/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/static/js/search.js b/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/static/js/search.js index 23b81b80d..d6cef2af1 100644 --- a/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/static/js/search.js +++ b/src/ansys_sphinx_theme/theme/ansys_sphinx_theme/static/js/search.js @@ -6,10 +6,10 @@ const MAIN_PAGE_CONTENT = document.querySelector(".bd-main"); const FUSE_VERSION = "6.4.6"; let SEARCH_BAR, - RESULTS, + RESULTS_CONTAINER, SEARCH_INPUT, CURRENT_INDEX = -1, - fuse; + fuseInstance; /** * Load fuse.js from CDN and initialize search functionality. @@ -20,8 +20,13 @@ require.config({ }, }); -require(["fuse"], function (Fuse) { - // Debounce utility +require(["fuse"], (Fuse) => { + /** + * Debounce utility to limit function execution rate. + * @param {Function} func + * @param {number} delay + * @returns {Function} + */ const debounce = (func, delay) => { let timeout; return (...args) => { @@ -30,25 +35,39 @@ require(["fuse"], function (Fuse) { }; }; - // Truncate text for preview + /** + * Truncate text for preview. + * @param {string} text + * @param {number} maxLength + * @returns {string} + */ const truncateTextPreview = (text, maxLength = 200) => text.length <= maxLength ? text : `${text.slice(0, maxLength)}...`; - // Get full path using Sphinx's data-content_root + /** + * Get full path using Sphinx's data-content_root. + * @param {string} targetFile + * @returns {string} + */ const getDynamicPath = (targetFile) => { const contentRoot = document.documentElement.getAttribute("data-content_root"); return `${contentRoot}${targetFile}`; }; - // Navigate to a given URL + /** + * Navigate to a given URL. + * @param {string} href + */ const navigateToHref = (href) => { window.location.href = getDynamicPath(href); }; - // Expand the search input UI + /** + * Expand the search input UI. + */ function expandSearchInput() { - RESULTS.style.display = "flex"; + RESULTS_CONTAINER.style.display = "flex"; SEARCH_INPUT.classList.add("expanded"); MAIN_PAGE_CONTENT.classList.add("blurred"); SEARCH_INPUT.focus(); @@ -60,9 +79,11 @@ require(["fuse"], function (Fuse) { if (modalSidebar) modalSidebar.style.opacity = "0.1"; } - // Collapse and reset the search UI + /** + * Collapse and reset the search UI. + */ function collapseSearchInput() { - RESULTS.style.display = "none"; + RESULTS_CONTAINER.style.display = "none"; SEARCH_INPUT.classList.remove("expanded"); SEARCH_INPUT.value = ""; MAIN_PAGE_CONTENT.classList.remove("blurred"); @@ -74,32 +95,39 @@ require(["fuse"], function (Fuse) { if (modalSidebar) modalSidebar.style.opacity = "1"; } - // Show banner when no results found + /** + * Show banner when no results found. + */ function noResultsFoundBanner() { - RESULTS.innerHTML = ""; - RESULTS.style.display = "flex"; + RESULTS_CONTAINER.innerHTML = ""; + RESULTS_CONTAINER.style.display = "flex"; const banner = document.createElement("div"); banner.className = "warning-banner"; banner.textContent = "No results found. Press Ctrl+Enter for extended search."; banner.style.fontStyle = "italic"; - RESULTS.appendChild(banner); + RESULTS_CONTAINER.appendChild(banner); } - // Show a temporary searching indicator + /** + * Show a temporary searching indicator. + */ function searchingForResultsBanner() { - RESULTS.innerHTML = ""; - RESULTS.style.display = "flex"; + RESULTS_CONTAINER.innerHTML = ""; + RESULTS_CONTAINER.style.display = "flex"; const banner = document.createElement("div"); banner.className = "searching-banner"; banner.textContent = "Searching..."; banner.style.fontStyle = "italic"; - RESULTS.appendChild(banner); + RESULTS_CONTAINER.appendChild(banner); } - // Display search results from Fuse + /** + * Display search results from Fuse. + * @param {Array} results + */ function displayResults(results) { - RESULTS.innerHTML = ""; + RESULTS_CONTAINER.innerHTML = ""; if (!results.length) return noResultsFoundBanner(); const fragment = document.createDocumentFragment(); @@ -136,11 +164,14 @@ require(["fuse"], function (Fuse) { }); fragment.appendChild(advancedSearchItem); - RESULTS.appendChild(fragment); - RESULTS.style.display = "flex"; + RESULTS_CONTAINER.appendChild(fragment); + RESULTS_CONTAINER.style.display = "flex"; } - // Focus the currently selected result item + /** + * Focus the currently selected result item. + * @param {NodeList} resultsItems + */ function focusSelected(resultsItems) { if (CURRENT_INDEX >= 0 && CURRENT_INDEX < resultsItems.length) { resultsItems.forEach((item) => item.classList.remove("selected")); @@ -151,23 +182,12 @@ require(["fuse"], function (Fuse) { } } - // Handle search query input with debounce - const handleSearchInput = debounce( - () => { - const query = SEARCH_INPUT.value.trim(); - if (!query) return (RESULTS.style.display = "none"); - - const searchResults = fuse.search(query, { - limit: parseInt(SEARCH_OPTIONS.limit), - }); - displayResults(searchResults); - }, - parseInt(SEARCH_OPTIONS.delay) || 300, - ); - - // Handle keyboard navigation inside search input + /** + * Handle keyboard navigation inside search input. + * @param {KeyboardEvent} event + */ function handleKeyDownSearchInput(event) { - const resultItems = RESULTS.querySelectorAll(".result-item"); + const resultItems = RESULTS_CONTAINER.querySelectorAll(".result-item"); switch (event.key) { case "Tab": event.preventDefault(); @@ -214,24 +234,42 @@ require(["fuse"], function (Fuse) { ) { searchingForResultsBanner(); } else { - RESULTS.style.display = "none"; + RESULTS_CONTAINER.style.display = "none"; } handleSearchInput(); } } - // Initialize and bind search elements + /** + * Handle search query input with debounce. + */ + const handleSearchInput = debounce( + () => { + const query = SEARCH_INPUT.value.trim(); + if (!query) return (RESULTS_CONTAINER.style.display = "none"); + + const searchResults = fuseInstance.search(query, { + limit: parseInt(SEARCH_OPTIONS.limit), + }); + displayResults(searchResults); + }, + parseInt(SEARCH_OPTIONS.delay) || 300, + ); + + /** + * Initialize and bind search elements. + */ function setupSearchElements() { if (window.innerWidth < 1200) { SEARCH_BAR = document.querySelector( "div.sidebar-header-items__end #search-bar", ); - RESULTS = document.querySelector( + RESULTS_CONTAINER = document.querySelector( "div.sidebar-header-items__end .static-search-results", ); } else { SEARCH_BAR = document.getElementById("search-bar"); - RESULTS = document.querySelector(".static-search-results"); + RESULTS_CONTAINER = document.querySelector(".static-search-results"); } if (!SEARCH_BAR) { console.warn("SEARCH_BAR not found for current view."); @@ -244,19 +282,38 @@ require(["fuse"], function (Fuse) { } } - // Handle global keydown events for search shortcuts + /** + * Handle global keydown events for search shortcuts. + * @param {KeyboardEvent} event + */ function handleGlobalKeyDown(event) { if (event.key === "Escape") collapseSearchInput(); else if (event.key === "k" && event.ctrlKey) expandSearchInput(); } - // Collapse search if clicking outside + /** + * Collapse search if clicking outside. + * @param {MouseEvent} event + */ function handleGlobalClick(event) { - if (!RESULTS.contains(event.target) && event.target !== SEARCH_INPUT) { + if ( + !RESULTS_CONTAINER.contains(event.target) && + event.target !== SEARCH_INPUT + ) { collapseSearchInput(); } } + /** + * Initialize Fuse with the given data and options. + * @param {Array} data + * @param {Object} options + */ + function initializeFuse(data, options) { + fuseInstance = new Fuse(data, options); + document.documentElement.setAttribute("data-fuse_active", "true"); + } + // Initialize search functionality on page load setupSearchElements(); window.addEventListener("resize", debounce(setupSearchElements, 250)); @@ -273,10 +330,4 @@ require(["fuse"], function (Fuse) { .catch((error) => console.error(`[AST]: Cannot fetch ${SEARCH_FILE}`, error.message), ); - - // Initialize Fuse with the given data and options - function initializeFuse(data, options) { - fuse = new Fuse(data, options); - document.documentElement.setAttribute("data-fuse_active", "true"); - } });