@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/core.ts CHANGED
@@ -45,6 +45,11 @@ export type CellSpanParams<TData extends RowData = RowData> = {
45
45
  value: unknown
46
46
  }
47
47
 
48
+ /** The raw option list a column's `editorOptions` can supply. */
49
+ export type EditorOptionSource = ReadonlyArray<
50
+ string | number | { value: string | number; label?: string; color?: string }
51
+ >
52
+
48
53
  /** Params passed to a column's `valueParser(...)` on edit commit. */
49
54
  export type ValueParserParams<TData extends RowData = RowData> = {
50
55
  /** The value after built-in per-`editorType` coercion. */
@@ -172,6 +177,16 @@ export function applyGroupAggregate<TData extends RowData>(
172
177
  }
173
178
  }
174
179
 
180
+ /**
181
+ * A column definition.
182
+ *
183
+ * `TFeatures` is a phantom parameter - it is threaded through nested
184
+ * `columns` groups but no member depends on it, so `{}`, `TableFeatures` and
185
+ * `typeof features` are all interchangeable here. It is deliberately left
186
+ * WITHOUT a default: `ColumnDef<Row>` would otherwise bind `Row` to this slot
187
+ * and silently type your data as `RowData`, losing every field-name check.
188
+ * Prefer {@link GridColumns} / {@link GridColumnDef} for the common case.
189
+ */
175
190
  export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> = {
176
191
  id?: string
177
192
  field?: keyof TData & string
@@ -336,16 +351,20 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
336
351
  *
337
352
  * Pass a function `(row) => options` for row-dependent (cascading)
338
353
  * options - e.g. City options that depend on Country in the same row.
354
+ *
355
+ * Either form may return a **Promise**, for options that come from the
356
+ * server. While it resolves, the editor shows a loading state and the cell
357
+ * renders its raw value.
358
+ *
359
+ * Results are cached so reopening an editor does not refetch: a static source
360
+ * per column, a per-row source per row AND per that row's data - so a cascade
361
+ * reloads by itself when the cell it depends on is edited. Call
362
+ * `api.refreshEditorOptions(columnId?)` when the list changes server-side.
339
363
  */
340
364
  editorOptions?:
341
- | ReadonlyArray<
342
- string | number | { value: string | number; label?: string; color?: string }
343
- >
344
- | ((
345
- row: TData,
346
- ) => ReadonlyArray<
347
- string | number | { value: string | number; label?: string; color?: string }
348
- >)
365
+ | EditorOptionSource
366
+ | Promise<EditorOptionSource>
367
+ | ((row: TData) => EditorOptionSource | Promise<EditorOptionSource>)
349
368
  /** When true, list/chips allow multiple selections. Cell value becomes an array. */
350
369
  editorMultiple?: boolean
351
370
  /** Separator used when joining array values for the readonly cell display. Defaults to ', '. */
@@ -409,6 +428,23 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
409
428
  | ((ctx: CellContext<TData>) => string | ReadonlyArray<string> | Record<string, boolean> | undefined | null)
410
429
  }
411
430
 
431
+ /**
432
+ * A column definition keyed only by your row type - the ergonomic form of
433
+ * {@link ColumnDef}, whose first parameter is a phantom feature bag that is
434
+ * almost always `{}`.
435
+ *
436
+ * ```ts
437
+ * const columns: GridColumns<Person> = [{ field: 'firstName', header: 'Name' }]
438
+ * ```
439
+ *
440
+ * Interchangeable with `ColumnDef<{}, TData>` and `ColumnDef<typeof features,
441
+ * TData>` in both directions, so it mixes freely with existing code.
442
+ */
443
+ export type GridColumnDef<TData extends RowData = RowData> = ColumnDef<TableFeatures, TData>
444
+
445
+ /** An array of {@link GridColumnDef} - what you pass to `<SvGrid columns={...}>`. */
446
+ export type GridColumns<TData extends RowData = RowData> = Array<GridColumnDef<TData>>
447
+
412
448
  export type Column<TData extends RowData> = {
413
449
  id: string
414
450
  columnDef: ColumnDef<any, TData>
@@ -647,6 +683,119 @@ export function createGroupedRowModel<TData extends RowData>(): RowModelFactory<
647
683
  return buildGroups(rows, 0, 0, 'group')
648
684
  }
649
685
  }
