@svgrid/grid 2.1.22 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +9 -2
  2. package/dist/DockNodeView.svelte +306 -0
  3. package/dist/DockNodeView.svelte.d.ts +7 -0
  4. package/dist/DockPopoutHost.svelte +17 -0
  5. package/dist/DockPopoutHost.svelte.d.ts +14 -0
  6. package/dist/FlexRender.svelte +96 -96
  7. package/dist/GridFooter.svelte +178 -178
  8. package/dist/GridMenus.svelte +83 -3
  9. package/dist/SvAutoComplete.svelte +5 -3
  10. package/dist/SvCarousel.svelte +141 -163
  11. package/dist/SvColorInput.svelte +187 -186
  12. package/dist/SvColorInput.svelte.d.ts +1 -1
  13. package/dist/SvComboBox.svelte +7 -3
  14. package/dist/SvCommand.svelte +28 -85
  15. package/dist/SvContextMenu.svelte +116 -116
  16. package/dist/SvCountryInput.svelte +1 -1
  17. package/dist/SvDateRangeInput.svelte +1 -1
  18. package/dist/SvDateTimePicker.svelte +9 -3
  19. package/dist/SvDateTimePicker.svelte.d.ts +3 -0
  20. package/dist/SvDockLayout.svelte +244 -0
  21. package/dist/SvDockLayout.svelte.d.ts +77 -0
  22. package/dist/SvDockManager.svelte +922 -0
  23. package/dist/SvDockManager.svelte.d.ts +106 -0
  24. package/dist/SvDrawer.svelte +250 -228
  25. package/dist/SvDrawer.svelte.d.ts +4 -0
  26. package/dist/SvDropDownList.svelte +85 -13
  27. package/dist/SvDropDownList.svelte.d.ts +3 -0
  28. package/dist/SvField.svelte +21 -4
  29. package/dist/SvField.svelte.d.ts +1 -0
  30. package/dist/SvForm.svelte +201 -91
  31. package/dist/SvForm.svelte.d.ts +15 -22
  32. package/dist/SvGrid.controller.svelte.d.ts +32 -1
  33. package/dist/SvGrid.controller.svelte.js +156 -27
  34. package/dist/SvGrid.css +193 -5
  35. package/dist/SvGrid.svelte +3131 -2786
  36. package/dist/SvGrid.types.d.ts +452 -1
  37. package/dist/SvGridBoard.svelte +2339 -2339
  38. package/dist/SvGridChart.svelte +1724 -1724
  39. package/dist/SvGridChartPanel.svelte +485 -485
  40. package/dist/SvGridDropdown.svelte +689 -682
  41. package/dist/SvGridSelect.svelte +1 -0
  42. package/dist/SvGroupCell.svelte +109 -109
  43. package/dist/SvListBox.svelte +36 -14
  44. package/dist/SvMaskedInput.svelte +4 -3
  45. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  46. package/dist/SvMenu.svelte +124 -124
  47. package/dist/SvMenuList.svelte +117 -165
  48. package/dist/SvMenuList.svelte.d.ts +2 -13
  49. package/dist/SvModal.svelte +12 -29
  50. package/dist/SvMultiSelect.svelte +1 -0
  51. package/dist/SvNumberInput.svelte +131 -130
  52. package/dist/SvNumberInput.svelte.d.ts +1 -1
  53. package/dist/SvOtpInput.svelte +158 -158
  54. package/dist/SvPagination.svelte +18 -18
  55. package/dist/SvPasswordInput.svelte +3 -2
  56. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  57. package/dist/SvPhoneInput.svelte +3 -2
  58. package/dist/SvPhoneInput.svelte.d.ts +1 -1
  59. package/dist/SvPopover.svelte +139 -139
  60. package/dist/SvRowGroupPanel.svelte +170 -170
  61. package/dist/SvScrollArea.svelte +61 -61
  62. package/dist/SvStepper.svelte +6 -9
  63. package/dist/SvTagsInput.svelte +3 -2
  64. package/dist/SvTagsInput.svelte.d.ts +1 -1
  65. package/dist/SvTooltip.svelte +127 -138
  66. package/dist/SvTour.svelte +204 -176
  67. package/dist/SvTree.svelte +377 -377
  68. package/dist/SvTreeSelect.svelte +230 -229
  69. package/dist/build-api.js +76 -54
  70. package/dist/cdn/svgrid.js +10913 -6450
  71. package/dist/cdn/svgrid.svelte-external.js +9628 -5481
  72. package/dist/chart-export.js +8 -8
  73. package/dist/columns.js +6 -0
  74. package/dist/conditional-formatting.d.ts +67 -3
  75. package/dist/conditional-formatting.js +124 -17
  76. package/dist/createCarousel.svelte.d.ts +72 -0
  77. package/dist/createCarousel.svelte.js +80 -0
  78. package/dist/createCommand.svelte.d.ts +62 -0
  79. package/dist/createCommand.svelte.js +109 -0
  80. package/dist/createForm.svelte.d.ts +62 -0
  81. package/dist/createForm.svelte.js +374 -0
  82. package/dist/createMenu.svelte.d.ts +88 -0
  83. package/dist/createMenu.svelte.js +153 -0
  84. package/dist/createOverlay.svelte.d.ts +38 -0
  85. package/dist/createOverlay.svelte.js +68 -0
  86. package/dist/createPagination.d.ts +74 -0
  87. package/dist/createPagination.js +71 -0
  88. package/dist/createPopoverSelect.svelte.js +6 -12
  89. package/dist/createStepper.d.ts +44 -0
  90. package/dist/createStepper.js +26 -0
  91. package/dist/createTooltip.svelte.d.ts +32 -0
  92. package/dist/createTooltip.svelte.js +61 -0
  93. package/dist/datetime/timezone.d.ts +36 -0
  94. package/dist/datetime/timezone.js +110 -0
  95. package/dist/dock-context.d.ts +52 -0
  96. package/dist/dock-context.js +1 -0
  97. package/dist/dock-manager-model.d.ts +136 -0
  98. package/dist/dock-manager-model.js +514 -0
  99. package/dist/dock-model.d.ts +105 -0
  100. package/dist/dock-model.js +288 -0
  101. package/dist/dock-popout.d.ts +30 -0
  102. package/dist/dock-popout.js +81 -0
  103. package/dist/editor-contract.d.ts +2 -0
  104. package/dist/filter-operators.d.ts +6 -0
  105. package/dist/filter-operators.js +35 -2
  106. package/dist/filtering/excel-filters.d.ts +18 -1
  107. package/dist/filtering/excel-filters.js +56 -0
  108. package/dist/form-field.d.ts +73 -0
  109. package/dist/form-field.js +15 -0
  110. package/dist/index.d.ts +25 -5
  111. package/dist/index.js +40 -4
  112. package/dist/list-nav.d.ts +18 -0
  113. package/dist/list-nav.js +30 -0
  114. package/dist/list-option.d.ts +2 -0
  115. package/dist/menu-item.d.ts +20 -0
  116. package/dist/menu-item.js +1 -0
  117. package/dist/menus.d.ts +5 -0
  118. package/dist/menus.js +68 -16
  119. package/dist/panel-resize.d.ts +25 -0
  120. package/dist/panel-resize.js +40 -0
  121. package/dist/popover.d.ts +29 -3
  122. package/dist/popover.js +22 -6
  123. package/dist/recurrence.d.ts +33 -7
  124. package/dist/recurrence.js +151 -13
  125. package/dist/row-drag.d.ts +8 -0
  126. package/dist/row-drag.js +11 -0
  127. package/dist/scheduler-ical.d.ts +30 -0
  128. package/dist/scheduler-ical.js +180 -0
  129. package/dist/scheduler-model.d.ts +269 -0
  130. package/dist/scheduler-model.js +602 -0
  131. package/dist/scheduler-view.svelte.d.ts +22 -0
  132. package/dist/scheduler-view.svelte.js +13 -0
  133. package/dist/spreadsheet.js +20 -12
  134. package/dist/ui-app.types.d.ts +3 -0
  135. package/package.json +14 -10
  136. package/src/DockNodeView.svelte +306 -0
  137. package/src/DockPopoutHost.svelte +17 -0
  138. package/src/FlexRender.svelte +96 -96
  139. package/src/GridFooter.svelte +178 -178
  140. package/src/GridMenus.svelte +83 -3
  141. package/src/SvAutoComplete.svelte +5 -3
  142. package/src/SvCarousel.svelte +141 -163
  143. package/src/SvCarousel.test.ts +68 -0
  144. package/src/SvColorInput.svelte +187 -186
  145. package/src/SvComboBox.svelte +7 -3
  146. package/src/SvCommand.svelte +28 -85
  147. package/src/SvCommand.test.ts +78 -0
  148. package/src/SvContextMenu.svelte +116 -116
  149. package/src/SvCountryInput.svelte +1 -1
  150. package/src/SvDateRangeInput.svelte +1 -1
  151. package/src/SvDateTimePicker.svelte +9 -3
  152. package/src/SvDockLayout.svelte +244 -0
  153. package/src/SvDockManager.svelte +922 -0
  154. package/src/SvDrawer.svelte +250 -228
  155. package/src/SvDropDownList.svelte +85 -13
  156. package/src/SvField.svelte +21 -4
  157. package/src/SvForm.svelte +201 -91
  158. package/src/SvForm.test.ts +299 -0
  159. package/src/SvGrid.controller.svelte.ts +3158 -3034
  160. package/src/SvGrid.css +193 -5
  161. package/src/SvGrid.svelte +3131 -2786
  162. package/src/SvGrid.types.ts +1489 -1027
  163. package/src/SvGridBoard.svelte +2339 -2339
  164. package/src/SvGridChart.svelte +1724 -1724
  165. package/src/SvGridChartPanel.svelte +485 -485
  166. package/src/SvGridDropdown.svelte +689 -682
  167. package/src/SvGridSelect.svelte +1 -0
  168. package/src/SvGroupCell.svelte +109 -109
  169. package/src/SvListBox.svelte +36 -14
  170. package/src/SvMaskedInput.svelte +4 -3
  171. package/src/SvMenu.svelte +124 -124
  172. package/src/SvMenu.test.ts +97 -73
  173. package/src/SvMenuList.svelte +117 -165
  174. package/src/SvModal.svelte +12 -29
  175. package/src/SvModal.test.ts +82 -0
  176. package/src/SvMultiSelect.svelte +1 -0
  177. package/src/SvNumberInput.svelte +131 -130
  178. package/src/SvOtpInput.svelte +158 -158
  179. package/src/SvPagination.svelte +18 -18
  180. package/src/SvPasswordInput.svelte +3 -2
  181. package/src/SvPhoneInput.svelte +3 -2
  182. package/src/SvPopover.svelte +139 -139
  183. package/src/SvRowGroupPanel.svelte +170 -170
  184. package/src/SvScrollArea.svelte +61 -61
  185. package/src/SvStepper.svelte +6 -9
  186. package/src/SvTagsInput.svelte +3 -2
  187. package/src/SvTooltip.svelte +127 -138
  188. package/src/SvTooltip.test.ts +73 -0
  189. package/src/SvTour.svelte +204 -176
  190. package/src/SvTree.svelte +377 -377
  191. package/src/SvTreeSelect.svelte +230 -229
  192. package/src/a11y.contract.test.ts +49 -49
  193. package/src/a11y.test.ts +59 -59
  194. package/src/a11y.ts +61 -61
  195. package/src/build-api.ts +870 -847
  196. package/src/cell-formatting.ts +169 -169
  197. package/src/cell-render.ts +469 -469
  198. package/src/chart-export.ts +201 -201
  199. package/src/chart.ts +2296 -2296
  200. package/src/collaboration.test.ts +104 -104
  201. package/src/collaboration.ts +167 -167
  202. package/src/columns.ts +5 -0
  203. package/src/conditional-formatting.test.ts +104 -0
  204. package/src/conditional-formatting.ts +176 -19
  205. package/src/core.performance.test.ts +30 -30
  206. package/src/core.ts +1111 -1111
  207. package/src/createAutocomplete.svelte.ts +132 -132
  208. package/src/createCarousel.svelte.ts +104 -0
  209. package/src/createCombobox.svelte.ts +179 -179
  210. package/src/createCommand.svelte.ts +125 -0
  211. package/src/createCountryInput.svelte.ts +157 -157
  212. package/src/createDropdownList.svelte.ts +162 -162
  213. package/src/createForm.svelte.ts +333 -0
  214. package/src/createGrid.svelte.ts +42 -42
  215. package/src/createGrid.test.ts +10 -10
  216. package/src/createGridState.svelte.ts +17 -17
  217. package/src/createMenu.svelte.ts +202 -0
  218. package/src/createOverlay.svelte.ts +101 -0
  219. package/src/createPagination.test.ts +65 -0
  220. package/src/createPagination.ts +84 -0
  221. package/src/createPopoverSelect.svelte.ts +206 -210
  222. package/src/createStepper.test.ts +62 -0
  223. package/src/createStepper.ts +61 -0
  224. package/src/createTooltip.svelte.ts +86 -0
  225. package/src/createTree.svelte.ts +319 -319
  226. package/src/datetime/timezone.test.ts +69 -0
  227. package/src/datetime/timezone.ts +134 -0
  228. package/src/dock-context.ts +47 -0
  229. package/src/dock-manager-model.test.ts +275 -0
  230. package/src/dock-manager-model.ts +593 -0
  231. package/src/dock-model.test.ts +163 -0
  232. package/src/dock-model.ts +368 -0
  233. package/src/dock-popout.ts +94 -0
  234. package/src/editing.test.ts +859 -859
  235. package/src/editing.ts +675 -675
  236. package/src/editor-contract.ts +2 -0
  237. package/src/export-data-api.test.ts +126 -126
  238. package/src/export-format.test.ts +107 -107
  239. package/src/export-format.ts +598 -598
  240. package/src/filter-operators.test.ts +59 -0
  241. package/src/filter-operators.ts +35 -2
  242. package/src/filtering/excel-filters.test.ts +86 -1
  243. package/src/filtering/excel-filters.ts +65 -0
  244. package/src/flex-render.ts +3 -3
  245. package/src/form-field.ts +87 -0
  246. package/src/index.ts +623 -469
  247. package/src/keyboard.test.ts +59 -59
  248. package/src/keyboard.ts +97 -97
  249. package/src/list-nav.test.ts +49 -0
  250. package/src/list-nav.ts +29 -0
  251. package/src/list-option.ts +2 -0
  252. package/src/menu-item.ts +21 -0
  253. package/src/menus.test.ts +48 -0
  254. package/src/menus.ts +639 -582
  255. package/src/merge-objects.ts +48 -48
  256. package/src/overlays.test.ts +90 -90
  257. package/src/panel-resize.ts +52 -0
  258. package/src/popover.bounds.test.ts +93 -0
  259. package/src/popover.ts +51 -7
  260. package/src/recurrence.test.ts +88 -1
  261. package/src/recurrence.ts +166 -15
  262. package/src/render-component.ts +28 -28
  263. package/src/row-drag.ts +10 -0
  264. package/src/scheduler-ical.test.ts +68 -0
  265. package/src/scheduler-ical.ts +180 -0
  266. package/src/scheduler-model.test.ts +546 -0
  267. package/src/scheduler-model.ts +853 -0
  268. package/src/scheduler-view.svelte.ts +33 -0
  269. package/src/selection.test.ts +754 -754
  270. package/src/selection.ts +600 -600
  271. package/src/server-data-source.test.ts +289 -289
  272. package/src/server-data-source.ts +413 -413
  273. package/src/sparkline.test.ts +68 -68
  274. package/src/sparkline.ts +169 -169
  275. package/src/spreadsheet.test.ts +489 -489
  276. package/src/spreadsheet.ts +312 -304
  277. package/src/static-functions.ts +11 -11
  278. package/src/subscribe.ts +38 -38
  279. package/src/svgrid-wrapper.types.ts +501 -501
  280. package/src/svgrid.behavior.test.ts +706 -706
  281. package/src/svgrid.charting.test.ts +527 -527
  282. package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
  283. package/src/svgrid.features.test.ts +157 -157
  284. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  285. package/src/svgrid.wrapper.test.ts +40 -40
  286. package/src/test-setup.ts +62 -62
  287. package/src/themes/index.ts +159 -159
  288. package/src/ui-app.types.ts +3 -0
  289. package/src/virtual.test.ts +6 -4
  290. package/src/virtualization/column-virtualizer.test.ts +27 -27
  291. package/src/virtualization/column-virtualizer.ts +30 -30
  292. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  293. package/src/virtualization/types.ts +30 -30
  294. package/src/virtualization/virtualizer.test.ts +47 -47
  295. package/src/virtualization/virtualizer.ts +296 -296
