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.icon || "search")}
${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;
}