Privacy Policy DSGVO with sheets for categories, export-, check- and first erase-functions
This commit is contained in:
+334
-28
@@ -163,6 +163,104 @@
|
||||
|
||||
const headerRow = thead.rows[0];
|
||||
|
||||
// Add a purely visual row-number column before every real table column.
|
||||
// It is recalculated after filtering, sorting and partial server reloads and
|
||||
// therefore never represents a database identifier.
|
||||
if (!headerRow.querySelector('th[data-row-number-column="1"]')) {
|
||||
const numberHeader = document.createElement('th');
|
||||
numberHeader.className = 'table-row-number-header';
|
||||
numberHeader.dataset.rowNumberColumn = '1';
|
||||
numberHeader.dataset.noSort = '1';
|
||||
numberHeader.dataset.noFilter = '1';
|
||||
numberHeader.textContent = '#';
|
||||
numberHeader.title = document.body.dataset.rowNumberLabel || 'Row number';
|
||||
headerRow.insertBefore(numberHeader, headerRow.firstChild);
|
||||
}
|
||||
|
||||
const numberHeader = headerRow.querySelector('th[data-row-number-column="1"]');
|
||||
const rowNumberStoragePart = table.dataset.storageKey || table.id || window.location.pathname;
|
||||
const rowNumberWidthKey = `assetmanager:table:${rowNumberStoragePart}:row-number-width`;
|
||||
const defaultRowNumberWidth = 64;
|
||||
const minimumRowNumberWidth = 48;
|
||||
const maximumRowNumberWidth = 180;
|
||||
|
||||
function readStoredRowNumberWidth() {
|
||||
try {
|
||||
const stored = Number.parseInt(window.localStorage.getItem(rowNumberWidthKey) || '', 10);
|
||||
if (Number.isFinite(stored)) {
|
||||
return Math.min(maximumRowNumberWidth, Math.max(minimumRowNumberWidth, stored));
|
||||
}
|
||||
} catch (_) {}
|
||||
return defaultRowNumberWidth;
|
||||
}
|
||||
|
||||
function applyRowNumberWidth(width) {
|
||||
const normalized = Math.min(maximumRowNumberWidth, Math.max(minimumRowNumberWidth, Math.round(width)));
|
||||
table.style.setProperty('--table-row-number-width', `${normalized}px`);
|
||||
table.dataset.rowNumberWidth = String(normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function installRowNumberResizer() {
|
||||
if (!numberHeader || numberHeader.querySelector('.table-row-number-resizer')) return;
|
||||
const handle = document.createElement('span');
|
||||
handle.className = 'table-row-number-resizer';
|
||||
handle.setAttribute('role', 'separator');
|
||||
handle.setAttribute('aria-orientation', 'vertical');
|
||||
handle.tabIndex = 0;
|
||||
handle.title = document.body.dataset.rowNumberResizeLabel || 'Drag to change column width';
|
||||
numberHeader.appendChild(handle);
|
||||
|
||||
const saveWidth = width => {
|
||||
const normalized = applyRowNumberWidth(width);
|
||||
try { window.localStorage.setItem(rowNumberWidthKey, String(normalized)); } catch (_) {}
|
||||
};
|
||||
|
||||
handle.addEventListener('pointerdown', event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const startX = event.clientX;
|
||||
const startWidth = Number.parseInt(table.dataset.rowNumberWidth || '', 10) || defaultRowNumberWidth;
|
||||
handle.setPointerCapture?.(event.pointerId);
|
||||
document.documentElement.classList.add('table-column-resizing');
|
||||
|
||||
const move = moveEvent => applyRowNumberWidth(startWidth + moveEvent.clientX - startX);
|
||||
const finish = finishEvent => {
|
||||
saveWidth(Number.parseInt(table.dataset.rowNumberWidth || '', 10) || startWidth);
|
||||
handle.releasePointerCapture?.(finishEvent.pointerId);
|
||||
document.documentElement.classList.remove('table-column-resizing');
|
||||
handle.removeEventListener('pointermove', move);
|
||||
handle.removeEventListener('pointerup', finish);
|
||||
handle.removeEventListener('pointercancel', finish);
|
||||
};
|
||||
handle.addEventListener('pointermove', move);
|
||||
handle.addEventListener('pointerup', finish);
|
||||
handle.addEventListener('pointercancel', finish);
|
||||
});
|
||||
|
||||
handle.addEventListener('keydown', event => {
|
||||
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
||||
event.preventDefault();
|
||||
const current = Number.parseInt(table.dataset.rowNumberWidth || '', 10) || defaultRowNumberWidth;
|
||||
saveWidth(current + (event.key === 'ArrowRight' ? 8 : -8));
|
||||
});
|
||||
}
|
||||
|
||||
applyRowNumberWidth(readStoredRowNumberWidth());
|
||||
installRowNumberResizer();
|
||||
|
||||
function ensureRowNumberCells() {
|
||||
[...tbody.rows].forEach(row => {
|
||||
if (row.querySelector('td[data-row-number-column="1"]')) return;
|
||||
const cell = document.createElement('td');
|
||||
cell.className = 'table-row-number-cell';
|
||||
cell.dataset.rowNumberColumn = '1';
|
||||
cell.setAttribute('aria-hidden', 'true');
|
||||
row.insertBefore(cell, row.firstChild);
|
||||
});
|
||||
}
|
||||
ensureRowNumberCells();
|
||||
|
||||
const isAssetTable = table.id === 'asset-table';
|
||||
const usesServerFilters = table.dataset.serverFilters === '1';
|
||||
let serverFilterTimer = null;
|
||||
@@ -173,15 +271,19 @@
|
||||
// real data-field keys on its movable columns; selection and action
|
||||
// columns must remain without a field key.
|
||||
if (!isAssetTable) {
|
||||
[...headerRow.cells].forEach((header, index) => {
|
||||
let syntheticIndex = 0;
|
||||
[...headerRow.cells].forEach(header => {
|
||||
if (header.dataset.rowNumberColumn === '1') return;
|
||||
if (!header.dataset.field) {
|
||||
header.dataset.field = `__col_${index}`;
|
||||
header.dataset.field = `__col_${syntheticIndex}`;
|
||||
header.dataset.syntheticField = '1';
|
||||
}
|
||||
syntheticIndex += 1;
|
||||
});
|
||||
|
||||
[...tbody.rows].forEach(row => {
|
||||
[...row.cells].forEach((cell, index) => {
|
||||
if (cell.dataset.rowNumberColumn === '1') return;
|
||||
const header = headerRow.cells[index];
|
||||
if (header && !cell.dataset.field) {
|
||||
cell.dataset.field = header.dataset.field;
|
||||
@@ -197,6 +299,160 @@
|
||||
|
||||
const filterDefinitions = [];
|
||||
|
||||
// User-resizable widths for all real table columns. Widths are stored per
|
||||
// table and column key. The visual row-number column keeps its dedicated
|
||||
// implementation above because it is generated dynamically.
|
||||
const columnWidthsStorageKey = `assetmanager:table:${rowNumberStoragePart}:column-widths`;
|
||||
const minimumColumnWidth = 56;
|
||||
const maximumColumnWidth = 900;
|
||||
|
||||
function readStoredColumnWidths() {
|
||||
try {
|
||||
const parsed = JSON.parse(window.localStorage.getItem(columnWidthsStorageKey) || '{}');
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
let storedColumnWidths = readStoredColumnWidths();
|
||||
|
||||
function columnStorageKey(header, columnIndex) {
|
||||
return header?.dataset.field || header?.dataset.serverFilterName || `__column_${columnIndex}`;
|
||||
}
|
||||
|
||||
function normalizedColumnWidth(width) {
|
||||
return Math.min(maximumColumnWidth, Math.max(minimumColumnWidth, Math.round(width)));
|
||||
}
|
||||
|
||||
function applyColumnWidth(columnIndex, width, options = {}) {
|
||||
const header = headerRow.cells[columnIndex];
|
||||
if (!header || header.dataset.rowNumberColumn === '1') return null;
|
||||
const normalized = normalizedColumnWidth(width);
|
||||
const cells = [header, filterRow.cells[columnIndex], ...[...tbody.rows].map(row => row.cells[columnIndex])];
|
||||
cells.filter(Boolean).forEach(cell => {
|
||||
cell.style.width = `${normalized}px`;
|
||||
cell.style.minWidth = `${normalized}px`;
|
||||
cell.style.maxWidth = `${normalized}px`;
|
||||
});
|
||||
header.dataset.userColumnWidth = String(normalized);
|
||||
|
||||
// The asset list calculates its scroll width from data-column-width.
|
||||
// Keep that existing mechanism in sync instead of introducing a second
|
||||
// competing table-width calculation.
|
||||
if (isAssetTable && header.dataset.field) {
|
||||
header.dataset.columnWidth = String(normalized);
|
||||
window.dispatchEvent(new CustomEvent('asset-table-column-width-changed', {
|
||||
detail: { table, field: header.dataset.field, width: normalized }
|
||||
}));
|
||||
}
|
||||
|
||||
if (options.save) {
|
||||
storedColumnWidths[columnStorageKey(header, columnIndex)] = normalized;
|
||||
try {
|
||||
window.localStorage.setItem(columnWidthsStorageKey, JSON.stringify(storedColumnWidths));
|
||||
} catch (_) {}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function applyStoredColumnWidths() {
|
||||
[...headerRow.cells].forEach((header, columnIndex) => {
|
||||
if (header.dataset.rowNumberColumn === '1') return;
|
||||
const stored = Number.parseInt(storedColumnWidths[columnStorageKey(header, columnIndex)], 10);
|
||||
if (Number.isFinite(stored)) applyColumnWidth(columnIndex, stored);
|
||||
});
|
||||
}
|
||||
|
||||
function resetColumnWidth(columnIndex) {
|
||||
const header = headerRow.cells[columnIndex];
|
||||
if (!header || header.dataset.rowNumberColumn === '1') return;
|
||||
delete storedColumnWidths[columnStorageKey(header, columnIndex)];
|
||||
try {
|
||||
window.localStorage.setItem(columnWidthsStorageKey, JSON.stringify(storedColumnWidths));
|
||||
} catch (_) {}
|
||||
|
||||
[header, filterRow.cells[columnIndex], ...[...tbody.rows].map(row => row.cells[columnIndex])]
|
||||
.filter(Boolean)
|
||||
.forEach(cell => {
|
||||
cell.style.removeProperty('width');
|
||||
cell.style.removeProperty('min-width');
|
||||
cell.style.removeProperty('max-width');
|
||||
});
|
||||
delete header.dataset.userColumnWidth;
|
||||
|
||||
if (isAssetTable && header.dataset.field) {
|
||||
const configured = Number.parseInt(header.dataset.defaultColumnWidth || '', 10);
|
||||
const fallback = Number.isFinite(configured) ? configured : 180;
|
||||
header.dataset.columnWidth = String(fallback);
|
||||
window.dispatchEvent(new CustomEvent('asset-table-column-width-changed', {
|
||||
detail: { table, field: header.dataset.field, width: fallback }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function installColumnResizers() {
|
||||
[...headerRow.cells].forEach((header, columnIndex) => {
|
||||
if (header.dataset.rowNumberColumn === '1' || header.colSpan > 1) return;
|
||||
if (header.querySelector(':scope > .table-column-resizer')) return;
|
||||
|
||||
if (isAssetTable && !header.dataset.defaultColumnWidth) {
|
||||
header.dataset.defaultColumnWidth = header.dataset.columnWidth || '';
|
||||
}
|
||||
|
||||
const handle = document.createElement('span');
|
||||
handle.className = 'table-column-resizer';
|
||||
handle.setAttribute('role', 'separator');
|
||||
handle.setAttribute('aria-orientation', 'vertical');
|
||||
handle.tabIndex = 0;
|
||||
handle.title = document.body.dataset.rowNumberResizeLabel || 'Drag to change column width';
|
||||
header.appendChild(handle);
|
||||
|
||||
handle.addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
handle.addEventListener('dblclick', event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
resetColumnWidth(columnIndex);
|
||||
});
|
||||
|
||||
handle.addEventListener('pointerdown', event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const startX = event.clientX;
|
||||
const startWidth = Math.round(header.getBoundingClientRect().width);
|
||||
handle.setPointerCapture?.(event.pointerId);
|
||||
document.documentElement.classList.add('table-column-resizing');
|
||||
|
||||
const move = moveEvent => applyColumnWidth(columnIndex, startWidth + moveEvent.clientX - startX);
|
||||
const finish = finishEvent => {
|
||||
const current = Number.parseInt(header.dataset.userColumnWidth || '', 10) || startWidth;
|
||||
applyColumnWidth(columnIndex, current, { save: true });
|
||||
handle.releasePointerCapture?.(finishEvent.pointerId);
|
||||
document.documentElement.classList.remove('table-column-resizing');
|
||||
handle.removeEventListener('pointermove', move);
|
||||
handle.removeEventListener('pointerup', finish);
|
||||
handle.removeEventListener('pointercancel', finish);
|
||||
};
|
||||
handle.addEventListener('pointermove', move);
|
||||
handle.addEventListener('pointerup', finish);
|
||||
handle.addEventListener('pointercancel', finish);
|
||||
});
|
||||
|
||||
handle.addEventListener('keydown', event => {
|
||||
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const current = Number.parseInt(header.dataset.userColumnWidth || '', 10)
|
||||
|| Math.round(header.getBoundingClientRect().width);
|
||||
applyColumnWidth(columnIndex, current + (event.key === 'ArrowRight' ? 12 : -12), { save: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function filterStatusTarget() {
|
||||
const wrapper = table.closest(
|
||||
'#asset-table-scroll, .management-table-scroll, .table-scroll, .table-wrap, .sticky-table'
|
||||
@@ -291,16 +547,22 @@
|
||||
}
|
||||
|
||||
function rebuildMissingCellKeys() {
|
||||
if (isAssetTable) return;
|
||||
ensureRowNumberCells();
|
||||
if (isAssetTable) {
|
||||
applyStoredColumnWidths();
|
||||
return;
|
||||
}
|
||||
|
||||
[...tbody.rows].forEach(row => {
|
||||
[...row.cells].forEach((cell, index) => {
|
||||
if (cell.dataset.rowNumberColumn === '1') return;
|
||||
const header = headerRow.cells[index];
|
||||
if (header && !cell.dataset.field) {
|
||||
cell.dataset.field = header.dataset.field;
|
||||
}
|
||||
});
|
||||
});
|
||||
applyStoredColumnWidths();
|
||||
}
|
||||
|
||||
function numericFilterMatches(rawValue, rawTerm) {
|
||||
@@ -405,6 +667,20 @@
|
||||
else serverFilterTimer = window.setTimeout(navigate, 700);
|
||||
}
|
||||
|
||||
function updateRowNumbers() {
|
||||
let visibleIndex = 0;
|
||||
[...tbody.rows].forEach(row => {
|
||||
const cell = row.querySelector('td[data-row-number-column="1"]');
|
||||
if (!cell) return;
|
||||
if (row.hidden) {
|
||||
cell.textContent = '';
|
||||
return;
|
||||
}
|
||||
visibleIndex += 1;
|
||||
cell.textContent = String(visibleIndex);
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
rebuildMissingCellKeys();
|
||||
|
||||
@@ -419,6 +695,7 @@
|
||||
const serverTotal = Number.parseInt(table.dataset.serverFilterTotal || '', 10);
|
||||
const total = Number.isFinite(serverTotal) ? serverTotal : tbody.rows.length;
|
||||
updateFilterPresentation(active, tbody.rows.length, total);
|
||||
updateRowNumbers();
|
||||
window.dispatchEvent(new CustomEvent('asset-table-filter-changed', {
|
||||
detail: {
|
||||
table,
|
||||
@@ -447,6 +724,7 @@
|
||||
});
|
||||
|
||||
updateFilterPresentation(active, visibleRows, tbody.rows.length);
|
||||
updateRowNumbers();
|
||||
|
||||
window.dispatchEvent(new CustomEvent('asset-table-filter-changed', {
|
||||
detail: {
|
||||
@@ -458,6 +736,43 @@
|
||||
}));
|
||||
}
|
||||
|
||||
function applySort(field, direction, options = {}) {
|
||||
const header = [...headerRow.cells].find(item => item.dataset.field === field);
|
||||
if (!header || header.dataset.noSort) return false;
|
||||
|
||||
const columnIndex = header.cellIndex;
|
||||
[...headerRow.cells].forEach(item => {
|
||||
delete item.dataset.sortDirection;
|
||||
item.classList.remove('sort-asc', 'sort-desc');
|
||||
});
|
||||
|
||||
const normalizedDirection = direction === 'desc' ? 'desc' : 'asc';
|
||||
header.dataset.sortDirection = normalizedDirection;
|
||||
header.classList.add(normalizedDirection === 'asc' ? 'sort-asc' : 'sort-desc');
|
||||
|
||||
const rows = [...tbody.rows];
|
||||
rows.sort((left, right) => {
|
||||
const definition = { field, columnIndex };
|
||||
const result = compareValues(
|
||||
textValue(cellForDefinition(left, definition)),
|
||||
textValue(cellForDefinition(right, definition))
|
||||
);
|
||||
return normalizedDirection === 'asc' ? result : -result;
|
||||
});
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
rows.forEach(row => fragment.appendChild(row));
|
||||
tbody.appendChild(fragment);
|
||||
applyFilters();
|
||||
|
||||
if (!options.silent) {
|
||||
window.dispatchEvent(new CustomEvent('asset-table-sort-changed', {
|
||||
detail: { table, field, direction: normalizedDirection }
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[...headerRow.cells].forEach((header, columnIndex) => {
|
||||
const field = header.dataset.field || '';
|
||||
const serverName = header.dataset.serverFilterName || '';
|
||||
@@ -468,37 +783,18 @@
|
||||
|
||||
header.addEventListener('click', event => {
|
||||
if (event.target.closest('input,button,a,select,textarea,label')) return;
|
||||
|
||||
const direction = header.dataset.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
|
||||
[...headerRow.cells].forEach(item => {
|
||||
delete item.dataset.sortDirection;
|
||||
item.classList.remove('sort-asc', 'sort-desc');
|
||||
});
|
||||
|
||||
header.dataset.sortDirection = direction;
|
||||
header.classList.add(direction === 'asc' ? 'sort-asc' : 'sort-desc');
|
||||
|
||||
const rows = [...tbody.rows];
|
||||
rows.sort((left, right) => {
|
||||
const definition = { field, columnIndex };
|
||||
const result = compareValues(
|
||||
textValue(cellForDefinition(left, definition)),
|
||||
textValue(cellForDefinition(right, definition))
|
||||
);
|
||||
return direction === 'asc' ? result : -result;
|
||||
});
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
rows.forEach(row => fragment.appendChild(row));
|
||||
tbody.appendChild(fragment);
|
||||
applyFilters();
|
||||
applySort(field, direction);
|
||||
});
|
||||
}
|
||||
|
||||
const filterCell = document.createElement('th');
|
||||
if (field) filterCell.dataset.field = field;
|
||||
filterCell.dataset.columnIndex = String(columnIndex);
|
||||
if (header.dataset.rowNumberColumn === '1') {
|
||||
filterCell.dataset.rowNumberColumn = '1';
|
||||
filterCell.className = 'table-row-number-filter';
|
||||
}
|
||||
|
||||
if (!header.dataset.noFilter && (!usesServerFilters || serverName)) {
|
||||
const input = document.createElement('input');
|
||||
@@ -541,6 +837,8 @@
|
||||
});
|
||||
|
||||
thead.appendChild(filterRow);
|
||||
installColumnResizers();
|
||||
applyStoredColumnWidths();
|
||||
|
||||
// Editable cells are always read live, so changed values immediately
|
||||
// participate in filtering without a stale cache.
|
||||
@@ -553,6 +851,8 @@
|
||||
});
|
||||
|
||||
table.assetApplyFilters = applyFilters;
|
||||
table.assetApplySort = (field, direction, options = {}) => applySort(field, direction, options);
|
||||
table.assetUpdateRowNumbers = updateRowNumbers;
|
||||
table.assetRebuildFilterCache = rebuildMissingCellKeys;
|
||||
table.dataset.tableToolsReady = '1';
|
||||
ensureFilterStatus();
|
||||
@@ -586,7 +886,13 @@
|
||||
const table = event.detail?.table;
|
||||
if (table?.assetRebuildFilterCache) {
|
||||
table.assetRebuildFilterCache();
|
||||
table.assetApplyFilters?.();
|
||||
const sortedHeader = table.tHead?.rows?.[0]?.querySelector('[data-sort-direction]');
|
||||
if (sortedHeader?.dataset.field && table.assetApplySort) {
|
||||
table.assetApplySort(sortedHeader.dataset.field, sortedHeader.dataset.sortDirection, {silent: true});
|
||||
} else {
|
||||
table.assetApplyFilters?.();
|
||||
}
|
||||
table.assetUpdateRowNumbers?.();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user