@svgrid/grid 1.1.0 → 1.1.2

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 (148) hide show
  1. package/dist/FlexRender.svelte +96 -96
  2. package/dist/GridMenus.svelte +205 -127
  3. package/dist/SvGrid.controller.svelte.d.ts +44 -1
  4. package/dist/SvGrid.controller.svelte.js +240 -44
  5. package/dist/SvGrid.css +325 -22
  6. package/dist/SvGrid.helpers.test.d.ts +1 -0
  7. package/dist/SvGrid.helpers.test.js +298 -0
  8. package/dist/SvGrid.svelte +729 -203
  9. package/dist/SvGrid.types.d.ts +91 -4
  10. package/dist/aligned-grids.d.ts +6 -0
  11. package/dist/aligned-grids.js +84 -0
  12. package/dist/aligned-grids.test.d.ts +1 -0
  13. package/dist/aligned-grids.test.js +75 -0
  14. package/dist/build-api.coverage.test.d.ts +20 -0
  15. package/dist/build-api.coverage.test.js +505 -0
  16. package/dist/build-api.js +61 -31
  17. package/dist/cell-render.test.d.ts +1 -0
  18. package/dist/cell-render.test.js +338 -0
  19. package/dist/cell-values.d.ts +1 -1
  20. package/dist/cell-values.js +7 -7
  21. package/dist/chart-export.test.d.ts +1 -0
  22. package/dist/chart-export.test.js +302 -0
  23. package/dist/chart.coverage.test.d.ts +1 -0
  24. package/dist/chart.coverage.test.js +748 -0
  25. package/dist/clipboard.js +88 -24
  26. package/dist/clipboard.test.d.ts +1 -0
  27. package/dist/clipboard.test.js +700 -0
  28. package/dist/collaboration.coverage.test.d.ts +1 -0
  29. package/dist/collaboration.coverage.test.js +200 -0
  30. package/dist/column-groups.d.ts +19 -0
  31. package/dist/column-groups.js +62 -0
  32. package/dist/column-groups.test.d.ts +1 -0
  33. package/dist/column-groups.test.js +56 -0
  34. package/dist/column-types.d.ts +10 -0
  35. package/dist/column-types.js +63 -0
  36. package/dist/column-types.test.d.ts +1 -0
  37. package/dist/column-types.test.js +62 -0
  38. package/dist/columns.test.d.ts +1 -0
  39. package/dist/columns.test.js +625 -0
  40. package/dist/core.d.ts +86 -1
  41. package/dist/core.js +2 -2
  42. package/dist/editing.d.ts +7 -0
  43. package/dist/editing.js +191 -5
  44. package/dist/editing.test.d.ts +1 -0
  45. package/dist/editing.test.js +732 -0
  46. package/dist/editors/cell-editors.coverage.test.d.ts +1 -0
  47. package/dist/editors/cell-editors.coverage.test.js +139 -0
  48. package/dist/facet-buckets.test.d.ts +1 -0
  49. package/dist/facet-buckets.test.js +296 -0
  50. package/dist/filter-operators.test.d.ts +1 -0
  51. package/dist/filter-operators.test.js +135 -0
  52. package/dist/hyperformula-adapter.test.d.ts +1 -0
  53. package/dist/hyperformula-adapter.test.js +205 -0
  54. package/dist/index.d.ts +5 -2
  55. package/dist/index.js +4 -1
  56. package/dist/keyboard-handlers.coverage.test.d.ts +1 -0
  57. package/dist/keyboard-handlers.coverage.test.js +495 -0
  58. package/dist/keyboard-handlers.js +7 -2
  59. package/dist/menus.js +1 -0
  60. package/dist/menus.test.d.ts +1 -0
  61. package/dist/menus.test.js +560 -0
  62. package/dist/named-views.coverage.test.d.ts +1 -0
  63. package/dist/named-views.coverage.test.js +180 -0
  64. package/dist/row-drag.d.ts +49 -0
  65. package/dist/row-drag.js +221 -0
  66. package/dist/row-drag.test.d.ts +1 -0
  67. package/dist/row-drag.test.js +142 -0
  68. package/dist/row-resize.test.d.ts +1 -0
  69. package/dist/row-resize.test.js +329 -0
  70. package/dist/scroll-sync.js +3 -0
  71. package/dist/scroll-sync.test.d.ts +1 -0
  72. package/dist/scroll-sync.test.js +290 -0
  73. package/dist/selection.d.ts +7 -1
  74. package/dist/selection.js +76 -36
  75. package/dist/selection.multi-range.test.d.ts +1 -0
  76. package/dist/selection.multi-range.test.js +55 -0
  77. package/dist/selection.test.d.ts +1 -0
  78. package/dist/selection.test.js +647 -0
  79. package/dist/server-data-source.coverage.test.d.ts +1 -0
  80. package/dist/server-data-source.coverage.test.js +154 -0
  81. package/dist/spreadsheet.d.ts +30 -0
  82. package/dist/spreadsheet.js +48 -0
  83. package/dist/spreadsheet.test.d.ts +1 -0
  84. package/dist/spreadsheet.test.js +446 -0
  85. package/dist/summaries.js +4 -4
  86. package/dist/sv-grid-scrollbar.js +13 -1
  87. package/dist/svgrid-wrapper.types.d.ts +19 -0
  88. package/dist/svgrid.behavior.test.js +20 -0
  89. package/dist/svgrid.interaction.test.js +31 -0
  90. package/dist/svgrid.new-features.wrapper.test.js +34 -2
  91. package/dist/test-setup.js +9 -3
  92. package/dist/virtualization/scroll-scaling.d.ts +17 -0
  93. package/dist/virtualization/scroll-scaling.js +35 -0
  94. package/dist/virtualization/scroll-scaling.test.js +42 -1
  95. package/package.json +2 -1
  96. package/src/FlexRender.svelte +96 -96
  97. package/src/GridMenus.svelte +205 -127
  98. package/src/SvGrid.controller.svelte.ts +204 -36
  99. package/src/SvGrid.css +277 -12
  100. package/src/SvGrid.svelte +727 -205
  101. package/src/SvGrid.types.ts +85 -4
  102. package/src/a11y.contract.test.ts +49 -49
  103. package/src/a11y.test.ts +59 -59
  104. package/src/a11y.ts +59 -59
  105. package/src/aligned-grids.test.ts +80 -0
  106. package/src/aligned-grids.ts +87 -0
  107. package/src/build-api.ts +43 -23
  108. package/src/cell-formatting.ts +169 -169
  109. package/src/cell-values.ts +4 -4
  110. package/src/clipboard.test.ts +49 -0
  111. package/src/clipboard.ts +51 -23
  112. package/src/column-groups.test.ts +59 -0
  113. package/src/column-groups.ts +80 -0
  114. package/src/column-types.test.ts +68 -0
  115. package/src/column-types.ts +82 -0
  116. package/src/core.performance.test.ts +30 -30
  117. package/src/core.ts +1077 -999
  118. package/src/createGrid.svelte.ts +42 -42
  119. package/src/createGrid.test.ts +10 -10
  120. package/src/createGridState.svelte.ts +17 -17
  121. package/src/editing.ts +161 -5
  122. package/src/flex-render.ts +3 -3
  123. package/src/index.ts +208 -196
  124. package/src/keyboard.test.ts +59 -59
  125. package/src/keyboard.ts +97 -97
  126. package/src/menus.ts +1 -0
  127. package/src/merge-objects.ts +48 -48
  128. package/src/render-component.ts +28 -28
  129. package/src/row-drag.test.ts +168 -0
  130. package/src/row-drag.ts +255 -0
  131. package/src/scroll-sync.ts +2 -0
  132. package/src/selection.multi-range.test.ts +61 -0
  133. package/src/selection.ts +71 -37
  134. package/src/spreadsheet.test.ts +46 -2
  135. package/src/spreadsheet.ts +58 -0
  136. package/src/static-functions.ts +11 -11
  137. package/src/subscribe.ts +38 -38
  138. package/src/summaries.ts +4 -4
  139. package/src/svgrid-wrapper.types.ts +412 -393
  140. package/src/svgrid.features.test.ts +157 -157
  141. package/src/svgrid.new-features.wrapper.test.ts +2 -2
  142. package/src/svgrid.wrapper.test.ts +40 -40
  143. package/src/virtualization/column-virtualizer.test.ts +27 -27
  144. package/src/virtualization/column-virtualizer.ts +30 -30
  145. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  146. package/src/virtualization/types.ts +30 -30
  147. package/src/virtualization/virtualizer.test.ts +47 -47
  148. package/src/virtualization/virtualizer.ts +296 -296
