@rkosafo/cai.components 0.0.78 → 0.0.80
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/README.md +8 -8
- package/dist/baseEditor/index.svelte +32 -32
- package/dist/builders/filters/FilterBuilder.svelte +641 -641
- package/dist/forms/FormCheckbox/FormCheckbox.svelte +53 -53
- package/dist/forms/FormClEditor/ClEdito.svelte +68 -68
- package/dist/forms/FormDatepicker/FormDatepicker.svelte +159 -159
- package/dist/forms/FormFileUpload/FormFileUplad.svelte +134 -134
- package/dist/forms/FormInput/FormInput.svelte +87 -87
- package/dist/forms/FormRadio/FormRadio.svelte +53 -53
- package/dist/forms/FormSelect/FormSelect.svelte +88 -88
- package/dist/forms/FormTextarea/FormTextarea.svelte +78 -78
- package/dist/forms/button-toggle/ButtonToggle.svelte +119 -119
- package/dist/forms/button-toggle/CheckIcon.svelte +28 -28
- package/dist/forms/checkbox/Checkbox.svelte +82 -82
- package/dist/forms/checkbox/CheckboxButton.svelte +92 -92
- package/dist/forms/datepicker/Datepicker.svelte +707 -707
- package/dist/forms/form/Form.svelte +69 -69
- package/dist/forms/input/Input.svelte +363 -363
- package/dist/forms/label/Label.svelte +38 -38
- package/dist/forms/radio/Radio.svelte +48 -48
- package/dist/forms/radio/RadioButton.svelte +22 -22
- package/dist/forms/select/Select.svelte +56 -56
- package/dist/forms/textarea/Textarea.svelte +165 -165
- package/dist/forms/toggle/Toggle.svelte +70 -70
- package/dist/layout/Chat/CategorySelector.svelte +52 -52
- package/dist/layout/Chat/ChatEntry.svelte +246 -246
- package/dist/layout/Chat/ChatEntrySkeleton.svelte +81 -81
- package/dist/layout/Chat/ChatHeader.svelte +172 -172
- package/dist/layout/Chat/ChatInput.svelte +207 -207
- package/dist/layout/Chat/DraggableWindow.svelte +230 -230
- package/dist/layout/Chat/PreviewPage.svelte +182 -182
- package/dist/layout/Chat/RichText.svelte +216 -216
- package/dist/layout/ComponentCanvas/Canvas.svelte +40 -40
- package/dist/layout/ComponentCanvas/ComponentRenderer.svelte +85 -85
- package/dist/layout/TF/Content/Content.svelte +21 -21
- package/dist/layout/TF/Header/Header.svelte +166 -166
- package/dist/layout/TF/Sidebar/Sidebar.svelte +148 -148
- package/dist/layout/TF/Wrapper/Wrapper.svelte +17 -17
- package/dist/layout/mailing/MailPaginator.svelte +36 -36
- package/dist/layout/mailing/MailSidebar.svelte +39 -39
- package/dist/layout/mailing/MailToolBar.svelte +174 -174
- package/dist/layout/mailing/MailingContent.svelte +10 -10
- package/dist/layout/mailing/MailingHeader.svelte +55 -55
- package/dist/layout/mailing/MailingMessageCard.svelte +112 -112
- package/dist/layout/mailing/MailingMessageViewer.svelte +87 -87
- package/dist/layout/mailing/MailingModule.svelte +448 -448
- package/dist/styles/docs.css +615 -615
- package/dist/styles/tf-layout.css +185 -185
- package/dist/themes/ThemeProvider.svelte +20 -20
- package/dist/types/index.d.ts +2 -0
- package/dist/typography/heading/Heading.svelte +35 -35
- package/dist/ui/accordion/Accordion.svelte +49 -49
- package/dist/ui/accordion/AccordionItem.svelte +173 -173
- package/dist/ui/alert/Alert.svelte +83 -83
- package/dist/ui/alertDialog/AlertDialog.svelte +40 -40
- package/dist/ui/avatar/Avatar.svelte +77 -77
- package/dist/ui/box/Box.svelte +28 -28
- package/dist/ui/breadcrumb/Breadcrumb.svelte +39 -39
- package/dist/ui/buttons/ActionButton.svelte +234 -234
- package/dist/ui/buttons/Button.svelte +102 -102
- package/dist/ui/buttons/GradientButton.svelte +59 -59
- package/dist/ui/datatable/Datatable.svelte +525 -525
- package/dist/ui/drawer/Drawer.svelte +300 -300
- package/dist/ui/dropdown/Dropdown.svelte +36 -36
- package/dist/ui/dropdown/DropdownDivider.svelte +11 -11
- package/dist/ui/dropdown/DropdownGroup.svelte +14 -14
- package/dist/ui/dropdown/DropdownHeader.svelte +14 -14
- package/dist/ui/dropdown/DropdownItem.svelte +52 -52
- package/dist/ui/footer/Footer.svelte +15 -15
- package/dist/ui/footer/FooterBrand.svelte +37 -37
- package/dist/ui/footer/FooterCopyright.svelte +45 -45
- package/dist/ui/footer/FooterIcon.svelte +22 -22
- package/dist/ui/footer/FooterLink.svelte +33 -33
- package/dist/ui/footer/FooterLinkGroup.svelte +13 -13
- package/dist/ui/icons/IconifyIcon.svelte +7 -7
- package/dist/ui/indicator/Indicator.svelte +42 -42
- package/dist/ui/modal/Modal.svelte +265 -265
- package/dist/ui/notificationList/NotificationList.svelte +123 -123
- package/dist/ui/pageLoader/PageLoader.svelte +14 -14
- package/dist/ui/pageLoader/PageLoader2.svelte +99 -0
- package/dist/ui/pageLoader/PageLoader2.svelte.d.ts +24 -0
- package/dist/ui/pageLoader/index.d.ts +2 -1
- package/dist/ui/pageLoader/index.js +2 -1
- package/dist/ui/paginate/Paginate.svelte +96 -96
- package/dist/ui/speedDial/SpeedDial.svelte +77 -77
- package/dist/ui/speedDial/SpeedDialButton.svelte +75 -75
- package/dist/ui/speedDial/SpeedDialTrigger.svelte +79 -79
- package/dist/ui/tab/Tab.svelte +93 -67
- package/dist/ui/table/Table.svelte +396 -396
- package/dist/ui/tableLoader/TableLoader.svelte +24 -24
- package/dist/ui/toast/Toast.svelte +337 -337
- package/dist/ui/toast/Toast.svelte.d.ts +10 -10
- package/dist/ui/toolbar/Toolbar.svelte +59 -59
- package/dist/ui/toolbar/ToolbarButton.svelte +56 -56
- package/dist/ui/toolbar/ToolbarGroup.svelte +43 -43
- package/dist/ui/tooltip/Tooltip.svelte +51 -51
- package/dist/utils/Popper.svelte +257 -257
- package/dist/utils/closeButton/CloseButton.svelte +88 -88
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +3 -3
- package/dist/utils/singleSelection.svelte.js +48 -48
- package/dist/youtube/index.svelte +12 -12
- package/package.json +1 -1
|
@@ -1,363 +1,363 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { getContext } from 'svelte';
|
|
3
|
-
import clsx from 'clsx';
|
|
4
|
-
import { input, clampSize } from './index.js';
|
|
5
|
-
import { getTheme, warnThemeDeprecation } from '../../themes/themeUtils.js';
|
|
6
|
-
import { CloseButton, type InputProps, type InputValue, type SizeType } from '../../index.js';
|
|
7
|
-
import { createDismissableContext } from '../../utils/dismissable.js';
|
|
8
|
-
|
|
9
|
-
let {
|
|
10
|
-
children,
|
|
11
|
-
left,
|
|
12
|
-
right,
|
|
13
|
-
value = $bindable(),
|
|
14
|
-
elementRef = $bindable(),
|
|
15
|
-
clearable = false,
|
|
16
|
-
size,
|
|
17
|
-
color = 'default',
|
|
18
|
-
class: className,
|
|
19
|
-
classes,
|
|
20
|
-
wrapperClass,
|
|
21
|
-
leftClass,
|
|
22
|
-
rightClass,
|
|
23
|
-
divClass,
|
|
24
|
-
clearableSvgClass,
|
|
25
|
-
clearableColor = 'none',
|
|
26
|
-
clearableClass,
|
|
27
|
-
clearableOnClick,
|
|
28
|
-
data = [],
|
|
29
|
-
maxSuggestions = 5,
|
|
30
|
-
onSelect,
|
|
31
|
-
comboClass,
|
|
32
|
-
comboItemClass,
|
|
33
|
-
oninput,
|
|
34
|
-
onfocus,
|
|
35
|
-
onblur,
|
|
36
|
-
onkeydown,
|
|
37
|
-
...restProps
|
|
38
|
-
}: InputProps<InputValue> = $props();
|
|
39
|
-
|
|
40
|
-
// input, left, right, close, combo, comboItem, div, svg
|
|
41
|
-
warnThemeDeprecation(
|
|
42
|
-
'Input',
|
|
43
|
-
{
|
|
44
|
-
wrapperClass,
|
|
45
|
-
leftClass,
|
|
46
|
-
rightClass,
|
|
47
|
-
divClass,
|
|
48
|
-
clearableSvgClass,
|
|
49
|
-
clearableClass,
|
|
50
|
-
comboClass
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
wrapperClass: 'wrapper',
|
|
54
|
-
leftClass: 'left',
|
|
55
|
-
rightClass: 'right',
|
|
56
|
-
divClass: 'div',
|
|
57
|
-
clearableSvgClass: 'svg',
|
|
58
|
-
clearableClass: 'close',
|
|
59
|
-
comboClass: 'comboItem'
|
|
60
|
-
}
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const styling = $derived(
|
|
64
|
-
classes ?? {
|
|
65
|
-
wrapper: wrapperClass,
|
|
66
|
-
left: leftClass,
|
|
67
|
-
right: rightClass,
|
|
68
|
-
div: divClass,
|
|
69
|
-
svg: clearableSvgClass,
|
|
70
|
-
close: clearableClass,
|
|
71
|
-
combo: comboClass,
|
|
72
|
-
comboItem: comboItemClass
|
|
73
|
-
}
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const theme = getTheme('input');
|
|
77
|
-
|
|
78
|
-
// onSelect is a custom combobox selection handler that takes a string
|
|
79
|
-
// standard DOM events, onInput, onFocus, onBlur, onKeydown will be deprecated in the next minor version
|
|
80
|
-
const resolvedOnInput = $derived(oninput);
|
|
81
|
-
const resolvedOnFocus = $derived(onfocus);
|
|
82
|
-
const resolvedOnBlur = $derived(onblur);
|
|
83
|
-
const resolvedOnKeydown = $derived(onkeydown);
|
|
84
|
-
|
|
85
|
-
// Automatically enable combobox when data is provided
|
|
86
|
-
const isCombobox = $derived(Array.isArray(data) && data.length > 0);
|
|
87
|
-
|
|
88
|
-
// tinted if put in component having its own background
|
|
89
|
-
let background: boolean = getContext('background');
|
|
90
|
-
|
|
91
|
-
// svelte-ignore non_reactive_update
|
|
92
|
-
let dummyFocusDiv: HTMLDivElement;
|
|
93
|
-
|
|
94
|
-
let group: { size: SizeType } = getContext('group');
|
|
95
|
-
let isGroup = !!group;
|
|
96
|
-
let _size = $derived(size || clampSize(group?.size) || 'md');
|
|
97
|
-
const _color = $derived(color === 'default' && background ? 'tinted' : color);
|
|
98
|
-
|
|
99
|
-
const {
|
|
100
|
-
base,
|
|
101
|
-
input: inputCls,
|
|
102
|
-
left: leftCls,
|
|
103
|
-
right: rightCls,
|
|
104
|
-
close,
|
|
105
|
-
combo,
|
|
106
|
-
comboItem
|
|
107
|
-
} = $derived(input({ size: _size, color: _color, grouped: isGroup }));
|
|
108
|
-
|
|
109
|
-
const clearAll = () => {
|
|
110
|
-
if (elementRef) {
|
|
111
|
-
// in order to avoid type error in setTimeout()
|
|
112
|
-
const input = elementRef;
|
|
113
|
-
input.value = '';
|
|
114
|
-
value = '';
|
|
115
|
-
|
|
116
|
-
backspaceUsed = false;
|
|
117
|
-
updateSuggestions();
|
|
118
|
-
// hack to focus outside
|
|
119
|
-
dummyFocusDiv?.focus();
|
|
120
|
-
setTimeout(() => {
|
|
121
|
-
input.focus();
|
|
122
|
-
}, 100);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (clearableOnClick) clearableOnClick();
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
createDismissableContext(clearAll);
|
|
129
|
-
|
|
130
|
-
// Combobox functionality
|
|
131
|
-
let isFocused = $state(false);
|
|
132
|
-
let filteredSuggestions: string[] = $state([]);
|
|
133
|
-
let selectedIndex = $state(-1);
|
|
134
|
-
let backspaceUsed = $state(false); // Track if backspace was used to clear
|
|
135
|
-
|
|
136
|
-
function updateSuggestions() {
|
|
137
|
-
if (!isCombobox || !isFocused) {
|
|
138
|
-
filteredSuggestions = [];
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const fullSearchTerm = ((value as string) || '').toLowerCase();
|
|
143
|
-
const lastSpaceIndex = fullSearchTerm.lastIndexOf(' ');
|
|
144
|
-
const searchTerm =
|
|
145
|
-
lastSpaceIndex === -1 ? fullSearchTerm : fullSearchTerm.substring(lastSpaceIndex + 1);
|
|
146
|
-
|
|
147
|
-
// Show suggestions if:
|
|
148
|
-
// 1. There's actual input text, OR
|
|
149
|
-
// 2. The input is empty but backspace was just used to clear it
|
|
150
|
-
if (searchTerm === '' && !backspaceUsed) {
|
|
151
|
-
filteredSuggestions = [];
|
|
152
|
-
} else {
|
|
153
|
-
// If there's text, filter suggestions
|
|
154
|
-
if (searchTerm) {
|
|
155
|
-
filteredSuggestions = data
|
|
156
|
-
.filter((item) => item.toLowerCase().includes(searchTerm))
|
|
157
|
-
.slice(0, maxSuggestions);
|
|
158
|
-
}
|
|
159
|
-
// If empty but backspace was used, show all suggestions
|
|
160
|
-
else if (backspaceUsed) {
|
|
161
|
-
filteredSuggestions = [...data].slice(0, maxSuggestions);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
selectedIndex = -1;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Watch for value changes
|
|
169
|
-
$effect(() => {
|
|
170
|
-
if (isCombobox) {
|
|
171
|
-
updateSuggestions();
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
function defaultHandleInput(event: Event) {
|
|
176
|
-
// Ensure value is treated as a string to safely check its length
|
|
177
|
-
const currentValueAsString = String(value || '');
|
|
178
|
-
if (currentValueAsString.length > 0) {
|
|
179
|
-
backspaceUsed = false;
|
|
180
|
-
}
|
|
181
|
-
updateSuggestions();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function defaultHandleFocus(event: FocusEvent) {
|
|
185
|
-
isFocused = true;
|
|
186
|
-
updateSuggestions();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function defaultHandleBlur(event: FocusEvent) {
|
|
190
|
-
// Small delay to allow click on suggestion to fire first
|
|
191
|
-
setTimeout(() => {
|
|
192
|
-
isFocused = false;
|
|
193
|
-
backspaceUsed = false; // Reset flag when focus is lost
|
|
194
|
-
filteredSuggestions = [];
|
|
195
|
-
}, 200);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function defaultHandleKeydown(event: KeyboardEvent) {
|
|
199
|
-
if (!isCombobox) return;
|
|
200
|
-
|
|
201
|
-
// Special handling for backspace/delete - track when it's used to clear the input
|
|
202
|
-
if (event.key === 'Backspace' || event.key === 'Delete') {
|
|
203
|
-
const currentValue = value as string;
|
|
204
|
-
// If this keypress will make the input empty
|
|
205
|
-
if (currentValue.length <= 1) {
|
|
206
|
-
backspaceUsed = true;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (!filteredSuggestions.length) return;
|
|
211
|
-
|
|
212
|
-
switch (event.key) {
|
|
213
|
-
case 'ArrowDown':
|
|
214
|
-
event.preventDefault();
|
|
215
|
-
selectedIndex = (selectedIndex + 1) % filteredSuggestions.length;
|
|
216
|
-
break;
|
|
217
|
-
case 'ArrowUp':
|
|
218
|
-
event.preventDefault();
|
|
219
|
-
selectedIndex = selectedIndex <= 0 ? filteredSuggestions.length - 1 : selectedIndex - 1;
|
|
220
|
-
break;
|
|
221
|
-
case 'Enter':
|
|
222
|
-
if (selectedIndex >= 0) {
|
|
223
|
-
event.preventDefault();
|
|
224
|
-
selectItem(filteredSuggestions[selectedIndex]);
|
|
225
|
-
}
|
|
226
|
-
break;
|
|
227
|
-
case 'Escape':
|
|
228
|
-
event.preventDefault();
|
|
229
|
-
filteredSuggestions = [];
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Combined event handlers that call custom handlers first, then default behavior
|
|
235
|
-
function handleInput(event: Event) {
|
|
236
|
-
if (resolvedOnInput) {
|
|
237
|
-
resolvedOnInput(event);
|
|
238
|
-
}
|
|
239
|
-
defaultHandleInput(event);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function handleFocus(event: FocusEvent) {
|
|
243
|
-
if (resolvedOnFocus) {
|
|
244
|
-
resolvedOnFocus(event);
|
|
245
|
-
}
|
|
246
|
-
defaultHandleFocus(event);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function handleBlur(event: FocusEvent) {
|
|
250
|
-
if (resolvedOnBlur) {
|
|
251
|
-
resolvedOnBlur(event);
|
|
252
|
-
}
|
|
253
|
-
defaultHandleBlur(event);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function handleKeydown(event: KeyboardEvent) {
|
|
257
|
-
if (resolvedOnKeydown) {
|
|
258
|
-
resolvedOnKeydown(event);
|
|
259
|
-
}
|
|
260
|
-
defaultHandleKeydown(event);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function selectItem(item: string) {
|
|
264
|
-
const currentValue = (value as string) || '';
|
|
265
|
-
const lastSpaceIndex = currentValue.lastIndexOf(' ');
|
|
266
|
-
|
|
267
|
-
if (lastSpaceIndex === -1) {
|
|
268
|
-
value = item + ' '; // Replace the whole value if no space, add trailing space
|
|
269
|
-
} else {
|
|
270
|
-
value = currentValue.substring(0, lastSpaceIndex + 1) + item + ' '; // Replace last word, add trailing space
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (onSelect) {
|
|
274
|
-
onSelect(item);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
filteredSuggestions = [];
|
|
278
|
-
selectedIndex = -1;
|
|
279
|
-
|
|
280
|
-
if (elementRef) {
|
|
281
|
-
elementRef.focus();
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
</script>
|
|
285
|
-
|
|
286
|
-
{#if clearable}
|
|
287
|
-
<div tabindex="-1" bind:this={dummyFocusDiv} class="sr-only"></div>
|
|
288
|
-
{/if}
|
|
289
|
-
|
|
290
|
-
{#if isCombobox}
|
|
291
|
-
<div class={clsx(isCombobox ? 'relative w-full' : '', theme?.wrapper, styling.wrapper)}>
|
|
292
|
-
{#if right || left || clearable}
|
|
293
|
-
<div class={base({ class: clsx(theme?.base, styling.div) })}>
|
|
294
|
-
{@render inputContent()}
|
|
295
|
-
</div>
|
|
296
|
-
{:else}
|
|
297
|
-
{@render inputContent()}
|
|
298
|
-
{/if}
|
|
299
|
-
|
|
300
|
-
{#if isCombobox && isFocused && filteredSuggestions.length > 0}
|
|
301
|
-
<div class={combo({ class: clsx(theme?.combo, styling.combo) })}>
|
|
302
|
-
{#each filteredSuggestions as item, i}
|
|
303
|
-
<button
|
|
304
|
-
type="button"
|
|
305
|
-
class="w-full px-3 py-2 text-left {i === selectedIndex
|
|
306
|
-
? 'bg-gray-100 dark:bg-gray-700'
|
|
307
|
-
: 'hover:bg-gray-50 dark:hover:bg-gray-700'} focus:outline-none"
|
|
308
|
-
onclick={() => selectItem(item)}
|
|
309
|
-
onmouseenter={() => (selectedIndex = i)}
|
|
310
|
-
>
|
|
311
|
-
<p class={comboItem({ class: clsx(theme?.comboItem, styling.comboItem) })}>{item}</p>
|
|
312
|
-
</button>
|
|
313
|
-
{/each}
|
|
314
|
-
</div>
|
|
315
|
-
{/if}
|
|
316
|
-
</div>
|
|
317
|
-
{:else if group}
|
|
318
|
-
{@render inputContent()}
|
|
319
|
-
{:else if right || left || clearable}
|
|
320
|
-
<div class={base({ class: clsx(theme?.base, styling.div) })}>
|
|
321
|
-
{@render inputContent()}
|
|
322
|
-
</div>
|
|
323
|
-
{:else}
|
|
324
|
-
{@render inputContent()}
|
|
325
|
-
{/if}
|
|
326
|
-
|
|
327
|
-
{#snippet inputContent()}
|
|
328
|
-
{#if left}
|
|
329
|
-
<div class={leftCls({ class: clsx(theme?.left, styling.left) })}>
|
|
330
|
-
{@render left()}
|
|
331
|
-
</div>
|
|
332
|
-
{/if}
|
|
333
|
-
{#if children}
|
|
334
|
-
{@render children({ ...restProps, class: inputCls() })}
|
|
335
|
-
{:else}
|
|
336
|
-
<input
|
|
337
|
-
{...restProps}
|
|
338
|
-
bind:value
|
|
339
|
-
bind:this={elementRef}
|
|
340
|
-
oninput={handleInput}
|
|
341
|
-
onfocus={handleFocus}
|
|
342
|
-
onblur={handleBlur}
|
|
343
|
-
onkeydown={handleKeydown}
|
|
344
|
-
class={inputCls({ class: clsx(theme?.input, className) })}
|
|
345
|
-
/>
|
|
346
|
-
{#if value !== undefined && value !== '' && clearable}
|
|
347
|
-
<CloseButton
|
|
348
|
-
class={close({ class: clsx(theme?.close, styling.close) })}
|
|
349
|
-
color={clearableColor}
|
|
350
|
-
aria-label="Clear search value"
|
|
351
|
-
svgClass={clsx(styling.svg)}
|
|
352
|
-
/>
|
|
353
|
-
{/if}
|
|
354
|
-
{/if}
|
|
355
|
-
{#if right}
|
|
356
|
-
<div class={rightCls({ class: clsx(theme?.right, styling.right) })}>
|
|
357
|
-
{@render right()}
|
|
358
|
-
</div>
|
|
359
|
-
{/if}
|
|
360
|
-
{/snippet}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext } from 'svelte';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { input, clampSize } from './index.js';
|
|
5
|
+
import { getTheme, warnThemeDeprecation } from '../../themes/themeUtils.js';
|
|
6
|
+
import { CloseButton, type InputProps, type InputValue, type SizeType } from '../../index.js';
|
|
7
|
+
import { createDismissableContext } from '../../utils/dismissable.js';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
children,
|
|
11
|
+
left,
|
|
12
|
+
right,
|
|
13
|
+
value = $bindable(),
|
|
14
|
+
elementRef = $bindable(),
|
|
15
|
+
clearable = false,
|
|
16
|
+
size,
|
|
17
|
+
color = 'default',
|
|
18
|
+
class: className,
|
|
19
|
+
classes,
|
|
20
|
+
wrapperClass,
|
|
21
|
+
leftClass,
|
|
22
|
+
rightClass,
|
|
23
|
+
divClass,
|
|
24
|
+
clearableSvgClass,
|
|
25
|
+
clearableColor = 'none',
|
|
26
|
+
clearableClass,
|
|
27
|
+
clearableOnClick,
|
|
28
|
+
data = [],
|
|
29
|
+
maxSuggestions = 5,
|
|
30
|
+
onSelect,
|
|
31
|
+
comboClass,
|
|
32
|
+
comboItemClass,
|
|
33
|
+
oninput,
|
|
34
|
+
onfocus,
|
|
35
|
+
onblur,
|
|
36
|
+
onkeydown,
|
|
37
|
+
...restProps
|
|
38
|
+
}: InputProps<InputValue> = $props();
|
|
39
|
+
|
|
40
|
+
// input, left, right, close, combo, comboItem, div, svg
|
|
41
|
+
warnThemeDeprecation(
|
|
42
|
+
'Input',
|
|
43
|
+
{
|
|
44
|
+
wrapperClass,
|
|
45
|
+
leftClass,
|
|
46
|
+
rightClass,
|
|
47
|
+
divClass,
|
|
48
|
+
clearableSvgClass,
|
|
49
|
+
clearableClass,
|
|
50
|
+
comboClass
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
wrapperClass: 'wrapper',
|
|
54
|
+
leftClass: 'left',
|
|
55
|
+
rightClass: 'right',
|
|
56
|
+
divClass: 'div',
|
|
57
|
+
clearableSvgClass: 'svg',
|
|
58
|
+
clearableClass: 'close',
|
|
59
|
+
comboClass: 'comboItem'
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const styling = $derived(
|
|
64
|
+
classes ?? {
|
|
65
|
+
wrapper: wrapperClass,
|
|
66
|
+
left: leftClass,
|
|
67
|
+
right: rightClass,
|
|
68
|
+
div: divClass,
|
|
69
|
+
svg: clearableSvgClass,
|
|
70
|
+
close: clearableClass,
|
|
71
|
+
combo: comboClass,
|
|
72
|
+
comboItem: comboItemClass
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const theme = getTheme('input');
|
|
77
|
+
|
|
78
|
+
// onSelect is a custom combobox selection handler that takes a string
|
|
79
|
+
// standard DOM events, onInput, onFocus, onBlur, onKeydown will be deprecated in the next minor version
|
|
80
|
+
const resolvedOnInput = $derived(oninput);
|
|
81
|
+
const resolvedOnFocus = $derived(onfocus);
|
|
82
|
+
const resolvedOnBlur = $derived(onblur);
|
|
83
|
+
const resolvedOnKeydown = $derived(onkeydown);
|
|
84
|
+
|
|
85
|
+
// Automatically enable combobox when data is provided
|
|
86
|
+
const isCombobox = $derived(Array.isArray(data) && data.length > 0);
|
|
87
|
+
|
|
88
|
+
// tinted if put in component having its own background
|
|
89
|
+
let background: boolean = getContext('background');
|
|
90
|
+
|
|
91
|
+
// svelte-ignore non_reactive_update
|
|
92
|
+
let dummyFocusDiv: HTMLDivElement;
|
|
93
|
+
|
|
94
|
+
let group: { size: SizeType } = getContext('group');
|
|
95
|
+
let isGroup = !!group;
|
|
96
|
+
let _size = $derived(size || clampSize(group?.size) || 'md');
|
|
97
|
+
const _color = $derived(color === 'default' && background ? 'tinted' : color);
|
|
98
|
+
|
|
99
|
+
const {
|
|
100
|
+
base,
|
|
101
|
+
input: inputCls,
|
|
102
|
+
left: leftCls,
|
|
103
|
+
right: rightCls,
|
|
104
|
+
close,
|
|
105
|
+
combo,
|
|
106
|
+
comboItem
|
|
107
|
+
} = $derived(input({ size: _size, color: _color, grouped: isGroup }));
|
|
108
|
+
|
|
109
|
+
const clearAll = () => {
|
|
110
|
+
if (elementRef) {
|
|
111
|
+
// in order to avoid type error in setTimeout()
|
|
112
|
+
const input = elementRef;
|
|
113
|
+
input.value = '';
|
|
114
|
+
value = '';
|
|
115
|
+
|
|
116
|
+
backspaceUsed = false;
|
|
117
|
+
updateSuggestions();
|
|
118
|
+
// hack to focus outside
|
|
119
|
+
dummyFocusDiv?.focus();
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
input.focus();
|
|
122
|
+
}, 100);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (clearableOnClick) clearableOnClick();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
createDismissableContext(clearAll);
|
|
129
|
+
|
|
130
|
+
// Combobox functionality
|
|
131
|
+
let isFocused = $state(false);
|
|
132
|
+
let filteredSuggestions: string[] = $state([]);
|
|
133
|
+
let selectedIndex = $state(-1);
|
|
134
|
+
let backspaceUsed = $state(false); // Track if backspace was used to clear
|
|
135
|
+
|
|
136
|
+
function updateSuggestions() {
|
|
137
|
+
if (!isCombobox || !isFocused) {
|
|
138
|
+
filteredSuggestions = [];
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const fullSearchTerm = ((value as string) || '').toLowerCase();
|
|
143
|
+
const lastSpaceIndex = fullSearchTerm.lastIndexOf(' ');
|
|
144
|
+
const searchTerm =
|
|
145
|
+
lastSpaceIndex === -1 ? fullSearchTerm : fullSearchTerm.substring(lastSpaceIndex + 1);
|
|
146
|
+
|
|
147
|
+
// Show suggestions if:
|
|
148
|
+
// 1. There's actual input text, OR
|
|
149
|
+
// 2. The input is empty but backspace was just used to clear it
|
|
150
|
+
if (searchTerm === '' && !backspaceUsed) {
|
|
151
|
+
filteredSuggestions = [];
|
|
152
|
+
} else {
|
|
153
|
+
// If there's text, filter suggestions
|
|
154
|
+
if (searchTerm) {
|
|
155
|
+
filteredSuggestions = data
|
|
156
|
+
.filter((item) => item.toLowerCase().includes(searchTerm))
|
|
157
|
+
.slice(0, maxSuggestions);
|
|
158
|
+
}
|
|
159
|
+
// If empty but backspace was used, show all suggestions
|
|
160
|
+
else if (backspaceUsed) {
|
|
161
|
+
filteredSuggestions = [...data].slice(0, maxSuggestions);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
selectedIndex = -1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Watch for value changes
|
|
169
|
+
$effect(() => {
|
|
170
|
+
if (isCombobox) {
|
|
171
|
+
updateSuggestions();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
function defaultHandleInput(event: Event) {
|
|
176
|
+
// Ensure value is treated as a string to safely check its length
|
|
177
|
+
const currentValueAsString = String(value || '');
|
|
178
|
+
if (currentValueAsString.length > 0) {
|
|
179
|
+
backspaceUsed = false;
|
|
180
|
+
}
|
|
181
|
+
updateSuggestions();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function defaultHandleFocus(event: FocusEvent) {
|
|
185
|
+
isFocused = true;
|
|
186
|
+
updateSuggestions();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function defaultHandleBlur(event: FocusEvent) {
|
|
190
|
+
// Small delay to allow click on suggestion to fire first
|
|
191
|
+
setTimeout(() => {
|
|
192
|
+
isFocused = false;
|
|
193
|
+
backspaceUsed = false; // Reset flag when focus is lost
|
|
194
|
+
filteredSuggestions = [];
|
|
195
|
+
}, 200);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function defaultHandleKeydown(event: KeyboardEvent) {
|
|
199
|
+
if (!isCombobox) return;
|
|
200
|
+
|
|
201
|
+
// Special handling for backspace/delete - track when it's used to clear the input
|
|
202
|
+
if (event.key === 'Backspace' || event.key === 'Delete') {
|
|
203
|
+
const currentValue = value as string;
|
|
204
|
+
// If this keypress will make the input empty
|
|
205
|
+
if (currentValue.length <= 1) {
|
|
206
|
+
backspaceUsed = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!filteredSuggestions.length) return;
|
|
211
|
+
|
|
212
|
+
switch (event.key) {
|
|
213
|
+
case 'ArrowDown':
|
|
214
|
+
event.preventDefault();
|
|
215
|
+
selectedIndex = (selectedIndex + 1) % filteredSuggestions.length;
|
|
216
|
+
break;
|
|
217
|
+
case 'ArrowUp':
|
|
218
|
+
event.preventDefault();
|
|
219
|
+
selectedIndex = selectedIndex <= 0 ? filteredSuggestions.length - 1 : selectedIndex - 1;
|
|
220
|
+
break;
|
|
221
|
+
case 'Enter':
|
|
222
|
+
if (selectedIndex >= 0) {
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
selectItem(filteredSuggestions[selectedIndex]);
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case 'Escape':
|
|
228
|
+
event.preventDefault();
|
|
229
|
+
filteredSuggestions = [];
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Combined event handlers that call custom handlers first, then default behavior
|
|
235
|
+
function handleInput(event: Event) {
|
|
236
|
+
if (resolvedOnInput) {
|
|
237
|
+
resolvedOnInput(event);
|
|
238
|
+
}
|
|
239
|
+
defaultHandleInput(event);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function handleFocus(event: FocusEvent) {
|
|
243
|
+
if (resolvedOnFocus) {
|
|
244
|
+
resolvedOnFocus(event);
|
|
245
|
+
}
|
|
246
|
+
defaultHandleFocus(event);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function handleBlur(event: FocusEvent) {
|
|
250
|
+
if (resolvedOnBlur) {
|
|
251
|
+
resolvedOnBlur(event);
|
|
252
|
+
}
|
|
253
|
+
defaultHandleBlur(event);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
257
|
+
if (resolvedOnKeydown) {
|
|
258
|
+
resolvedOnKeydown(event);
|
|
259
|
+
}
|
|
260
|
+
defaultHandleKeydown(event);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function selectItem(item: string) {
|
|
264
|
+
const currentValue = (value as string) || '';
|
|
265
|
+
const lastSpaceIndex = currentValue.lastIndexOf(' ');
|
|
266
|
+
|
|
267
|
+
if (lastSpaceIndex === -1) {
|
|
268
|
+
value = item + ' '; // Replace the whole value if no space, add trailing space
|
|
269
|
+
} else {
|
|
270
|
+
value = currentValue.substring(0, lastSpaceIndex + 1) + item + ' '; // Replace last word, add trailing space
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (onSelect) {
|
|
274
|
+
onSelect(item);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
filteredSuggestions = [];
|
|
278
|
+
selectedIndex = -1;
|
|
279
|
+
|
|
280
|
+
if (elementRef) {
|
|
281
|
+
elementRef.focus();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
</script>
|
|
285
|
+
|
|
286
|
+
{#if clearable}
|
|
287
|
+
<div tabindex="-1" bind:this={dummyFocusDiv} class="sr-only"></div>
|
|
288
|
+
{/if}
|
|
289
|
+
|
|
290
|
+
{#if isCombobox}
|
|
291
|
+
<div class={clsx(isCombobox ? 'relative w-full' : '', theme?.wrapper, styling.wrapper)}>
|
|
292
|
+
{#if right || left || clearable}
|
|
293
|
+
<div class={base({ class: clsx(theme?.base, styling.div) })}>
|
|
294
|
+
{@render inputContent()}
|
|
295
|
+
</div>
|
|
296
|
+
{:else}
|
|
297
|
+
{@render inputContent()}
|
|
298
|
+
{/if}
|
|
299
|
+
|
|
300
|
+
{#if isCombobox && isFocused && filteredSuggestions.length > 0}
|
|
301
|
+
<div class={combo({ class: clsx(theme?.combo, styling.combo) })}>
|
|
302
|
+
{#each filteredSuggestions as item, i}
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
class="w-full px-3 py-2 text-left {i === selectedIndex
|
|
306
|
+
? 'bg-gray-100 dark:bg-gray-700'
|
|
307
|
+
: 'hover:bg-gray-50 dark:hover:bg-gray-700'} focus:outline-none"
|
|
308
|
+
onclick={() => selectItem(item)}
|
|
309
|
+
onmouseenter={() => (selectedIndex = i)}
|
|
310
|
+
>
|
|
311
|
+
<p class={comboItem({ class: clsx(theme?.comboItem, styling.comboItem) })}>{item}</p>
|
|
312
|
+
</button>
|
|
313
|
+
{/each}
|
|
314
|
+
</div>
|
|
315
|
+
{/if}
|
|
316
|
+
</div>
|
|
317
|
+
{:else if group}
|
|
318
|
+
{@render inputContent()}
|
|
319
|
+
{:else if right || left || clearable}
|
|
320
|
+
<div class={base({ class: clsx(theme?.base, styling.div) })}>
|
|
321
|
+
{@render inputContent()}
|
|
322
|
+
</div>
|
|
323
|
+
{:else}
|
|
324
|
+
{@render inputContent()}
|
|
325
|
+
{/if}
|
|
326
|
+
|
|
327
|
+
{#snippet inputContent()}
|
|
328
|
+
{#if left}
|
|
329
|
+
<div class={leftCls({ class: clsx(theme?.left, styling.left) })}>
|
|
330
|
+
{@render left()}
|
|
331
|
+
</div>
|
|
332
|
+
{/if}
|
|
333
|
+
{#if children}
|
|
334
|
+
{@render children({ ...restProps, class: inputCls() })}
|
|
335
|
+
{:else}
|
|
336
|
+
<input
|
|
337
|
+
{...restProps}
|
|
338
|
+
bind:value
|
|
339
|
+
bind:this={elementRef}
|
|
340
|
+
oninput={handleInput}
|
|
341
|
+
onfocus={handleFocus}
|
|
342
|
+
onblur={handleBlur}
|
|
343
|
+
onkeydown={handleKeydown}
|
|
344
|
+
class={inputCls({ class: clsx(theme?.input, className) })}
|
|
345
|
+
/>
|
|
346
|
+
{#if value !== undefined && value !== '' && clearable}
|
|
347
|
+
<CloseButton
|
|
348
|
+
class={close({ class: clsx(theme?.close, styling.close) })}
|
|
349
|
+
color={clearableColor}
|
|
350
|
+
aria-label="Clear search value"
|
|
351
|
+
svgClass={clsx(styling.svg)}
|
|
352
|
+
/>
|
|
353
|
+
{/if}
|
|
354
|
+
{/if}
|
|
355
|
+
{#if right}
|
|
356
|
+
<div class={rightCls({ class: clsx(theme?.right, styling.right) })}>
|
|
357
|
+
{@render right()}
|
|
358
|
+
</div>
|
|
359
|
+
{/if}
|
|
360
|
+
{/snippet}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|