228 lines
6.2 KiB
JavaScript
228 lines
6.2 KiB
JavaScript
let initialized = false;
|
||
let resolveResultsFn = null;
|
||
let onSelectResultFn = null;
|
||
let currentResults = [];
|
||
let activeIndex = -1;
|
||
let searchTimerId = null;
|
||
|
||
function escapeHtml(value) {
|
||
return String(value)
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
function getElements() {
|
||
return {
|
||
modal: document.getElementById("search-modal"),
|
||
backdrop: document.getElementById("search-backdrop"),
|
||
input: document.getElementById("earth-search-input"),
|
||
clear: document.getElementById("earth-search-clear"),
|
||
meta: document.getElementById("earth-search-meta"),
|
||
results: document.getElementById("earth-search-results"),
|
||
empty: document.getElementById("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 = `
|
||
<span class="earth-search-result-icon" aria-hidden="true">
|
||
<span class="material-symbols-rounded">${escapeHtml(result.icon || "search")}</span>
|
||
</span>
|
||
<span class="earth-search-result-copy">
|
||
<span class="earth-search-result-title">${escapeHtml(result.title)}</span>
|
||
<span class="earth-search-result-subtitle">${escapeHtml(result.subtitle || "")}</span>
|
||
</span>
|
||
<span class="earth-search-result-type">${escapeHtml(result.typeLabel || "")}</span>
|
||
`;
|
||
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();
|
||
if (!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 { input, clear, close, backdrop } = getElements();
|
||
input?.addEventListener("input", scheduleSearch);
|
||
input?.addEventListener("keydown", handleKeydown);
|
||
clear?.addEventListener("click", () => {
|
||
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) return;
|
||
modal.classList.add("is-open");
|
||
modal.setAttribute("aria-hidden", "false");
|
||
window.setTimeout(() => {
|
||
input?.focus();
|
||
input?.select();
|
||
runSearch().catch((error) => {
|
||
console.warn("Running search failed:", error);
|
||
});
|
||
}, 16);
|
||
}
|
||
|
||
export function closeSearchPanel() {
|
||
const { modal } = getElements();
|
||
if (!modal) return;
|
||
modal.classList.remove("is-open");
|
||
modal.setAttribute("aria-hidden", "true");
|
||
}
|