body-scroll-lock-upgrade
Version:
Enables body scroll locking (for iOS Mobile and Tablet, Android, desktop Safari/Chrome/Firefox) without breaking scrolling of a target element (eg. modal/lightbox/flyouts/nav-menus)
244 lines (243 loc) • 8.71 kB
JavaScript
/**
* name: body-scroll-lock-upgrade
* version: v1.1.0
* author: Rick.li
*/
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.bodyScrollLockUpgrade = {}));
})(this, function(exports2) {
"use strict";
let hasPassiveEvents = false;
if (typeof window !== "undefined") {
const passiveTestOptions = {
get passive() {
hasPassiveEvents = true;
return void 0;
}
};
window.addEventListener("testPassive", null, passiveTestOptions);
window.removeEventListener("testPassive", null, passiveTestOptions);
}
const isIosDevice = typeof window !== "undefined" && window.navigator && window.navigator.platform && (/iP(ad|hone|od)/.test(window.navigator.platform) || window.navigator.platform === "MacIntel" && window.navigator.maxTouchPoints > 1);
let locks = [];
let locksIndex = /* @__PURE__ */ new Map();
let documentListenerAdded = false;
let initialClientY = -1;
let previousBodyOverflowSetting;
let htmlStyle;
let bodyStyle;
let previousBodyPaddingRight;
const allowTouchMove = (el) => locks.some((lock) => {
if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) {
return true;
}
return false;
});
const preventDefault = (rawEvent) => {
const e = rawEvent || window.event;
if (allowTouchMove(e.target)) {
return true;
}
if (e.touches.length > 1)
return true;
if (e.preventDefault)
e.preventDefault();
return false;
};
const setOverflowHidden = (options) => {
if (previousBodyPaddingRight === void 0) {
const reserveScrollBarGap = !!options && options.reserveScrollBarGap === true;
const scrollBarGap = window.innerWidth - document.documentElement.getBoundingClientRect().width;
if (reserveScrollBarGap && scrollBarGap > 0) {
const computedBodyPaddingRight = parseInt(
window.getComputedStyle(document.body).getPropertyValue("padding-right"),
10
);
previousBodyPaddingRight = document.body.style.paddingRight;
document.body.style.paddingRight = `${computedBodyPaddingRight + scrollBarGap}px`;
}
}
if (previousBodyOverflowSetting === void 0) {
previousBodyOverflowSetting = document.body.style.overflow;
document.body.style.overflow = "hidden";
}
};
const restoreOverflowSetting = () => {
if (previousBodyPaddingRight !== void 0) {
document.body.style.paddingRight = previousBodyPaddingRight;
previousBodyPaddingRight = void 0;
}
if (previousBodyOverflowSetting !== void 0) {
document.body.style.overflow = previousBodyOverflowSetting;
previousBodyOverflowSetting = void 0;
}
};
const setPositionFixed = () => window.requestAnimationFrame(() => {
const $html = document.documentElement;
const $body = document.body;
if (bodyStyle === void 0) {
htmlStyle = { ...$html.style };
bodyStyle = { ...$body.style };
const { scrollY, scrollX, innerHeight } = window;
$html.style.height = "100%";
$html.style.overflow = "hidden";
$body.style.position = "fixed";
$body.style.top = `${-scrollY}px`;
$body.style.left = `${-scrollX}px`;
$body.style.width = "100%";
$body.style.height = "auto";
$body.style.overflow = "hidden";
setTimeout(
() => window.requestAnimationFrame(() => {
const bottomBarHeight = innerHeight - window.innerHeight;
if (bottomBarHeight && scrollY >= innerHeight) {
$body.style.top = -(scrollY + bottomBarHeight) + "px";
}
}),
300
);
}
});
const restorePositionSetting = () => {
if (bodyStyle !== void 0) {
const y = -parseInt(document.body.style.top, 10);
const x = -parseInt(document.body.style.left, 10);
const $html = document.documentElement;
const $body = document.body;
$html.style.height = (htmlStyle == null ? void 0 : htmlStyle.height) || "";
$html.style.overflow = (htmlStyle == null ? void 0 : htmlStyle.overflow) || "";
$body.style.position = bodyStyle.position || "";
$body.style.top = bodyStyle.top || "";
$body.style.left = bodyStyle.left || "";
$body.style.width = bodyStyle.width || "";
$body.style.height = bodyStyle.height || "";
$body.style.overflow = bodyStyle.overflow || "";
window.scrollTo(x, y);
bodyStyle = void 0;
}
};
const isTargetElementTotallyScrolled = (targetElement) => targetElement ? targetElement.scrollHeight - targetElement.scrollTop <= targetElement.clientHeight : false;
const handleScroll = (event, targetElement) => {
const clientY = event.targetTouches[0].clientY - initialClientY;
if (allowTouchMove(event.target)) {
return false;
}
if (targetElement && targetElement.scrollTop === 0 && clientY > 0) {
return preventDefault(event);
}
if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) {
return preventDefault(event);
}
event.stopPropagation();
return true;
};
const disableBodyScroll = (targetElement, options) => {
if (!targetElement) {
console.error(
"disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices."
);
return;
}
locksIndex.set(
targetElement,
(locksIndex == null ? void 0 : locksIndex.get(targetElement)) ? (locksIndex == null ? void 0 : locksIndex.get(targetElement)) + 1 : 1
);
if (locks.some((lock2) => lock2.targetElement === targetElement)) {
return;
}
const lock = {
targetElement,
options: options || {}
};
locks = [...locks, lock];
if (isIosDevice) {
setPositionFixed();
} else {
setOverflowHidden(options);
}
if (isIosDevice) {
targetElement.ontouchstart = (event) => {
if (event.targetTouches.length === 1) {
initialClientY = event.targetTouches[0].clientY;
}
};
targetElement.ontouchmove = (event) => {
if (event.targetTouches.length === 1) {
handleScroll(event, targetElement);
}
};
if (!documentListenerAdded) {
document.addEventListener(
"touchmove",
preventDefault,
hasPassiveEvents ? { passive: false } : void 0
);
documentListenerAdded = true;
}
}
};
const clearAllBodyScrollLocks = () => {
if (isIosDevice) {
locks.forEach((lock) => {
lock.targetElement.ontouchstart = null;
lock.targetElement.ontouchmove = null;
});
if (documentListenerAdded) {
document.removeEventListener(
"touchmove",
preventDefault,
hasPassiveEvents ? { passive: false } : void 0
);
documentListenerAdded = false;
}
initialClientY = -1;
}
if (isIosDevice) {
restorePositionSetting();
} else {
restoreOverflowSetting();
}
locks = [];
locksIndex.clear();
};
const enableBodyScroll = (targetElement) => {
if (!targetElement) {
console.error(
"enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices."
);
return;
}
locksIndex.set(
targetElement,
(locksIndex == null ? void 0 : locksIndex.get(targetElement)) ? (locksIndex == null ? void 0 : locksIndex.get(targetElement)) - 1 : 0
);
if ((locksIndex == null ? void 0 : locksIndex.get(targetElement)) === 0) {
locks = locks.filter((lock) => lock.targetElement !== targetElement);
locksIndex == null ? void 0 : locksIndex.delete(targetElement);
}
if (isIosDevice) {
targetElement.ontouchstart = null;
targetElement.ontouchmove = null;
if (documentListenerAdded && locks.length === 0) {
document.removeEventListener(
"touchmove",
preventDefault,
hasPassiveEvents ? { passive: false } : void 0
);
documentListenerAdded = false;
}
}
if (locks.length === 0) {
if (isIosDevice) {
restorePositionSetting();
} else {
restoreOverflowSetting();
}
}
};
exports2.clearAllBodyScrollLocks = clearAllBodyScrollLocks;
exports2.disableBodyScroll = disableBodyScroll;
exports2.enableBodyScroll = enableBodyScroll;
Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
});
//# sourceMappingURL=index.umd.js.map