@djcali570/component-lib 0.1.96 → 0.1.97
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/DropDown5.svelte +116 -100
- package/package.json +1 -1
package/dist/DropDown5.svelte
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { BROWSER } from 'esm-env';
|
|
3
|
-
import {
|
|
3
|
+
import { onMount } from 'svelte';
|
|
4
4
|
import { fly } from 'svelte/transition';
|
|
5
5
|
import type { DropDown5ColorScheme, DropDownItem } from './types.js';
|
|
6
6
|
|
|
7
|
+
// Props
|
|
7
8
|
let {
|
|
8
9
|
colorScheme: partialColorScheme = {},
|
|
9
10
|
name = 'dropdown',
|
|
10
11
|
title = 'Title',
|
|
11
|
-
value = $bindable(),
|
|
12
|
-
valueKey = $bindable(),
|
|
13
|
-
extraValue = $bindable(),
|
|
14
|
-
onUpdate = (
|
|
12
|
+
value = $bindable<any>(),
|
|
13
|
+
valueKey = $bindable<string | null | undefined>(),
|
|
14
|
+
extraValue = $bindable<string | null | undefined>(),
|
|
15
|
+
onUpdate = () => {},
|
|
15
16
|
disabled = false,
|
|
16
17
|
dropdownItems = [] as DropDownItem[]
|
|
17
18
|
}: {
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
dropdownItems?: DropDownItem[];
|
|
27
28
|
} = $props();
|
|
28
29
|
|
|
30
|
+
// Default color scheme
|
|
29
31
|
const defaultColorScheme: DropDown5ColorScheme = {
|
|
30
32
|
textColor: '#D6D6D6',
|
|
31
33
|
bgColor: '#121212',
|
|
@@ -38,112 +40,134 @@
|
|
|
38
40
|
itemHoverTextColor: '#121212'
|
|
39
41
|
};
|
|
40
42
|
|
|
41
|
-
// Merge partial
|
|
42
|
-
const colorScheme = { ...defaultColorScheme, ...partialColorScheme };
|
|
43
|
+
// Merge partial scheme with defaults
|
|
44
|
+
const colorScheme = $derived({ ...defaultColorScheme, ...partialColorScheme });
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
let
|
|
46
|
+
// Refs & state
|
|
47
|
+
let containerRef: HTMLDivElement | null = $state(null);
|
|
46
48
|
let dropdownRef: HTMLDivElement | null = $state(null);
|
|
49
|
+
let showDropdown = $state(false);
|
|
47
50
|
let filteredItems: DropDownItem[] = $state([]);
|
|
51
|
+
let highlightedIndex = $state(-1);
|
|
52
|
+
const id = generateRandomString();
|
|
48
53
|
|
|
54
|
+
// Effects
|
|
49
55
|
$effect(() => {
|
|
50
56
|
if (!BROWSER || !showDropdown || !dropdownRef) return;
|
|
57
|
+
|
|
51
58
|
setTimeout(() => {
|
|
52
59
|
if (!dropdownRef) return;
|
|
53
|
-
|
|
60
|
+
|
|
61
|
+
const rect = dropdownRef.getBoundingClientRect();
|
|
54
62
|
const viewportHeight = window.innerHeight;
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
|
|
64
|
+
if (rect.bottom > viewportHeight) {
|
|
65
|
+
const scrollY = window.scrollY + (rect.bottom - viewportHeight) + 10;
|
|
57
66
|
window.scrollTo({ top: scrollY, behavior: 'smooth' });
|
|
58
67
|
}
|
|
59
68
|
}, 0);
|
|
60
69
|
});
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
71
|
+
$effect(() => {
|
|
72
|
+
if (!showDropdown || !dropdownRef || highlightedIndex < 0) return;
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
document.removeEventListener('click', closeDropdown);
|
|
69
|
-
} catch (error) {}
|
|
74
|
+
const items = dropdownRef.querySelectorAll<HTMLButtonElement>('button');
|
|
75
|
+
items[highlightedIndex]?.scrollIntoView({ block: 'nearest' });
|
|
70
76
|
});
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const randomIndex = Math.floor(Math.random() * characters.length);
|
|
83
|
-
result += characters.charAt(randomIndex);
|
|
84
|
-
}
|
|
78
|
+
// Outside click
|
|
79
|
+
function setupOutsideClick() {
|
|
80
|
+
if (!BROWSER) return;
|
|
81
|
+
|
|
82
|
+
const handler = (event: Event) => {
|
|
83
|
+
if (!containerRef?.contains(event.target as Node)) {
|
|
84
|
+
showDropdown = false;
|
|
85
|
+
validateInput();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
document.addEventListener('pointerdown', handler);
|
|
90
|
+
return () => document.removeEventListener('pointerdown', handler);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onMount(() => setupOutsideClick());
|
|
94
|
+
|
|
95
|
+
// Helpers
|
|
96
|
+
function generateRandomString(length = 6) {
|
|
97
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
98
|
+
return Array.from({ length }, () =>
|
|
99
|
+
chars.charAt(Math.floor(Math.random() * chars.length))
|
|
100
|
+
).join('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleBlur() {
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
if (!showDropdown) validateInput();
|
|
106
|
+
}, 100);
|
|
87
107
|
}
|
|
88
108
|
|
|
89
|
-
/**
|
|
90
|
-
* Show the dropdown list
|
|
91
|
-
*/
|
|
92
109
|
function showDropdownList() {
|
|
93
110
|
showDropdown = true;
|
|
94
111
|
filteredItems = dropdownItems;
|
|
112
|
+
highlightedIndex = filteredItems.length > 0 ? 0 : -1;
|
|
95
113
|
}
|
|
96
114
|
|
|
97
|
-
function closeDropdown(event: Event) {
|
|
98
|
-
const target = event.target as HTMLElement;
|
|
99
|
-
|
|
100
|
-
if (target.id !== `dropdown-${id}`) {
|
|
101
|
-
showDropdown = false;
|
|
102
|
-
validateInput();
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
115
|
function filterItems() {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
const search = (value ?? '').toLowerCase();
|
|
117
|
+
filteredItems = dropdownItems.filter((item) => item.value.toLowerCase().includes(search));
|
|
118
|
+
highlightedIndex = filteredItems.length > 0 ? 0 : -1;
|
|
109
119
|
}
|
|
120
|
+
|
|
110
121
|
function selectItem(item: DropDownItem) {
|
|
111
|
-
// Set all bound values
|
|
112
122
|
value = item.value;
|
|
113
123
|
valueKey = item.id ?? '';
|
|
114
124
|
extraValue = item.extraValue ?? '';
|
|
115
|
-
|
|
116
|
-
// Notify parent
|
|
117
125
|
onUpdate(valueKey, value, extraValue);
|
|
118
|
-
|
|
119
|
-
// Close dropdown
|
|
120
126
|
showDropdown = false;
|
|
127
|
+
highlightedIndex = -1;
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
function validateInput() {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// Valid match: sync values
|
|
130
|
-
value = matchedItem.value;
|
|
131
|
-
valueKey = matchedItem.id ?? '';
|
|
132
|
-
extraValue = matchedItem.extraValue ?? '';
|
|
133
|
-
} else {
|
|
134
|
-
// No match: clear all
|
|
131
|
+
const search = (value ?? '').toLowerCase();
|
|
132
|
+
const match = dropdownItems.find((item) => item.value.toLowerCase() === search);
|
|
133
|
+
|
|
134
|
+
if (match) selectItem(match);
|
|
135
|
+
else {
|
|
135
136
|
value = '';
|
|
136
137
|
valueKey = '';
|
|
137
138
|
extraValue = '';
|
|
139
|
+
onUpdate(valueKey, value, extraValue);
|
|
138
140
|
}
|
|
141
|
+
}
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
144
|
+
if (!showDropdown) return;
|
|
145
|
+
|
|
146
|
+
if (e.key === 'ArrowDown') {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
highlightedIndex = Math.min(highlightedIndex + 1, filteredItems.length - 1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (e.key === 'ArrowUp') {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
highlightedIndex = Math.max(highlightedIndex - 1, 0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (e.key === 'Enter' && highlightedIndex >= 0) {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
selectItem(filteredItems[highlightedIndex]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (e.key === 'Escape') {
|
|
162
|
+
showDropdown = false;
|
|
163
|
+
highlightedIndex = -1;
|
|
164
|
+
}
|
|
142
165
|
}
|
|
143
166
|
</script>
|
|
144
167
|
|
|
145
168
|
<div
|
|
146
169
|
class="dropdown5__container"
|
|
170
|
+
bind:this={containerRef}
|
|
147
171
|
style="
|
|
148
172
|
--dropdown5__textColor: {colorScheme.textColor};
|
|
149
173
|
--dropdown5__mainBgColor: {colorScheme.bgColor};
|
|
@@ -166,9 +190,10 @@
|
|
|
166
190
|
aria-haspopup="listbox"
|
|
167
191
|
{name}
|
|
168
192
|
bind:value
|
|
193
|
+
onkeydown={handleKeydown}
|
|
169
194
|
oninput={filterItems}
|
|
170
195
|
onfocus={showDropdownList}
|
|
171
|
-
onblur={
|
|
196
|
+
onblur={handleBlur}
|
|
172
197
|
autocomplete="off"
|
|
173
198
|
{disabled}
|
|
174
199
|
/>
|
|
@@ -188,24 +213,22 @@
|
|
|
188
213
|
role="listbox"
|
|
189
214
|
aria-labelledby="dropdown-{id}-title"
|
|
190
215
|
bind:this={dropdownRef}
|
|
191
|
-
|
|
192
|
-
transition:fly={{
|
|
193
|
-
y: 10
|
|
194
|
-
}}
|
|
216
|
+
transition:fly={{ y: 10 }}
|
|
195
217
|
>
|
|
196
218
|
<div style="height:100%;">
|
|
197
219
|
{#each filteredItems as item, index}
|
|
198
220
|
<button
|
|
199
221
|
type="button"
|
|
200
222
|
class="dropdown5__list__item"
|
|
223
|
+
class:selected={index === highlightedIndex}
|
|
201
224
|
role="option"
|
|
202
225
|
aria-selected={item.value === value ? 'true' : 'false'}
|
|
203
|
-
|
|
226
|
+
onmousedown={() => selectItem(item)}
|
|
204
227
|
>
|
|
205
|
-
|
|
206
|
-
<div
|
|
207
|
-
<div style="
|
|
208
|
-
|
|
228
|
+
<div class="fic">
|
|
229
|
+
<div style="padding-right: 0.5rem;">
|
|
230
|
+
<div class="fca" style="width: 1rem; height: 1rem;">
|
|
231
|
+
{#if item.value === value}
|
|
209
232
|
<svg
|
|
210
233
|
viewBox="0 0 12 12"
|
|
211
234
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -213,36 +236,24 @@
|
|
|
213
236
|
role="presentation"
|
|
214
237
|
focusable="false"
|
|
215
238
|
style="display: block; height: 12px; width: 12px; fill: currentcolor;"
|
|
216
|
-
><path
|
|
217
|
-
d="m10.5 1.939 1.061 1.061-7.061 7.061-.53-.531-3-3-.531-.53 1.061-1.061 3 3 5.47-5.469z"
|
|
218
|
-
></path></svg
|
|
219
239
|
>
|
|
220
|
-
|
|
240
|
+
<path
|
|
241
|
+
d="m10.5 1.939 1.061 1.061-7.061 7.061-.53-.531-3-3-.531-.53 1.061-1.061 3 3 5.47-5.469z"
|
|
242
|
+
></path>
|
|
243
|
+
</svg>
|
|
244
|
+
{/if}
|
|
221
245
|
</div>
|
|
222
|
-
{#if item.component}
|
|
223
|
-
{@const Component = item.component}
|
|
224
|
-
<div class="component" style={item.componentStyles}>
|
|
225
|
-
<Component {...item.props} />
|
|
226
|
-
</div>
|
|
227
|
-
{/if}
|
|
228
|
-
<div>{item.value}</div>
|
|
229
246
|
</div>
|
|
230
|
-
{:else}
|
|
231
|
-
<div class="fic">
|
|
232
|
-
<div style="padding-right: 0.5rem;">
|
|
233
|
-
<div style="width: 1rem; height: 1rem;"></div>
|
|
234
|
-
</div>
|
|
235
247
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
248
|
+
{#if item.component}
|
|
249
|
+
{@const Component = item.component}
|
|
250
|
+
<div class="component" style={item.componentStyles}>
|
|
251
|
+
<Component {...item.props} />
|
|
252
|
+
</div>
|
|
253
|
+
{/if}
|
|
242
254
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
{/if}
|
|
255
|
+
<div>{item.value}</div>
|
|
256
|
+
</div>
|
|
246
257
|
</button>
|
|
247
258
|
{/each}
|
|
248
259
|
</div>
|
|
@@ -332,4 +343,9 @@
|
|
|
332
343
|
width: 2rem;
|
|
333
344
|
height: 2rem;
|
|
334
345
|
}
|
|
346
|
+
|
|
347
|
+
.selected {
|
|
348
|
+
background-color: var(--dropdown5__itemHoverBgColor);
|
|
349
|
+
color: var(--dropdown5__itemHoverTextColor);
|
|
350
|
+
}
|
|
335
351
|
</style>
|