@svgrid/grid 2.2.27 → 2.2.29

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 (248) hide show
  1. package/README.md +199 -199
  2. package/dist/FlexRender.svelte +96 -96
  3. package/dist/GridFooter.svelte +179 -179
  4. package/dist/GridMenus.svelte +782 -734
  5. package/dist/SvAutoComplete.svelte +165 -165
  6. package/dist/SvCalendar.svelte +503 -503
  7. package/dist/SvCarousel.svelte +141 -141
  8. package/dist/SvCheckBox.svelte +101 -101
  9. package/dist/SvColorInput.svelte +181 -181
  10. package/dist/SvComboBox.svelte +275 -275
  11. package/dist/SvContextMenu.svelte +116 -116
  12. package/dist/SvCountryInput.svelte +160 -160
  13. package/dist/SvDrawer.svelte +254 -250
  14. package/dist/SvDropDownList.svelte +374 -374
  15. package/dist/SvDurationInput.svelte +126 -126
  16. package/dist/SvField.svelte +293 -293
  17. package/dist/SvForm.svelte +437 -437
  18. package/dist/SvGrid.controller.svelte.d.ts +14 -1
  19. package/dist/SvGrid.controller.svelte.js +112 -28
  20. package/dist/SvGrid.css +2704 -2723
  21. package/dist/SvGrid.svelte +3361 -3333
  22. package/dist/SvGrid.types.d.ts +20 -5
  23. package/dist/SvGridChart.svelte +1724 -1724
  24. package/dist/SvGridChartPanel.svelte +485 -485
  25. package/dist/SvGridChartView.svelte +70 -70
  26. package/dist/SvGridDropdown.svelte +696 -696
  27. package/dist/SvGridSelect.svelte +259 -259
  28. package/dist/SvGroupCell.svelte +109 -109
  29. package/dist/SvListBox.svelte +326 -287
  30. package/dist/SvListBox.svelte.d.ts +10 -1
  31. package/dist/SvMaskedInput.svelte +122 -122
  32. package/dist/SvMenu.svelte +124 -124
  33. package/dist/SvMenuList.svelte +146 -146
  34. package/dist/SvMultiSelect.svelte +281 -281
  35. package/dist/SvNumberInput.svelte +168 -168
  36. package/dist/SvOtpInput.svelte +158 -158
  37. package/dist/SvPasswordInput.svelte +151 -151
  38. package/dist/SvPhoneInput.svelte +133 -131
  39. package/dist/SvPopover.svelte +197 -197
  40. package/dist/SvRadioGroup.svelte +107 -107
  41. package/dist/SvRowGroupPanel.svelte +170 -170
  42. package/dist/SvScrollArea.svelte +61 -61
  43. package/dist/SvSlider.svelte +200 -200
  44. package/dist/SvSwitchButton.svelte +107 -107
  45. package/dist/SvTagsInput.svelte +112 -112
  46. package/dist/SvTextInput.svelte +147 -147
  47. package/dist/SvTimePicker.svelte +245 -245
  48. package/dist/SvToaster.svelte +159 -159
  49. package/dist/SvToggleButton.svelte +85 -85
  50. package/dist/SvTooltip.svelte +161 -161
  51. package/dist/SvTour.svelte +204 -204
  52. package/dist/SvTree.svelte +437 -437
  53. package/dist/SvTreeSelect.svelte +233 -233
  54. package/dist/cdn/GridMenus-Da_p7SKS.js +486 -0
  55. package/dist/cdn/GridMenus-Ds3OpzZT.js +490 -0
  56. package/dist/cdn/{SvDateTimePicker-B1sDc9Pz.js → SvDateTimePicker-B8GopjhC.js} +2 -2
  57. package/dist/cdn/{SvDateTimePicker-Bh0FTs1E.js → SvDateTimePicker-DB0aIEk8.js} +2 -2
  58. package/dist/cdn/{SvGridChart-Bgk4hDs8.js → SvGridChart-BcLCYvw9.js} +1 -1
  59. package/dist/cdn/{SvGridChart-BKQsO5fZ.js → SvGridChart-BhnFdLd0.js} +1 -1
  60. package/dist/cdn/{SvGridChartPanel-DkCc2Z7b.js → SvGridChartPanel-Cmx9DHAS.js} +1 -1
  61. package/dist/cdn/{SvGridChartPanel-DSkHIQDd.js → SvGridChartPanel-DvjwxLvK.js} +1 -1
  62. package/dist/cdn/{SvGridChartView-BrbVaELm.js → SvGridChartView-B40DTJSX.js} +1 -1
  63. package/dist/cdn/{SvGridChartView-i9rWvSKg.js → SvGridChartView-PwqI4s02.js} +1 -1
  64. package/dist/cdn/{SvGridDropdown-D08sVmAy.js → SvGridDropdown-DjALc_4f.js} +1 -1
  65. package/dist/cdn/{SvGridDropdown-CGxxopQ4.js → SvGridDropdown-hpnHvOGt.js} +1 -1
  66. package/dist/cdn/{src-cgs1wcUS.js → src-BLJfdyL0.js} +4034 -3889
  67. package/dist/cdn/{src-Cw8d9U8j.js → src-DYDCiC0D.js} +7053 -6908
  68. package/dist/cdn/svgrid.js +9 -9
  69. package/dist/cdn/svgrid.svelte-external.js +9 -9
  70. package/dist/chart-export.js +8 -8
  71. package/dist/column-groups.js +8 -10
  72. package/dist/column-id.d.ts +9 -0
  73. package/dist/column-id.js +25 -0
  74. package/dist/core.js +2 -1
  75. package/dist/createListbox.svelte.d.ts +15 -2
  76. package/dist/createListbox.svelte.js +27 -6
  77. package/dist/editing.d.ts +1 -0
  78. package/dist/editing.js +45 -10
  79. package/dist/filtering/excel-filters.d.ts +30 -10
  80. package/dist/filtering/excel-filters.js +96 -15
  81. package/dist/index.d.ts +2 -2
  82. package/dist/index.js +2 -2
  83. package/dist/menus.d.ts +2 -0
  84. package/dist/menus.js +25 -0
  85. package/dist/row-resize.js +40 -0
  86. package/dist/selection.js +1 -1
  87. package/package.json +10 -10
  88. package/src/FlexRender.svelte +96 -96
  89. package/src/GridFooter.svelte +179 -179
  90. package/src/GridMenus.svelte +782 -734
  91. package/src/SvAutoComplete.svelte +165 -165
  92. package/src/SvCalendar.svelte +503 -503
  93. package/src/SvCalendar.test.ts +226 -226
  94. package/src/SvCarousel.svelte +141 -141
  95. package/src/SvCheckBox.svelte +101 -101
  96. package/src/SvColorInput.svelte +181 -181
  97. package/src/SvComboBox.svelte +275 -275
  98. package/src/SvContextMenu.svelte +116 -116
  99. package/src/SvCountryInput.svelte +160 -160
  100. package/src/SvDrawer.svelte +254 -250
  101. package/src/SvDrawer.test.ts +18 -1
  102. package/src/SvDropDownList.svelte +374 -374
  103. package/src/SvDurationInput.svelte +126 -126
  104. package/src/SvField.svelte +293 -293
  105. package/src/SvForm.svelte +437 -437
  106. package/src/SvForm.test.ts +411 -411
  107. package/src/SvGrid.controller.svelte.ts +3335 -3239
  108. package/src/SvGrid.css +2704 -2723
  109. package/src/SvGrid.svelte +3361 -3333
  110. package/src/SvGrid.types.ts +1628 -1613
  111. package/src/SvGridChart.svelte +1724 -1724
  112. package/src/SvGridChartPanel.svelte +485 -485
  113. package/src/SvGridChartView.svelte +70 -70
  114. package/src/SvGridDropdown.svelte +696 -696
  115. package/src/SvGridSelect.svelte +259 -259
  116. package/src/SvGroupCell.svelte +109 -109
  117. package/src/SvListBox.svelte +326 -287
  118. package/src/SvMaskedInput.svelte +122 -122
  119. package/src/SvMenu.svelte +124 -124
  120. package/src/SvMenu.test.ts +97 -97
  121. package/src/SvMenuList.svelte +146 -146
  122. package/src/SvMultiSelect.svelte +281 -281
  123. package/src/SvNavPane.test.ts +15 -4
  124. package/src/SvNumberInput.svelte +168 -168
  125. package/src/SvOtpInput.svelte +158 -158
  126. package/src/SvPasswordInput.svelte +151 -151
  127. package/src/SvPhoneInput.svelte +133 -131
  128. package/src/SvPopover.svelte +197 -197
  129. package/src/SvRadioGroup.svelte +107 -107
  130. package/src/SvRowGroupPanel.svelte +170 -170
  131. package/src/SvScrollArea.svelte +61 -61
  132. package/src/SvSlider.svelte +200 -200
  133. package/src/SvSwitchButton.svelte +107 -107
  134. package/src/SvTagsInput.svelte +112 -112
  135. package/src/SvTextInput.svelte +147 -147
  136. package/src/SvTimePicker.svelte +245 -245
  137. package/src/SvToaster.svelte +159 -159
  138. package/src/SvToaster.test.ts +95 -95
  139. package/src/SvToggleButton.svelte +85 -85
  140. package/src/SvTooltip.svelte +161 -161
  141. package/src/SvTour.svelte +204 -204
  142. package/src/SvTree.svelte +437 -437
  143. package/src/SvTreeSelect.svelte +233 -233
  144. package/src/a11y/dismissable.test.ts +119 -119
  145. package/src/a11y/dismissable.ts +114 -114
  146. package/src/a11y.contract.test.ts +49 -49
  147. package/src/a11y.test.ts +59 -59
  148. package/src/a11y.ts +61 -61
  149. package/src/ai.test.ts +502 -502
  150. package/src/ai.ts +1391 -1391
  151. package/src/build-api.coverage.test.ts +633 -633
  152. package/src/build-api.ts +895 -895
  153. package/src/builtin-editors.grid.test.ts +85 -85
  154. package/src/cell-formatting.ts +169 -169
  155. package/src/cell-render.ts +469 -469
  156. package/src/chart-export.ts +201 -201
  157. package/src/chart-view.svelte.ts +36 -36
  158. package/src/chart.ts +2296 -2296
  159. package/src/collaboration.test.ts +104 -104
  160. package/src/collaboration.ts +167 -167
  161. package/src/column-groups.test.ts +48 -0
  162. package/src/column-groups.ts +8 -10
  163. package/src/column-id.ts +30 -0
  164. package/src/core.performance.test.ts +30 -30
  165. package/src/core.ts +1112 -1111
  166. package/src/createAutocomplete.svelte.ts +132 -132
  167. package/src/createCombobox.svelte.ts +191 -191
  168. package/src/createCountryInput.svelte.ts +157 -157
  169. package/src/createDropdownList.svelte.ts +168 -168
  170. package/src/createForm.svelte.ts +386 -386
  171. package/src/createGrid.svelte.ts +42 -42
  172. package/src/createGrid.test.ts +10 -10
  173. package/src/createGridState.svelte.ts +17 -17
  174. package/src/createListbox.svelte.ts +39 -7
  175. package/src/createMenu.svelte.ts +225 -225
  176. package/src/createPopoverSelect.svelte.ts +213 -213
  177. package/src/createSlider.svelte.ts +191 -191
  178. package/src/createTooltip.svelte.ts +144 -144
  179. package/src/createTree.svelte.ts +319 -319
  180. package/src/editing.test.ts +949 -859
  181. package/src/editing.ts +709 -675
  182. package/src/editor-contract.ts +157 -157
  183. package/src/export-data-api.test.ts +126 -126
  184. package/src/export-format.test.ts +107 -107
  185. package/src/export-format.ts +598 -598
  186. package/src/filter-operators.ts +157 -157
  187. package/src/filtering/excel-filters.test.ts +44 -1
  188. package/src/filtering/excel-filters.ts +91 -14
  189. package/src/flex-render.ts +3 -3
  190. package/src/form-field.ts +127 -127
  191. package/src/index.ts +715 -713
  192. package/src/js-scroller.svelte.ts +173 -173
  193. package/src/keyboard.test.ts +59 -59
  194. package/src/keyboard.ts +97 -97
  195. package/src/list-nav.test.ts +49 -49
  196. package/src/list-nav.ts +29 -29
  197. package/src/list-option.test.ts +56 -56
  198. package/src/list-option.ts +172 -172
  199. package/src/menus.test.ts +736 -700
  200. package/src/menus.ts +679 -652
  201. package/src/merge-objects.ts +48 -48
  202. package/src/overlays.test.ts +90 -90
  203. package/src/render-component.ts +28 -28
  204. package/src/row-resize.test.ts +100 -0
  205. package/src/row-resize.ts +36 -0
  206. package/src/scheduler-model.test.ts +562 -562
  207. package/src/scheduler-model.ts +872 -872
  208. package/src/selection.test.ts +754 -754
  209. package/src/selection.ts +600 -600
  210. package/src/server-data-source.test.ts +289 -289
  211. package/src/server-data-source.ts +413 -413
  212. package/src/sparkline.test.ts +68 -68
  213. package/src/sparkline.ts +169 -169
  214. package/src/spreadsheet.test.ts +489 -489
  215. package/src/spreadsheet.ts +312 -312
  216. package/src/static-functions.ts +11 -11
  217. package/src/subscribe.ts +38 -38
  218. package/src/svgrid-wrapper.types.ts +524 -524
  219. package/src/svgrid.api-extensions.test.ts +8 -3
  220. package/src/svgrid.behavior.test.ts +706 -706
  221. package/src/svgrid.cell-dom-ids.test.ts +96 -0
  222. package/src/svgrid.charting.test.ts +534 -534
  223. package/src/svgrid.conditional-stat-scope.test.ts +111 -0
  224. package/src/svgrid.features.test.ts +157 -157
  225. package/src/svgrid.filter-menu-listbox.svelte.test.ts +308 -0
  226. package/src/svgrid.filter-menu-scroll.test.ts +3 -1
  227. package/src/svgrid.group-header-ids.test.ts +121 -0
  228. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  229. package/src/svgrid.wrapper.test.ts +40 -40
  230. package/src/test-setup.ts +62 -62
  231. package/src/themes/index.ts +215 -215
  232. package/src/toast-store.svelte.ts +246 -246
  233. package/src/toast-store.test.ts +147 -147
  234. package/src/ui-buttons.test.ts +144 -144
  235. package/src/ui-inputs.test.ts +118 -118
  236. package/src/ui-range.test.ts +70 -70
  237. package/src/ui-selection.test.ts +155 -155
  238. package/src/ui-tier1.test.ts +142 -142
  239. package/src/virtual.test.ts +88 -88
  240. package/src/virtualization/column-virtualizer.test.ts +27 -27
  241. package/src/virtualization/column-virtualizer.ts +30 -30
  242. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  243. package/src/virtualization/types.ts +30 -30
  244. package/src/virtualization/virtualizer.test.ts +47 -47
  245. package/src/virtualization/virtualizer.ts +296 -296
  246. package/themes/ember.css +71 -71
  247. package/dist/cdn/GridMenus-BzxEYNtW.js +0 -421
  248. package/dist/cdn/GridMenus-Dr-nGEXn.js +0 -417
