Make large table widths fluid

This commit is contained in:
gitea_admin committed 2026-09-28 11:06:11 +02:00
1 parent 0165b6c1da
commit d88540d859
3 files changed
+58 -65

No files matched your search

+1 -1
View File
@@ -77,7 +77,7 @@
<p class="sg-preview-label">Component: Large Table</p> <p class="sg-preview-label">Component: Large Table</p>
<p>Suche, Sortierung und „Mehr laden“ sind hier Demo-Markup. Produktions-Implementierungen müssen Treffer serverseitig neu rendern.</p> <p>Suche, Sortierung und „Mehr laden“ sind hier Demo-Markup. Produktions-Implementierungen müssen Treffer serverseitig neu rendern.</p>
<p>Optionale Spaltenbreiten-Anpassung: <code>data-column-resize="true"</code> aktiviert die Funktion. Die Tabelle benötigt dafür eine stabile, portalweit eindeutige <code>data-column-resize-key</code>. Die Header-Zellen können mit <code>data-default-width</code> (Prozent, Pixel oder relative <code>fr</code>-Werte) und die Tabelle mit <code>data-column-min-width</code> konfiguriert werden. Die Runtime wird nach dynamischem Nachladen über <code>window.sgInitLargeTableColumnResize({ root })</code> erneut aufgerufen. Gespeicherte Werte liegen als <code>{ version: 1, widths: number[] }</code> unter <code>sg:large-table:column-widths:&lt;key&gt;</code> in <code>localStorage</code>.</p> <p>Optionale Spaltenbreiten-Anpassung: <code>data-column-resize="true"</code> aktiviert die Funktion. Die Tabelle benötigt dafür eine stabile, portalweit eindeutige <code>data-column-resize-key</code>. Die Header-Zellen können mit <code>data-default-width</code> (Prozent, Pixel oder relative <code>fr</code>-Werte) und die Tabelle mit <code>data-column-min-width</code> konfiguriert werden. Die Runtime wird nach dynamischem Nachladen über <code>window.sgInitLargeTableColumnResize({ root })</code> erneut aufgerufen. Gespeicherte Werte liegen als <code>{ version: 3, ratios: number[] }</code> unter <code>sg:large-table:column-widths:&lt;key&gt;</code> in <code>localStorage</code>; die Werte sind proportionale Anteile der verfügbaren Tabellenbreite.</p>
<article id="styleguide-large-table" class="sg-card sg-large-table" data-component="large-table" data-column-resize="true" data-column-resize-key="styleguide-large-table" data-column-min-width="8rem" role="table" aria-label="Beispiel Large Table"> <article id="styleguide-large-table" class="sg-card sg-large-table" data-component="large-table" data-column-resize="true" data-column-resize-key="styleguide-large-table" data-column-min-width="8rem" role="table" aria-label="Beispiel Large Table">
<div class="sg-large-table__viewport" data-component-part="large-table-viewport"> <div class="sg-large-table__viewport" data-component-part="large-table-viewport">
+56 -56
View File
@@ -1,6 +1,6 @@
(function initLargeTableColumnResizeModule() { (function initLargeTableColumnResizeModule() {
const STORAGE_PREFIX = 'sg:large-table:column-widths:'; const STORAGE_PREFIX = 'sg:large-table:column-widths:';
const STORAGE_VERSION = 2; const STORAGE_VERSION = 3;
const DEFAULT_MIN_WIDTH = 128; const DEFAULT_MIN_WIDTH = 128;
const KEYBOARD_STEP = 8; const KEYBOARD_STEP = 8;
const KEYBOARD_LARGE_STEP = 40; const KEYBOARD_LARGE_STEP = 40;
@@ -44,41 +44,27 @@
return value; return value;
}; };
const getGridTrackWidth = (table) => { const normalizeRatios = (values) => {
const row = table.querySelector('[data-component-part="large-table-header-row"]'); const safeValues = values.map((value) => (Number.isFinite(value) && value > 0 ? value : 0));
if (!row) { const total = safeValues.reduce((sum, value) => sum + value, 0);
return table.clientWidth;
}
const rowStyles = getComputedStyle(row);
const horizontalPadding = (parseFloat(rowStyles.paddingLeft) || 0)
+ (parseFloat(rowStyles.paddingRight) || 0);
return Math.max(0, row.clientWidth - horizontalPadding);
};
const normalizeWidths = (widths, tableWidth, minWidth) => {
const safeWidths = widths.map((width) => Math.max(minWidth, width));
const total = safeWidths.reduce((sum, width) => sum + width, 0);
if (total <= 0) { if (total <= 0) {
return Array(widths.length).fill(Math.max(minWidth, tableWidth / widths.length)); return Array(values.length).fill(1 / values.length);
} }
if (widths.some((width) => width !== width)) { return safeValues.map((value) => value / total);
return Array(widths.length).fill(Math.max(minWidth, tableWidth / widths.length));
}
return safeWidths.map((width) => width * tableWidth / total);
}; };
const getDefaultWidths = (table, headers, minWidth) => { const getDefaultRatios = (table, headers, minWidth) => {
const tableWidth = Math.max(getGridTrackWidth(table), minWidth * headers.length); const tableWidth = Math.max(table.querySelector('[data-component-part="large-table-viewport"]')?.clientWidth || 0, minWidth * headers.length);
const configured = headers.map((header) => parseDefaultWidth(header.dataset.defaultWidth, tableWidth)); const configured = headers.map((header) => parseDefaultWidth(header.dataset.defaultWidth, tableWidth));
if (configured.some((width) => width === null)) { if (configured.some((width) => width === null)) {
return Array(headers.length).fill(Math.max(minWidth, tableWidth / headers.length)); return Array(headers.length).fill(1 / headers.length);
} }
return normalizeWidths(configured, tableWidth, minWidth); return normalizeRatios(configured);
}; };
const getStorageKey = (table) => `${STORAGE_PREFIX}${table.dataset.columnResizeKey || ''}`; const getStorageKey = (table) => `${STORAGE_PREFIX}${table.dataset.columnResizeKey || ''}`;
const readStoredWidths = (table, count, minWidth) => { const readStoredRatios = (table, count) => {
const key = getStorageKey(table); const key = getStorageKey(table);
if (!table.dataset.columnResizeKey) { if (!table.dataset.columnResizeKey) {
return null; return null;
@@ -88,19 +74,19 @@
return null; return null;
} }
const stored = JSON.parse(window.localStorage.getItem(key)); const stored = JSON.parse(window.localStorage.getItem(key));
if (stored?.version !== STORAGE_VERSION || !Array.isArray(stored.widths) || stored.widths.length !== count) { if (stored?.version !== STORAGE_VERSION || !Array.isArray(stored.ratios) || stored.ratios.length !== count) {
return null; return null;
} }
if (stored.widths.some((width) => !Number.isFinite(width) || width < minWidth)) { if (stored.ratios.some((ratio) => !Number.isFinite(ratio) || ratio <= 0)) {
return null; return null;
} }
return stored.widths; return normalizeRatios(stored.ratios);
} catch (error) { } catch (error) {
return null; return null;
} }
}; };
const writeStoredWidths = (table, widths) => { const writeStoredRatios = (table, ratios) => {
if (!table.dataset.columnResizeKey) { if (!table.dataset.columnResizeKey) {
return; return;
} }
@@ -110,7 +96,7 @@
} }
window.localStorage.setItem(getStorageKey(table), JSON.stringify({ window.localStorage.setItem(getStorageKey(table), JSON.stringify({
version: STORAGE_VERSION, version: STORAGE_VERSION,
widths: widths.map((width) => Math.round(width)), ratios: normalizeRatios(ratios),
})); }));
} catch (error) { } catch (error) {
// Storage can be unavailable in privacy-restricted browser contexts. // Storage can be unavailable in privacy-restricted browser contexts.
@@ -133,35 +119,37 @@
}); });
}; };
const applyWidths = (table, widths, minWidth) => { const getRenderedWidths = (table, count, minWidth) => {
const safeWidths = widths.map((width) => Math.max(minWidth, width)); const headers = getColumnHeaders(table);
const baseTotal = safeWidths.reduce((sum, width) => sum + width, 0); const widths = headers.map((header) => header.getBoundingClientRect().width);
const effectiveTotal = Math.max(baseTotal, getGridTrackWidth(table)); if (widths.length === count && widths.every((width) => width > 0)) {
const effectiveWidths = safeWidths.map((width, index) => ( return widths;
index === safeWidths.length - 1 ? width + effectiveTotal - baseTotal : width }
)); return Array(count).fill(minWidth);
const total = effectiveWidths.reduce((sum, width) => sum + width, 0);
const columnTemplate = effectiveWidths.map((width, index) => (
index === safeWidths.length - 1
? `minmax(${Math.round(width)}px, 1fr)`
: `${Math.round(width)}px`
)).join(' ');
table.style.setProperty('--large-table-grid-template-columns', columnTemplate);
table.style.setProperty('--large-table-grid-min-width', `${Math.ceil(total)}px`);
table._largeTableColumnWidths = effectiveWidths;
updateHandleState(table, effectiveWidths, minWidth);
}; };
const resizeColumn = (table, index, delta, persist) => { const applyRatios = (table, ratios, minWidth) => {
const widths = [...table._largeTableColumnWidths]; const safeRatios = normalizeRatios(ratios);
const columnTemplate = safeRatios
.map((ratio) => `minmax(${Math.ceil(minWidth)}px, ${ratio}fr)`)
.join(' ');
table.style.setProperty('--large-table-grid-template-columns', columnTemplate);
table.style.setProperty('--large-table-grid-min-width', `${Math.ceil(minWidth * safeRatios.length)}px`);
table._largeTableColumnRatios = safeRatios;
table._largeTableColumnWidths = getRenderedWidths(table, safeRatios.length, minWidth);
updateHandleState(table, table._largeTableColumnWidths, minWidth);
};
const resizeColumn = (table, index, delta, persist, baseWidths = table._largeTableColumnWidths) => {
const widths = [...baseWidths];
const minWidth = getMinWidth(table); const minWidth = getMinWidth(table);
const nextLeft = Math.max(minWidth, Math.min(widths[index] + widths[index + 1] - minWidth, widths[index] + delta)); const nextLeft = Math.max(minWidth, Math.min(widths[index] + widths[index + 1] - minWidth, widths[index] + delta));
const appliedDelta = nextLeft - widths[index]; const appliedDelta = nextLeft - widths[index];
widths[index] = nextLeft; widths[index] = nextLeft;
widths[index + 1] -= appliedDelta; widths[index + 1] -= appliedDelta;
applyWidths(table, widths, minWidth); applyRatios(table, widths.map((width) => width / widths.reduce((sum, value) => sum + value, 0)), minWidth);
if (persist) { if (persist) {
writeStoredWidths(table, widths); writeStoredRatios(table, table._largeTableColumnRatios);
} }
}; };
@@ -171,8 +159,8 @@
return; return;
} }
const minWidth = getMinWidth(table); const minWidth = getMinWidth(table);
const stored = readStoredWidths(table, headers.length, minWidth); const stored = readStoredRatios(table, headers.length);
applyWidths(table, stored || getDefaultWidths(table, headers, minWidth), minWidth); applyRatios(table, stored || getDefaultRatios(table, headers, minWidth), minWidth);
if (table.dataset.largeTableResizeInitialized === 'true') { if (table.dataset.largeTableResizeInitialized === 'true') {
return; return;
@@ -201,20 +189,32 @@
if (!activeResize || activeResize.table !== table) { if (!activeResize || activeResize.table !== table) {
return; return;
} }
applyWidths(table, activeResize.startWidths, getMinWidth(table)); resizeColumn(
resizeColumn(table, activeResize.index, event.clientX - activeResize.startX, false); table,
activeResize.index,
event.clientX - activeResize.startX,
false,
activeResize.startWidths,
);
}); });
table.addEventListener('pointerup', () => { table.addEventListener('pointerup', () => {
if (!activeResize || activeResize.table !== table) { if (!activeResize || activeResize.table !== table) {
return; return;
} }
writeStoredWidths(table, table._largeTableColumnWidths); writeStoredRatios(table, table._largeTableColumnRatios);
activeResize.handle.dataset.largeTableResizeActive = 'false'; activeResize.handle.dataset.largeTableResizeActive = 'false';
table.dataset.columnResizeState = 'idle'; table.dataset.columnResizeState = 'idle';
activeResize = null; activeResize = null;
}); });
if (typeof ResizeObserver === 'function') {
table._largeTableResizeObserver = new ResizeObserver(() => {
applyRatios(table, table._largeTableColumnRatios, getMinWidth(table));
});
table._largeTableResizeObserver.observe(table.querySelector('[data-component-part="large-table-viewport"]'));
}
table.addEventListener('keydown', (event) => { table.addEventListener('keydown', (event) => {
const handle = event.target.closest('[data-large-table-resize-handle]'); const handle = event.target.closest('[data-large-table-resize-handle]');
if (!handle || !table.contains(handle)) { if (!handle || !table.contains(handle)) {
+1 -8
View File
@@ -106,14 +106,7 @@
.sg-large-table[data-column-resize="true"] .sg-large-table__row, .sg-large-table[data-column-resize="true"] .sg-large-table__row,
.sg-large-table[data-column-resize="true"] .sg-large-table__title-segment { .sg-large-table[data-column-resize="true"] .sg-large-table__title-segment {
width: max( width: max(100%, var(--large-table-grid-min-width, 0px));
100%,
calc(
var(--large-table-grid-min-width, 0px) +
var(--card-segment-padding-horizontal) +
var(--card-segment-padding-horizontal)
)
);
} }
.sg-large-table__row--hidden { .sg-large-table__row--hidden {