/* OpsxDB 文档站交互:搜索 / 代码复制 / 目录高亮 / 反馈 / 移动端导航 */ (function () { "use strict"; /* ---------- 代码块:语言标签 + 复制按钮 + 轻量高亮 ---------- */ document.querySelectorAll("article.md pre").forEach(function (pre) { var code = pre.querySelector("code"); if (!code) return; var lang = (code.className.match(/language-([\w-]+)/) || [, ""])[1]; if (lang) { var tag = document.createElement("span"); tag.className = "lang-tag"; tag.textContent = lang; pre.appendChild(tag); } // 复制按钮 var btn = document.createElement("button"); btn.className = "copy-btn"; btn.type = "button"; btn.textContent = "复制"; btn.addEventListener("click", function () { navigator.clipboard.writeText(code.textContent).then( function () { btn.textContent = "已复制 ✓"; setTimeout(function () { btn.textContent = "复制"; }, 1400); }, function () { btn.textContent = "复制失败"; } ); }); pre.appendChild(btn); // JSON / bash 轻量高亮 if (lang === "json") highlightJSON(code); else if (lang === "bash" || lang === "sh" || lang === "shell") highlightBash(code); }); function esc(s) { return s.replace(/&/g, "&").replace(//g, ">"); } function highlightJSON(code) { var src = code.textContent; code.innerHTML = esc(src).replace( /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g, function (m, str, colon, kw, num) { if (str && colon) return '' + str + "" + colon; if (str) return '' + str + ""; if (kw) return '' + kw + ""; if (num) return '' + num + ""; return m; } ); } function highlightBash(code) { var lines = code.textContent.split("\n"); code.innerHTML = lines .map(function (line) { if (/^\s*#/.test(line)) return '' + esc(line) + ""; return esc(line).replace(/(^|\s)(-{1,2}[\w-]+)/g, '$1$2'); }) .join("\n"); } /* ---------- blockquote 提示样式约定:> **提示** / > **注意** ---------- */ document.querySelectorAll("article.md blockquote").forEach(function (bq) { var t = (bq.textContent || "").trim(); if (/^(注意|警告|⚠)/.test(t)) bq.classList.add("warn"); else if (/^(提示|说明|💡|Tip)/i.test(t)) bq.classList.add("tip"); }); /* ---------- 搜索 ---------- */ var input = document.getElementById("search-input"); var panel = document.getElementById("search-panel"); var entries = null; var selIndex = -1; function ensureIndex(cb) { if (entries) return cb(); fetch("/docs/search-data.json") .then(function (r) { return r.json(); }) .then(function (data) { entries = data; cb(); }) .catch(function () { entries = []; cb(); }); } function snippet(text, q) { var i = text.toLowerCase().indexOf(q); if (i < 0) return text.slice(0, 60) + "…"; var start = Math.max(0, i - 24); return (start > 0 ? "…" : "") + text.slice(start, i + q.length + 40) + "…"; } function doSearch() { var q = input.value.trim().toLowerCase(); selIndex = -1; if (!q) { panel.hidden = true; return; } ensureIndex(function () { var hits = []; entries.forEach(function (e) { var titleHit = e.title.toLowerCase().indexOf(q) >= 0; var idx = e.text.toLowerCase().indexOf(q); if (titleHit || idx >= 0) { hits.push({ slug: e.slug, title: e.title, group: e.group, score: (titleHit ? 10 : 0) + (idx === 0 ? 3 : 0), snippet: titleHit ? snippet(e.text, q) : snippet(e.text, q), }); } }); hits.sort(function (a, b) { return b.score - a.score; }); hits = hits.slice(0, 8); if (!hits.length) { panel.innerHTML = '
没有找到相关内容,换个关键词试试
'; } else { panel.innerHTML = hits .map(function (h) { return ( '' + '' + h.title + '' + h.group + "" + '' + h.snippet + "" ); }) .join(""); } panel.hidden = false; }); } var debounce; if (input) { input.addEventListener("input", function () { clearTimeout(debounce); debounce = setTimeout(doSearch, 160); }); input.addEventListener("focus", function () { if (input.value.trim()) doSearch(); }); input.addEventListener("keydown", function (e) { var items = panel.querySelectorAll(".search-item"); if (e.key === "ArrowDown" && items.length) { e.preventDefault(); selIndex = Math.min(selIndex + 1, items.length - 1); } else if (e.key === "ArrowUp" && items.length) { e.preventDefault(); selIndex = Math.max(selIndex - 1, 0); } else if (e.key === "Enter") { if (selIndex >= 0 && items[selIndex]) { location.href = items[selIndex].href; return; } var first = items[0]; if (first) location.href = first.href; return; } else if (e.key === "Escape") { panel.hidden = true; return; } else { return; } items.forEach(function (el, i) { el.classList.toggle("sel", i === selIndex); }); }); document.addEventListener("click", function (e) { if (!e.target.closest(".search")) panel.hidden = true; }); } /* ---------- 目录滚动高亮 ---------- */ var tocLinks = Array.prototype.slice.call(document.querySelectorAll(".toc-nav a")); if (tocLinks.length && "IntersectionObserver" in window) { var byId = {}; tocLinks.forEach(function (a) { byId[a.getAttribute("href").slice(1)] = a; }); var io = new IntersectionObserver( function (entries) { entries.forEach(function (en) { if (en.isIntersecting) { tocLinks.forEach(function (a) { a.classList.remove("active"); }); var a = byId[en.target.id]; if (a) a.classList.add("active"); } }); }, { rootMargin: "-72px 0px -70% 0px", threshold: 0 } ); document.querySelectorAll("article.md h2[id], article.md h3[id]").forEach(function (h) { io.observe(h); }); } /* ---------- 反馈组件 ---------- */ var fb = document.getElementById("feedback"); if (fb) { var pageKey = "opsxdb-docs-fb:" + location.pathname; var voted = null; try { voted = localStorage.getItem(pageKey); } catch (e) {} var btns = fb.querySelectorAll(".fb-btn"); var thanks = document.getElementById("fb-thanks"); function mark(v) { btns.forEach(function (b) { if (b.dataset.v === v) b.classList.add("done"); }); thanks.hidden = false; try { localStorage.setItem(pageKey, v); } catch (e) {} } if (voted) mark(voted); btns.forEach(function (b) { b.addEventListener("click", function () { mark(b.dataset.v); }); }); } /* ---------- 打印 ---------- */ var printBtn = document.getElementById("print-btn"); if (printBtn) printBtn.addEventListener("click", function () { window.print(); }); /* ---------- 移动端侧栏 ---------- */ var menuBtn = document.getElementById("menu-btn"); var sidebar = document.getElementById("sidebar"); if (menuBtn && sidebar) { menuBtn.addEventListener("click", function () { sidebar.classList.toggle("open"); }); document.addEventListener("click", function (e) { if (sidebar.classList.contains("open") && !e.target.closest(".sidebar") && !e.target.closest(".menu-btn")) { sidebar.classList.remove("open"); } }); } })();