@lavalogic/scoria 0.37.44 → 0.37.46

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 (239) 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 +86 -83
  22. package/dist/Components/Contexts/Toast.svelte.d.ts +8 -5
  23. package/dist/Components/Contexts/ToastContainer.svelte +65 -65
  24. package/dist/Components/Contexts/Tooltip.svelte +37 -37
  25. package/dist/Components/DataMatrixIcon.svelte +112 -112
  26. package/dist/Components/DataMatrixIconProps.d.ts +3 -13
  27. package/dist/Components/DateInput.svelte +148 -148
  28. package/dist/Components/DatePicker.svelte +300 -300
  29. package/dist/Components/DesktopModal.svelte +90 -90
  30. package/dist/Components/Dial.svelte +204 -204
  31. package/dist/Components/DialProps.d.ts +1 -18
  32. package/dist/Components/DimensionInput.svelte +170 -170
  33. package/dist/Components/DragMenu/DragMenuHolder.svelte +119 -119
  34. package/dist/Components/DragMenu/DraggableCard.svelte +77 -77
  35. package/dist/Components/FileCard.svelte +24 -24
  36. package/dist/Components/FileUpload.svelte +236 -236
  37. package/dist/Components/GridInput.svelte +196 -200
  38. package/dist/Components/GridInputSet.svelte +29 -29
  39. package/dist/Components/HorizontalTabGroup.svelte +82 -82
  40. package/dist/Components/HorizontalTabGroupButton.svelte +44 -44
  41. package/dist/Components/Icon.svelte +128 -128
  42. package/dist/Components/IconProps.d.ts +2 -7
  43. package/dist/Components/Icons.js +357 -357
  44. package/dist/Components/LoadingAnimation.svelte +29 -29
  45. package/dist/Components/LoadingModal.svelte +45 -45
  46. package/dist/Components/LoadingOverlay.svelte +25 -25
  47. package/dist/Components/LoadingOverlayProps.d.ts +3 -7
  48. package/dist/Components/Modal.svelte +258 -258
  49. package/dist/Components/ModalProps.d.ts +0 -3
  50. package/dist/Components/MultiSelect.svelte +220 -220
  51. package/dist/Components/MultiSelectCommonProps.d.ts +6 -17
  52. package/dist/Components/Nav/Nav.svelte +110 -99
  53. package/dist/Components/Nav/NavProps.d.ts +4 -11
  54. package/dist/Components/Nav/NavServiceMenuList.svelte +43 -43
  55. package/dist/Components/Nav/NavTab.svelte +62 -62
  56. package/dist/Components/Nav/ServiceMenuItem.svelte +100 -100
  57. package/dist/Components/Nav/ServicesNav.svelte +80 -80
  58. package/dist/Components/NumberInput.svelte +157 -157
  59. package/dist/Components/PageHeading.svelte +25 -25
  60. package/dist/Components/PasswordInput.svelte +88 -88
  61. package/dist/Components/Portal.svelte +36 -36
  62. package/dist/Components/ProgressBubble.svelte +107 -108
  63. package/dist/Components/ProgressBubbleProps.d.ts +3 -3
  64. package/dist/Components/QuerySelect.svelte +366 -366
  65. package/dist/Components/QuerySelectProps.d.ts +4 -14
  66. package/dist/Components/SingleSelect.svelte +250 -250
  67. package/dist/Components/SingleSelectCommonProps.d.ts +3 -19
  68. package/dist/Components/StepButton.svelte +70 -70
  69. package/dist/Components/StepForm.svelte +140 -140
  70. package/dist/Components/Switch.svelte +69 -69
  71. package/dist/Components/Table/ARCHITECTURE.md +298 -0
  72. package/dist/Components/Table/Body/QuickSearchCell.svelte +683 -694
  73. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +51 -51
  74. package/dist/Components/Table/Body/Rows/ColumnList.svelte +59 -59
  75. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +456 -478
  76. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +79 -79
  77. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +185 -188
  78. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +124 -124
  79. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +120 -120
  80. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +230 -230
  81. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +105 -105
  82. package/dist/Components/Table/Body/Rows/Columns/SelectColumn.d.ts +12 -9
  83. package/dist/Components/Table/Body/Rows/Columns/SelectTableCell.d.ts +5 -5
  84. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +87 -87
  85. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +126 -126
  86. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +159 -159
  87. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +246 -246
  88. package/dist/Components/Table/Body/Rows/Columns/TableDatePickerProps.d.ts +4 -4
  89. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +216 -216
  90. package/dist/Components/Table/Body/Rows/Columns/TableQueryCell.d.ts +6 -5
  91. package/dist/Components/Table/Body/Rows/Columns/TableQueryColumn.d.ts +7 -6
  92. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +252 -257
  93. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +179 -179
  94. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +221 -229
  95. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte.d.ts +11 -11
  96. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +197 -197
  97. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +190 -190
  98. package/dist/Components/Table/Body/Rows/TableRow.svelte +290 -290
  99. package/dist/Components/Table/Body/TableBody.svelte +155 -155
  100. package/dist/Components/Table/ColumnDefinitions/ExampleItemColumnDefRepository.svelte.js +2 -3
  101. package/dist/Components/Table/ColumnFactory.svelte.d.ts +9 -11
  102. package/dist/Components/Table/ColumnFactory.svelte.js +110 -143
  103. package/dist/Components/Table/ColumnKindRegistry.d.ts +112 -0
  104. package/dist/Components/Table/ColumnKindRegistry.js +63 -0
  105. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +41 -41
  106. package/dist/Components/Table/Headers/SelectAllHeader.svelte +30 -30
  107. package/dist/Components/Table/Headers/TableHead.svelte +415 -413
  108. package/dist/Components/Table/Misc/ColumnPanel.svelte +94 -94
  109. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +112 -112
  110. package/dist/Components/Table/Misc/FilterInput.svelte +620 -644
  111. package/dist/Components/Table/Misc/FilterInput.svelte.d.ts +12 -12
  112. package/dist/Components/Table/Misc/FilterInputProps.d.ts +2 -2
  113. package/dist/Components/Table/Misc/FilterPanel.svelte +257 -257
  114. package/dist/Components/Table/Misc/FilterPanelModal.svelte +30 -30
  115. package/dist/Components/Table/Misc/SidePanel.svelte +35 -35
  116. package/dist/Components/Table/Misc/TableFooter.svelte +311 -311
  117. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +125 -125
  118. package/dist/Components/Table/Misc/TableSidebar.svelte +157 -157
  119. package/dist/Components/Table/Misc/ToolboxPanel.svelte +100 -100
  120. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +107 -107
  121. package/dist/Components/Table/NestedTable.svelte +130 -133
  122. package/dist/Components/Table/SubApis.svelte.d.ts +1 -5
  123. package/dist/Components/Table/SubApis.svelte.js +33 -32
  124. package/dist/Components/Table/Table.svelte +692 -745
  125. package/dist/Components/Table/TableHelpers.svelte.d.ts +6 -19
  126. package/dist/Components/Table/TableHelpers.svelte.js +6 -14
  127. package/dist/Components/Table/Types/Columns/Column.d.ts +2 -2
  128. package/dist/Components/Table/Types/Columns/DateColumn.d.ts +3 -3
  129. package/dist/Components/Table/Types/Columns/DateTableCell.d.ts +7 -7
  130. package/dist/Components/Table/Types/Columns/Definitions/Accessors/AccessorDef.svelte.d.ts +7 -8
  131. package/dist/Components/Table/Types/Columns/Definitions/Accessors/AccessorDef.svelte.js +4 -5
  132. package/dist/Components/Table/Types/Columns/Definitions/Accessors/CheckboxDef.svelte.d.ts +11 -14
  133. package/dist/Components/Table/Types/Columns/Definitions/Accessors/CheckboxDef.svelte.js +3 -4
  134. package/dist/Components/Table/Types/Columns/Definitions/Accessors/DateInputDef.svelte.d.ts +7 -11
  135. package/dist/Components/Table/Types/Columns/Definitions/Accessors/DateInputDef.svelte.js +3 -3
  136. package/dist/Components/Table/Types/Columns/Definitions/Accessors/NumberInputDef.svelte.d.ts +13 -18
  137. package/dist/Components/Table/Types/Columns/Definitions/Accessors/NumberInputDef.svelte.js +4 -5
  138. package/dist/Components/Table/Types/Columns/Definitions/Accessors/QueryDef.svelte.d.ts +5 -5
  139. package/dist/Components/Table/Types/Columns/Definitions/Accessors/QueryDef.svelte.js +2 -2
  140. package/dist/Components/Table/Types/Columns/Definitions/Accessors/SelectDef.svelte.d.ts +15 -12
  141. package/dist/Components/Table/Types/Columns/Definitions/Accessors/SelectDef.svelte.js +4 -5
  142. package/dist/Components/Table/Types/Columns/Definitions/Accessors/TextInputDef.svelte.d.ts +8 -12
  143. package/dist/Components/Table/Types/Columns/Definitions/Accessors/TextInputDef.svelte.js +3 -3
  144. package/dist/Components/Table/Types/Columns/Definitions/Actions/ActionsDef.svelte.d.ts +6 -6
  145. package/dist/Components/Table/Types/Columns/Definitions/Actions/ActionsDef.svelte.js +3 -3
  146. package/dist/Components/Table/Types/Columns/Definitions/ColumnDef.svelte.d.ts +49 -16
  147. package/dist/Components/Table/Types/Columns/Definitions/ColumnDef.svelte.js +27 -5
  148. package/dist/Components/Table/Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.d.ts +7 -7
  149. package/dist/Components/Table/Types/Columns/Definitions/Display/AbstractDisplayDef.svelte.js +4 -4
  150. package/dist/Components/Table/Types/Columns/Definitions/Display/BubbleDef.svelte.d.ts +5 -5
  151. package/dist/Components/Table/Types/Columns/Definitions/Display/BubbleDef.svelte.js +2 -2
  152. package/dist/Components/Table/Types/Columns/Definitions/Display/DisplayDef.svelte.d.ts +5 -5
  153. package/dist/Components/Table/Types/Columns/Definitions/Display/DisplayDef.svelte.js +2 -2
  154. package/dist/Components/Table/Types/Columns/Definitions/Display/ProgressBarDef.svelte.d.ts +5 -5
  155. package/dist/Components/Table/Types/Columns/Definitions/Display/ProgressBarDef.svelte.js +2 -2
  156. package/dist/Components/Table/Types/Columns/Definitions/Display/ValidityDef.svelte.d.ts +5 -5
  157. package/dist/Components/Table/Types/Columns/Definitions/Display/ValidityDef.svelte.js +2 -2
  158. package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.d.ts +11 -19
  159. package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.js +4 -8
  160. package/dist/Components/Table/Types/Columns/Definitions/FilterOnly/FilterOnlyDef.svelte.d.ts +6 -7
  161. package/dist/Components/Table/Types/Columns/Definitions/FilterOnly/FilterOnlyDef.svelte.js +3 -4
  162. package/dist/Components/Table/Types/Columns/Definitions/RowSelection/RowSelectionDef.svelte.d.ts +9 -9
  163. package/dist/Components/Table/Types/Columns/Definitions/RowSelection/RowSelectionDef.svelte.js +4 -4
  164. package/dist/Components/Table/Types/Columns/ExpandTableCell.d.ts +5 -5
  165. package/dist/Components/Table/Types/Columns/ProgressBarTableCell.d.ts +6 -6
  166. package/dist/Components/Table/Types/Columns/TableCell.d.ts +7 -6
  167. package/dist/Components/Table/Types/Context/ClipboardState.svelte.d.ts +98 -0
  168. package/dist/Components/Table/Types/Context/ClipboardState.svelte.js +290 -0
  169. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.d.ts +17 -45
  170. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.js +17 -45
  171. package/dist/Components/Table/Types/Context/FilteringState.svelte.d.ts +9 -20
  172. package/dist/Components/Table/Types/Context/FilteringState.svelte.js +9 -20
  173. package/dist/Components/Table/Types/Context/FocusState.svelte.d.ts +23 -42
  174. package/dist/Components/Table/Types/Context/FocusState.svelte.js +32 -59
  175. package/dist/Components/Table/Types/Context/ITable.d.ts +5 -8
  176. package/dist/Components/Table/Types/Context/PreferencesState.svelte.d.ts +29 -56
  177. package/dist/Components/Table/Types/Context/PreferencesState.svelte.js +28 -55
  178. package/dist/Components/Table/Types/Context/QueryValuesState.svelte.d.ts +50 -0
  179. package/dist/Components/Table/Types/Context/QueryValuesState.svelte.js +62 -0
  180. package/dist/Components/Table/Types/Context/SelectionState.svelte.d.ts +17 -28
  181. package/dist/Components/Table/Types/Context/SelectionState.svelte.js +17 -28
  182. package/dist/Components/Table/Types/Context/SortingState.svelte.d.ts +15 -29
  183. package/dist/Components/Table/Types/Context/SortingState.svelte.js +17 -31
  184. package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +212 -325
  185. package/dist/Components/Table/Types/Context/TableContext.svelte.js +292 -706
  186. package/dist/Components/Table/Types/Context/VirtualisationState.svelte.d.ts +62 -79
  187. package/dist/Components/Table/Types/Context/VirtualisationState.svelte.js +54 -73
  188. package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.d.ts +16 -27
  189. package/dist/Components/Table/Types/DataRepository/BuiltInRemoteRepository.svelte.js +16 -27
  190. package/dist/Components/Table/Types/DataRepository/IDataRepository.d.ts +12 -15
  191. package/dist/Components/Table/Types/DataRepository/TableDataRepository.svelte.d.ts +15 -30
  192. package/dist/Components/Table/Types/DataRepository/TableDataRepository.svelte.js +16 -28
  193. package/dist/Components/Table/Types/Header.d.ts +4 -4
  194. package/dist/Components/Table/Types/HeaderGroup.d.ts +5 -5
  195. package/dist/Components/Table/Types/Public/index.d.ts +2 -4
  196. package/dist/Components/Table/Types/Public/index.js +2 -4
  197. package/dist/Components/Table/clipboardEscape.d.ts +32 -0
  198. package/dist/Components/Table/clipboardEscape.js +45 -0
  199. package/dist/Components/Table/createTable.svelte.d.ts +8 -18
  200. package/dist/Components/Table/createTable.svelte.js +19 -47
  201. package/dist/Components/Test/TestContextConsumer.svelte +13 -13
  202. package/dist/Components/TextArea.svelte +130 -130
  203. package/dist/Components/TextInput.svelte +176 -176
  204. package/dist/Components/ThreeStateRadio.svelte +131 -131
  205. package/dist/Components/Touch/BoolCard.svelte +45 -45
  206. package/dist/Components/Touch/DateModal.svelte +209 -209
  207. package/dist/Components/Touch/EditCard.svelte +59 -59
  208. package/dist/Components/Touch/InfoCard.svelte +31 -31
  209. package/dist/Components/Touch/InfoTextCard.svelte +28 -28
  210. package/dist/Components/Touch/ModalExplanation.svelte +17 -17
  211. package/dist/Components/Touch/NumberKeyboard.svelte +225 -225
  212. package/dist/Components/Touch/NumberModal.svelte +79 -79
  213. package/dist/Components/Touch/PlusMinusCard.svelte +46 -46
  214. package/dist/Components/Touch/SelectModal.svelte +123 -123
  215. package/dist/Components/Touch/SummaryCard.svelte +93 -93
  216. package/dist/Components/Touch/TextAreaModal.svelte +95 -95
  217. package/dist/Components/Touch/TextModal.svelte +106 -106
  218. package/dist/Components/Touch/TouchCard.svelte +33 -33
  219. package/dist/Components/Touch/TouchModal.svelte +274 -274
  220. package/dist/Components/Touch/UtilityButton.svelte +109 -109
  221. package/dist/Components/VerticalTabGroup.svelte +79 -79
  222. package/dist/Components/VerticalTabGroupButton.svelte +58 -58
  223. package/dist/Contexts/ToastContext.svelte.js +3 -4
  224. package/dist/Helpers/Datamatrix.d.ts +19 -19
  225. package/dist/Helpers/Datamatrix.js +291 -291
  226. package/dist/Helpers/Helpers.svelte.d.ts +47 -4
  227. package/dist/Helpers/Helpers.svelte.js +97 -4
  228. package/dist/Helpers/dropdownPortal.d.ts +26 -41
  229. package/dist/Helpers/dropdownPortal.js +62 -116
  230. package/dist/Types/Examples/ExampleModal.svelte +35 -35
  231. package/dist/index.d.ts +1 -2
  232. package/dist/index.js +1 -1
  233. package/dist/scss/_basics.scss +12 -12
  234. package/dist/scss/_colours.scss +134 -134
  235. package/dist/scss/_mixins.scss +3358 -3360
  236. package/dist/scss/_motion.scss +57 -57
  237. package/dist/scss/_sizing.scss +85 -85
  238. package/dist/scss/_transitions.scss +8 -8
  239. package/package.json +8 -6
@@ -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,
@@ -807,6 +792,10 @@ div.input-wrapper-parent div.input-wrapper.right {
807
792
  flex-grow: 2;
808
793
  --input-padding-right: 2.6rem;
809
794
  }
795
+ div.input-wrapper-parent > :global(.select-wrapper) {
796
+ flex: 1 1 auto;
797
+ min-width: 0;
798
+ }
810
799
 
811
800
  [role=cell] {
812
801
  --input-width: 100%;
@@ -825,4 +814,4 @@ div.input-wrapper-parent div.input-wrapper.right {
825
814
  height: 60px;
826
815
  --input-height: 60px;
827
816
  background-color: #ffffff;
828
- }</style>
817
+ }</style>