package/src/build-api.ts CHANGED
@@ -133,8 +133,8 @@ export function createGridApi<
133
133
  const row = ctx.internalData[rowIndex];
134
134
  const column = findColumn(columnId);
135
135
  if (!row || !column) return undefined;
136
- if (column.columnDef.accessorFn)
137
- return column.columnDef.accessorFn(row);
136
+ if (column.columnDef.fieldFn)
137
+ return column.columnDef.fieldFn(row);
138
138
  if (column.columnDef.field)
139
139
  return (row as Record<string, unknown>)[column.columnDef.field];
140
140
  return undefined;
@@ -147,40 +147,49 @@ export function createGridApi<
147
147
  next[rowIndex] = { ...row, [column.columnDef.field]: value } as TData;
148
148
  ctx.internalData = next;
149
149
  },
150
+ startEditing(rowIndex, columnId) {
151
+ return ctx.startEditing(rowIndex, columnId);
152
+ },
153
+ stopEditing(cancel) {
154
+ return ctx.stopEditing(cancel ?? false);
155
+ },
150
156
  selectCells(ranges) {
151
- // Engine supports a single anchor/focus rectangle today. Honour
152
- // the first entry; ignore the rest. Empty array clears.
157
+ // Multiple rectangles are supported. Empty array clears everything.
153
158
  if (!ranges || ranges.length === 0) {
154
159
  ctx.selectionRange = { anchor: null, focus: null };
160
+ ctx.selectionRanges = [];
155
161
  return;
156
162
  }
157
- const [r1, c1, r2, c2] = ranges[0]!;
158
163
  const rowCount = ctx.internalData.length;
159
164
  const colCount = ctx.allColumns.length;
160
165
  if (rowCount === 0 || colCount === 0) return;
161
166
  // Clamp to the visible grid bounds so callers can pass open-ended
162
167
  // values like `[0, 0, Infinity, Infinity]` to mean "select all".
163
- const aRow = Math.max(0, Math.min(rowCount - 1, Math.min(r1, r2)));
164
- const fRow = Math.max(0, Math.min(rowCount - 1, Math.max(r1, r2)));
165
- const aCol = Math.max(0, Math.min(colCount - 1, Math.min(c1, c2)));
166
- const fCol = Math.max(0, Math.min(colCount - 1, Math.max(c1, c2)));
167
- ctx.selectionRange = {
168
- anchor: { rowIndex: aRow, colIndex: aCol },
169
- focus: { rowIndex: fRow, colIndex: fCol },
170
- };
171
- // Move the active cell to the range's top-left so keyboard
168
+ const toRange = ([r1, c1, r2, c2]: readonly [number, number, number, number]) => ({
169
+ anchor: {
170
+ rowIndex: Math.max(0, Math.min(rowCount - 1, Math.min(r1, r2))),
171
+ colIndex: Math.max(0, Math.min(colCount - 1, Math.min(c1, c2))),
172
+ },
173
+ focus: {
174
+ rowIndex: Math.max(0, Math.min(rowCount - 1, Math.max(r1, r2))),
175
+ colIndex: Math.max(0, Math.min(colCount - 1, Math.max(c1, c2))),
176
+ },
177
+ });
178
+ const built = ranges.map(toRange);
179
+ // Last range is the active one; the rest are committed extras.
180
+ ctx.selectionRanges = built.slice(0, -1);
181
+ const active = built[built.length - 1]!;
182
+ ctx.selectionRange = active;
183
+ // Move the active cell to the last range's top-left so keyboard
172
184
  // navigation continues from there.
173
- ctx.setActiveCell(aRow, aCol);
185
+ ctx.setActiveCell(active.anchor.rowIndex, active.anchor.colIndex);
174
186
  },
