Last active
April 21, 2022 20:47
-
-
Save shirakaba/5ae47e45752539cfa95f5f2ee922ac2e to your computer and use it in GitHub Desktop.
Scroll-trapping view (modal overlay)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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