@v4fire/client
Version:
V4Fire client core library
253 lines (202 loc) • 5.81 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unused-vars-experimental */
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
/**
* [[include:traits/i-lock-page-scroll/README.md]]
* @packageDocumentation
*/
import symbolGenerator from 'core/symbol';
import { is } from 'core/browser';
import type iBlock from 'super/i-block/i-block';
import type { ModEvent } from 'super/i-block/i-block';
export const
$$ = symbolGenerator();
const
group = 'lockHelpers';
export default abstract class iLockPageScroll {
/** @see [[iLockPageScroll.lock]] */
static lock: AddSelf<iLockPageScroll['lock'], iBlock> = (component, scrollableNode?) => {
const {
r,
r: {unsafe: {async: $a}}
} = component;
if (is.mobile !== false && is.iOS !== false) {
iLockPageScroll.initIOSScrollableNodeListeners(component, scrollableNode);
}
let
promise = Promise.resolve();
if (r[$$.isLocked] === true) {
return promise;
}
const lockLabel = {
join: true,
group,
label: $$.lock
};
if (is.mobile !== false) {
if (is.iOS !== false) {
$a.on(document, 'touchmove', (e: TouchEvent) => e.cancelable && e.preventDefault(), {
group,
label: $$.preventTouchMove,
options: {passive: false}
});
}
const {
body,
documentElement: html
} = document;
promise = $a.promise(new Promise((res) => {
$a.requestAnimationFrame(() => {
const scrollTop = Object.isTruly(html.scrollTop) ?
html.scrollTop :
body.scrollTop;
r[$$.scrollTop] = scrollTop;
body.style.top = (-scrollTop).px;
r.setRootMod('lockScrollMobile', true);
r[$$.isLocked] = true;
res();
}, lockLabel);
}), lockLabel);
} else {
promise = $a.promise(new Promise((res) => {
$a.requestAnimationFrame(() => {
const
{body} = document;
const
scrollBarWidth = globalThis.innerWidth - body.clientWidth;
r[$$.paddingRight] = body.style.paddingRight;
body.style.paddingRight = scrollBarWidth.px;
r.setRootMod('lockScrollDesktop', true);
r[$$.isLocked] = true;
res();
}, lockLabel);
}), lockLabel);
}
return promise;
};
/** @see [[iLockPageScroll.unlock]] */
static unlock: AddSelf<iLockPageScroll['unlock'], iBlock> = (component) => {
const {
r,
r: {unsafe: {async: $a}}
} = component.unsafe;
if (r[$$.isLocked] !== true) {
return Promise.resolve();
}
return $a.promise(new Promise((res) => {
$a.off({group});
$a.requestAnimationFrame(() => {
r.removeRootMod('lockScrollMobile', true);
r.removeRootMod('lockScrollDesktop', true);
r[$$.isLocked] = false;
iLockPageScroll.scrollableNodes = new WeakSet<Element>();
if (is.mobile !== false) {
globalThis.scrollTo(0, r[$$.scrollTop]);
}
document.body.style.paddingRight = r[$$.paddingRight] ?? '';
res();
}, {group, label: $$.unlockRaf, join: true});
}), {
group,
label: $$.unlock,
join: true
});
};
/**
* Initializes modifier event listeners for the specified components
*
* @emits `lock()`
* @emits `unlock()`
*
* @param component
*/
static initModEvents<T extends iBlock>(component: T & iLockPageScroll): void {
const {
r,
$async: $a,
localEmitter: $e
} = component.unsafe;
$e.on('block.mod.*.opened.*', (e: ModEvent) => {
if (e.type === 'remove' && e.reason !== 'removeMod') {
return;
}
void component[e.value === 'false' || e.type === 'remove' ? 'unlock' : 'lock']();
});
$a.worker(() => {
component.unlock().catch(stderr);
delete r[$$.paddingRight];
delete r[$$.scrollTop];
});
}
/**
* A set of scrollable nodes for the iOS platform
*/
protected static scrollableNodes: WeakSet<Element> = new WeakSet<Element>();
/**
* Initializes touch event listeners for the provided node on the iOS platform
* @see https://stackoverflow.com/questions/59193062/how-to-disable-scrolling-on-body-in-ios-13-safari-when-saved-as-pwa-to-the-hom
*
* @param component
* @param [scrollableNode]
*/
protected static initIOSScrollableNodeListeners(component: iBlock, scrollableNode?: Element): void {
const
{r: {unsafe: {async: $a}}} = component;
if (scrollableNode == null || iLockPageScroll.scrollableNodes.has(scrollableNode)) {
return;
}
iLockPageScroll.scrollableNodes.add(scrollableNode);
const
onTouchStart = (e: TouchEvent) => component[$$.initialY] = e.targetTouches[0].clientY;
$a.on(scrollableNode, 'touchstart', onTouchStart, {
group
});
const onTouchMove = (e: TouchEvent) => {
let
scrollTarget = <HTMLElement>(e.target ?? scrollableNode);
while (
scrollTarget !== scrollableNode &&
scrollTarget.scrollHeight <= scrollTarget.clientHeight && scrollTarget.parentElement
) {
scrollTarget = scrollTarget.parentElement;
}
const {
scrollTop,
scrollHeight,
clientHeight
} = scrollTarget;
const
clientY = e.targetTouches[0].clientY - component[$$.initialY],
isOnTop = clientY > 0 && scrollTop === 0,
isOnBottom = clientY < 0 && scrollTop + clientHeight + 1 >= scrollHeight;
if ((isOnTop || isOnBottom) && e.cancelable) {
return e.preventDefault();
}
e.stopPropagation();
};
$a.on(scrollableNode, 'touchmove', onTouchMove, {
group,
options: {passive: false}
});
}
/**
* Locks the document scroll, i.e.,
* it prevents any scrolling on the document except withing the specified node
*
* @param [scrollableNode] - node inside which is allowed to scroll
*/
lock(scrollableNode?: Element): Promise<void> {
return Object.throw();
}
/**
* Unlocks the document scroll
*/
unlock(): Promise<void> {
return Object.throw();
}
}