fix table with handling, icons implemented

This commit is contained in:
root
2026-08-09 08:50:13 +02:00
parent 5e32be7d0c
commit 3ff0794cfc
43 changed files with 1027 additions and 95 deletions
+133
View File
@@ -0,0 +1,133 @@
(() => {
function initImageEditor(editor) {
if (!editor || editor.dataset.imageEditorReady === '1') return;
const removeButton = editor.querySelector('[data-image-remove]');
const removeInput = editor.querySelector('[data-image-remove-input]');
let preview = editor.querySelector('[data-image-preview]');
let previewImage = preview?.querySelector('img');
const imageInput = editor.querySelector('input[type="file"][data-image-input]');
const libraryUpload = editor.querySelector('input[type="file"][data-image-library-upload]');
const libraryChoices = Array.from(editor.querySelectorAll('[data-image-library-choice]'));
const pending = editor.querySelector('[data-image-remove-pending]');
if (!removeInput) return;
const originalPreviewSrc = previewImage?.getAttribute('src') || '';
let objectUrl = null;
const releaseObjectUrl = () => {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
objectUrl = null;
}
};
const ensurePreview = () => {
if (preview && previewImage) return;
preview = document.createElement('div');
preview.className = 'asset-edit-image-preview';
preview.dataset.imagePreview = '';
preview.hidden = true;
preview.style.display = 'none';
previewImage = document.createElement('img');
previewImage.className = 'asset-edit-preview-image';
previewImage.alt = '';
preview.appendChild(previewImage);
const firstControl = editor.querySelector('[data-image-remove-input]');
editor.insertBefore(preview, firstControl);
};
const showPreview = src => {
ensurePreview();
if (src) previewImage.src = src;
preview.hidden = false;
preview.style.display = '';
if (removeButton) {
removeButton.hidden = false;
removeButton.disabled = false;
}
};
const hidePreview = () => {
if (!preview) return;
preview.hidden = true;
preview.style.display = 'none';
if (removeButton) { removeButton.hidden = true; removeButton.disabled = true; }
};
const clearLibraryChoices = () => {
libraryChoices.forEach(choice => { choice.checked = false; });
};
const clearRemoveState = () => {
removeInput.value = '0';
if (pending) pending.hidden = true;
if (removeButton) {
removeButton.disabled = false;
removeButton.setAttribute('aria-pressed', 'false');
}
};
const setRemoveState = remove => {
removeInput.value = remove ? '1' : '0';
if (pending) pending.hidden = !remove;
if (removeButton) {
removeButton.disabled = remove;
removeButton.setAttribute('aria-pressed', remove ? 'true' : 'false');
}
if (remove) {
releaseObjectUrl();
if (imageInput) imageInput.value = '';
if (libraryUpload) libraryUpload.value = '';
clearLibraryChoices();
hidePreview();
}
};
removeButton?.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
setRemoveState(true);
});
imageInput?.addEventListener('change', () => {
if (!imageInput.files || !imageInput.files.length) return;
releaseObjectUrl();
objectUrl = URL.createObjectURL(imageInput.files[0]);
if (libraryUpload) libraryUpload.value = '';
clearLibraryChoices();
clearRemoveState();
showPreview(objectUrl);
});
libraryUpload?.addEventListener('change', () => {
if (!libraryUpload.files || !libraryUpload.files.length) return;
releaseObjectUrl();
objectUrl = URL.createObjectURL(libraryUpload.files[0]);
if (imageInput) imageInput.value = '';
clearLibraryChoices();
clearRemoveState();
showPreview(objectUrl);
});
libraryChoices.forEach(choice => {
choice.addEventListener('change', () => {
if (!choice.checked) return;
releaseObjectUrl();
if (imageInput) imageInput.value = '';
if (libraryUpload) libraryUpload.value = '';
clearRemoveState();
showPreview(choice.dataset.imageSrc || choice.value);
});
});
window.addEventListener('beforeunload', releaseObjectUrl, { once: true });
editor.dataset.imageEditorReady = '1';
editor.dataset.originalImageSrc = originalPreviewSrc;
}
document.querySelectorAll('[data-image-editor]').forEach(initImageEditor);
})();
+230 -20
View File
@@ -302,18 +302,48 @@
return normalized;
}
function freezeTableGeometry() {
if (table.id === 'asset-table') return;
const widths = [...headerRow.cells].map(cell => Math.max(minimumColumnWidth, Math.round(cell.getBoundingClientRect().width || minimumColumnWidth)));
const total = widths.reduce((sum, width) => sum + width, 0);
widths.forEach((width, columnIndex) => {
if (headerRow.cells[columnIndex]?.dataset.rowNumberColumn === '1') return;
applyColumnWidth(columnIndex, width);
});
table.style.tableLayout = 'fixed';
table.style.width = `${total}px`;
table.style.minWidth = `${total}px`;
}
function synchronizeTableWidth() {
if (table.id === 'asset-table') return;
const total = [...headerRow.cells].reduce((sum, cell) => sum + Math.round(cell.getBoundingClientRect().width || 0), 0);
if (total > 0) {
table.style.width = `${total}px`;
table.style.minWidth = `${total}px`;
}
}
function applyStoredColumnWidths() {
let restored = false;
[...headerRow.cells].forEach((header, columnIndex) => {
if (header.dataset.rowNumberColumn === '1') return;
const stored = readStoredColumnWidth(header, columnIndex);
if (stored !== null) applyColumnWidth(columnIndex, stored);
if (stored !== null) {
applyColumnWidth(columnIndex, stored);
restored = true;
}
});
if (restored && table.id !== 'asset-table') {
table.style.tableLayout = 'fixed';
requestAnimationFrame(synchronizeTableWidth);
}
}
function installColumnResizers() {
[...headerRow.cells].forEach((header, columnIndex) => {
if (header.dataset.rowNumberColumn === '1') return;
if (header.dataset.noResize === '1') return;
if (header.dataset.noResize === '1' || header.dataset.rowNumberColumn === '1') return;
if (header.querySelector(':scope > .table-column-resizer')) return;
header.classList.add('table-resizable-column-header');
@@ -334,34 +364,72 @@
event.preventDefault();
event.stopPropagation();
const startX = event.clientX;
const measured = header.getBoundingClientRect().width;
const startWidth = measured > 0 ? measured : minimumColumnWidth;
handle.setPointerCapture?.(event.pointerId);
freezeTableGeometry();
const headers = [...headerRow.cells];
const rightmostIndex = headers.length - 1;
const startWidth = header.getBoundingClientRect().width || minimumColumnWidth;
const rightmostHeader = headers[rightmostIndex];
const rightmostStartWidth = rightmostHeader?.getBoundingClientRect().width || minimumColumnWidth;
const keepTableWidth = columnIndex !== rightmostIndex;
document.documentElement.classList.add('table-column-resizing');
const move = moveEvent => {
applyColumnWidth(columnIndex, startWidth + moveEvent.clientX - startX);
const resizePair = delta => {
if (!keepTableWidth) {
applyColumnWidth(columnIndex, startWidth + delta);
synchronizeTableWidth();
return;
}
// All columns between the dragged column and the far-right column
// stay unchanged. The far-right column alone absorbs the delta.
const minDelta = minimumColumnWidth - startWidth;
const maxDelta = rightmostStartWidth - minimumColumnWidth;
const effectiveDelta = Math.max(minDelta, Math.min(maxDelta, delta));
applyColumnWidth(columnIndex, startWidth + effectiveDelta);
applyColumnWidth(rightmostIndex, rightmostStartWidth - effectiveDelta);
};
const finish = finishEvent => {
const current = header.getBoundingClientRect().width || startWidth;
saveColumnWidth(header, columnIndex, current);
handle.releasePointerCapture?.(finishEvent.pointerId);
const move = moveEvent => resizePair(moveEvent.clientX - startX);
const finish = () => {
saveColumnWidth(header, columnIndex, header.getBoundingClientRect().width || startWidth);
if (keepTableWidth && rightmostHeader) {
saveColumnWidth(rightmostHeader, rightmostIndex, rightmostHeader.getBoundingClientRect().width || rightmostStartWidth);
} else {
synchronizeTableWidth();
}
document.documentElement.classList.remove('table-column-resizing');
handle.removeEventListener('pointermove', move);
handle.removeEventListener('pointerup', finish);
handle.removeEventListener('pointercancel', finish);
window.removeEventListener('pointermove', move, true);
window.removeEventListener('pointerup', finish, true);
window.removeEventListener('pointercancel', finish, true);
};
handle.addEventListener('pointermove', move);
handle.addEventListener('pointerup', finish);
handle.addEventListener('pointercancel', finish);
window.addEventListener('pointermove', move, true);
window.addEventListener('pointerup', finish, true);
window.addEventListener('pointercancel', finish, true);
});
handle.addEventListener('keydown', event => {
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
event.preventDefault();
event.stopPropagation();
freezeTableGeometry();
const headers = [...headerRow.cells];
const rightmostIndex = headers.length - 1;
const step = event.key === 'ArrowRight' ? 12 : -12;
const current = header.getBoundingClientRect().width || minimumColumnWidth;
saveColumnWidth(header, columnIndex, current + (event.key === 'ArrowRight' ? 12 : -12));
if (columnIndex === rightmostIndex) {
saveColumnWidth(header, columnIndex, current + step);
synchronizeTableWidth();
return;
}
const rightmostHeader = headers[rightmostIndex];
const rightmostWidth = rightmostHeader?.getBoundingClientRect().width || minimumColumnWidth;
const minDelta = minimumColumnWidth - current;
const maxDelta = rightmostWidth - minimumColumnWidth;
const effectiveDelta = Math.max(minDelta, Math.min(maxDelta, step));
saveColumnWidth(header, columnIndex, current + effectiveDelta);
if (rightmostHeader) saveColumnWidth(rightmostHeader, rightmostIndex, rightmostWidth - effectiveDelta);
});
});
}
@@ -796,8 +864,10 @@
});
thead.appendChild(filterRow);
installColumnResizers();
applyStoredColumnWidths();
if (isAssetTable) {
installColumnResizers();
applyStoredColumnWidths();
}
// Editable cells are always read live, so changed values immediately
// participate in filtering without a stale cache.
@@ -840,7 +910,147 @@
}));
}
function initUniversalTableResize(table) {
if (!table || table.id === 'asset-table' || table.dataset.columnResizeReady === '1') return;
const thead = table.tHead;
const tbody = table.tBodies?.[0];
if (!thead || !tbody || !thead.rows.length) return;
const headerRow = thead.rows[0];
const storagePart = table.dataset.storageKey || table.id || `${window.location.pathname}:table-${[...document.querySelectorAll('main table')].indexOf(table)}`;
const storagePrefix = `assetmanager:table:${storagePart}:column-width:`;
const minWidth = 56;
const maxWidth = 1200;
const normalize = width => Math.min(maxWidth, Math.max(minWidth, Math.round(width)));
const columnKey = (header, index) => header.dataset.field || `index-${index}`;
const applyWidth = (index, width) => {
const normalized = normalize(width);
[...thead.rows, ...tbody.rows].forEach(row => {
const cell = row.cells?.[index];
if (!cell) return;
cell.style.width = `${normalized}px`;
cell.style.minWidth = `${normalized}px`;
cell.style.maxWidth = `${normalized}px`;
});
return normalized;
};
const freezeGeometry = () => {
const widths = [...headerRow.cells].map(cell => normalize(cell.getBoundingClientRect().width || minWidth));
const total = widths.reduce((sum, width) => sum + width, 0);
widths.forEach((width, index) => applyWidth(index, width));
table.style.tableLayout = 'fixed';
table.style.width = `${total}px`;
table.style.minWidth = `${total}px`;
};
const syncTableWidth = () => {
const total = [...headerRow.cells].reduce((sum, cell) => sum + Math.round(cell.getBoundingClientRect().width || 0), 0);
if (total > 0) {
table.style.width = `${total}px`;
table.style.minWidth = `${total}px`;
}
};
[...headerRow.cells].forEach((header, index) => {
if (header.dataset.noResize === '1' || header.dataset.rowNumberColumn === '1') return;
const key = `${storagePrefix}${columnKey(header, index)}`;
try {
const stored = Number.parseInt(localStorage.getItem(key) || '', 10);
if (Number.isFinite(stored)) applyWidth(index, stored);
} catch (_) {}
if (header.querySelector(':scope > .table-column-resizer')) return;
header.classList.add('table-resizable-column-header');
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.columnResizeLabel || 'Drag to change the column width';
header.appendChild(handle);
const save = width => {
const normalized = applyWidth(index, width);
try { localStorage.setItem(key, String(normalized)); } catch (_) {}
};
handle.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); });
handle.addEventListener('pointerdown', event => {
event.preventDefault(); event.stopPropagation();
const startX = event.clientX;
freezeGeometry();
const headers = [...headerRow.cells];
const rightmostIndex = headers.length - 1;
const startWidth = header.getBoundingClientRect().width || minWidth;
const rightmostHeader = headers[rightmostIndex];
const rightmostStartWidth = rightmostHeader?.getBoundingClientRect().width || minWidth;
const keepTableWidth = index !== rightmostIndex;
document.documentElement.classList.add('table-column-resizing');
const resizePair = delta => {
if (!keepTableWidth) {
applyWidth(index, startWidth + delta);
syncTableWidth();
return;
}
const minDelta = minWidth - startWidth;
const maxDelta = rightmostStartWidth - minWidth;
const effectiveDelta = Math.max(minDelta, Math.min(maxDelta, delta));
applyWidth(index, startWidth + effectiveDelta);
applyWidth(rightmostIndex, rightmostStartWidth - effectiveDelta);
};
const move = e => resizePair(e.clientX - startX);
const finish = () => {
save(header.getBoundingClientRect().width || startWidth);
if (keepTableWidth && rightmostHeader) {
const rightKey = `${storagePrefix}${columnKey(rightmostHeader, rightmostIndex)}`;
const rightWidth = applyWidth(rightmostIndex, rightmostHeader.getBoundingClientRect().width || rightmostStartWidth);
try { localStorage.setItem(rightKey, String(rightWidth)); } catch (_) {}
} else {
syncTableWidth();
}
document.documentElement.classList.remove('table-column-resizing');
window.removeEventListener('pointermove', move, true);
window.removeEventListener('pointerup', finish, true);
window.removeEventListener('pointercancel', finish, true);
};
window.addEventListener('pointermove', move, true);
window.addEventListener('pointerup', finish, true);
window.addEventListener('pointercancel', finish, true);
});
handle.addEventListener('keydown', event => {
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
event.preventDefault(); event.stopPropagation();
freezeGeometry();
const headers = [...headerRow.cells];
const rightmostIndex = headers.length - 1;
const step = event.key === 'ArrowRight' ? 12 : -12;
const current = header.getBoundingClientRect().width || minWidth;
if (index === rightmostIndex) {
save(current + step);
syncTableWidth();
return;
}
const rightmostHeader = headers[rightmostIndex];
const rightmostWidth = rightmostHeader?.getBoundingClientRect().width || minWidth;
const minDelta = minWidth - current;
const maxDelta = rightmostWidth - minWidth;
const effectiveDelta = Math.max(minDelta, Math.min(maxDelta, step));
save(current + effectiveDelta);
if (rightmostHeader) {
const rightKey = `${storagePrefix}${columnKey(rightmostHeader, rightmostIndex)}`;
const rightWidth = applyWidth(rightmostIndex, rightmostWidth - effectiveDelta);
try { localStorage.setItem(rightKey, String(rightWidth)); } catch (_) {}
}
});
});
table.dataset.columnResizeReady = '1';
}
document.querySelectorAll('table.data-table').forEach(initTable);
document.querySelectorAll('main table:not(#asset-table):not([data-column-resize="off"])').forEach(initUniversalTableResize);
window.addEventListener('asset-table-content-reloaded', event => {
const table = event.detail?.table;