fix(autocomplete): add keyboard navigation and fix scroll/blur in all CODMAT dropdowns
Extract shared setupAutocomplete() into shared.js so all three autocomplete instances (mappings modal, inline add, quick-map modal) get keyboard nav (ArrowDown/Up/Enter/Escape), scroll-safe blur handling, and capture-phase keydown to prevent browser interception. Remove old onmousedown inline handlers, use data-codmat/data-label attributes instead. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -204,9 +204,126 @@ function renderMobileSegmented(containerId, pills, onSelect) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Shared Autocomplete ─────────────────────────
|
||||
|
||||
function setupAutocomplete(input, dropdown, selectedEl, fetchFn) {
|
||||
let activeIndex = -1;
|
||||
let acTimeout = null;
|
||||
|
||||
// Force-disable browser native autocomplete
|
||||
input.setAttribute('autocomplete', 'off');
|
||||
input.setAttribute('autocorrect', 'off');
|
||||
input.setAttribute('autocapitalize', 'off');
|
||||
input.setAttribute('spellcheck', 'false');
|
||||
if (!input.name) input.name = 'ac_' + Math.random().toString(36).slice(2, 8);
|
||||
|
||||
// Debounced input → fetch
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(acTimeout);
|
||||
acTimeout = setTimeout(() => {
|
||||
activeIndex = -1;
|
||||
fetchFn(input, dropdown, selectedEl);
|
||||
}, 250);
|
||||
});
|
||||
|
||||
// Prevent blur when interacting with dropdown (scroll, click)
|
||||
dropdown.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Click selection on items (delegated)
|
||||
dropdown.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('.autocomplete-item');
|
||||
if (!item) return;
|
||||
selectItem(item);
|
||||
});
|
||||
|
||||
// Switch back to mouse mode on mousemove
|
||||
dropdown.addEventListener('mousemove', () => {
|
||||
dropdown.classList.remove('keyboard-active');
|
||||
});
|
||||
|
||||
// Blur → close dropdown
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => dropdown.classList.add('d-none'), 150);
|
||||
});
|
||||
|
||||
// Keyboard navigation — capture phase to beat browser/extensions
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (dropdown.classList.contains('d-none')) return;
|
||||
const items = dropdown.querySelectorAll('.autocomplete-item');
|
||||
if (!items.length) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dropdown.classList.add('keyboard-active');
|
||||
if (activeIndex < items.length - 1) activeIndex++;
|
||||
updateActive(items);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dropdown.classList.add('keyboard-active');
|
||||
if (activeIndex > 0) activeIndex--;
|
||||
updateActive(items);
|
||||
} else if (e.key === 'Enter') {
|
||||
if (activeIndex >= 0 && activeIndex < items.length) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectItem(items[activeIndex]);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
dropdown.classList.add('d-none');
|
||||
activeIndex = -1;
|
||||
}
|
||||
}, true); // capture phase
|
||||
|
||||
// Track dropdown open/close
|
||||
input.setAttribute('aria-expanded', 'false');
|
||||
|
||||
function updateActive(items) {
|
||||
items.forEach((it, i) => {
|
||||
const isActive = i === activeIndex;
|
||||
it.classList.toggle('active', isActive);
|
||||
if (isActive) {
|
||||
it.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function selectItem(item) {
|
||||
const codmat = item.dataset.codmat;
|
||||
const label = item.dataset.label;
|
||||
if (!codmat) return;
|
||||
|
||||
// Find parent context: .codmat-line (mappings modal), .qm-line (quick-map), or inline
|
||||
const line = input.closest('.codmat-line') || input.closest('.qm-line');
|
||||
if (line) {
|
||||
const codmatInput = line.querySelector('.cl-codmat') || line.querySelector('.qm-codmat');
|
||||
const selectedLabel = line.querySelector('.cl-selected') || line.querySelector('.qm-selected');
|
||||
if (codmatInput) codmatInput.value = codmat;
|
||||
if (selectedLabel) selectedLabel.textContent = label;
|
||||
} else {
|
||||
// Inline context
|
||||
input.value = codmat;
|
||||
selectedEl.textContent = label;
|
||||
}
|
||||
dropdown.classList.add('d-none');
|
||||
input.setAttribute('aria-expanded', 'false');
|
||||
activeIndex = -1;
|
||||
}
|
||||
|
||||
// Observe dropdown visibility for aria-expanded
|
||||
const observer = new MutationObserver(() => {
|
||||
const open = !dropdown.classList.contains('d-none');
|
||||
input.setAttribute('aria-expanded', String(open));
|
||||
});
|
||||
observer.observe(dropdown, { attributes: true, attributeFilter: ['class'] });
|
||||
}
|
||||
|
||||
// ── Shared Quick Map Modal ────────────────────────
|
||||
let _qmOnSave = null;
|
||||
let _qmAcTimeout = null;
|
||||
|
||||
/**
|
||||
* Open the shared quick-map modal.
|
||||
@@ -276,13 +393,7 @@ function addQmCodmatLine(prefill) {
|
||||
const dropdown = div.querySelector('.qm-ac-dropdown');
|
||||
const selected = div.querySelector('.qm-selected');
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(_qmAcTimeout);
|
||||
_qmAcTimeout = setTimeout(() => _qmAutocomplete(input, dropdown, selected), 250);
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => dropdown.classList.add('d-none'), 200);
|
||||
});
|
||||
setupAutocomplete(input, dropdown, selected, _qmAutocomplete);
|
||||
}
|
||||
|
||||
async function _qmAutocomplete(input, dropdown, selectedEl) {
|
||||
@@ -294,22 +405,16 @@ async function _qmAutocomplete(input, dropdown, selectedEl) {
|
||||
const data = await res.json();
|
||||
if (!data.results || data.results.length === 0) { dropdown.classList.add('d-none'); return; }
|
||||
|
||||
dropdown.innerHTML = data.results.map(r =>
|
||||
`<div class="autocomplete-item" onmousedown="_qmSelectArticle(this, '${esc(r.codmat)}', '${esc(r.denumire)}${r.um ? ' (' + esc(r.um) + ')' : ''}')">
|
||||
dropdown.innerHTML = data.results.map((r, i) => {
|
||||
const label = r.denumire + (r.um ? ` (${r.um})` : '');
|
||||
return `<div class="autocomplete-item" id="ac-qm-${i}" data-codmat="${esc(r.codmat)}" data-label="${esc(label)}">
|
||||
<span class="codmat">${esc(r.codmat)}</span> — <span class="denumire">${esc(r.denumire)}</span>${r.um ? ` <small class="text-muted">(${esc(r.um)})</small>` : ''}
|
||||
</div>`
|
||||
).join('');
|
||||
</div>`;
|
||||
}).join('');
|
||||
dropdown.classList.remove('d-none');
|
||||
} catch { dropdown.classList.add('d-none'); }
|
||||
}
|
||||
|
||||
function _qmSelectArticle(el, codmat, label) {
|
||||
const line = el.closest('.qm-line');
|
||||
line.querySelector('.qm-codmat').value = codmat;
|
||||
line.querySelector('.qm-selected').textContent = label;
|
||||
line.querySelector('.qm-ac-dropdown').classList.add('d-none');
|
||||
}
|
||||
|
||||
async function saveQuickMapping() {
|
||||
const lines = document.querySelectorAll('#qmCodmatLines .qm-line');
|
||||
const mappings = [];
|
||||
|
||||
Reference in New Issue
Block a user