@@ -1,157 +1,157 @@
1
- // Filter-operator catalogue + the pure helpers that pick which operators
2
- // apply to a column and how they're labelled. Fully static - no grid state -
3
- // so it lives outside the controller.
4
- import type { Column, RowData } from "./index";
5
- import type { FilterOperator, FilterOption } from "./SvGrid.types";
6
-
7
- export const filterOperatorOptions: Array<FilterOption> = [
8
- { value: "contains", label: "Contains", iconName: "op-contains" },
9
- { value: "notContains", label: "Not contains", iconName: "op-notContains" },
10
- { value: "equals", label: "Equals", iconName: "op-equals" },
11
- { value: "notEquals", label: "Not equals", iconName: "op-notEquals" },
12
- { value: "startsWith", label: "Starts with", iconName: "op-startsWith" },
13
- { value: "endsWith", label: "Ends with", iconName: "op-endsWith" },
14
- { value: "regex", label: "Regex", iconName: "op-regex" },
15
- { value: "in", label: "In", iconName: "op-in" },
16
- { value: "notIn", label: "Not in", iconName: "op-notIn" },
17
- { value: "greaterThan", label: "Greater than", iconName: "op-greaterThan" },
18
- { value: "lessThan", label: "Less than", iconName: "op-lessThan" },
19
- { value: "between", label: "Between", iconName: "op-between" },
20
- { value: "isBlank", label: "Blank", iconName: "op-isBlank" },
21
- { value: "isNotBlank", label: "Not blank", iconName: "op-isNotBlank" },
22
- ];
23
- /** Operators whose predicate is a set-membership test over a token list.
24
- * The filter row renders a multi-value chip input for these instead of a
25
- * single text box. */
26
- export const SET_OPERATORS: ReadonlyArray<FilterOperator> = ["in", "notIn"];
27
- /** Operators that need no value input - they act on emptiness alone. */
28
- export const VALUELESS_OPERATORS: ReadonlyArray<FilterOperator> = [
29
- "isBlank",
30
- "isNotBlank",
31
- ];
32
- /** Which operators make sense for each column editor type. */
33
- export const TEXT_OPERATORS: Array<FilterOperator> = [
34
- "contains",
35
- "notContains",
36
- "equals",
37
- "notEquals",
38
- "startsWith",
39
- "endsWith",
40
- "regex",
41
- "in",
42
- "notIn",
43
- "isBlank",
44
- "isNotBlank",
45
- ];
46
- export const NUMBER_OPERATORS: Array<FilterOperator> = [
47
- "equals",
48
- "notEquals",
49
- "greaterThan",
50
- "lessThan",
51
- "between",
52
- "in",
53
- "notIn",
54
- "isBlank",
55
- "isNotBlank",
56
- ];
57
- export const DATE_OPERATORS: Array<FilterOperator> = [
58
- "equals",
59
- "notEquals",
60
- "lessThan",
61
- "greaterThan",
62
- "between",
63
- "isBlank",
64
- "isNotBlank",
65
- ];
66
- export const CHECKBOX_OPERATORS: Array<FilterOperator> = [
67
- "equals",
68
- "isBlank",
69
- "isNotBlank",
70
- ];
71
- export const fallbackOperatorOption: FilterOption = {
72
- value: "contains",
73
- label: "Contains",
74
- iconName: "op-contains",
75
- };
76
-
77
- export function operatorOption(value: FilterOperator): FilterOption {
78
- return (
79
- filterOperatorOptions.find((option) => option.value === value) ??
80
- fallbackOperatorOption
81
- );
82
- }
83
-
84
- /** Returns the operators that make sense for the given column's data type. */
85
- export function operatorsForColumn<TData extends RowData>(
86
- column: Column<TData> | undefined,
87
- ): Array<FilterOption> {
88
- const editorType = column?.columnDef.editorType ?? "text";
89
- const ids =
90
- editorType === "number"
91
- ? NUMBER_OPERATORS
92
- : editorType === "date" || editorType === "datetime"
93
- ? DATE_OPERATORS
94
- : editorType === "checkbox"
95
- ? CHECKBOX_OPERATORS
96
- : TEXT_OPERATORS;
97
- return ids
98
- .map((id) => filterOperatorOptions.find((option) => option.value === id))
99
- .filter((option): option is FilterOption => Boolean(option));
100
- }
101
-
102
- /** Default operator for a column (first one valid for its type). */
103
- export function defaultOperatorFor<TData extends RowData>(
104
- column: Column<TData> | undefined,
105
- ): FilterOperator {
106
- return operatorsForColumn(column)[0]?.value ?? "contains";
107
- }
108
-
109
- /** Date columns get friendlier labels for "less / greater than". */
110
- export function operatorLabelFor<TData extends RowData>(
111
- option: FilterOption,
112
- column: Column<TData> | undefined,
113
- ): string {
114
- const editorType = column?.columnDef.editorType;
115
- if (editorType === "date" || editorType === "datetime") {
116
- if (option.value === "lessThan") return "Before";
117
- if (option.value === "greaterThan") return "After";
118
- }
119
- return option.label;
120
- }
121
-
122
- /** GridMessages key for each operator's label (see grid-messages.ts). */
123
- const OPERATOR_MESSAGE_KEY: Record<FilterOperator, string> = {
124
- contains: "opContains",
125
- notContains: "opNotContains",
126
- equals: "opEquals",
127
- notEquals: "opNotEquals",
128
- startsWith: "opStartsWith",
129
- endsWith: "opEndsWith",
130
- regex: "opRegex",
131
- in: "opIn",
132
- notIn: "opNotIn",
133
- greaterThan: "opGreaterThan",
134
- lessThan: "opLessThan",
135
- between: "opBetween",
136
- isBlank: "opIsBlank",
137
- isNotBlank: "opIsNotBlank",
138
- };
139
-
140
- /**
141
- * Localized operator label: honors date `Before`/`After` relabels, then the
142
- * `localeText` override for the operator, falling back to the English
143
- * `option.label`. `messages` is a loose string map to avoid a type cycle.
144
- */
145
- export function localizeOperatorLabel<TData extends RowData>(
146
- option: FilterOption,
147
- column: Column<TData> | undefined,
148
- messages?: Record<string, string> | null,
149
- ): string {
150
- const editorType = column?.columnDef.editorType;
151
- if (editorType === "date" || editorType === "datetime") {
152
- if (option.value === "lessThan") return messages?.opBefore || "Before";
153
- if (option.value === "greaterThan") return messages?.opAfter || "After";
154
- }
155
- const key = OPERATOR_MESSAGE_KEY[option.value];
156
- return (key && messages?.[key]) || option.label;
157
- }
1
+ // Filter-operator catalogue + the pure helpers that pick which operators
2
+ // apply to a column and how they're labelled. Fully static - no grid state -
3
+ // so it lives outside the controller.
4
+ import type { Column, RowData } from "./index";
5
+ import type { FilterOperator, FilterOption } from "./SvGrid.types";
6
+
7
+ export const filterOperatorOptions: Array<FilterOption> = [
8
+ { value: "contains", label: "Contains", iconName: "op-contains" },
9
+ { value: "notContains", label: "Not contains", iconName: "op-notContains" },
10
+ { value: "equals", label: "Equals", iconName: "op-equals" },
11
+ { value: "notEquals", label: "Not equals", iconName: "op-notEquals" },
12
+ { value: "startsWith", label: "Starts with", iconName: "op-startsWith" },
13
+ { value: "endsWith", label: "Ends with", iconName: "op-endsWith" },
14
+ { value: "regex", label: "Regex", iconName: "op-regex" },
15
+ { value: "in", label: "In", iconName: "op-in" },
16
+ { value: "notIn", label: "Not in", iconName: "op-notIn" },
17
+ { value: "greaterThan", label: "Greater than", iconName: "op-greaterThan" },
18
+ { value: "lessThan", label: "Less than", iconName: "op-lessThan" },
19
+ { value: "between", label: "Between", iconName: "op-between" },
20
+ { value: "isBlank", label: "Blank", iconName: "op-isBlank" },
21
+ { value: "isNotBlank", label: "Not blank", iconName: "op-isNotBlank" },
22
+ ];
23
+ /** Operators whose predicate is a set-membership test over a token list.
24
+ * The filter row renders a multi-value chip input for these instead of a
25
+ * single text box. */
26
+ export const SET_OPERATORS: ReadonlyArray<FilterOperator> = ["in", "notIn"];
27
+ /** Operators that need no value input - they act on emptiness alone. */
28
+ export const VALUELESS_OPERATORS: ReadonlyArray<FilterOperator> = [
29
+ "isBlank",
30
+ "isNotBlank",
31
+ ];
32
+ /** Which operators make sense for each column editor type. */
33
+ export const TEXT_OPERATORS: Array<FilterOperator> = [
34
+ "contains",
35
+ "notContains",
36
+ "equals",
37
+ "notEquals",
38
+ "startsWith",
39
+ "endsWith",
40
+ "regex",
41
+ "in",
42
+ "notIn",
43
+ "isBlank",
44
+ "isNotBlank",
45
+ ];
46
+ export const NUMBER_OPERATORS: Array<FilterOperator> = [
47
+ "equals",
48
+ "notEquals",
49
+ "greaterThan",
50
+ "lessThan",
51
+ "between",
52
+ "in",
53
+ "notIn",
54
+ "isBlank",
55
+ "isNotBlank",
56
+ ];
57
+ export const DATE_OPERATORS: Array<FilterOperator> = [
58
+ "equals",
59
+ "notEquals",
60
+ "lessThan",
61
+ "greaterThan",
62
+ "between",
63
+ "isBlank",
64
+ "isNotBlank",
65
+ ];
66
+ export const CHECKBOX_OPERATORS: Array<FilterOperator> = [
67
+ "equals",
68
+ "isBlank",
69
+ "isNotBlank",
70
+ ];
71
+ export const fallbackOperatorOption: FilterOption = {
72
+ value: "contains",
73
+ label: "Contains",
74
+ iconName: "op-contains",
75
+ };
76
+
77
+ export function operatorOption(value: FilterOperator): FilterOption {
78
+ return (
79
+ filterOperatorOptions.find((option) => option.value === value) ??
80
+ fallbackOperatorOption
81
+ );
82
+ }
83
+
84
+ /** Returns the operators that make sense for the given column's data type. */
85
+ export function operatorsForColumn<TData extends RowData>(
86
+ column: Column<TData> | undefined,
87
+ ): Array<FilterOption> {
88
+ const editorType = column?.columnDef.editorType ?? "text";
89
+ const ids =
90
+ editorType === "number"
91
+ ? NUMBER_OPERATORS
92
+ : editorType === "date" || editorType === "datetime"
93
+ ? DATE_OPERATORS
94
+ : editorType === "checkbox"
95
+ ? CHECKBOX_OPERATORS
96
+ : TEXT_OPERATORS;
97
+ return ids
98
+ .map((id) => filterOperatorOptions.find((option) => option.value === id))
99
+ .filter((option): option is FilterOption => Boolean(option));
100
+ }
101
+
102
+ /** Default operator for a column (first one valid for its type). */
103
+ export function defaultOperatorFor<TData extends RowData>(
104
+ column: Column<TData> | undefined,
105
+ ): FilterOperator {
106
+ return operatorsForColumn(column)[0]?.value ?? "contains";
107
+ }
108
+
109
+ /** Date columns get friendlier labels for "less / greater than". */
110
+ export function operatorLabelFor<TData extends RowData>(
111
+ option: FilterOption,
112
+ column: Column<TData> | undefined,
113
+ ): string {
114
+ const editorType = column?.columnDef.editorType;
115
+ if (editorType === "date" || editorType === "datetime") {
116
+ if (option.value === "lessThan") return "Before";
117
+ if (option.value === "greaterThan") return "After";
118
+ }
119
+ return option.label;
120
+ }
121
+
122
+ /** GridMessages key for each operator's label (see grid-messages.ts). */
123
+ const OPERATOR_MESSAGE_KEY: Record<FilterOperator, string> = {
124
+ contains: "opContains",
125
+ notContains: "opNotContains",
126
+ equals: "opEquals",
127
+ notEquals: "opNotEquals",
128
+ startsWith: "opStartsWith",
129
+ endsWith: "opEndsWith",
130
+ regex: "opRegex",
131
+ in: "opIn",
132
+ notIn: "opNotIn",
133
+ greaterThan: "opGreaterThan",
134
+ lessThan: "opLessThan",
135
+ between: "opBetween",
136
+ isBlank: "opIsBlank",
137
+ isNotBlank: "opIsNotBlank",
138
+ };
139
+
140
+ /**
141
+ * Localized operator label: honors date `Before`/`After` relabels, then the
142
+ * `localeText` override for the operator, falling back to the English
143
+ * `option.label`. `messages` is a loose string map to avoid a type cycle.
144
+ */
145
+ export function localizeOperatorLabel<TData extends RowData>(
146
+ option: FilterOption,
147
+ column: Column<TData> | undefined,
148
+ messages?: Record<string, string> | null,
149
+ ): string {
150
+ const editorType = column?.columnDef.editorType;
151
+ if (editorType === "date" || editorType === "datetime") {
152
+ if (option.value === "lessThan") return messages?.opBefore || "Before";
153
+ if (option.value === "greaterThan") return messages?.opAfter || "After";
154
+ }
155
+ const key = OPERATOR_MESSAGE_KEY[option.value];
156
+ return (key && messages?.[key]) || option.label;
157
+ }
@@ -6,6 +6,7 @@ import {
6
6
  applyExcelFilter,
7
7
  splitInTokens,
8
8
  joinInTokens,
9
+ trailingInToken,
9
10
  type ExcelFilter,
10
11
  } from './excel-filters'
