/*
//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 = '';
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 = '' + [...sorted].map(v => ``).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