175
187
  getSelected() {
176
- const a = ctx.selectionRange.anchor;
177
- const f = ctx.selectionRange.focus;
178
- if (!a || !f) return [];
179
- const r1 = Math.min(a.rowIndex, f.rowIndex);
180
- const r2 = Math.max(a.rowIndex, f.rowIndex);
181
- const c1 = Math.min(a.colIndex, f.colIndex);
182
- const c2 = Math.max(a.colIndex, f.colIndex);
183
- return [[r1, c1, r2, c2]];
188
+ return ctx
189
+ .getSelectionRects()
190
+ .map((r: { minRow: number; minCol: number; maxRow: number; maxCol: number }) =>
191
+ [r.minRow, r.minCol, r.maxRow, r.maxCol] as [number, number, number, number],
192
+ );
184
193
  },
185
194
  addRow(row, position = "bottom") {
186
195
  this.addRows([row], position);
@@ -313,6 +322,17 @@ export function createGridApi<
313
322
  ...(filter.operator === "between"
314
323
  ? { valueTo: filter.valueTo ?? "" }
315
324
  : {}),
325
+ // Optional second condition + join (multi-condition filtering).
326
+ ...(filter.operator2
327
+ ? {
328
+ operator2: filter.operator2,
329
+ value2: filter.value2 ?? "",
330
+ join: filter.join ?? "AND",
331
+ ...(filter.operator2 === "between"
332
+ ? { valueTo2: filter.valueTo2 ?? "" }
333
+ : {}),
334
+ }
335
+ : {}),
316
336
  },
