UNPKG

tua-body-scroll-lock

Version:

🔐Body scroll locking that just works with everything

180 lines (144 loc) 5.08 kB
import { $, isServer, detectOS, getEventListenerOptions, } from './utils' type OverflowHiddenPcStyleType = 'overflow' | 'boxSizing' | 'paddingRight' type OverflowHiddenMobileStyleType = 'top' | 'width' | 'height' | 'overflow' | 'position' let lockedNum = 0 let initialClientY = 0 let initialClientX = 0 let unLockCallback: any = null let documentListenerAdded = false const lockedElements: HTMLElement[] = [] const eventListenerOptions = getEventListenerOptions({ passive: false }) const setOverflowHiddenPc = () => { const $body = $('body') const bodyStyle = { ...$body.style } const scrollBarWidth = window.innerWidth - document.body.clientWidth $body.style.overflow = 'hidden' $body.style.boxSizing = 'border-box' $body.style.paddingRight = `${scrollBarWidth}px` return () => { ;['overflow', 'boxSizing', 'paddingRight'].forEach((x: OverflowHiddenPcStyleType) => { $body.style[x] = bodyStyle[x] || '' }) } } const setOverflowHiddenMobile = () => { const $html = $('html') const $body = $('body') const scrollTop = $html.scrollTop || $body.scrollTop const htmlStyle = { ...$html.style } const bodyStyle = { ...$body.style } $html.style.height = '100%' $html.style.overflow = 'hidden' $body.style.top = `-${scrollTop}px` $body.style.width = '100%' $body.style.height = 'auto' $body.style.position = 'fixed' $body.style.overflow = 'hidden' return () => { $html.style.height = htmlStyle.height || '' $html.style.overflow = htmlStyle.overflow || '' ;['top', 'width', 'height', 'overflow', 'position'].forEach((x: OverflowHiddenMobileStyleType) => { $body.style[x] = bodyStyle[x] || '' }) window.scrollTo(0, scrollTop) } } const preventDefault = (event: TouchEvent) => { if (!event.cancelable) return event.preventDefault() } const handleScroll = (event: TouchEvent, targetElement: HTMLElement) => { if (targetElement) { const { scrollTop, scrollLeft, scrollWidth, scrollHeight, clientWidth, clientHeight, } = targetElement const clientX = event.targetTouches[0].clientX - initialClientX const clientY = event.targetTouches[0].clientY - initialClientY const isVertical = Math.abs(clientY) > Math.abs(clientX) const isOnTop = clientY > 0 && scrollTop === 0 const isOnLeft = clientX > 0 && scrollLeft === 0 const isOnRight = clientX < 0 && scrollLeft + clientWidth + 1 >= scrollWidth const isOnBottom = clientY < 0 && scrollTop + clientHeight + 1 >= scrollHeight if ( (isVertical && (isOnTop || isOnBottom)) || (!isVertical && (isOnLeft || isOnRight)) ) { return preventDefault(event) } } event.stopPropagation() return true } const checkTargetElement = (targetElement?: HTMLElement) => { if (targetElement) return if (targetElement === null) return if (process.env.NODE_ENV === 'production') return console.warn( `If scrolling is also required in the floating layer, ` + `the target element must be provided.` ) } const lock = (targetElement?: HTMLElement) => { if (isServer()) return checkTargetElement(targetElement) if (detectOS().ios) { // iOS if (targetElement && lockedElements.indexOf(targetElement) === -1) { targetElement.ontouchstart = (event) => { initialClientY = event.targetTouches[0].clientY initialClientX = event.targetTouches[0].clientX } targetElement.ontouchmove = (event) => { if (event.targetTouches.length !== 1) return handleScroll(event, targetElement) } lockedElements.push(targetElement) } if (!documentListenerAdded) { document.addEventListener('touchmove', preventDefault, eventListenerOptions) documentListenerAdded = true } } else if (lockedNum <= 0) { unLockCallback = detectOS().android ? setOverflowHiddenMobile() : setOverflowHiddenPc() } lockedNum += 1 } const unlock = (targetElement?: HTMLElement) => { if (isServer()) return checkTargetElement(targetElement) lockedNum -= 1 if (lockedNum > 0) return if ( !detectOS().ios && typeof unLockCallback === 'function' ) { unLockCallback() return } // iOS if (targetElement) { const index = lockedElements.indexOf(targetElement) if (index !== -1) { targetElement.ontouchmove = null targetElement.ontouchstart = null lockedElements.splice(index, 1) } } if (documentListenerAdded) { document.removeEventListener('touchmove', preventDefault, eventListenerOptions) documentListenerAdded = false } } export { lock, unlock }