686
+ export type TreeRowModelOptions = {
687
+ /** Field holding each row's parent id. Rows with no parent are roots. */
688
+ parentField: string
689
+ /** Field holding the row's own id. Defaults to `'id'`. */
690
+ idField?: string
691
+ }
692
+
693
+ /**
694
+ * Client-side tree data: nest the grid's own flat rows into a parent/child
695
+ * hierarchy that `createExpandedRowModel` then walks.
696
+ *
697
+ * This works on the rows the grid already built rather than on raw data, so
698
+ * tree rows keep their cells, editing, selection and formatting - they are real
699
+ * data rows that happen to have children, not synthetic banners like grouping's.
700
+ * That is also why the model is parent-id based: nested source arrays never
701
+ * become rows (the grid only builds rows for `data`), so nested input is
702
+ * flattened first with {@link flattenTreeData}. One code path, no duplicated
703
+ * row construction.
704
+ *
705
+ * Rows are tagged `__treeRow` so `isGroupRow` does not mistake an expandable
706
+ * data row for a full-width group banner.
707
+ */
708
+ export function createTreeRowModel<TData extends RowData>(
709
+ options: TreeRowModelOptions,
710
+ ): RowModelFactory<TData> {
711
+ const { parentField, idField = 'id' } = options
712
+ return ({ table, rows }) => {
713
+ if (!rows.length) return rows
714
+ const keyOf = (row: Row<TData>) => (row.original as any)?.[idField]
715
+ const parentOf = (row: Row<TData>) => (row.original as any)?.[parentField]
716
+
717
+ const present = new Set<unknown>()
718
+ for (const row of rows) present.add(keyOf(row))
719
+
720
+ const childrenByParent = new Map<unknown, Array<Row<TData>>>()
721
+ const roots: Array<Row<TData>> = []
722
+ for (const row of rows) {
723
+ const parent = parentOf(row)
724
+ // A row whose parent is absent (filtered out, or never existed) becomes a
725
+ // root rather than disappearing - silently dropping rows is worse than a
726
+ // shallower tree. Self-parenting is treated the same way.
727
+ if (parent == null || parent === keyOf(row) || !present.has(parent)) {
728
+ roots.push(row)
729
+ continue
730
+ }
731
+ const list = childrenByParent.get(parent) ?? []
732
+ list.push(row)
733
+ childrenByParent.set(parent, list)
734
+ }
735
+
736
+ // Guards a cycle in the parent chain from recursing forever.
737
+ const seen = new Set<unknown>()
738
+ const build = (row: Row<TData>, depth: number): Row<TData> => {
739
+ const key = keyOf(row)
740
+ const id = row.id
741
+ if (seen.has(key)) {
742
+ return { ...row, depth, subRows: [], getCanExpand: () => false } as Row<TData>
743
+ }
744
+ seen.add(key)
745
+ const subRows = (childrenByParent.get(key) ?? []).map((child) => build(child, depth + 1))
746
+ return {
747
+ ...row,
748
+ depth,
749
+ subRows,
750
+ leafCount: subRows.reduce((n, sub) => n + 1 + (sub.leafCount ?? 0), 0),
751
+ __treeRow: true,
752
+ getCanExpand: () => subRows.length > 0,
753
+ getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),
754
+ toggleExpanded: () => {
755
+ table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
756
+ },
757
+ } as Row<TData>
758
+ }
759
+
760
+ return roots.map((root) => build(root, 0))
761
+ }
762
+ }
763
+
764
+ export type FlattenTreeOptions = {
765
+ /** Field holding an array of child objects. */
766
+ childrenField: string
767
+ /** Field holding each object's id. Defaults to `'id'`. */
768
+ idField?: string
769
+ /** Field to WRITE the resolved parent id onto. Defaults to `'__parentId'`. */
770
+ parentField?: string
771
+ }
772
+
773
+ /**
774
+ * Flatten nested tree data into the flat parent-id shape `createTreeRowModel`
775
+ * consumes, stamping each child with its parent's id.
776
+ *
777
+ * Children are emitted directly after their parent so the natural order already
778
+ * matches the rendered tree. The `childrenField` array is left on the objects
779
+ * (harmless, and callers often still want it); only the parent link is added.
780
+ */
781
+ export function flattenTreeData<T extends RowData>(
782
+ data: ReadonlyArray<T>,
783
+ options: FlattenTreeOptions,
784
+ ): T[] {
785
+ const { childrenField, idField = 'id', parentField = '__parentId' } = options
786
+ const out: T[] = []
787
+ const walk = (nodes: ReadonlyArray<T>, parentId: unknown) => {
788
+ for (const node of nodes) {
789
+ const flat = { ...node, [parentField]: parentId } as T
790
+ out.push(flat)
791
+ const kids = (node as any)[childrenField]
792
+ if (Array.isArray(kids) && kids.length) walk(kids as ReadonlyArray<T>, (node as any)[idField])
793
+ }
794
+ }
795
+ walk(data, null)
796
+ return out
797
+ }
798
+
650
799
  export function createExpandedRowModel<TData extends RowData>(): RowModelFactory<TData> {
651
800
  return ({ table, rows }) => {
652
801
  const expanded: ExpandedState = table.getState().expanded ?? {}
@@ -66,7 +66,6 @@ export type MenuItemProps = {
66
66
  onclick: () => void
67
67
  }
68
68
 
69
- let uid = 0
70
69
 
71
70
  export function createMenu(config: MenuConfig) {
72
71
  const items = () => config.items()
@@ -154,7 +154,7 @@ describe('isCellEditable', () => {
154
154
  data: [{ a: 'hello' }],
155
155
  })
156
156
  ed.isCellEditable(ctx.allColumns[0], ctx.allRows[0])
157
- const cc = spy.mock.calls[0][0]
157
+ const cc = spy.mock.calls[0]![0]
158
158
  expect(cc.getValue()).toBe('hello')
159
159
  expect(cc.cell.getValue()).toBe('hello')
160
160
  expect(cc.cell.getContext()).toBe(cc)
package/src/editing.ts CHANGED
@@ -2,126 +2,23 @@
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
5
  parseEditorValue,
23
- normalizeEditorOptions,
24
- sortFns,
25
- tableFeatures,
26
- rowSortingFeature,
27
- columnFilteringFeature,
28
- columnGroupingFeature,
29
6
  type CellContext,
30
- type EditorContext,
31
- type CellEditorOption,
32
7
  type CellEditorType,
33
- type CellFormatter,
34
- type CellFormatConfig,
35
8
  type Column,
36
- type ColumnDef,
37
9
  type Row,
38
10
  type RowData,
39
- type SvGridApi,
40
11
  type TableFeatures,
41
12
  } from "./index";
42
13
  import "./sv-grid-scrollbar";
43
- import type { Snippet } from "svelte";
44
- import { getKeyboardIntent, getNextActiveCell } from "./keyboard";
14
+ import { getNextActiveCell } from "./keyboard";
45
15
  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
16
  getCellKey,
77
- resolveClassList,
78
- toDateInputValue,
79
- toDateTimeLocalInputValue,
80
- getEditableInputValue,
81
- getEditorInputType,
82
17
  toValueArray,
83
- getOptionLabel,
84
- getOptionColor,
85
- colorfulChipStyle,
86
- getEditorClass,
87
- asDate,
88
- clampMenuX,
89
- cssEscape,
90
- rawToNumber,
91
- formatFacetNumber,
92
- formatFacetDate,
93
18
  } from "./SvGrid.helpers";
94
- import { createSelection } from "./selection";
95
- import { createColumns } from "./columns";
96
- import { createGridApi } from "./build-api";
97
- import { createClipboard } from "./clipboard";
98
- import {
99
- filterOperatorOptions,
100
- fallbackOperatorOption,
101
- TEXT_OPERATORS,
102
- NUMBER_OPERATORS,
103
- DATE_OPERATORS,
104
- CHECKBOX_OPERATORS,
105
- operatorOption,
106
- operatorsForColumn,
107
- defaultOperatorFor,
108
- operatorLabelFor,
109
- } from "./filter-operators";
110
- import {
111
- type FacetBucket,
112
- isBucketableColumn,
113
- buildBuckets,
114
- isInBucket,
115
- } from "./facet-buckets";
116
19
  import {
117
20
  getColumnBaseValue,
118
21
  isGroupRow,
119
- toolPanelHeaderLabel,
120
- formatSummaryNumeric,
121
- getColumnAlign,
122
- getPinnedCellValue,
123
- getColumnAccessorValue,
124
- columnDefMatchesId,
125
22
  } from "./cell-values";
126
23
 
127
24
  type HistoryStep = {
@@ -224,7 +121,7 @@ export function createEditing<
224
121
  ) {
225
122
  return false;
226
123
  }
227
- if (editorType === "number" && !/[0-9.+\-]/.test(char)) return false;
124
+ if (editorType === "number" && !/[0-9.+-]/.test(char)) return false;
228
125
  ctx.editorSelectAll = false;
229
126
  ctx.editingCell = {
230
127
  rowId: row.id,
@@ -68,16 +68,23 @@ export type SvEditorProps = {
68
68
  * The interaction surface an editor exposes when embedded as a grid cell editor.
69
69
  * Standalone usage ignores these; the grid supplies them so every editor shares
70
70
  * one commit / cancel / move contract (Enter commits, Escape cancels, Tab moves)
71
- * instead of each cell-editor re-implementing key handling. Consumed in Phase 2
72
- * (the editor registry).
71
+ * instead of each cell-editor re-implementing key handling.
72
+ *
73
+ * `CellEditorContext` (editor-registry.ts) extends this type, so what the grid
74
+ * hands a registered editor and what this contract promises are the same thing
75
+ * by construction rather than by convention.
73
76
  */
74
77
  export type EditorInteraction<V = unknown> = {
75
- /** Commit the current value and stop editing (Enter, option pick, blur-commit). */
76
- onCommit?: (value: V) => void
78
+ /**
79
+ * Commit the value and stop editing (Enter, option pick, blur-commit).
80
+ * The argument is optional: an editor that has been reporting through
81
+ * `onChange` can just call `onCommit()` to commit what the grid already has.
82
+ */
83
+ onCommit?: (value?: V) => void
77
84
  /** Abandon the edit and restore the previous value (Escape). */
78
85
  onCancel?: () => void
79
86
  /** Commit, then move to the adjacent cell (Tab = 1, Shift+Tab = -1). */
80
- onCommitAndMove?: (value: V, direction: 1 | -1) => void
87
+ onCommitAndMove?: (value: V | undefined, direction: 1 | -1) => void
81
88
  /** Ask the surrounding popover/panel to close without committing. */
82
89
  onRequestClose?: () => void
83
90
  /** True while mounted inside a grid cell (lets an editor tune autofocus etc.). */
@@ -73,7 +73,6 @@ afterEach(() => {
73
73
 
74
74
  describe('registry wiring in SvGrid', () => {
75
75
  it('mounts a registered component when a custom-editorType cell edits', async () => {
76
- let committed: unknown = undefined
77
76
  registerCellEditor('stars', {
78
77
  component: SvRating,
79
78
  props: (ctx) => ({
@@ -90,7 +89,54 @@ describe('registry wiring in SvGrid', () => {
90
89
  // The registered SvRating editor is mounted in the editing overlay.
91
90
  const rating = target.querySelector('.sv-rating')
92
91
  expect(rating).not.toBeNull()
93
- void committed
92
+ } finally {
93
+ destroy()
94
+ }
95
+ })
96
+
97
+ it('hands the editor the full EditorInteraction contract, not a subset', async () => {
98
+ // The contract type used to be documented but never supplied: the registry
99
+ // passed only value/onChange/onCommit/onCancel, so an editor written against
100
+ // `EditorInteraction` (Tab-to-move, dismiss-without-commit, in-cell tuning)
101
+ // silently got undefined for half of it.
102
+ let seen: Record<string, unknown> = {}
103
+ registerCellEditor('stars', {
104
+ component: SvRating,
105
+ props: (ctx) => {
106
+ seen = ctx as unknown as Record<string, unknown>
107
+ return { value: ctx.value }
108
+ },
109
+ })
110
+
111
+ const { api, target, destroy } = await mountGrid()
112
+ try {
113
+ api.startEditing(0, 'score')
114
+ await tick()
115
+ await tick()
116
+ expect(typeof seen.onCommit).toBe('function')
117
+ expect(typeof seen.onCancel).toBe('function')
118
+ expect(typeof seen.onChange).toBe('function')
119
+ expect(typeof seen.onCommitAndMove).toBe('function')
120
+ expect(typeof seen.onRequestClose).toBe('function')
121
+ expect(seen.inCell).toBe(true)
122
+ expect(seen.rowId).toBeTruthy()
123
+ expect(seen.columnId).toBe('score')
124
+ void target
125
+ } finally {
126
+ destroy()
127
+ }
128
+ })
129
+
130
+ it('defaults to passing the whole contract when a registration has no props mapping', async () => {
131
+ // A registration with no `props` should still work for a contract-aware
132
+ // editor - that is the point of the default mapping.
133
+ registerCellEditor('stars', SvRating)
134
+ const { api, target, destroy } = await mountGrid()
135
+ try {
136
+ api.startEditing(0, 'score')
137
+ await tick()
138
+ await tick()
139
+ expect(target.querySelector('.sv-rating')).not.toBeNull()
94
140
  } finally {
95
141
  destroy()
96
142
  }
@@ -15,6 +15,8 @@ import {
15
15
  const Fake = (() => {}) as unknown as Component<any>
16
16
  const Fake2 = (() => {}) as unknown as Component<any>
17
17
 
18
+ // onCommitAndMove / onRequestClose / inCell are deliberately left off: the
19
+ // tests below assert that defaultEditorProps maps them to undefined.
18
20
  function ctx(overrides: Partial<CellEditorContext> = {}): CellEditorContext {
19
21
  return {
20
22
  value: 42,
@@ -24,7 +26,7 @@ function ctx(overrides: Partial<CellEditorContext> = {}): CellEditorContext {
24
26
  onCommit: () => {},
25
27
  onCancel: () => {},
26
28
  ...overrides,
27
- }
29
+ } as CellEditorContext
28
30
  }
29
31
 
30
32
  afterEach(() => {
@@ -20,9 +20,20 @@
20
20
  * mounting.
21
21
  */
22
22
  import type { Component } from 'svelte'
23
+ import type { EditorInteraction } from './editor-contract'
23
24
 
24
- /** Context the grid hands a cell editor when it mounts one for an edit. */
25
- export type CellEditorContext<T = unknown> = {
25
+ /**
26
+ * Context the grid hands a cell editor when it mounts one for an edit.
27
+ *
28
+ * Extends {@link EditorInteraction} - the shared commit / cancel / move contract
29
+ * the `Sv*` editors are written against - and narrows the parts the grid always
30
+ * supplies, so a registered component and a built-in editor speak the same
31
+ * language. `onCommitAndMove`, `onRequestClose` and `inCell` come from the
32
+ * contract and are always populated by the grid.
33
+ */
34
+ export type CellEditorContext<T = unknown> = Required<
35
+ Pick<EditorInteraction<T>, 'onCommit' | 'onCancel' | 'onCommitAndMove' | 'onRequestClose' | 'inCell'>
36
+ > & {
26
37
  /** The value currently being edited. */
27
38
  value: T
28
39
  /** Id of the row being edited. */
@@ -31,10 +42,6 @@ export type CellEditorContext<T = unknown> = {
31
42
  columnId: string
32
43
  /** Update the in-progress value WITHOUT ending the edit. */
33
44
  onChange: (value: T) => void
34
- /** Commit the value and stop editing (optionally pass a final value). */
35
- onCommit: (value?: T) => void
36
- /** Discard changes and stop editing (returns focus to the grid). */
37
- onCancel: () => void
38
45
  }
39
46
 
40
47
  /** How a registered component is mounted and wired for a cell edit. */
@@ -87,13 +94,22 @@ export function registeredCellEditorTypes(): string[] {
87
94
  return [...registry.keys()]
88
95
  }
89
96
 
90
- /** The default context → props mapping used when a registration has no `props`. */
97
+ /**
98
+ * The default context → props mapping used when a registration has no `props`.
99
+ * Passes the whole {@link EditorInteraction} surface, so an editor written
100
+ * against the shared contract works when registered with no mapping at all.
101
+ * Svelte ignores props a component doesn't declare, so the extra keys are inert
102
+ * for a simple editor that only wants `value` + `onCommit`.
103
+ */
91
104
  export function defaultEditorProps(ctx: CellEditorContext): Record<string, unknown> {
92
105
  return {
93
106
  value: ctx.value,
94
107
  onChange: ctx.onChange,
95
108
  onCommit: ctx.onCommit,
96
109
  onCancel: ctx.onCancel,
110
+ onCommitAndMove: ctx.onCommitAndMove,
111
+ onRequestClose: ctx.onRequestClose,
112
+ inCell: ctx.inCell,
97
113
  }
98
114
  }
99
115
 
@@ -521,6 +521,9 @@ const XML_ESCAPE: Record<string, string> = {
521
521
  }
522
522
  function xmlText(value: unknown): string {
523
523
  const s = value == null ? '' : value instanceof Date ? value.toISOString() : String(value)
524
+ // Stripping control characters is the point: XML 1.0 forbids them outright,
525
+ // so a stray \x00 from a data source would produce an unopenable file.
526
+ // eslint-disable-next-line no-control-regex
524
527
  return s.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '').replace(/[&<>"']/g, (c) => XML_ESCAPE[c]!)
525
528
  }
526
529
  /** Coerce a field name into a valid XML element name. */