Custom select

This commit is contained in:
ThePetrovich 2025-07-02 22:56:14 +08:00
parent 5a1a20df6c
commit 162bd0813f
2 changed files with 177 additions and 1 deletions

View file

@ -12,6 +12,7 @@
Icon, Icon,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import SelectSearchable from "$lib/components/SelectSearchable.svelte";
import { getForecast } from "$lib/prediction"; import { getForecast } from "$lib/prediction";
import type { FlightParameters, ProfileName, ProfileIdentifier } from "$lib/types"; import type { FlightParameters, ProfileName, ProfileIdentifier } from "$lib/types";
import { PROFILE_MAP, PROFILE_NAMES } from "$lib/types"; import { PROFILE_MAP, PROFILE_NAMES } from "$lib/types";
@ -238,7 +239,7 @@
<FormGroup spacing="mb-2"> <FormGroup spacing="mb-2">
<Label for="startPoint" class="form-label">Точка старта:</Label> <Label for="startPoint" class="form-label">Точка старта:</Label>
<InputGroup size="sm"> <InputGroup size="sm">
<Input <!-- <Input
type="select" type="select"
id="startPoint" id="startPoint"
bind:value={$FlightParametersStore.start_point} bind:value={$FlightParametersStore.start_point}
@ -259,6 +260,20 @@
<option value="Custom">Custom</option> <option value="Custom">Custom</option>
</optgroup> </optgroup>
</Input> </Input>
<Button color="secondary" title="Edit Saved Locations" onclick={handleClickPointListModal}>
<span>Редакт.</span>
<Icon name="journal-bookmark-fill" />
</Button> -->
<SelectSearchable
id="startPoint"
bind:selected={$FlightParametersStore.start_point}
options={$SavedPointsStore.map(point => ({
value: point.name,
label: point.name,
}))}
placeholder="Выберите точку старта"
searchPlaceholder="Поиск точки..."
/>
<Button color="secondary" title="Edit Saved Locations" onclick={handleClickPointListModal}> <Button color="secondary" title="Edit Saved Locations" onclick={handleClickPointListModal}>
<span>Редакт.</span> <span>Редакт.</span>
<Icon name="journal-bookmark-fill" /> <Icon name="journal-bookmark-fill" />

View file

@ -0,0 +1,161 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
interface Props extends HTMLAttributes<HTMLDivElement> {
options?: { value: any; label:string }[];
selected?: any;
placeholder?: string;
searchPlaceholder?: string;
disabled?: boolean;
class?: string;
}
let {
id = 'select-searchable',
options = [],
selected = $bindable(null),
placeholder = 'Select an option...',
searchPlaceholder = 'Search...',
disabled = false,
class: className = '',
...restProps
}: Props = $props();
const dispatch = createEventDispatcher<{ change: any }>();
let isOpen = $state(false);
let searchTerm = $state('');
let dropdownElement = $state<HTMLElement>();
let selectElement = $state<HTMLElement>();
let dropdownStyle = $state('');
let filteredOptions = $derived(
options.filter(option =>
option.label.toLowerCase().includes(searchTerm.toLowerCase())
)
);
let selectedLabel = $derived(
options.find(opt => opt.value === selected)?.label || ''
);
function updateDropdownPosition() {
if (!selectElement) return;
const rect = selectElement.getBoundingClientRect();
dropdownStyle = `
position: fixed;
top: ${rect.bottom}px;
left: ${rect.left}px;
min-width: ${rect.width}px;
`;
}
function toggleDropdown() {
if (!disabled) {
isOpen = !isOpen;
if (isOpen) {
searchTerm = '';
// Use next tick to ensure the element is rendered before getting its position
Promise.resolve().then(updateDropdownPosition);
}
}
}
function selectOption(option: { value: any; label: string }) {
selected = option.value;
isOpen = false;
searchTerm = '';
dispatch('change', selected);
}
function handleClickOutside(event: MouseEvent) {
if (selectElement && !selectElement.contains(event.target as Node)) {
isOpen = false;
}
}
$effect(() => {
if (isOpen) {
window.addEventListener('scroll', updateDropdownPosition, true);
window.addEventListener('resize', updateDropdownPosition);
}
return () => {
window.removeEventListener('scroll', updateDropdownPosition, true);
window.removeEventListener('resize', updateDropdownPosition);
};
});
</script>
<svelte:window onclick={handleClickOutside} />
<div
bind:this={selectElement}
{id}
class="form-control form-select select-container {className}"
class:disabled
class:show={isOpen}
onclick={toggleDropdown}
onkeydown={(e) => e.key === 'Enter' && toggleDropdown()}
role="combobox"
aria-haspopup="listbox"
aria-expanded={isOpen}
tabindex={disabled ? -1 : 0}
{...restProps}
>
<span class:text-muted={!selected}>{selectedLabel || placeholder}</span>
{#if isOpen}
<div class="dropdown-menu show" bind:this={dropdownElement} style={dropdownStyle}>
<div class="p-2">
<input
type="text"
class="form-control form-control-sm"
placeholder={searchPlaceholder}
bind:value={searchTerm}
onclick={(e) => e.stopPropagation()}
autofocus
/>
</div>
<div class="options-list">
{#each filteredOptions as option}
<button
type="button"
class="dropdown-item "
class:active={option.value === selected}
onclick={(e) => {
e.stopPropagation();
selectOption(option);
}}
onkeydown={(e) => e.key === 'Enter' && selectOption(option)}
role="option"
aria-selected={option.value === selected}
>
{option.label}
</button>
{/each}
{#if filteredOptions.length === 0}
<div class="dropdown-item text-muted disabled">No options found</div>
{/if}
</div>
</div>
{/if}
</div>
<style>
.select-container {
position: relative;
}
.dropdown-menu {
/* position is now set dynamically */
z-index: 1000;
width: max-content; /* Allow dropdown to grow with its content */
}
.options-list {
max-height: 40vh;
overflow-y: auto;
}
</style>