@@ -6,6 +6,8 @@ import {
6
6
  NUMBER_OPERATORS,
7
7
  DATE_OPERATORS,
8
8
  CHECKBOX_OPERATORS,
9
+ SET_OPERATORS,
10
+ VALUELESS_OPERATORS,
9
11
  operatorOption,
10
12
  operatorsForColumn,
11
13
  defaultOperatorFor,
@@ -55,6 +57,63 @@ describe("filter-operators: catalogue integrity", () => {
55
57
  expect(values).toContain("between");
56
58
  expect(values).toContain("isBlank");
57
59
  });
60
+
61
+ it("catalogues the extended operator set (parity with jQWidgets-style row)", () => {
62
+ const values = filterOperatorOptions.map((o) => o.value);
63
+ for (const op of [
64
+ "notContains",
65
+ "notEquals",
66
+ "endsWith",
67
+ "regex",
68
+ "in",
69
+ "notIn",
70
+ "isNotBlank",
71
+ ]) {
72
+ expect(values).toContain(op);
73
+ }
74
+ });
75
+
76
+ it("SET_OPERATORS and VALUELESS_OPERATORS reference catalogued operators", () => {
77
+ const known = new Set(filterOperatorOptions.map((o) => o.value));
78
+ for (const id of [...SET_OPERATORS, ...VALUELESS_OPERATORS]) {
79
+ expect(known.has(id)).toBe(true);
80
+ }
81
+ expect([...SET_OPERATORS]).toEqual(["in", "notIn"]);
82
+ expect([...VALUELESS_OPERATORS]).toEqual(["isBlank", "isNotBlank"]);
83
+ });
84
+ });
85
+
86
+ describe("filter-operators: per-type coverage of the new operators", () => {
87
+ it("text columns expose the full string + set operator range", () => {
88
+ for (const op of [
89
+ "notContains",
90
+ "notEquals",
91
+ "endsWith",
92
+ "regex",
93
+ "in",
94
+ "notIn",
95
+ "isNotBlank",
96
+ ] as FilterOperator[]) {
97
+ expect(TEXT_OPERATORS).toContain(op);
98
+ }
99
+ });
100
+
101
+ it("number columns gain notEquals + in/notIn + isNotBlank but not text-only ops", () => {
102
+ expect(NUMBER_OPERATORS).toContain("notEquals");
103
+ expect(NUMBER_OPERATORS).toContain("in");
104
+ expect(NUMBER_OPERATORS).toContain("isNotBlank");
105
+ expect(NUMBER_OPERATORS).not.toContain("contains");
106
+ expect(NUMBER_OPERATORS).not.toContain("regex");
107
+ });
108
+
109
+ it("checkbox columns gain isNotBlank alongside equals/isBlank", () => {
110
+ expect(CHECKBOX_OPERATORS).toContain("isNotBlank");
111
+ });
112
+
113
+ it("date columns keep before/after semantics and add isNotBlank", () => {
114
+ expect(DATE_OPERATORS).toContain("isNotBlank");
115
+ expect(DATE_OPERATORS).not.toContain("in");
116
+ });
58
117
  });