11
12
 
@@ -211,6 +212,48 @@ describe('in-token serialization helpers', () => {
211
212
  })
212
213
  it('joinInTokens round-trips through splitInTokens', () => {
213
214
  expect(splitInTokens(joinInTokens(['a', 'b', 'c']))).toEqual(['a', 'b', 'c'])
214
- expect(joinInTokens([' a ', '', 'b'])).toBe('a\nb')
215
+ expect(joinInTokens([' a ', '', 'b'])).toBe('a, b')
216
+ })
217
+ it('joinInTokens survives an <input type="text"> value round-trip', () => {
218
+ // The input value sanitiser strips newlines, so the separator must not be
219
+ // one - otherwise the list comes back as a single run-together token.
220
+ const input = document.createElement('input')
221
+ input.value = joinInTokens(['AAPL', 'MSFT'])
222
+ expect(splitInTokens(input.value)).toEqual(['AAPL', 'MSFT'])
223
+ })
224
+ it('trailingInToken returns the fragment still being typed', () => {
225
+ expect(trailingInToken('AAPL, MS')).toBe('MS')
226
+ expect(trailingInToken('AAPL,MS')).toBe('MS')
227
+ expect(trailingInToken('MS')).toBe('MS')
228
+ expect(trailingInToken('AAPL\nMS')).toBe('MS')
229
+ })
230
+ it('trailingInToken is empty once the fragment is separated off', () => {
231
+ expect(trailingInToken('AAPL, ')).toBe('')
232
+ expect(trailingInToken('AAPL,')).toBe('')
233
+ expect(trailingInToken('AAPL\n')).toBe('')
234
+ expect(trailingInToken('')).toBe('')
235
+ expect(trailingInToken(null)).toBe('')
236
+ })
237
+ it('quotes tokens containing a separator so they survive the round-trip', () => {
238
+ // Facet labels really do contain commas: numeric buckets come from
239
+ // toLocaleString ("1,234 - 5,678") and date buckets use a short month.
240
+ const labels = ['1,234 - 5,678', 'Aug 17, 2026', 'plain']
241
+ expect(splitInTokens(joinInTokens(labels))).toEqual(labels)
242
+ expect(joinInTokens(['Aug 17, 2026'])).toBe('"Aug 17, 2026"')
243
+ })
244
+ it('round-trips a token containing a quote', () => {
245
+ expect(splitInTokens(joinInTokens(['5" pipe']))).toEqual(['5" pipe'])
246
+ expect(splitInTokens(joinInTokens(['a"b, c']))).toEqual(['a"b, c'])
247
+ })
248
+ it('treats a quote mid-token as literal, not as an opening quote', () => {
249
+ expect(splitInTokens('5" pipe, plain')).toEqual(['5" pipe', 'plain'])
250
+ })
251
+ it('keeps inner spacing of a quoted token, trims an unquoted one', () => {
252
+ expect(splitInTokens('" padded ", bare ')).toEqual([' padded ', 'bare'])
253
+ })
254
+ it('trailingInToken understands quoted fragments', () => {
255
+ expect(trailingInToken('AAPL, "Aug 17, 20')).toBe('Aug 17, 20')
256
+ expect(trailingInToken('AAPL, "Aug 17, 2026"')).toBe('Aug 17, 2026')
257
+ expect(trailingInToken('"Aug 17, 2026", ')).toBe('')
215
258
  })
