@lavalogic/scoria 0.33.0 → 0.35.0

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.
Files changed (130) hide show
  1. package/README.md +58 -58
  2. package/dist/Components/AccordionGroup.svelte +44 -63
  3. package/dist/Components/AccordionGroup.svelte.d.ts +2 -25
  4. package/dist/Components/AccordionGroupButton.svelte +106 -114
  5. package/dist/Components/AccordionGroupButton.svelte.d.ts +2 -2
  6. package/dist/Components/AccordionGroupButtonProps.d.ts +21 -0
  7. package/dist/Components/AccordionGroupButtonProps.js +1 -0
  8. package/dist/Components/Action.svelte +138 -138
  9. package/dist/Components/AlertModal.svelte +125 -125
  10. package/dist/Components/Base/ContextWrapper.svelte +50 -50
  11. package/dist/Components/Base/Pagination.svelte +190 -190
  12. package/dist/Components/Base/Snippets.svelte +29 -29
  13. package/dist/Components/BigRadioSet.svelte +29 -29
  14. package/dist/Components/Bubble.svelte +77 -77
  15. package/dist/Components/Button.svelte +197 -197
  16. package/dist/Components/Checkbox.svelte +68 -68
  17. package/dist/Components/CollapsibleCard.svelte +70 -70
  18. package/dist/Components/Contexts/ContextMenu.svelte +157 -157
  19. package/dist/Components/Contexts/ContextMenuDivider.svelte +11 -11
  20. package/dist/Components/Contexts/ContextMenuOption.svelte +52 -52
  21. package/dist/Components/Contexts/Toast.svelte +61 -61
  22. package/dist/Components/Contexts/ToastContainer.svelte +46 -46
  23. package/dist/Components/Contexts/Tooltip.svelte +26 -26
  24. package/dist/Components/DataMatrixIcon.svelte +41 -41
  25. package/dist/Components/DateInput.svelte +109 -109
  26. package/dist/Components/DatePicker.svelte +312 -312
  27. package/dist/Components/DesktopModal.svelte +106 -106
  28. package/dist/Components/Dial.svelte +151 -151
  29. package/dist/Components/DimensionInput.svelte +184 -184
  30. package/dist/Components/DragMenu/DragMenuHolder.svelte +106 -106
  31. package/dist/Components/DragMenu/DraggableCard.svelte +41 -41
  32. package/dist/Components/FileCard.svelte +20 -20
  33. package/dist/Components/FileUpload.svelte +196 -196
  34. package/dist/Components/GridInput.svelte +190 -190
  35. package/dist/Components/GridInputSet.svelte +22 -22
  36. package/dist/Components/HorizontalTabGroup.svelte +78 -78
  37. package/dist/Components/HorizontalTabGroupButton.svelte +96 -96
  38. package/dist/Components/Icon.svelte +97 -97
  39. package/dist/Components/Icons.js +357 -357
  40. package/dist/Components/LoadingAnimation.svelte +32 -32
  41. package/dist/Components/LoadingModal.svelte +47 -47
  42. package/dist/Components/LoadingOverlay.svelte +27 -27
  43. package/dist/Components/Modal.svelte +251 -251
  44. package/dist/Components/MultiSelect.svelte +197 -197
  45. package/dist/Components/Nav/Nav.svelte +97 -97
  46. package/dist/Components/Nav/NavServiceMenuList.svelte +34 -34
  47. package/dist/Components/Nav/NavTab.svelte +91 -91
  48. package/dist/Components/Nav/ServiceMenuItem.svelte +91 -91
  49. package/dist/Components/Nav/ServicesNav.svelte +53 -53
  50. package/dist/Components/NumberInput.svelte +144 -144
  51. package/dist/Components/PageHeading.svelte +36 -36
  52. package/dist/Components/PasswordInput.svelte +73 -73
  53. package/dist/Components/Portal.svelte +25 -25
  54. package/dist/Components/ProgressBubble.svelte +80 -80
  55. package/dist/Components/QuerySelect.svelte +411 -411
  56. package/dist/Components/SingleSelect.svelte +261 -261
  57. package/dist/Components/StepButton.svelte +120 -120
  58. package/dist/Components/StepForm.svelte +108 -108
  59. package/dist/Components/Switch.svelte +36 -36
  60. package/dist/Components/Table/Body/QuickSearchCell.svelte +632 -632
  61. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +30 -30
  62. package/dist/Components/Table/Body/Rows/ColumnList.svelte +56 -56
  63. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +179 -179
  64. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +49 -49
  65. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +180 -180
  66. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +102 -102
  67. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +119 -119
  68. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +114 -114
  69. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +96 -96
  70. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +72 -72
  71. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +131 -131
  72. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +144 -144
  73. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +238 -238
  74. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +358 -358
  75. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +338 -338
  76. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +183 -183
  77. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +309 -309
  78. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +369 -369
  79. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +170 -170
  80. package/dist/Components/Table/Body/Rows/TableRow.svelte +259 -259
  81. package/dist/Components/Table/Body/TableBody.svelte +67 -67
  82. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +39 -39
  83. package/dist/Components/Table/Headers/SelectAllHeader.svelte +28 -28
  84. package/dist/Components/Table/Headers/TableHead.svelte +260 -260
  85. package/dist/Components/Table/Misc/ColumnPanel.svelte +78 -78
  86. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +99 -99
  87. package/dist/Components/Table/Misc/FilterInput.svelte +556 -556
  88. package/dist/Components/Table/Misc/FilterPanel.svelte +46 -46
  89. package/dist/Components/Table/Misc/FilterPanelModal.svelte +67 -67
  90. package/dist/Components/Table/Misc/SidePanel.svelte +32 -32
  91. package/dist/Components/Table/Misc/TableFooter.svelte +304 -304
  92. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +156 -156
  93. package/dist/Components/Table/Misc/TableSidebar.svelte +176 -176
  94. package/dist/Components/Table/Misc/ToolboxPanel.svelte +77 -77
  95. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +91 -91
  96. package/dist/Components/Table/NestedTable.svelte +84 -84
  97. package/dist/Components/Table/Table.svelte +492 -492
  98. package/dist/Components/Test/TestContextConsumer.svelte +13 -13
  99. package/dist/Components/TextArea.svelte +108 -108
  100. package/dist/Components/TextInput.svelte +147 -147
  101. package/dist/Components/ThreeStateRadio.svelte +124 -124
  102. package/dist/Components/Touch/BoolCard.svelte +52 -52
  103. package/dist/Components/Touch/DateModal.svelte +217 -217
  104. package/dist/Components/Touch/EditCard.svelte +52 -52
  105. package/dist/Components/Touch/InfoCard.svelte +24 -24
  106. package/dist/Components/Touch/InfoTextCard.svelte +21 -21
  107. package/dist/Components/Touch/ModalExplanation.svelte +20 -20
  108. package/dist/Components/Touch/NumberKeyboard.svelte +268 -268
  109. package/dist/Components/Touch/NumberModal.svelte +69 -69
  110. package/dist/Components/Touch/PlusMinusCard.svelte +50 -50
  111. package/dist/Components/Touch/SelectModal.svelte +105 -105
  112. package/dist/Components/Touch/SummaryCard.svelte +65 -65
  113. package/dist/Components/Touch/TextAreaModal.svelte +86 -86
  114. package/dist/Components/Touch/TextModal.svelte +123 -123
  115. package/dist/Components/Touch/TouchCard.svelte +28 -28
  116. package/dist/Components/Touch/TouchModal.svelte +259 -259
  117. package/dist/Components/Touch/UtilityButton.svelte +91 -91
  118. package/dist/Components/VerticalTabGroup.svelte +66 -66
  119. package/dist/Components/VerticalTabGroupButton.svelte +97 -97
  120. package/dist/Helpers/Datamatrix.d.ts +10 -10
  121. package/dist/Helpers/Datamatrix.js +1 -1
  122. package/dist/Helpers/Helpers.svelte.js +6 -4
  123. package/dist/Types/Examples/ExampleModal.svelte +34 -34
  124. package/dist/index.d.ts +1 -0
  125. package/dist/scss/_basics.scss +12 -12
  126. package/dist/scss/_colours.scss +134 -134
  127. package/dist/scss/_mixins.scss +3016 -3016
  128. package/dist/scss/_sizing.scss +69 -69
  129. package/dist/scss/_transitions.scss +8 -8
  130. package/package.json +2 -2
