@svgrid/grid 2.2.31 → 2.2.33

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 (196) hide show
  1. package/README.md +1 -1
  2. package/dist/DockNodeView.svelte +1 -1
  3. package/dist/GridFooter.svelte +0 -40
  4. package/dist/GridMenus.svelte +0 -77
  5. package/dist/SvAvatar.svelte +1 -1
  6. package/dist/SvCheckBox.svelte +2 -1
  7. package/dist/SvCircularProgress.svelte +1 -1
  8. package/dist/SvDateRangeInput.svelte +1 -1
  9. package/dist/SvGrid.controller.svelte.d.ts +20 -0
  10. package/dist/SvGrid.controller.svelte.js +358 -18
  11. package/dist/SvGrid.css +103 -3
  12. package/dist/SvGrid.svelte +164 -35
  13. package/dist/SvGrid.types.d.ts +81 -0
  14. package/dist/SvGridChart.svelte +2 -10
  15. package/dist/SvGridDropdown.svelte +38 -6
  16. package/dist/SvGridDropdown.svelte.d.ts +4 -0
  17. package/dist/SvGridSelect.svelte +12 -5
  18. package/dist/SvGridSelect.svelte.d.ts +6 -0
  19. package/dist/SvGroupCell.svelte +9 -2
  20. package/dist/SvKbd.svelte +1 -1
  21. package/dist/SvList.svelte +1 -1
  22. package/dist/SvMenubar.svelte +1 -1
  23. package/dist/SvMultiSelect.svelte +12 -4
  24. package/dist/SvMultiSelect.svelte.d.ts +6 -0
  25. package/dist/SvOtpInput.svelte +1 -1
  26. package/dist/SvPasswordInput.svelte +1 -1
  27. package/dist/SvProgress.svelte +1 -1
  28. package/dist/SvRating.svelte +1 -1
  29. package/dist/SvResult.svelte +1 -1
  30. package/dist/SvRichText.svelte +35 -16
  31. package/dist/SvRichText.svelte.d.ts +9 -0
  32. package/dist/SvSwitchButton.svelte +2 -1
  33. package/dist/SvToaster.svelte +1 -1
  34. package/dist/SvTour.svelte +4 -0
  35. package/dist/SvTree.svelte +9 -2
  36. package/dist/SvTree.svelte.d.ts +5 -0
  37. package/dist/SvTreeSelect.svelte +6 -4
  38. package/dist/build-api.js +16 -0
  39. package/dist/cdn/{GridMenus-Ds3OpzZT.js → GridMenus-B7XhhqdN.js} +34 -36
  40. package/dist/cdn/{GridMenus-Da_p7SKS.js → GridMenus-BxmIvCJh.js} +35 -37
  41. package/dist/cdn/{SvDateTimePicker-DB0aIEk8.js → SvDateTimePicker-ClqtVIj3.js} +2 -1
  42. package/dist/cdn/{SvDateTimePicker-B8GopjhC.js → SvDateTimePicker-o94wfSUa.js} +3 -2
  43. package/dist/cdn/{SvGridChart-BcLCYvw9.js → SvGridChart-Cr_90pPD.js} +1 -1
  44. package/dist/cdn/{SvGridChartPanel-DvjwxLvK.js → SvGridChartPanel-ICbxiUvK.js} +4 -4
  45. package/dist/cdn/{SvGridChartView-PwqI4s02.js → SvGridChartView-Bx42PMvf.js} +2 -2
  46. package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-CUOdm7YA.js} +93 -88
  47. package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D0VdjeR8.js} +129 -123
  48. package/dist/cdn/{disclose-version-DoD9AFD_.js → disclose-version-DljhZohK.js} +45 -45
  49. package/dist/cdn/{src-DYDCiC0D.js → src-erjBRYfX.js} +5952 -5615
  50. package/dist/cdn/{src-BLJfdyL0.js → src-ktbQVOk-.js} +4190 -3851
  51. package/dist/cdn/svgrid.js +9 -9
  52. package/dist/cdn/svgrid.svelte-external.js +8 -8
  53. package/dist/cell-render.d.ts +1 -0
  54. package/dist/cell-render.js +115 -1
  55. package/dist/cell-values.js +5 -0
  56. package/dist/chart.d.ts +2 -0
  57. package/dist/chart.js +1 -4
  58. package/dist/core.d.ts +80 -9
  59. package/dist/core.js +89 -0
  60. package/dist/createMenu.svelte.js +0 -1
  61. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  62. package/dist/editing.js +1 -1
  63. package/dist/editor-contract.d.ts +12 -5
  64. package/dist/editor-registry.d.ts +18 -7
  65. package/dist/editor-registry.js +10 -1
  66. package/dist/export-format.js +3 -0
  67. package/dist/group-display.d.ts +39 -0
  68. package/dist/group-display.js +52 -0
  69. package/dist/index.d.ts +2 -2
  70. package/dist/index.js +2 -2
  71. package/dist/keyboard-handlers.js +16 -0
  72. package/dist/recurrence.js +3 -2
  73. package/dist/selection.js +0 -1
  74. package/dist/svgrid-wrapper.types.d.ts +8 -2
  75. package/dist/themes/index.d.ts +10 -0
  76. package/dist/themes/index.js +26 -0
  77. package/package.json +3 -2
  78. package/src/DockNodeView.svelte +1 -1
  79. package/src/GridFooter.svelte +0 -40
  80. package/src/GridMenus.svelte +0 -77
  81. package/src/SvAccordion.test.ts +5 -5
  82. package/src/SvAvatar.svelte +1 -1
  83. package/src/SvButtonGroup.test.ts +6 -6
  84. package/src/SvCalendar.test.ts +1 -1
  85. package/src/SvCheckBox.svelte +2 -1
  86. package/src/SvCircularProgress.svelte +1 -1
  87. package/src/SvDateRangeInput.svelte +1 -1
  88. package/src/SvFileUpload.test.ts +1 -1
  89. package/src/SvGrid.controller.svelte.ts +367 -22
  90. package/src/SvGrid.css +103 -3
  91. package/src/SvGrid.svelte +164 -35
  92. package/src/SvGrid.types.ts +81 -1
  93. package/src/SvGridChart.svelte +2 -10
  94. package/src/SvGridDropdown.svelte +38 -6
  95. package/src/SvGridSelect.svelte +12 -5
  96. package/src/SvGroupCell.svelte +9 -2
  97. package/src/SvKbd.svelte +1 -1
  98. package/src/SvList.svelte +1 -1
  99. package/src/SvMenubar.svelte +1 -1
  100. package/src/SvMultiSelect.svelte +12 -4
  101. package/src/SvOtpInput.svelte +1 -1
  102. package/src/SvPasswordInput.svelte +1 -1
  103. package/src/SvProgress.svelte +1 -1
  104. package/src/SvRating.svelte +1 -1
  105. package/src/SvResult.svelte +1 -1
  106. package/src/SvRichText.svelte +35 -16
  107. package/src/SvSwitchButton.svelte +2 -1
  108. package/src/SvToaster.svelte +1 -1
  109. package/src/SvTour.svelte +4 -0
  110. package/src/SvTree.svelte +9 -2
  111. package/src/SvTreeSelect.svelte +6 -4
  112. package/src/ai.test.ts +1 -1
  113. package/src/build-api.ts +15 -105
  114. package/src/builtin-editors.test.ts +3 -1
  115. package/src/cell-formatting.test.ts +1 -1
  116. package/src/cell-render.test.ts +27 -3
  117. package/src/cell-render.ts +122 -95
  118. package/src/cell-values.ts +4 -0
  119. package/src/chart-export.test.ts +2 -2
  120. package/src/chart.ts +1 -2
  121. package/src/clipboard.test.ts +0 -2
  122. package/src/clipboard.ts +0 -102
  123. package/src/columns.ts +0 -115
  124. package/src/core.ts +157 -8
  125. package/src/createMenu.svelte.ts +0 -1
  126. package/src/editing.test.ts +1 -1
  127. package/src/editing.ts +2 -105
  128. package/src/editor-contract.ts +12 -5
  129. package/src/editor-registry.grid.test.ts +48 -2
  130. package/src/editor-registry.test.ts +3 -1
  131. package/src/editor-registry.ts +23 -7
  132. package/src/export-format.ts +3 -0
  133. package/src/facet-buckets.test.ts +33 -33
  134. package/src/features.ts +0 -90
  135. package/src/group-display.test.ts +91 -1
  136. package/src/group-display.ts +83 -0
  137. package/src/index.ts +9 -0
  138. package/src/keyboard-handlers.coverage.test.ts +1 -1
  139. package/src/keyboard-handlers.ts +17 -120
  140. package/src/list-power.test.ts +2 -2
  141. package/src/menus.test.ts +4 -5
  142. package/src/menus.ts +0 -112
  143. package/src/named-views.coverage.test.ts +3 -3
  144. package/src/new-features.test.ts +3 -1
  145. package/src/recurrence.ts +3 -2
  146. package/src/row-drag.test.ts +1 -1
  147. package/src/row-resize.test.ts +41 -42
  148. package/src/scheduler-model.ts +1 -1
  149. package/src/scroll-sync.test.ts +4 -7
  150. package/src/scroll-sync.ts +0 -122
  151. package/src/selection.ts +0 -109
  152. package/src/server-data-source.coverage.test.ts +1 -1
  153. package/src/server-group-model.test.ts +8 -8
  154. package/src/spreadsheet.test.ts +0 -1
  155. package/src/summaries.ts +0 -112
  156. package/src/svgrid-wrapper.types.ts +11 -9
  157. package/src/svgrid.async-editor-options.test.ts +271 -0
  158. package/src/svgrid.auto-row-height.test.ts +204 -0
  159. package/src/svgrid.behavior.test.ts +0 -1
  160. package/src/svgrid.charting.test.ts +4 -4
  161. package/src/svgrid.column-reorder.test.ts +0 -8
  162. package/src/svgrid.comments-autocomplete.test.ts +8 -2
  163. package/src/svgrid.context-menu.test.ts +25 -9
  164. package/src/svgrid.filter-depth.test.ts +163 -0
  165. package/src/svgrid.filter-menu-listbox.svelte.test.ts +16 -2
  166. package/src/svgrid.filter-menu-scroll.test.ts +10 -2
  167. package/src/svgrid.grand-total.test.ts +188 -0
  168. package/src/svgrid.group-display-mode.test.ts +171 -0
  169. package/src/svgrid.group-footers.test.ts +121 -0
  170. package/src/svgrid.group-pagination.test.ts +153 -0
  171. package/src/svgrid.tree-data.test.ts +186 -0
  172. package/src/svgriddropdown.async-panel.svelte.test.ts +195 -0
  173. package/src/themes/index.ts +42 -0
  174. package/src/tree-edit.test.ts +4 -4
  175. package/src/tree-row-model.test.ts +168 -0
  176. package/src/ui-localization.test.ts +113 -0
  177. package/themes/ag-alpine.css +22 -0
  178. package/themes/antd.css +22 -0
  179. package/themes/atlassian.css +22 -0
  180. package/themes/bootstrap.css +22 -0
  181. package/themes/carbon.css +22 -0
  182. package/themes/catppuccin.css +22 -0
  183. package/themes/dracula.css +22 -0
  184. package/themes/ember.css +22 -0
  185. package/themes/excel.css +22 -0
  186. package/themes/fluent.css +22 -0
  187. package/themes/github.css +22 -0
  188. package/themes/linear.css +22 -0
  189. package/themes/material.css +22 -0
  190. package/themes/nord.css +22 -0
  191. package/themes/notion.css +22 -0
  192. package/themes/salesforce.css +22 -0
  193. package/themes/sap.css +22 -0
  194. package/themes/shadcn.css +22 -0
  195. package/themes/tailwind.css +22 -0
  196. package/themes/vercel.css +22 -0
