@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.
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridMenus.svelte +205 -127
- package/dist/SvGrid.controller.svelte.d.ts +44 -1
- package/dist/SvGrid.controller.svelte.js +240 -44
- package/dist/SvGrid.css +325 -22
- package/dist/SvGrid.helpers.test.d.ts +1 -0
- package/dist/SvGrid.helpers.test.js +298 -0
- package/dist/SvGrid.svelte +729 -203
- package/dist/SvGrid.types.d.ts +91 -4
- package/dist/aligned-grids.d.ts +6 -0
- package/dist/aligned-grids.js +84 -0
- package/dist/aligned-grids.test.d.ts +1 -0
- package/dist/aligned-grids.test.js +75 -0
- package/dist/build-api.coverage.test.d.ts +20 -0
- package/dist/build-api.coverage.test.js +505 -0
- package/dist/build-api.js +61 -31
- package/dist/cell-render.test.d.ts +1 -0
- package/dist/cell-render.test.js +338 -0
- package/dist/cell-values.d.ts +1 -1
- package/dist/cell-values.js +7 -7
- package/dist/chart-export.test.d.ts +1 -0
- package/dist/chart-export.test.js +302 -0
- package/dist/chart.coverage.test.d.ts +1 -0
- package/dist/chart.coverage.test.js +748 -0
- package/dist/clipboard.js +88 -24
- package/dist/clipboard.test.d.ts +1 -0
- package/dist/clipboard.test.js +700 -0
- package/dist/collaboration.coverage.test.d.ts +1 -0
- package/dist/collaboration.coverage.test.js +200 -0
- package/dist/column-groups.d.ts +19 -0
- package/dist/column-groups.js +62 -0
- package/dist/column-groups.test.d.ts +1 -0
- package/dist/column-groups.test.js +56 -0
- package/dist/column-types.d.ts +10 -0
- package/dist/column-types.js +63 -0
- package/dist/column-types.test.d.ts +1 -0
- package/dist/column-types.test.js +62 -0
- package/dist/columns.test.d.ts +1 -0
- package/dist/columns.test.js +625 -0
- package/dist/core.d.ts +86 -1
- package/dist/core.js +2 -2
- package/dist/editing.d.ts +7 -0
- package/dist/editing.js +191 -5
- package/dist/editing.test.d.ts +1 -0
- package/dist/editing.test.js +732 -0
- package/dist/editors/cell-editors.coverage.test.d.ts +1 -0
- package/dist/editors/cell-editors.coverage.test.js +139 -0
- package/dist/facet-buckets.test.d.ts +1 -0
- package/dist/facet-buckets.test.js +296 -0
- package/dist/filter-operators.test.d.ts +1 -0
- package/dist/filter-operators.test.js +135 -0
- package/dist/hyperformula-adapter.test.d.ts +1 -0
- package/dist/hyperformula-adapter.test.js +205 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/keyboard-handlers.coverage.test.d.ts +1 -0
- package/dist/keyboard-handlers.coverage.test.js +495 -0
- package/dist/keyboard-handlers.js +7 -2
- package/dist/menus.js +1 -0
- package/dist/menus.test.d.ts +1 -0
- package/dist/menus.test.js +560 -0
- package/dist/named-views.coverage.test.d.ts +1 -0
- package/dist/named-views.coverage.test.js +180 -0
- package/dist/row-drag.d.ts +49 -0
- package/dist/row-drag.js +221 -0
- package/dist/row-drag.test.d.ts +1 -0
- package/dist/row-drag.test.js +142 -0
- package/dist/row-resize.test.d.ts +1 -0
- package/dist/row-resize.test.js +329 -0
- package/dist/scroll-sync.js +3 -0
- package/dist/scroll-sync.test.d.ts +1 -0
- package/dist/scroll-sync.test.js +290 -0
- package/dist/selection.d.ts +7 -1
- package/dist/selection.js +76 -36
- package/dist/selection.multi-range.test.d.ts +1 -0
- package/dist/selection.multi-range.test.js +55 -0
- package/dist/selection.test.d.ts +1 -0
- package/dist/selection.test.js +647 -0
- package/dist/server-data-source.coverage.test.d.ts +1 -0
- package/dist/server-data-source.coverage.test.js +154 -0
- package/dist/spreadsheet.d.ts +30 -0
- package/dist/spreadsheet.js +48 -0
- package/dist/spreadsheet.test.d.ts +1 -0
- package/dist/spreadsheet.test.js +446 -0
- package/dist/summaries.js +4 -4
- package/dist/sv-grid-scrollbar.js +13 -1
- package/dist/svgrid-wrapper.types.d.ts +19 -0
- package/dist/svgrid.behavior.test.js +20 -0
- package/dist/svgrid.interaction.test.js +31 -0
- package/dist/svgrid.new-features.wrapper.test.js +34 -2
- package/dist/test-setup.js +9 -3
- package/dist/virtualization/scroll-scaling.d.ts +17 -0
- package/dist/virtualization/scroll-scaling.js +35 -0
- package/dist/virtualization/scroll-scaling.test.js +42 -1
- package/package.json +2 -1
- package/src/FlexRender.svelte +96 -96
- package/src/GridMenus.svelte +205 -127
- package/src/SvGrid.controller.svelte.ts +204 -36
- package/src/SvGrid.css +277 -12
- package/src/SvGrid.svelte +727 -205
- package/src/SvGrid.types.ts +85 -4
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +59 -59
- package/src/aligned-grids.test.ts +80 -0
- package/src/aligned-grids.ts +87 -0
- package/src/build-api.ts +43 -23
- package/src/cell-formatting.ts +169 -169
- package/src/cell-values.ts +4 -4
- package/src/clipboard.test.ts +49 -0
- package/src/clipboard.ts +51 -23
- package/src/column-groups.test.ts +59 -0
- package/src/column-groups.ts +80 -0
- package/src/column-types.test.ts +68 -0
- package/src/column-types.ts +82 -0
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1077 -999
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/editing.ts +161 -5
- package/src/flex-render.ts +3 -3
- package/src/index.ts +208 -196
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/menus.ts +1 -0
- package/src/merge-objects.ts +48 -48
- package/src/render-component.ts +28 -28
- package/src/row-drag.test.ts +168 -0
- package/src/row-drag.ts +255 -0
- package/src/scroll-sync.ts +2 -0
- package/src/selection.multi-range.test.ts +61 -0
- package/src/selection.ts +71 -37
- package/src/spreadsheet.test.ts +46 -2
- package/src/spreadsheet.ts +58 -0
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +4 -4
- package/src/svgrid-wrapper.types.ts +412 -393
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +2 -2
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- 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.
|
|
137
|
-
return column.columnDef.
|
|
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
|
-
//
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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(
|
|
185
|
+
ctx.setActiveCell(active.anchor.rowIndex, active.anchor.colIndex);
|
|
174
186
|
},
|
|
175
187
|
getSelected() {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
},
|
package/src/cell-formatting.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/cell-values.ts
CHANGED
|
@@ -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.
|
|
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 `
|
|
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.
|
|
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.
|
|
104
|
+
if (def.fieldFn) return def.fieldFn(rowData);
|
|
105
105
|
if (def.field) return rowData[def.field];
|
|
106
106
|
return undefined;
|
|
107
107
|
}
|
package/src/clipboard.test.ts
CHANGED
|
@@ -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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
437
|
-
const display = ctx.getCellDisplayValue(row.id, column.id, base);
|
|
438
|
-
cells.push(String(display ?? ""));
|
|
467
|
+
lines.push(cells.join("\t"));
|
|
439
468
|
}
|
|
440
|
-
|
|
469
|
+
blocks.push(lines.join("\n"));
|
|
441
470
|
}
|
|
442
|
-
|
|
443
|
-
writeClipboardText(text);
|
|
471
|
+
writeClipboardText(blocks.join("\n\n"));
|
|
444
472
|
}
|
|
445
473
|
|
|
446
474
|
/**
|