@justjarethb/table-core
Version:
Fork of @tanstack/table-core with support for event listeners in different windows
418 lines (364 loc) • 12.1 kB
text/typescript
import { TableFeature } from '../core/table'
import { RowData, Column, Header, OnChangeFn, Table, Updater } from '../types'
import { makeStateUpdater } from '../utils'
import { ColumnPinningPosition } from './Pinning'
//
export interface ColumnSizingTableState {
columnSizing: ColumnSizingState
columnSizingInfo: ColumnSizingInfoState
}
export type ColumnSizingState = Record<string, number>
export interface ColumnSizingInfoState {
startOffset: null | number
startSize: null | number
deltaOffset: null | number
deltaPercentage: null | number
isResizingColumn: false | string
columnSizingStart: [string, number][]
}
export type ColumnResizeMode = 'onChange' | 'onEnd'
export interface ColumnSizingOptions {
enableColumnResizing?: boolean
columnResizeMode?: ColumnResizeMode
onColumnSizingChange?: OnChangeFn<ColumnSizingState>
onColumnSizingInfoChange?: OnChangeFn<ColumnSizingInfoState>
}
export interface ColumnSizingDefaultOptions {
columnResizeMode: ColumnResizeMode
onColumnSizingChange: OnChangeFn<ColumnSizingState>
onColumnSizingInfoChange: OnChangeFn<ColumnSizingInfoState>
}
export interface ColumnSizingInstance {
setColumnSizing: (updater: Updater<ColumnSizingState>) => void
setColumnSizingInfo: (updater: Updater<ColumnSizingInfoState>) => void
resetColumnSizing: (defaultState?: boolean) => void
resetHeaderSizeInfo: (defaultState?: boolean) => void
getTotalSize: () => number
getLeftTotalSize: () => number
getCenterTotalSize: () => number
getRightTotalSize: () => number
}
export interface ColumnSizingColumnDef {
enableResizing?: boolean
size?: number
minSize?: number
maxSize?: number
}
export interface ColumnSizingColumn {
getSize: () => number
getStart: (position?: ColumnPinningPosition) => number
getCanResize: () => boolean
getIsResizing: () => boolean
resetSize: () => void
}
export interface ColumnSizingHeader {
getSize: () => number
getStart: (position?: ColumnPinningPosition) => number
getResizeHandler: (context?: Document) => (event: unknown) => void
}
//
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',
onColumnSizingChange: makeStateUpdater('columnSizing', table),
onColumnSizingInfoChange: makeStateUpdater('columnSizingInfo', table),
}
},
createColumn: <TData extends RowData, TValue>(
column: Column<TData, TValue>,
table: Table<TData>
): ColumnSizingColumn => {
return {
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
)
},
getStart: position => {
const columns = !position
? table.getVisibleLeafColumns()
: position === 'left'
? table.getLeftVisibleLeafColumns()
: table.getRightVisibleLeafColumns()
const index = columns.findIndex(d => d.id === column.id)
if (index > 0) {
const prevSiblingColumn = columns[index - 1]!
return (
prevSiblingColumn.getStart(position) + prevSiblingColumn.getSize()
)
}
return 0
},
resetSize: () => {
table.setColumnSizing(({ [column.id]: _, ...rest }) => {
return rest
})
},
getCanResize: () => {
return (
(column.columnDef.enableResizing ?? true) &&
(table.options.enableColumnResizing ?? true)
)
},
getIsResizing: () => {
return table.getState().columnSizingInfo.isResizingColumn === column.id
},
}
},
createHeader: <TData extends RowData, TValue>(
header: Header<TData, TValue>,
table: Table<TData>
): ColumnSizingHeader => {
return {
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
},
getStart: () => {
if (header.index > 0) {
const prevSiblingHeader =
header.headerGroup.headers[header.index - 1]!
return prevSiblingHeader.getStart() + prevSiblingHeader.getSize()
}
return 0
},
getResizeHandler: (contextDocument = document) => {
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 deltaOffset = clientXPos - (old?.startOffset ?? 0)
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 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>
): ColumnSizingInstance => {
return {
setColumnSizing: updater => table.options.onColumnSizingChange?.(updater),
setColumnSizingInfo: updater =>
table.options.onColumnSizingInfoChange?.(updater),
resetColumnSizing: defaultState => {
table.setColumnSizing(
defaultState ? {} : table.initialState.columnSizing ?? {}
)
},
resetHeaderSizeInfo: defaultState => {
table.setColumnSizingInfo(
defaultState
? getDefaultColumnSizingInfoState()
: table.initialState.columnSizingInfo ??
getDefaultColumnSizingInfoState()
)
},
getTotalSize: () =>
table.getHeaderGroups()[0]?.headers.reduce((sum, header) => {
return sum + header.getSize()
}, 0) ?? 0,
getLeftTotalSize: () =>
table.getLeftHeaderGroups()[0]?.headers.reduce((sum, header) => {
return sum + header.getSize()
}, 0) ?? 0,
getCenterTotalSize: () =>
table.getCenterHeaderGroups()[0]?.headers.reduce((sum, header) => {
return sum + header.getSize()
}, 0) ?? 0,
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'
}