Compare commits
2 commits
5a1a20df6c
...
41668498ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41668498ea | ||
|
|
162bd0813f |
3 changed files with 200 additions and 10 deletions
|
|
@ -12,6 +12,7 @@
|
|||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import SelectSearchable from "$lib/components/SelectSearchable.svelte";
|
||||
import { getForecast } from "$lib/prediction";
|
||||
import type { FlightParameters, ProfileName, ProfileIdentifier } from "$lib/types";
|
||||
import { PROFILE_MAP, PROFILE_NAMES } from "$lib/types";
|
||||
|
|
@ -238,7 +239,7 @@
|
|||
<FormGroup spacing="mb-2">
|
||||
<Label for="startPoint" class="form-label">Точка старта:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input
|
||||
<!-- <Input
|
||||
type="select"
|
||||
id="startPoint"
|
||||
bind:value={$FlightParametersStore.start_point}
|
||||
|
|
@ -259,6 +260,20 @@
|
|||
<option value="Custom">Custom</option>
|
||||
</optgroup>
|
||||
</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}>
|
||||
<span>Редакт.</span>
|
||||
<Icon name="journal-bookmark-fill" />
|
||||
|
|
|
|||
|
|
@ -27,11 +27,9 @@
|
|||
let isAlertVisible = $state(false);
|
||||
let alertText = $state('');
|
||||
|
||||
// Derived state
|
||||
let modalTitle = $derived(isEditing ? 'Редактирование точки' : 'Сохраненные точки');
|
||||
|
||||
// Table handler
|
||||
let table = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 10 }));
|
||||
let search = $derived(table.createSearch(['name']));
|
||||
|
||||
$effect(() => {
|
||||
onChange();
|
||||
|
|
@ -126,10 +124,29 @@
|
|||
|
||||
<Modal {isOpen} toggle={closeModal} size="lg" fade={false} backdrop={true} scrollable>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{modalTitle}</h5>
|
||||
<h5 class="modal-title">Сохраненные точки</h5>
|
||||
<button type="button" class="btn-close" onclick={closeModal} aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="position-relative mb-2">
|
||||
<Input
|
||||
type="text"
|
||||
class="form-control-sm pe-5"
|
||||
placeholder="Поиск по названию..."
|
||||
bind:value={search.value}
|
||||
oninput={() => search.set()}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
class="position-absolute top-50 end-0 translate-middle-y me-2 rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 16px; height: 16px; border: none; background: var(--bs-secondary); color: var(--bs-white);"
|
||||
onclick={() => { search.value = ''; search.set(); }}
|
||||
disabled={!search.value}
|
||||
>
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
</Button>
|
||||
</div>
|
||||
<div bind:this={table.element} class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
|
|
@ -207,16 +224,13 @@
|
|||
<span class="form-text">Метры над ур. моря</span>
|
||||
</FormGroup>
|
||||
</div>
|
||||
<Button type="submit" color="success">
|
||||
<Button type="submit" color="success" size="sm">
|
||||
{isEditing ? 'Обновить точку' : 'Сохранить точку'}
|
||||
</Button>
|
||||
{#if isEditing}
|
||||
<Button type="button" color="secondary" onclick={resetForm} class="ms-2">Отмена</Button>
|
||||
<Button size="sm" type="button" color="secondary" onclick={resetForm}>Отмена</Button>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<Button color="secondary" onclick={closeModal}>Закрыть</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
161
src/lib/components/SelectSearchable.svelte
Normal file
161
src/lib/components/SelectSearchable.svelte
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue