| <script lang="ts"> |
| import { flyAndScale } from '$lib/utils/transitions'; |
| import { tick } from 'svelte'; |
| |
| |
| export let contentClass = |
| 'select-none rounded-2xl p-1 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-100 dark:border-gray-800'; |
| |
| |
| export let maxWidth = 200; |
| |
| |
| export let sideOffset = 8; |
| |
| let open = false; |
| let triggerEl; |
| let contentEl; |
| |
| function positionContent() { |
| if (!triggerEl || !contentEl) return; |
| const rect = triggerEl.getBoundingClientRect(); |
| |
| contentEl.style.position = 'fixed'; |
| contentEl.style.zIndex = '99999'; |
| |
| |
| contentEl.style.paddingLeft = '0'; |
| contentEl.style.paddingRight = '0'; |
| |
| |
| const innerContent = contentEl.firstElementChild; |
| const parentContainer = triggerEl.closest('[class*="rounded"]')?.parentElement; |
| if (parentContainer && innerContent) { |
| const parentWidth = parentContainer.offsetWidth; |
| if (parentWidth > 0) { |
| innerContent.style.minWidth = `${parentWidth}px`; |
| } |
| } |
| |
| |
| const contentWidth = innerContent?.offsetWidth || 200; |
| const rightSpace = window.innerWidth - rect.right; |
| |
| if (rightSpace >= contentWidth + sideOffset) { |
| |
| contentEl.style.left = `${rect.right}px`; |
| contentEl.style.right = 'auto'; |
| contentEl.style.paddingLeft = `${sideOffset}px`; |
| } else { |
| |
| contentEl.style.right = `${window.innerWidth - rect.left}px`; |
| contentEl.style.left = 'auto'; |
| contentEl.style.paddingRight = `${sideOffset}px`; |
| } |
| |
| |
| const contentHeight = contentEl.offsetHeight || 0; |
| let top = rect.top; |
| |
| |
| if (top + contentHeight + 16 > window.innerHeight) { |
| top = window.innerHeight - contentHeight - 16; |
| } |
| |
| |
| if (top < 16) { |
| top = 16; |
| } |
| |
| contentEl.style.top = `${top}px`; |
| } |
| |
| async function handleMouseEnter() { |
| open = true; |
| await tick(); |
| positionContent(); |
| |
| setTimeout(positionContent, 50); |
| } |
| |
| function handleMouseLeave(event) { |
| |
| if (contentEl?.contains(event.relatedTarget)) return; |
| if (triggerEl?.contains(event.relatedTarget)) return; |
| open = false; |
| } |
| |
| function handleContentMouseLeave(event) { |
| if (triggerEl?.contains(event.relatedTarget)) return; |
| if (contentEl?.contains(event.relatedTarget)) return; |
| open = false; |
| } |
| |
| function portal(node) { |
| document.body.appendChild(node); |
| return { |
| destroy() { |
| if (node.parentNode) { |
| node.parentNode.removeChild(node); |
| } |
| } |
| }; |
| } |
| </script> |
|
|
| <svelte:window on:scroll|capture={positionContent} on:resize={positionContent} /> |
|
|
| |
| <div |
| bind:this={triggerEl} |
| class="w-full" |
| on:mouseenter={handleMouseEnter} |
| on:mouseleave={handleMouseLeave} |
| > |
| <slot name="trigger" /> |
| </div> |
|
|
| {#if open} |
| |
| |
| <div use:portal bind:this={contentEl} on:mouseleave={handleContentMouseLeave}> |
| |
| <div class={contentClass} style="max-width: {maxWidth}px;" transition:flyAndScale> |
| <slot /> |
| </div> |
| </div> |
| {/if} |
|
|