File size: 4,926 Bytes
36befc4
 
 
 
 
76d4920
 
 
36befc4
76d4920
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36befc4
76d4920
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36befc4
 
 
 
 
 
76d4920
 
 
 
36befc4
76d4920
 
 
36befc4
76d4920
36befc4
76d4920
36befc4
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
<!-- 
	In https://github.com/huggingface/huggingface.js/pull/567, this component was changed significantly from the original one in moon/Popover.svelte.
	Therefore, if/when widgets code goes back to moon, make sure not to delete this Popover.svelte right away before reconciling the differences.
 -->

<script lang="ts">
	import { onMount, tick, createEventDispatcher } from "svelte";
	import { fade } from "svelte/transition";
	import { 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;

	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;

	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) {
				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;
		}
	}

	function handleClickDocument(e: MouseEvent) {
		const targetElement = e.target as HTMLElement;
		if (![popoverElement, anchorElement].some((el) => el === targetElement || el?.contains(targetElement))) {
			e.preventDefault();
			e.stopPropagation();
			dispatch("close");
		}
	}

	onMount(() => {
		updatePosition();
	});
</script>

<svelte:window on:resize={() => dispatch("close")} on:scroll={() => dispatch("close")} on:click={handleClickDocument} />

<div class="contents" use:portalToBody>
	<div
		class="pointer-events-none absolute bg-transparent"
		style:top="{top}px"
		style:left="{left}px"
		style:width="{width}px"
		style:height="{height}px"
	>
		<div
			bind:this={popoverElement}
			in:fade={{ duration: 100 }}
			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>