Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
<script lang="ts"> | |
import { onMount, tick, createEventDispatcher } from "svelte"; | |
import { fade } from "svelte/transition"; | |
import { debounce, portalToBody } from "../../utils/ViewUtils.js"; | |
export let classNames = ""; | |
export let anchorElement: HTMLElement; | |
export let alignment: "start" | "center" | "end" | "auto" = "auto"; | |
export let placement: "top" | "bottom" | "auto" | "prefer-top" | "prefer-bottom" = "auto"; | |
export let waitForContent = false; | |
export let size: "sm" | "md" = "md"; | |
export let invertedColors = false; | |
export let touchOnly = false; | |
let popoverElement: HTMLDivElement; | |
/// sizes of the arrow and its padding, needed to position the popover position correctly | |
const ARROW_PADDING = 24; | |
const ARROW_SIZE = 10; | |
/// to prevent the toast from being too close to the edge of the screen | |
const HIT_ZONE_MARGIN = 80; | |
const dispatch = createEventDispatcher<{ close: void }>(); | |
let computedAlignment = alignment === "auto" ? "center" : alignment; | |
let computedPlacement = placement === "auto" ? "bottom" : placement; | |
let left: number; | |
let top: number; | |
let width: number; | |
let height: number; | |
let popoverShift: number; | |
let isTouchOnly = false; | |
let isActive = true; | |
function updatePlacement(anchorBbox: DOMRect, pageHeight: number) { | |
if (pageHeight > 0) { | |
if (placement === "auto") { | |
/// check if the anchor is closer to the top or bottom of the page | |
computedPlacement = anchorBbox.top > pageHeight / 2 ? "top" : "bottom"; | |
} else if (placement === "prefer-top") { | |
/// check if the toast has enough space to be placed above the anchor | |
const popoverHeight = popoverElement.getBoundingClientRect().height; | |
computedPlacement = anchorBbox.top > popoverHeight + HIT_ZONE_MARGIN ? "top" : "bottom"; | |
} else if (placement === "prefer-bottom") { | |
/// check if the toast has enough space to be placed below the anchor | |
const popoverHeight = popoverElement.getBoundingClientRect().height; | |
computedPlacement = | |
anchorBbox.top + anchorBbox.height + popoverHeight + HIT_ZONE_MARGIN > pageHeight ? "top" : "bottom"; | |
} | |
} | |
} | |
function updateAlignment(anchorBbox: DOMRect, pageWidth: number) { | |
if (alignment === "auto" && pageWidth > 0) { | |
const popoverWidth = popoverElement.getBoundingClientRect().width; | |
if (anchorBbox.left + popoverWidth > pageWidth - HIT_ZONE_MARGIN) { | |
computedAlignment = "end"; | |
} else { | |
computedAlignment = "start"; | |
} | |
} | |
} | |
async function updatePosition() { | |
if (anchorElement && !waitForContent) { | |
await tick(); | |
const bbox = anchorElement.getBoundingClientRect(); | |
updateAlignment(bbox, window.innerWidth); | |
updatePlacement(bbox, window.innerHeight); | |
left = bbox.left + window.scrollX; | |
top = bbox.top + window.scrollY; | |
width = bbox.width; | |
height = bbox.height; | |
/// shift the popover so the arrow is exaclty at the middle of the anchor | |
popoverShift = width / 2 - ARROW_SIZE / 2 - ARROW_PADDING; | |
} | |
} | |
const debouncedShow = debounce(() => (isActive = true), 250); | |
function hide() { | |
if (!popoverElement?.matches(":hover")) { | |
isActive = false; | |
} | |
} | |
const debouncedHide = debounce(hide, 250); | |
onMount(() => { | |
isTouchOnly = touchOnly && window.matchMedia("(any-hover: none)").matches; | |
if (!isTouchOnly) { | |
updatePosition(); | |
if (anchorElement) { | |
anchorElement.addEventListener("mouseover", debouncedShow); | |
anchorElement.addEventListener("mouseleave", debouncedHide); | |
return () => { | |
anchorElement.removeEventListener("mouseover", debouncedShow); | |
anchorElement.removeEventListener("mouseleave", debouncedHide); | |
}; | |
} | |
} | |
}); | |
</script> | |
<svelte:window on:resize={() => dispatch("close")} on:scroll={() => dispatch("close")} /> | |
<div class={isTouchOnly ? "hidden sm:contents" : "contents"} use:portalToBody> | |
<div | |
class="pointer-events-none absolute bg-transparent hidden" | |
class:hidden={!isActive} | |
style:top="{top}px" | |
style:left="{left}px" | |
style:width="{width}px" | |
style:height="{height}px" | |
> | |
<div | |
bind:this={popoverElement} | |
in:fade={{ duration: 100 }} | |
on:mouseleave={debouncedHide} | |
class="pointer-events-auto absolute z-10 transform | |
{computedPlacement === 'top' ? 'bottom-full -translate-y-3' : 'top-full translate-y-2.5'} | |
{computedAlignment === 'start' ? 'left-0' : computedAlignment === 'end' ? 'right-0' : 'left-1/2 -translate-x-1/2'} | |
{classNames}" | |
> | |
<div | |
class="absolute z-0 rotate-45 transform | |
{size === 'sm' ? 'h-2 w-2' : 'h-2.5 w-2.5 rounded-sm'} | |
{invertedColors ? 'bg-black dark:bg-gray-800' : 'border bg-white shadow dark:bg-gray-800'} | |
{computedPlacement === 'top' ? 'top-full -translate-y-1' : 'bottom-full translate-y-1'} | |
{computedAlignment === 'start' ? 'left-6' : computedAlignment === 'center' ? 'left-1/2' : 'right-6'}" | |
/> | |
<div | |
class="shadow-alternate-xl relative z-5 border font-normal leading-tight transition-opacity | |
{size === 'sm' ? 'rounded px-2 py-1.5' : 'rounded-xl p-4'} | |
{invertedColors ? 'border-black bg-black text-white dark:bg-gray-800' : 'bg-white text-black dark:bg-gray-925'} | |
" | |
> | |
<slot /> | |
</div> | |
</div> | |
</div> | |
</div> | |