tua-body-scroll-lock
Version:
🔐Body scroll locking that just works with everything
176 lines (172 loc) • 6.09 kB
JavaScript
/**
* tua-body-scroll-lock v1.0.0
* (c) 2019 Evinma, BuptStEve
* @license MIT
*/
const isServer = () => typeof window === 'undefined';
const $ = (selector) => document.querySelector(selector);
const detectOS = (ua) => {
ua = ua || navigator.userAgent;
const ipad = /(iPad).*OS\s([\d_]+)/.test(ua);
const iphone = !ipad && /(iPhone\sOS)\s([\d_]+)/.test(ua);
const android = /(Android);?[\s/]+([\d.]+)?/.test(ua);
const ios = iphone || ipad;
return { ios, android };
};
function getEventListenerOptions(options) {
/* istanbul ignore if */
if (isServer())
return false;
if (!options) {
throw new Error('options must be provided');
}
let isSupportOptions = false;
const listenerOptions = {
get passive() {
isSupportOptions = true;
return;
},
};
/* istanbul ignore next */
const noop = () => { };
const testEvent = '__TUA_BSL_TEST_PASSIVE__';
window.addEventListener(testEvent, noop, listenerOptions);
window.removeEventListener(testEvent, noop, listenerOptions);
const { capture } = options;
/* istanbul ignore next */
return isSupportOptions
? options
: typeof capture !== 'undefined'
? capture
: false;
}
let lockedNum = 0;
let initialClientY = 0;
let initialClientX = 0;
let unLockCallback = null;
let documentListenerAdded = false;
const lockedElements = [];
const eventListenerOptions = getEventListenerOptions({ passive: false });
const setOverflowHiddenPc = () => {
const $body = $('body');
const bodyStyle = Object.assign({}, $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) => {
$body.style[x] = bodyStyle[x] || '';
});
};
};
const setOverflowHiddenMobile = () => {
const $html = $('html');
const $body = $('body');
const scrollTop = $html.scrollTop || $body.scrollTop;
const htmlStyle = Object.assign({}, $html.style);
const bodyStyle = Object.assign({}, $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) => {
$body.style[x] = bodyStyle[x] || '';
});
window.scrollTo(0, scrollTop);
};
};
const preventDefault = (event) => {
if (!event.cancelable)
return;
event.preventDefault();
};
const handleScroll = (event, targetElement) => {
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) => {
if (targetElement)
return;
if (targetElement === null)
return;
console.warn(`If scrolling is also required in the floating layer, ` +
`the target element must be provided.`);
};
const lock = (targetElement) => {
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) => {
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 };