asset_browser/public/modules/searchAutocomplete.js
2026-03-27 09:18:39 -04:00

153 lines
4.9 KiB
JavaScript
Executable file

// searchAutocomplete.js — universal search dropdown for the scan/search input.
import { searchLocal } from './assetBrowser.js';
let _onLocalSelect = null;
let _onRemoteSearch = null;
let _input = null;
let _dropdown = null;
let _items = []; // { type:'asset'|'syncro', asset?, serial?, contact?, customerName?, query? }
let _activeIdx = -1;
let _inputTimer = null;
let _blurTimer = null;
// ── Init ──────────────────────────────────────────────────────────────────────
export function initSearchAutocomplete({ onLocalSelect, onRemoteSearch }) {
_onLocalSelect = onLocalSelect;
_onRemoteSearch = onRemoteSearch;
_input = document.getElementById('scan-input');
if (!_input) return;
// Append dropdown inside scan-input-wrap so it's positioned relative to it
_dropdown = document.createElement('div');
_dropdown.id = 'search-autocomplete';
_dropdown.className = 'search-autocomplete';
_dropdown.hidden = true;
_input.closest('.scan-input-wrap').appendChild(_dropdown);
_input.addEventListener('input', _onInput);
_input.addEventListener('blur', () => {
_blurTimer = setTimeout(_close, 150);
});
_input.addEventListener('focus', () => {
clearTimeout(_blurTimer);
if (_input.value.trim().length >= 2) _onInput();
});
// Prevent blur when clicking a dropdown item
_dropdown.addEventListener('mousedown', e => e.preventDefault());
_dropdown.addEventListener('click', e => {
const item = e.target.closest('.ac-item');
if (item) _selectIdx(Number(item.dataset.idx));
});
}
// ── Key handler — called by scanner's key interceptor ─────────────────────────
export function handleAutocompleteKey(e) {
if (_dropdown.hidden) return false;
if (e.key === 'ArrowDown') {
e.preventDefault();
_setActive(Math.min(_activeIdx + 1, _items.length - 1));
return true;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
_setActive(Math.max(_activeIdx - 1, 0));
return true;
}
if (e.key === 'Enter') {
if (_activeIdx >= 0) {
e.preventDefault();
_selectIdx(_activeIdx);
return true;
}
_close(); // close but let scanner handle Enter
return false;
}
if (e.key === 'Escape') {
_close();
return true; // prevent scanner's clearInput
}
return false;
}
// ── Internal ──────────────────────────────────────────────────────────────────
function _onInput() {
clearTimeout(_inputTimer);
_inputTimer = setTimeout(() => {
const query = _input.value.trim();
if (query.length < 2) { _close(); return; }
_renderDropdown(query);
}, 200);
}
function _renderDropdown(query) {
const localResults = searchLocal(query);
_items = [
...localResults.map(r => ({ type: 'asset', ...r })),
{ type: 'syncro', query },
];
_activeIdx = -1;
_dropdown.innerHTML = _items.map((item, i) => {
if (item.type === 'syncro') {
return `<div class="ac-item ac-item-syncro" data-idx="${i}">
<svg class="ac-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<span>Search Syncro for <em>${_esc(item.query)}</em></span>
</div>`;
}
const { asset, serial, contact, customerName } = item;
const meta = [customerName, serial, contact].filter(Boolean).join(' · ');
return `<div class="ac-item" data-idx="${i}">
<svg class="ac-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
</svg>
<div class="ac-item-body">
<div class="ac-item-name">${_esc(asset.name ?? `Asset ${asset.id}`)}</div>
${meta ? `<div class="ac-item-meta">${_esc(meta)}</div>` : ''}
</div>
</div>`;
}).join('');
_dropdown.hidden = false;
}
function _setActive(idx) {
_activeIdx = idx;
_dropdown.querySelectorAll('.ac-item').forEach((el, i) => {
el.classList.toggle('ac-active', i === idx);
if (i === idx) el.scrollIntoView({ block: 'nearest' });
});
}
function _selectIdx(idx) {
const item = _items[idx];
if (!item) return;
_close();
_input.value = '';
_input.dispatchEvent(new Event('input')); // sync clear-btn visibility
if (item.type === 'asset') {
_onLocalSelect?.(item.asset);
} else {
_onRemoteSearch?.(item.query);
}
}
function _close() {
_dropdown.hidden = true;
_activeIdx = -1;
_items = [];
}
function _esc(s) {
return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}