UNPKG

@tanstack/table-core

Version:

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

614 lines (612 loc) 21.5 kB
import { callMemoOrStaticFn, cloneState, copyInstancePropertiesWithoutMemos, hasOwn, makeObjectMap } from "../../utils.js"; //#region src/features/row-selection/rowSelectionFeature.utils.ts /** * Creates the default row selection state. * * The feature default is an empty map, meaning no rows are selected. Reset APIs * use this value when `defaultState` is `true`. * * @example * ```ts * const selection = getDefaultRowSelectionState() * ``` */ function getDefaultRowSelectionState() { return makeObjectMap(); } /** * Routes a row selection updater through the table's selection change handler. * * The updater may be a next selection map or a function of the previous map, * matching the instance `table.setRowSelection` behavior. * * @example * ```ts * table_setRowSelection(table, (old) => ({ ...old, [rowId]: true })) * ``` */ function table_setRowSelection(table, updater) { table.options.onRowSelectionChange?.(updater); } /** * Resets `rowSelection` to the configured initial state or feature default. * * With no argument, the reset clones `table.initialState.rowSelection` when it * exists. Passing `true` ignores initial state and resets to `{}`. * * @example * ```ts * table_resetRowSelection(table) * table_resetRowSelection(table, true) * ``` */ function table_resetRowSelection(table, defaultState) { table._lastSelectedRowId = null; table_setRowSelection(table, defaultState ? makeObjectMap() : Object.assign(makeObjectMap(), cloneState(table.initialState.rowSelection ?? {}))); } /** * Selects or deselects every selectable row before grouping. * * Omitting `value` toggles based on `table_getIsAllRowsSelected(table)`. * Selecting skips sub-rows whose ancestors block descent via * `enableSubRowSelection`. Deselecting removes matching selectable ids from the * existing selection map; rows that cannot be selected keep their selection * unless `opts.deselectAll` is `true`. * * @example * ```ts * table_toggleAllRowsSelected(table) * ``` */ function table_toggleAllRowsSelected(table, value, opts) { table._lastSelectedRowId = null; table_setRowSelection(table, (old) => { value = typeof value !== "undefined" ? value : !callMemoOrStaticFn(table, "getIsAllRowsSelected", table_getIsAllRowsSelected); if (opts?.deselectAll && !value) return makeObjectMap(); const rowSelection = Object.assign(makeObjectMap(), old); const preGroupedFlatRows = table.getPreGroupedRowModel().flatRows; if (value) { const subtreeCache = /* @__PURE__ */ new Map(); preGroupedFlatRows.forEach((row) => { if (isRowSelectableInSelectAll(row, subtreeCache)) rowSelection[row.id] = true; }); } else preGroupedFlatRows.forEach((row) => { if (row_getCanSelect(row)) delete rowSelection[row.id]; }); return rowSelection; }); } /** * Selects or deselects every selectable row on the current page. * * Omitting `value` toggles based on `table_getIsAllPageRowsSelected(table)`. * Child rows are included when sub-row selection allows it. * * @example * ```ts * table_toggleAllPageRowsSelected(table) * ``` */ function table_toggleAllPageRowsSelected(table, value, opts) { table._lastSelectedRowId = null; table_setRowSelection(table, (old) => { const resolvedValue = typeof value !== "undefined" ? value : !callMemoOrStaticFn(table, "getIsAllPageRowsSelected", table_getIsAllPageRowsSelected); if (opts?.deselectAll && !resolvedValue) return makeObjectMap(); const rowSelection = Object.assign(makeObjectMap(), old); table.getRowModel().rows.forEach((row) => { mutateRowIsSelected(rowSelection, row.id, resolvedValue, true, table, true); }); return rowSelection; }); } /** * Reads the row model before row selection is projected into selected rows. * * Selection does not alter the base row pipeline, so this returns the core row * model. * * @example * ```ts * const rowsBeforeSelection = table_getPreSelectedRowModel(table) * ``` */ function table_getPreSelectedRowModel(table) { return table.getCoreRowModel(); } /** * Builds a row model containing selected rows from the core row model. * * If no row ids are selected, an empty row model is returned without walking * the rows. * * @example * ```ts * const selectedRows = table_getSelectedRowModel(table) * ``` */ function table_getSelectedRowModel(table) { const rowModel = table.getCoreRowModel(); if (!callMemoOrStaticFn(table, "getIsSomeRowsSelected", table_getIsSomeRowsSelected)) return { rows: [], flatRows: [], rowsById: makeObjectMap() }; return selectRowsFn(rowModel, table); } /** * Builds a row model containing selected rows from the filtered row model. * * If no row ids are selected, an empty row model is returned without walking * the rows. * * @example * ```ts * const selectedRows = table_getFilteredSelectedRowModel(table) * ``` */ function table_getFilteredSelectedRowModel(table) { const rowModel = table.getFilteredRowModel(); if (!callMemoOrStaticFn(table, "getIsSomeRowsSelected", table_getIsSomeRowsSelected)) return { rows: [], flatRows: [], rowsById: makeObjectMap() }; return selectRowsFn(rowModel, table); } /** * Builds a row model containing selected rows from the grouped row model. * * If no row ids are selected, an empty row model is returned without walking * the rows. * * @example * ```ts * const selectedRows = table_getGroupedSelectedRowModel(table) * ``` */ function table_getGroupedSelectedRowModel(table) { const rowModel = table.getSortedRowModel(); if (!callMemoOrStaticFn(table, "getIsSomeRowsSelected", table_getIsSomeRowsSelected)) return { rows: [], flatRows: [], rowsById: makeObjectMap() }; return selectRowsFn(rowModel, table); } /** * Returns the ids of all selected rows. * * @example * ```ts * const selectedRowIds = table_getSelectedRowIds(table) * ``` */ function table_getSelectedRowIds(table) { return Object.keys(table.atoms.rowSelection?.get() ?? {}); } /** * Checks whether every selectable filtered row is selected. * * The result is false when there are no filtered rows or when selection state is * empty. Sub-rows whose ancestors block descent via `enableSubRowSelection` are * ignored, matching the rows that `table_toggleAllRowsSelected` selects. * * @example * ```ts * const allSelected = table_getIsAllRowsSelected(table) * ``` */ function table_getIsAllRowsSelected(table) { const preGroupedFlatRows = table.getFilteredRowModel().flatRows; const rowSelection = table.atoms.rowSelection?.get() ?? {}; let isAllRowsSelected = Boolean(preGroupedFlatRows.length && Object.keys(rowSelection).length); if (isAllRowsSelected) { const subtreeCache = /* @__PURE__ */ new Map(); if (preGroupedFlatRows.some((row) => !isRowSelected(row, rowSelection) && isRowSelectableInSelectAll(row, subtreeCache))) isAllRowsSelected = false; } return isAllRowsSelected; } /** * Checks whether every selectable row on the current page is selected. * * Non-selectable rows are ignored for this calculation, as are sub-rows whose * ancestors block descent via `enableSubRowSelection`. * * @example * ```ts * const allPageRowsSelected = table_getIsAllPageRowsSelected(table) * ``` */ function table_getIsAllPageRowsSelected(table) { const paginationFlatRows = table.getPaginatedRowModel().flatRows; const rowSelection = table.atoms.rowSelection?.get() ?? {}; const subtreeCache = /* @__PURE__ */ new Map(); let sawSelectableRow = false; for (let i = 0; i < paginationFlatRows.length; i++) { const row = paginationFlatRows[i]; if (!isRowSelected(row, rowSelection)) { if (isRowSelectableInSelectAll(row, subtreeCache)) return false; } else if (!sawSelectableRow && isRowSelectableInSelectAll(row, subtreeCache)) sawSelectableRow = true; } return sawSelectableRow; } /** * Checks whether at least one row id is selected. * * The result stays true when every row is selected. * * @example * ```ts * const someRowsSelected = table_getIsSomeRowsSelected(table) * ``` */ function table_getIsSomeRowsSelected(table) { return callMemoOrStaticFn(table, "getSelectedRowIds", table_getSelectedRowIds).length > 0; } /** * Checks whether at least one selectable row on the current page is selected. * * @example * ```ts * const somePageRowsSelected = table_getIsSomePageRowsSelected(table) * ``` */ function table_getIsSomePageRowsSelected(table) { return table.getPaginatedRowModel().flatRows.filter((row) => row_getCanSelect(row)).some((row) => row_getIsSelected(row) || callMemoOrStaticFn(row, "getIsSomeSelected", row_getIsSomeSelected)); } /** * Creates a checkbox-style handler that selects or deselects all rows. * * The handler reads `event.target.checked`, so it is intended for controls whose * checked state means "all rows selected". * * @example * ```ts * const onChange = table_getToggleAllRowsSelectedHandler(table) * ``` */ function table_getToggleAllRowsSelectedHandler(table) { return (e) => { table_toggleAllRowsSelected(table, e.target.checked); }; } /** * Creates a checkbox-style handler that selects or deselects current page rows. * * The handler reads `event.target.checked`, so it is intended for controls whose * checked state means "all page rows selected". * * @example * ```ts * const onChange = table_getToggleAllPageRowsSelectedHandler(table) * ``` */ function table_getToggleAllPageRowsSelectedHandler(table) { return (e) => { table_toggleAllPageRowsSelected(table, e.target.checked); }; } /** * Selects or deselects this row. * * Omitting `value` toggles the row. Child rows are selected recursively unless * `opts.selectChildren` is `false`, sub-row selection is disabled, or the row * only supports single selection. Pass `deselectParents: true` to also remove * ancestor row ids from the selection when this row is deselected. * * @example * ```ts * row_toggleSelected(row) * row_toggleSelected(row, true) * row_toggleSelected(row, false) * row_toggleSelected(row, true, { selectChildren: false }) * row_toggleSelected(row, false, { deselectParents: true }) * ``` */ function row_toggleSelected(row, value, opts) { const isSelected = row_getIsSelected(row); table_setRowSelection(row.table, (old) => { value = typeof value !== "undefined" ? value : !isSelected; const rowSelection = Object.assign(makeObjectMap(), old); mutateRowIsSelected(rowSelection, row.id, value, (opts?.selectChildren ?? true) && row_getCanMultiSelect(row), row.table); if (!value && opts?.deselectParents) pruneAncestorRowIds(rowSelection, row); return rowSelection; }); } /** * Checks whether this row id is selected in `state.rowSelection`. * * Missing row ids are treated as not selected. * * @example * ```ts * const selected = row_getIsSelected(row) * ``` */ function row_getIsSelected(row) { return isRowSelected(row, row.table.atoms.rowSelection?.get() ?? {}); } /** * Checks whether some, but not all, selectable descendants are selected. * * This supports indeterminate selection UI for parent rows. * * @example * ```ts * const partial = row_getIsSomeSelected(row) * ``` */ function row_getIsSomeSelected(row) { return isSubRowSelected(row) === "some"; } /** * Checks whether all selectable descendants are selected. * * Rows without selectable descendants return false. * * @example * ```ts * const allChildrenSelected = row_getIsAllSubRowsSelected(row) * ``` */ function row_getIsAllSubRowsSelected(row) { return isSubRowSelected(row) === "all"; } /** * Checks whether this row can be selected. * * `options.enableRowSelection` may be a boolean or a row predicate; it defaults * to `true`. * * @example * ```ts * const canSelect = row_getCanSelect(row) * ``` */ function row_getCanSelect(row) { const options = row.table.options; if (typeof options.enableRowSelection === "function") return options.enableRowSelection(row); return options.enableRowSelection ?? true; } /** * Checks whether selecting this row should also select its subRows. * * `options.enableSubRowSelection` may be a boolean or a row predicate; it * defaults to `true`. * * @example * ```ts * const canSelectChildren = row_getCanSelectSubRows(row) * ``` */ function row_getCanSelectSubRows(row) { const options = row.table.options; if (typeof options.enableSubRowSelection === "function") return options.enableSubRowSelection(row); return options.enableSubRowSelection ?? true; } /** * Checks whether this row can be selected alongside other rows. * * `options.enableMultiRowSelection` may be a boolean or a row predicate; it * defaults to `true`. * * @example * ```ts * const canMultiSelect = row_getCanMultiSelect(row) * ``` */ function row_getCanMultiSelect(row) { const options = row.table.options; if (typeof options.enableMultiRowSelection === "function") return options.enableMultiRowSelection(row); return options.enableMultiRowSelection ?? true; } /** * Creates a checkbox-style handler that selects or deselects this row. * * The handler is a no-op when the row cannot be selected and reads * `event.target.checked`. Shift events select or deselect the inclusive range * from the most recent selectable row handled by this table. Pass * `selectChildren: false` to limit changes to rows explicitly present in the * display-order interval, and `deselectParents: true` to remove ancestor row * ids from the selection when rows are deselected. * * @example * ```ts * const onChange = row_getToggleSelectedHandler(row) * ``` */ function row_getToggleSelectedHandler(row, opts) { const canSelect = row_getCanSelect(row); return (e) => { if (!canSelect) return; const event = e; const table = row.table; const checked = event.target.checked; const anchorId = table._lastSelectedRowId; if (!(table.options.enableRowRangeSelection !== false && anchorId !== null && row_getCanMultiSelect(row) && (table.options.isRowRangeSelectionEvent?.(e) ?? false)) || !selectRowRange(row, anchorId, checked, opts)) row_toggleSelected(row, checked, opts); table._lastSelectedRowId = row.id; }; } /** * Resolves and mutates an inclusive interval in the table's latest logical * display order. * * The anchor is resolved without throwing from the pre-pagination row model, * then the core row model. Both endpoint display indexes must still identify * those rows in the current order and both endpoints must support * multi-selection. Eligible interval rows are applied through one row * selection updater; non-selectable and non-multi-selectable rows are skipped. * Returns `false` when the interaction should fall back to an ordinary toggle. */ function selectRowRange(row, anchorId, value, opts) { const includeChildren = opts?.selectChildren ?? true; const table = row.table; const rows = table.getRowsInDisplayOrder(); const anchorRow = table.getPrePaginatedRowModel().rowsById[anchorId] ?? table.getCoreRowModel().rowsById[anchorId]; if (!anchorRow) return false; const anchorIndex = anchorRow.getDisplayIndex(); const rowIndex = row.getDisplayIndex(); const anchorAtIndex = rows[anchorIndex]; const rowAtIndex = rows[rowIndex]; if (anchorIndex < 0 || rowIndex < 0 || anchorIndex >= rows.length || rowIndex >= rows.length || anchorAtIndex?.id !== anchorRow.id || rowAtIndex?.id !== row.id || !row_getCanMultiSelect(anchorRow) || !row_getCanMultiSelect(row)) return false; const start = Math.min(anchorIndex, rowIndex); const end = Math.max(anchorIndex, rowIndex); table_setRowSelection(table, (old) => { const rowSelection = Object.assign(makeObjectMap(), old); for (let index = start; index <= end; index++) { const rangeRow = rows[index]; if (!row_getCanSelect(rangeRow) || !row_getCanMultiSelect(rangeRow)) continue; mutateRowIsSelected(rowSelection, rangeRow.id, value, includeChildren, table); if (!value && opts?.deselectParents) pruneAncestorRowIds(rowSelection, rangeRow); } return rowSelection; }); return true; } function mutateRowIsSelected(rowSelection, rowId, value, includeChildren, table, respectCanSelectOnDeselect) { const row = table.getRow(rowId, true); if (value) { if (!row_getCanMultiSelect(row)) Object.keys(rowSelection).forEach((key) => delete rowSelection[key]); if (row_getCanSelect(row)) rowSelection[rowId] = true; } else if (!respectCanSelectOnDeselect || row_getCanSelect(row)) delete rowSelection[rowId]; if (includeChildren && row.subRows.length && row_getCanSelectSubRows(row)) row.subRows.forEach((r) => mutateRowIsSelected(rowSelection, r.id, value, includeChildren, table, respectCanSelectOnDeselect)); } /** * Returns whether a select-all cascade can reach this row: the row itself is * selectable and no ancestor blocks descent via `enableSubRowSelection`. * * `subtreeCache` memoizes the per-ancestor verdict for one select-all pass, so * ancestor chains shared by sibling rows are only walked (and the * `enableSubRowSelection` predicate only invoked) once per unique ancestor. */ function isRowSelectableInSelectAll(row, subtreeCache) { if (!row_getCanSelect(row)) return false; const table = row.table; if (table.options.enableSubRowSelection === true) return true; const parentId = row.parentId; if (parentId === void 0) return true; const cached = subtreeCache.get(parentId); if (cached !== void 0) return cached; const rowsById = table.getCoreRowModel().rowsById; const visited = []; let selectable = true; let currentId = parentId; while (currentId !== void 0) { const known = subtreeCache.get(currentId); if (known !== void 0) { selectable = known; break; } visited.push(currentId); const parent = rowsById[currentId] ?? table.getRow(currentId, true); if (!row_getCanSelectSubRows(parent)) { selectable = false; break; } currentId = parent.parentId; } visited.forEach((id) => subtreeCache.set(id, selectable)); return selectable; } function pruneAncestorRowIds(rowSelection, row) { const rowsById = row.table.getCoreRowModel().rowsById; let parentId = row.parentId; while (parentId !== void 0) { delete rowSelection[parentId]; parentId = (rowsById[parentId] ?? row.table.getRow(parentId, true)).parentId; } } function selectRowsRecursively(rows, rowSelection, selectedFlatRows, selectedRowsById) { const result = []; for (let i = 0; i < rows.length; i++) { const row = rows[i]; const isSelected = isRowSelected(row, rowSelection); if (isSelected) { selectedFlatRows.push(row); selectedRowsById[row.id] = row; } if (row.subRows.length) { const newSubRows = selectRowsRecursively(row.subRows, rowSelection, selectedFlatRows, selectedRowsById); if (isSelected) { const cloned = Object.create(Object.getPrototypeOf(row)); copyInstancePropertiesWithoutMemos(cloned, row); cloned.subRows = newSubRows; result.push(cloned); } } else if (isSelected) result.push(row); } return result; } /** * Builds a row model containing rows selected by the current row selection state. * * The result is derived from the supplied row model, so selected ids absent from * that model are not materialized as rows. * * @example * ```ts * const selectedRows = selectRowsFn(rowModel) * ``` */ function selectRowsFn(rowModel, table) { const newSelectedFlatRows = []; const newSelectedRowsById = makeObjectMap(); const rowSelection = table.atoms.rowSelection?.get() ?? {}; return { rows: selectRowsRecursively(rowModel.rows, rowSelection, newSelectedFlatRows, newSelectedRowsById), flatRows: newSelectedFlatRows, rowsById: newSelectedRowsById }; } /** * Returns whether a row id is selected in the current row selection state. * * @example * ```ts * const selected = isRowSelected(row) * ``` */ function isRowSelected(row, rowSelection) { return !!(hasOwn(rowSelection, row.id) && rowSelection[row.id]); } /** * Returns whether all, some, or none of a row's selectable descendants are selected. * * The result is used to drive indeterminate row selection UI. * * @example * ```ts * const selectedState = isSubRowSelected(row) * ``` */ function isSubRowSelected(row) { if (!row.subRows.length) return false; const rowSelection = row.table.atoms.rowSelection?.get() ?? {}; let someSelected = false; let allChildrenSelected = true; let someSelectable = false; for (let i = 0; i < row.subRows.length; i++) { const subRow = row.subRows[i]; if (someSelected && !allChildrenSelected) break; if (row_getCanSelect(subRow)) { someSelectable = true; if (isRowSelected(subRow, rowSelection)) someSelected = true; else allChildrenSelected = false; } if (subRow.subRows.length) { const subRowChildrenSelected = isSubRowSelected(subRow); if (subRowChildrenSelected === "all") { someSelected = true; someSelectable = true; } else if (subRowChildrenSelected === "some") { someSelected = true; allChildrenSelected = false; someSelectable = true; } else allChildrenSelected = false; } } if (!someSelectable) return false; return allChildrenSelected ? "all" : someSelected ? "some" : false; } //#endregion export { getDefaultRowSelectionState, isRowSelected, isSubRowSelected, row_getCanMultiSelect, row_getCanSelect, row_getCanSelectSubRows, row_getIsAllSubRowsSelected, row_getIsSelected, row_getIsSomeSelected, row_getToggleSelectedHandler, row_toggleSelected, selectRowsFn, table_getFilteredSelectedRowModel, table_getGroupedSelectedRowModel, table_getIsAllPageRowsSelected, table_getIsAllRowsSelected, table_getIsSomePageRowsSelected, table_getIsSomeRowsSelected, table_getPreSelectedRowModel, table_getSelectedRowIds, table_getSelectedRowModel, table_getToggleAllPageRowsSelectedHandler, table_getToggleAllRowsSelectedHandler, table_resetRowSelection, table_setRowSelection, table_toggleAllPageRowsSelected, table_toggleAllRowsSelected };