59
118
 
60
119
  describe("operatorOption", () => {
@@ -6,35 +6,68 @@ import type { FilterOperator, FilterOption } from "./SvGrid.types";
6
6
 
7
7
  export const filterOperatorOptions: Array<FilterOption> = [
8
8
  { value: "contains", label: "Contains", iconName: "op-contains" },
9
+ { value: "notContains", label: "Not contains", iconName: "op-notContains" },
9
10
  { value: "equals", label: "Equals", iconName: "op-equals" },
11
+ { value: "notEquals", label: "Not equals", iconName: "op-notEquals" },
10
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" },
11
17
  { value: "greaterThan", label: "Greater than", iconName: "op-greaterThan" },
12
18
  { value: "lessThan", label: "Less than", iconName: "op-lessThan" },
13
19
  { value: "between", label: "Between", iconName: "op-between" },
14
- { value: "isBlank", label: "Is blank", iconName: "op-isBlank" },
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",
15
31
  ];
16
32
  /** Which operators make sense for each column editor type. */
17
33
  export const TEXT_OPERATORS: Array<FilterOperator> = [
18
34
  "contains",
35
+ "notContains",
19
36
  "equals",
37
+ "notEquals",
20
38
  "startsWith",
39
+ "endsWith",
40
+ "regex",
41
+ "in",
42
+ "notIn",
21
43
  "isBlank",
44
+ "isNotBlank",
22
45
  ];
23
46
  export const NUMBER_OPERATORS: Array<FilterOperator> = [
24
47
  "equals",
48
+ "notEquals",
25
49
  "greaterThan",
26
50
  "lessThan",
27
51
  "between",
52
+ "in",
53
+ "notIn",
28
54
  "isBlank",
55
+ "isNotBlank",
29
56
  ];
30
57
  export const DATE_OPERATORS: Array<FilterOperator> = [
31
58
  "equals",
59
+ "notEquals",
32
60
  "lessThan",
33
61
  "greaterThan",
34
62
  "between",
35
63
  "isBlank",
64
+ "isNotBlank",
65
+ ];
66
+ export const CHECKBOX_OPERATORS: Array<FilterOperator> = [
67
+ "equals",
68
+ "isBlank",
69
+ "isNotBlank",
36
70
  ];
37
- export const CHECKBOX_OPERATORS: Array<FilterOperator> = ["equals", "isBlank"];
38
71
  export const fallbackOperatorOption: FilterOption = {
39
72
  value: "contains",
40
73
  label: "Contains",
@@ -2,7 +2,12 @@
2
2
  * Unit tests for applyExcelFilter. Every operator + edge case.
3
3
  */
4
4
  import { describe, expect, it } from 'vitest'
5
- import { applyExcelFilter, type ExcelFilter } from './excel-filters'
5
+ import {
6
+ applyExcelFilter,
7
+ splitInTokens,
8
+ joinInTokens,
9
+ type ExcelFilter,
10
+ } from './excel-filters'
6
11
 
7
12
  const make = (
8
13
  operator: ExcelFilter['operator'],
@@ -129,3 +134,83 @@ describe('applyExcelFilter - isBlank', () => {
129
134
  expect(applyExcelFilter(0, make('isBlank'))).toBe(false)
130
135
  })
131
136
  })
137
+
138
+ describe('applyExcelFilter - isNotBlank', () => {
139
+ it('is the exact inverse of isBlank', () => {
140
+ expect(applyExcelFilter(null, make('isNotBlank'))).toBe(false)
141
+ expect(applyExcelFilter('', make('isNotBlank'))).toBe(false)
142
+ expect(applyExcelFilter(' ', make('isNotBlank'))).toBe(false)
143
+ expect(applyExcelFilter('x', make('isNotBlank'))).toBe(true)
144
+ expect(applyExcelFilter(0, make('isNotBlank'))).toBe(true)
145
+ })
146
+ })
147
+
148
+ describe('applyExcelFilter - notContains', () => {
149
+ it('is the inverse of contains', () => {
150
+ expect(applyExcelFilter('Hello World', make('notContains', 'world'))).toBe(false)
151
+ expect(applyExcelFilter('Hello', make('notContains', 'xyz'))).toBe(true)
152
+ })
153
+ it('treats empty value as universal match (nothing excluded)', () => {
154
+ expect(applyExcelFilter('anything', make('notContains', ''))).toBe(true)
155
+ })
156
+ })
157
+
158
+ describe('applyExcelFilter - notEquals', () => {
159
+ it('is the inverse of equals, case-insensitive', () => {
160
+ expect(applyExcelFilter('Boston', make('notEquals', 'boston'))).toBe(false)
161
+ expect(applyExcelFilter('Boston', make('notEquals', 'bost'))).toBe(true)
162
+ })
163
+ })
164
+
165
+ describe('applyExcelFilter - endsWith', () => {
166
+ it('matches a suffix case-insensitively', () => {
167
+ expect(applyExcelFilter('Brooklyn', make('endsWith', 'LYN'))).toBe(true)
168
+ expect(applyExcelFilter('Brooklyn', make('endsWith', 'brook'))).toBe(false)
169
+ })
170
+ })
171
+
172
+ describe('applyExcelFilter - regex', () => {
173
+ it('matches with a valid case-insensitive pattern', () => {
174
+ expect(applyExcelFilter('T-240007', make('regex', '^T-\\d+$'))).toBe(true)
175
+ expect(applyExcelFilter('BABA', make('regex', 'ba'))).toBe(true)
176
+ expect(applyExcelFilter('XYZ', make('regex', '^a'))).toBe(false)
177
+ })
178
+ it('treats an empty pattern as a universal match', () => {
179
+ expect(applyExcelFilter('anything', make('regex', ''))).toBe(true)
180
+ })
181
+ it('matches nothing (no throw) when the pattern is invalid', () => {
182
+ expect(applyExcelFilter('anything', make('regex', '('))).toBe(false)
183
+ expect(applyExcelFilter('a[b', make('regex', '['))).toBe(false)
184
+ })
185
+ })
186
+
187
+ describe('applyExcelFilter - in / notIn', () => {
188
+ it('in matches when the cell equals any token (newline- or comma-separated)', () => {
189
+ expect(applyExcelFilter('BP', make('in', 'TSM\nBP\nBABA'))).toBe(true)
190
+ expect(applyExcelFilter('bp', make('in', 'TSM, BP, BABA'))).toBe(true)
191
+ expect(applyExcelFilter('HSBA', make('in', 'TSM\nBP'))).toBe(false)
192
+ })
193
+ it('notIn is the inverse of in', () => {
194
+ expect(applyExcelFilter('BP', make('notIn', 'TSM\nBP'))).toBe(false)
195
+ expect(applyExcelFilter('HSBA', make('notIn', 'TSM\nBP'))).toBe(true)
196
+ })
197
+ it('matches only whole values, not substrings', () => {
198
+ expect(applyExcelFilter('BPX', make('in', 'BP'))).toBe(false)
199
+ })
200
+ it('treats an empty token list as a universal match', () => {
201
+ expect(applyExcelFilter('anything', make('in', ''))).toBe(true)
202
+ expect(applyExcelFilter('anything', make('notIn', ' '))).toBe(true)
203
+ })
204
+ })
205
+
206
+ describe('in-token serialization helpers', () => {
207
+ it('splitInTokens accepts newline and comma separators and trims', () => {
208
+ expect(splitInTokens('a\nb, c ,,\n')).toEqual(['a', 'b', 'c'])
209
+ expect(splitInTokens('')).toEqual([])
210
+ expect(splitInTokens(null)).toEqual([])
211
+ })
212
+ it('joinInTokens round-trips through splitInTokens', () => {
213
+ expect(splitInTokens(joinInTokens(['a', 'b', 'c']))).toEqual(['a', 'b', 'c'])
214
+ expect(joinInTokens([' a ', '', 'b'])).toBe('a\nb')
215
+ })
216
+ })
@@ -1,11 +1,18 @@
1
1
  export type ExcelFilterOperator =
2
2
  | 'contains'
3
+ | 'notContains'
3
4
  | 'equals'
5
+ | 'notEquals'
4
6
  | 'startsWith'
7
+ | 'endsWith'
8
+ | 'regex'
9
+ | 'in'
10
+ | 'notIn'
5
11
  | 'greaterThan'
6
12
  | 'lessThan'
7
13
  | 'between'
8
14
  | 'isBlank'
15
+ | 'isNotBlank'
9
16
 
10
17
  export type ExcelFilter = {
11
18
  id: string
@@ -48,6 +55,33 @@ export function normalizeForFilter(
48
55
  : stripped.toLowerCase()
49
56
  }
50
57
 
58
+ /**
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}.
64
+ */
65
+ export const IN_TOKEN_SEP = '\n'
66
+
67
+ /**
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.
72
+ */
73
+ export function splitInTokens(value: unknown): string[] {
74
+ return String(value ?? '')
75
+ .split(/[\n,]/)
76
+ .map((t) => t.trim())
77
+ .filter((t) => t.length > 0)
78
+ }
79
+
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)
83
+ }
84
+
51
85
  export function applyExcelFilter(
52
86
  cellValue: unknown,
53
87
  filter: ExcelFilter,
@@ -59,10 +93,39 @@ export function applyExcelFilter(
59
93
  switch (filter.operator) {
60
94
  case 'contains':
61
95
  return normalizedText.includes(normalizedValue)
96
+ case 'notContains':
97
+ // An empty needle is no constraint (mirrors `contains` returning true),
98
+ // so nothing is excluded until the user types something.
99
+ return normalizedValue === '' || !normalizedText.includes(normalizedValue)
62
100
  case 'equals':
63
101
  return normalizedText === normalizedValue
102
+ case 'notEquals':
103
+ return normalizedValue === '' || normalizedText !== normalizedValue
64
104
  case 'startsWith':
65
105
  return normalizedText.startsWith(normalizedValue)
106
+ case 'endsWith':
107
+ return normalizedText.endsWith(normalizedValue)
108
+ case 'regex': {
109
+ // Case-insensitive by default (matches the accent/case-folded feel of
110
+ // the other text operators). An invalid pattern matches nothing rather
111
+ // than throwing, so a half-typed regex never crashes the row model.
112
+ const pattern = String(filter.value ?? '')
113
+ if (!pattern) return true
114
+ try {
115
+ return new RegExp(pattern, 'i').test(text)
116
+ } catch {
117
+ return false
118
+ }
119
+ }
120
+ case 'in':
121
+ case 'notIn': {
122
+ const tokens = splitInTokens(filter.value)
123
+ if (tokens.length === 0) return true
124
+ const hit = tokens.some(
125
+ (t) => normalizeForFilter(t, options?.locale) === normalizedText,
126
+ )
127
+ return filter.operator === 'in' ? hit : !hit
128
+ }
66
129
  case 'greaterThan': {
67
130
  const a = Number(cellValue)
68
131
  const b = Number(filter.value)
@@ -97,5 +160,7 @@ export function applyExcelFilter(
97
160
  }
98
161
  case 'isBlank':
99
162
  return text.trim().length === 0
163
+ case 'isNotBlank':
164
+ return text.trim().length > 0
100
165
  }
101
166
  }
@@ -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'
@@ -0,0 +1,87 @@
1
+ import type { Validator } from './validators'
2
+
3
+ /**
4
+ * Field descriptor for <SvForm> and the headless `createForm` core. Kept in a
5
+ * plain `.ts` module so the core can import the type without pulling in the
6
+ * `.svelte` component (which would create a `.svelte.ts` -> `.svelte` import
7
+ * cycle Vite's dev import-analysis fails to resolve).
8
+ */
9
+ export type FormFieldType =
10
+ | 'text' | 'email' | 'tel' | 'textarea' | 'number' | 'password'
11
+ | 'select' | 'checkbox' | 'switch' | 'date' | 'color' | 'rating'
12
+ | 'array'
13
+
14
+ export type FormField = {
15
+ name: string
16
+ label: string
17
+ type?: FormFieldType
18
+ required?: boolean
19
+ placeholder?: string
20
+ options?: Array<{ value: string | number; label: string; color?: string }>
21
+ /** Declarative validation rules (see `rules` - email/pattern/min/compare...). */
22
+ rules?: ReadonlyArray<Validator>
23
+ /** Return an error message, or null/undefined when valid. */
24
+ validate?: (value: any, values: Record<string, any>) => string | null | undefined
25
+ /**
26
+ * Async validator (e.g. "is this username taken?"). Runs after the sync checks
27
+ * pass, debounced (`asyncDebounce`, default 300ms) and stale-guarded so only the
28
+ * latest result wins. While it runs, `form.isValidating(name)` is true and the
29
+ * field shows a checking indicator. Submit waits for all async validators.
30
+ */
31
+ asyncValidate?: (value: any, values: Record<string, any>) => Promise<string | null | undefined>
32
+ /** Debounce (ms) before `asyncValidate` runs on edit. Default 300. */
33
+ asyncDebounce?: number
34
+ /** Span full width in the grid. */
35
+ full?: boolean
36
+ /**
37
+ * Show the field only when this is `true` / returns `true` (default: always).
38
+ * A field derived from other values, e.g. `visible: (v) => v.hasAddress`.
39
+ * Hidden fields are skipped in validation and excluded from the submitted values.
40
+ */
41
+ visible?: boolean | ((values: Record<string, any>) => boolean)
42
+ /** Disable the field, statically or derived from other values. */
43
+ disabled?: boolean | ((values: Record<string, any>) => boolean)
44
+ /**
45
+ * For `type: 'array'` - the fields of each repeatable item. The value is an
46
+ * array of item objects; each item's fields validate against that item (so
47
+ * cross-field rules within a row work). `required`/`minItems`/`maxItems` gate
48
+ * the array length.
49
+ */
50
+ itemFields?: ReadonlyArray<FormField>
51
+ /** Label for the "add item" button of an array field. Default "+ Add". */
52
+ addLabel?: string
53
+ /** Min / max item count for an array field. */
54
+ minItems?: number
55
+ maxItems?: number
56
+ }
57
+
58
+ /**
59
+ * A group of fields rendered as a titled fieldset - and, when `<SvForm stepper>`
60
+ * is set, as one step of a wizard. Use in place of a plain `FormField` inside the
61
+ * `fields` array; the schema may freely mix flat fields and sections.
62
+ */
63
+ export type FormSection = {
64
+ section: string
65
+ description?: string
66
+ fields: ReadonlyArray<FormField>
67
+ /** Column count for this section's grid (defaults to the form's `columns`). */
68
+ columns?: number
69
+ }
70
+
71
+ /** An entry in a form schema: either a single field or a titled section/step. */
72
+ export type FormEntry = FormField | FormSection
73
+
74
+ /** Narrow a schema entry to a section (has a `fields` array). */
75
+ export function isFormSection(e: FormEntry): e is FormSection {
76
+ return Array.isArray((e as FormSection).fields)
77
+ }
78
+
79
+ /** Flatten a mixed schema (fields + sections) to the flat list of fields. */
80
+ export function flattenFields(entries: ReadonlyArray<FormEntry>): FormField[] {
81
+ const out: FormField[] = []
82
+ for (const e of entries) {
83
+ if (isFormSection(e)) out.push(...e.fields)
84
+ else out.push(e)
85
+ }
86
+ return out
87
+ }