216
259
  })
@@ -57,29 +57,106 @@ export function normalizeForFilter(
57
57
 
58
58
  /**
59
59
  * Delimiter used to serialise the `in` / `notIn` value list into the single
60
- * `value` string that the filter model carries. A newline is used because it
61
- * (unlike a comma) practically never appears inside a typed token, so tokens
62
- * survive the round-trip unescaped. The chip input in the filter row reads and
63
- * writes with the same separator via {@link splitInTokens} / {@link joinInTokens}.
60
+ * `value` string that the filter model carries.
61
+ *
62
+ * This has to be single-line safe: the tool panel and the filter menu show the
63
+ * serialised list in a plain `<input type="text">`, and the HTML input value
64
+ * sanitiser STRIPS newlines - so a newline-joined list came back out of the DOM
65
+ * as one run-together token. `splitInTokens` accepts newlines too, so values
66
+ * serialised by older builds still parse.
64
67
  */
65
- export const IN_TOKEN_SEP = '\n'
68
+ export const IN_TOKEN_SEP = ', '
66
69
 
67
70
  /**
68
- * Split a serialised `in` / `notIn` value into its individual tokens. Accepts
69
- * both the newline separator the chip input writes and a plain comma-separated
70
- * string (what a raw text field, e.g. the tool-panel filter, produces), so the
71
- * same value round-trips through either UI.
71
+ * Split a serialised `in` / `notIn` value into its individual tokens.
72
+ *
73
+ * The grammar is CSV-shaped: comma OR newline separates, and a token may be
74
+ * double-quoted to carry a separator literally (`""` escapes a quote inside
75
+ * one). Quoting is what makes a comma separator safe - facet values commonly
76
+ * contain commas of their own, because numeric bucket labels are built with
77
+ * `toLocaleString` ("1,234 - 5,678") and date labels with a short month
78
+ * ("Aug 17, 2026"). A quote only opens a token when it is the token's first
79
+ * non-space character, so an unquoted `5" pipe` stays literal.
72
80
  */
73
81
  export function splitInTokens(value: unknown): string[] {
74
- return String(value ?? '')
75
- .split(/[\n,]/)
82
+ const { tokens, trailing } = scanInTokens(value)
83
+ if (trailing) tokens.push(trailing)
84
+ return tokens
85
+ }
86
+
87
+ /**
88
+ * Serialise a list of tokens back into the single `in` / `notIn` value,
89
+ * quoting any token that carries a separator or a quote of its own.
90
+ */
91
+ export function joinInTokens(tokens: ReadonlyArray<string>): string {
92
+ return tokens
76
93
  .map((t) => t.trim())
77
94
  .filter((t) => t.length > 0)
95
+ .map((t) => (/["\n,]/.test(t) ? `"${t.replace(/"/g, '""')}"` : t))
96
+ .join(IN_TOKEN_SEP)
78
97
  }
79
98
 
80
- /** Serialise a list of tokens back into the single `in` / `notIn` value. */
81
- export function joinInTokens(tokens: ReadonlyArray<string>): string {
82
- return tokens.map((t) => t.trim()).filter((t) => t.length > 0).join(IN_TOKEN_SEP)
99
+ /**
100
+ * The token still being typed at the end of an `in` / `notIn` value - the text
101
+ * after the last separator. `'AAPL, MS'` -> `'MS'`; a value ending in a
102
+ * separator (or empty) has no trailing token.
103
+ *
104
+ * Inputs that hold the whole token list use this to drive the value-suggestion
105
+ * dropdown: the trailing fragment is the search query, not a committed token.
106
+ */
107
+ export function trailingInToken(value: unknown): string {
108
+ return scanInTokens(value).trailing
109
+ }
110
+
111
+ /**
112
+ * One pass over a serialised `in` / `notIn` value, splitting it into the
113
+ * tokens that are terminated by a separator and the unterminated fragment at
114
+ * the end. Every token helper above is a view onto this, so they can never
115
+ * disagree about where a quoted token starts or ends.
116
+ */
117
+ function scanInTokens(value: unknown): { tokens: string[]; trailing: string } {
118
+ const raw = String(value ?? '')
119
+ const tokens: string[] = []
120
+ let buf = ''
121
+ let quoted = false
122
+ let wasQuoted = false
123
+ let closed = false
124
+
125
+ // A quoted token keeps its inner spacing; an unquoted one is trimmed.
126
+ const take = () => {
127
+ const t = wasQuoted ? buf : buf.trim()
128
+ buf = ''
129
+ wasQuoted = false
130
+ closed = false
131
+ return t
132
+ }
133
+
134
+ for (let i = 0; i < raw.length; i += 1) {
135
+ const ch = raw[i]
136
+ if (quoted) {
137
+ if (ch !== '"') { buf += ch; continue }
138
+ // A doubled quote is an escaped literal quote, not the closing one.
139
+ if (raw[i + 1] === '"') { buf += '"'; i += 1; continue }
140
+ quoted = false
141
+ closed = true
142
+ continue
143
+ }
144
+ if (ch === ',' || ch === '\n') {
145
+ const t = take()
146
+ if (t.length) tokens.push(t)
147
+ continue
148
+ }
149
+ // Only opens a quoted token at the token's start - `5" pipe` is literal.
150
+ if (ch === '"' && !closed && !buf.trim()) {
151
+ quoted = true
152
+ wasQuoted = true
153
+ buf = ''
154
+ continue
155
+ }
156
+ buf += ch
157
+ }
158
+
159
+ return { tokens, trailing: take() }
83
160
  }
84
161
 
85
162
  export function applyExcelFilter(
@@ -1,3 +1,3 @@
1
- /// <reference types="svelte" />
2
-
3
- export { default as FlexRender } from './FlexRender.svelte'
1
+ /// <reference types="svelte" />
2
+
3
+ export { default as FlexRender } from './FlexRender.svelte'