react-table
Version:
Hooks for building lightweight, fast and extendable datagrids for React
297 lines (259 loc) • 7.19 kB
JavaScript
import React from 'react'
import {
actions,
defaultColumn,
makePropGetter,
useGetLatest,
ensurePluginOrder,
useMountedLayoutEffect,
} from '../publicUtils'
import { getFirstDefined, passiveEventSupported } from '../utils'
// Default Column
defaultColumn.canResize = true
// Actions
actions.columnStartResizing = 'columnStartResizing'
actions.columnResizing = 'columnResizing'
actions.columnDoneResizing = 'columnDoneResizing'
actions.resetResize = 'resetResize'
export const useResizeColumns = hooks => {
hooks.getResizerProps = [defaultGetResizerProps]
hooks.getHeaderProps.push({
style: {
position: 'relative',
},
})
hooks.stateReducers.push(reducer)
hooks.useInstance.push(useInstance)
hooks.useInstanceBeforeDimensions.push(useInstanceBeforeDimensions)
}
const defaultGetResizerProps = (props, { instance, header }) => {
const { dispatch } = instance
const onResizeStart = (e, header) => {
let isTouchEvent = false
if (e.type === 'touchstart') {
// lets not respond to multiple touches (e.g. 2 or 3 fingers)
if (e.touches && e.touches.length > 1) {
return
}
isTouchEvent = true
}
const headersToResize = getLeafHeaders(header)
const headerIdWidths = headersToResize.map(d => [d.id, d.totalWidth])
const clientX = isTouchEvent ? Math.round(e.touches[0].clientX) : e.clientX
let raf
let mostRecentClientX
const dispatchEnd = () => {
window.cancelAnimationFrame(raf)
raf = null
dispatch({ type: actions.columnDoneResizing })
}
const dispatchMove = () => {
window.cancelAnimationFrame(raf)
raf = null
dispatch({ type: actions.columnResizing, clientX: mostRecentClientX })
}
const scheduleDispatchMoveOnNextAnimationFrame = clientXPos => {
mostRecentClientX = clientXPos
if (!raf) {
raf = window.requestAnimationFrame(dispatchMove)
}
}
const handlersAndEvents = {
mouse: {
moveEvent: 'mousemove',
moveHandler: e => scheduleDispatchMoveOnNextAnimationFrame(e.clientX),
upEvent: 'mouseup',
upHandler: e => {
document.removeEventListener(
'mousemove',
handlersAndEvents.mouse.moveHandler
)
document.removeEventListener(
'mouseup',
handlersAndEvents.mouse.upHandler
)
dispatchEnd()
},
},
touch: {
moveEvent: 'touchmove',
moveHandler: e => {
if (e.cancelable) {
e.preventDefault()
e.stopPropagation()
}
scheduleDispatchMoveOnNextAnimationFrame(e.touches[0].clientX)
return false
},
upEvent: 'touchend',
upHandler: e => {
document.removeEventListener(
handlersAndEvents.touch.moveEvent,
handlersAndEvents.touch.moveHandler
)
document.removeEventListener(
handlersAndEvents.touch.upEvent,
handlersAndEvents.touch.moveHandler
)
dispatchEnd()
},
},
}
const events = isTouchEvent
? handlersAndEvents.touch
: handlersAndEvents.mouse
const passiveIfSupported = passiveEventSupported()
? { passive: false }
: false
document.addEventListener(
events.moveEvent,
events.moveHandler,
passiveIfSupported
)
document.addEventListener(
events.upEvent,
events.upHandler,
passiveIfSupported
)
dispatch({
type: actions.columnStartResizing,
columnId: header.id,
columnWidth: header.totalWidth,
headerIdWidths,
clientX,
})
}
return [
props,
{
onMouseDown: e => e.persist() || onResizeStart(e, header),
onTouchStart: e => e.persist() || onResizeStart(e, header),
style: {
cursor: 'col-resize',
},
draggable: false,
role: 'separator',
},
]
}
useResizeColumns.pluginName = 'useResizeColumns'
function reducer(state, action) {
if (action.type === actions.init) {
return {
columnResizing: {
columnWidths: {},
},
...state,
}
}
if (action.type === actions.resetResize) {
return {
...state,
columnResizing: {
columnWidths: {},
},
}
}
if (action.type === actions.columnStartResizing) {
const { clientX, columnId, columnWidth, headerIdWidths } = action
return {
...state,
columnResizing: {
...state.columnResizing,
startX: clientX,
headerIdWidths,
columnWidth,
isResizingColumn: columnId,
},
}
}
if (action.type === actions.columnResizing) {
const { clientX } = action
const { startX, columnWidth, headerIdWidths = [] } = state.columnResizing
const deltaX = clientX - startX
const percentageDeltaX = deltaX / columnWidth
const newColumnWidths = {}
headerIdWidths.forEach(([headerId, headerWidth]) => {
newColumnWidths[headerId] = Math.max(
headerWidth + headerWidth * percentageDeltaX,
0
)
})
return {
...state,
columnResizing: {
...state.columnResizing,
columnWidths: {
...state.columnResizing.columnWidths,
...newColumnWidths,
},
},
}
}
if (action.type === actions.columnDoneResizing) {
return {
...state,
columnResizing: {
...state.columnResizing,
startX: null,
isResizingColumn: null,
},
}
}
}
const useInstanceBeforeDimensions = instance => {
const {
flatHeaders,
disableResizing,
getHooks,
state: { columnResizing },
} = instance
const getInstance = useGetLatest(instance)
flatHeaders.forEach(header => {
const canResize = getFirstDefined(
header.disableResizing === true ? false : undefined,
disableResizing === true ? false : undefined,
true
)
header.canResize = canResize
header.width =
columnResizing.columnWidths[header.id] ||
header.originalWidth ||
header.width
header.isResizing = columnResizing.isResizingColumn === header.id
if (canResize) {
header.getResizerProps = makePropGetter(getHooks().getResizerProps, {
instance: getInstance(),
header,
})
}
})
}
function useInstance(instance) {
const { plugins, dispatch, autoResetResize = true, columns } = instance
ensurePluginOrder(plugins, ['useAbsoluteLayout'], 'useResizeColumns')
const getAutoResetResize = useGetLatest(autoResetResize)
useMountedLayoutEffect(() => {
if (getAutoResetResize()) {
dispatch({ type: actions.resetResize })
}
}, [columns])
const resetResizing = React.useCallback(
() => dispatch({ type: actions.resetResize }),
[dispatch]
)
Object.assign(instance, {
resetResizing,
})
}
function getLeafHeaders(header) {
const leafHeaders = []
const recurseHeader = header => {
if (header.columns && header.columns.length) {
header.columns.map(recurseHeader)
}
leafHeaders.push(header)
}
recurseHeader(header)
return leafHeaders
}