@svgrid/grid 3.0.4 → 3.0.6
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/README.md +32 -11
- package/dist/GridMenus.svelte +34 -4
- package/dist/SvGrid.controller.svelte.d.ts +33 -5
- package/dist/SvGrid.controller.svelte.js +397 -58
- package/dist/SvGrid.css +200 -2
- package/dist/SvGrid.svelte +350 -40
- package/dist/SvGrid.types.d.ts +433 -7
- package/dist/SvGridDropdown.svelte +20 -0
- package/dist/SvGridDropdown.svelte.d.ts +12 -0
- package/dist/SvModal.svelte +22 -0
- package/dist/SvModal.svelte.d.ts +7 -0
- package/dist/a11y/dismissable.d.ts +10 -0
- package/dist/a11y/dismissable.js +24 -2
- package/dist/build-api.js +92 -37
- package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-C-NtjlrJ.js} +155 -146
- package/dist/cdn/GridMenus-TssHej3q.js +644 -0
- package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
- package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-BlyCSp0q.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-ClRRA9lW.js} +1 -1
- package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-B4EV8U6t.js} +4 -4
- package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DsmwTyZF.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-cHloHobN.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-vOl7C0Za.js} +4 -4
- package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-AcqF-Ll5.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-biMtSBPA.js} +5 -5
- package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-Dx_GmZuL.js} +2 -2
- package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-I3rDHXAq.js} +2 -2
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
- package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
- package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
- package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-DVVKOSI9.js} +1 -1
- package/dist/cdn/{chart-Dz7SqMXH.js → chart-DEZxwC0i.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-D12XcTZJ.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DguS7gC5.js} +1 -1
- package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-CMAXBGY-.js} +1 -1
- package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-BkEp61Gj.js} +1 -1
- package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
- package/dist/cdn/{export-format-CUDLy2yn.js → export-format-YzdwBww2.js} +1 -1
- package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
- package/dist/cdn/server-block-cache-DIqb3VZn.js +241 -0
- package/dist/cdn/{src-V1uu8iE9.js → src-CgoeSM8k.js} +5339 -4753
- package/dist/cdn/{src-BKhZ6eXd.js → src-DfwxB1TH.js} +9817 -9231
- package/dist/cdn/svgrid.js +28 -27
- package/dist/cdn/svgrid.svelte-external.js +28 -27
- package/dist/cdn/validate-AZoD8BoR.js +80 -0
- package/dist/cell-formatting.js +18 -1
- package/dist/cell-render.js +25 -6
- package/dist/clipboard.d.ts +2 -2
- package/dist/clipboard.js +72 -23
- package/dist/columns.js +7 -2
- package/dist/conditional-formatting.d.ts +2 -0
- package/dist/conditional-formatting.js +2 -0
- package/dist/core.d.ts +22 -6
- package/dist/core.js +401 -38
- package/dist/editing.js +34 -0
- package/dist/gantt-view.svelte.d.ts +24 -0
- package/dist/gantt-view.svelte.js +13 -0
- package/dist/grid-icons.d.ts +2 -2
- package/dist/grid-icons.js +2 -2
- package/dist/grid-messages.d.ts +10 -0
- package/dist/grid-messages.js +9 -0
- package/dist/headless.d.ts +5 -0
- package/dist/headless.js +12 -0
- package/dist/history.d.ts +21 -0
- package/dist/history.js +34 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/keyboard-handlers.js +24 -1
- package/dist/keyboard.js +4 -0
- package/dist/row-drag-touch.d.ts +1 -1
- package/dist/row-drag.js +35 -1
- package/dist/row-model.d.ts +139 -0
- package/dist/row-model.js +16 -0
- package/dist/row-resize.js +22 -1
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +31 -3
- package/dist/server-block-cache.d.ts +219 -0
- package/dist/server-block-cache.js +539 -0
- package/dist/server-data-source.d.ts +274 -8
- package/dist/server-data-source.js +229 -20
- package/dist/server.d.ts +13 -0
- package/dist/server.js +13 -0
- package/dist/sparkline.js +7 -5
- package/dist/svgrid-wrapper.types.d.ts +33 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +29 -3
- package/package.json +11 -1
- package/src/GridMenus.svelte +34 -4
- package/src/SvGrid.controller.svelte.ts +397 -61
- package/src/SvGrid.css +200 -2
- package/src/SvGrid.svelte +350 -40
- package/src/SvGrid.types.ts +453 -7
- package/src/SvGridDropdown.svelte +20 -0
- package/src/SvModal.svelte +22 -0
- package/src/SvModal.test.ts +30 -1
- package/src/a11y/dismissable.test.ts +10 -1
- package/src/a11y/dismissable.ts +22 -2
- package/src/build-api.ts +96 -31
- package/src/cell-formatting.test.ts +17 -0
- package/src/cell-formatting.ts +18 -1
- package/src/cell-render.test.ts +24 -2
- package/src/cell-render.ts +24 -8
- package/src/clipboard.test.ts +107 -0
- package/src/clipboard.ts +62 -23
- package/src/columns.test.ts +4 -4
- package/src/columns.ts +7 -2
- package/src/conditional-formatting.test.ts +8 -0
- package/src/conditional-formatting.ts +3 -0
- package/src/core.rowmodel-cache.test.ts +51 -0
- package/src/core.tick-repair.test.ts +287 -0
- package/src/core.ts +407 -38
- package/src/editing.test.ts +52 -0
- package/src/editing.ts +33 -0
- package/src/gantt-stub.test.svelte +38 -0
- package/src/gantt-view.svelte.ts +35 -0
- package/src/grid-icons.ts +2 -2
- package/src/grid-messages.test.ts +7 -0
- package/src/grid-messages.ts +22 -0
- package/src/headless.ts +24 -0
- package/src/history.test.ts +49 -1
- package/src/history.ts +48 -2
- package/src/icon-seam.test.ts +2 -4
- package/src/index.ts +38 -10
- package/src/keyboard-handlers.coverage.test.ts +65 -0
- package/src/keyboard-handlers.ts +26 -1
- package/src/keyboard.ts +3 -0
- package/src/row-drag-touch.ts +2 -2
- package/src/row-drag.test.ts +51 -0
- package/src/row-drag.ts +35 -2
- package/src/row-model.ts +152 -0
- package/src/row-resize.ts +24 -1
- package/src/selection.test.ts +3 -0
- package/src/selection.ts +33 -3
- package/src/server-block-cache.test.ts +672 -0
- package/src/server-block-cache.ts +690 -0
- package/src/server-data-source.infinite.test.ts +405 -0
- package/src/server-data-source.test.ts +19 -0
- package/src/server-data-source.ts +496 -29
- package/src/server.ts +49 -0
- package/src/sparkline.test.ts +7 -0
- package/src/sparkline.ts +6 -4
- package/src/svgrid-wrapper.types.ts +32 -0
- package/src/svgrid.api.test.ts +21 -0
- package/src/svgrid.charting.test.ts +12 -4
- package/src/svgrid.context-menu.test.ts +12 -0
- package/src/svgrid.detail-rows.svelte.test.ts +187 -0
- package/src/svgrid.filter-menu-scroll.test.ts +9 -0
- package/src/svgrid.gantt-seam.test.ts +199 -0
- package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
- package/src/svgrid.menu-scroll-close.test.ts +10 -0
- package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +305 -0
- package/src/svgrid.row-pinning.test.ts +15 -1
- package/src/svgrid.selection-bar-seam.test.ts +1 -1
- package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
- package/src/svgrid.upsell-license.test.ts +8 -5
- package/src/test-setup.ts +12 -0
- package/src/transaction.test.ts +58 -0
- package/src/validate.test.ts +29 -0
- package/src/validate.ts +34 -3
- package/dist/SvGroupCell.svelte +0 -141
- package/dist/SvGroupCell.svelte.d.ts +0 -49
- package/dist/SvRowGroupPanel.svelte +0 -186
- package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
- package/dist/cdn/GridMenus-7kbpnnBW.js +0 -635
- package/dist/cdn/validate-_CDJzgIo.js +0 -75
- package/dist/server-group-model.d.ts +0 -98
- package/dist/server-group-model.js +0 -263
- package/src/SvGroupCell.svelte +0 -141
- package/src/SvRowGroupPanel.svelte +0 -186
- package/src/server-group-model.test.ts +0 -294
- package/src/server-group-model.ts +0 -370
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
2
|
import { createDismissableLayer, dismissableDepth, onScrollOutside } from './dismissable'
|
|
3
3
|
|
|
4
|
+
const nextFrame = () => new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
|
5
|
+
|
|
4
6
|
function panel(): HTMLElement {
|
|
5
7
|
const el = document.createElement('div')
|
|
6
8
|
document.body.appendChild(el)
|
|
@@ -77,13 +79,20 @@ describe('createDismissableLayer', () => {
|
|
|
77
79
|
expect(dismissableDepth()).toBe(0)
|
|
78
80
|
})
|
|
79
81
|
|
|
80
|
-
it('close-on-scroll ignores scrolling inside the panel', () => {
|
|
82
|
+
it('close-on-scroll ignores scrolling inside the panel', async () => {
|
|
81
83
|
const el = panel()
|
|
82
84
|
const list = document.createElement('div') // e.g. the filter menu's facet list
|
|
83
85
|
el.appendChild(list)
|
|
84
86
|
let closed = 0
|
|
85
87
|
const off = onScrollOutside(() => el, () => closed++)
|
|
86
88
|
|
|
89
|
+
// A scroll that finished before the panel opened is still queued for
|
|
90
|
+
// the next rendering step; it lands after the listener is asked for
|
|
91
|
+
// and must not close what the same click just opened.
|
|
92
|
+
document.body.dispatchEvent(new Event('scroll'))
|
|
93
|
+
expect(closed).toBe(0)
|
|
94
|
+
await nextFrame()
|
|
95
|
+
|
|
87
96
|
// Inner scroll containers don't bubble, so this only reaches the capture
|
|
88
97
|
// listener - it must NOT close the menu the user is scrolling.
|
|
89
98
|
list.dispatchEvent(new Event('scroll'))
|
package/src/a11y/dismissable.ts
CHANGED
|
@@ -101,6 +101,16 @@ export function dismissableDepth(): number {
|
|
|
101
101
|
* window listener gets wrong: inner scroll containers don't bubble, so the
|
|
102
102
|
* capture listener sees them and dismisses the panel the user is scrolling.
|
|
103
103
|
*
|
|
104
|
+
* The listener arms one animation frame after it is asked for, not at once.
|
|
105
|
+
* Scroll events are queued and dispatched in the next rendering step, so a
|
|
106
|
+
* scroll that FINISHED before the click that opened the panel - the browser
|
|
107
|
+
* pulling a half-hidden funnel button into view as it focuses it, a test
|
|
108
|
+
* driver scrolling the header into place - still lands after the panel has
|
|
109
|
+
* mounted and wired this listener, and used to close it on the spot. The
|
|
110
|
+
* panel measured its anchor after that scroll, so the event carries nothing
|
|
111
|
+
* it needs to react to. Rendering runs the scroll steps before the animation
|
|
112
|
+
* frame callbacks, which is why one frame is exactly the right delay.
|
|
113
|
+
*
|
|
104
114
|
* Returns the unsubscribe function.
|
|
105
115
|
*/
|
|
106
116
|
export function onScrollOutside(element: () => DismissTarget, onOutside: () => void): () => void {
|
|
@@ -109,6 +119,16 @@ export function onScrollOutside(element: () => DismissTarget, onOutside: () => v
|
|
|
109
119
|
if (containsTarget(element(), event.target as Node)) return
|
|
110
120
|
onOutside()
|
|
111
121
|
}
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
let armed = false
|
|
123
|
+
let frame: number | null = requestAnimationFrame(() => {
|
|
124
|
+
frame = null
|
|
125
|
+
armed = true
|
|
126
|
+
window.addEventListener('scroll', handler, true)
|
|
127
|
+
})
|
|
128
|
+
return () => {
|
|
129
|
+
if (frame !== null) cancelAnimationFrame(frame)
|
|
130
|
+
frame = null
|
|
131
|
+
if (armed) window.removeEventListener('scroll', handler, true)
|
|
132
|
+
armed = false
|
|
133
|
+
}
|
|
114
134
|
}
|
package/src/build-api.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
columnDefMatchesId,
|
|
27
27
|
} from "./cell-values";
|
|
28
28
|
import { hasAdvancedFilterEngine } from "./advanced-filter.svelte";
|
|
29
|
-
import { undoHistory, redoHistory } from "./history";
|
|
29
|
+
import { undoHistory, redoHistory, peekUndoHistory, peekRedoHistory } from "./history";
|
|
30
30
|
import { buildCommandContext } from "./command-context";
|
|
31
31
|
|
|
32
32
|
type OrientedRange = {
|
|
@@ -35,6 +35,32 @@ type OrientedRange = {
|
|
|
35
35
|
highlight: { row: number; col: number }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Row id -> index, per data array, for `applyTransaction`.
|
|
40
|
+
*
|
|
41
|
+
* An update batch used to walk every row calling `getRowId` to find the ones
|
|
42
|
+
* it replaced - 100k calls per tick on a 100k-row feed, whatever the batch
|
|
43
|
+
* size. The map is built once per array and, for an update-only transaction,
|
|
44
|
+
* carried to the array it produces: same ids at the same indices. An add or
|
|
45
|
+
* remove changes indices, so those transactions leave the next array to
|
|
46
|
+
* build its own map on first use. Keyed weakly so a replaced array takes its
|
|
47
|
+
* map with it.
|
|
48
|
+
*/
|
|
49
|
+
const rowIndexByData = new WeakMap<ReadonlyArray<unknown>, Map<string, number>>()
|
|
50
|
+
|
|
51
|
+
function rowIndexOf<TData>(
|
|
52
|
+
data: ReadonlyArray<TData>,
|
|
53
|
+
getId: (row: TData, index: number) => string,
|
|
54
|
+
): Map<string, number> {
|
|
55
|
+
let byId = rowIndexByData.get(data)
|
|
56
|
+
if (!byId) {
|
|
57
|
+
byId = new Map()
|
|
58
|
+
for (let i = 0; i < data.length; i++) byId.set(getId(data[i]!, i), i)
|
|
59
|
+
rowIndexByData.set(data, byId)
|
|
60
|
+
}
|
|
61
|
+
return byId
|
|
62
|
+
}
|
|
63
|
+
|
|
38
64
|
/**
|
|
39
65
|
* The selection as the user made it: committed ranges first, the active one
|
|
40
66
|
* last, each keeping its anchor as `from` and its focus as `to`. The active
|
|
@@ -111,6 +137,10 @@ export function createGridApi<
|
|
|
111
137
|
};
|
|
112
138
|
|
|
113
139
|
return {
|
|
140
|
+
getElement() {
|
|
141
|
+
const el = ctx.scrollContainer as HTMLElement | null
|
|
142
|
+
return (el?.closest(".sv-grid-root") as HTMLElement | null) ?? null
|
|
143
|
+
},
|
|
114
144
|
getCellValue(rowIndex, columnId) {
|
|
115
145
|
const row = ctx.internalData[rowIndex];
|
|
116
146
|
const column = findColumn(columnId);
|
|
@@ -243,38 +273,41 @@ export function createGridApi<
|
|
|
243
273
|
},
|
|
244
274
|
applyTransaction(tx) {
|
|
245
275
|
const getId = ctx.props.getRowId;
|
|
246
|
-
|
|
276
|
+
const current = ctx.internalData as ReadonlyArray<TData>;
|
|
277
|
+
let next = current.slice() as Array<TData>;
|
|
247
278
|
let added = 0;
|
|
248
279
|
let updated = 0;
|
|
249
280
|
let removed = 0;
|
|
250
281
|
|
|
282
|
+
// Everything resolves against the indices of `current`: the id ->
|
|
283
|
+
// index map answers updates and id removals with one lookup each, a
|
|
284
|
+
// reference removal with one `indexOf`, and no pass over the rows
|
|
285
|
+
// that did not change. A row both updated and removed in the same
|
|
286
|
+
// transaction ends up removed, as it did when removes ran first.
|
|
287
|
+
const removeIdx = new Set<number>();
|
|
251
288
|
if (tx.remove?.length) {
|
|
252
|
-
const
|
|
253
|
-
const removeRefs = new Set<TData>();
|
|
289
|
+
const byId = getId ? rowIndexOf(current, getId) : null;
|
|
254
290
|
for (const r of tx.remove) {
|
|
255
|
-
|
|
256
|
-
|
|
291
|
+
const idx = typeof r === "string" ? byId?.get(r) : current.indexOf(r as TData);
|
|
292
|
+
if (idx !== undefined && idx >= 0) removeIdx.add(idx);
|
|
257
293
|
}
|
|
258
|
-
next = next.filter((row, i) => {
|
|
259
|
-
const hit =
|
|
260
|
-
removeRefs.has(row) ||
|
|
261
|
-
(getId ? removeIds.has(getId(row, i)) : false);
|
|
262
|
-
if (hit) removed += 1;
|
|
263
|
-
return !hit;
|
|
264
|
-
});
|
|
265
294
|
}
|
|
266
295
|
|
|
267
296
|
if (tx.update?.length && getId) {
|
|
268
|
-
const byId =
|
|
269
|
-
for (const u of tx.update)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
297
|
+
const byId = rowIndexOf(current, getId);
|
|
298
|
+
for (const u of tx.update) {
|
|
299
|
+
const idx = byId.get(getId(u, 0));
|
|
300
|
+
if (idx === undefined) continue;
|
|
301
|
+
next[idx] = u;
|
|
302
|
+
updated += 1;
|
|
303
|
+
}
|
|
304
|
+
// Same ids at the same indices: the next array can reuse the map.
|
|
305
|
+
if (!removeIdx.size && !tx.add?.length) rowIndexByData.set(next, byId);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (removeIdx.size) {
|
|
309
|
+
next = next.filter((_, i) => !removeIdx.has(i));
|
|
310
|
+
removed = removeIdx.size;
|
|
278
311
|
}
|
|
279
312
|
|
|
280
313
|
if (tx.add?.length) {
|
|
@@ -507,6 +540,28 @@ export function createGridApi<
|
|
|
507
540
|
getData() {
|
|
508
541
|
return ctx.internalData;
|
|
509
542
|
},
|
|
543
|
+
getRowId(row) {
|
|
544
|
+
// The stable id the grid keys a row by: what `getRowId` computed,
|
|
545
|
+
// or the row's position when no `getRowId` prop is set. A number is
|
|
546
|
+
// a DISPLAY index into the rendered rows, the space getActiveCell,
|
|
547
|
+
// scrollToRow and the cell methods work in; a row object is looked
|
|
548
|
+
// up wherever it sits. Null when the index or the object is unknown,
|
|
549
|
+
// so a caller can tell "no such row" from a real id.
|
|
550
|
+
if (typeof row === "number") {
|
|
551
|
+
const found = ctx.allRows[row];
|
|
552
|
+
return found && !isGroupRow(found) ? (found.id as string) : null;
|
|
553
|
+
}
|
|
554
|
+
const inRows = ctx.allRows.find(
|
|
555
|
+
(r: { original?: unknown; id: string }) => r.original === row,
|
|
556
|
+
);
|
|
557
|
+
if (inRows) return inRows.id as string;
|
|
558
|
+
const getId = ctx.props.getRowId as
|
|
559
|
+
| ((r: TData, index: number) => string)
|
|
560
|
+
| undefined;
|
|
561
|
+
const index = ctx.internalData.indexOf(row as TData);
|
|
562
|
+
if (index < 0) return null;
|
|
563
|
+
return getId ? getId(row as TData, index) : String(index);
|
|
564
|
+
},
|
|
510
565
|
getColumns() {
|
|
511
566
|
// Snapshot every column with its human-readable label and
|
|
512
567
|
// visibility flag. Used by external code (exporters, column
|
|
@@ -631,10 +686,10 @@ export function createGridApi<
|
|
|
631
686
|
ctx.autosizeColumn(columnId);
|
|
632
687
|
},
|
|
633
688
|
getRowHeight(rowIndex: number) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
return
|
|
689
|
+
// The height the grid draws the row at, whatever decided it: a drag,
|
|
690
|
+
// a measurement under auto height, a detail row's own height, the
|
|
691
|
+
// declared rowHeight or the default.
|
|
692
|
+
return ctx.rowSizePxOf(rowIndex);
|
|
638
693
|
},
|
|
639
694
|
setRowHeight(rowIndex: number, height: number | null) {
|
|
640
695
|
ctx.setRowResizeHeight(rowIndex, height);
|
|
@@ -705,6 +760,9 @@ export function createGridApi<
|
|
|
705
760
|
canUndo() { void ctx.historyVersion; return ctx.historyPtr >= 0 },
|
|
706
761
|
canRedo() { void ctx.historyVersion; return ctx.historyPtr < ctx.history.length - 1 },
|
|
707
762
|
clearHistory() { ctx.history = []; ctx.historyPtr = -1; ctx.historyVersion += 1 },
|
|
763
|
+
setHistoryTag(tag) { ctx.historyTag = tag },
|
|
764
|
+
peekUndo() { void ctx.historyVersion; return peekUndoHistory(ctx) },
|
|
765
|
+
peekRedo() { void ctx.historyVersion; return peekRedoHistory(ctx) },
|
|
708
766
|
// ---- Find
|
|
709
767
|
openFind() { ctx.findOpen = true },
|
|
710
768
|
closeFind() { ctx.findOpen = false; ctx.findQuery = '' },
|
|
@@ -735,9 +793,10 @@ export function createGridApi<
|
|
|
735
793
|
});
|
|
736
794
|
},
|
|
737
795
|
selectAllRows() {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
796
|
+
// One implementation of "select everything", shared with the header
|
|
797
|
+
// checkbox, so it knows about an external selection model and about
|
|
798
|
+
// rows whose data has not arrived.
|
|
799
|
+
ctx.setSelectAllRows(true);
|
|
741
800
|
},
|
|
742
801
|
toggleRowSelected(id) {
|
|
743
802
|
ctx.toggleRowSelectionById(id);
|
|
@@ -745,7 +804,13 @@ export function createGridApi<
|
|
|
745
804
|
// ---- Pagination
|
|
746
805
|
getPageInfo() {
|
|
747
806
|
const { pageIndex, pageSize } = ctx.paginationState;
|
|
748
|
-
|
|
807
|
+
// Under external pagination the grid holds ONE page, so counting the
|
|
808
|
+
// rows in hand would report a 95-row table as a 20-row one. The
|
|
809
|
+
// consumer tells us the real total through rowCount, which is what
|
|
810
|
+
// the footer has always shown.
|
|
811
|
+
const total = ctx.externalPaginationEnabled
|
|
812
|
+
? (ctx.props.rowCount ?? ctx.allRowsBeforePagination.length)
|
|
813
|
+
: ctx.allRowsBeforePagination.length;
|
|
749
814
|
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
|
|
750
815
|
return { pageIndex, pageSize, pageCount, total };
|
|
751
816
|
},
|
|
@@ -271,3 +271,20 @@ describe('formatNumericWithConfig - cache reuse', () => {
|
|
|
271
271
|
expect(typeof result).toBe('string')
|
|
272
272
|
})
|
|
273
273
|
})
|
|
274
|
+
|
|
275
|
+
describe('formatNumericWithConfig - formatter cache key', () => {
|
|
276
|
+
it('does not hand a signDisplay column the formatter another column cached without it', () => {
|
|
277
|
+
// Same digits, no sign: cached first, as a currency-like column would.
|
|
278
|
+
expect(
|
|
279
|
+
formatNumericWithConfig(3.27, { type: 'number', locales: 'en-US', options: { minimumFractionDigits: 2, maximumFractionDigits: 2 } }),
|
|
280
|
+
).toBe('3.27')
|
|
281
|
+
// Same digits, with a sign: must not reuse the one above.
|
|
282
|
+
expect(
|
|
283
|
+
formatNumericWithConfig(3.27, {
|
|
284
|
+
type: 'number',
|
|
285
|
+
locales: 'en-US',
|
|
286
|
+
options: { minimumFractionDigits: 2, maximumFractionDigits: 2, signDisplay: 'always' },
|
|
287
|
+
}),
|
|
288
|
+
).toBe('+3.27')
|
|
289
|
+
})
|
|
290
|
+
})
|
package/src/cell-formatting.ts
CHANGED
|
@@ -101,7 +101,24 @@ export function getNumberFormatter(
|
|
|
101
101
|
'|' +
|
|
102
102
|
(options.currencyDisplay ?? '') +
|
|
103
103
|
'|' +
|
|
104
|
-
(options.notation ?? '')
|
|
104
|
+
(options.notation ?? '') +
|
|
105
|
+
// Every option that changes the output belongs in the key. `signDisplay`
|
|
106
|
+
// was missing, so a percent column asking for "+3.27" got the formatter
|
|
107
|
+
// a currency column with the same digits had already cached, and no sign.
|
|
108
|
+
'|' +
|
|
109
|
+
(options.signDisplay ?? '') +
|
|
110
|
+
'|' +
|
|
111
|
+
(options.minimumIntegerDigits ?? '') +
|
|
112
|
+
'|' +
|
|
113
|
+
(options.compactDisplay ?? '') +
|
|
114
|
+
'|' +
|
|
115
|
+
(options.unit ?? '') +
|
|
116
|
+
'|' +
|
|
117
|
+
(options.unitDisplay ?? '') +
|
|
118
|
+
'|' +
|
|
119
|
+
(options.minimumSignificantDigits ?? '') +
|
|
120
|
+
'|' +
|
|
121
|
+
(options.maximumSignificantDigits ?? '')
|
|
105
122
|
let fmt = numberFormatterCache.get(key)
|
|
106
123
|
if (!fmt) {
|
|
107
124
|
fmt = new Intl.NumberFormat(locales as string | string[] | undefined, options)
|
package/src/cell-render.test.ts
CHANGED
|
@@ -141,6 +141,26 @@ describe('createCellRender / computeCellValidity', () => {
|
|
|
141
141
|
message: 'Salary must be at least $1,000',
|
|
142
142
|
})
|
|
143
143
|
})
|
|
144
|
+
|
|
145
|
+
it('judges the open editor draft, parsed as a commit would parse it', () => {
|
|
146
|
+
// With rejectInvalid a refused value never lands, so the red flag while
|
|
147
|
+
// the value is typed is the only feedback the user gets.
|
|
148
|
+
const col = makeColumn('salary', { validate: validateSalary, editorType: 'number' })
|
|
149
|
+
const typing = createCellRender(makeCtx({
|
|
150
|
+
editedCellValues: {},
|
|
151
|
+
editingCell: { rowId: 'r1', columnId: 'salary', editorType: 'number', value: '5' },
|
|
152
|
+
}))
|
|
153
|
+
expect(typing.computeCellValidity(makeRow('r1', { salary: 85000 }), col)).toEqual({
|
|
154
|
+
invalid: true,
|
|
155
|
+
message: 'Salary must be at least $1,000',
|
|
156
|
+
})
|
|
157
|
+
// Another cell's editor does not bleed into this one.
|
|
158
|
+
const elsewhere = createCellRender(makeCtx({
|
|
159
|
+
editedCellValues: {},
|
|
160
|
+
editingCell: { rowId: 'r2', columnId: 'salary', editorType: 'number', value: '5' },
|
|
161
|
+
}))
|
|
162
|
+
expect(elsewhere.computeCellValidity(makeRow('r1', { salary: 85000 }), col)).toEqual({ invalid: false, message: null })
|
|
163
|
+
})
|
|
144
164
|
})
|
|
145
165
|
|
|
146
166
|
describe('createCellRender / computeRowClass', () => {
|
|
@@ -489,9 +509,11 @@ describe('createCellRender / formatPinnedValue', () => {
|
|
|
489
509
|
})
|
|
490
510
|
|
|
491
511
|
describe('createCellRender / computePinnedCellClass', () => {
|
|
492
|
-
it('
|
|
512
|
+
it('applies a static cellClass, a string or a list, the way a body cell does', () => {
|
|
493
513
|
const cr = createCellRender(makeCtx())
|
|
494
|
-
expect(cr.computePinnedCellClass({ a: 1 }, makeColumn('a', { cellClass: 'x' }))).toBe('')
|
|
514
|
+
expect(cr.computePinnedCellClass({ a: 1 }, makeColumn('a', { cellClass: 'x' }))).toBe('x')
|
|
515
|
+
expect(cr.computePinnedCellClass({ a: 1 }, makeColumn('a', { cellClass: ['x', 'y'] }))).toBe('x y')
|
|
516
|
+
expect(cr.computePinnedCellClass({ a: 1 }, makeColumn('a', {}))).toBe('')
|
|
495
517
|
})
|
|
496
518
|
|
|
497
519
|
it('invokes a function cellClass with a null row and the pinned value', () => {
|
package/src/cell-render.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import {
|
|
31
31
|
getPinnedCellValue,
|
|
32
32
|
} from "./cell-values";
|
|
33
|
+
import { parseEditorValue } from "./editors/cell-editors";
|
|
33
34
|
|
|
34
35
|
export function createCellRender<
|
|
35
36
|
TFeatures extends TableFeatures = TableFeatures,
|
|
@@ -131,11 +132,22 @@ export function createCellRender<
|
|
|
131
132
|
// cache, so `getCellValueByColumnId` would return the stale pre-edit value
|
|
132
133
|
// and validation would never re-run after an edit. Consult the overlay
|
|
133
134
|
// first - same rule the display path (getCellDisplayValue) and summaries
|
|
134
|
-
// use
|
|
135
|
+
// use. While this cell's editor is open the draft is what the rule sees,
|
|
136
|
+
// parsed the way a commit would parse it, so the red highlight and the
|
|
137
|
+
// message show as the user types - which, with `rejectInvalid`, is the
|
|
138
|
+
// only time a refused value is ever visible.
|
|
135
139
|
const edited = ctx.editedCellValues ?? {}
|
|
136
140
|
const key = getCellKey(row.id, column.id)
|
|
141
|
+
const editing = ctx.editingCell
|
|
137
142
|
const value =
|
|
138
|
-
|
|
143
|
+
editing && editing.rowId === row.id && editing.columnId === column.id
|
|
144
|
+
? parseEditorValue(editing.editorType, editing.value, {
|
|
145
|
+
multiple: column.columnDef.editorMultiple === true,
|
|
146
|
+
dateOnly: column.columnDef.cellDataType === "dateString",
|
|
147
|
+
})
|
|
148
|
+
: key in edited
|
|
149
|
+
? edited[key]
|
|
150
|
+
: row.getCellValueByColumnId(column.id)
|
|
139
151
|
const out = rule({
|
|
140
152
|
value,
|
|
141
153
|
row: row.original as TData,
|
|
@@ -470,13 +482,17 @@ export function createCellRender<
|
|
|
470
482
|
rowData: TData,
|
|
471
483
|
column: Column<TData>,
|
|
472
484
|
): string {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
485
|
+
const raw = column.columnDef.cellClass;
|
|
486
|
+
if (raw == null) return "";
|
|
487
|
+
// A static class applies to the pinned cell the way it does to a body
|
|
488
|
+
// cell; a function gets the pinned value and `row: null`, since the
|
|
489
|
+
// pinned row is not a row of the table.
|
|
490
|
+
if (typeof raw === "string" || Array.isArray(raw)) return resolveClassList(raw);
|
|
491
|
+
if (typeof raw !== "function") return "";
|
|
492
|
+
const cellClass = raw as unknown as
|
|
493
|
+
(ctx: { getValue: () => unknown; value: unknown; row: null; column: Column<TData> }) => string | ReadonlyArray<string> | Record<string, boolean> | undefined | null;
|
|
477
494
|
const value = getPinnedCellValue(rowData, column);
|
|
478
|
-
|
|
479
|
-
return out ? String(out) : "";
|
|
495
|
+
return resolveClassList(cellClass({ getValue: () => value, value, row: null, column }));
|
|
480
496
|
}
|
|
481
497
|
|
|
482
498
|
return {
|
package/src/clipboard.test.ts
CHANGED
|
@@ -441,6 +441,25 @@ describe('onFillPointerUp / applyFillPattern', () => {
|
|
|
441
441
|
expect(ctx.selectionRange.focus).toEqual({ rowIndex: 3, colIndex: 0 })
|
|
442
442
|
})
|
|
443
443
|
|
|
444
|
+
it('turns the fill the other way when the release holds Ctrl, as Excel does', () => {
|
|
445
|
+
// A series becomes a copy of the block...
|
|
446
|
+
const ctx = makeCtx({
|
|
447
|
+
columns: [{ id: 'a', field: 'a', editable: true, editorType: 'number' }],
|
|
448
|
+
data: [{ a: 1 }, { a: 2 }, { a: 0 }, { a: 0 }, { a: 0 }],
|
|
449
|
+
})
|
|
450
|
+
ctx.fillDrag = { sourceMinRow: 0, sourceMaxRow: 1, sourceMinCol: 0, sourceMaxCol: 0, targetRow: 4, targetCol: 0 }
|
|
451
|
+
createClipboard(ctx).onFillPointerUp({ ctrlKey: true } as PointerEvent)
|
|
452
|
+
expect([ctx.internalData[2].a, ctx.internalData[3].a, ctx.internalData[4].a]).toEqual([1, 2, 1])
|
|
453
|
+
// ...and one number, which copies on its own, becomes a series.
|
|
454
|
+
const one = makeCtx({
|
|
455
|
+
columns: [{ id: 'a', field: 'a', editable: true, editorType: 'number' }],
|
|
456
|
+
data: [{ a: 7 }, { a: 0 }, { a: 0 }],
|
|
457
|
+
})
|
|
458
|
+
one.fillDrag = { sourceMinRow: 0, sourceMaxRow: 0, sourceMinCol: 0, sourceMaxCol: 0, targetRow: 2, targetCol: 0 }
|
|
459
|
+
createClipboard(one).onFillPointerUp({ ctrlKey: true } as PointerEvent)
|
|
460
|
+
expect([one.internalData[1].a, one.internalData[2].a]).toEqual([8, 9])
|
|
461
|
+
})
|
|
462
|
+
|
|
444
463
|
it('records the drag as one grouped history entry, so Ctrl+Z takes it all back', () => {
|
|
445
464
|
// The fill used to write through writeCellRaw and push nothing: a drag
|
|
446
465
|
// over twenty rows was invisible to undo.
|
|
@@ -487,6 +506,38 @@ describe('onFillPointerUp / applyFillPattern', () => {
|
|
|
487
506
|
expect(numeric.internalData[3].a).toBe(4)
|
|
488
507
|
})
|
|
489
508
|
|
|
509
|
+
it('skips a collapsed row the consumer does not count, and moves the series over the rows that show', () => {
|
|
510
|
+
// A filtered row in a spreadsheet: not written, not a step of the
|
|
511
|
+
// number series, and the formula's delta is the real row distance.
|
|
512
|
+
const ctx = makeCtx({
|
|
513
|
+
columns: [{ id: 'a', field: 'a', editable: true, editorType: 'number' }],
|
|
514
|
+
data: [{ a: 10 }, { a: 20 }, { a: 0 }, { a: 0 }, { a: 0 }],
|
|
515
|
+
})
|
|
516
|
+
ctx.isRowCollapsed = (r: number) => r === 3
|
|
517
|
+
const deltas: number[] = []
|
|
518
|
+
ctx.props.processCellForFill = ({ delta }: { delta: { rows: number } }) => { deltas.push(delta.rows); return undefined }
|
|
519
|
+
ctx.fillDrag = { sourceMinRow: 0, sourceMaxRow: 1, sourceMinCol: 0, sourceMaxCol: 0, targetRow: 4, targetCol: 0 }
|
|
520
|
+
createClipboard(ctx).onFillPointerUp()
|
|
521
|
+
// Row 3 is left holding its 0; the series 30, (skip), 40 lands on 2 and 4.
|
|
522
|
+
expect(ctx.internalData.map((r: any) => r.a)).toEqual([10, 20, 30, 0, 40])
|
|
523
|
+
// The hook saw the real row distance from the source cell each target
|
|
524
|
+
// cycled from: row 2 is 2 below source row 0, row 4 is 3 below source
|
|
525
|
+
// row 1, not the 1 and 2 a step count would have given.
|
|
526
|
+
expect(deltas).toEqual([2, 3])
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it('includeCollapsedRows brings a collapsed row back into the fill', () => {
|
|
530
|
+
const ctx = makeCtx({
|
|
531
|
+
columns: [{ id: 'a', field: 'a', editable: true, editorType: 'number' }],
|
|
532
|
+
data: [{ a: 10 }, { a: 0 }, { a: 0 }],
|
|
533
|
+
})
|
|
534
|
+
ctx.isRowCollapsed = (r: number) => r === 1
|
|
535
|
+
ctx.props.includeCollapsedRows = (r: number) => r === 1
|
|
536
|
+
ctx.fillDrag = { sourceMinRow: 0, sourceMaxRow: 0, sourceMinCol: 0, sourceMaxCol: 0, targetRow: 2, targetCol: 0 }
|
|
537
|
+
createClipboard(ctx).onFillPointerUp()
|
|
538
|
+
expect(ctx.internalData.map((r: any) => r.a)).toEqual([10, 10, 10])
|
|
539
|
+
})
|
|
540
|
+
|
|
490
541
|
it('double-click fills down as far as the neighbouring column has data', () => {
|
|
491
542
|
const ctx = makeCtx({
|
|
492
543
|
columns: [
|
|
@@ -676,6 +727,45 @@ describe('copySelectionToClipboard', () => {
|
|
|
676
727
|
expect(writeText).toHaveBeenCalledWith('a0\tb0\na1\tb1')
|
|
677
728
|
})
|
|
678
729
|
|
|
730
|
+
// A QA pass on the spreadsheet shell: a filtered or hidden row is folded
|
|
731
|
+
// to nothing, and a copy that carried it put rows the user had filtered
|
|
732
|
+
// out into the paste.
|
|
733
|
+
it('leaves out a collapsed row, as a spreadsheet leaves out a filtered one', () => {
|
|
734
|
+
const ctx = makeCtx({
|
|
735
|
+
data: [
|
|
736
|
+
{ a: 'a0', b: 'b0' },
|
|
737
|
+
{ a: 'a1', b: 'b1' },
|
|
738
|
+
{ a: 'a2', b: 'b2' },
|
|
739
|
+
],
|
|
740
|
+
})
|
|
741
|
+
ctx.isRowCollapsed = (index: number) => index === 1
|
|
742
|
+
ctx.selectionRange = {
|
|
743
|
+
anchor: { rowIndex: 0, colIndex: 0 },
|
|
744
|
+
focus: { rowIndex: 2, colIndex: 1 },
|
|
745
|
+
}
|
|
746
|
+
const cb = createClipboard(ctx)
|
|
747
|
+
cb.copySelectionToClipboard()
|
|
748
|
+
expect(writeText).toHaveBeenCalledWith('a0\tb0\na2\tb2')
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
it('leaves out a collapsed column too, header row included', () => {
|
|
752
|
+
const ctx = makeCtx({
|
|
753
|
+
data: [
|
|
754
|
+
{ a: 'a0', b: 'b0' },
|
|
755
|
+
{ a: 'a1', b: 'b1' },
|
|
756
|
+
],
|
|
757
|
+
})
|
|
758
|
+
ctx.collapsedColumns = { b: true }
|
|
759
|
+
ctx.props.copyHeadersToClipboard = true
|
|
760
|
+
ctx.selectionRange = {
|
|
761
|
+
anchor: { rowIndex: 0, colIndex: 0 },
|
|
762
|
+
focus: { rowIndex: 1, colIndex: 1 },
|
|
763
|
+
}
|
|
764
|
+
const cb = createClipboard(ctx)
|
|
765
|
+
cb.copySelectionToClipboard()
|
|
766
|
+
expect(writeText).toHaveBeenCalledWith('a\na0\na1')
|
|
767
|
+
})
|
|
768
|
+
|
|
679
769
|
it('prepends a header row when copyHeadersToClipboard is set', () => {
|
|
680
770
|
const ctx = makeCtx({
|
|
681
771
|
data: [
|
|
@@ -849,6 +939,23 @@ describe('clearSelectedCells', () => {
|
|
|
849
939
|
expect(ctx.grid.store.setState).toHaveBeenCalled()
|
|
850
940
|
})
|
|
851
941
|
|
|
942
|
+
// The same QA pass: Delete over a filtered block wiped the rows the
|
|
943
|
+
// filter had hidden, which is data the user could not even see.
|
|
944
|
+
it('leaves a collapsed row alone, as a spreadsheet leaves a filtered one', () => {
|
|
945
|
+
const ctx = makeCtx({
|
|
946
|
+
columns: [{ id: 'a', field: 'a', editable: true, editorType: 'text' }],
|
|
947
|
+
data: [{ a: 'one' }, { a: 'two' }, { a: 'three' }],
|
|
948
|
+
})
|
|
949
|
+
ctx.isRowCollapsed = (index: number) => index === 1
|
|
950
|
+
ctx.selectionRange = {
|
|
951
|
+
anchor: { rowIndex: 0, colIndex: 0 },
|
|
952
|
+
focus: { rowIndex: 2, colIndex: 0 },
|
|
953
|
+
}
|
|
954
|
+
const cb = createClipboard(ctx)
|
|
955
|
+
expect(cb.clearSelectedCells()).toBe(true)
|
|
956
|
+
expect(ctx.internalData.map((r: { a: string }) => r.a)).toEqual(['', 'two', ''])
|
|
957
|
+
})
|
|
958
|
+
|
|
852
959
|
it('fires onCellValueChange per cleared cell (so formula engines recompute)', () => {
|
|
853
960
|
const onCellValueChange = vi.fn()
|
|
854
961
|
const ctx = makeCtx({
|