import { NileGrid } from './nile-grid'; import { applyColumnsTemplate } from './nile-grid.layout'; import { getHead } from './nile-grid.utils'; import { PrefixColumn } from './types/nile-grid.types'; /** computeColumnWidths * @param nileGrid Grid instance (reads minColumnWidth, columnWidths) * @param colCount Total columns * @param columnWidths Mutable widths array to fill/update * @param slotEl Slot element (to read head for desired widths) * @returns void */ export function computeColumnWidths( nileGrid: NileGrid, colCount: number, columnWidths: number[], slotEl: any, lockedWidthColumns: boolean[], headerChanged: boolean ): void { const minCol = nileGrid.minColumnWidth; const containerWidth = getContainerWidth(nileGrid); const desired = readDesiredFromHead(slotEl, colCount, containerWidth, nileGrid, lockedWidthColumns, headerChanged, columnWidths); const isSeeded = seedWidthsFromDesired(desired, columnWidths, minCol); distributeFlex(columnWidths, isSeeded, containerWidth, minCol); if(!nileGrid.enableScroll) { normalizeToContainer(columnWidths, containerWidth, minCol); } clampAndRound(columnWidths, minCol); applyColumnsTemplate(nileGrid, columnWidths); } /** getContainerWidth * @param nileGrid Grid root element * @returns Numeric container width in pixels (accounting for padding/offset) */ function getContainerWidth(nileGrid: NileGrid): number { return Math.max(0, (nileGrid.getBoundingClientRect().width || 0) - 4); } /** * @param slotEl The slot element containing head rows * @param colCount Number of columns * @param containerWidth Width of the parent container * @param prevWidths Previously computed column widths * @returns Array of desired column widths */ function readDesiredFromHead( slotEl: any, colCount: number, containerWidth: number, nileGrid: NileGrid, lockedWidthColumns: boolean[], headerChanged: boolean, prevWidths?: number[] ): (number | null)[] { const desired = Array(colCount).fill(null); if (headerChanged) { const head = getHead(slotEl) as HTMLElement | null; if (head) fillDesiredFromHeadRows(head, desired, colCount, containerWidth, lockedWidthColumns); } const hasPrevWidths = !!prevWidths?.length && prevWidths.some(w => w > 0); if (hasPrevWidths && prevWidths?.length === colCount) { fillFromPrevWidths(desired, prevWidths, nileGrid); } return desired; } /** * @param head Head element containing grid header rows * @param desired Desired widths array to fill * @param colCount Column count * @param containerWidth Grid container width * @param fixedWidthColumns Fixed width columns */ function fillDesiredFromHeadRows( head: HTMLElement, desired: (number | null)[], colCount: number, containerWidth: number, lockedWidthColumns: boolean[] ) { const headRows = [...head.children].filter( el => el.tagName.toLowerCase() === 'nile-grid-row' ) as HTMLElement[]; for (const row of headRows) { fillFromHeadRow(row, desired, colCount, containerWidth, lockedWidthColumns); } } /** * @param row A single header row element * @param desired Desired widths array * @param colCount Total number of columns * @param containerWidth Container width * @param lockedWidthColumns Locked width columns * @returns void */ function fillFromHeadRow( row: HTMLElement, desired: (number | null)[], colCount: number, containerWidth: number, lockedWidthColumns: boolean[] ) { const items = [...row.children].filter( el => el.tagName.toLowerCase() === 'nile-grid-head-item' ) as HTMLElement[]; const parsed = items .map(h => parseHeadItemWidth(h, containerWidth, lockedWidthColumns)) .filter(x => x.cw != null); parsed.sort((a, b) => a.colspan - b.colspan); parsed.forEach(({ cStart, colspan, cw }) => { assignWidth(desired, cStart, colspan, cw!, colCount); }); } /** * @param h Header item element * @param containerWidth Width of parent container * @param lockedWidthColumns Locked width columns * @returns Parsed object containing start, colspan, and computed width */ function parseHeadItemWidth(h: HTMLElement, containerWidth: number, lockedWidthColumns: boolean[]) { const cStart = Number((h as any).dataset?.cStart) || 1; const colspan = Math.max(1, Number(h.getAttribute('colspan')) || 1); const raw = (h as any).width ?? h.getAttribute('width') ?? null; if (h.hasAttribute('lockWidth')) { lockedWidthColumns[cStart - 1] = true; } const cw = parseCustomWidth(raw, containerWidth); return { cStart, colspan, cw }; } /** * @param desired Array of desired widths * @param cStart Start column index * @param colspan Column span value * @param cw Computed width value * @param colCount Total column count */ function assignWidth( desired: (number | null)[], cStart: number, colspan: number, cw: number, colCount: number ) { const perCol = Math.round(cw / colspan); for (let k = 0; k < colspan; k++) { const idx = cStart - 1 + k; if (idx >= 0 && idx < colCount && desired[idx] == null) { desired[idx] = perCol; } } } /** * @param desired Desired widths array * @param prevWidths Previous column widths */ function fillFromPrevWidths(desired: (number | null)[], prevWidths: number[], nileGrid: NileGrid) { for (let i = 0; i < prevWidths.length; i++) { if (desired[i] == null && Number.isFinite(prevWidths[i]!)) { desired[i] = Math.max(nileGrid.minColumnWidth, Math.round(prevWidths[i]!)); } } } /** parseCustomWidth * @param raw User-specified width (number | string like "120px" or "20%") * @param containerWidth Container width for % resolution * @returns Width in pixels or null if invalid */ function parseCustomWidth(raw: any, containerWidth: number): number | null { if (raw == null) return null; if (typeof raw === 'number' && Number.isFinite(raw)) return raw; const s = String(raw).trim().toLowerCase(); if (!s) return null; if (s.endsWith('px')) { const v = parseFloat(s); return Number.isFinite(v) ? v : null; } if (s.endsWith('%')) { const v = parseFloat(s); return Number.isFinite(v) ? (v / 100) * containerWidth : null; } const v = parseFloat(s); return Number.isFinite(v) ? v : null; } /** seedWidthsFromDesired * @param desired Array of desired widths (px) or nulls * @param columnWidths Output array to seed (mutated) * @param minCol Minimum per-column width * @returns Boolean[] flags indicating which columns were seeded */ function seedWidthsFromDesired( desired: (number | null)[], columnWidths: number[], minCol: number ): boolean[] { const isSeeded = Array(desired.length).fill(false); for (let i = 0; i < desired.length; i++) { const w = desired[i] ?? null; columnWidths[i] = w != null && !Number.isNaN(w) ? Math.max(minCol, Number(w)) : 0; isSeeded[i] = w != null && !Number.isNaN(w); } return isSeeded; } /** distributeFlex * @param columnWidths Mutable widths array * @param isSeeded Boolean[] indicating fixed/seeded columns * @param containerWidth Total width to fill * @param minCol Minimum width per flexible column * @returns void */ function distributeFlex( columnWidths: number[], isSeeded: boolean[], containerWidth: number, minCol: number ) { const sumSeed = columnWidths.reduce((a, b) => a + b, 0); const remaining = containerWidth - sumSeed; const flexIdxs = Array.from( { length: columnWidths.length }, (_, i) => i ).filter(i => !isSeeded[i]); if (flexIdxs.length === 0) return; const each = remaining / flexIdxs.length; for (const i of flexIdxs) columnWidths[i] = Math.max(minCol, each); } // Normalization (container fitting): adjust total column widths to match the container width proportionally. /** * @param columnWidths Array of current column widths * @param containerWidth Total grid container width * @param minCol Minimum allowed column width */ function normalizeToContainer( columnWidths: number[], containerWidth: number, minCol: number ) { let total = sumWidths(columnWidths); if (total < containerWidth) { expandWidths(columnWidths, containerWidth, total); } else if (total > containerWidth) { shrinkWidths(columnWidths, containerWidth, total, minCol); } } /** * @param widths Array of column widths * @returns Sum of all column widths */ function sumWidths(widths: number[]) { return widths.reduce((a, b) => a + b, 0); } /** * @param columnWidths Current column widths * @param containerWidth Container total width * @param total Current total width */ function expandWidths( columnWidths: number[], containerWidth: number, total: number ) { if (!columnWidths.length) return; const add = containerWidth - total; const weight = total || 1; for (let i = 0; i < columnWidths.length; i++) { columnWidths[i] += add * (columnWidths[i] / weight); } } /** * @param columnWidths Current widths * @param containerWidth Container width * @param total Current total * @param minCol Minimum width constraint */ function shrinkWidths( columnWidths: number[], containerWidth: number, total: number, minCol: number ) { let need = total - containerWidth; for (let iter = 0; iter < 8 && need > 0.5; iter++) { const { candidates, weightSum } = collectReducible(columnWidths, minCol); if (!candidates.length || weightSum <= 0) break; const reduced = reduceWidths( columnWidths, candidates, weightSum, need, minCol ); need -= reduced; if (reduced < 0.5) break; } } /** * @param columnWidths Current widths * @param minCol Minimum column width * @returns Reducible column indexes and weight sum */ function collectReducible(columnWidths: number[], minCol: number) { const candidates: number[] = []; let weightSum = 0; for (let i = 0; i < columnWidths.length; i++) { const reducible = Math.max(0, columnWidths[i] - minCol); if (reducible > 0) { candidates.push(i); weightSum += columnWidths[i]; } } return { candidates, weightSum }; } /** * @param columnWidths Current widths array * @param candidates Reducible column indexes * @param weightSum Sum of reducible widths * @param need Total amount to reduce * @param minCol Minimum width * @returns Total reduced width */ function reduceWidths( columnWidths: number[], candidates: number[], weightSum: number, need: number, minCol: number ) { let reduced = 0; for (const i of candidates) { const reducible = Math.max(0, columnWidths[i] - minCol); const share = columnWidths[i] / weightSum; const cut = Math.min(reducible, need * share); columnWidths[i] -= cut; reduced += cut; } return reduced; } function clampAndRound(columnWidths: number[], minCol: number) { for (let i = 0; i < columnWidths.length; i++) { columnWidths[i] = Math.max(minCol, Math.round(columnWidths[i])); } } /** * @param columnWidths Current widths array * @param minCol Minimum column width * @returns Total reducible pixels */ export function totalReducibleRight(w: number[], start: number, floor: number, fixed?: boolean[]) { let sum = 0; for (let i = start; i < w.length; i++) { if (fixed?.[i]) continue; sum += Math.max(0, w[i] - floor); } return sum; } /** * @param columnWidths Current widths * @param amount Pixels to remove from rightmost columns */ export function takeFromRight( widths: number[], start: number, needInit: number, floor: number, fixed?: boolean[] ) { let need = needInit; for (let i = start; i < widths.length && need > 0; i++) { if (fixed?.[i]) continue; const room = widths[i] - floor; if (room <= 0) continue; const cut = Math.min(room, need); widths[i] -= cut; need -= cut; } return need; } /** * @param columnWidths Current widths * @param amount Pixels to add to rightmost columns */ export function giveToRight(widths: number[], start: number, giveInit: number) { let give = giveInit; if (start < widths.length && give > 0) { widths[start] += give; give = 0; } return give; } export function addVariablesValue(nileGrid: NileGrid) { const columnWidths = nileGrid.columnWidths; const leftStickyColumns = nileGrid.stickyLeftIndexes; const rightStickyColumns = nileGrid.stickyRightIndexes; const prefixColumns: PrefixColumn[] = []; const root = nileGrid as HTMLElement; // --- LEFT STICKY --- let totalLeft = 0; for (let i = 0; i < leftStickyColumns.length; i++) { const colIndex = leftStickyColumns[i]; prefixColumns.push({ colNumber: colIndex, left: totalLeft, right: 0 }); root.style.setProperty(`--sticky-left-${colIndex}`, `${totalLeft}px`); totalLeft += columnWidths[colIndex]; } // --- RIGHT STICKY --- let totalRight = 0; for (let i = rightStickyColumns.length - 1; i >= 0; i--) { const colIndex = rightStickyColumns[i]; prefixColumns.push({ colNumber: colIndex, left: 0, right: totalRight }); root.style.setProperty(`--sticky-right-${colIndex}`, `${totalRight}px`); totalRight += columnWidths[colIndex]; } nileGrid.prefixSumColumnsWidth = prefixColumns; }