|
25 | 25 |
|
26 | 26 | <UpdatingDropdown bind:this={updatingDropdown} {taskResult} {isLoggedIn} {onupdate} /> |
27 | 27 | --> |
| 28 | +<script module lang="ts"> |
| 29 | + import { writable } from 'svelte/store'; |
| 30 | +
|
| 31 | + // Avoid multiple activation of dropdowns. |
| 32 | + const activeDropdownId = writable<string | null>(null); |
| 33 | +</script> |
| 34 | + |
28 | 35 | <script lang="ts"> |
29 | 36 | import { getStores } from '$app/stores'; |
30 | 37 | import { enhance } from '$app/forms'; |
| 38 | + import { onMount } from 'svelte'; |
| 39 | + import { browser } from '$app/environment'; |
31 | 40 |
|
32 | 41 | import { Dropdown, DropdownUl, DropdownLi, uiHelpers } from 'svelte-5-ui-lib'; |
33 | 42 | import Check from 'lucide-svelte/icons/check'; |
|
56 | 65 | let dropdownStatus = $state(false); |
57 | 66 | let closeDropdown = dropdown.close; |
58 | 67 |
|
59 | | - let dropdownX = $state(0); |
60 | | - let dropdownY = $state(0); |
61 | | - let isLowerHalfInScreen = $state(false); |
| 68 | + let dropdownPosition = $state({ x: 0, y: 0, isLower: false }); |
| 69 | +
|
| 70 | + const componentId = Math.random().toString(36).substring(2); |
| 71 | +
|
| 72 | + onMount(() => { |
| 73 | + const unsubscribe = activeDropdownId.subscribe((id) => { |
| 74 | + if (id && id !== componentId && dropdownStatus) { |
| 75 | + closeDropdown(); |
| 76 | + } |
| 77 | + }); |
| 78 | +
|
| 79 | + return unsubscribe; |
| 80 | + }); |
62 | 81 |
|
63 | 82 | $effect(() => { |
64 | 83 | activeUrl = $page.url.pathname; |
65 | 84 | dropdownStatus = dropdown.isOpen; |
66 | 85 |
|
67 | 86 | if (dropdownStatus) { |
68 | | - document.documentElement.style.setProperty('--dropdown-x', `${dropdownX}px`); |
69 | | - document.documentElement.style.setProperty('--dropdown-y', `${dropdownY}px`); |
| 87 | + document.documentElement.style.setProperty('--dropdown-x', `${dropdownPosition.x}px`); |
| 88 | + document.documentElement.style.setProperty('--dropdown-y', `${dropdownPosition.y}px`); |
70 | 89 | } |
71 | 90 | }); |
72 | 91 |
|
73 | 92 | export function toggle(event?: MouseEvent): void { |
74 | 93 | if (event) { |
| 94 | + event.stopPropagation(); |
75 | 95 | getDropdownPosition(event); |
76 | 96 | } |
77 | 97 |
|
| 98 | + activeDropdownId.set(componentId); |
78 | 99 | dropdown.toggle(); |
| 100 | +
|
| 101 | + // Note: Wait until the next event cycle before accepting a click event. |
| 102 | + if (!dropdownStatus) { |
| 103 | + setTimeout(() => {}, 0); |
| 104 | + } |
79 | 105 | } |
80 | 106 |
|
81 | 107 | function getDropdownPosition(event: MouseEvent): void { |
82 | 108 | const rect = (event.currentTarget as HTMLElement).getBoundingClientRect(); |
83 | 109 |
|
84 | | - dropdownX = rect.right; |
85 | | - dropdownY = rect.bottom; |
86 | | -
|
87 | | - isLowerHalfInScreen = rect.top > window.innerHeight / 2; |
| 110 | + dropdownPosition = { |
| 111 | + x: rect.right, |
| 112 | + y: rect.bottom, |
| 113 | + isLower: rect.top > window.innerHeight / 2, |
| 114 | + }; |
88 | 115 | } |
89 | 116 |
|
90 | 117 | function getDropdownClasses(isLower: boolean): string { |
|
208 | 235 | }; |
209 | 236 | return option; |
210 | 237 | }); |
| 238 | +
|
| 239 | + function manageDropdown(node: HTMLElement) { |
| 240 | + if (!browser) { |
| 241 | + return { |
| 242 | + destroy: () => {}, |
| 243 | + }; |
| 244 | + } |
| 245 | +
|
| 246 | + let justOpened = false; |
| 247 | +
|
| 248 | + const handleScroll = () => { |
| 249 | + if (dropdownStatus) { |
| 250 | + closeDropdown(); |
| 251 | + } |
| 252 | + }; |
| 253 | +
|
| 254 | + const handleWindowClick = (event: MouseEvent) => { |
| 255 | + if (justOpened) { |
| 256 | + justOpened = false; |
| 257 | + return; |
| 258 | + } |
| 259 | +
|
| 260 | + if (dropdownStatus && !node.contains(event.target as Node)) { |
| 261 | + closeDropdown(); |
| 262 | + } |
| 263 | + }; |
| 264 | +
|
| 265 | + window.addEventListener('scroll', handleScroll, { passive: true }); |
| 266 | + window.addEventListener('click', handleWindowClick); |
| 267 | +
|
| 268 | + return { |
| 269 | + update(newStatus: boolean) { |
| 270 | + if (newStatus && !dropdownStatus) { |
| 271 | + justOpened = true; |
| 272 | +
|
| 273 | + setTimeout(() => { |
| 274 | + justOpened = false; |
| 275 | + }, 0); |
| 276 | + } |
| 277 | +
|
| 278 | + dropdownStatus = newStatus; |
| 279 | + }, |
| 280 | +
|
| 281 | + destroy() { |
| 282 | + if (browser) { |
| 283 | + window.removeEventListener('scroll', handleScroll); |
| 284 | + window.removeEventListener('click', handleWindowClick); |
| 285 | + } |
| 286 | + }, |
| 287 | + }; |
| 288 | + } |
211 | 289 | </script> |
212 | 290 |
|
213 | | -<div class="fixed inset-0 pointer-events-none z-50 w-full h-full"> |
| 291 | +<div class="fixed inset-0 pointer-events-none z-50 w-full h-full" use:manageDropdown> |
214 | 292 | <Dropdown |
215 | 293 | {activeUrl} |
216 | 294 | {dropdownStatus} |
217 | 295 | {closeDropdown} |
218 | | - class={getDropdownClasses(isLowerHalfInScreen)} |
| 296 | + class={getDropdownClasses(dropdownPosition.isLower)} |
219 | 297 | > |
220 | 298 | <DropdownUl class="border rounded-lg shadow"> |
221 | 299 | {#if isLoggedIn} |
|
0 commit comments