344 lines
13 KiB
TypeScript
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
|