import { TableFeature } from '../core/table' import { OnChangeFn, Table, Row, RowModel, Updater, RowData } from '../types' import { makeStateUpdater, memo } from '../utils' export type RowSelectionState = Record export interface RowSelectionTableState { rowSelection: RowSelectionState } export interface RowSelectionOptions { enableRowSelection?: boolean | ((row: Row) => boolean) enableMultiRowSelection?: boolean | ((row: Row) => boolean) enableSubRowSelection?: boolean | ((row: Row) => boolean) onRowSelectionChange?: OnChangeFn // enableGroupingRowSelection?: // | boolean // | (( // row: Row // ) => boolean) // isAdditiveSelectEvent?: (e: unknown) => boolean // isInclusiveSelectEvent?: (e: unknown) => boolean // selectRowsFn?: ( // table: Table, // rowModel: RowModel // ) => RowModel } export interface RowSelectionRow { getIsSelected: () => boolean getIsSomeSelected: () => boolean getIsAllSubRowsSelected: () => boolean getCanSelect: () => boolean getCanMultiSelect: () => boolean getCanSelectSubRows: () => boolean toggleSelected: (value?: boolean) => void getToggleSelectedHandler: () => (event: unknown) => void } export interface RowSelectionInstance { getToggleAllRowsSelectedHandler: () => (event: unknown) => void getToggleAllPageRowsSelectedHandler: () => (event: unknown) => void setRowSelection: (updater: Updater) => void resetRowSelection: (defaultState?: boolean) => void getIsAllRowsSelected: () => boolean getIsAllPageRowsSelected: () => boolean getIsSomeRowsSelected: () => boolean getIsSomePageRowsSelected: () => boolean toggleAllRowsSelected: (value?: boolean) => void toggleAllPageRowsSelected: (value?: boolean) => void getPreSelectedRowModel: () => RowModel getSelectedRowModel: () => RowModel getFilteredSelectedRowModel: () => RowModel getGroupedSelectedRowModel: () => RowModel } // export const RowSelection: TableFeature = { getInitialState: (state): RowSelectionTableState => { return { rowSelection: {}, ...state, } }, getDefaultOptions: ( table: Table ): RowSelectionOptions => { return { onRowSelectionChange: makeStateUpdater('rowSelection', table), enableRowSelection: true, enableMultiRowSelection: true, enableSubRowSelection: true, // enableGroupingRowSelection: false, // isAdditiveSelectEvent: (e: unknown) => !!e.metaKey, // isInclusiveSelectEvent: (e: unknown) => !!e.shiftKey, } }, createTable: ( table: Table ): RowSelectionInstance => { return { setRowSelection: updater => table.options.onRowSelectionChange?.(updater), resetRowSelection: defaultState => table.setRowSelection( defaultState ? {} : table.initialState.rowSelection ?? {} ), toggleAllRowsSelected: value => { table.setRowSelection(old => { value = typeof value !== 'undefined' ? value : !table.getIsAllRowsSelected() const rowSelection = { ...old } const preGroupedFlatRows = table.getPreGroupedRowModel().flatRows // We don't use `mutateRowIsSelected` here for performance reasons. // All of the rows are flat already, so it wouldn't be worth it if (value) { preGroupedFlatRows.forEach(row => { if (!row.getCanSelect()) { return } rowSelection[row.id] = true }) } else { preGroupedFlatRows.forEach(row => { delete rowSelection[row.id] }) } return rowSelection }) }, toggleAllPageRowsSelected: value => table.setRowSelection(old => { const resolvedValue = typeof value !== 'undefined' ? value : !table.getIsAllPageRowsSelected() const rowSelection: RowSelectionState = { ...old } table.getRowModel().rows.forEach(row => { mutateRowIsSelected(rowSelection, row.id, resolvedValue, table) }) return rowSelection }), // addRowSelectionRange: rowId => { // const { // rows, // rowsById, // options: { selectGroupingRows, selectSubRows }, // } = table // const findSelectedRow = (rows: Row[]) => { // let found // rows.find(d => { // if (d.getIsSelected()) { // found = d // return true // } // const subFound = findSelectedRow(d.subRows || []) // if (subFound) { // found = subFound // return true // } // return false // }) // return found // } // const firstRow = findSelectedRow(rows) || rows[0] // const lastRow = rowsById[rowId] // let include = false // const selectedRowIds = {} // const addRow = (row: Row) => { // mutateRowIsSelected(selectedRowIds, row.id, true, { // rowsById, // selectGroupingRows: selectGroupingRows!, // selectSubRows: selectSubRows!, // }) // } // table.rows.forEach(row => { // const isFirstRow = row.id === firstRow.id // const isLastRow = row.id === lastRow.id // if (isFirstRow || isLastRow) { // if (!include) { // include = true // } else if (include) { // addRow(row) // include = false // } // } // if (include) { // addRow(row) // } // }) // table.setRowSelection(selectedRowIds) // }, getPreSelectedRowModel: () => table.getCoreRowModel(), getSelectedRowModel: memo( () => [table.getState().rowSelection, table.getCoreRowModel()], (rowSelection, rowModel) => { if (!Object.keys(rowSelection).length) { return { rows: [], flatRows: [], rowsById: {}, } } return selectRowsFn(table, rowModel) }, { key: process.env.NODE_ENV === 'development' && 'getSelectedRowModel', debug: () => table.options.debugAll ?? table.options.debugTable, } ), getFilteredSelectedRowModel: memo( () => [table.getState().rowSelection, table.getFilteredRowModel()], (rowSelection, rowModel) => { if (!Object.keys(rowSelection).length) { return { rows: [], flatRows: [], rowsById: {}, } } return selectRowsFn(table, rowModel) }, { key: process.env.NODE_ENV === 'production' && 'getFilteredSelectedRowModel', debug: () => table.options.debugAll ?? table.options.debugTable, } ), getGroupedSelectedRowModel: memo( () => [table.getState().rowSelection, table.getSortedRowModel()], (rowSelection, rowModel) => { if (!Object.keys(rowSelection).length) { return { rows: [], flatRows: [], rowsById: {}, } } return selectRowsFn(table, rowModel) }, { key: process.env.NODE_ENV === 'production' && 'getGroupedSelectedRowModel', debug: () => table.options.debugAll ?? table.options.debugTable, } ), /// // getGroupingRowCanSelect: rowId => { // const row = table.getRow(rowId) // if (!row) { // throw new Error() // } // if (typeof table.options.enableGroupingRowSelection === 'function') { // return table.options.enableGroupingRowSelection(row) // } // return table.options.enableGroupingRowSelection ?? false // }, getIsAllRowsSelected: () => { const preGroupedFlatRows = table.getFilteredRowModel().flatRows const { rowSelection } = table.getState() let isAllRowsSelected = Boolean( preGroupedFlatRows.length && Object.keys(rowSelection).length ) if (isAllRowsSelected) { if ( preGroupedFlatRows.some( row => row.getCanSelect() && !rowSelection[row.id] ) ) { isAllRowsSelected = false } } return isAllRowsSelected }, getIsAllPageRowsSelected: () => { const paginationFlatRows = table.getPaginationRowModel().flatRows const { rowSelection } = table.getState() let isAllPageRowsSelected = !!paginationFlatRows.length if ( isAllPageRowsSelected && paginationFlatRows.some( row => row.getCanSelect() && !rowSelection[row.id] ) ) { isAllPageRowsSelected = false } return isAllPageRowsSelected }, getIsSomeRowsSelected: () => { const totalSelected = Object.keys( table.getState().rowSelection ?? {} ).length return ( totalSelected > 0 && totalSelected < table.getFilteredRowModel().flatRows.length ) }, getIsSomePageRowsSelected: () => { const paginationFlatRows = table.getPaginationRowModel().flatRows return table.getIsAllPageRowsSelected() ? false : paginationFlatRows.some( d => d.getIsSelected() || d.getIsSomeSelected() ) }, getToggleAllRowsSelectedHandler: () => { return (e: unknown) => { table.toggleAllRowsSelected( ((e as MouseEvent).target as HTMLInputElement).checked ) } }, getToggleAllPageRowsSelectedHandler: () => { return (e: unknown) => { table.toggleAllPageRowsSelected( ((e as MouseEvent).target as HTMLInputElement).checked ) } }, } }, createRow: ( row: Row, table: Table ): RowSelectionRow => { return { toggleSelected: value => { const isSelected = row.getIsSelected() table.setRowSelection(old => { value = typeof value !== 'undefined' ? value : !isSelected if (isSelected === value) { return old } const selectedRowIds = { ...old } mutateRowIsSelected(selectedRowIds, row.id, value, table) return selectedRowIds }) }, getIsSelected: () => { const { rowSelection } = table.getState() return isRowSelected(row, rowSelection) }, getIsSomeSelected: () => { const { rowSelection } = table.getState() return isSubRowSelected(row, rowSelection, table) === 'some' }, getIsAllSubRowsSelected: () => { const { rowSelection } = table.getState() return isSubRowSelected(row, rowSelection, table) === 'all' }, getCanSelect: () => { if (typeof table.options.enableRowSelection === 'function') { return table.options.enableRowSelection(row) } return table.options.enableRowSelection ?? true }, getCanSelectSubRows: () => { if (typeof table.options.enableSubRowSelection === 'function') { return table.options.enableSubRowSelection(row) } return table.options.enableSubRowSelection ?? true }, getCanMultiSelect: () => { if (typeof table.options.enableMultiRowSelection === 'function') { return table.options.enableMultiRowSelection(row) } return table.options.enableMultiRowSelection ?? true }, getToggleSelectedHandler: () => { const canSelect = row.getCanSelect() return (e: unknown) => { if (!canSelect) return row.toggleSelected( ((e as MouseEvent).target as HTMLInputElement)?.checked ) } }, } }, } const mutateRowIsSelected = ( selectedRowIds: Record, id: string, value: boolean, table: Table ) => { const row = table.getRow(id) // const isGrouped = row.getIsGrouped() // if ( // TODO: enforce grouping row selection rules // !isGrouped || // (isGrouped && table.options.enableGroupingRowSelection) // ) { if (value) { if (!row.getCanMultiSelect()) { Object.keys(selectedRowIds).forEach(key => delete selectedRowIds[key]) } if (row.getCanSelect()) { selectedRowIds[id] = true } } else { delete selectedRowIds[id] } // } if (row.subRows?.length && row.getCanSelectSubRows()) { row.subRows.forEach(row => mutateRowIsSelected(selectedRowIds, row.id, value, table) ) } } export function selectRowsFn( table: Table, rowModel: RowModel ): RowModel { const rowSelection = table.getState().rowSelection const newSelectedFlatRows: Row[] = [] const newSelectedRowsById: Record> = {} // Filters top level and nested rows const recurseRows = (rows: Row[], depth = 0): Row[] => { return rows .map(row => { const isSelected = isRowSelected(row, rowSelection) if (isSelected) { newSelectedFlatRows.push(row) newSelectedRowsById[row.id] = row } if (row.subRows?.length) { row = { ...row, subRows: recurseRows(row.subRows, depth + 1), } } if (isSelected) { return row } }) .filter(Boolean) as Row[] } return { rows: recurseRows(rowModel.rows), flatRows: newSelectedFlatRows, rowsById: newSelectedRowsById, } } export function isRowSelected( row: Row, selection: Record ): boolean { return selection[row.id] ?? false } export function isSubRowSelected( row: Row, selection: Record, table: Table ): boolean | 'some' | 'all' { if (row.subRows && row.subRows.length) { let allChildrenSelected = true let someSelected = false row.subRows.forEach(subRow => { // Bail out early if we know both of these if (someSelected && !allChildrenSelected) { return } if (isRowSelected(subRow, selection)) { someSelected = true } else { allChildrenSelected = false } }) return allChildrenSelected ? 'all' : someSelected ? 'some' : false } return false }