|
| 1 | +var txtSearch = document.getElementById("txtSearch"); |
| 2 | +var resultPanel = document.getElementById("tocSearchResult"); |
| 3 | +var searchPanelOutline = document.getElementById("tocSearchPanelInner"); |
| 4 | +var searchPanel = document.getElementById("tocSearchPanel"); |
| 5 | + |
| 6 | +let highlightedIndex = -1; |
| 7 | + |
| 8 | +function escapeHTML(str) { |
| 9 | + return str.replace(/&/g, '&') |
| 10 | + .replace(/</g, '<') |
| 11 | + .replace(/>/g, '>') |
| 12 | + .replace(/"/g, '"') |
| 13 | + .replace(/'/g, '''); |
| 14 | +} |
| 15 | + |
| 16 | +// This getFullText function is designed to replicate the original GH Pages logic |
| 17 | +// for constructing the display string, including stop conditions and popping. |
| 18 | +function getFullText(furoATagElement) { |
| 19 | + var sb = []; |
| 20 | + |
| 21 | + if (furoATagElement && furoATagElement.textContent) { |
| 22 | + sb.push(furoATagElement.textContent.trim()); |
| 23 | + } |
| 24 | + |
| 25 | + var currentDOMNode = furoATagElement ? furoATagElement.parentElement : null; |
| 26 | + |
| 27 | + while (currentDOMNode != null) { |
| 28 | + if (currentDOMNode.classList && |
| 29 | + (currentDOMNode.classList.contains('sidebar-tree') || |
| 30 | + currentDOMNode.classList.contains('sidebar-scroll') || |
| 31 | + currentDOMNode.classList.contains('sidebar-sticky') || |
| 32 | + currentDOMNode.tagName === 'NAV' |
| 33 | + )) { |
| 34 | + break; |
| 35 | + } |
| 36 | + |
| 37 | + if (currentDOMNode.tagName === 'LI' && currentDOMNode.className && currentDOMNode.className.includes('toctree-l')) { |
| 38 | + let liChildren = Array.from(currentDOMNode.children); |
| 39 | + let currentLiATag = null; |
| 40 | + for(let child of liChildren) { |
| 41 | + if (child.tagName === 'A' && child.classList.contains('reference') && child.classList.contains('internal')) { |
| 42 | + currentLiATag = child; |
| 43 | + break; |
| 44 | + } else if (child.tagName === 'P') { |
| 45 | + let pLink = child.querySelector('a.reference.internal'); |
| 46 | + if (pLink) { |
| 47 | + currentLiATag = pLink; |
| 48 | + break; |
| 49 | + } |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + if (currentLiATag && currentLiATag.textContent) { |
| 54 | + var token = currentLiATag.textContent.trim(); |
| 55 | + if (sb.length > 0) { |
| 56 | + if (token.indexOf(" ") !== -1 || token === "Interfaces" || token === "Types") { |
| 57 | + break; |
| 58 | + } |
| 59 | + } |
| 60 | + if (sb.length === 0 || sb[sb.length -1] !== token) { |
| 61 | + sb.push(token); |
| 62 | + } |
| 63 | + } |
| 64 | + } |
| 65 | + currentDOMNode = currentDOMNode.parentElement; |
| 66 | + } |
| 67 | + |
| 68 | + if (sb.length > 2) { |
| 69 | + sb.pop(); |
| 70 | + sb.pop(); |
| 71 | + } |
| 72 | + |
| 73 | + sb.reverse(); |
| 74 | + return sb.join("."); |
| 75 | +} |
| 76 | + |
| 77 | +function txtSearchFocus(event) { |
| 78 | + var searchText = txtSearch.value; |
| 79 | + if (searchText.length > 0 && resultPanel.children.length > 0 && resultPanel.textContent !== "No results found") { |
| 80 | + resultPanel.style.display = "block"; |
| 81 | + } |
| 82 | + if (searchPanelOutline) searchPanelOutline.classList.add("search_panel_focused"); |
| 83 | + if (searchPanelOutline) searchPanelOutline.classList.remove("search_panel_unfocused"); |
| 84 | +} |
| 85 | + |
| 86 | +function isChildOf(child, parent) { |
| 87 | + while (child != null) { |
| 88 | + if (child == parent) return true; |
| 89 | + child = child.parentNode; |
| 90 | + } |
| 91 | + return false; |
| 92 | +} |
| 93 | + |
| 94 | +function closePanel() { |
| 95 | + if (resultPanel) resultPanel.style.display = "none"; |
| 96 | +} |
| 97 | + |
| 98 | +function openPanel() { |
| 99 | + if (resultPanel) resultPanel.style.display = "block"; |
| 100 | + positDropdown(); |
| 101 | +} |
| 102 | + |
| 103 | +function txtSearchLostFocus(event) { |
| 104 | + if (searchPanelOutline) searchPanelOutline.classList.add("search_panel_unfocused"); |
| 105 | + if (searchPanelOutline) searchPanelOutline.classList.remove("search_panel_focused"); |
| 106 | +} |
| 107 | + |
| 108 | +function updateItemHighlight() { |
| 109 | + if (!resultPanel) return; |
| 110 | + let items = resultPanel.children; |
| 111 | + Array.from(items).forEach((item, index) => { |
| 112 | + item.classList.toggle('highlighted_search_selection', index === highlightedIndex); |
| 113 | + }); |
| 114 | + if (highlightedIndex >= 0 && highlightedIndex < items.length) { |
| 115 | + items[highlightedIndex].scrollIntoView({ block: 'nearest' }); |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +function searchResultItemOnClick(event) { |
| 120 | + let target = event.target; |
| 121 | + while (target && target.tagName !== 'A') { |
| 122 | + target = target.parentElement; |
| 123 | + } |
| 124 | + if (target && target.tagName === 'A') { |
| 125 | + const link = target.getAttribute('href'); |
| 126 | + if (link) { |
| 127 | + window.location.href = link; |
| 128 | + } |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +function isPerfectMatch(searchText, title) { |
| 133 | + if (!searchText || !title) return false; |
| 134 | + searchText = searchText.toLowerCase(); |
| 135 | + title = title.toLowerCase(); |
| 136 | + |
| 137 | + // Split title into words and check if search text matches any word exactly |
| 138 | + const words = title.split(/\s+/); |
| 139 | + return words.some(word => word === searchText); |
| 140 | +} |
| 141 | + |
| 142 | +function txtSearchChange(event) { |
| 143 | + var searchText = txtSearch.value.trim(); |
| 144 | + if (!resultPanel || !txtSearch) return; |
| 145 | + |
| 146 | + resultPanel.innerHTML = ""; |
| 147 | + var searchTokens = searchText.toLowerCase().split(/[\\.:,\s]+/).filter(t => t.length > 0); |
| 148 | + |
| 149 | + if (searchText.length === 0 || searchTokens.length === 0) { |
| 150 | + resultPanel.style.display = "none"; |
| 151 | + highlightedIndex = -1; |
| 152 | + return; |
| 153 | + } |
| 154 | + |
| 155 | + var matchedResults = []; |
| 156 | + let hasPerfectMatch = false; |
| 157 | + |
| 158 | + // -------- PHASE 1: TOC-based hierarchical search (existing logic) -------- |
| 159 | + const allTocLinks = document.querySelectorAll('.sidebar-tree a.reference.internal'); |
| 160 | + allTocLinks.forEach(innermostATag => { |
| 161 | + let currentTokenIndex = searchTokens.length - 1; |
| 162 | + let currentMatchCandidateLI = innermostATag.parentElement; |
| 163 | + let successfullyMatchedTocTokens = 0; |
| 164 | + let tempSearchTokens = [...searchTokens]; |
| 165 | + |
| 166 | + while (currentMatchCandidateLI && currentTokenIndex >= 0) { |
| 167 | + if (currentMatchCandidateLI.tagName === 'LI' && currentMatchCandidateLI.className.includes('toctree-l')) { |
| 168 | + let liTextElement = currentMatchCandidateLI.querySelector('a.reference.internal'); |
| 169 | + let textToMatch = (currentMatchCandidateLI === innermostATag.parentElement) ? |
| 170 | + innermostATag.textContent.trim().toLowerCase() : |
| 171 | + (liTextElement ? liTextElement.textContent.trim().toLowerCase() : ""); |
| 172 | + |
| 173 | + if (textToMatch.includes(tempSearchTokens[currentTokenIndex])) { |
| 174 | + successfullyMatchedTocTokens++; |
| 175 | + currentTokenIndex--; |
| 176 | + if (currentTokenIndex >=0) { |
| 177 | + let parentUL = currentMatchCandidateLI.parentElement; |
| 178 | + if (parentUL && parentUL.tagName === 'UL') { |
| 179 | + currentMatchCandidateLI = parentUL.parentElement; |
| 180 | + if(currentMatchCandidateLI && currentMatchCandidateLI.tagName !== 'LI') { |
| 181 | + currentMatchCandidateLI = null; |
| 182 | + } |
| 183 | + } else { |
| 184 | + currentMatchCandidateLI = null; |
| 185 | + } |
| 186 | + } |
| 187 | + } else { |
| 188 | + break; |
| 189 | + } |
| 190 | + } else { |
| 191 | + currentMatchCandidateLI = currentMatchCandidateLI.parentElement; |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + if (successfullyMatchedTocTokens === tempSearchTokens.length) { |
| 196 | + let displayString = getFullText(innermostATag); |
| 197 | + let score = 2000 - displayString.length; |
| 198 | + let displayStringLower = displayString.toLowerCase(); |
| 199 | + let allQueryTokensInDisplayString = true; |
| 200 | + for(let token of tempSearchTokens){ |
| 201 | + if(!displayStringLower.includes(token)){ |
| 202 | + allQueryTokensInDisplayString = false; |
| 203 | + break; |
| 204 | + } |
| 205 | + } |
| 206 | + if(allQueryTokensInDisplayString){ |
| 207 | + const existing = matchedResults.find(r => r.display === displayString && r.href === innermostATag.getAttribute("href")); |
| 208 | + if (!existing) { |
| 209 | + const isPerfect = isPerfectMatch(searchText, displayString); |
| 210 | + if (isPerfect) hasPerfectMatch = true; |
| 211 | + matchedResults.push({ |
| 212 | + display: displayString, |
| 213 | + href: innermostATag.getAttribute("href"), |
| 214 | + score: score + (isPerfect ? 1000 : 0), |
| 215 | + type: 'toc' |
| 216 | + }); |
| 217 | + } |
| 218 | + } |
| 219 | + } |
| 220 | + }); |
| 221 | + |
| 222 | + // -------- PHASE 2: Content search using Search._index -------- |
| 223 | + if (typeof Search !== 'undefined' && Search._index && typeof DOCUMENTATION_OPTIONS !== 'undefined') { |
| 224 | + const searchIndex = Search._index; |
| 225 | + let docResults = {}; |
| 226 | + const stemmer = (typeof Stemmer !== 'undefined' && Stemmer.stemWord) ? Stemmer.stemWord : (word => word.toLowerCase()); |
| 227 | + const queryOriginalTokens = searchText.toLowerCase().split(/[\\.:,\s]+/).filter(t => t.length > 0); |
| 228 | + |
| 229 | + let stemmedSearchTokens = queryOriginalTokens.map(stemmer); |
| 230 | + |
| 231 | + stemmedSearchTokens.forEach((term, i) => { |
| 232 | + let originalTerm = queryOriginalTokens[i]; |
| 233 | + let termDocs = searchIndex.terms[term]; |
| 234 | + if(!termDocs && searchIndex.terms[originalTerm]) { |
| 235 | + termDocs = searchIndex.terms[originalTerm]; |
| 236 | + } |
| 237 | + |
| 238 | + if (termDocs) { |
| 239 | + termDocs.forEach(docInfo => { |
| 240 | + let docIndex = Array.isArray(docInfo) ? docInfo[0] : docInfo; |
| 241 | + if (!docResults[docIndex]) { |
| 242 | + docResults[docIndex] = { |
| 243 | + title: searchIndex.titles[docIndex], |
| 244 | + filename: searchIndex.filenames[docIndex], |
| 245 | + docname: searchIndex.docnames[docIndex], |
| 246 | + matchCount: 0, |
| 247 | + score: 0 |
| 248 | + }; |
| 249 | + } |
| 250 | + docResults[docIndex].matchCount++; |
| 251 | + docResults[docIndex].score += (searchIndex.terms[term] && searchIndex.terms[term].length || 0) > 100 ? 1 : 10; |
| 252 | + if (searchIndex.titles[docIndex] && searchIndex.titles[docIndex].toLowerCase().includes(originalTerm)) { |
| 253 | + docResults[docIndex].score += 20; |
| 254 | + } |
| 255 | + }); |
| 256 | + } |
| 257 | + }); |
| 258 | + |
| 259 | + for (const docIndex in docResults) { |
| 260 | + const result = docResults[docIndex]; |
| 261 | + if (result.matchCount === stemmedSearchTokens.length) { |
| 262 | + if (result.docname && !result.docname.startsWith('external/core-module-reference')) { |
| 263 | + const pageTitle = result.title || "Untitled Page"; |
| 264 | + const urlRoot = DOCUMENTATION_OPTIONS.URL_ROOT || ''; |
| 265 | + let pageUrl = result.filename; |
| 266 | + if (urlRoot.endsWith('/') && pageUrl.startsWith('/')) { |
| 267 | + pageUrl = urlRoot + pageUrl.substring(1); |
| 268 | + } else if (!urlRoot.endsWith('/') && !pageUrl.startsWith('/') && urlRoot !== '') { |
| 269 | + pageUrl = urlRoot + '/' + pageUrl; |
| 270 | + } else { |
| 271 | + pageUrl = urlRoot + pageUrl; |
| 272 | + } |
| 273 | + |
| 274 | + const existing = matchedResults.find(r => r.href === pageUrl); |
| 275 | + if (!existing) { |
| 276 | + const isPerfect = isPerfectMatch(searchText, pageTitle); |
| 277 | + if (isPerfect) hasPerfectMatch = true; |
| 278 | + matchedResults.push({ |
| 279 | + display: pageTitle, |
| 280 | + href: pageUrl, |
| 281 | + score: result.score + 500 + (isPerfect ? 1000 : 0), |
| 282 | + type: 'content' |
| 283 | + }); |
| 284 | + } |
| 285 | + } |
| 286 | + } |
| 287 | + } |
| 288 | + } |
| 289 | + |
| 290 | + matchedResults.sort((a, b) => b.score - a.score); |
| 291 | + |
| 292 | + // Add the "Search for..." item at the top |
| 293 | + resultPanel.innerHTML = `<div class='search_result_item' data-type='search'><a href="search.html?q=${encodeURIComponent(searchText)}"><span>Search Documentation for "${escapeHTML(searchText)}"</span></a></div>`; |
| 294 | + |
| 295 | + // Add the rest of the results |
| 296 | + resultPanel.innerHTML += matchedResults.map(r => |
| 297 | + `<div class='search_result_item' data-type='${r.type}'><a href='${r.href}'><span>${escapeHTML(r.display)}</span></a></div>` |
| 298 | + ).join(""); |
| 299 | + |
| 300 | + Array.from(resultPanel.children).forEach(child => { |
| 301 | + child.addEventListener("click", searchResultItemOnClick); |
| 302 | + }); |
| 303 | + |
| 304 | + if (matchedResults.length > 0 || searchText.length > 0) { |
| 305 | + // If we have a perfect match, highlight the first matching result |
| 306 | + // Otherwise, highlight the "Search for..." item |
| 307 | + highlightedIndex = hasPerfectMatch ? 1 : 0; |
| 308 | + openPanel(); |
| 309 | + } else { |
| 310 | + highlightedIndex = 0; |
| 311 | + closePanel(); |
| 312 | + } |
| 313 | + updateItemHighlight(); |
| 314 | +} |
| 315 | + |
| 316 | +const input = txtSearch; |
| 317 | + |
| 318 | +if (input) { |
| 319 | + input.addEventListener('keydown', (e) => { |
| 320 | + if (!resultPanel) return; |
| 321 | + const items = resultPanel.children; |
| 322 | + if (items.length === 0 && e.key !== 'Escape') return; |
| 323 | + |
| 324 | + if (e.key === 'ArrowDown') { |
| 325 | + highlightedIndex++; |
| 326 | + if (highlightedIndex >= items.length) highlightedIndex = 0; |
| 327 | + e.preventDefault(); |
| 328 | + } else if (e.key === 'ArrowUp') { |
| 329 | + highlightedIndex--; |
| 330 | + if (highlightedIndex < 0) highlightedIndex = items.length - 1; |
| 331 | + e.preventDefault(); |
| 332 | + } else if (e.key === 'Enter') { |
| 333 | + if (highlightedIndex > -1 && items[highlightedIndex]) { |
| 334 | + let selectedATag = items[highlightedIndex].querySelector('a'); |
| 335 | + if (selectedATag && selectedATag.href) { |
| 336 | + window.location.href = selectedATag.href; |
| 337 | + } |
| 338 | + e.preventDefault(); |
| 339 | + } |
| 340 | + } else if (e.key === 'Escape') { |
| 341 | + closePanel(); |
| 342 | + } |
| 343 | + updateItemHighlight(); |
| 344 | + }); |
| 345 | + |
| 346 | + input.addEventListener("blur", (e) => { |
| 347 | + setTimeout(() => { |
| 348 | + if (searchPanel && !searchPanel.contains(document.activeElement)) { |
| 349 | + txtSearchLostFocus(e); |
| 350 | + if(document.activeElement && !document.activeElement.closest('.search_result_item')){ |
| 351 | + closePanel(); |
| 352 | + } |
| 353 | + } |
| 354 | + }, 150); |
| 355 | + }); |
| 356 | + input.addEventListener("focus", txtSearchFocus); |
| 357 | + input.addEventListener("input", txtSearchChange); |
| 358 | +} |
| 359 | + |
| 360 | +function positDropdown() { |
| 361 | + if (searchPanel && searchPanelOutline && resultPanel) { |
| 362 | + resultPanel.style.top = `${searchPanel.offsetHeight}px`; |
| 363 | + resultPanel.style.width = `${searchPanelOutline.offsetWidth}px`; |
| 364 | + } |
| 365 | +} |
| 366 | + |
| 367 | +window.addEventListener('load', positDropdown); |
| 368 | +window.addEventListener('resize', positDropdown); |
| 369 | + |
| 370 | +document.addEventListener('click', function(event) { |
| 371 | + if (resultPanel && searchPanel && !resultPanel.contains(event.target) && !searchPanel.contains(event.target) && event.target !== txtSearch) { |
| 372 | + closePanel(); |
| 373 | + } |
| 374 | +}); |
| 375 | + |
| 376 | +document.addEventListener('keydown', function(event) { |
| 377 | + if (event.code === 'Backquote') { |
| 378 | + event.preventDefault(); |
| 379 | + if (txtSearch) txtSearch.focus(); |
| 380 | + } |
| 381 | +}); |
| 382 | + |
| 383 | +if (txtSearch && txtSearch.offsetParent !== null) { |
| 384 | + positDropdown(); |
| 385 | +} |
0 commit comments