package/src/selection.ts CHANGED
@@ -2,125 +2,17 @@
2
2
  // reading/writing controller state via the `ctx` handle; the reactive core
3
3
  // ($state/$derived/$effect) stays in the controller.
4
4
  import {
5
- applyExcelFilter,
6
- normalizeForFilter,
7
- createColumnVirtualizer,
8
- createCoreRowModel,
9
- createExpandedRowModel,
10
- createFilteredRowModel,
11
- createGroupedRowModel,
12
- createPaginatedRowModel,
13
- createSvelteVirtualizer,
14
- createSortedRowModel,
15
- createSvGrid,
16
- filterFns,
17
- getGridCellA11yProps,
18
5
  getGridCellDomId,
19
- getGridHeaderA11yProps,
20
- getGridRootA11yProps,
21
- getGridRowA11yProps,
22
- parseEditorValue,
23
- normalizeEditorOptions,
24
- sortFns,
25
- tableFeatures,
26
- rowSortingFeature,
27
- columnFilteringFeature,
28
- columnGroupingFeature,
29
- type CellContext,
30
- type EditorContext,
31
- type CellEditorOption,
32
- type CellEditorType,
33
- type CellFormatter,
34
- type CellFormatConfig,
35
- type Column,
36
- type ColumnDef,
37
- type Row,
38
6
  type RowData,
39
- type SvGridApi,
40
7
  type TableFeatures,
41
8
  } from "./index";
