106 lines
3.4 KiB
JavaScript
106 lines
3.4 KiB
JavaScript
// 联系人自动补全模块
|
|
const _acCache = {};
|
|
|
|
async function acFetchNames(field) {
|
|
if (_acCache[field] && Date.now() - _acCache[field].ts < 60000) return _acCache[field].data;
|
|
try {
|
|
const res = await apiFetch(API + '/api/contacts?field=' + field);
|
|
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) => {
|
|
let display = escapeHtml(n);
|
|
if (q) {
|
|
const idx = n.toLowerCase().indexOf(q);
|
|
if (idx >= 0) {
|
|
display = escapeHtml(n.slice(0, idx)) +
|
|
'<span class="ac-highlight">' + escapeHtml(n.slice(idx, idx + q.length)) + '</span>' +
|
|
escapeHtml(n.slice(idx + q.length));
|
|
}
|
|
}
|
|
return `<div class="ac-item" data-idx="${i}" data-value="${escapeAttr(n)}">${display}</div>`;
|
|
}).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');
|
|
});
|
|
}
|