450 lines
15 KiB
JavaScript
450 lines
15 KiB
JavaScript
/*
|
||
//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
|