Compare commits
No commits in common. "41668498ead206b1a8cbe5a9669710eded7d4276" and "5a1a20df6c3d9fbe57c53e83cff74af365e7026a" have entirely different histories.
41668498ea
...
5a1a20df6c
3 changed files with 10 additions and 200 deletions
|
|
@ -12,7 +12,6 @@
|
||||||
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";
|
||||||
|
|
@ -239,7 +238,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}
|
||||||
|
|
@ -260,20 +259,6 @@
|
||||||
<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" />
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,11 @@
|
||||||
let isAlertVisible = $state(false);
|
let isAlertVisible = $state(false);
|
||||||
let alertText = $state('');
|
let alertText = $state('');
|
||||||
|
|
||||||
|
// Derived state
|
||||||
|
let modalTitle = $derived(isEditing ? 'Редактирование точки' : 'Сохраненные точки');
|
||||||
|
|
||||||
// Table handler
|
// Table handler
|
||||||
let table = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 10 }));
|
let table = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 10 }));
|
||||||
let search = $derived(table.createSearch(['name']));
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
onChange();
|
onChange();
|
||||||
|
|
@ -124,29 +126,10 @@
|
||||||
|
|
||||||
<Modal {isOpen} toggle={closeModal} size="lg" fade={false} backdrop={true} scrollable>
|
<Modal {isOpen} toggle={closeModal} size="lg" fade={false} backdrop={true} scrollable>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Сохраненные точки</h5>
|
<h5 class="modal-title">{modalTitle}</h5>
|
||||||
<button type="button" class="btn-close" onclick={closeModal} aria-label="Close"></button>
|
<button type="button" class="btn-close" onclick={closeModal} aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<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">
|
<div bind:this={table.element} class="table-responsive">
|
||||||
<table class="table table-sm">
|
<table class="table table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -224,13 +207,16 @@
|
||||||
<span class="form-text">Метры над ур. моря</span>
|
<span class="form-text">Метры над ур. моря</span>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" color="success" size="sm">
|
<Button type="submit" color="success">
|
||||||
{isEditing ? 'Обновить точку' : 'Сохранить точку'}
|
{isEditing ? 'Обновить точку' : 'Сохранить точку'}
|
||||||
</Button>
|
</Button>
|
||||||
{#if isEditing}
|
{#if isEditing}
|
||||||
<Button size="sm" type="button" color="secondary" onclick={resetForm}>Отмена</Button>
|
<Button type="button" color="secondary" onclick={resetForm} class="ms-2">Отмена</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<Button color="secondary" onclick={closeModal}>Закрыть</Button>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
<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