317
337
  };
318
338
  },
@@ -1,169 +1,169 @@
1
- /**
2
- * Helpers for SvGrid column `format` presets (numbers, currency, percent, date patterns).
3
- */
4
-
5
- /** Map shortcut date patterns to Intl options (merged with caller `options`). */
6
- export function resolveDatePattern(
7
- pattern: string | undefined,
8
- kind: 'date' | 'datetime',
9
- ): Intl.DateTimeFormatOptions | undefined {
10
- if (!pattern) return undefined
11
-
12
- const p = pattern.trim()
13
-
14
- switch (p) {
15
- /** Short numeric local date (no weekday). */
16
- case 'd':
17
- return kind === 'datetime'
18
- ? {
19
- year: 'numeric',
20
- month: 'numeric',
21
- day: 'numeric',
22
- hour: 'numeric',
23
- minute: 'numeric',
24
- }
25
- : { year: 'numeric', month: 'numeric', day: 'numeric' }
26
- /** Long spelled-out date (+ time if datetime). */
27
- case 'D':
28
- return kind === 'datetime'
29
- ? {
30
- weekday: 'long',
31
- year: 'numeric',
32
- month: 'long',
33
- day: 'numeric',
34
- hour: 'numeric',
35
- minute: 'numeric',
36
- }
37
- : { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
38
- /** ISO-friendly yyyy-mm-dd (with time when datetime). */
39
- case 'y-m-d':
40
- return kind === 'datetime'
41
- ? {
42
- year: 'numeric',
43
- month: '2-digit',
44
- day: '2-digit',
45
- hour: '2-digit',
46
- minute: '2-digit',
47
- hour12: false,
48
- }
49
- : { year: 'numeric', month: '2-digit', day: '2-digit' }
50
- case 'medium':
51
- return { dateStyle: 'medium', timeStyle: kind === 'datetime' ? 'short' : undefined }
52
- case 'short':
53
- return { dateStyle: 'short', timeStyle: kind === 'datetime' ? 'short' : undefined }
54
- case 'long':
55
- return { dateStyle: 'long', timeStyle: kind === 'datetime' ? 'short' : undefined }
56
- default:
57
- return undefined
58
- }
59
- }
60
-
61
- type NumericFormatInput = {
62
- type: 'number' | 'currency' | 'percent'
63
- locales?: string | string[]
64
- currency?: string
65
- /** When type is percent: treat value as 0–100 instead of Intl’s 0–1 fraction */
66
- valueIsPercentPoints?: boolean
67
- options?: Intl.NumberFormatOptions
68
- }
69
-
70
- /**
71
- * `Intl.NumberFormat` is one of the most expensive constructors in the
72
- * platform - building one per cell on every scroll makes large grids
73
- * noticeably stutter. Cache instances by config signature (locale + style
74
- * + currency + options).
75
- */
76
- const numberFormatterCache = new Map<string, Intl.NumberFormat>()
77
- function getNumberFormatter(
78
- locales: string | string[] | undefined,
79
- options: Intl.NumberFormatOptions,
80
- ): Intl.NumberFormat {
81
- // Stable key - for normal column configs the options object is reused
82
- // (declared in module scope), so this stringifies a small static shape.
83
- const key =
84
- (Array.isArray(locales) ? locales.join(',') : locales ?? '') +
85
- '|' +
86
- (options.style ?? '') +
87
- '|' +
88
- (options.currency ?? '') +
89
- '|' +
90
- (options.minimumFractionDigits ?? '') +
91
- '|' +
92
- (options.maximumFractionDigits ?? '') +
93
- '|' +
94
- (options.useGrouping ?? '') +
95
- '|' +
96
- (options.currencyDisplay ?? '') +
97
- '|' +
98
- (options.notation ?? '')
99
- let fmt = numberFormatterCache.get(key)
100
- if (!fmt) {
101
- fmt = new Intl.NumberFormat(locales, options)
102
- numberFormatterCache.set(key, fmt)
103
- }
104
- return fmt
105
- }
106
-
107
- /** Cache of `Intl.DateTimeFormat` by `(locale, options)` signature. */
108
- const dateFormatterCache = new Map<string, Intl.DateTimeFormat>()
109
- export function getDateFormatter(
110
- locales: string | string[] | undefined,
111
- options: Intl.DateTimeFormatOptions,
112
- ): Intl.DateTimeFormat {
113
- const key =
114
- (Array.isArray(locales) ? locales.join(',') : locales ?? '') +
115
- '|' +
116
- (options.dateStyle ?? '') +
117
- '|' +
118
- (options.timeStyle ?? '') +
119
- '|' +
120
- (options.year ?? '') +
121
- '|' +
122
- (options.month ?? '') +
123
- '|' +
124
- (options.day ?? '') +
125
- '|' +
126
- (options.hour ?? '') +
127
- '|' +
128
- (options.minute ?? '') +
129
- '|' +
130
- (options.second ?? '') +
131
- '|' +
132
- (options.weekday ?? '') +
133
- '|' +
134
- (options.hour12 ?? '')
135
- let fmt = dateFormatterCache.get(key)
136
- if (!fmt) {
137
- fmt = new Intl.DateTimeFormat(locales, options)
138
- dateFormatterCache.set(key, fmt)
139
- }
140
- return fmt
141
- }
142
-
143
- /** Format a numeric value for number / currency / percent column formats. */
144
- export function formatNumericWithConfig(value: unknown, config: NumericFormatInput): string {
145
- const n = typeof value === 'number' ? value : Number(value)
146
- if (!Number.isFinite(n)) return String(value ?? '')
147
-
148
- const locales = config.locales
149
-
150
- if (config.type === 'currency') {
151
- return getNumberFormatter(locales, {
152
- style: 'currency',
153
- currency: config.currency ?? 'USD',
154
- ...config.options,
155
- }).format(n)
156
- }
157
-
158
- if (config.type === 'percent') {
159
- const frac = config.valueIsPercentPoints === true ? n / 100 : n
160
- return getNumberFormatter(locales, {
161
- style: 'percent',
162
- minimumFractionDigits: 0,
163
- maximumFractionDigits: 2,
164
- ...config.options,
165
- }).format(frac)
166
- }
167
-
168
- return getNumberFormatter(locales, config.options ?? {}).format(n)
169
- }
1
+ /**
2
+ * Helpers for SvGrid column `format` presets (numbers, currency, percent, date patterns).
3
+ */
4
+
5
+ /** Map shortcut date patterns to Intl options (merged with caller `options`). */
6
+ export function resolveDatePattern(
7
+ pattern: string | undefined,
8
+ kind: 'date' | 'datetime',
9
+ ): Intl.DateTimeFormatOptions | undefined {
10
+ if (!pattern) return undefined
11
+
12
+ const p = pattern.trim()
13
+
14
+ switch (p) {
15
+ /** Short numeric local date (no weekday). */
16
+ case 'd':
17
+ return kind === 'datetime'
18
+ ? {
19
+ year: 'numeric',
20
+ month: 'numeric',
21
+ day: 'numeric',
22
+ hour: 'numeric',
23
+ minute: 'numeric',
24
+ }
25
+ : { year: 'numeric', month: 'numeric', day: 'numeric' }
26
+ /** Long spelled-out date (+ time if datetime). */
27
+ case 'D':
28
+ return kind === 'datetime'
29
+ ? {
30
+ weekday: 'long',
31
+ year: 'numeric',
32
+ month: 'long',
33
+ day: 'numeric',
34
+ hour: 'numeric',
35
+ minute: 'numeric',
36
+ }
37
+ : { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
38
+ /** ISO-friendly yyyy-mm-dd (with time when datetime). */
39
+ case 'y-m-d':
40
+ return kind === 'datetime'
41
+ ? {
42
+ year: 'numeric',
43
+ month: '2-digit',
44
+ day: '2-digit',
45
+ hour: '2-digit',
46
+ minute: '2-digit',
47
+ hour12: false,
48
+ }
49
+ : { year: 'numeric', month: '2-digit', day: '2-digit' }
50
+ case 'medium':
51
+ return { dateStyle: 'medium', timeStyle: kind === 'datetime' ? 'short' : undefined }
52
+ case 'short':
53
+ return { dateStyle: 'short', timeStyle: kind === 'datetime' ? 'short' : undefined }
54
+ case 'long':
55
+ return { dateStyle: 'long', timeStyle: kind === 'datetime' ? 'short' : undefined }
56
+ default:
57
+ return undefined
58
+ }
59
+ }
60
+
61
+ type NumericFormatInput = {
62
+ type: 'number' | 'currency' | 'percent'
63
+ locales?: string | string[]
64
+ currency?: string
65
+ /** When type is percent: treat value as 0–100 instead of Intl’s 0–1 fraction */
66
+ valueIsPercentPoints?: boolean
67
+ options?: Intl.NumberFormatOptions
68
+ }
69
+
70
+ /**
71
+ * `Intl.NumberFormat` is one of the most expensive constructors in the
72
+ * platform - building one per cell on every scroll makes large grids
73
+ * noticeably stutter. Cache instances by config signature (locale + style
74
+ * + currency + options).
75
+ */
76
+ const numberFormatterCache = new Map<string, Intl.NumberFormat>()
77
+ function getNumberFormatter(
78
+ locales: string | string[] | undefined,
79
+ options: Intl.NumberFormatOptions,
80
+ ): Intl.NumberFormat {
81
+ // Stable key - for normal column configs the options object is reused
82
+ // (declared in module scope), so this stringifies a small static shape.
83
+ const key =
84
+ (Array.isArray(locales) ? locales.join(',') : locales ?? '') +
85
+ '|' +
86
+ (options.style ?? '') +
87
+ '|' +
88
+ (options.currency ?? '') +
89
+ '|' +
90
+ (options.minimumFractionDigits ?? '') +
91
+ '|' +
92
+ (options.maximumFractionDigits ?? '') +
93
+ '|' +
94
+ (options.useGrouping ?? '') +
95
+ '|' +
96
+ (options.currencyDisplay ?? '') +
97
+ '|' +
98
+ (options.notation ?? '')
99
+ let fmt = numberFormatterCache.get(key)
100
+ if (!fmt) {
101
+ fmt = new Intl.NumberFormat(locales, options)
102
+ numberFormatterCache.set(key, fmt)
103
+ }
104
+ return fmt
105
+ }
106
+
107
+ /** Cache of `Intl.DateTimeFormat` by `(locale, options)` signature. */
108
+ const dateFormatterCache = new Map<string, Intl.DateTimeFormat>()
109
+ export function getDateFormatter(
110
+ locales: string | string[] | undefined,
111
+ options: Intl.DateTimeFormatOptions,
112
+ ): Intl.DateTimeFormat {
113
+ const key =
114
+ (Array.isArray(locales) ? locales.join(',') : locales ?? '') +
115
+ '|' +
116
+ (options.dateStyle ?? '') +
117
+ '|' +
118
+ (options.timeStyle ?? '') +
119
+ '|' +
120
+ (options.year ?? '') +
121
+ '|' +
122
+ (options.month ?? '') +
123
+ '|' +
124
+ (options.day ?? '') +
125
+ '|' +
126
+ (options.hour ?? '') +
127
+ '|' +
128
+ (options.minute ?? '') +
129
+ '|' +
130
+ (options.second ?? '') +
131
+ '|' +
132
+ (options.weekday ?? '') +
133
+ '|' +
134
+ (options.hour12 ?? '')
135
+ let fmt = dateFormatterCache.get(key)
136
+ if (!fmt) {
137
+ fmt = new Intl.DateTimeFormat(locales, options)
138
+ dateFormatterCache.set(key, fmt)
139
+ }
140
+ return fmt
141
+ }
142
+
143
+ /** Format a numeric value for number / currency / percent column formats. */
144
+ export function formatNumericWithConfig(value: unknown, config: NumericFormatInput): string {
145
+ const n = typeof value === 'number' ? value : Number(value)
146
+ if (!Number.isFinite(n)) return String(value ?? '')
147
+
148
+ const locales = config.locales
149
+
150
+ if (config.type === 'currency') {
151
+ return getNumberFormatter(locales, {
152
+ style: 'currency',
153
+ currency: config.currency ?? 'USD',
154
+ ...config.options,
155
+ }).format(n)
156
+ }
157
+
158
+ if (config.type === 'percent') {
159
+ const frac = config.valueIsPercentPoints === true ? n / 100 : n
160
+ return getNumberFormatter(locales, {
161
+ style: 'percent',
162
+ minimumFractionDigits: 0,
163
+ maximumFractionDigits: 2,
164
+ ...config.options,
165
+ }).format(frac)
166
+ }
167
+
168
+ return getNumberFormatter(locales, config.options ?? {}).format(n)
169
+ }
@@ -13,7 +13,7 @@ import { formatNumericWithConfig } from "./cell-formatting";
13
13
 
14
14
  export function getColumnBaseValue<TData extends RowData>(row: Row<TData>, column: Column<TData>) {
15
15
  const def = column.columnDef;
16
- if (def.accessorFn) return def.accessorFn(row.original);
16
+ if (def.fieldFn) return def.fieldFn(row.original);
17
17
  if (def.field) return row.original[def.field];
18
18
  return row.getCellValueByColumnId(column.id);
19
19
  }
@@ -85,7 +85,7 @@ export function getColumnAlign<TData extends RowData>(column: Column<TData>): "l
85
85
  /**
86
86
  * Read a cell value from a PINNED row. Pinned rows aren't bound to a
87
87
  * TanStack Row<TData>, so we resolve the value directly off the raw
88
- * TData via `accessorFn` or `field`. Mirrors `getColumnBaseValue`'s
88
+ * TData via `fieldFn` or `field`. Mirrors `getColumnBaseValue`'s
89
89
  * lookup path minus the Row indirection.
90
90
  */
91
91
  export function getPinnedCellValue<TData extends RowData>(
@@ -93,7 +93,7 @@ export function getPinnedCellValue<TData extends RowData>(
93
93
  column: Column<TData>,
94
94
  ): unknown {
95
95
  const def = column.columnDef;
96
- if (def.accessorFn) return def.accessorFn(rowData);
96
+ if (def.fieldFn) return def.fieldFn(rowData);
97
97
  const field = def.field as string | undefined;
98
98
  if (!field) return undefined;
99
99
  return (rowData as unknown as Record<string, unknown>)[field];
@@ -101,7 +101,7 @@ export function getPinnedCellValue<TData extends RowData>(
101
101
 
102
102
  export function getColumnAccessorValue<TData extends RowData>(rowData: TData, column: Column<TData>) {
103
103
  const def = column.columnDef;
104
- if (def.accessorFn) return def.accessorFn(rowData);
104
+ if (def.fieldFn) return def.fieldFn(rowData);
105
105
  if (def.field) return rowData[def.field];
106
106
  return undefined;
107
107
  }
@@ -63,6 +63,24 @@ function makeCtx(opts: FakeOptions = {}) {
63
63
  },
64
64
  _rows: buildRows(),
65
65
  selectionRange: { anchor: null, focus: null },
66
+ selectionRanges: [] as Array<{ anchor: any; focus: any }>,
67
+ // Mirrors the real controller's getSelectionRects: committed ranges + active.
68
+ getSelectionRects(): Array<{ minRow: number; maxRow: number; minCol: number; maxCol: number }> {
69
+ const rects: Array<{ minRow: number; maxRow: number; minCol: number; maxCol: number }> = []
70
+ const norm = (r: { anchor: any; focus: any }) =>
71
+ r?.anchor && r?.focus
72
+ ? {
73
+ minRow: Math.min(r.anchor.rowIndex, r.focus.rowIndex),
74
+ maxRow: Math.max(r.anchor.rowIndex, r.focus.rowIndex),
75
+ minCol: Math.min(r.anchor.colIndex, r.focus.colIndex),
76
+ maxCol: Math.max(r.anchor.colIndex, r.focus.colIndex),
77
+ }
78
+ : null
79
+ for (const r of ctx.selectionRanges) { const n = norm(r); if (n) rects.push(n) }
80
+ const active = norm(ctx.selectionRange)
81
+ if (active) rects.push(active)
82
+ return rects
83
+ },
66
84
  fillDrag: null,
67
85
  editedCellValues: {},
68
86
  userHasActivatedCell: false,
@@ -528,6 +546,37 @@ describe('copySelectionToClipboard', () => {
528
546
  expect(writeText).toHaveBeenCalledWith('a0\tb0\na1\tb1')
529
547
  })
530
548
 
549
+ it('prepends a header row when copyHeadersToClipboard is set', () => {
550
+ const ctx = makeCtx({
551
+ data: [
552
+ { a: 'a0', b: 'b0' },
553
+ { a: 'a1', b: 'b1' },
554
+ ],
555
+ })
556
+ ctx.props.copyHeadersToClipboard = true
557
+ ctx.selectionRange = {
558
+ anchor: { rowIndex: 0, colIndex: 0 },
559
+ focus: { rowIndex: 1, colIndex: 1 },
560
+ }
561
+ const cb = createClipboard(ctx)
562
+ cb.copySelectionToClipboard()
563
+ expect(writeText).toHaveBeenCalledWith('a\tb\na0\tb0\na1\tb1')
564
+ })
565
+
566
+ it('runs each value through processCellForClipboard', () => {
567
+ const ctx = makeCtx({
568
+ data: [{ a: 'a0', b: 'b0' }],
569
+ })
570
+ ctx.props.processCellForClipboard = ({ value }: { value: unknown }) => `[${value}]`
571
+ ctx.selectionRange = {
572
+ anchor: { rowIndex: 0, colIndex: 0 },
573
+ focus: { rowIndex: 0, colIndex: 1 },
574
+ }
575
+ const cb = createClipboard(ctx)
576
+ cb.copySelectionToClipboard()
577
+ expect(writeText).toHaveBeenCalledWith('[a0]\t[b0]')
578
+ })
579
+
531
580
  it('emits empty strings for missing columns and nullish values', () => {
532
581
  const ctx = makeCtx({
533
582
  columns: [{ id: 'a', field: 'a' }],
package/src/clipboard.ts CHANGED
@@ -415,32 +415,60 @@ export function createClipboard<
415
415
  }
416
416
 
417
417
  function copySelectionToClipboard() {
418
- const anchor = ctx.selectionRange.anchor;
419
- const focus = ctx.selectionRange.focus;
420
- if (!anchor || !focus) return;
421
- const minRow = Math.min(anchor.rowIndex, focus.rowIndex);
422
- const maxRow = Math.max(anchor.rowIndex, focus.rowIndex);
423
- const minCol = Math.min(anchor.colIndex, focus.colIndex);
424
- const maxCol = Math.max(anchor.colIndex, focus.colIndex);
425
- const lines: Array<string> = [];
426
- for (let r = minRow; r <= maxRow; r += 1) {
427
- const row = ctx.allRows[r];
428
- if (!row || isGroupRow(row)) continue;
429
- const cells: Array<string> = [];
430
- for (let c = minCol; c <= maxCol; c += 1) {
431
- const column = ctx.allColumns[c];
432
- if (!column) {
433
- cells.push("");
434
- continue;
418
+ // Copy every selected rectangle. Each range becomes a TSV block; multiple
419
+ // ranges are stacked and separated by a blank line (Sheets-style).
420
+ const rects = ctx.getSelectionRects() as Array<{
421
+ minRow: number;
422
+ maxRow: number;
423
+ minCol: number;
424
+ maxCol: number;
425
+ }>;
426
+ if (!rects.length) return;
427
+ // Optional: prepend a header row (Excel "copy with headers") and/or run
428
+ // each value through a consumer hook before it hits the clipboard.
429
+ const withHeaders = ctx.props.copyHeadersToClipboard === true;
430
+ const processCell = ctx.props.processCellForClipboard as
431
+ | ((params: { value: unknown; column: unknown; row: unknown; rowIndex: number; columnId: string }) => unknown)
432
+ | undefined;
433
+ const blocks: Array<string> = [];
434
+ for (const rect of rects) {
435
+ const lines: Array<string> = [];
436
+ if (withHeaders) {
437
+ const header: Array<string> = [];
438
+ for (let c = rect.minCol; c <= rect.maxCol; c += 1) {
439
+ const column = ctx.allColumns[c];
440
+ header.push(column ? toolPanelHeaderLabel(column) : "");
441
+ }
442
+ lines.push(header.join("\t"));
443
+ }
444
+ for (let r = rect.minRow; r <= rect.maxRow; r += 1) {
445
+ const row = ctx.allRows[r];
446
+ if (!row || isGroupRow(row)) continue;
447
+ const cells: Array<string> = [];
448
+ for (let c = rect.minCol; c <= rect.maxCol; c += 1) {
449
+ const column = ctx.allColumns[c];
450
+ if (!column) {
451
+ cells.push("");
452
+ continue;
453
+ }
454
+ const base = getColumnBaseValue(row, column);
455
+ let value: unknown = ctx.getCellDisplayValue(row.id, column.id, base);
456
+ if (processCell) {
457
+ value = processCell({
458
+ value,
459
+ column,
460
+ row: row.original,
461
+ rowIndex: r,
462
+ columnId: column.id,
463
+ });
464
+ }
465
+ cells.push(String(value ?? ""));
435
466
  }
436
- const base = getColumnBaseValue(row, column);
437
- const display = ctx.getCellDisplayValue(row.id, column.id, base);
438
- cells.push(String(display ?? ""));
467
+ lines.push(cells.join("\t"));
439
468
  }
440
- lines.push(cells.join("\t"));
469
+ blocks.push(lines.join("\n"));
441
470
  }
442
- const text = lines.join("\n");
443
- writeClipboardText(text);
471
+ writeClipboardText(blocks.join("\n\n"));
444
472
  }
445
473
 
446
474
  /**