Files
gamer/public/js/autocomplete.js
T

106 lines
3.4 KiB
JavaScript
Raw Normal View History

2026-03-12 11:23:10 +08:00
// 联系人自动补全模块
const _acCache = {};
async function acFetchNames(field) {
if (_acCache[field] && Date.now() - _acCache[field].ts < 60000) return _acCache[field].data;
try {
2026-07-01 16:13:47 +08:00
const res = await apiFetch(API + '/api/contacts?field=' + field);
2026-03-12 11:23:10 +08:00
if (!res.ok) return [];
const data = await res.json();
_acCache[field] = { data, ts: Date.now() };
return data;
} catch { return []; }
}
function acInvalidateCache(field) {
if (field) delete _acCache[field];
else Object.keys(_acCache).forEach(k => delete _acCache[k]);
}
function acSetup(inputId, field) {
const input = document.getElementById(inputId);
if (!input || input._acBound) return;
input._acBound = true;
input.setAttribute('autocomplete', 'off');
const wrap = document.createElement('div');
wrap.className = 'ac-wrap';
input.parentNode.insertBefore(wrap, input);
wrap.appendChild(input);
const list = document.createElement('div');
list.className = 'ac-list';
wrap.appendChild(list);
let names = [];
let activeIdx = -1;
async function loadNames() {
names = await acFetchNames(field);
}
function render(query) {
const q = query.trim().toLowerCase();
const filtered = q ? names.filter(n => n.toLowerCase().includes(q)) : names;
if (filtered.length === 0) { list.classList.remove('show'); return; }
activeIdx = -1;
list.innerHTML = filtered.slice(0, 10).map((n, i) => {
2026-07-01 16:13:47 +08:00
let display = escapeHtml(n);
2026-03-12 11:23:10 +08:00
if (q) {
const idx = n.toLowerCase().indexOf(q);
if (idx >= 0) {
2026-07-01 16:13:47 +08:00
display = escapeHtml(n.slice(0, idx)) +
'<span class="ac-highlight">' + escapeHtml(n.slice(idx, idx + q.length)) + '</span>' +
escapeHtml(n.slice(idx + q.length));
2026-03-12 11:23:10 +08:00
}
}
2026-07-01 16:13:47 +08:00
return `<div class="ac-item" data-idx="${i}" data-value="${escapeAttr(n)}">${display}</div>`;
2026-03-12 11:23:10 +08:00
}).join('');
list.classList.add('show');
}
function pick(value) {
input.value = value;
list.classList.remove('show');
input.dispatchEvent(new Event('input', { bubbles: true }));
}
input.addEventListener('focus', async () => {
await loadNames();
render(input.value);
});
input.addEventListener('input', () => { render(input.value); });
input.addEventListener('keydown', (e) => {
const items = list.querySelectorAll('.ac-item');
if (!list.classList.contains('show') || items.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
activeIdx = Math.min(activeIdx + 1, items.length - 1);
items.forEach((el, i) => el.classList.toggle('active', i === activeIdx));
items[activeIdx].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp') {
e.preventDefault();
activeIdx = Math.max(activeIdx - 1, 0);
items.forEach((el, i) => el.classList.toggle('active', i === activeIdx));
items[activeIdx].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'Enter' && activeIdx >= 0) {
e.preventDefault();
pick(items[activeIdx].dataset.value);
} else if (e.key === 'Escape') {
list.classList.remove('show');
}
});
list.addEventListener('mousedown', (e) => {
e.preventDefault(); // 防止 input 失焦
const item = e.target.closest('.ac-item');
if (item) pick(item.dataset.value);
});
document.addEventListener('click', (e) => {
if (!wrap.contains(e.target)) list.classList.remove('show');
});
}