mirror of https://github.com/vuejs/vitepress
parent
4f373d3186
commit
310679a76c
@ -0,0 +1,141 @@
|
||||
import { inBrowser } from 'vitepress'
|
||||
import {
|
||||
computed,
|
||||
onScopeDispose,
|
||||
shallowRef,
|
||||
type WritableComputedRef
|
||||
} from 'vue'
|
||||
|
||||
const isIOS =
|
||||
inBrowser &&
|
||||
(/iP(?:ad|hone|od)/.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1))
|
||||
|
||||
const scrollKeys = new Set([
|
||||
' ',
|
||||
'PageUp',
|
||||
'PageDown',
|
||||
'Home',
|
||||
'End',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight'
|
||||
])
|
||||
|
||||
const listenerOptions = { capture: true, passive: false } as const
|
||||
|
||||
function isScrollable(target: EventTarget | null): boolean {
|
||||
let el = target instanceof Element ? target : null
|
||||
while (el && el !== document.body) {
|
||||
const { overflowX, overflowY } = getComputedStyle(el)
|
||||
if (
|
||||
((overflowY === 'auto' || overflowY === 'scroll') &&
|
||||
el.scrollHeight > el.clientHeight) ||
|
||||
((overflowX === 'auto' || overflowX === 'scroll') &&
|
||||
el.scrollWidth > el.clientWidth)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
el = el.parentElement
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function blockScroll(e: Event) {
|
||||
if ('touches' in e && (e as TouchEvent).touches.length > 1) return
|
||||
if (!isScrollable(e.target)) e.preventDefault()
|
||||
}
|
||||
|
||||
function blockScrollKeys(e: KeyboardEvent) {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey || !scrollKeys.has(e.key)) return
|
||||
const el = e.target
|
||||
if (
|
||||
el instanceof HTMLElement &&
|
||||
(el.isContentEditable || el.matches('input, textarea, select'))
|
||||
) {
|
||||
return
|
||||
}
|
||||
blockScroll(e)
|
||||
}
|
||||
|
||||
// all instances lock the same element, so the style juggling is refcounted
|
||||
let overflowLockCount = 0
|
||||
let eventLockCount = 0
|
||||
let initialOverflow: string | undefined
|
||||
let initialGutter: string | undefined
|
||||
|
||||
function lockOverflow() {
|
||||
if (++overflowLockCount > 1) return
|
||||
const html = document.documentElement
|
||||
if (!getComputedStyle(html).scrollbarGutter.includes('stable')) {
|
||||
initialGutter = html.style.scrollbarGutter
|
||||
html.style.scrollbarGutter = 'stable'
|
||||
}
|
||||
initialOverflow = document.body.style.overflow
|
||||
document.body.style.overflow = 'hidden'
|
||||
if (isIOS) {
|
||||
document.addEventListener('touchmove', blockScroll, listenerOptions)
|
||||
}
|
||||
}
|
||||
|
||||
function unlockOverflow() {
|
||||
if (--overflowLockCount > 0) return
|
||||
if (isIOS) {
|
||||
document.removeEventListener('touchmove', blockScroll, listenerOptions)
|
||||
}
|
||||
document.body.style.overflow = initialOverflow ?? ''
|
||||
if (initialGutter !== undefined) {
|
||||
document.documentElement.style.scrollbarGutter = initialGutter
|
||||
}
|
||||
initialOverflow = initialGutter = undefined
|
||||
}
|
||||
|
||||
function lockEvents() {
|
||||
if (++eventLockCount > 1) return
|
||||
document.addEventListener('wheel', blockScroll, listenerOptions)
|
||||
document.addEventListener('touchmove', blockScroll, listenerOptions)
|
||||
document.addEventListener('keydown', blockScrollKeys, listenerOptions)
|
||||
}
|
||||
|
||||
function unlockEvents() {
|
||||
if (--eventLockCount > 0) return
|
||||
document.removeEventListener('wheel', blockScroll, listenerOptions)
|
||||
document.removeEventListener('touchmove', blockScroll, listenerOptions)
|
||||
document.removeEventListener('keydown', blockScrollKeys, listenerOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks page scrolling behind an overlay.
|
||||
*
|
||||
* Prefer `scrollbar-gutter: stable` + `overflow: hidden` so layout width
|
||||
* stays stable when the scrollbar is hidden. If unsupported, fall back to
|
||||
* blocking scroll events while still allowing events inside scrollable
|
||||
* elements.
|
||||
*/
|
||||
export function useBodyScrollLock(): WritableComputedRef<boolean> {
|
||||
const isLocked = shallowRef(false)
|
||||
let useEvents = false
|
||||
|
||||
function lock() {
|
||||
if (isLocked.value) return
|
||||
useEvents =
|
||||
window.innerWidth > document.documentElement.clientWidth &&
|
||||
!CSS.supports('scrollbar-gutter', 'stable')
|
||||
useEvents ? lockEvents() : lockOverflow()
|
||||
isLocked.value = true
|
||||
}
|
||||
|
||||
function unlock() {
|
||||
if (!isLocked.value) return
|
||||
useEvents ? unlockEvents() : unlockOverflow()
|
||||
isLocked.value = false
|
||||
}
|
||||
|
||||
onScopeDispose(unlock)
|
||||
|
||||
return computed({
|
||||
get: () => isLocked.value,
|
||||
set: (value) => (value ? lock() : unlock())
|
||||
})
|
||||
}
|
||||
Loading…
Reference in new issue