|
| 1 | +import { isClient, isIOS } from '@vueuse/core' |
| 2 | + |
| 3 | +export function useBodyScrollLock(): () => void { |
| 4 | + if (!isClient) |
| 5 | + return () => {} |
| 6 | + |
| 7 | + const body = document.body |
| 8 | + if (body.hasAttribute('data-scroll-lock')) |
| 9 | + return () => {} |
| 10 | + |
| 11 | + const html = document.documentElement |
| 12 | + const bodyStyle = body.style |
| 13 | + |
| 14 | + const originalStyles = { |
| 15 | + overflow: bodyStyle.overflow, |
| 16 | + overflowX: bodyStyle.overflowX, |
| 17 | + overflowY: bodyStyle.overflowY, |
| 18 | + position: bodyStyle.position, |
| 19 | + top: bodyStyle.top, |
| 20 | + left: bodyStyle.left, |
| 21 | + right: bodyStyle.right, |
| 22 | + bottom: bodyStyle.bottom, |
| 23 | + scrollBehavior: html.style.scrollBehavior, |
| 24 | + } |
| 25 | + |
| 26 | + const initialOverflow = bodyStyle.overflow |
| 27 | + const scrollY = window.scrollY |
| 28 | + |
| 29 | + bodyStyle.top = `-${scrollY}px` |
| 30 | + bodyStyle.overflowX = 'hidden' |
| 31 | + html.style.scrollBehavior = 'auto' |
| 32 | + |
| 33 | + bodyStyle.setProperty('overflow-y', 'scroll', 'important') |
| 34 | + bodyStyle.position = 'fixed' |
| 35 | + bodyStyle.left = '0' |
| 36 | + bodyStyle.right = '0' |
| 37 | + bodyStyle.bottom = '0' |
| 38 | + body.setAttribute('data-scroll-lock', 'true') |
| 39 | + |
| 40 | + let stopTouchMoveListener: (() => void) | undefined |
| 41 | + |
| 42 | + if (isIOS) { |
| 43 | + function onTouchmove(e: TouchEvent) { |
| 44 | + preventDefault(e) |
| 45 | + } |
| 46 | + |
| 47 | + document.addEventListener('touchmove', onTouchmove, { |
| 48 | + passive: false, |
| 49 | + }) |
| 50 | + |
| 51 | + stopTouchMoveListener = () => { |
| 52 | + document.removeEventListener('touchmove', onTouchmove) |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + function onlock() { |
| 57 | + bodyStyle.overflow = initialOverflow ?? '' |
| 58 | + body.removeAttribute('data-scroll-lock') |
| 59 | + |
| 60 | + bodyStyle.overflowY = originalStyles.overflowY |
| 61 | + bodyStyle.position = originalStyles.position |
| 62 | + bodyStyle.left = originalStyles.left |
| 63 | + bodyStyle.right = originalStyles.right |
| 64 | + bodyStyle.bottom = originalStyles.bottom |
| 65 | + |
| 66 | + bodyStyle.top = originalStyles.top |
| 67 | + window.scrollTo(0, scrollY) |
| 68 | + html.style.scrollBehavior = originalStyles.scrollBehavior |
| 69 | + |
| 70 | + stopTouchMoveListener?.() |
| 71 | + } |
| 72 | + |
| 73 | + return onlock |
| 74 | +} |
| 75 | + |
| 76 | +function preventDefault(event: TouchEvent): boolean { |
| 77 | + const _target = event.target as Element |
| 78 | + |
| 79 | + // Do not prevent if element or parentNodes have overflow: scroll set. |
| 80 | + if (checkOverflowScroll(_target)) |
| 81 | + return false |
| 82 | + |
| 83 | + // Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom). |
| 84 | + if (event.touches.length > 1) |
| 85 | + return true |
| 86 | + |
| 87 | + if (event.preventDefault) |
| 88 | + event.preventDefault() |
| 89 | + |
| 90 | + return false |
| 91 | +} |
| 92 | + |
| 93 | +function checkOverflowScroll(el: Element): boolean { |
| 94 | + const style = window.getComputedStyle(el) |
| 95 | + if ( |
| 96 | + style.overflowX === 'scroll' |
| 97 | + || style.overflowY === 'scroll' |
| 98 | + || (style.overflowX === 'auto' && el.clientWidth < el.scrollWidth) |
| 99 | + || (style.overflowY === 'auto' && el.clientHeight < el.scrollHeight) |
| 100 | + ) { |
| 101 | + return true |
| 102 | + } |
| 103 | + else { |
| 104 | + const parent = el.parentNode as Element |
| 105 | + |
| 106 | + if (!parent || parent.tagName === 'BODY') |
| 107 | + return false |
| 108 | + |
| 109 | + return checkOverflowScroll(parent) |
| 110 | + } |
| 111 | +} |
0 commit comments