Files
oxygen-ui/src/components/common/HideScrollbar.tsx

344 lines
13 KiB
TypeScript

import React from 'react'
import '@/assets/css/components/common/hide-scrollbar.scss'
interface HideScrollbarProps
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
isPreventScroll?: boolean
isPreventVerticalScroll?: boolean
isPreventHorizontalScroll?: boolean
isShowVerticalScrollbar?: boolean
isShowHorizontalScrollbar?: boolean
}
export interface HideScrollbarElement {
scrollTo(x: number, y: number, smooth?: boolean): void
scrollX(x: number, smooth?: boolean): void
scrollY(y: number, smooth?: boolean): void
scrollLeft(length: number, smooth?: boolean): void
scrollRight(length: number, smooth?: boolean): void
scrollUp(length: number, smooth?: boolean): void
scrollDown(length: number, smooth?: boolean): void
getX(): number
getY(): number
addEventListenerWithType<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => never,
options?: boolean | AddEventListenerOptions
): void
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions
): void
removeEventListenerWithType<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => never,
options?: boolean | EventListenerOptions
): void
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions
): void
}
const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>((props, ref) => {
const rootRef = useRef<HTMLDivElement>(null)
const lastTouchPosition = useRef<{ x: number; y: number }>({ x: -1, y: -1 })
const [verticalScrollbarWidth, setVerticalScrollbarWidth] = useState(0)
const [horizontalScrollbarWidth, setHorizontalScrollbarWidth] = useState(0)
const [verticalScrollbarLength, setVerticalScrollbarLength] = useState(100)
const [horizontalScrollbarLength, setHorizontalScrollbarLength] = useState(100)
const {
isPreventScroll,
isPreventVerticalScroll,
isPreventHorizontalScroll,
isShowVerticalScrollbar,
isShowHorizontalScrollbar,
..._props
} = props
const handleDefaultWheel = useCallback(
(event: WheelEvent) => {
if (!event.altKey && !event.ctrlKey) {
if (isPreventScroll) {
event.preventDefault()
return
}
if (isPreventVerticalScroll && !event.shiftKey && !event.deltaX) {
event.preventDefault()
return
}
if (isPreventHorizontalScroll && (event.shiftKey || !event.deltaY)) {
event.preventDefault()
return
}
}
},
[isPreventScroll, isPreventHorizontalScroll, isPreventVerticalScroll]
)
const handleDefaultTouchStart = useCallback(
(event: TouchEvent) => {
if (event.touches.length !== 1 || isPreventScroll) {
lastTouchPosition.current = { x: -1, y: -1 }
return
}
const { clientX, clientY } = event.touches[0]
lastTouchPosition.current = { x: clientX, y: clientY }
},
[isPreventScroll]
)
const handleDefaultTouchmove = useCallback(
(event: TouchEvent) => {
event.preventDefault()
if (event.touches.length !== 1 || isPreventScroll) {
lastTouchPosition.current = { x: -1, y: -1 }
return
}
const { clientX, clientY } = event.touches[0]
if (!isPreventVerticalScroll) {
rootRef.current?.scrollTo({
top: rootRef.current?.scrollTop + (lastTouchPosition.current.y - clientY),
behavior: 'instant'
})
}
if (!isPreventHorizontalScroll) {
rootRef.current?.scrollTo({
left: rootRef.current?.scrollLeft + (lastTouchPosition.current.x - clientX),
behavior: 'instant'
})
}
lastTouchPosition.current = { x: clientX, y: clientY }
},
[isPreventScroll, isPreventHorizontalScroll, isPreventVerticalScroll]
)
const handleDefaultClickMiddleMouseButton = useCallback((event: MouseEvent) => {
if (event.button === 1) {
event.preventDefault()
}
}, [])
const handleDefaultKeyDown = useCallback(
(event: KeyboardEvent) => {
if (
isPreventScroll &&
[
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
' ',
'',
'PageUp',
'PageDown',
'Home',
'End'
].find((value) => value === event.key)
) {
event.preventDefault()
}
if (
isPreventVerticalScroll &&
['ArrowUp', 'ArrowDown', ' ', '', 'PageUp', 'PageDown', 'Home', 'End'].find(
(value) => value === event.key
)
) {
event.preventDefault()
}
if (
isPreventHorizontalScroll &&
['ArrowLeft', 'ArrowRight'].find((value) => value === event.key)
) {
event.preventDefault()
}
},
[isPreventScroll, isPreventHorizontalScroll, isPreventVerticalScroll]
)
useImperativeHandle<HideScrollbarElement, HideScrollbarElement>(
ref,
() => {
return {
scrollTo(x, y, smooth?: boolean) {
rootRef.current?.scrollTo({
left: x,
top: y,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
scrollX(x, smooth?: boolean) {
rootRef.current?.scrollTo({
left: x,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
scrollY(y, smooth?: boolean) {
rootRef.current?.scrollTo({
top: y,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
scrollLeft(length, smooth?: boolean) {
rootRef.current?.scrollTo({
left: rootRef.current?.scrollLeft - length,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
scrollRight(length, smooth?: boolean) {
rootRef.current?.scrollTo({
left: rootRef.current?.scrollLeft + length,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
scrollUp(length, smooth?: boolean) {
rootRef.current?.scrollTo({
top: rootRef.current?.scrollTop - length,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
scrollDown(length, smooth?: boolean) {
rootRef.current?.scrollTo({
top: rootRef.current?.scrollTop + length,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
getX() {
return rootRef.current?.scrollLeft ?? 0
},
getY() {
return rootRef.current?.scrollTop ?? 0
},
addEventListenerWithType<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => never,
options?: boolean | AddEventListenerOptions
): void {
rootRef.current?.addEventListener<K>(type, listener, options)
},
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions
): void {
rootRef.current?.addEventListener(type, listener, options)
},
removeEventListenerWithType<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => never,
options?: boolean | EventListenerOptions
): void {
rootRef.current?.removeEventListener<K>(type, listener, options)
},
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions
): void {
rootRef.current?.removeEventListener(type, listener, options)
}
}
},
[]
)
useEffect(() => {
const hideScrollbarElement = rootRef.current
const windowResizeListener = () => {
setVerticalScrollbarWidth(
(hideScrollbarElement?.offsetWidth ?? 0) - (hideScrollbarElement?.clientWidth ?? 0)
)
setHorizontalScrollbarWidth(
(hideScrollbarElement?.offsetHeight ?? 0) -
(hideScrollbarElement?.clientHeight ?? 0)
)
return windowResizeListener
}
setTimeout(() => {
windowResizeListener()
}, 1000)
window.addEventListener('resize', windowResizeListener())
if (isPreventScroll || isPreventVerticalScroll || isPreventHorizontalScroll) {
rootRef.current?.addEventListener('wheel', handleDefaultWheel, { passive: false })
rootRef.current?.addEventListener('touchstart', handleDefaultTouchStart, {
passive: false
})
rootRef.current?.addEventListener('touchmove', handleDefaultTouchmove, {
passive: false
})
rootRef.current?.addEventListener('mousedown', handleDefaultClickMiddleMouseButton)
rootRef.current?.addEventListener('keydown', handleDefaultKeyDown)
} else {
rootRef.current?.removeEventListener('wheel', handleDefaultWheel)
rootRef.current?.removeEventListener('touchstart', handleDefaultTouchStart)
rootRef.current?.removeEventListener('touchmove', handleDefaultTouchmove)
rootRef.current?.removeEventListener('mousedown', handleDefaultClickMiddleMouseButton)
rootRef.current?.removeEventListener('keydown', handleDefaultKeyDown)
}
return () => {
window.removeEventListener('resize', windowResizeListener)
}
}, [
handleDefaultClickMiddleMouseButton,
handleDefaultKeyDown,
handleDefaultTouchStart,
handleDefaultTouchmove,
handleDefaultWheel,
isPreventHorizontalScroll,
isPreventScroll,
isPreventVerticalScroll
])
return (
<>
<div className={'hide-scrollbar-mask'}>
<div
ref={rootRef}
className={'hide-scrollbar-selection'}
tabIndex={0}
style={{
width: `calc(100vw + ${verticalScrollbarWidth}px)`,
height: `calc(100vh + ${horizontalScrollbarWidth}px)`
}}
{..._props}
>
{props.children}
</div>
<div hidden={!isShowVerticalScrollbar} className={'scrollbar vertical-scrollbar'}>
<div className={'box'}>
<div
className={'block'}
style={{ height: `${verticalScrollbarLength}%` }}
/>
</div>
</div>
<div
hidden={!isShowHorizontalScrollbar}
className={'scrollbar horizontal-scrollbar'}
>
<div className={'box'}>
<div
className={'block'}
style={{ width: `${horizontalScrollbarLength}%` }}
/>
</div>
</div>
</div>
</>
)
})
export default HideScrollbar