File size: 3,933 Bytes
0bd62e5 |
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 |
<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>
|