Files
radixOS/public/javascript/tableFilter.js
2026-04-22 11:55:23 +02:00

450 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
//Generic table filter module
// Usage:
TableFilter({
table: document.querySelector('#myTable'),
exceptedColumns: [ 'Status_ID' ],
filterConfig: {
columnModes: {
ID: 'text', // Textsuche in dieser Spalte
Status: 'dropdown', // Dropdown-Werte aus Tabelle sammeln
Objekt: 'text', // Textsuche
Priorität: 'dropdown',
Gewerk: 'dropdown',
Typ: 'dropdown',
Bedarfsmelder: 'text'
Bearbeiter: 'text'
Genehmiger: 'text'
},
checkboxFilter: {
column: 'Status_ID',
rules: [
{ label: 'Bearbeitung', test: v => [1,4,6,7,12,13,14].includes(parseInt(v)) },
{ label: 'Genehmigt', test: v => [2,5,9,10,11].includes(parseInt(v)) },
{ label: 'Abgelehnt', test: v => [3].includes(parseInt(v)) },
{ label: 'Abgeschlossen', test: v => [8].includes(parseInt(v)) }
]
}
}
});
// Generic table filter module (one textbox OR column-based dropdown)
// New features:
// - Selecting a column in the column-selector decides: textbox OR dropdown (configured in options)
// - If column-selector = empty value ⇒ full-row search
// - No extra checkbox needed
// - UI stays compact (only ONE input element + ONE dropdown)
*/
function TableFilter(options) {
try {
const table = options.table;
const cfg = options.filterConfig;
const exceptedColumns = options.exceptedColumns || [];
// Gruppenerkennung + Verwaltung
const groupMap = new Map(); // groupRow → childRows[]
let groupsInitialized = false;
const state = {
selectedColumn: '',
searchText: '',
dropdownValue: '',
dropdownCache: {},
checkbox: new Set(),
};
//------------------------------------
// Sticky Container für Filter + Header
//------------------------------------
if(document.querySelectorAll('.table-filter-container').length > 0) {
// Array.from(document.querySelectorAll('.table-filter-container')).forEach(filterContainer => {
// filterContainer.remove();
// })
const existing = table.previousElementSibling;
if (existing && existing.classList.contains('table-filter-container')) {
existing.remove();
}
}
const container = document.createElement('div');
container.className = 'table-filter-container';
container.style.position = 'sticky';
container.style.top = '0px';
container.style.left = '0px';
container.style.zIndex = 20;
// Filter UI wird hier reingesetzt
table.before(container);
// Tabelle selbst: header sticky
const thead = table.querySelector('thead');
const headerCells = Array.from(table.querySelectorAll('thead th'));
const getColumnIndex = name => headerCells.findIndex(c => c.textContent.trim() === name);
//------------------------------------
// Dynamisches Filter-UI
//------------------------------------
function createDynamicFilterUI() {
const wrap = document.createElement('div');
wrap.style="display:flex;flex-direction:row;align-items:center;gap:10px"
const colSelectLabel = document.createElement('label');
const colSelect = document.createElement('select');
const emptyOpt = document.createElement('option');
emptyOpt.value = '';
emptyOpt.textContent = '-- Alles --';
colSelect.appendChild(emptyOpt);
colSelectLabel.innerText = 'Spalte';
headerCells.forEach(cell => {
if(!exceptedColumns.includes(cell.textContent.trim())) {
const name = cell.textContent.trim();
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
colSelect.appendChild(opt);
}
});
const input = document.createElement('input');
input.type = 'search';
input.style.display = 'none';
const select = document.createElement('select');
select.style.display = 'none';
select.innerHTML = '<option value="">-- Alle --</option>';
function populateDropdownForColumn(colName) {
const colIndex = getColumnIndex(colName);
const rows = Array.from(table.querySelectorAll('tbody tr')).filter(r => !r.classList.contains('grouprow'));
const values = new Set();
rows.forEach(r => {
const cell = r.children[colIndex];
if (cell) values.add(cell.textContent.trim());
});
const sorted = new Set([...values].sort((a, b) => a.localeCompare(b)));
select.innerHTML = '<option value="">-- Alle --</option>' + [...sorted].map(v => `<option value="${v}">${v}</option>`).join('');
}
colSelect.addEventListener('change', () => {
state.selectedColumn = colSelect.value;
state.searchText = '';
state.dropdownValue = '';
input.value = '';
select.value = '';
if (!state.selectedColumn) {
input.style.display = '';
select.style.display = 'none';
return applyFilters();
}
const mode = cfg.columnModes[state.selectedColumn];
if (mode === 'text') {
input.style.display = '';
select.style.display = 'none';
} else if (mode === 'dropdown') {
input.style.display = 'none';
select.style.display = '';
populateDropdownForColumn(state.selectedColumn);
} else {
input.style.display = '';
select.style.display = 'none';
}
applyFilters();
});
input.style.display = '';
input.addEventListener('input', () => { state.searchText = input.value.toLowerCase(); applyFilters(); });
select.addEventListener('change', () => { state.dropdownValue = select.value; applyFilters(); });
wrap.append(colSelectLabel, colSelect, document.createElement('br'));
wrap.append(input, select);
container.appendChild(wrap);
}
//--------------------------------------------
// Filterbreite an Parent minus Scrollbar
//--------------------------------------------
function updateFilterWidth() {
if (!table || !container) return;
const wrapper = table.parentElement; // z.B. .table-wrapper
if (!wrapper) return;
const style = getComputedStyle(table.querySelector('thead th'));
const rect = wrapper.getBoundingClientRect();
const scrollbarWidth = wrapper.offsetWidth - wrapper.clientWidth;
container.style.width = (rect.width - scrollbarWidth - parseFloat(style.paddingRight)) + "px";
container.style.maxWidth = (rect.width - scrollbarWidth - parseFloat(style.paddingRight)) + "px";
}
// Initial setzen
updateFilterWidth();
// Fenstergröße beobachten
window.addEventListener('resize', updateFilterWidth);
// Dynamische Anpassung bei Wrapper Resize
const wrapper = table.parentElement;
if (wrapper) {
new ResizeObserver(updateFilterWidth).observe(wrapper);
}
//------------------------------------
// Checkbox Filter
//------------------------------------
function createCheckboxFilter() {
const cfgCheck = cfg.checkboxFilter;
const idx = getColumnIndex(cfgCheck.column);
if (idx < 0) return;
const wrapper = document.createElement('div');
cfgCheck.rules.forEach(rule => {
const checkWrapper = document.createElement('label');
const checkInput = document.createElement('input');
const checkTrack = document.createElement('span');
const checkThumb = document.createElement('span');
checkWrapper.classList.add("cb", "cb-switch");
checkWrapper.style.marginRight = '15px'
checkInput.type = 'checkbox';
checkInput.addEventListener('change', () => {
if (checkInput.checked) state.checkbox.add(rule);
else state.checkbox.delete(rule);
applyFilters();
});
checkTrack.classList.add('switch-track');
checkTrack.setAttribute("aria-hidden", 'true');
checkThumb.classList.add('switch-thumb');
checkThumb.setAttribute("aria-hidden", 'true');
checkTrack.append(checkThumb)
checkWrapper.append(rule.label, checkInput, checkTrack);
wrapper.appendChild(checkWrapper);
});
container.appendChild(wrapper);
}
//------------------------------------
// Live Counter
//------------------------------------
const counter = document.createElement('div');
counter.className = 'live-counter';
container.appendChild(counter);
//------------------------------------
// Filter Logik
//------------------------------------
function detectGroups() {
if (groupsInitialized) return;
groupsInitialized = true;
const rows = Array.from(table.querySelectorAll('tbody tr'));
let currentGroupRow = null;
let childBuffer = [];
rows.forEach(row => {
if (row.classList.contains('grouprow')) {
// vorige Gruppe speichern
if (currentGroupRow && childBuffer.length > 0) {
groupMap.set(currentGroupRow, childBuffer);
}
// neue Gruppe starten
currentGroupRow = row;
childBuffer = [];
}
else if (currentGroupRow) {
childBuffer.push(row);
}
});
// letzte Gruppe speichern
if (currentGroupRow && childBuffer.length > 0) {
groupMap.set(currentGroupRow, childBuffer);
}
// jedem groupRow ein Toggle verpassen (wenn nicht vorhanden)
groupMap.forEach((childRows, groupRow) => {
const toggle = groupRow.querySelector("span");
if (!toggle || toggle._groupToggleBound) return;
toggle._groupToggleBound = true;
toggle.addEventListener("click", () => toggleGroup(groupRow));
groupRow.addEventListener("dblclick", () => toggleGroup(groupRow));
});
}
function toggleGroup(groupRow) {
const childRows = groupMap.get(groupRow);
if (!childRows) return;
const toggle = groupRow.querySelector("span");
const collapsed = toggle.textContent === '+';
if (collapsed) {
// ausklappen → nur gefilterte Rows zeigen
childRows.forEach(r => {
if (!r._filteredOut) r.style.display = '';
});
toggle.textContent = '-';
} else {
// einklappen → alle verstecken
childRows.forEach(r => r.style.display = 'none');
toggle.textContent = '+';
}
}
function applyFilters() {
detectGroups();
const rows = Array.from(table.querySelectorAll('tbody tr'));
let visible = 0;
rows.forEach(row => {
if (row.classList.contains('grouprow')) { row.style.display = ''; return; }
let show = true;
if (state.selectedColumn === '') {
if (state.searchText) show = [...row.cells].some(c => c.textContent.toLowerCase().includes(state.searchText));
} else {
const idx = getColumnIndex(state.selectedColumn);
const mode = cfg.columnModes[state.selectedColumn];
if (mode === 'text' && state.searchText) {
show = row.cells[idx]?.textContent.toLowerCase().includes(state.searchText);
}
if (mode === 'dropdown' && state.dropdownValue) {
show = row.cells[idx]?.textContent === state.dropdownValue;
}
}
if (state.checkbox.size > 0) {
const idx = getColumnIndex(cfg.checkboxFilter.column);
const cellVal = row.cells[idx]?.textContent ?? '';
const pass = [...state.checkbox].some(rule => rule.test(cellVal));
if (!pass) show = false;
}
row._filteredOut = !show; // fürs Gruppensystem speichern
// Wenn Gruppen existieren:
if (groupMap.size > 0) {
// Prüfen ob row zu einer Gruppe gehört
let parentGroup = null;
for (const [g, list] of groupMap.entries()) {
if (list.includes(row)) {
parentGroup = g;
break;
}
}
if (parentGroup) {
const toggle = parentGroup.querySelector("span");
const collapsed = toggle.textContent === '+';
if (collapsed) {
// eingeklappt → immer ausblenden
row.style.display = 'none';
if (show) visible++;
} else {
// ausgeklappt → nur gefilterte anzeigen
row.style.display = show ? '' : 'none';
if (show) visible++;
}
return;
}
} else {
if (show) visible++;
}
// normale Zeile ohne Gruppe:
row.style.display = show ? '' : 'none';
});
counter.textContent = `${visible} Treffer`;
}
//------------------------------------
// Init
//------------------------------------
createDynamicFilterUI();
if(options.filterConfig.checkboxFilter) createCheckboxFilter();
applyFilters();
if (thead) {
thead.style.position = 'sticky';
thead.style.top = (container.offsetHeight - 1) + 'px';
thead.style.zIndex = 10;
}
//------------------------------------
// Public API
//------------------------------------
return {
refreshDropdown() { if (state.selectedColumn && cfg.columnModes[state.selectedColumn] === 'dropdown') {} },
reapply() { applyFilters(); }
};
} catch(err) {
alert(err.stack)
}
}
//#region Sort table
// let sortDirection = {};
function sortTable(tableId,colIndex) {
const sortDirection = {};
const table = document.getElementById(tableId);
const tbody = table.tBodies[0];
const rows = Array.from(tbody.querySelectorAll("tr"));
// Toggle sort direction
sortDirection[colIndex] = !sortDirection[colIndex];
rows.sort((a, b) => {
const cellA = a.cells[colIndex].textContent.trim();
const cellB = b.cells[colIndex].textContent.trim();
// Numeric sort if possible
const numA = parseFloat(cellA);
const numB = parseFloat(cellB);
if (!isNaN(numA) && !isNaN(numB)) {
return sortDirection[colIndex] ? numA - numB : numB - numA;
} else {
return sortDirection[colIndex]
? cellA.localeCompare(cellB)
: cellB.localeCompare(cellA);
}
});
// Remove old rows
tbody.innerHTML = "";
rows.forEach(row => tbody.appendChild(row));
// Update sort arrows
const th = table.querySelectorAll("th");
th.forEach((header, i) => {
header.classList.remove("sort-asc", "sort-desc");
if (i === colIndex) {
header.classList.add(sortDirection[colIndex] ? "sort-asc" : "sort-desc");
}
});
}
//#endregion