Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
File size: 5,152 Bytes
76d4920 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
<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>
|