42
9
  import "./sv-grid-scrollbar";
43
- import type { Snippet } from "svelte";
44
- import { getKeyboardIntent, getNextActiveCell } from "./keyboard";
45
- import {
46
- formatNumericWithConfig,
47
- getDateFormatter,
48
- resolveDatePattern,
49
- } from "./cell-formatting";
50
- import {
51
- RenderSnippetConfig,
52
- RenderComponentConfig,
53
- } from "./render-component";
54
- import { buildFillPattern } from "./fill-patterns";
55
- import { buildSparkline, toSparklineValues } from "./sparkline";
56
- import {
57
- resolveCellFormat,
58
- computeColumnStat,
59
- formatsNeedingStats,
60
- type ColumnStat,
61
- type ResolvedCellFormat,
62
- } from "./conditional-formatting";
63
- import SvGridDropdown from "./SvGridDropdown.svelte";
64
10
  import type {
65
- Props,
66
- SelectionPoint,
67
11
  SelectionRange,
68
- CellEditState,
69
- FilterOperator,
70
- FilterOption,
71
- MenuPosition,
72
12
  } from "./SvGrid.types";
73
- import {
74
- cfTextStyle,
75
- fmtStat,
76
- getCellKey,
77
- resolveClassList,
78
- toDateInputValue,
79
- toDateTimeLocalInputValue,
80
- getEditableInputValue,
81
- getEditorInputType,
82
- toValueArray,
83
- getOptionLabel,
84
- getOptionColor,
85
- colorfulChipStyle,
86
- getEditorClass,
87
- asDate,
88
- clampMenuX,
89
- cssEscape,
90
- rawToNumber,
91
- formatFacetNumber,
92
- formatFacetDate,
93
- } from "./SvGrid.helpers";
94
- import { createColumns } from "./columns";
95
- import { createGridApi } from "./build-api";
96
- import { createClipboard } from "./clipboard";
97
- import {
98
- filterOperatorOptions,
99
- fallbackOperatorOption,
100
- TEXT_OPERATORS,
101
- NUMBER_OPERATORS,
102
- DATE_OPERATORS,
103
- CHECKBOX_OPERATORS,
104
- operatorOption,
105
- operatorsForColumn,
106
- defaultOperatorFor,
107
- operatorLabelFor,
108
- } from "./filter-operators";
109
- import {
110
- type FacetBucket,
111
- isBucketableColumn,
112
- buildBuckets,
113
- isInBucket,
114
- } from "./facet-buckets";
115
13
  import {
116
14
  getColumnBaseValue,
117
15
  isGroupRow,
118
- toolPanelHeaderLabel,
119
- formatSummaryNumeric,
120
- getColumnAlign,
121
- getPinnedCellValue,
122
- getColumnAccessorValue,
123
- columnDefMatchesId,
124
16
  } from "./cell-values";
