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
+120 -6
View File
@@ -1,4 +1,4 @@
:root{font-family:Arial,sans-serif;color:#222;background:#f3f5f7}*{box-sizing:border-box}body{margin:0}header{height:58px;background:#036;color:#fff;display:flex;align-items:center;padding:0 24px;gap:36px}.brand{font-size:22px;font-weight:bold}nav{display:flex;gap:20px}header a{color:#fff;text-decoration:none}main{width:100%;max-width:none;margin:24px 0;padding:0 24px}.toolbar{display:flex;justify-content:space-between;align-items:center}.button,button{background:#0878bd;color:#fff;border:0;border-radius:4px;padding:10px 15px;text-decoration:none;cursor:pointer}.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px}.card{display:flex;gap:14px;background:#fff;border:1px solid #d8dde2;border-radius:6px;padding:14px;text-decoration:none;color:#222}.card img,.placeholder{width:58px;height:58px;object-fit:cover;border-radius:5px}.placeholder{display:grid;place-items:center;background:#dbeaf3;font-size:26px}.card small{display:block;color:#667;margin-top:8px}table{width:100%;border-collapse:collapse;background:#fff}th,td{text-align:left;border-bottom:1px solid #ddd;padding:10px}th{background:#e8edf1}a{color:#056ca8}.filters{display:flex;gap:10px;margin:12px 0 18px;flex-wrap:wrap}.filters a{background:#fff;border:1px solid #ccd4da;padding:7px 10px;border-radius:4px;text-decoration:none}form{background:#fff;padding:20px;border:1px solid #d8dde2;border-radius:6px}label{display:block;margin-bottom:14px;font-weight:bold}input,select,textarea{display:block;width:100%;padding:9px;margin-top:5px;border:1px solid #bbb;border-radius:4px;font:inherit}textarea{min-height:100px}.detail{display:grid;grid-template-columns:280px 1fr;gap:24px;background:#fff;padding:20px}.asset-image{max-width:100%;max-height:260px;object-fit:contain}dl{display:grid;grid-template-columns:220px 1fr;margin:0}dt,dd{padding:9px;border-bottom:1px solid #eee;margin:0}dt{font-weight:bold}@media(max-width:700px){.detail{grid-template-columns:1fr}dl{grid-template-columns:1fr}header{padding:0 10px;gap:12px}nav{gap:9px}}
:root{font-family:Arial,sans-serif;color:#222;background:#f3f5f7}*{box-sizing:border-box}body{margin:0}header{height:58px;background:#036;color:#fff;display:flex;align-items:center;padding:0 24px;gap:36px}.brand{font-size:22px;font-weight:bold}nav{display:flex;gap:20px}header a{color:#fff;text-decoration:none}main{width:100%;max-width:none;margin:24px 0;padding:0 24px}.toolbar{display:flex;justify-content:space-between;align-items:center}.button,button{background:#0878bd;color:#fff;border:0;border-radius:4px;padding:10px 15px;text-decoration:none;cursor:pointer}.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px}.card{display:flex;gap:14px;background:#fff;border:1px solid #d8dde2;border-radius:6px;padding:14px;text-decoration:none;color:#222}.card img,.placeholder{width:58px;height:58px;object-fit:contain;border-radius:5px}.card img{flex:0 0 58px;padding:2px;background:transparent}.placeholder{display:grid;place-items:center;background:#dbeaf3;font-size:26px}.card small{display:block;color:#667;margin-top:8px}table{width:100%;border-collapse:collapse;background:#fff}th,td{text-align:left;border-bottom:1px solid #ddd;padding:10px}th{background:#e8edf1}a{color:#056ca8}.filters{display:flex;gap:10px;margin:12px 0 18px;flex-wrap:wrap}.filters a{background:#fff;border:1px solid #ccd4da;padding:7px 10px;border-radius:4px;text-decoration:none}form{background:#fff;padding:20px;border:1px solid #d8dde2;border-radius:6px}label{display:block;margin-bottom:14px;font-weight:bold}input,select,textarea{display:block;width:100%;padding:9px;margin-top:5px;border:1px solid #bbb;border-radius:4px;font:inherit}textarea{min-height:100px}.detail{display:grid;grid-template-columns:280px 1fr;gap:24px;background:#fff;padding:20px}.asset-image{max-width:100%;max-height:260px;object-fit:contain}dl{display:grid;grid-template-columns:220px 1fr;margin:0}dt,dd{padding:9px;border-bottom:1px solid #eee;margin:0}dt{font-weight:bold}@media(max-width:700px){.detail{grid-template-columns:1fr}dl{grid-template-columns:1fr}header{padding:0 10px;gap:12px}nav{gap:9px}}
.panel{background:#fff;border:1px solid #ccd4dc;border-radius:4px;padding:18px;margin-top:18px;overflow:auto}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.card{background:#fff;border:1px solid #ccd4dc;padding:16px;display:flex;flex-direction:column}.card strong{font-size:28px}.config-list{display:grid;grid-template-columns:minmax(130px,220px) 1fr;gap:8px 16px}.sync-status{display:inline-block;padding:3px 8px;border-radius:12px;background:#e5e7eb}.status-success,.status-synced,.status-created,.status-updated{background:#d9f2df}.status-conflict,.status-partial{background:#fff0c2}.status-error,.status-missing{background:#ffd9d9}code{word-break:break-all}
.log-header{display:flex;align-items:center;justify-content:space-between;gap:16px}.log-header h2{margin:0}.sync-log{height:340px;overflow:auto;background:#111827;color:#e5e7eb;border-radius:5px;padding:12px;font-family:Consolas,"Courier New",monospace;font-size:13px;line-height:1.55;white-space:pre-wrap}.log-line{padding:1px 0}.log-debug{color:#9ca3af}.log-info{color:#dbeafe}.log-success{color:#86efac}.log-warning{color:#fde68a}.log-error{color:#fca5a5}button:disabled{opacity:.65;cursor:wait}
.toolbar-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.inline-form{display:inline;margin:0;padding:0;background:transparent;border:0}.button-danger{background:#b42318}.button-danger:hover{background:#912018}.empty-state{text-align:center;color:#667;padding:24px}
@@ -2934,12 +2934,12 @@ html.table-column-resizing * {
/* User-resizable widths for every data-table column. */
.data-table th.table-resizable-column-header {
table th.table-resizable-column-header {
position: relative;
padding-right: 14px;
overflow: visible;
}
.data-table .table-column-resizer {
table .table-column-resizer {
position: absolute;
top: 0;
right: -4px;
@@ -2949,7 +2949,7 @@ html.table-column-resizing * {
z-index: 4;
touch-action: none;
}
.data-table .table-column-resizer::after {
table .table-column-resizer::after {
content: "";
position: absolute;
top: 20%;
@@ -2957,7 +2957,121 @@ html.table-column-resizing * {
left: 4px;
border-left: 1px solid transparent;
}
.data-table .table-column-resizer:hover::after,
.data-table .table-column-resizer:focus-visible::after {
table .table-column-resizer:hover::after,
table .table-column-resizer:focus-visible::after {
border-left-color: currentColor;
}
/* Asset list device preview: first real column after the generated row number. */
.asset-visual-column {
width: 64px;
min-width: 64px;
max-width: 64px;
text-align: center;
padding-left: 6px !important;
padding-right: 6px !important;
}
.asset-visual-link { display:inline-flex; align-items:center; justify-content:center; width:48px; height:42px; }
.asset-list-device-image { max-width:46px; max-height:38px; width:auto; height:auto; object-fit:contain; border-radius:4px; }
.asset-list-device-placeholder { font-size:24px; opacity:.35; line-height:1; }
.mesh-icon-category-label { align-items:stretch; }
.mesh-icon-category-title { display:flex; align-items:center; gap:8px; min-height:38px; }
.mesh-icon-preview { width:38px; height:38px; object-fit:contain; flex:0 0 38px; }
/* v0.5.5.55 asset/category image management */
.asset-edit-image-preview{display:flex;align-items:center;gap:14px;margin:8px 0 10px;padding:10px;border:1px solid #d8dde2;border-radius:6px;background:var(--am-surface-soft,#f8fafc);max-width:520px}
.asset-edit-preview-image{width:110px;height:90px;object-fit:contain;border-radius:5px;background:transparent;border:1px solid #e1e5e9}
.asset-edit-image-preview-actions{display:flex;flex-direction:column;align-items:flex-start;gap:8px}
.button-small{padding:7px 10px;font-size:.9rem}
.category-image-cell{text-align:center}
.category-table-image{width:52px;height:42px;object-fit:contain;border-radius:4px;display:inline-block;background:transparent}
html[data-theme="dark"] .asset-edit-image-preview{border-color:#46515d;background:#252b31}
html[data-theme="dark"] .asset-edit-preview-image{background:transparent;border-color:#4a5561}
/* v0.5.5.56: consistent transparent image presentation and reliable table resize handles. */
.asset-list-device-image,
.category-table-image,
.asset-edit-preview-image,
.card img {
background: transparent !important;
}
.card img,
.asset-list-device-image,
.category-table-image,
.asset-edit-preview-image {
object-fit: contain !important;
object-position: center center;
}
table th.table-resizable-column-header {
position: relative;
}
table th.table-resizable-column-header > .table-column-resizer {
right: 0;
width: 12px;
z-index: 30;
}
table th.table-resizable-column-header > .table-column-resizer::after {
left: auto;
right: 0;
border-left-color: color-mix(in srgb, currentColor 28%, transparent);
}
table th.table-resizable-column-header > .table-column-resizer:hover::after,
table th.table-resizable-column-header > .table-column-resizer:focus-visible::after {
border-left-color: currentColor;
}
/* v0.5.5.57: universal table resize visibility and reliable image editor state. */
.asset-edit-image-preview[hidden],
.image-remove-pending[hidden] {
display: none !important;
}
.image-remove-pending {
margin: 8px 0 10px;
padding: 9px 11px;
max-width: 520px;
border: 1px solid var(--am-warning, #d97706);
border-radius: 6px;
background: color-mix(in srgb, var(--am-warning, #d97706) 10%, transparent);
font-size: .9rem;
}
/* Every resizable header has a permanent separator; the transparent hit area
remains wider than the visible line for easy mouse/touch dragging. */
table th.table-resizable-column-header {
position: relative !important;
box-shadow: inset -1px 0 0 color-mix(in srgb, currentColor 28%, transparent);
}
table th.table-resizable-column-header > .table-column-resizer {
display: block !important;
pointer-events: auto !important;
position: absolute !important;
top: 0 !important;
right: -5px !important;
bottom: 0 !important;
width: 11px !important;
z-index: 100 !important;
cursor: col-resize !important;
touch-action: none;
background: transparent;
}
table th.table-resizable-column-header:hover > .table-column-resizer::after,
table th.table-resizable-column-header > .table-column-resizer:focus-visible::after {
border-left-color: currentColor !important;
}
/* Reusable standard image library */
.standard-image-library{margin-top:14px;padding-top:12px;border-top:1px solid var(--border,#d8dde2);max-width:760px}
.image-library-help,.image-library-empty{margin:4px 0 9px}
.standard-image-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:8px;margin:8px 0 12px;max-height:330px;overflow:auto;padding:2px}
.standard-image-choice{position:relative;display:flex;flex-direction:column;align-items:center;gap:5px;min-width:0;padding:7px;border:1px solid var(--border,#d8dde2);border-radius:7px;cursor:pointer;background:transparent}
.standard-image-choice:hover{border-color:var(--primary,#2583d8)}
.standard-image-choice:has(input:checked){outline:2px solid var(--primary,#2583d8);outline-offset:-2px}
.standard-image-choice input{position:absolute;top:6px;left:6px;margin:0}
.standard-image-choice img{width:72px;height:58px;object-fit:contain;background:transparent}
.standard-image-choice span{display:block;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center;font-size:.78rem}
.image-upload-label{display:flex;flex-direction:column;align-items:flex-start;gap:5px;margin-top:8px}
html[data-theme="dark"] .standard-image-library{border-color:#46515d}
html[data-theme="dark"] .standard-image-choice{border-color:#46515d}
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

+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;