init
This commit is contained in:
469
docs/js/app.js
Normal file
469
docs/js/app.js
Normal file
@@ -0,0 +1,469 @@
|
||||
/**
|
||||
* app.js — Main logic for sunvpy static documentation site.
|
||||
* Handles routing, markdown rendering, navigation, TOC, and theme.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// --- State ---
|
||||
let navData = null;
|
||||
let flatPages = []; // ordered array of all slugs
|
||||
let currentPage = null;
|
||||
let isDark = false;
|
||||
let mermaidInitialized = false;
|
||||
|
||||
// --- DOM refs ---
|
||||
const sidebarNav = document.getElementById("sidebarNav");
|
||||
const article = document.getElementById("article");
|
||||
const pageNav = document.getElementById("pageNav");
|
||||
const tocNav = document.getElementById("tocNav");
|
||||
const contentEl = document.getElementById("content");
|
||||
|
||||
// --- Init ---
|
||||
async function init() {
|
||||
// Theme
|
||||
const saved = localStorage.getItem("sunvpy-theme");
|
||||
if (saved === "dark" || (!saved && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
||||
setTheme(true, false);
|
||||
}
|
||||
|
||||
// Load nav
|
||||
try {
|
||||
const resp = await fetch("data/nav.json");
|
||||
navData = await resp.json();
|
||||
} catch (e) {
|
||||
article.innerHTML = '<div class="loading">无法加载导航数据。请通过 HTTP 服务器访问(如 python -m http.server)。</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build flat page list
|
||||
flatPages = [];
|
||||
navData.sections.forEach(function (section) {
|
||||
if (section.pages) {
|
||||
section.pages.forEach(function (p) { flatPages.push(p.slug); });
|
||||
}
|
||||
if (section.groups) {
|
||||
section.groups.forEach(function (group) {
|
||||
group.pages.forEach(function (p) { flatPages.push(p.slug); });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Build sidebar
|
||||
renderSidebar();
|
||||
|
||||
// Route
|
||||
handleRoute();
|
||||
window.addEventListener("hashchange", handleRoute);
|
||||
|
||||
// Theme toggle buttons
|
||||
document.getElementById("themeToggle").addEventListener("click", function () { setTheme(!isDark, true); });
|
||||
document.getElementById("themeToggleMobile").addEventListener("click", function () { setTheme(!isDark, true); });
|
||||
|
||||
// Mobile sidebar
|
||||
document.getElementById("hamburger").addEventListener("click", toggleMobileSidebar);
|
||||
document.getElementById("sidebarBackdrop").addEventListener("click", closeMobileSidebar);
|
||||
}
|
||||
|
||||
// --- Theme ---
|
||||
function setTheme(dark, save) {
|
||||
isDark = dark;
|
||||
document.documentElement.setAttribute("data-theme", dark ? "dark" : "light");
|
||||
|
||||
const lightSheet = document.getElementById("hljs-light-theme");
|
||||
const darkSheet = document.getElementById("hljs-dark-theme");
|
||||
lightSheet.disabled = dark;
|
||||
darkSheet.disabled = !dark;
|
||||
|
||||
if (save) localStorage.setItem("sunvpy-theme", dark ? "dark" : "light");
|
||||
|
||||
// Re-render mermaid if already initialized
|
||||
if (mermaidInitialized && currentPage) {
|
||||
rerenderMermaid();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sidebar ---
|
||||
function renderSidebar() {
|
||||
var html = "";
|
||||
navData.sections.forEach(function (section) {
|
||||
var isActive = currentPage && isSectionActive(section);
|
||||
html += '<div class="nav-section' + (isActive ? "" : " collapsed") + '" data-section="' + section.id + '">';
|
||||
html += '<div class="nav-section-header" onclick="toggleSection(this)">';
|
||||
html += '<svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
||||
html += escHtml(section.title);
|
||||
html += "</div>";
|
||||
html += '<div class="nav-section-body">';
|
||||
|
||||
if (section.pages) {
|
||||
section.pages.forEach(function (page) {
|
||||
html += renderPageLink(page);
|
||||
});
|
||||
}
|
||||
if (section.groups) {
|
||||
section.groups.forEach(function (group) {
|
||||
html += '<div class="nav-group">';
|
||||
html += '<div class="nav-group-label">' + escHtml(group.title) + "</div>";
|
||||
group.pages.forEach(function (page) {
|
||||
html += renderPageLink(page);
|
||||
});
|
||||
html += "</div>";
|
||||
});
|
||||
}
|
||||
|
||||
html += "</div></div>";
|
||||
});
|
||||
sidebarNav.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderPageLink(page) {
|
||||
var active = currentPage === page.slug ? " active" : "";
|
||||
var levelClass = page.level ? page.level.toLowerCase() : "";
|
||||
var badge = page.level ? ' <span class="level-badge ' + levelClass + '">' + escHtml(page.level.charAt(0)) + "</span>" : "";
|
||||
return '<a class="nav-page' + active + '" href="#' + page.slug + '">' + escHtml(page.title) + badge + "</a>";
|
||||
}
|
||||
|
||||
function isSectionActive(section) {
|
||||
if (!currentPage) return false;
|
||||
if (section.pages) {
|
||||
for (var i = 0; i < section.pages.length; i++) {
|
||||
if (section.pages[i].slug === currentPage) return true;
|
||||
}
|
||||
}
|
||||
if (section.groups) {
|
||||
for (var g = 0; g < section.groups.length; g++) {
|
||||
for (var j = 0; j < section.groups[g].pages.length; j++) {
|
||||
if (section.groups[g].pages[j].slug === currentPage) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Expose toggleSection globally for onclick
|
||||
window.toggleSection = function (header) {
|
||||
header.parentElement.classList.toggle("collapsed");
|
||||
};
|
||||
|
||||
// --- Routing ---
|
||||
function handleRoute() {
|
||||
var hash = window.location.hash.slice(1) || flatPages[0];
|
||||
if (!hash || hash === currentPage) return;
|
||||
loadPage(hash);
|
||||
}
|
||||
|
||||
// --- Load & Render Page ---
|
||||
async function loadPage(slug) {
|
||||
currentPage = slug;
|
||||
|
||||
// Update sidebar active state
|
||||
renderSidebar();
|
||||
|
||||
// Scroll content to top
|
||||
contentEl.scrollTop = 0;
|
||||
|
||||
// Show loading
|
||||
article.innerHTML = '<div class="loading">加载中...</div>';
|
||||
tocNav.innerHTML = "";
|
||||
pageNav.innerHTML = "";
|
||||
|
||||
// Fetch markdown
|
||||
var md;
|
||||
try {
|
||||
var resp = await fetch("content/" + slug + ".md");
|
||||
if (!resp.ok) throw new Error(resp.status);
|
||||
md = await resp.text();
|
||||
} catch (e) {
|
||||
article.innerHTML = '<div class="loading">无法加载页面: ' + escHtml(slug) + "</div>";
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-process: transform cross-doc links
|
||||
md = md.replace(/\]\((\d+-[\w-]+)\)/g, function (match, linkSlug) {
|
||||
// Only transform if it matches a known page slug
|
||||
if (flatPages.indexOf(linkSlug) !== -1) {
|
||||
return "](#" + linkSlug + ")";
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
// Configure marked
|
||||
var renderer = new marked.Renderer();
|
||||
|
||||
// Custom paragraph handler for "Sources:" lines
|
||||
renderer.paragraph = function (obj) {
|
||||
var text = typeof obj === "object" ? obj.text : obj;
|
||||
if (text && (text.indexOf("Sources:") === 0 || text.indexOf("Sources: [") === 0)) {
|
||||
return '<div class="source-ref">' + text + "</div>";
|
||||
}
|
||||
return "<p>" + text + "</p>";
|
||||
};
|
||||
|
||||
// Add IDs to headings
|
||||
var headingIndex = {};
|
||||
renderer.heading = function (text, depth) {
|
||||
// marked v15 passes heading data as object or text+depth
|
||||
var headingText = typeof text === "object" ? text.text : text;
|
||||
var headingDepth = typeof text === "object" ? text.depth : depth;
|
||||
var raw = headingText.replace(/<[^>]+>/g, "");
|
||||
var baseId = raw.toLowerCase()
|
||||
.replace(/[^\w\u4e00-\u9fff]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
if (!baseId) baseId = "heading";
|
||||
if (headingIndex[baseId] !== undefined) {
|
||||
headingIndex[baseId]++;
|
||||
baseId = baseId + "-" + headingIndex[baseId];
|
||||
} else {
|
||||
headingIndex[baseId] = 0;
|
||||
}
|
||||
return '<h' + headingDepth + ' id="' + escAttr(baseId) + '">' + headingText + "</h" + headingDepth + ">";
|
||||
};
|
||||
|
||||
// Code block handler
|
||||
renderer.code = function (obj) {
|
||||
var code, lang;
|
||||
if (typeof obj === "object") {
|
||||
code = obj.text;
|
||||
lang = obj.lang || "";
|
||||
} else {
|
||||
code = obj;
|
||||
lang = "";
|
||||
}
|
||||
if (lang === "mermaid") {
|
||||
return '<div class="mermaid-wrapper"><div class="mermaid" data-mermaid-source="' + escAttr(code) + '">' + escHtml(code) + "</div></div>";
|
||||
}
|
||||
var highlighted = "";
|
||||
try {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
highlighted = hljs.highlight(code, { language: lang }).value;
|
||||
} else {
|
||||
highlighted = hljs.highlightAuto(code).value;
|
||||
}
|
||||
} catch (e) {
|
||||
highlighted = escHtml(code);
|
||||
}
|
||||
var langLabel = lang ? '<span class="code-lang">' + escHtml(lang) + "</span>" : "";
|
||||
return "<pre>" + langLabel + '<code class="hljs language-' + escAttr(lang) + '">' + highlighted + "</code><button class=\"copy-btn\" onclick=\"copyCode(this)\">复制</button></pre>";
|
||||
};
|
||||
|
||||
marked.use({
|
||||
renderer: renderer,
|
||||
gfm: true,
|
||||
breaks: false,
|
||||
});
|
||||
|
||||
// Render
|
||||
var html;
|
||||
try {
|
||||
html = marked.parse(md);
|
||||
} catch (e) {
|
||||
console.error("Markdown render error:", e);
|
||||
article.innerHTML = '<div class="loading">渲染出错: ' + escHtml(String(e)) + '</div>';
|
||||
return;
|
||||
}
|
||||
article.innerHTML = html;
|
||||
|
||||
// Add page title from nav data
|
||||
var pageInfo = findPageInfo(slug);
|
||||
if (pageInfo) {
|
||||
var titleDiv = document.createElement("div");
|
||||
titleDiv.className = "page-title";
|
||||
titleDiv.innerHTML = "<h1>" + escHtml(pageInfo.title) + "</h1>";
|
||||
if (pageInfo.section) {
|
||||
titleDiv.innerHTML += '<div class="page-meta">' + escHtml(pageInfo.section);
|
||||
if (pageInfo.level) titleDiv.innerHTML += ' · <span class="level-badge ' + pageInfo.level.toLowerCase() + '">' + escHtml(pageInfo.level) + "</span>";
|
||||
titleDiv.innerHTML += "</div>";
|
||||
}
|
||||
article.insertBefore(titleDiv, article.firstChild);
|
||||
}
|
||||
|
||||
// Render mermaid diagrams
|
||||
await renderMermaid();
|
||||
|
||||
// Build TOC
|
||||
buildTOC();
|
||||
|
||||
// Prev/Next navigation
|
||||
renderPageNav();
|
||||
|
||||
// Update document title
|
||||
document.title = (pageInfo ? pageInfo.title + " — " : "") + "sunvpy 文档";
|
||||
}
|
||||
|
||||
// --- Mermaid ---
|
||||
async function renderMermaid() {
|
||||
var mermaidDivs = article.querySelectorAll(".mermaid");
|
||||
if (mermaidDivs.length === 0) return;
|
||||
|
||||
if (!mermaidInitialized) {
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: isDark ? "dark" : "default",
|
||||
securityLevel: "loose",
|
||||
maxTextSize: 90000,
|
||||
});
|
||||
mermaidInitialized = true;
|
||||
}
|
||||
|
||||
try {
|
||||
await mermaid.run({ nodes: mermaidDivs, suppressErrors: true });
|
||||
} catch (e) {
|
||||
console.warn("Mermaid render error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function rerenderMermaid() {
|
||||
var wrappers = article.querySelectorAll(".mermaid-wrapper");
|
||||
wrappers.forEach(function (wrapper) {
|
||||
var div = wrapper.querySelector(".mermaid");
|
||||
if (!div) return;
|
||||
var source = div.getAttribute("data-mermaid-source");
|
||||
if (source) {
|
||||
div.removeAttribute("data-processed");
|
||||
div.innerHTML = escHtml(source);
|
||||
}
|
||||
});
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: isDark ? "dark" : "default",
|
||||
securityLevel: "loose",
|
||||
maxTextSize: 90000,
|
||||
});
|
||||
|
||||
try {
|
||||
await mermaid.run({ nodes: article.querySelectorAll(".mermaid"), suppressErrors: true });
|
||||
} catch (e) {
|
||||
console.warn("Mermaid re-render error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- TOC ---
|
||||
function buildTOC() {
|
||||
var headings = article.querySelectorAll("h2, h3");
|
||||
if (headings.length === 0) {
|
||||
tocNav.innerHTML = '<div style="color:var(--text-muted);font-size:0.82rem">无目录</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = "<ul>";
|
||||
headings.forEach(function (h) {
|
||||
var cls = h.tagName === "H3" ? "toc-h3" : "";
|
||||
html += '<li><a class="' + cls + '" href="#' + escAttr(h.id) + '">' + escHtml(h.textContent) + "</a></li>";
|
||||
});
|
||||
html += "</ul>";
|
||||
tocNav.innerHTML = html;
|
||||
|
||||
// Scroll spy
|
||||
setupScrollSpy(headings);
|
||||
}
|
||||
|
||||
function setupScrollSpy(headings) {
|
||||
var links = tocNav.querySelectorAll("a");
|
||||
|
||||
contentEl.removeEventListener("scroll", scrollSpyHandler);
|
||||
contentEl.addEventListener("scroll", scrollSpyHandler);
|
||||
|
||||
function scrollSpyHandler() {
|
||||
var scrollTop = contentEl.scrollTop;
|
||||
var activeIdx = 0;
|
||||
for (var i = headings.length - 1; i >= 0; i--) {
|
||||
if (headings[i].offsetTop - 100 <= scrollTop) {
|
||||
activeIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
links.forEach(function (link, idx) {
|
||||
link.classList.toggle("active", idx === activeIdx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Prev/Next ---
|
||||
function renderPageNav() {
|
||||
var idx = flatPages.indexOf(currentPage);
|
||||
if (idx === -1) { pageNav.innerHTML = ""; return; }
|
||||
|
||||
var html = "";
|
||||
if (idx > 0) {
|
||||
var prevInfo = findPageInfo(flatPages[idx - 1]);
|
||||
html += '<a class="page-nav-btn prev" href="#' + flatPages[idx - 1] + '">';
|
||||
html += '<span class="label">上一节</span>';
|
||||
html += '<span class="title">' + escHtml(prevInfo ? prevInfo.title : flatPages[idx - 1]) + "</span>";
|
||||
html += "</a>";
|
||||
} else {
|
||||
html += "<div></div>";
|
||||
}
|
||||
if (idx < flatPages.length - 1) {
|
||||
var nextInfo = findPageInfo(flatPages[idx + 1]);
|
||||
html += '<a class="page-nav-btn next" href="#' + flatPages[idx + 1] + '">';
|
||||
html += '<span class="label">下一节</span>';
|
||||
html += '<span class="title">' + escHtml(nextInfo ? nextInfo.title : flatPages[idx + 1]) + "</span>";
|
||||
html += "</a>";
|
||||
}
|
||||
pageNav.innerHTML = html;
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
function findPageInfo(slug) {
|
||||
for (var s = 0; s < navData.sections.length; s++) {
|
||||
var section = navData.sections[s];
|
||||
if (section.pages) {
|
||||
for (var i = 0; i < section.pages.length; i++) {
|
||||
if (section.pages[i].slug === slug) {
|
||||
return Object.assign({}, section.pages[i], { section: section.title });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (section.groups) {
|
||||
for (var g = 0; g < section.groups.length; g++) {
|
||||
for (var j = 0; j < section.groups[g].pages.length; j++) {
|
||||
if (section.groups[g].pages[j].slug === slug) {
|
||||
return Object.assign({}, section.groups[g].pages[j], { section: section.title });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
if (!s) return "";
|
||||
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
|
||||
function escAttr(s) {
|
||||
return escHtml(s);
|
||||
}
|
||||
|
||||
// Copy code to clipboard
|
||||
window.copyCode = function (btn) {
|
||||
var code = btn.parentElement.querySelector("code");
|
||||
var text = code.textContent;
|
||||
navigator.clipboard.writeText(text).then(function () {
|
||||
btn.textContent = "已复制";
|
||||
setTimeout(function () { btn.textContent = "复制"; }, 1500);
|
||||
});
|
||||
};
|
||||
|
||||
// --- Mobile sidebar ---
|
||||
function toggleMobileSidebar() {
|
||||
document.getElementById("sidebar").classList.toggle("open");
|
||||
document.getElementById("sidebarBackdrop").classList.toggle("open");
|
||||
}
|
||||
function closeMobileSidebar() {
|
||||
document.getElementById("sidebar").classList.remove("open");
|
||||
document.getElementById("sidebarBackdrop").classList.remove("open");
|
||||
}
|
||||
|
||||
// Close mobile sidebar on nav
|
||||
document.addEventListener("click", function (e) {
|
||||
if (e.target.classList.contains("nav-page")) {
|
||||
closeMobileSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Start ---
|
||||
init();
|
||||
})();
|
||||
161
docs/js/search.js
Normal file
161
docs/js/search.js
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* search.js — Full-text search for sunvpy docs using Fuse.js
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var fuse = null;
|
||||
var searchIndex = null;
|
||||
var searchOverlay = document.getElementById("searchOverlay");
|
||||
var searchInput = document.getElementById("searchInput");
|
||||
var searchResults = document.getElementById("searchResults");
|
||||
var debounceTimer = null;
|
||||
|
||||
// Open search
|
||||
function openSearch() {
|
||||
searchOverlay.classList.add("active");
|
||||
searchInput.value = "";
|
||||
searchResults.innerHTML = '<div class="search-hint">输入关键词开始搜索</div>';
|
||||
searchInput.focus();
|
||||
loadIndex();
|
||||
}
|
||||
|
||||
// Close search
|
||||
function closeSearch() {
|
||||
searchOverlay.classList.remove("active");
|
||||
searchInput.blur();
|
||||
}
|
||||
|
||||
// Lazy-load search index
|
||||
async function loadIndex() {
|
||||
if (searchIndex) return;
|
||||
try {
|
||||
var resp = await fetch("data/search-index.json");
|
||||
searchIndex = await resp.json();
|
||||
fuse = new Fuse(searchIndex, {
|
||||
keys: [
|
||||
{ name: "title", weight: 0.4 },
|
||||
{ name: "headings", weight: 0.3 },
|
||||
{ name: "content", weight: 0.3 },
|
||||
],
|
||||
threshold: 0.3,
|
||||
ignoreLocation: true,
|
||||
includeMatches: true,
|
||||
minMatchCharLength: 2,
|
||||
});
|
||||
} catch (e) {
|
||||
searchResults.innerHTML = '<div class="search-hint">无法加载搜索索引</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Perform search
|
||||
function doSearch(query) {
|
||||
if (!fuse || !query.trim()) {
|
||||
searchResults.innerHTML = '<div class="search-hint">输入关键词开始搜索</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var results = fuse.search(query, { limit: 20 });
|
||||
|
||||
if (results.length === 0) {
|
||||
searchResults.innerHTML = '<div class="search-hint">未找到匹配结果</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = "";
|
||||
results.forEach(function (result) {
|
||||
var item = result.item;
|
||||
var context = getContext(result.matches, item);
|
||||
html += '<a class="search-result-item" href="#' + escAttr(item.slug) + '" onclick="closeSearch()">'
|
||||
+ '<div class="result-title">' + escHtml(item.title) + "</div>"
|
||||
+ '<div class="result-section">' + escHtml(item.section) + "</div>"
|
||||
+ '<div class="result-context">' + context + "</div>"
|
||||
+ "</a>";
|
||||
});
|
||||
searchResults.innerHTML = html;
|
||||
}
|
||||
|
||||
// Get matching context snippet
|
||||
function getContext(matches, item) {
|
||||
if (!matches || matches.length === 0) return "";
|
||||
|
||||
// Prefer title/content match context
|
||||
for (var i = 0; i < matches.length; i++) {
|
||||
var m = matches[i];
|
||||
if (m.key === "content" && m.indices && m.indices.length > 0) {
|
||||
var idx = m.indices[0];
|
||||
var start = Math.max(0, idx[0] - 30);
|
||||
var end = Math.min(item.content.length, idx[1] + 30);
|
||||
var snippet = (start > 0 ? "..." : "") + item.content.substring(start, end) + (end < item.content.length ? "..." : "");
|
||||
return highlightMatch(snippet, m.value.substring(idx[0], idx[1] + 1));
|
||||
}
|
||||
if (m.key === "headings" && m.indices && m.indices.length > 0) {
|
||||
var arr = item.headings;
|
||||
for (var h = 0; h < arr.length; h++) {
|
||||
if (arr[h].toLowerCase().indexOf(m.value.toLowerCase()) !== -1) {
|
||||
return highlightMatch(arr[h], m.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function highlightMatch(text, match) {
|
||||
if (!match) return escHtml(text);
|
||||
var escaped = escHtml(text);
|
||||
var escapedMatch = escHtml(match);
|
||||
return escaped.replace(new RegExp(escapeRegex(escapedMatch), "gi"), "<mark>$&</mark>");
|
||||
}
|
||||
|
||||
function escapeRegex(s) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
// Close search global
|
||||
window.closeSearch = closeSearch;
|
||||
|
||||
// Event listeners
|
||||
searchInput.addEventListener("input", function () {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(function () {
|
||||
doSearch(searchInput.value);
|
||||
}, 200);
|
||||
});
|
||||
|
||||
document.getElementById("searchClose").addEventListener("click", closeSearch);
|
||||
searchOverlay.addEventListener("click", function (e) {
|
||||
if (e.target === searchOverlay) closeSearch();
|
||||
});
|
||||
|
||||
// Keyboard shortcut: Ctrl+K / Cmd+K
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
||||
e.preventDefault();
|
||||
openSearch();
|
||||
}
|
||||
if (e.key === "Escape" && searchOverlay.classList.contains("active")) {
|
||||
closeSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Sidebar search button
|
||||
document.getElementById("sidebarSearchBtn").addEventListener("click", openSearch);
|
||||
var mobileSearchBtn = document.getElementById("searchBtnMobile");
|
||||
if (mobileSearchBtn) mobileSearchBtn.addEventListener("click", openSearch);
|
||||
|
||||
// Navigate on result click
|
||||
searchResults.addEventListener("click", function (e) {
|
||||
var item = e.target.closest(".search-result-item");
|
||||
if (item) {
|
||||
closeSearch();
|
||||
}
|
||||
});
|
||||
|
||||
function escHtml(s) {
|
||||
if (!s) return "";
|
||||
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
|
||||
function escAttr(s) { return escHtml(s); }
|
||||
})();
|
||||
Reference in New Issue
Block a user