125
17
 
126
18
  export function createSelection<
@@ -244,7 +136,6 @@ export function createSelection<
244
136
  `td[data-svgrid-row="${rowIndex}"][data-svgrid-col="${colIndex}"]`,
245
137
  );
246
138
  if (cellEl) {
247
- const headerHeight = ctx.theadEl?.offsetHeight ?? 0;
248
139
  const containerRect = ctx.scrollContainer.getBoundingClientRect();
249
140
  const cellRect = cellEl.getBoundingClientRect();
250
141
  const cellTopInView = cellRect.top - containerRect.top;
@@ -22,7 +22,7 @@ describe('createServerDataSource - request params', () => {
22
22
  })
23
23
 
24
24
  it('setFilter resets to page 0', async () => {
25
- const getRows = vi.fn(async () => ({ rows: [], rowCount: 100 }))
25
+ const getRows = vi.fn(async (_req: ServerRequest) => ({ rows: [], rowCount: 100 }))
26
26
  const ctl = createServerDataSource<Row>({ getRows }, { pageSize: 10, onChange: () => {} })
27
27
  ctl.setPage(3)
28
28
  await flush()
@@ -23,16 +23,16 @@ function makeGroupSource(data: Row[], delay = 0): ServerDataSource<Row> & { call
23
23
  this.calls += 1
24
24
  if (delay) await new Promise((r) => setTimeout(r, delay))
25
25
  const subset = data.filter((r) =>
26
- req.groupKeys.every((k, i) => String((r as Record<string, unknown>)[req.groupBy[i]!]) === k),
26
+ req.groupKeys!.every((k, i) => String((r as Record<string, unknown>)[req.groupBy![i]!]) === k),
27
27
  )
28
- const level = req.groupKeys.length
29
- if (level < req.groupBy.length) {
30
- const field = req.groupBy[level]!
28
+ const level = req.groupKeys!.length
29
+ if (level < req.groupBy!.length) {
30
+ const field = req.groupBy![level]!
31
31
  const map = new Map<string, Record<string, unknown>>()
32
32
  for (const r of subset) {
33
33
  const key = String((r as Record<string, unknown>)[field])
34
34
  const agg = map.get(key) ?? { [field]: (r as Record<string, unknown>)[field] }
35
- for (const a of req.aggregations) {
35
+ for (const a of req.aggregations!) {
36
36
  const cur = (agg[a.col] as number) ?? 0
37
37
  agg[a.col] = a.fn === 'count' ? cur + 1 : cur + ((r as Record<string, unknown>)[a.col] as number)
38
38
  }
@@ -114,10 +114,10 @@ describe('createServerGroupModel', () => {
114
114
 
115
115
  it('collapse removes the children again', async () => {
116
116
  const src = makeGroupSource(DATA)
117
- let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
117
+ // Reads go through ctl.getState() below, so the change callback is a no-op.
118
118
  const ctl = createServerGroupModel<Row>(src, {
119
119
  groupBy: ['country', 'city'],
120
- onChange: (s) => (state = s as never),
120
+ onChange: () => {},
121
121
  })
122
122
  ctl.refresh()
123
123
  await flush()
@@ -241,7 +241,7 @@ function makeTreeSource(nodes: Node[]): ServerDataSource<Node> & { calls: number
241
241
  calls: 0,
242
242
  async getRows(req) {
243
243
  this.calls += 1
244
- const parent = req.groupKeys.length ? req.groupKeys[req.groupKeys.length - 1] : null
244
+ const parent = req.groupKeys!.length ? req.groupKeys![req.groupKeys!.length - 1] : null
245
245
  const children = nodes.filter((n) => (n.parentId ?? null) === (parent ?? null))
246
246
  return { rows: children, rowCount: children.length }
247
247
  },
@@ -9,7 +9,6 @@ if (typeof globalThis.CSS === 'undefined') {
9
9
  // @ts-expect-error - minimal shim for the test environment
10
10
  globalThis.CSS = { escape: (s: string) => s.replace(/["\\]/g, '\\$&') }
11
11
  } else if (typeof globalThis.CSS.escape !== 'function') {
12
- // @ts-expect-error - augment partial CSS object
13
12
  globalThis.CSS.escape = (s: string) => s.replace(/["\\]/g, '\\$&')
14
13
  }
15
14
 
package/src/summaries.ts CHANGED
@@ -2,129 +2,17 @@
2
2
  // reading/writing controller state via the `ctx` handle; the reactive core
3
3
  // ($state/$derived/$effect) stays in the controller.
4
4
  import {
5
- applyExcelFilter,
6
- normalizeForFilter,
7
- createColumnVirtualizer,
8
- createCoreRowModel,
9
- createExpandedRowModel,
10
- createFilteredRowModel,
11
- createGroupedRowModel,
12
- createPaginatedRowModel,
13
- createSvelteVirtualizer,
14
- createSortedRowModel,
15
- createSvGrid,
16
- filterFns,
17
- getGridCellA11yProps,
18
- getGridCellDomId,
19
- getGridHeaderA11yProps,
20
- getGridRootA11yProps,
21
- getGridRowA11yProps,
22
- parseEditorValue,
23
- normalizeEditorOptions,
24
- sortFns,
25
- tableFeatures,
26
- rowSortingFeature,
27
- columnFilteringFeature,
28
- columnGroupingFeature,
29
- type CellContext,
30
- type EditorContext,
31
- type CellEditorOption,
32
- type CellEditorType,
33
- type CellFormatter,
34
- type CellFormatConfig,
35
5
  type Column,
36
- type ColumnDef,
37
6
  type Row,
38
7
  type RowData,
39
- type SvGridApi,
40
8
  type TableFeatures,
41
9
  } from "./index";
42
10
  import "./sv-grid-scrollbar";
43
- import type { Snippet } from "svelte";
44
- import { getKeyboardIntent, getNextActiveCell } from "./keyboard";
45
11
  import {
46
- formatNumericWithConfig,
47
- getDateFormatter,
48
- resolveDatePattern,
49
- } from "./cell-formatting";
50
- import {
51
- RenderSnippetConfig,
52
- RenderComponentConfig,
53
- } from "./render-component";
54
- import { buildFillPattern } from "./fill-patterns";
55
- import { buildSparkline, toSparklineValues } from "./sparkline";
56
- import {
57
- resolveCellFormat,
58
- computeColumnStat,
59
- formatsNeedingStats,
60
- type ColumnStat,
61
- type ResolvedCellFormat,
62
- } from "./conditional-formatting";
63
- import SvGridDropdown from "./SvGridDropdown.svelte";
64
- import type {
65
- Props,
66
- SelectionPoint,
67
- SelectionRange,
68
- CellEditState,
69
- FilterOperator,
70
- FilterOption,
71
- MenuPosition,
72
- } from "./SvGrid.types";
73
- import {
74
- cfTextStyle,
75
- fmtStat,
76
12
  getCellKey,
77
- resolveClassList,
78
- toDateInputValue,
79
- toDateTimeLocalInputValue,
80
- getEditableInputValue,
81
- getEditorInputType,
82
- toValueArray,
83
- getOptionLabel,
84
- getOptionColor,
85
- colorfulChipStyle,
86
- getEditorClass,
87
- asDate,
88
- clampMenuX,
89
- cssEscape,
90
- rawToNumber,
91
- formatFacetNumber,
92
- formatFacetDate,
93
13
  } from "./SvGrid.helpers";
94
- import { createMenus } from "./menus";
95
- import { createCellRender } from "./cell-render";
96
- import { createEditing } from "./editing";
97
- import { createSelection } from "./selection";
98
- import { createColumns } from "./columns";
99
- import { createGridApi } from "./build-api";
100
- import { createClipboard } from "./clipboard";
101
- import {
102
- filterOperatorOptions,
103
- fallbackOperatorOption,
104
- TEXT_OPERATORS,
105
- NUMBER_OPERATORS,
106
- DATE_OPERATORS,
107
- CHECKBOX_OPERATORS,
108
- operatorOption,
109
- operatorsForColumn,
110
- defaultOperatorFor,
111
- operatorLabelFor,
112
- } from "./filter-operators";
113
- import {
114
- type FacetBucket,
115
- isBucketableColumn,
116
- buildBuckets,
117
- isInBucket,
118
- } from "./facet-buckets";
119
14
  import {
120
- getColumnBaseValue,
121
- isGroupRow,
122
- toolPanelHeaderLabel,
123
15
  formatSummaryNumeric,
124
- getColumnAlign,
125
- getPinnedCellValue,
126
- getColumnAccessorValue,
127
- columnDefMatchesId,
128
16
  } from "./cell-values";
129
17
 
130
18
  export function createSummaries<
@@ -1,18 +1,14 @@
1
1
  import type { CellFormatConfig, ColumnDef, RowData, SvGridOptions, TableFeatures } from './core'
2
2
  // Type-only import (the reverse of SvGrid.types importing SvGridApi); TS resolves the
3
3
  // type-level cycle. Backs the generic key/value typing of `setOption` / `getOption`.
4
- import type { Props } from './SvGrid.types'
4
+ import type { FilterOperator, Props } from './SvGrid.types'
5
5
  import type { GridExportOptions, GridClipboardOptions } from './export-format'
6
6
  import type { ChartSpec, ChartType } from './chart'
7
7
 
8
- export type SvGridFilterOperator =
9
- | 'contains'
10
- | 'equals'
11
- | 'startsWith'
12
- | 'greaterThan'
13
- | 'lessThan'
14
- | 'between'
15
- | 'isBlank'
8
+ // Aliased to the core union rather than restated: the API surfaces below hand
9
+ // back whatever the grid actually filtered with, so a hand-maintained subset
10
+ // here silently mistypes operators like 'endsWith' that reach callers at runtime.
11
+ export type SvGridFilterOperator = FilterOperator
16
12
 
17
13
  /**
18
14
  * A serializable snapshot of everything that makes up the current "view":
@@ -247,6 +243,12 @@ export type SvGridApi<
247
243
  */
248
244
  setFacetFilter(columnId: string, values: ReadonlyArray<string> | null): void
249
245
  clearFilter(columnId: string): void
246
+ /**
247
+ * Drop cached async `editorOptions` so the next render refetches them.
248
+ * Omit `columnId` to clear every column. Only affects columns whose
249
+ * `editorOptions` returns a Promise - static lists are never refetched.
250
+ */
251
+ refreshEditorOptions(columnId?: string): void
250
252
  /**
251
253
  * Clear every active column filter (menu, filter-row, set-list, and global).
252
254
  * Resets the grid to "no filtering" in a single call.
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Async `editorOptions` (#18): an option list that arrives from the server.
3
+ *
4
+ * Render paths call the option resolver synchronously and often, so an async
5
+ * source cannot block. The first call starts the fetch and returns an empty
6
+ * list; the resolved value lands in reactive state and re-renders the open
7
+ * editor. In-flight keys are tracked so repeated renders do not refire the
8
+ * request, and so the dropdown can say "Loading…" rather than "No options",
9
+ * which would read as "nothing to pick".
10
+ */
11
+ import { afterEach, describe, expect, it, vi } from 'vitest'
12
+ import { mount, unmount } from 'svelte'
13
+ import SvGrid from './SvGrid.svelte'
14
+ import {
15
+ createCoreRowModel,
16
+ createFilteredRowModel,
17
+ createPaginatedRowModel,
18
+ createSortedRowModel,
19
+ sortFns,
20
+ tableFeatures,
21
+ rowSortingFeature,
22
+ } from './index'
23
+
24
+ type Row = { id: number; country: string; city: string }
25
+
26
+ const data: Row[] = [
27
+ { id: 1, country: 'FR', city: 'Paris' },
28
+ { id: 2, country: 'JP', city: 'Osaka' },
29
+ ]
30
+
31
+ const tick = () => new Promise<void>((r) => queueMicrotask(r))
32
+ // The list editors lazy-load `SvGridDropdown` with a dynamic import, so
33
+ // microtasks alone never get it mounted - the settle has to yield the macrotask
34
+ // queue too, and then again for the resolved options to re-render.
35
+ const settle = async () => {
36
+ for (let i = 0; i < 4; i += 1) {
37
+ await new Promise((r) => setTimeout(r, 0))
38
+ await tick()
39
+ }
40
+ }
41
+
42
+ function mountGrid(columns: unknown[]) {
43
+ const target = document.createElement('div')
44
+ document.body.appendChild(target)
45
+ let api: never = null as never
46
+ const app = mount(SvGrid, {
47
+ target,
48
+ props: {
49
+ data,
50
+ columns,
51
+ features: tableFeatures({ rowSortingFeature }),
52
+ editable: true,
53
+ onApiReady: (a: never) => { api = a },
54
+ _rowModels: {
55
+ coreRowModel: createCoreRowModel(),
56
+ filteredRowModel: createFilteredRowModel(),
57
+ sortedRowModel: createSortedRowModel(sortFns),
58
+ paginatedRowModel: createPaginatedRowModel(),
59
+ },
60
+ containerHeight: 400,
61
+ virtualization: false,
62
+ } as never,
63
+ })
64
+ return { target, getApi: () => api, destroy: () => { unmount(app); target.remove() } }
65
+ }
66
+
67
+ let cleanup: (() => void) | null = null
68
+ afterEach(() => { cleanup?.(); cleanup = null })
69
+
70
+ /** Open the editor for row 0 of `columnId` through the public api. */
71
+ async function openEditor(api: never, columnId: string) {
72
+ ;(api as never as { startEditing: (r: number, c: string) => boolean }).startEditing(0, columnId)
73
+ await settle()
74
+ }
75
+
76
+ describe('async editorOptions (#18)', () => {
77
+ it('resolves a promised option list and renders it', async () => {
78
+ const columns = [
79
+ { field: 'country', header: 'Country', width: 120 },
80
+ {
81
+ field: 'city',
82
+ header: 'City',
83
+ width: 160,
84
+ editorType: 'select',
85
+ editorOptions: Promise.resolve(['Paris', 'Lyon', 'Osaka']),
86
+ },
87
+ ]
88
+ const g = mountGrid(columns)
89
+ cleanup = g.destroy
90
+ await settle()
91
+ await openEditor(g.getApi(), 'city')
92
+ expect(document.body.textContent).toContain('Lyon')
93
+ })
94
+
95
+ it('fetches once no matter how many times render asks', async () => {
96
+ const source = vi.fn(async () => ['a', 'b'])
97
+ const columns = [
98
+ { field: 'country', header: 'Country', width: 120 },
99
+ {
100
+ field: 'city',
101
+ header: 'City',
102
+ width: 160,
103
+ editorType: 'select',
104
+ editorOptions: source(),
105
+ },
106
+ ]
107
+ const g = mountGrid(columns)
108
+ cleanup = g.destroy
109
+ await settle()
110
+ await openEditor(g.getApi(), 'city')
111
+ await settle()
112
+ // The promise was created once; the resolver must not re-await per render.
113
+ expect(source).toHaveBeenCalledTimes(1)
114
+ })
115
+
116
+ it('calls a per-row async source ONCE per row, not once per render', async () => {
117
+ // The resolver runs inside the template, so calling an async source on
118
+ // every render fires a real request per render - and any state the source
119
+ // touches gets mutated mid-render, which Svelte rejects outright. Reported
120
+ // from the demo as `state_unsafe_mutation`.
121
+ const calls: number[] = []
122
+ const columns = [
123
+ { field: 'country', header: 'Country', width: 120 },
124
+ {
125
+ field: 'city',
126
+ header: 'City',
127
+ width: 160,
128
+ editorType: 'select',
129
+ editorOptions: (row: Row) => {
130
+ calls.push(row.id)
131
+ return Promise.resolve(['Paris', 'Lyon'])
132
+ },
133
+ },
134
+ ]
135
+ const g = mountGrid(columns)
136
+ cleanup = g.destroy
137
+ await settle()
138
+ await openEditor(g.getApi(), 'city')
139
+ await settle()
140
+ const afterOpen = calls.length
141
+ expect(afterOpen).toBeGreaterThan(0)
142
+ // Force real re-render passes while the editor is open. Each one used to
143
+ // re-invoke the source and start another request, which in a browser
144
+ // becomes an endless fetch loop as every resolution re-renders.
145
+ const api = g.getApi() as never as {
146
+ setOption: (k: string, v: unknown) => void
147
+ }
148
+ for (const zebra of [true, false, true]) {
149
+ api.setOption('zebraRows', zebra)
150
+ await settle()
151
+ }
152
+ expect(calls.length).toBe(afterOpen)
153
+ })
154
+
155
+ it('refetches a cascade when the row it depends on changes', async () => {
156
+ // A cascade is a function of the row's OTHER cells. Keying the cache by row
157
+ // id alone made the first result permanent - changing Country left the old
158
+ // city list in place forever.
159
+ const asked: string[] = []
160
+ const columns = [
161
+ { field: 'country', header: 'Country', width: 120 },
162
+ {
163
+ field: 'city',
164
+ header: 'City',
165
+ width: 160,
166
+ editorType: 'select',
167
+ editorOptions: (row: Row) => {
168
+ asked.push(row.country)
169
+ return Promise.resolve(row.country === 'FR' ? ['Paris', 'Lyon'] : ['Osaka', 'Kyoto'])
170
+ },
171
+ },
172
+ ]
173
+ const g = mountGrid(columns)
174
+ cleanup = g.destroy
175
+ await settle()
176
+ const api = g.getApi() as never as {
177
+ startEditing: (r: number, c: string) => boolean
178
+ stopEditing: (cancel?: boolean) => boolean
179
+ setCellValue: (r: number, c: string, v: unknown) => void
180
+ }
181
+
182
+ await openEditor(api as never, 'city')
183
+ expect(document.body.textContent).toContain('Lyon')
184
+ api.stopEditing(true)
185
+ await settle()
186
+
187
+ // Row 0 moves from FR to JP.
188
+ api.setCellValue(0, 'country', 'JP')
189
+ await settle()
190
+
191
+ await openEditor(api as never, 'city')
192
+ expect(asked).toContain('JP')
193
+ expect(document.body.textContent).toContain('Kyoto')
194
+ expect(document.body.textContent).not.toContain('Lyon')
195
+ })
196
+
197
+ it('resolves a per-row (cascading) async source', async () => {
198
+ const columns = [
199
+ { field: 'country', header: 'Country', width: 120 },
200
+ {
201
+ field: 'city',
202
+ header: 'City',
203
+ width: 160,
204
+ editorType: 'select',
205
+ editorOptions: (row: Row) =>
206
+ Promise.resolve(row.country === 'FR' ? ['Paris', 'Lyon'] : ['Osaka', 'Kyoto']),
207
+ },
208
+ ]
209
+ const g = mountGrid(columns)
210
+ cleanup = g.destroy
211
+ await settle()
212
+ await openEditor(g.getApi(), 'city')
213
+ // Row 1 is FR - it must not be offered the JP list.
214
+ expect(document.body.textContent).toContain('Lyon')
215
+ expect(document.body.textContent).not.toContain('Kyoto')
216
+ })
217
+
218
+ it('falls back to an empty list when the source rejects', async () => {
219
+ const rejected = Promise.reject(new Error('offline'))
220
+ rejected.catch(() => {}) // keep the reporter clean; the grid attaches its own handler
221
+ const columns = [
222
+ { field: 'country', header: 'Country', width: 120 },
223
+ {
224
+ field: 'city',
225
+ header: 'City',
226
+ width: 160,
227
+ editorType: 'select',
228
+ editorOptions: rejected,
229
+ },
230
+ ]
231
+ const g = mountGrid(columns)
232
+ cleanup = g.destroy
233
+ await settle()
234
+ await openEditor(g.getApi(), 'city')
235
+ await settle()
236
+ // The editor must settle on "No options", never spin forever.
237
+ expect(document.body.textContent).toContain('No options')
238
+ })
239
+
240
+ it('exposes refreshEditorOptions on the api', async () => {
241
+ const columns = [
242
+ { field: 'country', header: 'Country', width: 120 },
243
+ { field: 'city', header: 'City', width: 160 },
244
+ ]
245
+ const g = mountGrid(columns)
246
+ cleanup = g.destroy
247
+ await settle()
248
+ const api = g.getApi() as never as { refreshEditorOptions: (c?: string) => void }
249
+ expect(typeof api.refreshEditorOptions).toBe('function')
250
+ expect(() => api.refreshEditorOptions('city')).not.toThrow()
251
+ expect(() => api.refreshEditorOptions()).not.toThrow()
252
+ })
253
+
254
+ it('leaves a plain array source synchronous', async () => {
255
+ const columns = [
256
+ { field: 'country', header: 'Country', width: 120 },
257
+ {
258
+ field: 'city',
259
+ header: 'City',
260
+ width: 160,
261
+ editorType: 'select',
262
+ editorOptions: ['Paris', 'Lyon'],
263
+ },
264
+ ]
265
+ const g = mountGrid(columns)
266
+ cleanup = g.destroy
267
+ await settle()
268
+ await openEditor(g.getApi(), 'city')
269
+ expect(document.body.textContent).toContain('Lyon')
270
+ })
271
+ })