initial files
This commit is contained in:
450
public/javascript/tableFilter.js
Normal file
450
public/javascript/tableFilter.js
Normal file
@@ -0,0 +1,450 @@
|
||||
/*
|
||||
//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
|
||||
Reference in New Issue
Block a user