Added support for Streamdeck Pedal and updated UI to better fit the Packed UI style
This commit is contained in:
184
pirp/public/assets/combobox.js
Normal file
184
pirp/public/assets/combobox.js
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
202
pirp/public/assets/command-palette.js
Normal file
202
pirp/public/assets/command-palette.js
Normal file
@@ -0,0 +1,202 @@
|
||||
// PIRP Command Palette (Ctrl+K / Cmd+K)
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var overlay = null;
|
||||
var input = null;
|
||||
var resultsList = null;
|
||||
var debounceTimer = null;
|
||||
var activeIndex = -1;
|
||||
var currentResults = [];
|
||||
|
||||
var typeLabels = {
|
||||
invoice: 'Rechnungen',
|
||||
customer: 'Kunden',
|
||||
expense: 'Ausgaben',
|
||||
journal: 'Journal'
|
||||
};
|
||||
|
||||
var typeIcons = {
|
||||
invoice: '\u25B8',
|
||||
customer: '\u25CF',
|
||||
expense: '\u25A0',
|
||||
journal: '\u25C6'
|
||||
};
|
||||
|
||||
function create() {
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'cmd-palette-overlay hidden';
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) close();
|
||||
});
|
||||
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'cmd-palette';
|
||||
|
||||
input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.placeholder = 'Suche nach Rechnungen, Kunden, Ausgaben, Journal...';
|
||||
input.addEventListener('input', function() {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(doSearch, 200);
|
||||
});
|
||||
input.addEventListener('keydown', handleKeydown);
|
||||
|
||||
resultsList = document.createElement('div');
|
||||
resultsList.className = 'cmd-palette-results';
|
||||
|
||||
modal.appendChild(input);
|
||||
modal.appendChild(resultsList);
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!overlay) create();
|
||||
overlay.classList.remove('hidden');
|
||||
input.value = '';
|
||||
resultsList.innerHTML = '';
|
||||
activeIndex = -1;
|
||||
currentResults = [];
|
||||
setTimeout(function() { input.focus(); }, 10);
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (overlay) overlay.classList.add('hidden');
|
||||
}
|
||||
|
||||
function isOpen() {
|
||||
return overlay && !overlay.classList.contains('hidden');
|
||||
}
|
||||
|
||||
function doSearch() {
|
||||
var q = input.value.trim();
|
||||
if (q.length < 2) {
|
||||
resultsList.innerHTML = '';
|
||||
activeIndex = -1;
|
||||
currentResults = [];
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('search_api.php?q=' + encodeURIComponent(q))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (!data.ok) return;
|
||||
currentResults = data.results;
|
||||
activeIndex = -1;
|
||||
renderResults(data.results);
|
||||
})
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
function renderResults(results) {
|
||||
resultsList.innerHTML = '';
|
||||
if (!results.length) {
|
||||
var empty = document.createElement('div');
|
||||
empty.className = 'cmd-palette-empty';
|
||||
empty.textContent = 'Keine Ergebnisse';
|
||||
resultsList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by type
|
||||
var groups = {};
|
||||
var order = [];
|
||||
results.forEach(function(r) {
|
||||
if (!groups[r.type]) {
|
||||
groups[r.type] = [];
|
||||
order.push(r.type);
|
||||
}
|
||||
groups[r.type].push(r);
|
||||
});
|
||||
|
||||
var idx = 0;
|
||||
order.forEach(function(type) {
|
||||
var header = document.createElement('div');
|
||||
header.className = 'cmd-palette-group';
|
||||
header.textContent = typeLabels[type] || type;
|
||||
resultsList.appendChild(header);
|
||||
|
||||
groups[type].forEach(function(r) {
|
||||
var item = document.createElement('a');
|
||||
item.className = 'cmd-palette-item';
|
||||
item.href = r.url;
|
||||
item.setAttribute('data-idx', idx);
|
||||
|
||||
var icon = document.createElement('span');
|
||||
icon.className = 'cmd-palette-icon';
|
||||
icon.textContent = typeIcons[r.type] || '\u25B8';
|
||||
item.appendChild(icon);
|
||||
|
||||
var text = document.createElement('span');
|
||||
text.className = 'cmd-palette-text';
|
||||
|
||||
var title = document.createElement('span');
|
||||
title.className = 'cmd-palette-title';
|
||||
title.textContent = r.title;
|
||||
text.appendChild(title);
|
||||
|
||||
if (r.subtitle) {
|
||||
var sub = document.createElement('span');
|
||||
sub.className = 'cmd-palette-subtitle';
|
||||
sub.textContent = r.subtitle;
|
||||
text.appendChild(sub);
|
||||
}
|
||||
|
||||
item.appendChild(text);
|
||||
item.addEventListener('mouseenter', function() {
|
||||
setActive(parseInt(item.getAttribute('data-idx')));
|
||||
});
|
||||
resultsList.appendChild(item);
|
||||
idx++;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setActive(idx) {
|
||||
var items = resultsList.querySelectorAll('.cmd-palette-item');
|
||||
items.forEach(function(el) { el.classList.remove('active'); });
|
||||
activeIndex = idx;
|
||||
if (idx >= 0 && idx < items.length) {
|
||||
items[idx].classList.add('active');
|
||||
items[idx].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
var items = resultsList.querySelectorAll('.cmd-palette-item');
|
||||
var count = items.length;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setActive(activeIndex < count - 1 ? activeIndex + 1 : 0);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setActive(activeIndex > 0 ? activeIndex - 1 : count - 1);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (activeIndex >= 0 && activeIndex < count) {
|
||||
window.location = items[activeIndex].href;
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
// Global keyboard listener
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
if (isOpen()) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
}
|
||||
if (e.key === 'Escape' && isOpen()) {
|
||||
e.preventDefault();
|
||||
close();
|
||||
}
|
||||
});
|
||||
})();
|
||||
1391
pirp/public/assets/style.css
Normal file
1391
pirp/public/assets/style.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user