/* //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