94 行
3.7 KiB
TypeScript
94 行
3.7 KiB
TypeScript
import { useContext, useEffect, useRef, useState } from 'react'
|
|
|
|
import { CountsTableScope, MAX_COUNT_WIDTH, MIN_COUNT_WIDTH,
|
|
useCountsTableLayout } from '@/lib/countsTableLayout'
|
|
import { cn } from '@/lib/utils'
|
|
import { useUITranslation } from '@/lib/useUITranslation'
|
|
|
|
|
|
const CountsResizeHandle = ({ field, label }: {
|
|
field: string; label: { text: string; language: string } }) => {
|
|
const family = useContext (CountsTableScope)
|
|
const layout = useCountsTableLayout ()
|
|
const { message } = useUITranslation ('counts')
|
|
const [active, setActive] = useState (false)
|
|
const press = useRef<{ x: number; y: number; width: number; armed: boolean
|
|
timer?: ReturnType<typeof setTimeout> } | null> (null)
|
|
const saved = useRef<{ cursor: string; userSelect: string } | null> (null)
|
|
const restore = () => {
|
|
if (saved.current == null) return
|
|
document.body.style.cursor = saved.current.cursor
|
|
document.body.style.userSelect = saved.current.userSelect
|
|
saved.current = null
|
|
}
|
|
const finish = () => {
|
|
if (press.current?.timer != null) clearTimeout (press.current.timer)
|
|
press.current = null
|
|
setActive (false)
|
|
restore ()
|
|
}
|
|
const arm = () => {
|
|
if (press.current == null) return
|
|
press.current.armed = true
|
|
saved.current = { cursor: document.body.style.cursor,
|
|
userSelect: document.body.style.userSelect }
|
|
document.body.style.cursor = 'col-resize'
|
|
document.body.style.userSelect = 'none'
|
|
setActive (true)
|
|
}
|
|
useEffect (() => () => {
|
|
if (press.current?.timer != null) clearTimeout (press.current.timer)
|
|
if (saved.current != null)
|
|
{
|
|
document.body.style.cursor = saved.current.cursor
|
|
document.body.style.userSelect = saved.current.userSelect
|
|
}
|
|
}, [])
|
|
const name = message ('resize.column', { column: label.text })
|
|
return (
|
|
<span role="separator" tabIndex={0} aria-orientation="vertical"
|
|
aria-label={name.text} lang={name.language}
|
|
aria-valuenow={layout.width (family, field)} aria-valuemin={MIN_COUNT_WIDTH}
|
|
aria-valuemax={MAX_COUNT_WIDTH} title={name.text}
|
|
data-counts-resize
|
|
className={cn ('absolute inset-y-0 right-0 z-30 w-3 cursor-col-resize touch-none',
|
|
'select-none outline-offset-[-2px] focus-visible:outline',
|
|
'before:absolute before:inset-y-1 before:left-1/2 before:border-l',
|
|
'before:border-slate-300 dark:before:border-slate-600',
|
|
'hover:before:border-blue-500 dark:hover:before:border-blue-400',
|
|
active && 'before:border-blue-500 dark:before:border-blue-400')}
|
|
onPointerDown={event => {
|
|
event.stopPropagation ()
|
|
if (!event.isPrimary || event.button !== 0) return
|
|
event.preventDefault ()
|
|
finish ()
|
|
event.currentTarget.setPointerCapture (event.pointerId)
|
|
press.current = { x: event.clientX, y: event.clientY,
|
|
width: layout.width (family, field), armed: false }
|
|
if (event.pointerType === 'mouse') arm ()
|
|
else press.current.timer = setTimeout (arm, 400)
|
|
}}
|
|
onPointerMove={event => {
|
|
event.stopPropagation ()
|
|
const state = press.current
|
|
if (state == null) return
|
|
if (state.armed)
|
|
layout.resize (family, field, state.width + event.clientX - state.x)
|
|
else if (Math.hypot (event.clientX - state.x, event.clientY - state.y) > 8)
|
|
finish ()
|
|
}}
|
|
onPointerUp={finish} onPointerCancel={finish} onLostPointerCapture={finish}
|
|
onClick={event => { event.preventDefault (); event.stopPropagation () }}
|
|
onContextMenu={event => event.preventDefault ()}
|
|
onKeyDown={event => {
|
|
event.stopPropagation ()
|
|
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
|
|
event.preventDefault ()
|
|
layout.resize (family, field, layout.width (family, field)
|
|
+ (event.key === 'ArrowLeft' ? -10 : 10))
|
|
}}/>)
|
|
}
|
|
|
|
|
|
export default CountsResizeHandle
|