ankiutils 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/BaseSelect.svelte +199 -0
- package/dist/components/BaseSelect.svelte.d.ts +15 -0
- package/dist/components/Dropdown.svelte +53 -0
- package/dist/components/Dropdown.svelte.d.ts +11 -0
- package/dist/components/KeyboardInput.svelte +43 -0
- package/dist/components/KeyboardInput.svelte.d.ts +6 -0
- package/dist/components/ModalSelector.svelte +91 -0
- package/dist/components/ModalSelector.svelte.d.ts +15 -0
- package/dist/components/MultiSelect.svelte +38 -0
- package/dist/components/MultiSelect.svelte.d.ts +14 -0
- package/dist/components/Select.svelte +43 -0
- package/dist/components/Select.svelte.d.ts +14 -0
- package/dist/components/SelectOptions.svelte +86 -0
- package/dist/components/SelectOptions.svelte.d.ts +20 -0
- package/dist/components/index.d.ts +8 -2
- package/dist/components/index.js +7 -2
- package/dist/style.css +4 -0
- package/package.json +7 -2
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import SelectOptions, { type SelectOption } from './SelectOptions.svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
id?: string;
|
|
7
|
+
options: SelectOption[];
|
|
8
|
+
selectedOptions: string[];
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
searchPlaceholder?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
clearable?: boolean;
|
|
13
|
+
multiple?: boolean;
|
|
14
|
+
onSelected?: (value: string[]) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
id,
|
|
19
|
+
options,
|
|
20
|
+
selectedOptions = $bindable<string[]>([]),
|
|
21
|
+
placeholder = 'Select an option...',
|
|
22
|
+
searchPlaceholder = 'Search...',
|
|
23
|
+
disabled = false,
|
|
24
|
+
clearable = false,
|
|
25
|
+
multiple = false,
|
|
26
|
+
onSelected = () => {}
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
let searchTerm = $state('');
|
|
30
|
+
let isOpen = $state(false);
|
|
31
|
+
let highlightedIndex = $state(-1);
|
|
32
|
+
|
|
33
|
+
let containerElement: HTMLDivElement;
|
|
34
|
+
let inputElement: HTMLInputElement;
|
|
35
|
+
let optionElements: (HTMLButtonElement | HTMLInputElement)[] = $state([]);
|
|
36
|
+
|
|
37
|
+
let filteredOptions = $derived(
|
|
38
|
+
searchTerm
|
|
39
|
+
? options.filter((option) => option.label.toLowerCase().includes(searchTerm.toLowerCase()))
|
|
40
|
+
: options
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
let displayValue = $derived(
|
|
44
|
+
isOpen
|
|
45
|
+
? searchTerm
|
|
46
|
+
: selectedOptions
|
|
47
|
+
.map((option) => options.find((o) => o.value === option)?.label)
|
|
48
|
+
.filter((o) => o?.trim())
|
|
49
|
+
.join(', ') || ''
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
let selectOptionsComponent: SelectOptions | undefined = $state();
|
|
53
|
+
|
|
54
|
+
async function onOpenDropdown() {
|
|
55
|
+
searchTerm = '';
|
|
56
|
+
await tick();
|
|
57
|
+
inputElement?.focus();
|
|
58
|
+
if (highlightedIndex >= 0) {
|
|
59
|
+
scrollHighlightedIntoView();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function onCloseDropdown() {
|
|
64
|
+
searchTerm = '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function clearSelection() {
|
|
68
|
+
if (multiple) {
|
|
69
|
+
selectedOptions = [];
|
|
70
|
+
} else {
|
|
71
|
+
selectedOptions = [''];
|
|
72
|
+
}
|
|
73
|
+
searchTerm = '';
|
|
74
|
+
inputElement?.focus();
|
|
75
|
+
onSelected(selectedOptions);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function scrollHighlightedIntoView() {
|
|
79
|
+
await tick();
|
|
80
|
+
if (highlightedIndex >= 0 && optionElements[highlightedIndex]) {
|
|
81
|
+
optionElements[highlightedIndex].scrollIntoView({
|
|
82
|
+
behavior: 'smooth',
|
|
83
|
+
block: 'nearest'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
89
|
+
if (!isOpen) {
|
|
90
|
+
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
selectOptionsComponent?.openDropdown();
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
switch (event.key) {
|
|
98
|
+
case 'Escape':
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
selectOptionsComponent?.closeDropdown();
|
|
101
|
+
break;
|
|
102
|
+
case 'ArrowDown':
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
highlightedIndex = Math.min(highlightedIndex + 1, filteredOptions.length - 1);
|
|
105
|
+
scrollHighlightedIntoView();
|
|
106
|
+
break;
|
|
107
|
+
case 'ArrowUp':
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
highlightedIndex = Math.max(highlightedIndex - 1, -1);
|
|
110
|
+
scrollHighlightedIntoView();
|
|
111
|
+
break;
|
|
112
|
+
case 'Enter':
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
|
|
115
|
+
selectOptionsComponent?.selectOption(filteredOptions[highlightedIndex]);
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
case 'Tab':
|
|
119
|
+
selectOptionsComponent?.closeDropdown();
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function handleClickOutside(event: MouseEvent) {
|
|
125
|
+
if (containerElement && !containerElement.contains(event.target as Node)) {
|
|
126
|
+
selectOptionsComponent?.closeDropdown();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$effect(() => {
|
|
131
|
+
if (isOpen) {
|
|
132
|
+
document.addEventListener('click', handleClickOutside);
|
|
133
|
+
return () => document.removeEventListener('click', handleClickOutside);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<div class="dropdown" bind:this={containerElement}>
|
|
139
|
+
<div class="flex gap-2">
|
|
140
|
+
<input
|
|
141
|
+
bind:this={inputElement}
|
|
142
|
+
{id}
|
|
143
|
+
class="input select-input"
|
|
144
|
+
type="text"
|
|
145
|
+
value={displayValue}
|
|
146
|
+
placeholder={isOpen ? searchPlaceholder : placeholder}
|
|
147
|
+
readonly={!isOpen}
|
|
148
|
+
{disabled}
|
|
149
|
+
oninput={(e) => {
|
|
150
|
+
if (isOpen) {
|
|
151
|
+
searchTerm = e.currentTarget.value;
|
|
152
|
+
highlightedIndex = -1;
|
|
153
|
+
optionElements = [];
|
|
154
|
+
}
|
|
155
|
+
}}
|
|
156
|
+
onclick={() => (isOpen = true)}
|
|
157
|
+
onkeydown={handleKeydown}
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
<div class="join gap-1" role="group">
|
|
161
|
+
{#if clearable && isOpen && selectedOptions.length > 0 && !disabled}
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
class="join-item btn btn-soft btn-primary clear-btn"
|
|
165
|
+
onclick={clearSelection}
|
|
166
|
+
aria-label="Clear selection"
|
|
167
|
+
>
|
|
168
|
+
<i class="bi bi-x"></i>
|
|
169
|
+
</button>
|
|
170
|
+
{/if}
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
class="join-item btn btn-soft btn-primary"
|
|
174
|
+
class:disabled
|
|
175
|
+
onclick={() => (isOpen = !isOpen)}
|
|
176
|
+
aria-label="Toggle dropdown"
|
|
177
|
+
>
|
|
178
|
+
<i class="bi bi-chevron-{isOpen ? 'up' : 'down'}"></i>
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{#if isOpen}
|
|
184
|
+
<SelectOptions
|
|
185
|
+
bind:this={selectOptionsComponent}
|
|
186
|
+
bind:isOpen
|
|
187
|
+
options={filteredOptions}
|
|
188
|
+
bind:selectedOptions
|
|
189
|
+
{multiple}
|
|
190
|
+
{onSelected}
|
|
191
|
+
{onOpenDropdown}
|
|
192
|
+
{onCloseDropdown}
|
|
193
|
+
/>
|
|
194
|
+
{/if}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<style>.select-input:focus, .select-input:focus-within {
|
|
198
|
+
outline: none;
|
|
199
|
+
}</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type SelectOption } from './SelectOptions.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
id?: string;
|
|
4
|
+
options: SelectOption[];
|
|
5
|
+
selectedOptions: string[];
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
searchPlaceholder?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
clearable?: boolean;
|
|
10
|
+
multiple?: boolean;
|
|
11
|
+
onSelected?: (value: string[]) => void;
|
|
12
|
+
}
|
|
13
|
+
declare const BaseSelect: import("svelte").Component<Props, {}, "selectedOptions">;
|
|
14
|
+
type BaseSelect = ReturnType<typeof BaseSelect>;
|
|
15
|
+
export default BaseSelect;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import SelectOptions, { type SelectOption } from './SelectOptions.svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
label: string;
|
|
6
|
+
options: SelectOption[];
|
|
7
|
+
selectedOptions: string[];
|
|
8
|
+
multiple?: boolean;
|
|
9
|
+
onSelected?: (value: string[]) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
label,
|
|
14
|
+
options,
|
|
15
|
+
selectedOptions = $bindable<string[]>([]),
|
|
16
|
+
multiple = false,
|
|
17
|
+
onSelected = () => {}
|
|
18
|
+
}: Props = $props();
|
|
19
|
+
|
|
20
|
+
let isOpen = $state(false);
|
|
21
|
+
let containerElement: HTMLDivElement;
|
|
22
|
+
let selectOptionsComponent: SelectOptions | undefined = $state();
|
|
23
|
+
|
|
24
|
+
function handleClickOutside(event: MouseEvent) {
|
|
25
|
+
if (containerElement && !containerElement.contains(event.target as Node)) {
|
|
26
|
+
selectOptionsComponent?.closeDropdown();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
$effect(() => {
|
|
31
|
+
if (isOpen) {
|
|
32
|
+
document.addEventListener('click', handleClickOutside);
|
|
33
|
+
return () => document.removeEventListener('click', handleClickOutside);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<div class="dropdown" bind:this={containerElement}>
|
|
39
|
+
<button class="btn" onclick={() => (isOpen = !isOpen)}>
|
|
40
|
+
<span>{label}</span>
|
|
41
|
+
<i class="bi bi-chevron-{isOpen ? 'up' : 'down'}"></i>
|
|
42
|
+
</button>
|
|
43
|
+
{#if isOpen}
|
|
44
|
+
<SelectOptions
|
|
45
|
+
bind:this={selectOptionsComponent}
|
|
46
|
+
{options}
|
|
47
|
+
bind:selectedOptions
|
|
48
|
+
{multiple}
|
|
49
|
+
{onSelected}
|
|
50
|
+
bind:isOpen
|
|
51
|
+
/>
|
|
52
|
+
{/if}
|
|
53
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SelectOption } from './SelectOptions.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
label: string;
|
|
4
|
+
options: SelectOption[];
|
|
5
|
+
selectedOptions: string[];
|
|
6
|
+
multiple?: boolean;
|
|
7
|
+
onSelected?: (value: string[]) => void;
|
|
8
|
+
}
|
|
9
|
+
declare const Dropdown: import("svelte").Component<Props, {}, "selectedOptions">;
|
|
10
|
+
type Dropdown = ReturnType<typeof Dropdown>;
|
|
11
|
+
export default Dropdown;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
keys: string[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
let { keys = $bindable([]) }: Props = $props();
|
|
7
|
+
let keysDisplay = $derived.by(() => {
|
|
8
|
+
let components = [];
|
|
9
|
+
for (let key of keys) {
|
|
10
|
+
if (key === ' ') {
|
|
11
|
+
key = 'space';
|
|
12
|
+
}
|
|
13
|
+
components.push(key[0].toUpperCase() + key.slice(1));
|
|
14
|
+
}
|
|
15
|
+
return components.join(',');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function onKeydown(event: Event) {
|
|
19
|
+
keys = [...keys, (event as KeyboardEvent).key];
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function clearKeys() {
|
|
24
|
+
keys = [];
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<div class="join">
|
|
29
|
+
<input
|
|
30
|
+
class="join-item input"
|
|
31
|
+
type="text"
|
|
32
|
+
value={keysDisplay}
|
|
33
|
+
onkeydown={onKeydown}
|
|
34
|
+
placeholder="Press shortcut"
|
|
35
|
+
/>
|
|
36
|
+
<button class="join-item btn btn-primary btn-soft" aria-label="Clear" onclick={clearKeys}>
|
|
37
|
+
<i class="bi bi-x"></i>
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<style>input:focus, input:focus-within {
|
|
42
|
+
outline: none;
|
|
43
|
+
}</style>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface Option {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
selectedOption: string;
|
|
9
|
+
options: Option[];
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
onSelected?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
selectedOption = $bindable(),
|
|
16
|
+
onSelected,
|
|
17
|
+
options,
|
|
18
|
+
placeholder = 'Search...'
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
let search = $state('');
|
|
21
|
+
let filteredOptions = $derived.by(() =>
|
|
22
|
+
search
|
|
23
|
+
? options.filter(
|
|
24
|
+
(opt) =>
|
|
25
|
+
opt.value.toLowerCase().includes(search.toLowerCase()) ||
|
|
26
|
+
opt.label.toLowerCase().includes(search.toLowerCase())
|
|
27
|
+
)
|
|
28
|
+
: options
|
|
29
|
+
);
|
|
30
|
+
let selectedIndex = $derived.by(() => {
|
|
31
|
+
for (let i = 0; i < filteredOptions.length; i++) {
|
|
32
|
+
if (isOptionSelected(filteredOptions[i])) return i;
|
|
33
|
+
}
|
|
34
|
+
return -1;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let inputElement: HTMLInputElement;
|
|
38
|
+
let modalElement: HTMLDialogElement;
|
|
39
|
+
let optionElements: HTMLButtonElement[] = $state([]);
|
|
40
|
+
|
|
41
|
+
function isOptionSelected(option: Option) {
|
|
42
|
+
return option.value.toLowerCase() === selectedOption.toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function selectOption(language: Option) {
|
|
46
|
+
selectedOption = language.value;
|
|
47
|
+
modalElement.close();
|
|
48
|
+
onSelected?.();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function show() {
|
|
52
|
+
modalElement.showModal();
|
|
53
|
+
inputElement.focus();
|
|
54
|
+
if (selectedIndex >= 0) {
|
|
55
|
+
optionElements[selectedIndex].scrollIntoView({
|
|
56
|
+
block: 'center'
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<dialog class="modal" aria-hidden="true" bind:this={modalElement}>
|
|
63
|
+
<div class="modal-box max-w-5xl pt-1">
|
|
64
|
+
<input
|
|
65
|
+
bind:this={inputElement}
|
|
66
|
+
class="search-input input w-full sticky mb-2 top-0"
|
|
67
|
+
type="text"
|
|
68
|
+
bind:value={search}
|
|
69
|
+
{placeholder}
|
|
70
|
+
/>
|
|
71
|
+
<div class="grid grid-cols-[repeat(auto-fill,minmax(25ch,1fr))] gap-2">
|
|
72
|
+
{#each filteredOptions as option, i (option.value)}
|
|
73
|
+
<button
|
|
74
|
+
bind:this={optionElements[i]}
|
|
75
|
+
class="btn"
|
|
76
|
+
class:btn-primary={isOptionSelected(option)}
|
|
77
|
+
onclick={() => selectOption(option)}
|
|
78
|
+
>
|
|
79
|
+
{option.label}
|
|
80
|
+
</button>
|
|
81
|
+
{/each}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<form method="dialog" class="modal-backdrop">
|
|
85
|
+
<button>close</button>
|
|
86
|
+
</form>
|
|
87
|
+
</dialog>
|
|
88
|
+
|
|
89
|
+
<style>.search-input:focus, .search-input:focus-within {
|
|
90
|
+
outline: none;
|
|
91
|
+
}</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface Option {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
interface Props {
|
|
6
|
+
selectedOption: string;
|
|
7
|
+
options: Option[];
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
onSelected?: () => void;
|
|
10
|
+
}
|
|
11
|
+
declare const ModalSelector: import("svelte").Component<Props, {
|
|
12
|
+
show: () => void;
|
|
13
|
+
}, "selectedOption">;
|
|
14
|
+
type ModalSelector = ReturnType<typeof ModalSelector>;
|
|
15
|
+
export default ModalSelector;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import BaseSelect from './BaseSelect.svelte';
|
|
3
|
+
import { type SelectOption } from './SelectOptions.svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
id?: string;
|
|
7
|
+
options: SelectOption[];
|
|
8
|
+
selectedOptions: string[];
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
searchPlaceholder?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
clearable?: boolean;
|
|
13
|
+
onSelected?: (options: string[]) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
id,
|
|
18
|
+
options,
|
|
19
|
+
selectedOptions = $bindable<string[]>([]),
|
|
20
|
+
placeholder,
|
|
21
|
+
searchPlaceholder,
|
|
22
|
+
disabled = false,
|
|
23
|
+
clearable = true,
|
|
24
|
+
onSelected = () => {}
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<BaseSelect
|
|
29
|
+
{id}
|
|
30
|
+
{options}
|
|
31
|
+
bind:selectedOptions
|
|
32
|
+
{placeholder}
|
|
33
|
+
{searchPlaceholder}
|
|
34
|
+
{disabled}
|
|
35
|
+
{clearable}
|
|
36
|
+
multiple={true}
|
|
37
|
+
{onSelected}
|
|
38
|
+
/>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SelectOption } from './SelectOptions.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
id?: string;
|
|
4
|
+
options: SelectOption[];
|
|
5
|
+
selectedOptions: string[];
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
searchPlaceholder?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
clearable?: boolean;
|
|
10
|
+
onSelected?: (options: string[]) => void;
|
|
11
|
+
}
|
|
12
|
+
declare const MultiSelect: import("svelte").Component<Props, {}, "selectedOptions">;
|
|
13
|
+
type MultiSelect = ReturnType<typeof MultiSelect>;
|
|
14
|
+
export default MultiSelect;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import BaseSelect from './BaseSelect.svelte';
|
|
3
|
+
import { type SelectOption } from './SelectOptions.svelte';
|
|
4
|
+
interface Props {
|
|
5
|
+
id?: string;
|
|
6
|
+
options: SelectOption[];
|
|
7
|
+
value?: string;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
searchPlaceholder?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
clearable?: boolean;
|
|
12
|
+
onSelected?: (value: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
id,
|
|
17
|
+
options,
|
|
18
|
+
value = $bindable(''),
|
|
19
|
+
placeholder,
|
|
20
|
+
searchPlaceholder,
|
|
21
|
+
disabled = false,
|
|
22
|
+
clearable = false,
|
|
23
|
+
onSelected = () => {}
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
let selectedOptions = $state<string[]>([value]);
|
|
27
|
+
|
|
28
|
+
$effect(() => {
|
|
29
|
+
value = selectedOptions[0] || '';
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<BaseSelect
|
|
34
|
+
{id}
|
|
35
|
+
{options}
|
|
36
|
+
bind:selectedOptions
|
|
37
|
+
{placeholder}
|
|
38
|
+
{searchPlaceholder}
|
|
39
|
+
{disabled}
|
|
40
|
+
{clearable}
|
|
41
|
+
multiple={false}
|
|
42
|
+
onSelected={(options) => onSelected(options[0])}
|
|
43
|
+
/>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SelectOption } from './SelectOptions.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
id?: string;
|
|
4
|
+
options: SelectOption[];
|
|
5
|
+
value?: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
searchPlaceholder?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
clearable?: boolean;
|
|
10
|
+
onSelected?: (value: string) => void;
|
|
11
|
+
}
|
|
12
|
+
declare const Select: import("svelte").Component<Props, {}, "value">;
|
|
13
|
+
type Select = ReturnType<typeof Select>;
|
|
14
|
+
export default Select;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface SelectOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
options: SelectOption[];
|
|
9
|
+
selectedOptions: string[];
|
|
10
|
+
multiple?: boolean;
|
|
11
|
+
isOpen?: boolean;
|
|
12
|
+
onSelected?: (value: string[]) => void;
|
|
13
|
+
onOpenDropdown?: () => void;
|
|
14
|
+
onCloseDropdown?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
options,
|
|
19
|
+
selectedOptions = $bindable<string[]>([]),
|
|
20
|
+
multiple = false,
|
|
21
|
+
isOpen = $bindable(false),
|
|
22
|
+
onSelected,
|
|
23
|
+
onOpenDropdown,
|
|
24
|
+
onCloseDropdown
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
let optionElements: (HTMLButtonElement | HTMLInputElement)[] = $state([]);
|
|
28
|
+
|
|
29
|
+
export async function openDropdown() {
|
|
30
|
+
isOpen = true;
|
|
31
|
+
optionElements = [];
|
|
32
|
+
onOpenDropdown?.();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function closeDropdown() {
|
|
36
|
+
isOpen = false;
|
|
37
|
+
optionElements = [];
|
|
38
|
+
onCloseDropdown?.();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function selectOption(option: SelectOption) {
|
|
42
|
+
if (multiple) {
|
|
43
|
+
if (selectedOptions.includes(option.value)) {
|
|
44
|
+
selectedOptions = selectedOptions.filter((o) => o !== option.value);
|
|
45
|
+
} else {
|
|
46
|
+
selectedOptions.push(option.value);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
selectedOptions = [option.value];
|
|
50
|
+
closeDropdown();
|
|
51
|
+
}
|
|
52
|
+
onSelected?.(selectedOptions);
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<div class="menu dropdown-content bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm gap-1">
|
|
57
|
+
{#if options.length === 0}
|
|
58
|
+
<div class="text-gray-400">No options found</div>
|
|
59
|
+
{:else}
|
|
60
|
+
{#each options as option, index (option.value)}
|
|
61
|
+
{@const checked = selectedOptions.includes(option.value)}
|
|
62
|
+
|
|
63
|
+
{#if multiple}
|
|
64
|
+
<input
|
|
65
|
+
bind:this={optionElements[index]}
|
|
66
|
+
type="checkbox"
|
|
67
|
+
class="btn"
|
|
68
|
+
class:btn-primary={checked}
|
|
69
|
+
{checked}
|
|
70
|
+
aria-label={option.label}
|
|
71
|
+
onclick={() => selectOption(option)}
|
|
72
|
+
/>
|
|
73
|
+
{:else}
|
|
74
|
+
<button
|
|
75
|
+
bind:this={optionElements[index]}
|
|
76
|
+
type="button"
|
|
77
|
+
class="btn"
|
|
78
|
+
class:btn-primary={checked}
|
|
79
|
+
onclick={() => selectOption(option)}
|
|
80
|
+
>
|
|
81
|
+
{option.label}
|
|
82
|
+
</button>
|
|
83
|
+
{/if}
|
|
84
|
+
{/each}
|
|
85
|
+
{/if}
|
|
86
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface SelectOption {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
interface Props {
|
|
6
|
+
options: SelectOption[];
|
|
7
|
+
selectedOptions: string[];
|
|
8
|
+
multiple?: boolean;
|
|
9
|
+
isOpen?: boolean;
|
|
10
|
+
onSelected?: (value: string[]) => void;
|
|
11
|
+
onOpenDropdown?: () => void;
|
|
12
|
+
onCloseDropdown?: () => void;
|
|
13
|
+
}
|
|
14
|
+
declare const SelectOptions: import("svelte").Component<Props, {
|
|
15
|
+
openDropdown: () => Promise<void>;
|
|
16
|
+
closeDropdown: () => void;
|
|
17
|
+
selectOption: (option: SelectOption) => void;
|
|
18
|
+
}, "selectedOptions" | "isOpen">;
|
|
19
|
+
type SelectOptions = ReturnType<typeof SelectOptions>;
|
|
20
|
+
export default SelectOptions;
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
import Spinner from
|
|
2
|
-
|
|
1
|
+
import Spinner from './Spinner.svelte';
|
|
2
|
+
import type { SelectOption } from './SelectOptions.svelte';
|
|
3
|
+
import Select from './Select.svelte';
|
|
4
|
+
import MultiSelect from './MultiSelect.svelte';
|
|
5
|
+
import Dropdown from './Dropdown.svelte';
|
|
6
|
+
import ModalSelector from './ModalSelector.svelte';
|
|
7
|
+
import KeyboardInput from './KeyboardInput.svelte';
|
|
8
|
+
export { Spinner, type SelectOption, Select, MultiSelect, Dropdown, ModalSelector, KeyboardInput };
|
package/dist/components/index.js
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
import Spinner from
|
|
2
|
-
|
|
1
|
+
import Spinner from './Spinner.svelte';
|
|
2
|
+
import Select from './Select.svelte';
|
|
3
|
+
import MultiSelect from './MultiSelect.svelte';
|
|
4
|
+
import Dropdown from './Dropdown.svelte';
|
|
5
|
+
import ModalSelector from './ModalSelector.svelte';
|
|
6
|
+
import KeyboardInput from './KeyboardInput.svelte';
|
|
7
|
+
export { Spinner, Select, MultiSelect, Dropdown, ModalSelector, KeyboardInput };
|
package/dist/style.css
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ankiutils",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"license": "AGPL-3.0-or-later",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev",
|
|
@@ -31,16 +31,20 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
+
"bootstrap-icons": "^1.13.1",
|
|
34
35
|
"svelte": "^5.0.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@eslint/compat": "^1.4.0",
|
|
38
39
|
"@eslint/js": "^9.39.1",
|
|
39
|
-
"@sveltejs/adapter-
|
|
40
|
+
"@sveltejs/adapter-static": "^3.0.10",
|
|
40
41
|
"@sveltejs/kit": "^2.49.1",
|
|
41
42
|
"@sveltejs/package": "^2.5.7",
|
|
42
43
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
44
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
43
45
|
"@types/node": "^24",
|
|
46
|
+
"bootstrap-icons": "^1.13.1",
|
|
47
|
+
"daisyui": "^5.5.14",
|
|
44
48
|
"eslint": "^9.39.1",
|
|
45
49
|
"eslint-config-prettier": "^10.1.8",
|
|
46
50
|
"eslint-plugin-svelte": "^3.13.1",
|
|
@@ -51,6 +55,7 @@
|
|
|
51
55
|
"sass": "^1.97.2",
|
|
52
56
|
"svelte": "^5.45.6",
|
|
53
57
|
"svelte-check": "^4.3.4",
|
|
58
|
+
"tailwindcss": "^4.1.18",
|
|
54
59
|
"typescript": "^5.9.3",
|
|
55
60
|
"typescript-eslint": "^8.48.1",
|
|
56
61
|
"vite": "^7.2.6"
|