@lavalogic/scoria 0.37.44 → 0.37.45

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 (237) hide show
  1. package/README.md +58 -58
  2. package/dist/Components/AccordionGroup.svelte +46 -46
  3. package/dist/Components/AccordionGroupButton.svelte +76 -76
  4. package/dist/Components/Action.svelte +136 -136
  5. package/dist/Components/ActionProps.d.ts +2 -6
  6. package/dist/Components/AlertModal.svelte +195 -183
  7. package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +702 -702
  8. package/dist/Components/Base/BaseModal.svelte +125 -125
  9. package/dist/Components/Base/ContextWrapper.svelte +72 -72
  10. package/dist/Components/Base/Pagination.svelte +198 -198
  11. package/dist/Components/Base/Snippets.svelte +36 -36
  12. package/dist/Components/BigRadioSet.svelte +38 -38
  13. package/dist/Components/Bubble.svelte +101 -101
  14. package/dist/Components/Button.svelte +209 -209
  15. package/dist/Components/ButtonProps.d.ts +2 -6
  16. package/dist/Components/Checkbox.svelte +86 -86
  17. package/dist/Components/CollapsibleCard.svelte +78 -78
  18. package/dist/Components/Contexts/ContextMenu.svelte +164 -164
  19. package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
  20. package/dist/Components/Contexts/ContextMenuOption.svelte +60 -60
  21. package/dist/Components/Contexts/Toast.svelte +83 -83
  22. package/dist/Components/Contexts/ToastContainer.svelte +65 -65
  23. package/dist/Components/Contexts/Tooltip.svelte +37 -37
  24. package/dist/Components/DataMatrixIcon.svelte +112 -112
  25. package/dist/Components/DataMatrixIconProps.d.ts +3 -13
  26. package/dist/Components/DateInput.svelte +148 -148
  27. package/dist/Components/DatePicker.svelte +300 -300
  28. package/dist/Components/DesktopModal.svelte +90 -90
  29. package/dist/Components/Dial.svelte +204 -204
  30. package/dist/Components/DialProps.d.ts +1 -18
  31. package/dist/Components/DimensionInput.svelte +170 -170
  32. package/dist/Components/DragMenu/DragMenuHolder.svelte +119 -119
  33. package/dist/Components/DragMenu/DraggableCard.svelte +77 -77
  34. package/dist/Components/FileCard.svelte +24 -24
  35. package/dist/Components/FileUpload.svelte +236 -236
  36. package/dist/Components/GridInput.svelte +196 -200
  37. package/dist/Components/GridInputSet.svelte +29 -29
  38. package/dist/Components/HorizontalTabGroup.svelte +82 -82
  39. package/dist/Components/HorizontalTabGroupButton.svelte +44 -44
  40. package/dist/Components/Icon.svelte +128 -128
  41. package/dist/Components/IconProps.d.ts +2 -7
  42. package/dist/Components/Icons.js +357 -357
  43. package/dist/Components/LoadingAnimation.svelte +29 -29
  44. package/dist/Components/LoadingModal.svelte +45 -45
  45. package/dist/Components/LoadingOverlay.svelte +25 -25
  46. package/dist/Components/LoadingOverlayProps.d.ts +3 -7
  47. package/dist/Components/Modal.svelte +258 -258
  48. package/dist/Components/ModalProps.d.ts +0 -3
  49. package/dist/Components/MultiSelect.svelte +220 -220
  50. package/dist/Components/MultiSelectCommonProps.d.ts +6 -17
  51. package/dist/Components/Nav/Nav.svelte +99 -99
  52. package/dist/Components/Nav/NavProps.d.ts +4 -11
  53. package/dist/Components/Nav/NavServiceMenuList.svelte +43 -43
  54. package/dist/Components/Nav/NavTab.svelte +62 -62
  55. package/dist/Components/Nav/ServiceMenuItem.svelte +100 -100
  56. package/dist/Components/Nav/ServicesNav.svelte +80 -80
  57. package/dist/Components/NumberInput.svelte +157 -157
  58. package/dist/Components/PageHeading.svelte +25 -25
  59. package/dist/Components/PasswordInput.svelte +88 -88
  60. package/dist/Components/Portal.svelte +36 -36
  61. package/dist/Components/ProgressBubble.svelte +107 -108
  62. package/dist/Components/ProgressBubbleProps.d.ts +3 -3
  63. package/dist/Components/QuerySelect.svelte +366 -366
  64. package/dist/Components/QuerySelectProps.d.ts +4 -14
  65. package/dist/Components/SingleSelect.svelte +250 -250
  66. package/dist/Components/SingleSelectCommonProps.d.ts +3 -19
  67. package/dist/Components/StepButton.svelte +70 -70
  68. package/dist/Components/StepForm.svelte +140 -140
  69. package/dist/Components/Switch.svelte +69 -69
  70. package/dist/Components/Table/ARCHITECTURE.md +298 -0
  71. package/dist/Components/Table/Body/QuickSearchCell.svelte +679 -694
  72. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +51 -51
  73. package/dist/Components/Table/Body/Rows/ColumnList.svelte +59 -59
  74. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +456 -478
  75. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +79 -79
  76. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +185 -188
  77. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +124 -124
  78. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +120 -120
  79. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +230 -230
  80. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +105 -105
  81. package/dist/Components/Table/Body/Rows/Columns/SelectColumn.d.ts +12 -9
  82. package/dist/Components/Table/Body/Rows/Columns/SelectTableCell.d.ts +5 -5
  83. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +87 -87
  84. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +126 -126
  85. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +159 -159
  86. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +246 -246
  87. package/dist/Components/Table/Body/Rows/Columns/TableDatePickerProps.d.ts +4 -4
  88. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +216 -216
  89. package/dist/Components/Table/Body/Rows/Columns/TableQueryCell.d.ts +6 -5
  90. package/dist/Components/Table/Body/Rows/Columns/TableQueryColumn.d.ts +7 -6
  91. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +252 -257
  92. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +179 -179
  93. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +221 -229
  94. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte.d.ts +11 -11
  95. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +197 -197
  96. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +190 -190
  97. package/dist/Components/Table/Body/Rows/TableRow.svelte +290 -290
  98. package/dist/Components/Table/Body/TableBody.svelte +155 -155
  99. package/dist/Components/Table/ColumnDefinitions/ExampleItemColumnDefRepository.svelte.js +2 -3
  100. package/dist/Components/Table/ColumnFactory.svelte.d.ts +9 -11
  101. package/dist/Components/Table/ColumnFactory.svelte.js +101 -118
  102. package/dist/Components/Table/ColumnKindRegistry.d.ts +112 -0
  103. package/dist/Components/Table/ColumnKindRegistry.js +63 -0
  104. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +41 -41
  105. package/dist/Components/Table/Headers/SelectAllHeader.svelte +30 -30
  106. package/dist/Components/Table/Headers/TableHead.svelte +415 -413
  107. package/dist/Components/Table/Misc/ColumnPanel.svelte +94 -94
  108. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +112 -112
  109. package/dist/Components/Table/Misc/FilterInput.svelte +620 -644
  110. package/dist/Components/Table/Misc/FilterInput.svelte.d.ts +12 -12
  111. package/dist/Components/Table/Misc/FilterInputProps.d.ts +2 -2
  112. package/dist/Components/Table/Misc/FilterPanel.svelte +257 -257
  113. package/dist/Components/Table/Misc/FilterPanelModal.svelte +30 -30
  114. package/dist/Components/Table/Misc/SidePanel.svelte +35 -35
  115. package/dist/Components/Table/Misc/TableFooter.svelte +311 -311
  116. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +125 -125
  117. package/dist/Components/Table/Misc/TableSidebar.svelte +157 -157
  118. package/dist/Components/Table/Misc/ToolboxPanel.svelte +100 -100
  119. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +107 -107
  120. package/dist/Components/Table/NestedTable.svelte +130 -133
  121. package/dist/Components/Table/SubApis.svelte.d.ts +1 -5
  122. package/dist/Components/Table/SubApis.svelte.js +33 -32
  123. package/dist/Components/Table/Table.svelte +692 -745
  124. package/dist/Components/Table/TableHelpers.svelte.d.ts +6 -19
  125. package/dist/Components/Table/TableHelpers.svelte.js +6 -14
  126. package/dist/Components/Table/Types/Columns/Column.d.ts +2 -2
  127. package/dist/Components/Table/Types/Columns/DateColumn.d.ts +3 -3
  128. package/dist/Components/Table/Types/Columns/DateTableCell.d.ts +7 -7
  129. package/dist/Components/Table/Types/Columns/Definitions/Accessors/AccessorDef.svelte.d.ts +7 -8
  130. package/dist/Components/Table/Types/Columns/Definitions/Accessors/AccessorDef.svelte.js +4 -5
  131. package/dist/Components/Table/Types/Columns/Definitions/Accessors/CheckboxDef.svelte.d.ts +11 -14
  132. package/dist/Components/Table/Types/Columns/Definitions/Accessors/CheckboxDef.svelte.js +3 -4
  133. package/dist/Components/Table/Types/Columns/Definitions/Accessors/DateInputDef.svelte.d.ts +7 -11
  134. package/dist/Components/Table/Types/Columns/Definitions/Accessors/DateInputDef.svelte.js +3 -3
  135. package/dist/Components/Table/Types/Columns/Definitions/Accessors/NumberInputDef.svelte.d.ts +13 -18
  136. package/dist/Components/Table/Types/Columns/Definitions/Accessors/NumberInputDef.svelte.js +4 -5
  137. package/dist/Components/Table/Types/Columns/Definitions/Accessors/QueryDef.svelte.d.ts +5 -5
  138. package/dist/Components/Table/Types/Columns/Definitions/Accessors/QueryDef.svelte.js +2 -2
  139. package/dist/Components/Table/Types/Columns/Definitions/Accessors/SelectDef.svelte.d.ts +15 -12
  140. package/dist/Components/Table/Types/Columns/Definitions/Accessors/SelectDef.svelte.js +4 -5
  141. package/dist/Components/Table/Types/Columns/Definitions/Accessors/TextInputDef.svelte.d.ts +8 -12
  142. package/dist/Components/Table/Types/Columns/Definitions/Accessors/TextInputDef.svelte.js +3 -3
  143. package/dist/Components/Table/Types/Columns/Definitions/Actions/ActionsDef.svelte.d.ts +6 -6
  144. package/dist/Components/Table/Types/Columns/Definitions/Actions/ActionsDef.svelte.js +3 -3
  145. package/dist/Components/Table/Types/Columns/Definitions/ColumnDef.svelte.d.ts +49 -16
  146. package/dist/Components/Table/Types/Columns/Definitions/ColumnDef.svelte.js +27 -5
  147. package/dist/Components/Table/Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.d.ts +7 -7
  148. package/dist/Components/Table/Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.js +4 -4
  149. package/dist/Components/Table/Types/Columns/Definitions/Display/BubbleDef.svelte.d.ts +5 -5
  150. package/dist/Components/Table/Types/Columns/Definitions/Display/BubbleDef.svelte.js +2 -2
  151. package/dist/Components/Table/Types/Columns/Definitions/Display/DisplayDef.svelte.d.ts +5 -5
  152. package/dist/Components/Table/Types/Columns/Definitions/Display/DisplayDef.svelte.js +2 -2
  153. package/dist/Components/Table/Types/Columns/Definitions/Display/ProgressBarDef.svelte.d.ts +5 -5
  154. package/dist/Components/Table/Types/Columns/Definitions/Display/ProgressBarDef.svelte.js +2 -2
  155. package/dist/Components/Table/Types/Columns/Definitions/Display/ValidityDef.svelte.d.ts +5 -5
  156. package/dist/Components/Table/Types/Columns/Definitions/Display/ValidityDef.svelte.js +2 -2
  157. package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.d.ts +11 -19
  158. package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.js +4 -8
  159. package/dist/Components/Table/Types/Columns/Definitions/FilterOnly/FilterOnlyDef.svelte.d.ts +6 -7
  160. package/dist/Components/Table/Types/Columns/Definitions/FilterOnly/FilterOnlyDef.svelte.js +3 -4
  161. package/dist/Components/Table/Types/Columns/Definitions/RowSelection/RowSelectionDef.svelte.d.ts +9 -9
  162. package/dist/Components/Table/Types/Columns/Definitions/RowSelection/RowSelectionDef.svelte.js +4 -4
  163. package/dist/Components/Table/Types/Columns/ExpandTableCell.d.ts +5 -5
  164. package/dist/Components/Table/Types/Columns/ProgressBarTableCell.d.ts +6 -6
  165. package/dist/Components/Table/Types/Columns/TableCell.d.ts +7 -6
  166. package/dist/Components/Table/Types/Context/ClipboardState.svelte.d.ts +98 -0
  167. package/dist/Components/Table/Types/Context/ClipboardState.svelte.js +290 -0
  168. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.d.ts +17 -45
  169. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.js +17 -45
  170. package/dist/Components/Table/Types/Context/FilteringState.svelte.d.ts +9 -20
  171. package/dist/Components/Table/Types/Context/FilteringState.svelte.js +9 -20
  172. package/dist/Components/Table/Types/Context/FocusState.svelte.d.ts +23 -42
  173. package/dist/Components/Table/Types/Context/FocusState.svelte.js +32 -59
  174. package/dist/Components/Table/Types/Context/ITable.d.ts +5 -8
  175. package/dist/Components/Table/Types/Context/PreferencesState.svelte.d.ts +29 -56
  176. package/dist/Components/Table/Types/Context/PreferencesState.svelte.js +28 -55
  177. package/dist/Components/Table/Types/Context/QueryValuesState.svelte.d.ts +50 -0
  178. package/dist/Components/Table/Types/Context/QueryValuesState.svelte.js +62 -0
  179. package/dist/Components/Table/Types/Context/SelectionState.svelte.d.ts +17 -28
  180. package/dist/Components/Table/Types/Context/SelectionState.svelte.js +17 -28
  181. package/dist/Components/Table/Types/Context/SortingState.svelte.d.ts +15 -29
  182. package/dist/Components/Table/Types/Context/SortingState.svelte.js +17 -31
  183. package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +212 -325
  184. package/dist/Components/Table/Types/Context/TableContext.svelte.js +292 -706
  185. package/dist/Components/Table/Types/Context/VirtualisationState.svelte.d.ts +62 -79
  186. package/dist/Components/Table/Types/Context/VirtualisationState.svelte.js +54 -73
  187. package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.d.ts +16 -27
  188. package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.js +16 -27
  189. package/dist/Components/Table/Types/DataRepository/IDataRepository.d.ts +12 -15
  190. package/dist/Components/Table/Types/DataRepository/TableDataRepository.svelte.d.ts +15 -30
  191. package/dist/Components/Table/Types/DataRepository/TableDataRepository.svelte.js +16 -28
  192. package/dist/Components/Table/Types/Header.d.ts +4 -4
  193. package/dist/Components/Table/Types/HeaderGroup.d.ts +5 -5
  194. package/dist/Components/Table/Types/Public/index.d.ts +2 -4
  195. package/dist/Components/Table/Types/Public/index.js +2 -4
  196. package/dist/Components/Table/clipboardEscape.d.ts +32 -0
  197. package/dist/Components/Table/clipboardEscape.js +45 -0
  198. package/dist/Components/Table/createTable.svelte.d.ts +8 -18
  199. package/dist/Components/Table/createTable.svelte.js +19 -47
  200. package/dist/Components/Test/TestContextConsumer.svelte +13 -13
  201. package/dist/Components/TextArea.svelte +130 -130
  202. package/dist/Components/TextInput.svelte +176 -176
  203. package/dist/Components/ThreeStateRadio.svelte +131 -131
  204. package/dist/Components/Touch/BoolCard.svelte +45 -45
  205. package/dist/Components/Touch/DateModal.svelte +209 -209
  206. package/dist/Components/Touch/EditCard.svelte +59 -59
  207. package/dist/Components/Touch/InfoCard.svelte +31 -31
  208. package/dist/Components/Touch/InfoTextCard.svelte +28 -28
  209. package/dist/Components/Touch/ModalExplanation.svelte +17 -17
  210. package/dist/Components/Touch/NumberKeyboard.svelte +225 -225
  211. package/dist/Components/Touch/NumberModal.svelte +79 -79
  212. package/dist/Components/Touch/PlusMinusCard.svelte +46 -46
  213. package/dist/Components/Touch/SelectModal.svelte +123 -123
  214. package/dist/Components/Touch/SummaryCard.svelte +93 -93
  215. package/dist/Components/Touch/TextAreaModal.svelte +95 -95
  216. package/dist/Components/Touch/TextModal.svelte +106 -106
  217. package/dist/Components/Touch/TouchCard.svelte +33 -33
  218. package/dist/Components/Touch/TouchModal.svelte +274 -274
  219. package/dist/Components/Touch/UtilityButton.svelte +109 -109
  220. package/dist/Components/VerticalTabGroup.svelte +79 -79
  221. package/dist/Components/VerticalTabGroupButton.svelte +58 -58
  222. package/dist/Contexts/ToastContext.svelte.js +3 -4
  223. package/dist/Helpers/Datamatrix.d.ts +19 -19
  224. package/dist/Helpers/Datamatrix.js +291 -291
  225. package/dist/Helpers/Helpers.svelte.d.ts +2 -4
  226. package/dist/Helpers/Helpers.svelte.js +2 -4
  227. package/dist/Helpers/dropdownPortal.d.ts +26 -41
  228. package/dist/Helpers/dropdownPortal.js +62 -116
  229. package/dist/Types/Examples/ExampleModal.svelte +35 -35
  230. package/dist/index.d.ts +0 -1
  231. package/dist/scss/_basics.scss +12 -12
  232. package/dist/scss/_colours.scss +134 -134
  233. package/dist/scss/_mixins.scss +3348 -3360
  234. package/dist/scss/_motion.scss +57 -57
  235. package/dist/scss/_sizing.scss +85 -85
  236. package/dist/scss/_transitions.scss +8 -8
  237. package/package.json +2 -1
