|
<script lang="ts"> |
|
import { fly } from "svelte/transition"; |
|
import { createEventDispatcher } from "svelte"; |
|
export let choices: [string, string | number][]; |
|
export let filtered_indices: number[]; |
|
export let show_options = false; |
|
export let disabled = false; |
|
export let selected_indices: (string | number)[] = []; |
|
export let active_index: number | null = null; |
|
|
|
let distance_from_top: number; |
|
let distance_from_bottom: number; |
|
let input_height: number; |
|
let input_width: number; |
|
let refElement: HTMLDivElement; |
|
let listElement: HTMLUListElement; |
|
let top: string | null, bottom: string | null, max_height: number; |
|
let innerHeight: number; |
|
|
|
function calculate_window_distance(): void { |
|
const { top: ref_top, bottom: ref_bottom } = |
|
refElement.getBoundingClientRect(); |
|
distance_from_top = ref_top; |
|
distance_from_bottom = innerHeight - ref_bottom; |
|
} |
|
|
|
let scroll_timeout: NodeJS.Timeout | null = null; |
|
function scroll_listener(): void { |
|
if (!show_options) return; |
|
if (scroll_timeout !== null) { |
|
clearTimeout(scroll_timeout); |
|
} |
|
|
|
scroll_timeout = setTimeout(() => { |
|
calculate_window_distance(); |
|
scroll_timeout = null; |
|
}, 10); |
|
} |
|
|
|
$: { |
|
if (show_options && refElement) { |
|
if (listElement && selected_indices.length > 0) { |
|
let elements = listElement.querySelectorAll("li"); |
|
for (const element of Array.from(elements)) { |
|
if ( |
|
element.getAttribute("data-index") === |
|
selected_indices[0].toString() |
|
) { |
|
listElement?.scrollTo?.(0, (element as HTMLLIElement).offsetTop); |
|
break; |
|
} |
|
} |
|
} |
|
calculate_window_distance(); |
|
const rect = refElement.parentElement?.getBoundingClientRect(); |
|
input_height = rect?.height || 0; |
|
input_width = rect?.width || 0; |
|
} |
|
if (distance_from_bottom > distance_from_top) { |
|
top = `${distance_from_top}px`; |
|
max_height = distance_from_bottom; |
|
bottom = null; |
|
} else { |
|
bottom = `${distance_from_bottom + input_height}px`; |
|
max_height = distance_from_top - input_height; |
|
top = null; |
|
} |
|
} |
|
|
|
const dispatch = createEventDispatcher(); |
|
</script> |
|
|
|
<svelte:window on:scroll={scroll_listener} bind:innerHeight /> |
|
|
|
<div class="reference" bind:this={refElement} /> |
|
{#if show_options && !disabled} |
|
<ul |
|
class="options" |
|
transition:fly={{ duration: 200, y: 5 }} |
|
on:mousedown|preventDefault={(e) => dispatch("change", e)} |
|
style:top |
|
style:bottom |
|
style:max-height={`calc(${max_height}px - var(--window-padding))`} |
|
style:width={input_width + "px"} |
|
bind:this={listElement} |
|
role="listbox" |
|
> |
|
{#each filtered_indices as index} |
|
<li |
|
class="item" |
|
class:selected={selected_indices.includes(index)} |
|
class:active={index === active_index} |
|
class:bg-gray-100={index === active_index} |
|
class:dark:bg-gray-600={index === active_index} |
|
style:width={input_width + "px"} |
|
data-index={index} |
|
aria-label={choices[index][0]} |
|
data-testid="dropdown-option" |
|
role="option" |
|
aria-selected={selected_indices.includes(index)} |
|
> |
|
<span class:hide={!selected_indices.includes(index)} class="inner-item"> |
|
✓ |
|
</span> |
|
{choices[index][0]} |
|
</li> |
|
{/each} |
|
</ul> |
|
{/if} |
|
|
|
<style> |
|
.options { |
|
--window-padding: var(--size-8); |
|
position: fixed; |
|
z-index: var(--layer-top); |
|
margin-left: 0; |
|
box-shadow: var(--shadow-drop-lg); |
|
border-radius: var(--container-radius); |
|
background: var(--background-fill-primary); |
|
min-width: fit-content; |
|
max-width: inherit; |
|
overflow: auto; |
|
color: var(--body-text-color); |
|
list-style: none; |
|
} |
|
|
|
.item { |
|
display: flex; |
|
cursor: pointer; |
|
padding: var(--size-2); |
|
word-break: break-word; |
|
} |
|
|
|
.item:hover, |
|
.active { |
|
background: var(--background-fill-secondary); |
|
} |
|
|
|
.inner-item { |
|
padding-right: var(--size-1); |
|
} |
|
|
|
.hide { |
|
visibility: hidden; |
|
} |
|
</style> |
|
|