UNPKG

@tanstack/table-core

Version:

Headless UI for building powerful tables & datagrids for TS/JS.

262 lines (260 loc) 8.85 kB
import { callMemoOrStaticFn, isFunction, makeObjectMap } from "../../utils.js"; //#region src/features/cell-spanning/cellSpanningFeature.utils.ts const EMPTY_ROW_SPANS = makeObjectMap(); const EMPTY_COLUMN_INDEXES = makeObjectMap(); const EMPTY_COL_SPANS = []; const NO_SECTION_STARTS = []; /** * Resolves the rows in the order a renderer actually draws them, plus the * positions at which a new visual section begins. * * Without row pinning this is the final row model itself, returned by * reference so the common path allocates nothing. With row pinning it is the * concatenation the three-list renderer walks: `getTopRows()`, * `getCenterRows()`, `getBottomRows()`. `getCenterRows()` filters the row * model by the pinned ids, so no row appears in two sections and every row * gets exactly one position; with `keepPinnedRows` a pinned row that filtering * removed from the row model still appears exactly once, in its pinned * section. * * The pinning reads are optional-chained so the feature stays correct when * `rowPinningFeature` is not registered. */ function getRenderedRows(table) { const rows = table.getRowModel().rows; const rowPinning = table.atoms.rowPinning?.get(); if (!rowPinning || !rowPinning.top.length && !rowPinning.bottom.length) return { rows, sectionStarts: NO_SECTION_STARTS }; const pinnedTable = table; const top = pinnedTable.getTopRows?.() ?? []; const center = pinnedTable.getCenterRows?.() ?? rows; const bottom = pinnedTable.getBottomRows?.() ?? []; return { rows: [ ...top, ...center, ...bottom ], sectionStarts: [top.length, top.length + center.length] }; } /** * Resolves the visible columns in the order cells render, plus the two * boundaries between the start-pinned, center, and end-pinned regions. * * Read off a rendered row's visible cells rather than re-derived, so the * ordering is the renderer's own by construction. `getVisibleCells` only * exists when `columnVisibilityFeature` is registered; without it renderers * walk `row.getAllCells()`, whose order is `table.getAllLeafColumns()`. */ function getRenderedColumns(table, firstRow) { const cells = firstRow.getVisibleCells?.() ?? firstRow.getAllCells(); const columns = new Array(cells.length); for (let i = 0; i < cells.length; i++) columns[i] = cells[i].column; const pinning = table.atoms.columnPinning?.get(); let centerStart = 0; let centerEnd = columns.length; if (pinning) { while (centerStart < centerEnd && pinning.start.includes(columns[centerStart].id)) centerStart++; while (centerEnd > centerStart && pinning.end.includes(columns[centerEnd - 1].id)) centerEnd--; } return { columns, centerStart, centerEnd }; } /** * Checks whether this column takes part in cell spanning. * * A column def opting out with `enableCellSpanning: false` wins over the table * option, matching how the other per-column enable flags resolve. * * @example * ```ts * const canSpan = column_getCanSpan(column) * ``` */ function column_getCanSpan(column) { if (column.columnDef.enableCellSpanning === false) return false; return column.table.options.enableCellSpanning ?? true; } /** * Builds the table's cell span index for the rows that are currently rendered. * * Spans are always derived from scratch from the final row model, so sorting, * filtering, pagination, expansion, and row pinning only change adjacency and * the index follows. Nothing is persisted and there is nothing to configure. * * @example * ```ts * const spanIndex = table_getCellSpanIndex(table) * ``` */ function table_getCellSpanIndex(table) { const { rows, sectionStarts } = getRenderedRows(table); const rowCount = rows.length; const empty = { colSpans: EMPTY_COL_SPANS, columnIndexes: EMPTY_COLUMN_INDEXES, rowSpans: EMPTY_ROW_SPANS, rows }; if (!rowCount || table.options.enableCellSpanning === false) return empty; const { columns, centerStart, centerEnd } = getRenderedColumns(table, rows[0]); const columnCount = columns.length; if (!columnCount) return empty; const columnIndexes = makeObjectMap(); for (let c = 0; c < columnCount; c++) columnIndexes[columns[c].id] = c; const breaks = new Uint8Array(rowCount); for (let s = 0; s < sectionStarts.length; s++) { const start = sectionStarts[s]; if (start < rowCount) breaks[start] = 1; } for (let r = 0; r < rowCount; r++) { const row = rows[r]; row._cellSpanRowIndex = r; const prev = r > 0 ? rows[r - 1] : void 0; if (prev && (row.depth !== prev.depth || row.parentId !== prev.parentId) || row.getIsGrouped?.() === true) breaks[r] = 1; } const colSpans = []; let anyColSpan = false; for (let c = 0; c < columnCount; c++) { const column = columns[c]; const spanColumns = column.columnDef.spanColumns; if (spanColumns === void 0 || !column_getCanSpan(column)) continue; const regionEnd = c < centerStart ? centerStart : c < centerEnd ? centerEnd : columnCount; for (let r = 0; r < rowCount; r++) { const existing = colSpans[r]; if (existing && existing[c] === 0) continue; const requested = isFunction(spanColumns) ? spanColumns({ column, row: rows[r], table }) : spanColumns; if (!(requested > 1)) continue; const span = Math.min(requested, regionEnd - c); if (span < 2) continue; const target = existing ?? (colSpans[r] = new Int32Array(columnCount).fill(1)); target[c] = span; for (let k = c + 1; k < c + span; k++) target[k] = 0; anyColSpan = true; } } const rowSpans = makeObjectMap(); for (let c = 0; c < columnCount; c++) { const column = columns[c]; const spanRows = column.columnDef.spanRows; if (!spanRows || !column_getCanSpan(column)) continue; if (column.getIsGrouped?.() === true) continue; const columnId = column.id; const predicate = isFunction(spanRows) ? spanRows : void 0; const spans = new Int32Array(rowCount).fill(1); let anyRun = false; let anchorIndex = -1; let anchorValue; let anchorColSpan = 1; for (let r = 0; r < rowCount; r++) { const row = rows[r]; const rowColSpans = anyColSpan ? colSpans[r] : void 0; const cellColSpan = rowColSpans ? rowColSpans[c] : 1; if (cellColSpan === 0) { anchorIndex = -1; continue; } const value = row.getValue(columnId); if (anchorIndex !== -1 && !breaks[r] && cellColSpan === anchorColSpan && (predicate ? predicate({ anchorRow: rows[anchorIndex], anchorValue, column, previousRow: rows[r - 1], row, table, value }) : value != null && Object.is(value, anchorValue))) { spans[r] = 0; spans[anchorIndex] = spans[anchorIndex] + 1; anyRun = true; continue; } anchorIndex = r; anchorValue = value; anchorColSpan = cellColSpan; } if (anyRun) rowSpans[columnId] = spans; } return { colSpans, columnIndexes, rowSpans, rows }; } function resolveRowIndex(index, cell) { const rowIndex = cell.row._cellSpanRowIndex; if (rowIndex === void 0 || index.rows[rowIndex] !== cell.row) return -1; return rowIndex; } /** * Returns how many rows this cell spans. * * `1` when it does not span, and `0` when a spanning cell above it covers it, * matching the `header.rowSpan` convention where `0` means "skip this cell". * * Deliberately not memoized: a per-cell memo would allocate a closure and a * dependency array for every cell, costing more than the two lookups this * performs against the table-level span index. * * @example * ```ts * const rowSpan = cell_getRowSpan(cell) * ``` */ function cell_getRowSpan(cell) { const table = cell.row.table; const index = callMemoOrStaticFn(table, "getCellSpanIndex", table_getCellSpanIndex); const spans = index.rowSpans[cell.column.id]; if (!spans) return 1; const rowIndex = resolveRowIndex(index, cell); if (rowIndex === -1) return 1; return spans[rowIndex]; } /** * Returns how many columns this cell spans. * * `1` when it does not span, and `0` when another cell's column span covers * it. * * @example * ```ts * const colSpan = cell_getColSpan(cell) * ``` */ function cell_getColSpan(cell) { const table = cell.row.table; const index = callMemoOrStaticFn(table, "getCellSpanIndex", table_getCellSpanIndex); const rowIndex = resolveRowIndex(index, cell); if (rowIndex === -1) return 1; const spans = index.colSpans[rowIndex]; if (!spans) return 1; const columnIndex = index.columnIndexes[cell.column.id]; return columnIndex === void 0 ? 1 : spans[columnIndex]; } /** * Checks whether another cell's span covers this cell. * * Covered cells must not be rendered; the cell that covers them carries the * content and the span attributes. * * @example * ```ts * const isCovered = cell_getIsCovered(cell) * ``` */ function cell_getIsCovered(cell) { return cell_getRowSpan(cell) === 0 || cell_getColSpan(cell) === 0; } //#endregion export { cell_getColSpan, cell_getIsCovered, cell_getRowSpan, column_getCanSpan, table_getCellSpanIndex };