Skip to content

Instantly share code, notes, and snippets.

@shirakaba
Last active April 21, 2022 20:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shirakaba/5ae47e45752539cfa95f5f2ee922ac2e to your computer and use it in GitHub Desktop.
Save shirakaba/5ae47e45752539cfa95f5f2ee922ac2e to your computer and use it in GitHub Desktop.
Scroll-trapping view (modal overlay)
<script lang="ts">
import Popover from "./Popover.svelte";
import InputView from "./InputView.svelte";
import { onMount } from "svelte";
import type { InputViewProps } from "./InputInterfaces";
import { InputParser } from "./InputParser";
import Loader from "./Loader.svelte";
export let inputModel: InputViewProps|undefined = void 0;
export const inputParser: InputParser = new InputParser();
export let sourceRect: { width: number, height: number, x: number, y: number } = {
width: 20,
height: 20,
x: 100,
y: 50,
};
export let preferredWidth = 700;
export let preferredHeight = 400;
export let visible = false;
export let onLemmaIndexSelected: (i: number) => void = (i: number) => {}
let restoreBodyStyle = () => {};
$: {
if(visible && mounted){
backdropWidth = window.innerWidth;
backdropHeight = window.innerHeight;
window.addEventListener('resize', resizeListener, true);
/**
* Prevent the document underneath from being scrolled while the modal is open.
*
* Our first approach, via "position: fixed;" didn't quite work on Twitter:
* @see https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/#lets-enhance-the-fixed-body-approach
*
* ... So now we're try this trick to disable the "wheel" event:
* @see https://stackoverflow.com/questions/9538868/prevent-body-from-scrolling-when-a-modal-is-opened
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event
*/
const options = {
capture: true,
passive: false,
once: false,
};
window.addEventListener("wheel", trapScroll, options);
restoreBodyStyle = () => {
window.removeEventListener("wheel", trapScroll, options);
};
} else {
window.removeEventListener('resize', resizeListener, true);
restoreBodyStyle?.();
}
}
let backdropWidth = 0;
let backdropHeight = 0;
function trapScroll(event: WheelEvent): void {
if((event as any).lbIgnore){
return;
}
function scrolledToRelevantHorizontalEdge(scrollview: HTMLElement, event: WheelEvent): boolean {
const scrolledToLeft = 0 === scrollview.scrollLeft;
const scrolledToRight = scrollview.scrollLeft + scrollview.clientWidth === scrollview.scrollWidth;
const scrolledToRelevantEdge = (event.deltaX < -0 && scrolledToLeft) || (event.deltaX > 0 && scrolledToRight);
// console.log(`[${scrollview.id}] dx: ${event.deltaX}; dY: ${event.deltaY}; scrolledToLeft: ${scrolledToLeft}; scrolledToRight: ${scrolledToRight}; scrolledToRelevantHorizontalEdge: ${scrolledToRelevantEdge}`);
return scrolledToRelevantEdge;
}
function scrolledToRelevantVerticalEdge(scrollview: HTMLElement, event: WheelEvent): boolean {
const scrolledToTop = 0 === scrollview.scrollTop;
/**
* Unlike offsetWidth, which is calculable from CSS, clientWidth is affected by scrollbar width/height.
* @see https://stackoverflow.com/a/21064102/5951226
*/
const scrolledToBottom = scrollview.scrollTop + scrollview.clientHeight === scrollview.scrollHeight;
return (event.deltaY < -0 && scrolledToTop) || (event.deltaY > 0 && scrolledToBottom);
}
function blockScrolling(event: WheelEvent): void {
// Prevent document scrolling.
event.preventDefault();
event.stopImmediatePropagation();
}
/**
* Clone a WheelEvent, overwriting the deltaX and deltaY as desired, then redispatch it.
* Intended for zeroing out the deltaX whilst preserving the deltaY, and vice versa.
*/
function redispatch(event: WheelEvent, deltaX: number, deltaY: number): void {
const newEvent = new WheelEvent(event.type, {
deltaMode: event.deltaMode,
deltaX,
deltaY,
deltaZ: event.deltaZ,
button: event.button,
buttons: event.buttons,
clientX: event.clientX,
clientY: event.clientY,
movementX: event.movementX,
movementY: event.movementY,
relatedTarget: event.relatedTarget,
screenX: event.screenX,
screenY: event.screenY,
altKey: event.altKey,
ctrlKey: event.ctrlKey,
metaKey: event.metaKey,
modifierAltGraph: (event as any).modifierAltGraph,
modifierCapsLock: (event as any).modifierCapsLock,
modifierFn: (event as any).modifierFn,
modifierFnLock: (event as any).modifierFnLock,
modifierHyper: (event as any).modifierHyper,
modifierNumLock: (event as any).modifierNumLock,
modifierScrollLock: (event as any).modifierScrollLock,
modifierSuper: (event as any).modifierSuper,
modifierSymbol: (event as any).modifierSymbol,
modifierSymbolLock: (event as any).modifierSymbolLock,
shiftKey: event.shiftKey,
detail: event.detail,
view: event.view,
bubbles: event.bubbles,
cancelable: event.cancelable,
composed: event.composed,
});
(newEvent as any).lbIgnore = true;
blockScrolling(event);
event.target.dispatchEvent(newEvent);
return;
}
const scrollview = document.getElementById("linguabrowse-popup-dict-scrollview");
const lemmascroller = document.getElementById("linguabrowse-popup-dict-lemmas");
if(Math.abs(event.deltaX) > 0 && lemmascroller?.contains(event.target as Node)){
if(scrolledToRelevantHorizontalEdge(lemmascroller, event)){
blockScrolling(event);
}
// Permit the scroll, as it will be swallowed by the lemmascroller.
return;
}
if(!scrollview?.contains(event.target as Node)){
event.preventDefault();
event.stopImmediatePropagation();
return;
}
if(Math.abs(event.deltaY) > 0){
if(scrolledToRelevantVerticalEdge(scrollview, event)){
// Rewrite any event to have deltaY of 0 as we're at the vertical limit
if(Math.abs(event.deltaX) > 0){
if(scrolledToRelevantHorizontalEdge(scrollview, event)){
blockScrolling(event);
} else {
redispatch(event, event.deltaX, 0);
}
return;
}
// We're at the vertical limit, and there's no X movement to save, so block it.
blockScrolling(event);
return;
}
// No need to rewrite deltaY, as we do want to perform a vertical scroll.
if(Math.abs(event.deltaX) > 0){
if(scrolledToRelevantHorizontalEdge(scrollview, event)){
// Rewrite any event to have deltaX of 0 as we're at the horizontal limit
redispatch(event, 0, event.deltaY);
}
// Allow the event to dispatch, as we're at neither the horizontal nor vertical limit.
return;
}
return;
}
if(Math.abs(event.deltaX) > 0){
if(scrolledToRelevantHorizontalEdge(scrollview, event)){
// No Y to save, and we're at the horizontal limit.
blockScrolling(event);
}
// Allow the event to dispatch, as there's no Y, and there's horizontal space to spare.
return;
}
// No X nor Y movement. Maybe it was a Z scroll. Safe to let it bubble.
}
function resizeListener(event: UIEvent){
if(!document.body){
return;
}
backdropWidth = window.innerWidth;
backdropHeight = window.innerHeight;
}
let arrowDirection: "up"|"down"|"left"|"right"|"none" = "none";
let popoverWidth = 0;
let popoverHeight = 0;
let mounted = false;
export let loading = false;
let loaderImgLoadingError = false;
function onloaderImgLoadingError(error: Event){
loaderImgLoadingError = true;
}
export let onBackdropClick: (event: MouseEvent) => void = function onBackdropClick(event: MouseEvent){
// console.log(`[onBackdropClick]`, event);
if((event.target as HTMLElement).id === "linguabrowse-popup-backdrop"){
visible = false;
}
};
onMount(() => {
mounted = true;
return () => {
mounted = false;
window.removeEventListener('resize', resizeListener, true);
};
});
</script>
{#if backdropWidth > 0 && backdropHeight > 0 && inputModel}
<Popover
sourceRectWidth={sourceRect.width}
sourceRectHeight={sourceRect.height}
sourceRectX={sourceRect.x}
sourceRectY={sourceRect.y}
{backdropWidth}
{backdropHeight}
modalVisible={visible}
preferredWidth={loading ? 100 : preferredWidth}
preferredHeight={loading ? 100 : preferredHeight}
popoverMinimumLayoutMargins={{ top: 10, left: 10, right: 25, bottom: 10 }}
onBackdropClick={onBackdropClick}
bind:popoverWidth={popoverWidth}
bind:popoverHeight={popoverHeight}
bind:arrowDirection={arrowDirection}
>
{#if loading}
<div>
{#if loaderImgLoadingError}
<table linguabrowse-ignore="" style="width: 100px; height: 100px; text-align: center;">
<tr>
<td>Loading...</td>
</tr>
</table>
{:else}
<Loader onError={onloaderImgLoadingError}/>
{/if}
</div>
{:else}
<InputView
{...inputModel}
{onLemmaIndexSelected}
width={popoverWidth}
height={popoverHeight}
/>
{/if}
</Popover>
{/if}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment