UNPKG

@tanstack/table-core

Version:

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

580 lines (525 loc) 20.7 kB
import { _getVisibleLeafColumns } from '..' import { RowData, Column, Header, OnChangeFn, Table, Updater, TableFeature, } from '../types' import { getMemoOptions, makeStateUpdater, memo } from '../utils' import { ColumnPinningPosition } from './ColumnPinning' import { safelyAccessDocument } from '../utils/document' // export interface ColumnSizingTableState { columnSizing: ColumnSizingState columnSizingInfo: ColumnSizingInfoState } export type ColumnSizingState = Record<string, number> export interface ColumnSizingInfoState { columnSizingStart: [string, number][] deltaOffset: null | number deltaPercentage: null | number isResizingColumn: false | string startOffset: null | number startSize: null | number } export type ColumnResizeMode = 'onChange' | 'onEnd' export type ColumnResizeDirection = 'ltr' | 'rtl' export interface ColumnSizingOptions { /** * Determines when the columnSizing state is updated. `onChange` updates the state when the user is dragging the resize handle. `onEnd` updates the state when the user releases the resize handle. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#columnresizemode) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ columnResizeMode?: ColumnResizeMode /** * Enables or disables column resizing for the column. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#enablecolumnresizing) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ enableColumnResizing?: boolean /** * Enables or disables right-to-left support for resizing the column. defaults to 'ltr'. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#columnResizeDirection) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ columnResizeDirection?: ColumnResizeDirection /** * If provided, this function will be called with an `updaterFn` when `state.columnSizing` changes. This overrides the default internal state management, so you will also need to supply `state.columnSizing` from your own managed state. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#oncolumnsizingchange) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ onColumnSizingChange?: OnChangeFn<ColumnSizingState> /** * If provided, this function will be called with an `updaterFn` when `state.columnSizingInfo` changes. This overrides the default internal state management, so you will also need to supply `state.columnSizingInfo` from your own managed state. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#oncolumnsizinginfochange) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ onColumnSizingInfoChange?: OnChangeFn<ColumnSizingInfoState> } export type ColumnSizingDefaultOptions = Pick< ColumnSizingOptions, | 'columnResizeMode' | 'onColumnSizingChange' | 'onColumnSizingInfoChange' | 'columnResizeDirection' > export interface ColumnSizingInstance { /** * If pinning, returns the total size of the center portion of the table by calculating the sum of the sizes of all unpinned/center leaf-columns. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#getcentertotalsize) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ getCenterTotalSize: () => number /** * Returns the total size of the left portion of the table by calculating the sum of the sizes of all left leaf-columns. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#getlefttotalsize) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ getLeftTotalSize: () => number /** * Returns the total size of the right portion of the table by calculating the sum of the sizes of all right leaf-columns. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#getrighttotalsize) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ getRightTotalSize: () => number /** * Returns the total size of the table by calculating the sum of the sizes of all leaf-columns. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#gettotalsize) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ getTotalSize: () => number /** * Resets column sizing to its initial state. If `defaultState` is `true`, the default state for the table will be used instead of the initialValue provided to the table. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#resetcolumnsizing) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ resetColumnSizing: (defaultState?: boolean) => void /** * Resets column sizing info to its initial state. If `defaultState` is `true`, the default state for the table will be used instead of the initialValue provided to the table. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#resetheadersizeinfo) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ resetHeaderSizeInfo: (defaultState?: boolean) => void /** * Sets the column sizing state using an updater function or a value. This will trigger the underlying `onColumnSizingChange` function if one is passed to the table options, otherwise the state will be managed automatically by the table. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#setcolumnsizing) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ setColumnSizing: (updater: Updater<ColumnSizingState>) => void /** * Sets the column sizing info state using an updater function or a value. This will trigger the underlying `onColumnSizingInfoChange` function if one is passed to the table options, otherwise the state will be managed automatically by the table. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#setcolumnsizinginfo) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ setColumnSizingInfo: (updater: Updater<ColumnSizingInfoState>) => void } export interface ColumnSizingColumnDef { /** * Enables or disables column resizing for the column. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#enableresizing) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ enableResizing?: boolean /** * The maximum allowed size for the column * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#maxsize) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ maxSize?: number /** * The minimum allowed size for the column * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#minsize) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ minSize?: number /** * The desired size for the column * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#size) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ size?: number } export interface ColumnSizingColumn { /** * Returns `true` if the column can be resized. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#getcanresize) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ getCanResize: () => boolean /** * Returns `true` if the column is currently being resized. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#getisresizing) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ getIsResizing: () => boolean /** * Returns the current size of the column. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#getsize) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ getSize: () => number /** * Returns the offset measurement along the row-axis (usually the x-axis for standard tables) for the header. This is effectively a sum of the offset measurements of all preceding (left) headers in relation to the current column. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#getstart) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ getStart: (position?: ColumnPinningPosition | 'center') => number /** * Returns the offset measurement along the row-axis (usually the x-axis for standard tables) for the header. This is effectively a sum of the offset measurements of all succeeding (right) headers in relation to the current column. */ getAfter: (position?: ColumnPinningPosition | 'center') => number /** * Resets the column to its initial size. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#resetsize) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ resetSize: () => void } export interface ColumnSizingHeader { /** * Returns an event handler function that can be used to resize the header. It can be used as an: * - `onMouseDown` handler * - `onTouchStart` handler * * The dragging and release events are automatically handled for you. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#getresizehandler) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ getResizeHandler: (context?: Document) => (event: unknown) => void /** * Returns the current size of the header. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#getsize) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ getSize: () => number /** * Returns the offset measurement along the row-axis (usually the x-axis for standard tables) for the header. This is effectively a sum of the offset measurements of all preceding headers. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-sizing#getstart) * @link [Guide](https://tanstack.com/table/v8/docs/guide/column-sizing) */ getStart: (position?: ColumnPinningPosition) => number } // export const defaultColumnSizing = { size: 150, minSize: 20, maxSize: Number.MAX_SAFE_INTEGER, } const getDefaultColumnSizingInfoState = (): ColumnSizingInfoState => ({ startOffset: null, startSize: null, deltaOffset: null, deltaPercentage: null, isResizingColumn: false, columnSizingStart: [], }) export const ColumnSizing: TableFeature = { getDefaultColumnDef: (): ColumnSizingColumnDef => { return defaultColumnSizing }, getInitialState: (state): ColumnSizingTableState => { return { columnSizing: {}, columnSizingInfo: getDefaultColumnSizingInfoState(), ...state, } }, getDefaultOptions: <TData extends RowData>( table: Table<TData> ): ColumnSizingDefaultOptions => { return { columnResizeMode: 'onEnd', columnResizeDirection: 'ltr', onColumnSizingChange: makeStateUpdater('columnSizing', table), onColumnSizingInfoChange: makeStateUpdater('columnSizingInfo', table), } }, createColumn: <TData extends RowData, TValue>( column: Column<TData, TValue>, table: Table<TData> ): void => { column.getSize = () => { const columnSize = table.getState().columnSizing[column.id] return Math.min( Math.max( column.columnDef.minSize ?? defaultColumnSizing.minSize, columnSize ?? column.columnDef.size ?? defaultColumnSizing.size ), column.columnDef.maxSize ?? defaultColumnSizing.maxSize ) } column.getStart = memo( position => [ position, _getVisibleLeafColumns(table, position), table.getState().columnSizing, ], (position, columns) => columns .slice(0, column.getIndex(position)) .reduce((sum, column) => sum + column.getSize(), 0), getMemoOptions(table.options, 'debugColumns', 'getStart') ) column.getAfter = memo( position => [ position, _getVisibleLeafColumns(table, position), table.getState().columnSizing, ], (position, columns) => columns .slice(column.getIndex(position) + 1) .reduce((sum, column) => sum + column.getSize(), 0), getMemoOptions(table.options, 'debugColumns', 'getAfter') ) column.resetSize = () => { table.setColumnSizing(({ [column.id]: _, ...rest }) => { return rest }) } column.getCanResize = () => { return ( (column.columnDef.enableResizing ?? true) && (table.options.enableColumnResizing ?? true) ) } column.getIsResizing = () => { return table.getState().columnSizingInfo.isResizingColumn === column.id } }, createHeader: <TData extends RowData, TValue>( header: Header<TData, TValue>, table: Table<TData> ): void => { header.getSize = () => { let sum = 0 const recurse = (header: Header<TData, TValue>) => { if (header.subHeaders.length) { header.subHeaders.forEach(recurse) } else { sum += header.column.getSize() ?? 0 } } recurse(header) return sum } header.getStart = () => { if (header.index > 0) { const prevSiblingHeader = header.headerGroup.headers[header.index - 1]! return prevSiblingHeader.getStart() + prevSiblingHeader.getSize() } return 0 } header.getResizeHandler = _contextDocument => { const column = table.getColumn(header.column.id) const canResize = column?.getCanResize() return (e: unknown) => { if (!column || !canResize) { return } ;(e as any).persist?.() if (isTouchStartEvent(e)) { // lets not respond to multiple touches (e.g. 2 or 3 fingers) if (e.touches && e.touches.length > 1) { return } } const startSize = header.getSize() const columnSizingStart: [string, number][] = header ? header.getLeafHeaders().map(d => [d.column.id, d.column.getSize()]) : [[column.id, column.getSize()]] const clientX = isTouchStartEvent(e) ? Math.round(e.touches[0]!.clientX) : (e as MouseEvent).clientX const newColumnSizing: ColumnSizingState = {} const updateOffset = ( eventType: 'move' | 'end', clientXPos?: number ) => { if (typeof clientXPos !== 'number') { return } table.setColumnSizingInfo(old => { const deltaDirection = table.options.columnResizeDirection === 'rtl' ? -1 : 1 const deltaOffset = (clientXPos - (old?.startOffset ?? 0)) * deltaDirection const deltaPercentage = Math.max( deltaOffset / (old?.startSize ?? 0), -0.999999 ) old.columnSizingStart.forEach(([columnId, headerSize]) => { newColumnSizing[columnId] = Math.round( Math.max(headerSize + headerSize * deltaPercentage, 0) * 100 ) / 100 }) return { ...old, deltaOffset, deltaPercentage, } }) if ( table.options.columnResizeMode === 'onChange' || eventType === 'end' ) { table.setColumnSizing(old => ({ ...old, ...newColumnSizing, })) } } const onMove = (clientXPos?: number) => updateOffset('move', clientXPos) const onEnd = (clientXPos?: number) => { updateOffset('end', clientXPos) table.setColumnSizingInfo(old => ({ ...old, isResizingColumn: false, startOffset: null, startSize: null, deltaOffset: null, deltaPercentage: null, columnSizingStart: [], })) } const contextDocument = safelyAccessDocument(_contextDocument) const mouseEvents = { moveHandler: (e: MouseEvent) => onMove(e.clientX), upHandler: (e: MouseEvent) => { contextDocument?.removeEventListener( 'mousemove', mouseEvents.moveHandler ) contextDocument?.removeEventListener( 'mouseup', mouseEvents.upHandler ) onEnd(e.clientX) }, } const touchEvents = { moveHandler: (e: TouchEvent) => { if (e.cancelable) { e.preventDefault() e.stopPropagation() } onMove(e.touches[0]!.clientX) return false }, upHandler: (e: TouchEvent) => { contextDocument?.removeEventListener( 'touchmove', touchEvents.moveHandler ) contextDocument?.removeEventListener( 'touchend', touchEvents.upHandler ) if (e.cancelable) { e.preventDefault() e.stopPropagation() } onEnd(e.touches[0]?.clientX) }, } const passiveIfSupported = passiveEventSupported() ? { passive: false } : false if (isTouchStartEvent(e)) { contextDocument?.addEventListener( 'touchmove', touchEvents.moveHandler, passiveIfSupported ) contextDocument?.addEventListener( 'touchend', touchEvents.upHandler, passiveIfSupported ) } else { contextDocument?.addEventListener( 'mousemove', mouseEvents.moveHandler, passiveIfSupported ) contextDocument?.addEventListener( 'mouseup', mouseEvents.upHandler, passiveIfSupported ) } table.setColumnSizingInfo(old => ({ ...old, startOffset: clientX, startSize, deltaOffset: 0, deltaPercentage: 0, columnSizingStart, isResizingColumn: column.id, })) } } }, createTable: <TData extends RowData>(table: Table<TData>): void => { table.setColumnSizing = updater => table.options.onColumnSizingChange?.(updater) table.setColumnSizingInfo = updater => table.options.onColumnSizingInfoChange?.(updater) table.resetColumnSizing = defaultState => { table.setColumnSizing( defaultState ? {} : table.initialState.columnSizing ?? {} ) } table.resetHeaderSizeInfo = defaultState => { table.setColumnSizingInfo( defaultState ? getDefaultColumnSizingInfoState() : table.initialState.columnSizingInfo ?? getDefaultColumnSizingInfoState() ) } table.getTotalSize = () => table.getHeaderGroups()[0]?.headers.reduce((sum, header) => { return sum + header.getSize() }, 0) ?? 0 table.getLeftTotalSize = () => table.getLeftHeaderGroups()[0]?.headers.reduce((sum, header) => { return sum + header.getSize() }, 0) ?? 0 table.getCenterTotalSize = () => table.getCenterHeaderGroups()[0]?.headers.reduce((sum, header) => { return sum + header.getSize() }, 0) ?? 0 table.getRightTotalSize = () => table.getRightHeaderGroups()[0]?.headers.reduce((sum, header) => { return sum + header.getSize() }, 0) ?? 0 }, } let passiveSupported: boolean | null = null export function passiveEventSupported() { if (typeof passiveSupported === 'boolean') return passiveSupported let supported = false try { const options = { get passive() { supported = true return false }, } const noop = () => {} window.addEventListener('test', noop, options) window.removeEventListener('test', noop) } catch (err) { supported = false } passiveSupported = supported return passiveSupported } function isTouchStartEvent(e: unknown): e is TouchEvent { return (e as TouchEvent).type === 'touchstart' }