@@ -1,696 +1,681 @@
1
- <svelte:options runes />
2
-
3
- <script
4
- lang="ts"
5
- generics="T extends object, IdType extends Primitive, FilterValueType"
6
- >
7
- import { dev } from '$app/environment';
8
- import Action from '../../Action.svelte';
9
- import Button from '../../Button.svelte';
10
- import MultiSelect from '../../MultiSelect.svelte';
11
- import NumberInput from '../../NumberInput.svelte';
12
- import SingleSelect from '../../SingleSelect.svelte';
13
- import TextInput from '../../TextInput.svelte';
14
- import ThreeStateRadio from '../../ThreeStateRadio.svelte';
15
- import DateModal from '../../Touch/DateModal.svelte';
16
- import { hasQuery } from '../TableHelpers.svelte.js';
17
- import { devCatch, isDateTuple, isNumericArray } from '../../../Helpers/Helpers.svelte.js';
18
- import { Colour, ColourSet } from '../../../scss/colours.js';
19
- import { InputVariant } from '../../../Types/Internal/InputVariant.js';
20
- import type { SelectOption } from '../../../Types/Internal/SelectOption.js';
21
- import { Size } from '../../../Types/Internal/Size.js';
22
- import { getContext, onDestroy, onMount, untrack } from 'svelte';
23
- import { cubicOut } from 'svelte/easing';
24
- import { fade } from 'svelte/transition';
25
-
26
- import QuerySelect from '../../QuerySelect.svelte';
27
- import { Ternary } from '../../../Types/Internal/Ternary.js';
28
- import { numberDateFilterTypes } from '../Misc/numberDateFilterTypes.js';
29
- import { stringFilterTypes } from '../Misc/stringFilterTypes.js';
30
- import { quickFilterContextIdentifier } from '../Table.svelte';
31
- import { CheckboxDef } from '../Types/Columns/Definitions/Accessors/CheckboxDef.svelte.js';
32
- import { DateInputDef } from '../Types/Columns/Definitions/Accessors/DateInputDef.svelte.js';
33
- import { NumberInputDef } from '../Types/Columns/Definitions/Accessors/NumberInputDef.svelte.js';
34
- import { SelectDef } from '../Types/Columns/Definitions/Accessors/SelectDef.svelte.js';
35
- import { TableContext } from '../Types/Context/TableContext.svelte.js';
36
- import type { Primitive } from '../Types/Context/TableInitOptions.js';
37
- import { CustomFilterType } from '../Types/Filtering/CustomFilterType.js';
38
- import { NumberDateFilterMode } from '../Types/Filtering/NumberDateFilterMode.js';
39
- import { StringFilterMode } from '../Types/Filtering/StringFilterMode.js';
40
- import type { QuickSearchCellProps } from './QuickSearchCellProps.js';
41
-
42
- const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
43
-
44
- const { column, customFilters }: QuickSearchCellProps<T, FilterValueType> = $props();
45
-
46
- const def = $derived(column.columnDef);
47
-
48
- const customFilter = $derived(customFilters?.get(def.id));
49
-
50
- const isSelect = $derived(
51
- def instanceof SelectDef ||
52
- def.filterValueType == CustomFilterType.Select ||
53
- customFilter?.type === CustomFilterType.Select
54
- );
55
-
56
- const hasFilterFn = $derived(!!def.filterFn && typeof def.filterFn === 'function');
57
-
58
- // A column declares it is filterable when it either (a) has a local
59
- // `filterFn`, (b) declares a `filterValueType` (server-side filtering
60
- // case: `paginationRepo.dateFilter` etc. are only defined for LOCAL
61
- // data repositories, so remote-pagination tables otherwise silently
62
- // hide every typed column from the quick-filter row), or (c) is keyed
63
- // in `customFilters` (remote filter chips supplied via
64
- // `defsWrapper.remoteFilters`).
65
- const hasDeclaredFilterType = $derived(def.filterValueType != null);
66
-
67
- const isSearchable = $derived(
68
- def.allowFiltering &&
69
- (hasFilterFn || hasDeclaredFilterType || customFilters?.has(def.id) == true)
70
- );
71
-
72
- const isNumeric = $derived(
73
- def instanceof NumberInputDef ||
74
- def.filterValueType == 'Number' ||
75
- customFilter?.type === CustomFilterType.Number
76
- );
77
-
78
- function emptyOptsAndWarn(): Array<SelectOption<unknown>> {
79
- console.warn(`def ${def.id} is declared as a select filter but has no getOptions() function`);
80
- return [];
81
- }
82
- const options = $derived(
83
- isSelect ? (def.getOptions?.() ?? customFilter?.options ?? emptyOptsAndWarn()) : []
84
- );
85
- const isDate = $derived(def instanceof DateInputDef);
86
- const isRemoteSelect = $derived(customFilter?.type === CustomFilterType.Select);
87
-
88
- const selectSymbol = Symbol();
89
-
90
- const openedSelect = getContext<{ value: symbol | null } | null | undefined>(
91
- quickFilterContextIdentifier
92
- );
93
-
94
- const isSelectOpen = $derived(openedSelect?.value === selectSymbol);
95
-
96
- // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
97
- let value: unknown | null = $state(null);
98
- let valueMin: number | null = $state(null);
99
- let valueMax: number | null = $state(null);
100
-
101
- let selectedOptions: Array<SelectOption<unknown>> = $state([]);
102
- let lastQueryOptions: Array<SelectOption<unknown>> = $state([]);
103
-
104
- const inputId = $derived(`${tableContext.tableName}-cell-quicksearch-${column.id}`);
105
-
106
- const filterMode = $derived(tableContext.paginationRepo?.filterModes.get(def.id));
107
-
108
- const selectedNumberFilterType: SelectOption<NumberDateFilterMode> | null = $derived.by(() => {
109
- const value = (filterMode as NumberDateFilterMode | undefined) ?? NumberDateFilterMode.In;
110
- return numberDateFilterTypes.find((it) => it.value === value) ?? null;
111
- });
112
-
113
- const selectedStringFilterType: SelectOption<StringFilterMode> | null = $derived.by(() => {
114
- const value = (filterMode as StringFilterMode | undefined) ?? StringFilterMode.Contains;
115
- return stringFilterTypes.find((it) => it.value === value) ?? null;
116
- });
117
-
118
- const filteringStateValue = $derived(
119
- tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value
120
- );
121
-
122
- $effect(() => {
123
- if (isRemoteSelect && !tableContext.paginationRepo?.filtering) {
124
- return;
125
- }
126
- void filteringStateValue;
127
-
128
- if (hasQuery(customFilter)) {
129
- if (def.debug) {
130
- console.debug(`${def.id}: `, 'has query');
131
- }
132
-
133
- untrack(async () => {
134
- const gotValue = filteringStateValue as Array<unknown> | undefined;
135
-
136
- try {
137
- // as unknown as string | number | undefined;
138
-
139
- if (gotValue != undefined) {
140
- if (def.debug) {
141
- console.debug(`${def.id}: query values:`, tableContext.dataRepo.__queryValues);
142
- }
143
- const key = customFilter.id;
144
- const x = await tableContext.dataRepo.__queryValues.get(key);
145
-
146
- if (def.debug) {
147
- console.debug(`${def.id}: `, 'value:', $state.snapshot(x));
148
- }
149
- value = (await x) ?? null;
150
- } else if (def.debug) {
151
- console.debug(
152
- `${def.id}: `,
153
- 'filtering state value was undefined',
154
- $state.snapshot(tableContext.paginationRepo?.filtering)
155
- );
156
- }
157
- } catch (e) {
158
- console.warn(e);
159
- value = null;
160
- }
161
- }).catch(devCatch);
162
- } else if (isSelect) {
163
- if (def.debug) {
164
- console.debug(`${def.id}: `, 'is select (not query)');
165
- }
166
-
167
- if (!tableContext.paginationRepo?.filtering) {
168
- return;
169
- }
170
-
171
- const gotValue = filteringStateValue as Array<unknown> | undefined;
172
-
173
- function isSelectOption(it: unknown): it is SelectOption<unknown> {
174
- return it != null && typeof it == 'object';
175
- }
176
- untrack(async () => {
177
- try {
178
- const opts = await options;
179
- if (def.debug) {
180
- console.debug(`${def.id}: possible options:`, $state.snapshot(opts));
181
- }
182
- const x = opts.filter((it) => {
183
- if (!isSelectOption(it)) {
184
- return false;
185
- }
186
- if (it.value == undefined) {
187
- return true;
188
- }
189
-
190
- if (typeof it.value == 'string' || typeof it.value == 'number') {
191
- return (gotValue as unknown as Array<string | number> | undefined)?.includes(
192
- it.value
193
- );
194
- }
195
-
196
- return false;
197
- });
198
-
199
- if (def.debug) {
200
- console.debug(`${def.id}: `, 'selected options:', x);
201
- }
202
- selectedOptions = x;
203
- } catch (e) {
204
- if (dev || def.debug) {
205
- console.debug(`${def.id}: `, e);
206
- }
207
- }
208
- }).catch(devCatch);
209
- } else if (isNumeric) {
210
- if (
211
- selectedNumberFilterType?.value === NumberDateFilterMode.Between &&
212
- tableContext.paginationRepo?.filtering
213
- ) {
214
- const gotValue = filteringStateValue;
215
- if (gotValue != undefined) {
216
- const [min, max] = gotValue as [number, number | undefined];
217
- value = min;
218
- valueMin = min;
219
- valueMax = max ?? null;
220
- } else {
221
- value = valueMin = valueMax = null;
222
- }
223
- } else if (tableContext.paginationRepo?.filtering) {
224
- const gotValue = filteringStateValue;
225
-
226
- if (gotValue != undefined) {
227
- const [min] = gotValue as [number, number | undefined];
228
- value = min;
229
- valueMin = min;
230
- valueMax = min;
231
- } else {
232
- value = valueMin = valueMax = null;
233
- }
234
- }
235
- } else if (tableContext.paginationRepo?.filtering) {
236
- const gotValue = filteringStateValue;
237
- if (gotValue == null || typeof gotValue == 'string' || typeof gotValue == 'number') {
238
- assertNotPromise(gotValue);
239
- value = gotValue;
240
- }
241
- }
242
- });
243
-
244
- /**
245
- * Dev-only invariant: a Promise must never reach the synchronous primitive
246
- * sink (we should always have awaited it upstream). Throws in dev to make
247
- * any regression loud; no-ops in production builds.
248
- */
249
- function assertNotPromise(gotValue: unknown) {
250
- if (dev && gotValue != null && typeof gotValue == 'object' && gotValue instanceof Promise) {
251
- throw new Error(`Unexpected Promise`);
252
- }
253
- }
254
- onMount(() => {
255
- const filterOption =
256
- tableContext.paginationRepo?.filtering &&
257
- tableContext.paginationRepo.filtering.find((it) => it.id === def.id);
258
-
259
- if (filterOption) {
260
- if (isNumericArray(filterOption.value)) {
261
- [valueMin, valueMax] = filterOption.value;
262
- if (valueMax == undefined) {
263
- valueMax = valueMin;
264
- value = valueMin?.toString();
265
- } else if (valueMin == undefined) {
266
- valueMin = valueMax;
267
- value = valueMax.toString();
268
- } else {
269
- value = valueMin.toString();
270
- }
271
- } else if (!hasQuery(customFilter)) {
272
- const gotValue = filterOption.value;
273
- if (gotValue == null || typeof gotValue == 'string' || typeof gotValue == 'number') {
274
- assertNotPromise(gotValue);
275
- value = gotValue;
276
- }
277
- }
278
- }
279
- });
280
-
281
- function closeOnCtrlF(e: KeyboardEvent) {
282
- if (e.key.toLocaleUpperCase() === 'F' && e.ctrlKey) {
283
- return;
284
- }
285
- e.stopPropagation();
286
- }
287
-
288
- let showFromDateModal = $state(false);
289
- let showToDateModal = $state(false);
290
-
291
- function generateOnChangeObject(_label: string) {
292
- return (newValue: SelectOption<unknown> | null) => {
293
- tableContext.paginationRepo?.filterBy(def.id, newValue?.value ?? null);
294
- // Cache by `def.id` (which equals `customFilter.id`) so the
295
- // `$effect` lookup `__queryValues.get(customFilter.id)`
296
- // finds it on the next reactive pass. Previously we keyed
297
- // the cache by the human-readable `label`, so the effect's
298
- // lookup returned `undefined` and the QuerySelect was
299
- // immediately reset to the placeholder.
300
- tableContext.dataRepo.__queryValues.set(def.id, newValue);
301
- assertNotPromise(newValue);
302
- value = newValue;
303
- };
304
- }
305
-
306
- function generateOnChangePrimitive(_label: string) {
307
- // Phase 7.6: AsPrimitive QuerySelect now hands us the primitive
308
- // directly (previously typed as `SelectOption` with a hidden
309
- // `.value` dig). The discriminated typing makes the flow
310
- // straightforward.
311
- return (newValue: unknown) => {
312
- tableContext.paginationRepo?.filterBy(def.id, newValue ?? null);
313
- // See `generateOnChangeObject` - cache key matches the
314
- // `$effect` lookup above.
315
- tableContext.dataRepo.__queryValues.set(def.id, newValue ?? null);
316
- assertNotPromise(newValue);
317
- value = newValue ?? null;
318
- };
319
- }
320
-
321
- let isNullFilter = $derived(selectedNumberFilterType?.value == NumberDateFilterMode.null);
322
-
323
- /**
324
- * Debounce window for typed filter inputs. For a 10k-row table each
325
- * keystroke would otherwise kick off a full filter pass; coalescing
326
- * adjacent keystrokes within `FILTER_DEBOUNCE_MS` cuts the cost to
327
- * roughly one filter pass per pause-in-typing.
328
- */
329
- const FILTER_DEBOUNCE_MS = 300;
330
-
331
- /**
332
- * Sequence counter used to discard the result of a stale debounce
333
- * timer. When the user types again before the timer fires, the
334
- * `pendingSeq` increments; only the timer whose captured `seq`
335
- * matches `pendingSeq` is allowed to call `filterBy`.
336
- */
337
- let pendingSeq = 0;
338
- let debounceTimer: ReturnType<typeof setTimeout> | undefined;
339
-
340
- function debouncedFilterBy(value: unknown): void {
341
- pendingSeq += 1;
342
- const seq = pendingSeq;
343
- if (debounceTimer !== undefined) {
344
- clearTimeout(debounceTimer);
345
- }
346
- debounceTimer = setTimeout(() => {
347
- debounceTimer = undefined;
348
- if (seq !== pendingSeq) {
349
- // A newer keystroke superseded this one before the timer
350
- // fired; the newer timer will handle the filter call.
351
- return;
352
- }
353
- tableContext.paginationRepo?.filterBy(def.id, value);
354
- }, FILTER_DEBOUNCE_MS);
355
- }
356
-
357
- onDestroy(() => {
358
- if (debounceTimer !== undefined) {
359
- clearTimeout(debounceTimer);
360
- debounceTimer = undefined;
361
- }
362
- });
363
- </script>
364
-
365
- <div class="input-cell {inputId}">
366
- {#if isSearchable}
367
- <div class="input-wrapper-parent">
368
- {#if isDate}
369
- {@const dateValues = tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)
370
- ?.value ?? [null, null]}
371
- {#if isDateTuple(dateValues)}
372
- {@const [fromDate, toDate] = dateValues}
373
- <div class="spaced-around">
374
- <Button
375
- size={Size.FillHeight}
376
- fullWidth
377
- onclick={() => {
378
- showFromDateModal = true;
379
- }}>From {fromDate?.toLocaleDateString() ?? 'Any Date'}</Button
380
- >
381
-
382
- <Button
383
- size={Size.FillHeight}
384
- fullWidth
385
- onclick={() => {
386
- showToDateModal = true;
387
- }}>To {toDate?.toLocaleDateString() ?? 'Any Date'}</Button
388
- >
389
- </div>
390
-
391
- {#if showFromDateModal}
392
- <DateModal
393
- header="Edit Date From"
394
- value={fromDate}
395
- cancelEditing={() => {
396
- showFromDateModal = false;
397
- }}
398
- confirm={(e) => {
399
- tableContext.paginationRepo?.filterBy(def.id, [e, toDate]);
400
- showFromDateModal = false;
401
- }}
402
- />
403
- {/if}
404
-
405
- {#if showToDateModal}
406
- <DateModal
407
- inclusive
408
- header="Edit Date To"
409
- value={toDate}
410
- cancelEditing={() => {
411
- showToDateModal = false;
412
- }}
413
- confirm={(e) => {
414
- tableContext.paginationRepo?.filterBy(def.id, [fromDate, e]);
415
- showToDateModal = false;
416
- }}
417
- />
418
- {/if}
419
- {/if}
420
- {:else if isSelect}
421
- {#if hasQuery(customFilter)}
422
- {#if customFilter.valueAsObject !== true}
423
- {@const label = customFilter.id}
424
-
425
- <!-- TODO: multiselect -->
426
-
427
- <!-- FIXME: Most likely Unsafe -->
428
- <QuerySelect
429
- valueAsObject={false}
430
- clearable
431
- name={customFilter.objectName ?? 'Item'}
432
- valueProp="value"
433
- labelProp="label"
434
- value={value as null}
435
- query={customFilter.query}
436
- bind:options={lastQueryOptions}
437
- onchange={generateOnChangePrimitive(label)}
438
- />
439
- {:else}
440
- {@const label = customFilter.id}
441
-
442
- <!-- TODO: multiselect -->
443
-
444
- <QuerySelect
445
- valueAsObject={true}
446
- clearable
447
- name={customFilter.objectName ?? 'Item'}
448
- valueProp="value"
449
- labelProp="label"
450
- value={value as null}
451
- query={customFilter.query}
452
- bind:options={lastQueryOptions}
453
- onchange={generateOnChangeObject(label)}
454
- />
455
- {/if}
456
- {:else}
457
- {#await options then options}
458
- <MultiSelect
459
- labelProp="label"
460
- valueProp="value"
461
- {options}
462
- valueAsObject
463
- onchange={(e: Array<SelectOption<unknown>>) => {
464
- tableContext.paginationRepo?.filterBy(
465
- def.id,
466
- e.map((it) => it.value)
467
- );
468
- }}
469
- selectedValues={selectedOptions}
470
- collapseSelection
471
- />
472
- {/await}
473
- {/if}
474
- {:else if def instanceof CheckboxDef}
475
- {@const value = (() => {
476
- switch (tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value) {
477
- case true: {
478
- return Ternary.True;
479
- }
480
- case false: {
481
- return Ternary.False;
482
- }
483
- default: {
484
- return Ternary.Neutral;
485
- }
486
- }
487
- })()}
488
- <div class="centered">
489
- <ThreeStateRadio
490
- {value}
491
- yesLabel="Selected"
492
- noLabel="Not Selected"
493
- neutralLabel="Any"
494
- neutralIcon={{ name: 'circle-minus' }}
495
- onchange={(v) => {
496
- switch (v) {
497
- case Ternary.False: {
498
- tableContext.paginationRepo?.filterBy(def.id, false);
499
- return;
500
- }
501
- case Ternary.True: {
502
- tableContext.paginationRepo?.filterBy(def.id, true);
503
- return;
504
- }
505
- case Ternary.Neutral:
506
- default: {
507
- tableContext.paginationRepo?.filterBy(def.id, null);
508
- return;
509
- }
510
- }
511
- }}
512
- />
513
- </div>
514
- {:else if isNumeric}
515
- {#if selectedNumberFilterType?.value === NumberDateFilterMode.Between}
516
- <div class="input-wrapper left">
517
- <NumberInput
518
- data-type="quicksearch"
519
- id={inputId}
520
- bind:value={valueMin}
521
- variant={InputVariant.Table}
522
- placeholder="Min&hellip;"
523
- oninput={() => {
524
- debouncedFilterBy([valueMin, valueMax]);
525
- }}
526
- onkeydown={closeOnCtrlF}
527
- />
528
- </div>
529
- <div class="input-wrapper right">
530
- <NumberInput
531
- data-type="quicksearch"
532
- id={inputId}
533
- bind:value={valueMax}
534
- placeholder="Max&hellip;"
535
- variant={InputVariant.Table}
536
- oninput={() => {
537
- debouncedFilterBy([valueMin, valueMax]);
538
- }}
539
- onkeydown={closeOnCtrlF}
540
- />
541
- </div>
542
- {:else}
543
- <div class="input-wrapper right">
544
- {#await isNullFilter}
545
- loading
546
- {:then isNullFilter}
547
- {#if isNullFilter}
548
- <TextInput
549
- data-type="quicksearch"
550
- id={inputId}
551
- value="is Null"
552
- disabled
553
- variant={InputVariant.Table}
554
- />
555
- {:else}
556
- <!-- NOTE: This is a text input in order to support the __in functionality, which is a comma separated list of numbers -->
557
- <TextInput
558
- data-type="quicksearch"
559
- id={inputId}
560
- bind:value={value as string | null}
561
- placeholder="Filter&hellip;"
562
- variant={InputVariant.Table}
563
- oninput={() => {
564
- debouncedFilterBy([value || null]);
565
- }}
566
- onkeydown={closeOnCtrlF}
567
- />
568
- {/if}
569
- {:catch e}
570
- <!-- eslint-disable-next-line svelte/no-at-debug-tags -->
571
- {@debug e}
572
- {/await}
573
- </div>
574
- {/if}
575
- {:else if value == null || typeof value == 'string'}
576
- <div class="input-wrapper right">
577
- {#if selectedStringFilterType?.value == StringFilterMode.null}
578
- <TextInput
579
- data-type="quicksearch"
580
- id={inputId}
581
- value="is Null"
582
- disabled
583
- variant={InputVariant.Table}
584
- />
585
- {:else if selectedStringFilterType?.value == StringFilterMode.Empty}
586
- <TextInput
587
- data-type="quicksearch"
588
- id={inputId}
589
- value="is Empty"
590
- disabled
591
- variant={InputVariant.Table}
592
- />
593
- {:else}
594
- <TextInput
595
- data-type="quicksearch"
596
- id={inputId}
597
- oninput={() => {
598
- debouncedFilterBy(value);
599
- }}
600
- variant={InputVariant.Table}
601
- bind:value={value as string | null}
602
- placeholder="Filter&hellip;"
603
- />
604
- {/if}
605
- </div>
606
- {/if}
607
-
608
- <!-- TODO button for date, checkbox or select for checkbox -->
609
- </div>
610
-
611
- {#if !isSelect && !isDate && !(def instanceof CheckboxDef)}
612
- <div class="stick-right">
613
- <Action
614
- data-type="quicksearch"
615
- size={Size.FillHeight}
616
- colourSet={ColourSet.Auxiliary}
617
- icon={{
618
- name: 'filter-2',
619
- size: Size.MediumLarge,
620
- colour: Colour['$ui-blue-5'],
621
- }}
622
- onclick={() => {
623
- if (!openedSelect) {
624
- return;
625
- }
626
- if (openedSelect.value === selectSymbol) {
627
- openedSelect.value = null;
628
- } else {
629
- openedSelect.value = selectSymbol;
630
- }
631
- }}
632
- />
633
- </div>
634
- {/if}
635
- {/if}
636
-
637
- {#if isSelectOpen}
638
- <div
639
- class="floating"
640
- transition:fade={{ easing: cubicOut, duration: 100 }}
641
- >
642
- <div class="select-wrapper">
643
- {#if isNumeric}
644
- <SingleSelect
645
- options={numberDateFilterTypes}
646
- labelProp="label"
647
- valueProp="value"
648
- name="Filter Type"
649
- valueAsObject
650
- value={selectedNumberFilterType}
651
- onchange={(newValue: SelectOption<NumberDateFilterMode> | null) => {
652
- const oldFilterType = selectedNumberFilterType;
653
-
654
- const newFilterType = newValue?.value ?? NumberDateFilterMode.In;
655
-
656
- if (newFilterType !== oldFilterType?.value) {
657
- tableContext.paginationRepo?.setFilterMode(def.id, newFilterType);
658
-
659
- if (newFilterType === NumberDateFilterMode.Between) {
660
- tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMin]);
661
- } else if (newFilterType === NumberDateFilterMode.In) {
662
- tableContext.paginationRepo?.filterBy(def.id, [value]);
663
- } else {
664
- tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMax]);
665
- }
666
- }
667
- }}
668
- onkeydown={(e: KeyboardEvent) => {
669
- e.stopPropagation();
670
- }}
671
- />
672
- {:else}
673
- <SingleSelect
674
- options={stringFilterTypes}
675
- labelProp="label"
676
- valueProp="value"
677
- name="Filter Type"
678
- valueAsObject
679
- value={selectedStringFilterType}
680
- onchange={(newValue: SelectOption<StringFilterMode> | null) => {
681
- const filterType = newValue?.value ?? StringFilterMode.Contains;
682
- tableContext.paginationRepo?.setFilterMode(def.id, filterType);
683
- }}
684
- onkeydown={(e: KeyboardEvent) => {
685
- e.stopPropagation();
686
- }}
687
- />
688
- {/if}
689
- </div>
690
- </div>
691
- {/if}
692
- </div>
693
-
1
+ <svelte:options runes />
2
+
3
+ <script
4
+ lang="ts"
5
+ generics="T extends object, IdType extends Primitive, FilterValueType"
6
+ >
7
+ import { dev } from '$app/environment';
8
+ import Action from '../../Action.svelte';
9
+ import Button from '../../Button.svelte';
10
+ import MultiSelect from '../../MultiSelect.svelte';
11
+ import NumberInput from '../../NumberInput.svelte';
12
+ import SingleSelect from '../../SingleSelect.svelte';
13
+ import TextInput from '../../TextInput.svelte';
14
+ import ThreeStateRadio from '../../ThreeStateRadio.svelte';
15
+ import DateModal from '../../Touch/DateModal.svelte';
16
+ import { hasQuery } from '../TableHelpers.svelte.js';
17
+ import { devCatch, isDateTuple, isNumericArray } from '../../../Helpers/Helpers.svelte.js';
18
+ import { Colour, ColourSet } from '../../../scss/colours.js';
19
+ import { InputVariant } from '../../../Types/Internal/InputVariant.js';
20
+ import type { SelectOption } from '../../../Types/Internal/SelectOption.js';
21
+ import { Size } from '../../../Types/Internal/Size.js';
22
+ import { getContext, onDestroy, onMount, untrack } from 'svelte';
23
+ import { cubicOut } from 'svelte/easing';
24
+ import { fade } from 'svelte/transition';
25
+
26
+ import QuerySelect from '../../QuerySelect.svelte';
27
+ import { Ternary } from '../../../Types/Internal/Ternary.js';
28
+ import { numberDateFilterTypes } from '../Misc/numberDateFilterTypes.js';
29
+ import { stringFilterTypes } from '../Misc/stringFilterTypes.js';
30
+ import { quickFilterContextIdentifier } from '../Table.svelte';
31
+ import { CheckboxDef } from '../Types/Columns/Definitions/Accessors/CheckboxDef.svelte.js';
32
+ import { DateInputDef } from '../Types/Columns/Definitions/Accessors/DateInputDef.svelte.js';
33
+ import { NumberInputDef } from '../Types/Columns/Definitions/Accessors/NumberInputDef.svelte.js';
34
+ import { SelectDef } from '../Types/Columns/Definitions/Accessors/SelectDef.svelte.js';
35
+ import { TableContext } from '../Types/Context/TableContext.svelte.js';
36
+ import type { Primitive } from '../Types/Context/TableInitOptions.js';
37
+ import { CustomFilterType } from '../Types/Filtering/CustomFilterType.js';
38
+ import { NumberDateFilterMode } from '../Types/Filtering/NumberDateFilterMode.js';
39
+ import { StringFilterMode } from '../Types/Filtering/StringFilterMode.js';
40
+ import type { QuickSearchCellProps } from './QuickSearchCellProps.js';
41
+
42
+ const tableContext = getContext<TableContext<T, IdType>>(TableContext.identifier);
43
+
44
+ const { column, customFilters }: QuickSearchCellProps<T, FilterValueType> = $props();
45
+
46
+ const def = $derived(column.columnDef);
47
+
48
+ const customFilter = $derived(customFilters?.get(def.id));
49
+
50
+ const isSelect = $derived(
51
+ def instanceof SelectDef ||
52
+ def.filterValueType == CustomFilterType.Select ||
53
+ customFilter?.type === CustomFilterType.Select
54
+ );
55
+
56
+ const hasFilterFn = $derived(!!def.filterFn && typeof def.filterFn === 'function');
57
+
58
+ // A column is filterable when it has any of: a local `filterFn`, a
59
+ // declared `filterValueType` (server-side filtering — required for
60
+ // remote-pagination tables whose data repo never assigns `filterFn`),
61
+ // or a `customFilters` entry from `defsWrapper.remoteFilters`.
62
+ const hasDeclaredFilterType = $derived(def.filterValueType != null);
63
+
64
+ const isSearchable = $derived(
65
+ def.allowFiltering &&
66
+ (hasFilterFn || hasDeclaredFilterType || customFilters?.has(def.id) == true)
67
+ );
68
+
69
+ const isNumeric = $derived(
70
+ def instanceof NumberInputDef ||
71
+ def.filterValueType == 'Number' ||
72
+ customFilter?.type === CustomFilterType.Number
73
+ );
74
+
75
+ function emptyOptsAndWarn(): Array<SelectOption<unknown>> {
76
+ console.warn(`def ${def.id} is declared as a select filter but has no getOptions() function`);
77
+ return [];
78
+ }
79
+ const options = $derived(
80
+ isSelect ? (def.getOptions?.() ?? customFilter?.options ?? emptyOptsAndWarn()) : []
81
+ );
82
+ const isDate = $derived(def instanceof DateInputDef);
83
+ const isRemoteSelect = $derived(customFilter?.type === CustomFilterType.Select);
84
+
85
+ const selectSymbol = Symbol();
86
+
87
+ const openedSelect = getContext<{ value: symbol | null } | null | undefined>(
88
+ quickFilterContextIdentifier
89
+ );
90
+
91
+ const isSelectOpen = $derived(openedSelect?.value === selectSymbol);
92
+
93
+ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
94
+ let value: unknown | null = $state(null);
95
+ let valueMin: number | null = $state(null);
96
+ let valueMax: number | null = $state(null);
97
+
98
+ let selectedOptions: Array<SelectOption<unknown>> = $state([]);
99
+ let lastQueryOptions: Array<SelectOption<unknown>> = $state([]);
100
+
101
+ const inputId = $derived(`${tableContext.tableName}-cell-quicksearch-${column.id}`);
102
+
103
+ const filterMode = $derived(tableContext.paginationRepo?.filterModes.get(def.id));
104
+
105
+ const selectedNumberFilterType: SelectOption<NumberDateFilterMode> | null = $derived.by(() => {
106
+ const value = (filterMode as NumberDateFilterMode | undefined) ?? NumberDateFilterMode.In;
107
+ return numberDateFilterTypes.find((it) => it.value === value) ?? null;
108
+ });
109
+
110
+ const selectedStringFilterType: SelectOption<StringFilterMode> | null = $derived.by(() => {
111
+ const value = (filterMode as StringFilterMode | undefined) ?? StringFilterMode.Contains;
112
+ return stringFilterTypes.find((it) => it.value === value) ?? null;
113
+ });
114
+
115
+ const filteringStateValue = $derived(
116
+ tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value
117
+ );
118
+
119
+ $effect(() => {
120
+ if (isRemoteSelect && !tableContext.paginationRepo?.filtering) {
121
+ return;
122
+ }
123
+ void filteringStateValue;
124
+
125
+ if (hasQuery(customFilter)) {
126
+ if (def.debug) {
127
+ console.debug(`${def.id}: `, 'has query');
128
+ }
129
+
130
+ untrack(async () => {
131
+ const gotValue = filteringStateValue as Array<unknown> | undefined;
132
+
133
+ try {
134
+ // as unknown as string | number | undefined;
135
+
136
+ if (gotValue != undefined) {
137
+ if (def.debug) {
138
+ console.debug(`${def.id}: query values:`, tableContext.queryValues.snapshot());
139
+ }
140
+ const x = tableContext.queryValues.get(def);
141
+
142
+ if (def.debug) {
143
+ console.debug(`${def.id}: `, 'value:', $state.snapshot(x));
144
+ }
145
+ value = x ?? null;
146
+ } else if (def.debug) {
147
+ console.debug(
148
+ `${def.id}: `,
149
+ 'filtering state value was undefined',
150
+ $state.snapshot(tableContext.paginationRepo?.filtering)
151
+ );
152
+ }
153
+ } catch (e) {
154
+ console.warn(e);
155
+ value = null;
156
+ }
157
+ }).catch(devCatch);
158
+ } else if (isSelect) {
159
+ if (def.debug) {
160
+ console.debug(`${def.id}: `, 'is select (not query)');
161
+ }
162
+
163
+ if (!tableContext.paginationRepo?.filtering) {
164
+ return;
165
+ }
166
+
167
+ const gotValue = filteringStateValue as Array<unknown> | undefined;
168
+
169
+ function isSelectOption(it: unknown): it is SelectOption<unknown> {
170
+ return it != null && typeof it == 'object';
171
+ }
172
+ untrack(async () => {
173
+ try {
174
+ const opts = await options;
175
+ if (def.debug) {
176
+ console.debug(`${def.id}: possible options:`, $state.snapshot(opts));
177
+ }
178
+ const x = opts.filter((it) => {
179
+ if (!isSelectOption(it)) {
180
+ return false;
181
+ }
182
+ if (it.value == undefined) {
183
+ return true;
184
+ }
185
+
186
+ if (typeof it.value == 'string' || typeof it.value == 'number') {
187
+ return (gotValue as unknown as Array<string | number> | undefined)?.includes(
188
+ it.value
189
+ );
190
+ }
191
+
192
+ return false;
193
+ });
194
+
195
+ if (def.debug) {
196
+ console.debug(`${def.id}: `, 'selected options:', x);
197
+ }
198
+ selectedOptions = x;
199
+ } catch (e) {
200
+ if (dev || def.debug) {
201
+ console.debug(`${def.id}: `, e);
202
+ }
203
+ }
204
+ }).catch(devCatch);
205
+ } else if (isNumeric) {
206
+ if (
207
+ selectedNumberFilterType?.value === NumberDateFilterMode.Between &&
208
+ tableContext.paginationRepo?.filtering
209
+ ) {
210
+ const gotValue = filteringStateValue;
211
+ if (gotValue != undefined) {
212
+ const [min, max] = gotValue as [number, number | undefined];
213
+ value = min;
214
+ valueMin = min;
215
+ valueMax = max ?? null;
216
+ } else {
217
+ value = valueMin = valueMax = null;
218
+ }
219
+ } else if (tableContext.paginationRepo?.filtering) {
220
+ const gotValue = filteringStateValue;
221
+
222
+ if (gotValue != undefined) {
223
+ const [min] = gotValue as [number, number | undefined];
224
+ value = min;
225
+ valueMin = min;
226
+ valueMax = min;
227
+ } else {
228
+ value = valueMin = valueMax = null;
229
+ }
230
+ }
231
+ } else if (tableContext.paginationRepo?.filtering) {
232
+ const gotValue = filteringStateValue;
233
+ if (gotValue == null || typeof gotValue == 'string' || typeof gotValue == 'number') {
234
+ assertNotPromise(gotValue);
235
+ value = gotValue;
236
+ }
237
+ }
238
+ });
239
+
240
+ /**
241
+ * Dev-only invariant: a Promise must never reach the synchronous primitive
242
+ * sink (we should always have awaited it upstream). Throws in dev to make
243
+ * any regression loud; no-ops in production builds.
244
+ */
245
+ function assertNotPromise(gotValue: unknown) {
246
+ if (dev && gotValue != null && typeof gotValue == 'object' && gotValue instanceof Promise) {
247
+ throw new Error(`Unexpected Promise`);
248
+ }
249
+ }
250
+ onMount(() => {
251
+ const filterOption =
252
+ tableContext.paginationRepo?.filtering &&
253
+ tableContext.paginationRepo.filtering.find((it) => it.id === def.id);
254
+
255
+ if (filterOption) {
256
+ if (isNumericArray(filterOption.value)) {
257
+ [valueMin, valueMax] = filterOption.value;
258
+ if (valueMax == undefined) {
259
+ valueMax = valueMin;
260
+ value = valueMin?.toString();
261
+ } else if (valueMin == undefined) {
262
+ valueMin = valueMax;
263
+ value = valueMax.toString();
264
+ } else {
265
+ value = valueMin.toString();
266
+ }
267
+ } else if (!hasQuery(customFilter)) {
268
+ const gotValue = filterOption.value;
269
+ if (gotValue == null || typeof gotValue == 'string' || typeof gotValue == 'number') {
270
+ assertNotPromise(gotValue);
271
+ value = gotValue;
272
+ }
273
+ }
274
+ }
275
+ });
276
+
277
+ function closeOnCtrlF(e: KeyboardEvent) {
278
+ if (e.key.toLocaleUpperCase() === 'F' && e.ctrlKey) {
279
+ return;
280
+ }
281
+ e.stopPropagation();
282
+ }
283
+
284
+ let showFromDateModal = $state(false);
285
+ let showToDateModal = $state(false);
286
+
287
+ function generateOnChangeObject(_label: string) {
288
+ return (newValue: SelectOption<unknown> | null) => {
289
+ tableContext.paginationRepo?.filterBy(def.id, newValue?.value ?? null);
290
+ tableContext.queryValues.set(def, newValue);
291
+ assertNotPromise(newValue);
292
+ value = newValue;
293
+ };
294
+ }
295
+
296
+ function generateOnChangePrimitive(_label: string) {
297
+ // AsPrimitive QuerySelect hands us the primitive value directly.
298
+ return (newValue: unknown) => {
299
+ tableContext.paginationRepo?.filterBy(def.id, newValue ?? null);
300
+ tableContext.queryValues.set(def, newValue ?? null);
301
+ assertNotPromise(newValue);
302
+ value = newValue ?? null;
303
+ };
304
+ }
305
+
306
+ let isNullFilter = $derived(selectedNumberFilterType?.value == NumberDateFilterMode.null);
307
+
308
+ /**
309
+ * Debounce window for typed filter inputs. For a 10k-row table each
310
+ * keystroke would otherwise kick off a full filter pass; coalescing
311
+ * adjacent keystrokes within `FILTER_DEBOUNCE_MS` cuts the cost to
312
+ * roughly one filter pass per pause-in-typing.
313
+ */
314
+ const FILTER_DEBOUNCE_MS = 300;
315
+
316
+ /**
317
+ * Sequence counter used to discard the result of a stale debounce
318
+ * timer. When the user types again before the timer fires, the
319
+ * `pendingSeq` increments; only the timer whose captured `seq`
320
+ * matches `pendingSeq` is allowed to call `filterBy`.
321
+ */
322
+ let pendingSeq = 0;
323
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined;
324
+
325
+ function debouncedFilterBy(value: unknown): void {
326
+ pendingSeq += 1;
327
+ const seq = pendingSeq;
328
+ if (debounceTimer !== undefined) {
329
+ clearTimeout(debounceTimer);
330
+ }
331
+ debounceTimer = setTimeout(() => {
332
+ debounceTimer = undefined;
333
+ if (seq !== pendingSeq) {
334
+ // A newer keystroke superseded this one before the timer
335
+ // fired; the newer timer will handle the filter call.
336
+ return;
337
+ }
338
+ tableContext.paginationRepo?.filterBy(def.id, value);
339
+ }, FILTER_DEBOUNCE_MS);
340
+ }
341
+
342
+ onDestroy(() => {
343
+ if (debounceTimer !== undefined) {
344
+ clearTimeout(debounceTimer);
345
+ debounceTimer = undefined;
346
+ }
347
+ });
348
+ </script>
349
+
350
+ <div class="input-cell {inputId}">
351
+ {#if isSearchable}
352
+ <div class="input-wrapper-parent">
353
+ {#if isDate}
354
+ {@const dateValues = tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)
355
+ ?.value ?? [null, null]}
356
+ {#if isDateTuple(dateValues)}
357
+ {@const [fromDate, toDate] = dateValues}
358
+ <div class="spaced-around">
359
+ <Button
360
+ size={Size.FillHeight}
361
+ fullWidth
362
+ onclick={() => {
363
+ showFromDateModal = true;
364
+ }}>From {fromDate?.toLocaleDateString() ?? 'Any Date'}</Button
365
+ >
366
+
367
+ <Button
368
+ size={Size.FillHeight}
369
+ fullWidth
370
+ onclick={() => {
371
+ showToDateModal = true;
372
+ }}>To {toDate?.toLocaleDateString() ?? 'Any Date'}</Button
373
+ >
374
+ </div>
375
+
376
+ {#if showFromDateModal}
377
+ <DateModal
378
+ header="Edit Date From"
379
+ value={fromDate}
380
+ cancelEditing={() => {
381
+ showFromDateModal = false;
382
+ }}
383
+ confirm={(e) => {
384
+ tableContext.paginationRepo?.filterBy(def.id, [e, toDate]);
385
+ showFromDateModal = false;
386
+ }}
387
+ />
388
+ {/if}
389
+
390
+ {#if showToDateModal}
391
+ <DateModal
392
+ inclusive
393
+ header="Edit Date To"
394
+ value={toDate}
395
+ cancelEditing={() => {
396
+ showToDateModal = false;
397
+ }}
398
+ confirm={(e) => {
399
+ tableContext.paginationRepo?.filterBy(def.id, [fromDate, e]);
400
+ showToDateModal = false;
401
+ }}
402
+ />
403
+ {/if}
404
+ {/if}
405
+ {:else if isSelect}
406
+ {#if hasQuery(customFilter)}
407
+ {#if customFilter.valueAsObject !== true}
408
+ {@const label = customFilter.id}
409
+
410
+ <!-- TODO: multiselect -->
411
+
412
+ <!-- FIXME: Most likely Unsafe -->
413
+ <QuerySelect
414
+ valueAsObject={false}
415
+ clearable
416
+ name={customFilter.objectName ?? 'Item'}
417
+ valueProp="value"
418
+ labelProp="label"
419
+ value={value as null}
420
+ query={customFilter.query}
421
+ bind:options={lastQueryOptions}
422
+ onchange={generateOnChangePrimitive(label)}
423
+ />
424
+ {:else}
425
+ {@const label = customFilter.id}
426
+
427
+ <!-- TODO: multiselect -->
428
+
429
+ <QuerySelect
430
+ valueAsObject={true}
431
+ clearable
432
+ name={customFilter.objectName ?? 'Item'}
433
+ valueProp="value"
434
+ labelProp="label"
435
+ value={value as null}
436
+ query={customFilter.query}
437
+ bind:options={lastQueryOptions}
438
+ onchange={generateOnChangeObject(label)}
439
+ />
440
+ {/if}
441
+ {:else}
442
+ {#await options then options}
443
+ <MultiSelect
444
+ labelProp="label"
445
+ valueProp="value"
446
+ {options}
447
+ valueAsObject
448
+ onchange={(e: Array<SelectOption<unknown>>) => {
449
+ tableContext.paginationRepo?.filterBy(
450
+ def.id,
451
+ e.map((it) => it.value)
452
+ );
453
+ }}
454
+ selectedValues={selectedOptions}
455
+ collapseSelection
456
+ />
457
+ {/await}
458
+ {/if}
459
+ {:else if def instanceof CheckboxDef}
460
+ {@const value = (() => {
461
+ switch (tableContext.paginationRepo?.filtering.find((it) => it.id === def.id)?.value) {
462
+ case true: {
463
+ return Ternary.True;
464
+ }
465
+ case false: {
466
+ return Ternary.False;
467
+ }
468
+ default: {
469
+ return Ternary.Neutral;
470
+ }
471
+ }
472
+ })()}
473
+ <div class="centered">
474
+ <ThreeStateRadio
475
+ {value}
476
+ yesLabel="Selected"
477
+ noLabel="Not Selected"
478
+ neutralLabel="Any"
479
+ neutralIcon={{ name: 'circle-minus' }}
480
+ onchange={(v) => {
481
+ switch (v) {
482
+ case Ternary.False: {
483
+ tableContext.paginationRepo?.filterBy(def.id, false);
484
+ return;
485
+ }
486
+ case Ternary.True: {
487
+ tableContext.paginationRepo?.filterBy(def.id, true);
488
+ return;
489
+ }
490
+ case Ternary.Neutral:
491
+ default: {
492
+ tableContext.paginationRepo?.filterBy(def.id, null);
493
+ return;
494
+ }
495
+ }
496
+ }}
497
+ />
498
+ </div>
499
+ {:else if isNumeric}
500
+ {#if selectedNumberFilterType?.value === NumberDateFilterMode.Between}
501
+ <div class="input-wrapper left">
502
+ <NumberInput
503
+ data-type="quicksearch"
504
+ id={inputId}
505
+ bind:value={valueMin}
506
+ variant={InputVariant.Table}
507
+ placeholder="Min&hellip;"
508
+ oninput={() => {
509
+ debouncedFilterBy([valueMin, valueMax]);
510
+ }}
511
+ onkeydown={closeOnCtrlF}
512
+ />
513
+ </div>
514
+ <div class="input-wrapper right">
515
+ <NumberInput
516
+ data-type="quicksearch"
517
+ id={inputId}
518
+ bind:value={valueMax}
519
+ placeholder="Max&hellip;"
520
+ variant={InputVariant.Table}
521
+ oninput={() => {
522
+ debouncedFilterBy([valueMin, valueMax]);
523
+ }}
524
+ onkeydown={closeOnCtrlF}
525
+ />
526
+ </div>
527
+ {:else}
528
+ <div class="input-wrapper right">
529
+ {#await isNullFilter}
530
+ loading
531
+ {:then isNullFilter}
532
+ {#if isNullFilter}
533
+ <TextInput
534
+ data-type="quicksearch"
535
+ id={inputId}
536
+ value="is Null"
537
+ disabled
538
+ variant={InputVariant.Table}
539
+ />
540
+ {:else}
541
+ <!-- NOTE: This is a text input in order to support the __in functionality, which is a comma separated list of numbers -->
542
+ <TextInput
543
+ data-type="quicksearch"
544
+ id={inputId}
545
+ bind:value={value as string | null}
546
+ placeholder="Filter&hellip;"
547
+ variant={InputVariant.Table}
548
+ oninput={() => {
549
+ debouncedFilterBy([value || null]);
550
+ }}
551
+ onkeydown={closeOnCtrlF}
552
+ />
553
+ {/if}
554
+ {:catch e}
555
+ <!-- eslint-disable-next-line svelte/no-at-debug-tags -->
556
+ {@debug e}
557
+ {/await}
558
+ </div>
559
+ {/if}
560
+ {:else if value == null || typeof value == 'string'}
561
+ <div class="input-wrapper right">
562
+ {#if selectedStringFilterType?.value == StringFilterMode.null}
563
+ <TextInput
564
+ data-type="quicksearch"
565
+ id={inputId}
566
+ value="is Null"
567
+ disabled
568
+ variant={InputVariant.Table}
569
+ />
570
+ {:else if selectedStringFilterType?.value == StringFilterMode.Empty}
571
+ <TextInput
572
+ data-type="quicksearch"
573
+ id={inputId}
574
+ value="is Empty"
575
+ disabled
576
+ variant={InputVariant.Table}
577
+ />
578
+ {:else}
579
+ <TextInput
580
+ data-type="quicksearch"
581
+ id={inputId}
582
+ oninput={() => {
583
+ debouncedFilterBy(value);
584
+ }}
585
+ variant={InputVariant.Table}
586
+ bind:value={value as string | null}
587
+ placeholder="Filter&hellip;"
588
+ />
589
+ {/if}
590
+ </div>
591
+ {/if}
592
+
593
+ <!-- TODO button for date, checkbox or select for checkbox -->
594
+ </div>
595
+
596
+ {#if !isSelect && !isDate && !(def instanceof CheckboxDef)}
597
+ <div class="stick-right">
598
+ <Action
599
+ data-type="quicksearch"
600
+ size={Size.FillHeight}
601
+ colourSet={ColourSet.Auxiliary}
602
+ icon={{
603
+ name: 'filter-2',
604
+ size: Size.MediumLarge,
605
+ colour: Colour['$ui-blue-5'],
606
+ }}
607
+ onclick={() => {
608
+ if (!openedSelect) {
609
+ return;
610
+ }
611
+ if (openedSelect.value === selectSymbol) {
612
+ openedSelect.value = null;
613
+ } else {
614
+ openedSelect.value = selectSymbol;
615
+ }
616
+ }}
617
+ />
618
+ </div>
619
+ {/if}
620
+ {/if}
621
+
622
+ {#if isSelectOpen}
623
+ <div
624
+ class="floating"
625
+ transition:fade={{ easing: cubicOut, duration: 100 }}
626
+ >
627
+ <div class="select-wrapper">
628
+ {#if isNumeric}
629
+ <SingleSelect
630
+ options={numberDateFilterTypes}
631
+ labelProp="label"
632
+ valueProp="value"
633
+ name="Filter Type"
634
+ valueAsObject
635
+ value={selectedNumberFilterType}
636
+ onchange={(newValue: SelectOption<NumberDateFilterMode> | null) => {
637
+ const oldFilterType = selectedNumberFilterType;
638
+
639
+ const newFilterType = newValue?.value ?? NumberDateFilterMode.In;
640
+
641
+ if (newFilterType !== oldFilterType?.value) {
642
+ tableContext.paginationRepo?.setFilterMode(def.id, newFilterType);
643
+
644
+ if (newFilterType === NumberDateFilterMode.Between) {
645
+ tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMin]);
646
+ } else if (newFilterType === NumberDateFilterMode.In) {
647
+ tableContext.paginationRepo?.filterBy(def.id, [value]);
648
+ } else {
649
+ tableContext.paginationRepo?.filterBy(def.id, [valueMin, valueMax]);
650
+ }
651
+ }
652
+ }}
653
+ onkeydown={(e: KeyboardEvent) => {
654
+ e.stopPropagation();
655
+ }}
656
+ />
657
+ {:else}
658
+ <SingleSelect
659
+ options={stringFilterTypes}
660
+ labelProp="label"
661
+ valueProp="value"
662
+ name="Filter Type"
663
+ valueAsObject
664
+ value={selectedStringFilterType}
665
+ onchange={(newValue: SelectOption<StringFilterMode> | null) => {
666
+ const filterType = newValue?.value ?? StringFilterMode.Contains;
667
+ tableContext.paginationRepo?.setFilterMode(def.id, filterType);
668
+ }}
669
+ onkeydown={(e: KeyboardEvent) => {
670
+ e.stopPropagation();
671
+ }}
672
+ />
673
+ {/if}
674
+ </div>
675
+ </div>
676
+ {/if}
677
+ </div>
678
+
694
679
  <style>.focusable,
695
680
  .checkbox-cell,
696
681
  .input-cell,
@@ -825,4 +810,4 @@ div.input-wrapper-parent div.input-wrapper.right {
825
810
  height: 60px;
826
811
  --input-height: 60px;
827
812
  background-color: #ffffff;
828
- }</style>
813
+ }</style>