diff --git a/assets/css/style.css b/assets/css/style.css index 72cd779..06faa3d 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -94,7 +94,7 @@ a { text-decoration: none; color: inherit; } font-size: 15px; border-radius: 10px; color: #3d4753; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -#word li:hover { background: var(--primary-soft); color: var(--primary); } +#word li:hover, #word li.active { background: var(--primary-soft); color: var(--primary); } /* ---------- 板块卡片 ---------- */ .mylist { diff --git a/assets/js/app.js b/assets/js/app.js index 4d01363..e18e440 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -30,11 +30,20 @@ var timer = null; var lastQ = null; var seq = 0; // 请求序号:防止先发的慢响应覆盖后发的快响应 + var items = []; // 当前展示的建议词 + var active = -1; // 选中项下标(键盘 ↓/↑ 与鼠标划过共用);-1 = 没选中 - function hideWord() { if (word) { word.innerHTML = ''; word.style.display = 'none'; } } - function showWord(items) { + function hideWord() { + if (word) { word.innerHTML = ''; word.style.display = 'none'; } + items = []; + active = -1; + } + + function showWord(list) { if (!word) return; - if (!items || !items.length) { hideWord(); 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; @@ -43,6 +52,14 @@ 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(); @@ -60,18 +77,56 @@ if (input) { input.addEventListener('input', function () { clearTimeout(timer); + active = -1; // 一开始打字就取消选中:之后回车应当用输入框原文 + paintWord(); timer = setTimeout(askSuggest, 180); }); input.addEventListener('keydown', function (e) { - if (e.key === 'Enter' && word && word.style.display === 'block' && word.firstChild) { - // 默认走第一条建议 - input.value = word.firstChild.textContent; - hideWord(); + var open = !!word && word.style.display === 'block' && items.length > 0; + + // ↓ / ↑:在下拉里移动选中项(只动高亮,不动输入框;回车时才落到输入框) + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + if (!open) { return; } + e.preventDefault(); // 别让光标跳到文本首尾、也别让页面跟着滚 + if (active < 0) { + active = e.key === 'ArrowDown' ? 0 : items.length - 1; + } else { + active = (active + (e.key === 'ArrowDown' ? 1 : -1) + 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;