@@ -1,413 +1,413 @@
1
- <svelte:options runes />
2
-
3
- <script
4
- lang="ts"
5
- module
6
- >
7
- interface QuerySelectPropsAsObjectCommonProps<V extends object, VP extends ValueProp<V>> {
8
- labelProp: NonNullableKey<V>;
9
- valueProp: VP;
10
-
11
- query: (value: string) => Promise<Array<V>>;
12
- onchange: (newValue: V | null) => void;
13
-
14
- options?: Array<V>;
15
- name?: string;
16
- disabled?: boolean;
17
- width?: string;
18
- height?: string;
19
- label?: string;
20
- required?: boolean;
21
- invalid?: boolean;
22
- onfocus?: (target: HTMLInputElement) => void;
23
- inputId?: string;
24
- focusOnLoad?: boolean;
25
- clearable?: boolean;
26
- isValid?: (item: V | null) => boolean;
27
- }
28
-
29
- export interface QuerySelectPropsAsObject<
30
- V extends object,
31
- VP extends ValueProp<V>,
32
- > extends QuerySelectPropsAsObjectCommonProps<V, VP> {
33
- valueAsObject: true;
34
- value: V | null;
35
- }
36
-
37
- export interface QuerySelectPropsAsPrimitive<
38
- V extends object,
39
- VP extends ValueProp<V>,
40
- > extends QuerySelectPropsAsObjectCommonProps<V, VP> {
41
- valueAsObject: false;
42
- value: V[VP] | null;
43
- }
44
-
45
- export type QuerySelectProps<V extends object, VP extends ValueProp<V>> =
46
- | QuerySelectPropsAsObject<V, VP>
47
- | QuerySelectPropsAsPrimitive<V, VP>;
48
- </script>
49
-
50
- <!-- #region Script -->
51
- <script
52
- lang="ts"
53
- generics="V extends object, VP extends ValueProp<V>"
54
- >
55
- import { ToastContext } from '../Contexts/ToastContext.svelte.js';
56
-
57
- import { dev } from '$app/environment';
58
- import { devCatch, generateShortUUID, vowels } from '../Helpers/Helpers.svelte.js';
59
- import { Colour, ColourSet } from '../scss/colours.js';
60
- import type { NonNullableKey, ValueProp } from '../Types/Internal/Misc.js';
61
- import { Size } from '../Types/Internal/Size.js';
62
- import Svelecte from 'svelecte';
63
- import { getContext, onMount, type Snippet, tick, untrack } from 'svelte';
64
- import Action from './Action.svelte';
65
-
66
- let {
67
- label,
68
- labelProp,
69
- valueProp,
70
- clearable = false,
71
- required = false,
72
- invalid = false,
73
- valueAsObject,
74
- name = 'Item',
75
- disabled,
76
- query,
77
- width,
78
- height,
79
- focusOnLoad = false,
80
- inputId = generateShortUUID(),
81
- value: selectedValue,
82
- options = $bindable<Array<V>>([]),
83
- isValid,
84
- onchange,
85
- onfocus,
86
- }: QuerySelectProps<V, VP> = $props();
87
-
88
- export function forceQuery() {
89
- clearTimeout(timeoutHandler);
90
-
91
- const input = document.getElementById(inputId);
92
- const searchValue = (input as HTMLInputElement).value;
93
-
94
- const currentHandler = (timeoutHandler = setTimeout(() => {
95
- tryQuery(currentHandler, searchValue).catch(devCatch);
96
- }, 500) as unknown as number);
97
- tryQuery(currentHandler, searchValue).catch(devCatch);
98
- }
99
-
100
- let ref: Svelecte | undefined = $state();
101
- let hasAutoFocused = $state(false);
102
- let loadingDiv: HTMLDivElement | undefined = $state();
103
- let isLoading = $state(false);
104
- let hasRegisteredListeners = $state(false);
105
-
106
- function isObjectValue(value: unknown): value is V {
107
- return value !== null && typeof value == 'object' && labelProp in value && valueProp in value;
108
- }
109
-
110
- $effect.pre(() => {
111
- void selectedValue;
112
- untrack(() => {
113
- if (selectedValue != null) {
114
- if (valueAsObject) {
115
- if (isObjectValue(selectedValue)) {
116
- if (!options.some((opt: V) => opt[valueProp] === selectedValue[valueProp])) {
117
- options.push(selectedValue);
118
- }
119
- } else if (dev) {
120
- console.warn(`valueAsObject is true but the value doesn't match`, selectedValue);
121
- return;
122
- }
123
- } else if (!options.some((opt: V) => opt[valueProp] === selectedValue)) {
124
- options.push({
125
- //best-effort solution. Not a good one
126
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
127
- [labelProp]: selectedValue.toString(),
128
- [valueProp]: selectedValue,
129
- } as V);
130
- }
131
- }
132
- });
133
- });
134
-
135
- let isInvalid = $derived(
136
- (required && !selectedValue) ||
137
- invalid ||
138
- isValid?.(
139
- valueAsObject
140
- ? (selectedValue as V)
141
- : (options.find((a) => a[valueProp] === selectedValue) ?? null)
142
- ) === false
143
- );
144
-
145
- const toastContext = getContext<ToastContext>(ToastContext.identifier);
146
-
147
- function forceShowDropdown(e: HTMLInputElement) {
148
- if (focusOnLoad && !hasAutoFocused) {
149
- hasAutoFocused = true;
150
-
151
- const input = e;
152
-
153
- input.click();
154
-
155
- // HACK. the default selectOnTab functionality selects the highlighted element,
156
- // this selects the first element on the list
157
- // please decide which one we'll be using and be consistent
158
- // input.addEventListener('keydown', (e) => {
159
- // if (e.key === 'Tab') {
160
- // const inputElement = e.target as HTMLInputElement | undefined;
161
- // if (inputElement && inputElement.value.trim()) {
162
- // (
163
- // inputElement?.parentElement?.parentElement?.parentElement?.parentElement?.querySelector(
164
- // '[data-pos="0"]' //first element from dropdown
165
- // ) as HTMLElement | undefined
166
- // )?.click();
167
- // } else if (optional) {
168
- // dispatch('change', null);
169
- // }
170
- // }
171
- // });
172
- }
173
- }
174
-
175
- // HACK svelecte type shenanigans
176
- function isFocusable(ref: unknown): ref is HTMLInputElement {
177
- return ref != null;
178
- }
179
-
180
- onMount(() => {
181
- if (focusOnLoad) {
182
- tick()
183
- .then(() => {
184
- if (isFocusable(ref)) {
185
- ref.focus();
186
- }
187
- })
188
- .catch(devCatch);
189
- }
190
-
191
- const input = document.getElementById(inputId);
192
- // input?.addEventListener('focus', () => {
193
- // hidden = true;
194
- // });
195
- // input?.addEventListener('blur', () => {
196
- // hidden = false;
197
- // });
198
-
199
- // HACK callback for is-open on/off
200
- const dropdownRef = input?.closest('div.svelecte')?.querySelector('.sv_dropdown') ?? undefined;
201
- const observer = new MutationObserver((mutations: Array<MutationRecord>) => {
202
- for (let i = mutations.length - 1; i > -1; i--) {
203
- const mutation = mutations[i];
204
- if (mutation.attributeName === 'class') {
205
- isExpanded = dropdownRef?.classList.contains('is-open') ?? false;
206
- }
207
- }
208
- });
209
-
210
- if (dropdownRef) {
211
- observer.observe(dropdownRef, { attributes: true });
212
- }
213
-
214
- return () => {
215
- observer.disconnect();
216
- };
217
- });
218
-
219
- let timeoutHandler = -1;
220
-
221
- $effect(() => {
222
- if (loadingDiv && isLoading) {
223
- loadingDiv.classList.add('is-loading');
224
- } else {
225
- loadingDiv?.classList.remove('is-loading');
226
- }
227
- });
228
-
229
- function registerInputListeners(e: HTMLInputElement) {
230
- if (!hasRegisteredListeners) {
231
- hasRegisteredListeners = true;
232
-
233
- const inputElement = e;
234
-
235
- loadingDiv = inputElement.parentElement?.parentElement?.nextElementSibling as
236
- | HTMLDivElement
237
- | undefined;
238
-
239
- const currentHandler = (timeoutHandler = setTimeout(() => {
240
- tryQuery(currentHandler, '').catch(devCatch);
241
- }, 500) as unknown as number);
242
-
243
- inputElement.addEventListener('input', (e) => {
244
- clearTimeout(timeoutHandler);
245
-
246
- const searchValue = (e.target as HTMLInputElement).value;
247
-
248
- const currentHandler = (timeoutHandler = setTimeout(() => {
249
- tryQuery(currentHandler, searchValue).catch(devCatch);
250
- }, 500) as unknown as number);
251
- });
252
-
253
- inputElement.addEventListener('keydown', (e) => {
254
- if (e.key === 'Enter') {
255
- tick()
256
- .then(() => {
257
- const searchValue = (e.target as HTMLInputElement).value;
258
- if (searchValue) {
259
- // dispatch('search', { value: inputElement.value });
260
- clearTimeout(timeoutHandler);
261
-
262
- const currentHandler = (timeoutHandler = setTimeout(() => {
263
- tryQuery(currentHandler, searchValue, true).catch(devCatch);
264
- }, 1) as unknown as number);
265
- }
266
- })
267
- .catch(devCatch);
268
- }
269
- });
270
- }
271
- }
272
-
273
- async function tryQuery(handler: number, value: string, andSelectIt = false) {
274
- if (timeoutHandler === handler) {
275
- isLoading = true;
276
- try {
277
- const newValues = await query(value);
278
- isLoading = false;
279
-
280
- if (timeoutHandler === handler) {
281
- options = newValues;
282
-
283
- if (andSelectIt) {
284
- if (newValues.length === 1) {
285
- onChange(newValues[0]);
286
- }
287
- }
288
- }
289
- } catch (e) {
290
- toastContext.toast(e);
291
- }
292
- }
293
- }
294
-
295
- function onChange(newValue: V | null | undefined) {
296
- if (clearable || newValue != null) {
297
- onchange(newValue ?? null);
298
- }
299
- }
300
-
301
- const an = $derived(vowels.has(name[0].toLocaleLowerCase()) ? 'an' : 'a');
302
-
303
- let isExpanded = $state(false);
304
- </script>
305
-
306
- <!-- #endregion -->
307
-
308
- <!-- #region HTML -->
309
-
310
- <div
311
- class="select-wrapper"
312
- class:invalid={isInvalid}
313
- style={`${width ? `--input-width: ${width};` : ''}${height ? `--input-height: ${height};` : ''}`}
314
- >
315
- {#if label}
316
- <label for={inputId}>
317
- <span
318
- >{label}{#if !required}&nbsp;- <i>optional</i>{/if}</span
319
- >
320
- <span class="border"
321
- >{label}{#if !required}&nbsp;- <i>optional</i>{/if}</span
322
- >
323
- </label>
324
- {/if}
325
- <!-- strict mode is used to stop svelecte from rejecting perfectly valid values... wtf -->
326
- <Svelecte
327
- strictMode={false}
328
- keepCreated
329
- onFocus={(e: HTMLInputElement) => {
330
- registerInputListeners(e);
331
- forceShowDropdown(e);
332
- onfocus?.(e);
333
- }}
334
- placeholder="Please Select{name ? ` ${an} ${name}` : ''}"
335
- bind:this={ref}
336
- {inputId}
337
- {disabled}
338
- valueField={valueProp}
339
- labelField={labelProp}
340
- virtualList={false}
341
- searchable
342
- searchProps={{ keepSelectionInList: true }}
343
- lazyDropdown
344
- {valueAsObject}
345
- {options}
346
- value={selectedValue as Array<unknown>}
347
- {onChange}
348
- selectOnTab
349
- deselectMode={required && !clearable ? 'none' : 'toggle'}
350
- keepSelectionInList
351
- {name}
352
- selection={selection as (selectedOptions: Array<object>) => ReturnType<Snippet>}
353
- {append}
354
- >
355
- {#snippet toggleIcon()}
356
- <div class="nothing"></div>
357
- <!-- <div class="icon-bg">
358
- <Icon name="chevron-down" size={Size.MediumSmall} />
359
- </div> -->
360
- {/snippet}
361
- </Svelecte>
362
-
363
- {#snippet append()}
364
- <div class="icons">
365
- {#if clearable && selectedValue}
366
- <Action
367
- {disabled}
368
- noTabIndex
369
- colourSet={ColourSet.Auxiliary}
370
- size={Size.Small}
371
- onclick={(e: MouseEvent) => {
372
- if (isExpanded) {
373
- e.stopPropagation();
374
- }
375
-
376
- onChange(null);
377
- }}
378
- icon={{
379
- name: 'close-menu-cross',
380
- colour: Colour['$ui-blue-9'],
381
- }}
382
- />
383
- {/if}
384
-
385
- <Action
386
- {disabled}
387
- noTabIndex
388
- colourSet={ColourSet.Auxiliary}
389
- size={Size.Small}
390
- icon={{
391
- name: isExpanded ? 'chevron-up' : 'chevron-down',
392
- colour: Colour['$ui-blue-9'],
393
- }}
394
- />
395
- </div>
396
- {/snippet}
397
-
398
- {#snippet selection(selectedOptions: Array<V>)}
399
- {#if selectedOptions.length == 0}
400
- <span class="selection">Please Select {an} {name}</span>
401
- {:else}
402
- <span class="selection">{selectedOptions[0][labelProp]}</span>
403
- {/if}
404
- {/snippet}
405
- </div>
406
-
407
- <!-- #endregion -->
408
-
409
- <!-- #region CSS
410
- -->
1
+ <svelte:options runes />
2
+
3
+ <script
4
+ lang="ts"
5
+ module
6
+ >
7
+ interface QuerySelectPropsAsObjectCommonProps<V extends object, VP extends ValueProp<V>> {
8
+ labelProp: NonNullableKey<V>;
9
+ valueProp: VP;
10
+
11
+ query: (value: string) => Promise<Array<V>>;
12
+ onchange: (newValue: V | null) => void;
13
+
14
+ options?: Array<V>;
15
+ name?: string;
16
+ disabled?: boolean;
17
+ width?: string;
18
+ height?: string;
19
+ label?: string;
20
+ required?: boolean;
21
+ invalid?: boolean;
22
+ onfocus?: (target: HTMLInputElement) => void;
23
+ inputId?: string;
24
+ focusOnLoad?: boolean;
25
+ clearable?: boolean;
26
+ isValid?: (item: V | null) => boolean;
27
+ }
28
+
29
+ export interface QuerySelectPropsAsObject<
30
+ V extends object,
31
+ VP extends ValueProp<V>,
32
+ > extends QuerySelectPropsAsObjectCommonProps<V, VP> {
33
+ valueAsObject: true;
34
+ value: V | null;
35
+ }
36
+
37
+ export interface QuerySelectPropsAsPrimitive<
38
+ V extends object,
39
+ VP extends ValueProp<V>,
40
+ > extends QuerySelectPropsAsObjectCommonProps<V, VP> {
41
+ valueAsObject: false;
42
+ value: V[VP] | null;
43
+ }
44
+
45
+ export type QuerySelectProps<V extends object, VP extends ValueProp<V>> =
46
+ | QuerySelectPropsAsObject<V, VP>
47
+ | QuerySelectPropsAsPrimitive<V, VP>;
48
+ </script>
49
+
50
+ <!-- #region Script -->
51
+ <script
52
+ lang="ts"
53
+ generics="V extends object, VP extends ValueProp<V>"
54
+ >
55
+ import { ToastContext } from '../Contexts/ToastContext.svelte.js';
56
+
57
+ import { dev } from '$app/environment';
58
+ import { devCatch, generateShortUUID, vowels } from '../Helpers/Helpers.svelte.js';
59
+ import { Colour, ColourSet } from '../scss/colours.js';
60
+ import type { NonNullableKey, ValueProp } from '../Types/Internal/Misc.js';
61
+ import { Size } from '../Types/Internal/Size.js';
62
+ import Svelecte from 'svelecte';
63
+ import { getContext, onMount, type Snippet, tick, untrack } from 'svelte';
64
+ import Action from './Action.svelte';
65
+
66
+ let {
67
+ label,
68
+ labelProp,
69
+ valueProp,
70
+ clearable = false,
71
+ required = false,
72
+ invalid = false,
73
+ valueAsObject,
74
+ name = 'Item',
75
+ disabled,
76
+ query,
77
+ width,
78
+ height,
79
+ focusOnLoad = false,
80
+ inputId = generateShortUUID(),
81
+ value: selectedValue,
82
+ options = $bindable<Array<V>>([]),
83
+ isValid,
84
+ onchange,
85
+ onfocus,
86
+ }: QuerySelectProps<V, VP> = $props();
87
+
88
+ export function forceQuery() {
89
+ clearTimeout(timeoutHandler);
90
+
91
+ const input = document.getElementById(inputId);
92
+ const searchValue = (input as HTMLInputElement).value;
93
+
94
+ const currentHandler = (timeoutHandler = setTimeout(() => {
95
+ tryQuery(currentHandler, searchValue).catch(devCatch);
96
+ }, 500) as unknown as number);
97
+ tryQuery(currentHandler, searchValue).catch(devCatch);
98
+ }
99
+
100
+ let ref: Svelecte | undefined = $state();
101
+ let hasAutoFocused = $state(false);
102
+ let loadingDiv: HTMLDivElement | undefined = $state();
103
+ let isLoading = $state(false);
104
+ let hasRegisteredListeners = $state(false);
105
+
106
+ function isObjectValue(value: unknown): value is V {
107
+ return value !== null && typeof value == 'object' && labelProp in value && valueProp in value;
108
+ }
109
+
110
+ $effect.pre(() => {
111
+ void selectedValue;
112
+ untrack(() => {
113
+ if (selectedValue != null) {
114
+ if (valueAsObject) {
115
+ if (isObjectValue(selectedValue)) {
116
+ if (!options.some((opt: V) => opt[valueProp] === selectedValue[valueProp])) {
117
+ options.push(selectedValue);
118
+ }
119
+ } else if (dev) {
120
+ console.warn(`valueAsObject is true but the value doesn't match`, selectedValue);
121
+ return;
122
+ }
123
+ } else if (!options.some((opt: V) => opt[valueProp] === selectedValue)) {
124
+ options.push({
125
+ //best-effort solution. Not a good one
126
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
127
+ [labelProp]: selectedValue.toString(),
128
+ [valueProp]: selectedValue,
129
+ } as V);
130
+ }
131
+ }
132
+ });
133
+ });
134
+
135
+ let isInvalid = $derived(
136
+ (required && !selectedValue) ||
137
+ invalid ||
138
+ isValid?.(
139
+ valueAsObject
140
+ ? (selectedValue as V)
141
+ : (options.find((a) => a[valueProp] === selectedValue) ?? null)
142
+ ) === false
143
+ );
144
+
145
+ const toastContext = getContext<ToastContext>(ToastContext.identifier);
146
+
147
+ function forceShowDropdown(e: HTMLInputElement) {
148
+ if (focusOnLoad && !hasAutoFocused) {
149
+ hasAutoFocused = true;
150
+
151
+ const input = e;
152
+
153
+ input.click();
154
+
155
+ // HACK. the default selectOnTab functionality selects the highlighted element,
156
+ // this selects the first element on the list
157
+ // please decide which one we'll be using and be consistent
158
+ // input.addEventListener('keydown', (e) => {
159
+ // if (e.key === 'Tab') {
160
+ // const inputElement = e.target as HTMLInputElement | undefined;
161
+ // if (inputElement && inputElement.value.trim()) {
162
+ // (
163
+ // inputElement?.parentElement?.parentElement?.parentElement?.parentElement?.querySelector(
164
+ // '[data-pos="0"]' //first element from dropdown
165
+ // ) as HTMLElement | undefined
166
+ // )?.click();
167
+ // } else if (optional) {
168
+ // dispatch('change', null);
169
+ // }
170
+ // }
171
+ // });
172
+ }
173
+ }
174
+
175
+ // HACK svelecte type shenanigans
176
+ function isFocusable(ref: unknown): ref is HTMLInputElement {
177
+ return ref != null;
178
+ }
179
+
180
+ onMount(() => {
181
+ if (focusOnLoad) {
182
+ tick()
183
+ .then(() => {
184
+ if (isFocusable(ref)) {
185
+ ref.focus();
186
+ }
187
+ })
188
+ .catch(devCatch);
189
+ }
190
+
191
+ const input = document.getElementById(inputId);
192
+ // input?.addEventListener('focus', () => {
193
+ // hidden = true;
194
+ // });
195
+ // input?.addEventListener('blur', () => {
196
+ // hidden = false;
197
+ // });
198
+
199
+ // HACK callback for is-open on/off
200
+ const dropdownRef = input?.closest('div.svelecte')?.querySelector('.sv_dropdown') ?? undefined;
201
+ const observer = new MutationObserver((mutations: Array<MutationRecord>) => {
202
+ for (let i = mutations.length - 1; i > -1; i--) {
203
+ const mutation = mutations[i];
204
+ if (mutation.attributeName === 'class') {
205
+ isExpanded = dropdownRef?.classList.contains('is-open') ?? false;
206
+ }
207
+ }
208
+ });
209
+
210
+ if (dropdownRef) {
211
+ observer.observe(dropdownRef, { attributes: true });
212
+ }
213
+
214
+ return () => {
215
+ observer.disconnect();
216
+ };
217
+ });
218
+
219
+ let timeoutHandler = -1;
220
+
221
+ $effect(() => {
222
+ if (loadingDiv && isLoading) {
223
+ loadingDiv.classList.add('is-loading');
224
+ } else {
225
+ loadingDiv?.classList.remove('is-loading');
226
+ }
227
+ });
228
+
229
+ function registerInputListeners(e: HTMLInputElement) {
230
+ if (!hasRegisteredListeners) {
231
+ hasRegisteredListeners = true;
232
+
233
+ const inputElement = e;
234
+
235
+ loadingDiv = inputElement.parentElement?.parentElement?.nextElementSibling as
236
+ | HTMLDivElement
237
+ | undefined;
238
+
239
+ const currentHandler = (timeoutHandler = setTimeout(() => {
240
+ tryQuery(currentHandler, '').catch(devCatch);
241
+ }, 500) as unknown as number);
242
+
243
+ inputElement.addEventListener('input', (e) => {
244
+ clearTimeout(timeoutHandler);
245
+
246
+ const searchValue = (e.target as HTMLInputElement).value;
247
+
248
+ const currentHandler = (timeoutHandler = setTimeout(() => {
249
+ tryQuery(currentHandler, searchValue).catch(devCatch);
250
+ }, 500) as unknown as number);
251
+ });
252
+
253
+ inputElement.addEventListener('keydown', (e) => {
254
+ if (e.key === 'Enter') {
255
+ tick()
256
+ .then(() => {
257
+ const searchValue = (e.target as HTMLInputElement).value;
258
+ if (searchValue) {
259
+ // dispatch('search', { value: inputElement.value });
260
+ clearTimeout(timeoutHandler);
261
+
262
+ const currentHandler = (timeoutHandler = setTimeout(() => {
263
+ tryQuery(currentHandler, searchValue, true).catch(devCatch);
264
+ }, 1) as unknown as number);
265
+ }
266
+ })
267
+ .catch(devCatch);
268
+ }
269
+ });
270
+ }
271
+ }
272
+
273
+ async function tryQuery(handler: number, value: string, andSelectIt = false) {
274
+ if (timeoutHandler === handler) {
275
+ isLoading = true;
276
+ try {
277
+ const newValues = await query(value);
278
+ isLoading = false;
279
+
280
+ if (timeoutHandler === handler) {
281
+ options = newValues;
282
+
283
+ if (andSelectIt) {
284
+ if (newValues.length === 1) {
285
+ onChange(newValues[0]);
286
+ }
287
+ }
288
+ }
289
+ } catch (e) {
290
+ toastContext.toast(e);
291
+ }
292
+ }
293
+ }
294
+
295
+ function onChange(newValue: V | null | undefined) {
296
+ if (clearable || newValue != null) {
297
+ onchange(newValue ?? null);
298
+ }
299
+ }
300
+
301
+ const an = $derived(vowels.has(name[0].toLocaleLowerCase()) ? 'an' : 'a');
302
+
303
+ let isExpanded = $state(false);
304
+ </script>
305
+
306
+ <!-- #endregion -->
307
+
308
+ <!-- #region HTML -->
309
+
310
+ <div
311
+ class="select-wrapper"
312
+ class:invalid={isInvalid}
313
+ style={`${width ? `--input-width: ${width};` : ''}${height ? `--input-height: ${height};` : ''}`}
314
+ >
315
+ {#if label}
316
+ <label for={inputId}>
317
+ <span
318
+ >{label}{#if !required}&nbsp;- <i>optional</i>{/if}</span
319
+ >
320
+ <span class="border"
321
+ >{label}{#if !required}&nbsp;- <i>optional</i>{/if}</span
322
+ >
323
+ </label>
324
+ {/if}
325
+ <!-- strict mode is used to stop svelecte from rejecting perfectly valid values... wtf -->
326
+ <Svelecte
327
+ strictMode={false}
328
+ keepCreated
329
+ onFocus={(e: HTMLInputElement) => {
330
+ registerInputListeners(e);
331
+ forceShowDropdown(e);
332
+ onfocus?.(e);
333
+ }}
334
+ placeholder="Please Select{name ? ` ${an} ${name}` : ''}"
335
+ bind:this={ref}
336
+ {inputId}
337
+ {disabled}
338
+ valueField={valueProp}
339
+ labelField={labelProp}
340
+ virtualList={false}
341
+ searchable
342
+ searchProps={{ keepSelectionInList: true }}
343
+ lazyDropdown
344
+ {valueAsObject}
345
+ {options}
346
+ value={selectedValue as Array<unknown>}
347
+ {onChange}
348
+ selectOnTab
349
+ deselectMode={required && !clearable ? 'none' : 'toggle'}
350
+ keepSelectionInList
351
+ {name}
352
+ selection={selection as (selectedOptions: Array<object>) => ReturnType<Snippet>}
353
+ {append}
354
+ >
355
+ {#snippet toggleIcon()}
356
+ <div class="nothing"></div>
357
+ <!-- <div class="icon-bg">
358
+ <Icon name="chevron-down" size={Size.MediumSmall} />
359
+ </div> -->
360
+ {/snippet}
361
+ </Svelecte>
362
+
363
+ {#snippet append()}
364
+ <div class="icons">
365
+ {#if clearable && selectedValue}
366
+ <Action
367
+ {disabled}
368
+ noTabIndex
369
+ colourSet={ColourSet.Auxiliary}
370
+ size={Size.Small}
371
+ onclick={(e: MouseEvent) => {
372
+ if (isExpanded) {
373
+ e.stopPropagation();
374
+ }
375
+
376
+ onChange(null);
377
+ }}
378
+ icon={{
379
+ name: 'close-menu-cross',
380
+ colour: Colour['$ui-blue-9'],
381
+ }}
382
+ />
383
+ {/if}
384
+
385
+ <Action
386
+ {disabled}
387
+ noTabIndex
388
+ colourSet={ColourSet.Auxiliary}
389
+ size={Size.Small}
390
+ icon={{
391
+ name: isExpanded ? 'chevron-up' : 'chevron-down',
392
+ colour: Colour['$ui-blue-9'],
393
+ }}
394
+ />
395
+ </div>
396
+ {/snippet}
397
+
398
+ {#snippet selection(selectedOptions: Array<V>)}
399
+ {#if selectedOptions.length == 0}
400
+ <span class="selection">Please Select {an} {name}</span>
401
+ {:else}
402
+ <span class="selection">{selectedOptions[0][labelProp]}</span>
403
+ {/if}
404
+ {/snippet}
405
+ </div>
406
+
407
+ <!-- #endregion -->
408
+
409
+ <!-- #region CSS
410
+ -->
411
411
  <style>.select-wrapper {
412
412
  min-width: var(--input-min-width, 12rem);
413
413
  width: var(--input-width, 100%);
@@ -581,4 +581,4 @@ div.icons {
581
581
  }
582
582
  .dropdown-item .toggle-icon.yes {
583
583
  opacity: 1;
584
- }</style>
584
+ }</style>