let initialized = false; let resolveResultsFn = null; let onSelectResultFn = null; let currentResults = []; let activeIndex = -1; let searchTimerId = null; let isOpen = false; function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function getElements() { const isMobile = document.body.classList.contains("layout-mode-mobile"); return { modal: document.getElementById("search-modal"), backdrop: document.getElementById("search-backdrop"), input: document.getElementById( isMobile ? "mobile-earth-search-input" : "earth-search-input", ), clear: document.getElementById( isMobile ? "mobile-earth-search-clear" : "earth-search-clear", ), meta: document.getElementById( isMobile ? "mobile-earth-search-meta" : "earth-search-meta", ), results: document.getElementById( isMobile ? "mobile-earth-search-results" : "earth-search-results", ), empty: document.getElementById( isMobile ? "mobile-earth-search-empty" : "earth-search-empty", ), close: document.getElementById("search-close"), }; } function setMeta(text) { const { meta } = getElements(); if (meta) meta.textContent = text; } function updateEmptyState(query) { const { empty } = getElements(); if (!empty) return; if (!query) { empty.textContent = "支持搜索海缆、登陆点、卫星、算力中心、BGP 事件与观测站。"; return; } empty.textContent = "未找到匹配对象,可尝试名称、地点、NORAD、ASN、前缀等关键词。"; } function renderResults(query) { const { results, empty } = getElements(); if (!results || !empty) return; results.innerHTML = ""; const hasResults = currentResults.length > 0; empty.hidden = hasResults; updateEmptyState(query); if (!hasResults) return; currentResults.forEach((result, index) => { const button = document.createElement("button"); button.type = "button"; button.className = "earth-search-result"; button.setAttribute("role", "option"); button.dataset.index = String(index); button.innerHTML = ` ${escapeHtml(result.title)} ${escapeHtml(result.subtitle || "")} ${escapeHtml(result.typeLabel || "")} `; button.addEventListener("click", async () => { await selectResult(index); }); results.appendChild(button); }); syncActiveResult(); } function syncActiveResult() { const { results } = getElements(); if (!results) return; Array.from(results.children).forEach((node, index) => { node.classList.toggle("is-active", index === activeIndex); }); } function moveActiveResult(delta) { if (currentResults.length === 0) return; activeIndex = ((activeIndex < 0 ? 0 : activeIndex) + delta + currentResults.length) % currentResults.length; syncActiveResult(); const { results } = getElements(); const activeNode = results?.children?.[activeIndex]; activeNode?.scrollIntoView({ block: "nearest" }); } async function selectResult(index) { const result = currentResults[index]; if (!result || typeof onSelectResultFn !== "function") return; closeSearchPanel(); try { await onSelectResultFn(result); } catch (error) { console.error("Search selection failed:", error); } } async function runSearch() { const { input, clear } = getElements(); if (!input) return; const query = input.value.trim(); if (clear) { clear.hidden = query.length === 0; } if (!query) { currentResults = []; activeIndex = -1; setMeta("输入关键词以搜索当前地球对象"); renderResults(""); return; } setMeta("正在检索…"); try { const nextResults = await resolveResultsFn?.(query); currentResults = Array.isArray(nextResults) ? nextResults : []; activeIndex = currentResults.length > 0 ? 0 : -1; setMeta(`找到 ${currentResults.length} 个结果`); renderResults(query); } catch (error) { console.error("Search failed:", error); currentResults = []; activeIndex = -1; setMeta("搜索失败"); renderResults(query); } } function scheduleSearch() { if (searchTimerId) { clearTimeout(searchTimerId); } searchTimerId = window.setTimeout(() => { searchTimerId = null; runSearch(); }, 120); } function handleKeydown(event) { const { modal, input } = getElements(); const isMobile = document.body.classList.contains("layout-mode-mobile"); if (!isMobile && !modal?.classList.contains("is-open")) return; if (event.key === "Escape") { event.preventDefault(); closeSearchPanel(); return; } if (event.target !== input) return; if (event.key === "ArrowDown") { event.preventDefault(); moveActiveResult(1); } else if (event.key === "ArrowUp") { event.preventDefault(); moveActiveResult(-1); } else if (event.key === "Enter" && activeIndex >= 0) { event.preventDefault(); selectResult(activeIndex).catch((error) => { console.warn("Selecting search result failed:", error); }); } } export function initSearchPanel({ resolveResults, onSelectResult } = {}) { resolveResultsFn = resolveResults; onSelectResultFn = onSelectResult; if (initialized) return; initialized = true; const inputs = ["earth-search-input", "mobile-earth-search-input"] .map((id) => document.getElementById(id)) .filter((node) => node instanceof HTMLInputElement); const clears = ["earth-search-clear", "mobile-earth-search-clear"] .map((id) => document.getElementById(id)) .filter((node) => node instanceof HTMLButtonElement); const close = document.getElementById("search-close"); const backdrop = document.getElementById("search-backdrop"); inputs.forEach((input) => { input.addEventListener("input", scheduleSearch); input.addEventListener("keydown", handleKeydown); }); clears.forEach((clear) => { clear.addEventListener("click", () => { const { input } = getElements(); if (!input) return; input.value = ""; input.focus(); runSearch().catch((error) => { console.warn("Clearing search failed:", error); }); }); }); close?.addEventListener("click", () => { closeSearchPanel(); }); backdrop?.addEventListener("click", () => { closeSearchPanel(); }); document.addEventListener("keydown", handleKeydown); } export function openSearchPanel() { const { modal, input } = getElements(); if (!modal && !document.body.classList.contains("layout-mode-mobile")) return; if (isOpen) return; isOpen = true; document.body.classList.add("earth-search-open"); modal?.classList.add("is-open"); modal?.setAttribute("aria-hidden", "false"); window.dispatchEvent( new CustomEvent("earth:search-open-change", { detail: { open: true } }), ); window.setTimeout(() => { input?.focus(); input?.select(); runSearch().catch((error) => { console.warn("Running search failed:", error); }); }, 16); } export function focusSearchInput({ select = false } = {}) { const { input } = getElements(); if (!(input instanceof HTMLInputElement)) return; input.focus(); if (select) { input.select(); } } export function refreshSearchResults() { return runSearch(); } export function closeSearchPanel() { const { modal } = getElements(); if (!modal && !document.body.classList.contains("layout-mode-mobile")) return; if (!isOpen) return; isOpen = false; document.body.classList.remove("earth-search-open"); modal?.classList.remove("is-open"); modal?.setAttribute("aria-hidden", "true"); window.dispatchEvent( new CustomEvent("earth:search-open-change", { detail: { open: false } }), ); } export function isSearchPanelOpen() { return isOpen; }