185 lines
6.2 KiB
JavaScript
185 lines
6.2 KiB
JavaScript
/**
|
|
* PirpCombobox - Tippbare Dropdown-Auswahl als Ersatz für <select>
|
|
*
|
|
* Verwendung:
|
|
* new PirpCombobox(containerEl, accountOptions, {
|
|
* placeholder: '--Konto--',
|
|
* selectedValue: 'kasse_s|soll||',
|
|
* onSelect: function(opt) { ... }
|
|
* });
|
|
*
|
|
* accountOptions: { "Gruppenname": [ {value, label, side, rev_id, exp_id}, ... ], ... }
|
|
*/
|
|
class PirpCombobox {
|
|
constructor(container, options, config) {
|
|
this.container = container;
|
|
this.options = options; // grouped: { group: [{value, label, side, rev_id, exp_id}] }
|
|
this.config = config || {};
|
|
this.flatOptions = [];
|
|
this.highlighted = -1;
|
|
|
|
// Flatten options for filtering
|
|
for (const [group, opts] of Object.entries(this.options)) {
|
|
for (const opt of opts) {
|
|
this.flatOptions.push({ group, ...opt });
|
|
}
|
|
}
|
|
|
|
this.build();
|
|
this.bindEvents();
|
|
|
|
// Set initial selection
|
|
if (this.config.selectedValue) {
|
|
const parts = this.config.selectedValue.split('|');
|
|
const match = this.flatOptions.find(o =>
|
|
o.value === parts[0] && o.side === parts[1] &&
|
|
String(o.rev_id) === String(parts[2]) &&
|
|
String(o.exp_id) === String(parts[3])
|
|
);
|
|
if (match) {
|
|
this.input.value = match.label;
|
|
this.selectedOpt = match;
|
|
}
|
|
}
|
|
}
|
|
|
|
build() {
|
|
this.container.classList.add('pirp-combobox');
|
|
|
|
this.input = document.createElement('input');
|
|
this.input.type = 'text';
|
|
this.input.placeholder = this.config.placeholder || '--Konto--';
|
|
this.input.autocomplete = 'off';
|
|
|
|
this.dropdown = document.createElement('div');
|
|
this.dropdown.className = 'pirp-combobox-dropdown';
|
|
this.dropdown.style.display = 'none';
|
|
|
|
this.container.appendChild(this.input);
|
|
this.container.appendChild(this.dropdown);
|
|
|
|
this.selectedOpt = null;
|
|
}
|
|
|
|
renderDropdown(filter) {
|
|
this.dropdown.innerHTML = '';
|
|
this.highlighted = -1;
|
|
const f = (filter || '').toLowerCase();
|
|
let visibleItems = [];
|
|
let lastGroup = null;
|
|
|
|
for (const opt of this.flatOptions) {
|
|
if (f && opt.label.toLowerCase().indexOf(f) === -1) continue;
|
|
if (opt.group !== lastGroup) {
|
|
lastGroup = opt.group;
|
|
const groupEl = document.createElement('div');
|
|
groupEl.className = 'pirp-combobox-group';
|
|
groupEl.textContent = opt.group;
|
|
this.dropdown.appendChild(groupEl);
|
|
}
|
|
const optEl = document.createElement('div');
|
|
optEl.className = 'pirp-combobox-option';
|
|
optEl.textContent = opt.label;
|
|
optEl.dataset.index = visibleItems.length;
|
|
visibleItems.push({ el: optEl, opt });
|
|
this.dropdown.appendChild(optEl);
|
|
}
|
|
|
|
this.visibleItems = visibleItems;
|
|
}
|
|
|
|
open() {
|
|
this.renderDropdown(this.input.value === (this.selectedOpt ? this.selectedOpt.label : '') ? '' : this.input.value);
|
|
this.dropdown.style.display = 'block';
|
|
}
|
|
|
|
close() {
|
|
this.dropdown.style.display = 'none';
|
|
this.highlighted = -1;
|
|
}
|
|
|
|
select(opt) {
|
|
this.selectedOpt = opt;
|
|
this.input.value = opt.label;
|
|
this.close();
|
|
if (this.config.onSelect) {
|
|
this.config.onSelect(opt);
|
|
}
|
|
}
|
|
|
|
highlightIndex(idx) {
|
|
if (!this.visibleItems || !this.visibleItems.length) return;
|
|
if (this.highlighted >= 0 && this.highlighted < this.visibleItems.length) {
|
|
this.visibleItems[this.highlighted].el.classList.remove('highlighted');
|
|
}
|
|
this.highlighted = Math.max(0, Math.min(idx, this.visibleItems.length - 1));
|
|
this.visibleItems[this.highlighted].el.classList.add('highlighted');
|
|
this.visibleItems[this.highlighted].el.scrollIntoView({ block: 'nearest' });
|
|
}
|
|
|
|
bindEvents() {
|
|
this.input.addEventListener('focus', () => {
|
|
this.open();
|
|
});
|
|
|
|
this.input.addEventListener('input', () => {
|
|
this.renderDropdown(this.input.value);
|
|
this.dropdown.style.display = 'block';
|
|
if (this.visibleItems.length > 0) {
|
|
this.highlightIndex(0);
|
|
}
|
|
});
|
|
|
|
this.input.addEventListener('keydown', (e) => {
|
|
if (this.dropdown.style.display === 'none') {
|
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
this.open();
|
|
e.preventDefault();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
this.highlightIndex(this.highlighted + 1);
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
this.highlightIndex(this.highlighted - 1);
|
|
} else if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (this.highlighted >= 0 && this.visibleItems[this.highlighted]) {
|
|
this.select(this.visibleItems[this.highlighted].opt);
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
if (this.selectedOpt) {
|
|
this.input.value = this.selectedOpt.label;
|
|
}
|
|
this.close();
|
|
}
|
|
});
|
|
|
|
this.input.addEventListener('blur', () => {
|
|
// Delay to allow click on option to register
|
|
setTimeout(() => {
|
|
if (this.selectedOpt) {
|
|
this.input.value = this.selectedOpt.label;
|
|
}
|
|
this.close();
|
|
}, 150);
|
|
});
|
|
|
|
// Use mousedown to prevent blur race condition
|
|
this.dropdown.addEventListener('mousedown', (e) => {
|
|
e.preventDefault(); // Prevent blur
|
|
const optEl = e.target.closest('.pirp-combobox-option');
|
|
if (optEl) {
|
|
const idx = parseInt(optEl.dataset.index);
|
|
if (this.visibleItems[idx]) {
|
|
this.select(this.visibleItems[idx].opt);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|