246 lines
8.9 KiB
JavaScript
246 lines
8.9 KiB
JavaScript
(function initLargeTableColumnResizeModule() {
|
|
const STORAGE_PREFIX = 'sg:large-table:column-widths:';
|
|
const STORAGE_VERSION = 3;
|
|
const DEFAULT_MIN_WIDTH = 128;
|
|
const KEYBOARD_STEP = 8;
|
|
const KEYBOARD_LARGE_STEP = 40;
|
|
let activeResize = null;
|
|
|
|
const getTables = (root) => root.querySelectorAll('[data-component="large-table"][data-column-resize="true"]');
|
|
|
|
const getColumnHeaders = (table) => Array.from(
|
|
table.querySelectorAll('[data-component-part="large-table-header-row"] [role="columnheader"]'),
|
|
);
|
|
|
|
const getMinWidth = (table) => {
|
|
const configured = table.dataset.columnMinWidth;
|
|
const value = configured || getComputedStyle(table).getPropertyValue('--large-table-column-min-width').trim();
|
|
if (!value) {
|
|
return DEFAULT_MIN_WIDTH;
|
|
}
|
|
|
|
const probe = document.createElement('span');
|
|
probe.style.cssText = `position:absolute;width:${value};visibility:hidden;`;
|
|
table.appendChild(probe);
|
|
const width = probe.getBoundingClientRect().width;
|
|
probe.remove();
|
|
return width > 0 ? width : DEFAULT_MIN_WIDTH;
|
|
};
|
|
|
|
const parseDefaultWidth = (rawValue, tableWidth) => {
|
|
if (!rawValue) {
|
|
return null;
|
|
}
|
|
const value = Number.parseFloat(rawValue);
|
|
if (!Number.isFinite(value) || value <= 0) {
|
|
return null;
|
|
}
|
|
if (rawValue.trim().endsWith('%')) {
|
|
return tableWidth * value / 100;
|
|
}
|
|
if (rawValue.trim().endsWith('fr')) {
|
|
return value;
|
|
}
|
|
return value;
|
|
};
|
|
|
|
const normalizeRatios = (values) => {
|
|
const safeValues = values.map((value) => (Number.isFinite(value) && value > 0 ? value : 0));
|
|
const total = safeValues.reduce((sum, value) => sum + value, 0);
|
|
if (total <= 0) {
|
|
return Array(values.length).fill(1 / values.length);
|
|
}
|
|
return safeValues.map((value) => value / total);
|
|
};
|
|
|
|
const getDefaultRatios = (table, headers, minWidth) => {
|
|
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));
|
|
if (configured.some((width) => width === null)) {
|
|
return Array(headers.length).fill(1 / headers.length);
|
|
}
|
|
return normalizeRatios(configured);
|
|
};
|
|
|
|
const getStorageKey = (table) => `${STORAGE_PREFIX}${table.dataset.columnResizeKey || ''}`;
|
|
|
|
const readStoredRatios = (table, count) => {
|
|
const key = getStorageKey(table);
|
|
if (!table.dataset.columnResizeKey) {
|
|
return null;
|
|
}
|
|
try {
|
|
if (!window.localStorage) {
|
|
return null;
|
|
}
|
|
const stored = JSON.parse(window.localStorage.getItem(key));
|
|
if (stored?.version !== STORAGE_VERSION || !Array.isArray(stored.ratios) || stored.ratios.length !== count) {
|
|
return null;
|
|
}
|
|
if (stored.ratios.some((ratio) => !Number.isFinite(ratio) || ratio <= 0)) {
|
|
return null;
|
|
}
|
|
return normalizeRatios(stored.ratios);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const writeStoredRatios = (table, ratios) => {
|
|
if (!table.dataset.columnResizeKey) {
|
|
return;
|
|
}
|
|
try {
|
|
if (!window.localStorage) {
|
|
return;
|
|
}
|
|
window.localStorage.setItem(getStorageKey(table), JSON.stringify({
|
|
version: STORAGE_VERSION,
|
|
ratios: normalizeRatios(ratios),
|
|
}));
|
|
} catch (error) {
|
|
// Storage can be unavailable in privacy-restricted browser contexts.
|
|
}
|
|
};
|
|
|
|
const updateHandleState = (table, widths, minWidth) => {
|
|
const handles = table.querySelectorAll('[data-large-table-resize-handle]');
|
|
handles.forEach((handle) => {
|
|
const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10);
|
|
const current = widths[index];
|
|
const maximum = Math.max(minWidth, widths[index] + widths[index + 1] - minWidth);
|
|
if (table.id) {
|
|
handle.setAttribute('aria-controls', table.id);
|
|
}
|
|
handle.setAttribute('aria-valuemin', String(Math.round(minWidth)));
|
|
handle.setAttribute('aria-valuemax', String(Math.round(maximum)));
|
|
handle.setAttribute('aria-valuenow', String(Math.round(current)));
|
|
handle.setAttribute('aria-valuetext', `${Math.round(current)} Pixel`);
|
|
});
|
|
};
|
|
|
|
const getRenderedWidths = (table, count, minWidth) => {
|
|
const headers = getColumnHeaders(table);
|
|
const widths = headers.map((header) => header.getBoundingClientRect().width);
|
|
if (widths.length === count && widths.every((width) => width > 0)) {
|
|
return widths;
|
|
}
|
|
return Array(count).fill(minWidth);
|
|
};
|
|
|
|
const applyRatios = (table, ratios, minWidth) => {
|
|
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 nextLeft = Math.max(minWidth, Math.min(widths[index] + widths[index + 1] - minWidth, widths[index] + delta));
|
|
const appliedDelta = nextLeft - widths[index];
|
|
widths[index] = nextLeft;
|
|
widths[index + 1] -= appliedDelta;
|
|
applyRatios(table, widths.map((width) => width / widths.reduce((sum, value) => sum + value, 0)), minWidth);
|
|
if (persist) {
|
|
writeStoredRatios(table, table._largeTableColumnRatios);
|
|
}
|
|
};
|
|
|
|
const initTable = (table) => {
|
|
const headers = getColumnHeaders(table);
|
|
if (headers.length < 2) {
|
|
return;
|
|
}
|
|
const minWidth = getMinWidth(table);
|
|
const stored = readStoredRatios(table, headers.length);
|
|
applyRatios(table, stored || getDefaultRatios(table, headers, minWidth), minWidth);
|
|
|
|
if (table.dataset.largeTableResizeInitialized === 'true') {
|
|
return;
|
|
}
|
|
table.dataset.largeTableResizeInitialized = 'true';
|
|
|
|
table.addEventListener('pointerdown', (event) => {
|
|
const handle = event.target.closest('[data-large-table-resize-handle]');
|
|
if (!handle || !table.contains(handle)) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
activeResize = {
|
|
table,
|
|
handle,
|
|
index: Number.parseInt(handle.dataset.largeTableResizeHandle, 10),
|
|
startX: event.clientX,
|
|
startWidths: [...table._largeTableColumnWidths],
|
|
};
|
|
handle.dataset.largeTableResizeActive = 'true';
|
|
table.dataset.columnResizeState = 'resizing';
|
|
handle.setPointerCapture?.(event.pointerId);
|
|
});
|
|
|
|
table.addEventListener('pointermove', (event) => {
|
|
if (!activeResize || activeResize.table !== table) {
|
|
return;
|
|
}
|
|
resizeColumn(
|
|
table,
|
|
activeResize.index,
|
|
event.clientX - activeResize.startX,
|
|
false,
|
|
activeResize.startWidths,
|
|
);
|
|
});
|
|
|
|
table.addEventListener('pointerup', () => {
|
|
if (!activeResize || activeResize.table !== table) {
|
|
return;
|
|
}
|
|
writeStoredRatios(table, table._largeTableColumnRatios);
|
|
activeResize.handle.dataset.largeTableResizeActive = 'false';
|
|
table.dataset.columnResizeState = 'idle';
|
|
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) => {
|
|
const handle = event.target.closest('[data-large-table-resize-handle]');
|
|
if (!handle || !table.contains(handle)) {
|
|
return;
|
|
}
|
|
const index = Number.parseInt(handle.dataset.largeTableResizeHandle, 10);
|
|
const step = event.shiftKey ? KEYBOARD_LARGE_STEP : KEYBOARD_STEP;
|
|
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
|
event.preventDefault();
|
|
resizeColumn(table, index, event.key === 'ArrowRight' ? step : -step, true);
|
|
} else if (event.key === 'Home' || event.key === 'End') {
|
|
event.preventDefault();
|
|
const minWidth = getMinWidth(table);
|
|
const current = table._largeTableColumnWidths[index];
|
|
const target = event.key === 'Home' ? minWidth : current + table._largeTableColumnWidths[index + 1] - minWidth;
|
|
resizeColumn(table, index, target - current, true);
|
|
}
|
|
});
|
|
|
|
};
|
|
|
|
window.sgInitLargeTableColumnResize = (options = {}) => {
|
|
const root = options.root || document;
|
|
getTables(root).forEach(initTable);
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => window.sgInitLargeTableColumnResize(), { once: true });
|
|
})();
|