@lavalogic/scoria 0.35.0 → 0.36.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 (126) hide show
  1. package/README.md +58 -58
  2. package/dist/Components/AccordionGroup.svelte +42 -42
  3. package/dist/Components/AccordionGroupButton.svelte +99 -99
  4. package/dist/Components/Action.svelte +138 -138
  5. package/dist/Components/AlertModal.svelte +125 -125
  6. package/dist/Components/Base/ContextWrapper.svelte +50 -50
  7. package/dist/Components/Base/Pagination.svelte +190 -190
  8. package/dist/Components/Base/Snippets.svelte +29 -29
  9. package/dist/Components/BigRadioSet.svelte +29 -29
  10. package/dist/Components/Bubble.svelte +77 -77
  11. package/dist/Components/Button.svelte +197 -197
  12. package/dist/Components/Checkbox.svelte +68 -68
  13. package/dist/Components/CollapsibleCard.svelte +70 -70
  14. package/dist/Components/Contexts/ContextMenu.svelte +157 -157
  15. package/dist/Components/Contexts/ContextMenuDivider.svelte +11 -11
  16. package/dist/Components/Contexts/ContextMenuOption.svelte +52 -52
  17. package/dist/Components/Contexts/Toast.svelte +61 -61
  18. package/dist/Components/Contexts/ToastContainer.svelte +46 -46
  19. package/dist/Components/Contexts/Tooltip.svelte +26 -26
  20. package/dist/Components/DataMatrixIcon.svelte +41 -41
  21. package/dist/Components/DateInput.svelte +109 -109
  22. package/dist/Components/DatePicker.svelte +312 -312
  23. package/dist/Components/DesktopModal.svelte +106 -106
  24. package/dist/Components/Dial.svelte +151 -151
  25. package/dist/Components/DimensionInput.svelte +184 -184
  26. package/dist/Components/DragMenu/DragMenuHolder.svelte +106 -106
  27. package/dist/Components/DragMenu/DraggableCard.svelte +41 -41
  28. package/dist/Components/FileCard.svelte +20 -20
  29. package/dist/Components/FileUpload.svelte +196 -196
  30. package/dist/Components/GridInput.svelte +190 -190
  31. package/dist/Components/GridInputSet.svelte +22 -22
  32. package/dist/Components/HorizontalTabGroup.svelte +78 -78
  33. package/dist/Components/HorizontalTabGroupButton.svelte +96 -96
  34. package/dist/Components/Icon.svelte +97 -97
  35. package/dist/Components/Icons.js +357 -357
  36. package/dist/Components/LoadingAnimation.svelte +32 -32
  37. package/dist/Components/LoadingModal.svelte +47 -47
  38. package/dist/Components/LoadingOverlay.svelte +27 -27
  39. package/dist/Components/Modal.svelte +251 -251
  40. package/dist/Components/MultiSelect.svelte +197 -197
  41. package/dist/Components/Nav/Nav.svelte +97 -97
  42. package/dist/Components/Nav/NavServiceMenuList.svelte +34 -34
  43. package/dist/Components/Nav/NavTab.svelte +91 -91
  44. package/dist/Components/Nav/ServiceMenuItem.svelte +91 -91
  45. package/dist/Components/Nav/ServicesNav.svelte +53 -53
  46. package/dist/Components/NumberInput.svelte +144 -144
  47. package/dist/Components/PageHeading.svelte +36 -36
  48. package/dist/Components/PasswordInput.svelte +73 -73
  49. package/dist/Components/Portal.svelte +25 -25
  50. package/dist/Components/ProgressBubble.svelte +80 -80
  51. package/dist/Components/QuerySelect.svelte +411 -411
  52. package/dist/Components/SingleSelect.svelte +261 -261
  53. package/dist/Components/StepButton.svelte +120 -120
  54. package/dist/Components/StepForm.svelte +108 -108
  55. package/dist/Components/Switch.svelte +36 -36
  56. package/dist/Components/Table/Body/QuickSearchCell.svelte +632 -632
  57. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +30 -30
  58. package/dist/Components/Table/Body/Rows/ColumnList.svelte +56 -56
  59. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +179 -179
  60. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +49 -49
  61. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +180 -180
  62. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +102 -102
  63. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +119 -119
  64. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +114 -114
  65. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +96 -96
  66. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +72 -72
  67. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +131 -131
  68. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +144 -144
  69. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +238 -238
  70. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +358 -358
  71. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +338 -338
  72. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +183 -183
  73. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +309 -309
  74. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +369 -369
  75. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +170 -170
  76. package/dist/Components/Table/Body/Rows/TableRow.svelte +259 -259
  77. package/dist/Components/Table/Body/TableBody.svelte +67 -67
  78. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +39 -39
  79. package/dist/Components/Table/Headers/SelectAllHeader.svelte +28 -28
  80. package/dist/Components/Table/Headers/TableHead.svelte +260 -260
  81. package/dist/Components/Table/Misc/ColumnPanel.svelte +78 -78
  82. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +99 -99
  83. package/dist/Components/Table/Misc/FilterInput.svelte +556 -556
  84. package/dist/Components/Table/Misc/FilterPanel.svelte +46 -46
  85. package/dist/Components/Table/Misc/FilterPanelModal.svelte +67 -67
  86. package/dist/Components/Table/Misc/SidePanel.svelte +32 -32
  87. package/dist/Components/Table/Misc/TableFooter.svelte +304 -304
  88. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +156 -156
  89. package/dist/Components/Table/Misc/TableSidebar.svelte +176 -176
  90. package/dist/Components/Table/Misc/ToolboxPanel.svelte +77 -77
  91. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +91 -91
  92. package/dist/Components/Table/NestedTable.svelte +84 -84
  93. package/dist/Components/Table/Table.svelte +492 -492
  94. package/dist/Components/Test/TestContextConsumer.svelte +13 -13
  95. package/dist/Components/TextArea.svelte +108 -108
  96. package/dist/Components/TextInput.svelte +147 -147
  97. package/dist/Components/ThreeStateRadio.svelte +124 -124
  98. package/dist/Components/Touch/BoolCard.svelte +52 -52
  99. package/dist/Components/Touch/DateModal.svelte +217 -217
  100. package/dist/Components/Touch/EditCard.svelte +52 -52
  101. package/dist/Components/Touch/InfoCard.svelte +24 -24
  102. package/dist/Components/Touch/InfoTextCard.svelte +21 -21
  103. package/dist/Components/Touch/ModalExplanation.svelte +20 -20
  104. package/dist/Components/Touch/NumberKeyboard.svelte +268 -268
  105. package/dist/Components/Touch/NumberModal.svelte +69 -69
  106. package/dist/Components/Touch/PlusMinusCard.svelte +50 -50
  107. package/dist/Components/Touch/SelectModal.svelte +105 -105
  108. package/dist/Components/Touch/SummaryCard.svelte +65 -65
  109. package/dist/Components/Touch/TextAreaModal.svelte +86 -86
  110. package/dist/Components/Touch/TextModal.svelte +123 -123
  111. package/dist/Components/Touch/TouchCard.svelte +28 -28
  112. package/dist/Components/Touch/TouchModal.svelte +259 -259
  113. package/dist/Components/Touch/UtilityButton.svelte +91 -91
  114. package/dist/Components/VerticalTabGroup.svelte +66 -66
  115. package/dist/Components/VerticalTabGroupButton.svelte +97 -97
  116. package/dist/Helpers/Datamatrix.d.ts +10 -10
  117. package/dist/Helpers/Datamatrix.js +1 -1
  118. package/dist/Helpers/Helpers.svelte.d.ts +1 -0
  119. package/dist/Helpers/Helpers.svelte.js +15 -0
  120. package/dist/Types/Examples/ExampleModal.svelte +34 -34
  121. package/dist/scss/_basics.scss +12 -12
  122. package/dist/scss/_colours.scss +134 -134
  123. package/dist/scss/_mixins.scss +3016 -3016
  124. package/dist/scss/_sizing.scss +69 -69
  125. package/dist/scss/_transitions.scss +8 -8
  126. package/package.json +30 -30
@@ -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>