/* 简航 SimpleNav —— 前台交互:搜索引擎切换 + 联想下拉 + 合集标签页 */ (function () { 'use strict'; var cfg = window.SIMPLENAV || {}; /* ---------- 搜索引擎切换 ---------- */ var tabs = document.querySelectorAll('#engine-tabs .s-engine'); var form = document.getElementById('search-form'); var input = document.getElementById('search-text'); function activateEngine(el) { tabs.forEach(function (t) { t.classList.remove('active'); }); el.classList.add('active'); var search = el.getAttribute('data-search'); var param = el.getAttribute('data-param'); var slug = el.getAttribute('data-slug'); var ph = el.getAttribute('data-placeholder') || '搜索'; if (form) { form.setAttribute('action', search); form.setAttribute('data-slug', slug); } if (input) { input.setAttribute('name', param); input.setAttribute('placeholder', ph); } hideWord(); // 已经输了字就顺手换一份建议(auto 模式下各引擎来源不同) if (input && input.value.trim() !== '') { askSuggest(); } } tabs.forEach(function (el) { el.addEventListener('click', function () { activateEngine(el); input && input.focus(); }); }); /* ---------- 联想下拉 ---------- */ var word = document.getElementById('word'); var timer = null; var lastQ = null; var seq = 0; // 请求序号:防止先发的慢响应覆盖后发的快响应 var items = []; // 当前展示的建议词 var active = -1; // 选中项下标(键盘 ↓/↑、,/. 与鼠标划过共用);-1 = 没选中 /* 联想词的选中按键(后台「设置 → 联想词按键」): arrows(默认,等同改造前)/ punct / both */ var keyMode = cfg.suggestKeys || 'arrows'; function acceptsArrows() { return keyMode === 'arrows' || keyMode === 'both'; } function acceptsPunct() { return keyMode === 'punct' || keyMode === 'both'; } /* ⚠️ 认**物理键**而不是字符:中文输入法下按逗号打出的是全角「,」,e.key 会变成「,」, 但 e.code 仍然是 'Comma' —— 「跟输入法一样」就该按物理键认,否则中文用户按不出来。 */ function isComma(e) { return e.code === 'Comma' || e.key === ','; } function isPeriod(e) { return e.code === 'Period' || e.key === '.'; } function hideWord() { if (word) { word.innerHTML = ''; word.style.display = 'none'; } items = []; active = -1; } function showWord(list) { if (!word) return; items = (list || []).slice(); active = -1; // 换了新建议就取消选中:此时回车应当用输入框原文 if (!items.length) { hideWord(); return; } word.innerHTML = items.map(function (t) { var d = document.createElement('li'); d.textContent = t; return d.outerHTML; }).join(''); word.style.display = 'block'; } /** 把 .active 标到当前选中项上(键盘与鼠标共用这一个状态) */ function paintWord() { if (!word) return; Array.prototype.forEach.call(word.children, function (li, i) { if (i === active) { li.classList.add('active'); } else { li.classList.remove('active'); } }); } function askSuggest() { if (!input) return; var q = input.value.trim(); if (q === '') { lastQ = null; hideWord(); return; } if (q === lastQ) { return; } lastQ = q; var mine = ++seq; var slug = form ? (form.getAttribute('data-slug') || '') : ''; fetch(cfg.suggest + '?slug=' + encodeURIComponent(slug) + '&q=' + encodeURIComponent(q)) .then(function (r) { return r.json(); }) .then(function (data) { if (mine === seq) { showWord(data && data.items); } }) .catch(function () { if (mine === seq) { hideWord(); } }); } if (input) { input.addEventListener('input', function () { clearTimeout(timer); active = -1; // 一开始打字就取消选中:之后回车应当用输入框原文 paintWord(); timer = setTimeout(askSuggest, 180); }); input.addEventListener('keydown', function (e) { var open = !!word && word.style.display === 'block' && items.length > 0; // 移动选中项:只动高亮、不动输入框(回车时才落到输入框)。 // 用哪组键由后台「联想词按键」决定:arrows = ↓/↑ / punct = , 和 . / both = 都行。 var dir = 0; if (acceptsArrows() && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { dir = e.key === 'ArrowDown' ? 1 : -1; } else if (acceptsPunct() && isComma(e)) { dir = -1; // , = 上一条(同 ↑) } else if (acceptsPunct() && isPeriod(e)) { dir = 1; // . = 下一条(同 ↓) } if (dir !== 0) { // ⚠️ 没有下拉时**必须放行** —— 这时 , 和 . 是正常输入字符(如搜「python, pandas」)。 if (!open) { return; } e.preventDefault(); // 别让光标跳到文本首尾、也别让页面跟着滚 if (active < 0) { active = dir > 0 ? 0 : items.length - 1; } else { active = (active + dir + items.length) % items.length; } paintWord(); return; } if (e.key === 'Escape') { hideWord(); return; } if (e.key === 'Enter') { // 只有**用 ↓/↑ 选中**了某条建议,回车才按建议词搜索。 // 没选中就一律用输入框里的原文 —— 老实现是「回车就套用第一条建议」, // 会把用户自己输入、而建议里没有的词悄悄换掉。 if (open && active >= 0) { input.value = items[active]; hideWord(); e.preventDefault(); if (form) { form.submit(); } } // active < 0:不拦截,交给浏览器默认的表单提交(即用输入框原文搜) return; } }); } /* 鼠标划过某条建议 → 同步高亮(与 ↓/↑ 共用 active,回车即可确认) */ document.addEventListener('mouseover', function (e) { if (!word || !e.target.closest) { return; } var hovered = e.target.closest('#word li'); if (!hovered) { return; } Array.prototype.forEach.call(word.children, function (li, i) { if (li === hovered) { active = i; } }); paintWord(); }); document.addEventListener('click', function (e) { if (word && !word.contains(e.target) && e.target !== input) { hideWord(); } var li = e.target.closest ? e.target.closest('#word li') : null; if (li && input) { input.value = li.textContent; hideWord(); if (form) { form.submit(); } } }); /* ---------- 合集标签页 ---------- */ var collTabs = document.querySelectorAll('#coll-tabs .tab'); collTabs.forEach(function (tab) { tab.addEventListener('click', function () { collTabs.forEach(function (t) { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); }); tab.classList.add('active'); tab.setAttribute('aria-selected', 'true'); document.querySelectorAll('.tab-panel').forEach(function (p) { p.classList.remove('active'); }); var panel = document.getElementById(tab.getAttribute('data-target')); if (panel) { panel.classList.add('active'); } }); }); /* ---------- 图标加载失败 → 退回首字母块(与 PHP 端配色一致) ---------- */ var PALETTE = ['#2F6BF7', '#5B8DEF', '#0EA5E9', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#14B8A6', '#F97316']; function colorOf(s) { var h = 0; for (var i = 0; i < s.length; i++) { h = (h * 31 + s.charCodeAt(i)) | 0; } return PALETTE[Math.abs(h) % PALETTE.length]; } document.querySelectorAll('.mylist img.icon').forEach(function (img) { img.addEventListener('error', function () { var a = img.closest('a'); if (!a) { img.style.visibility = 'hidden'; return; } var nameEl = a.querySelector('.name'); var label = (nameEl ? nameEl.textContent : '?').trim() || '?'; var span = document.createElement('span'); span.className = 'ico-letter'; span.style.background = colorOf(label); span.textContent = label.charAt(0); img.parentNode.replaceChild(span, img); }, { once: true }); }); })();