@svgrid/grid 3.0.5 → 3.0.7
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 +28 -7
- package/dist/GridMenus.svelte +34 -4
- package/dist/SvGrid.controller.svelte.d.ts +23 -3
- package/dist/SvGrid.controller.svelte.js +235 -54
- package/dist/SvGrid.css +143 -7
- package/dist/SvGrid.helpers.js +2 -4
- package/dist/SvGrid.svelte +223 -38
- package/dist/SvGrid.types.d.ts +77 -4
- 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 +81 -31
- package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-BgUS-W3o.js} +151 -142
- package/dist/cdn/GridMenus-CcwU8I4t.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-9yz-SbdZ.js} +2 -2
- package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-D7-ql1Hd.js} +2 -2
- package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DMUOFxOe.js} +4 -4
- package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-N7ik0oWy.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-CB_Jnm2k.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-Ca7gF8ZK.js} +4 -4
- package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-9GOJRHP7.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-Dl-FujN8.js} +5 -5
- package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-B1LFgoPP.js} +2 -2
- package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-By3n7DGZ.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-Mnv84zoE.js} +28 -6
- package/dist/cdn/{chart-Dz7SqMXH.js → chart-By7h2Guv.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-BC-tOgFY.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DUaXr-sb.js} +1 -1
- package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-BDtrNEiA.js} +1 -1
- package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-C1aOAPBZ.js} +1 -1
- package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
- package/dist/cdn/{editor-registry-BhgE3S84.js → editor-registry-BVeHvlZR.js} +49 -50
- package/dist/cdn/{export-format-CUDLy2yn.js → export-format-y1wdyCcv.js} +93 -97
- package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
- package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
- package/dist/cdn/{src-jcFMkBnN.js → src-25JmJ_7g.js} +4768 -4149
- package/dist/cdn/{src-Bklofsad.js → src-kE9B98r6.js} +4796 -4177
- package/dist/cdn/svgrid.js +18 -18
- package/dist/cdn/svgrid.svelte-external.js +18 -18
- package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
- package/dist/cell-formatting.d.ts +9 -0
- package/dist/cell-formatting.js +52 -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 +478 -40
- package/dist/editing.js +24 -0
- package/dist/export-format.d.ts +8 -0
- package/dist/export-format.js +23 -16
- package/dist/grid-messages.d.ts +4 -0
- package/dist/grid-messages.js +4 -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 +1 -1
- 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 +6 -0
- package/dist/row-resize.js +22 -1
- package/dist/selection.js +1 -1
- package/dist/server-block-cache.d.ts +5 -0
- package/dist/server-block-cache.js +8 -0
- package/dist/server-data-source.d.ts +35 -3
- package/dist/server-data-source.js +11 -2
- package/dist/server.d.ts +1 -1
- package/dist/sparkline.js +7 -5
- package/dist/svgrid-wrapper.types.d.ts +33 -0
- package/dist/validate.js +9 -1
- package/dist/virtualization/column-virtualizer.d.ts +1 -0
- package/dist/virtualization/svelte-virtualizer.svelte.d.ts +1 -0
- package/dist/virtualization/svelte-virtualizer.svelte.js +1 -0
- package/dist/virtualization/virtualizer.d.ts +3 -0
- package/dist/virtualization/virtualizer.js +14 -1
- package/package.json +6 -1
- package/src/GridMenus.svelte +34 -4
- package/src/SvGrid.controller.svelte.ts +236 -57
- package/src/SvGrid.css +143 -7
- package/src/SvGrid.helpers.ts +2 -4
- package/src/SvGrid.svelte +223 -38
- package/src/SvGrid.types.ts +77 -4
- 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 +85 -27
- package/src/cell-formatting.test.ts +17 -0
- package/src/cell-formatting.ts +49 -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.sort.test.ts +86 -0
- package/src/core.tick-repair.test.ts +287 -0
- package/src/core.ts +478 -40
- package/src/editing.test.ts +52 -0
- package/src/editing.ts +25 -0
- package/src/export-dates.test.ts +114 -0
- package/src/export-format.ts +26 -14
- package/src/grid-messages.test.ts +7 -0
- package/src/grid-messages.ts +10 -0
- package/src/headless.ts +24 -0
- package/src/history.test.ts +49 -1
- package/src/history.ts +48 -2
- package/src/index.ts +2 -0
- 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 +6 -0
- package/src/row-resize.ts +24 -1
- package/src/selection.ts +1 -1
- package/src/server-block-cache.test.ts +27 -0
- package/src/server-block-cache.ts +13 -0
- package/src/server-data-source.infinite.test.ts +62 -0
- package/src/server-data-source.test.ts +19 -0
- package/src/server-data-source.ts +48 -4
- package/src/server.ts +2 -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.data-push.svelte.test.ts +94 -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 +1 -1
- package/src/svgrid.live-update-paths.svelte.test.ts +186 -0
- package/src/svgrid.menu-scroll-close.test.ts +10 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +16 -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 +1 -1
- package/src/test-setup.ts +12 -0
- package/src/transaction.test.ts +58 -0
- package/src/validate.ts +7 -1
- package/src/virtualization/svelte-virtualizer.svelte.ts +1 -0
- package/src/virtualization/virtualizer.test.ts +44 -0
- package/src/virtualization/virtualizer.ts +13 -1
- package/dist/cdn/GridMenus-DLL-qZeu.js +0 -635
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({
|
package/src/clipboard.ts
CHANGED
|
@@ -100,9 +100,9 @@ export function createClipboard<
|
|
|
100
100
|
if (before !== value) steps.push({ rowId: row.id, columnId, field, before, after: value });
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function applyFillPattern() {
|
|
103
|
+
function applyFillPattern(toggled = false) {
|
|
104
104
|
const steps: HistoryStep[] = [];
|
|
105
|
-
runHistoryGroup(ctx, () => applyFillCells(steps));
|
|
105
|
+
runHistoryGroup(ctx, () => applyFillCells(steps, toggled));
|
|
106
106
|
if (steps.length) pushHistory(ctx, steps, nextGroupId());
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -117,7 +117,7 @@ export function createClipboard<
|
|
|
117
117
|
pattern: unknown[],
|
|
118
118
|
i: number,
|
|
119
119
|
sourceStart: number,
|
|
120
|
-
|
|
120
|
+
at: number,
|
|
121
121
|
step: 1 | -1,
|
|
122
122
|
axis: "rows" | "cols",
|
|
123
123
|
rowIndex: number,
|
|
@@ -126,7 +126,10 @@ export function createClipboard<
|
|
|
126
126
|
const hook = ctx.props.processCellForFill;
|
|
127
127
|
if (hook && sources.length) {
|
|
128
128
|
const k = i % sources.length;
|
|
129
|
-
|
|
129
|
+
// The real distance to the cell being filled, which is not i when a
|
|
130
|
+
// row the fill skips lies between: a formula moves by the rows it
|
|
131
|
+
// actually travelled.
|
|
132
|
+
const distance = at - (sourceStart + step * k);
|
|
130
133
|
const delta = axis === "rows" ? { rows: distance, cols: 0 } : { rows: 0, cols: distance };
|
|
131
134
|
const decided = hook({ value: sources[k], delta, rowIndex, columnId });
|
|
132
135
|
if (decided !== undefined) return decided;
|
|
@@ -169,7 +172,18 @@ export function createClipboard<
|
|
|
169
172
|
applyFillPattern();
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
|
|
175
|
+
/** The fill's values the other way round from the pattern's default. */
|
|
176
|
+
function toggledPattern(sources: unknown[], count: number): unknown[] {
|
|
177
|
+
if (sources.length === 1 && typeof sources[0] === 'number' && Number.isFinite(sources[0])) {
|
|
178
|
+
return Array.from({ length: count }, (_, i) => (sources[0] as number) + i + 1);
|
|
179
|
+
}
|
|
180
|
+
return Array.from({ length: count }, (_, i) => sources[i % sources.length]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function applyFillCells(steps: HistoryStep[], toggled = false) {
|
|
184
|
+
const pattern = (sources: unknown[], count: number) => (toggled ? toggledPattern(sources, count) : buildFillPattern(sources, count));
|
|
185
|
+
const includeCollapsed = ctx.props.includeCollapsedRows as ((rowIndex: number) => boolean) | undefined;
|
|
186
|
+
const skipsRow = (r: number) => ctx.isRowCollapsed?.(r) === true && !(includeCollapsed?.(r) ?? false);
|
|
173
187
|
const d = ctx.fillDrag;
|
|
174
188
|
if (!d) return;
|
|
175
189
|
// Clear fillDrag FIRST so a thrown error doesn't leave the grid
|
|
@@ -197,27 +211,30 @@ export function createClipboard<
|
|
|
197
211
|
for (let r = d.sourceMinRow; r <= d.sourceMaxRow; r += 1) {
|
|
198
212
|
sourceColValues.push(readCellRaw(r, column.id));
|
|
199
213
|
}
|
|
214
|
+
// A collapsed row the consumer does not count (a filtered row in a
|
|
215
|
+
// spreadsheet) is neither written nor a step of the series: the
|
|
216
|
+
// pattern runs over the rows that are there, as Excel's does.
|
|
200
217
|
if (newMaxRow > d.sourceMaxRow) {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
218
|
+
const targets: number[] = [];
|
|
219
|
+
for (let r = d.sourceMaxRow + 1; r <= newMaxRow; r += 1) if (!skipsRow(r)) targets.push(r);
|
|
220
|
+
const fills = pattern(sourceColValues, targets.length);
|
|
221
|
+
targets.forEach((targetRow, i) => {
|
|
205
222
|
if (ctx.isCellEditableAt(targetRow, c))
|
|
206
223
|
writeFilled(steps, targetRow, column.id,
|
|
207
|
-
filledValue(sourceColValues, fills, i, d.sourceMinRow,
|
|
208
|
-
}
|
|
224
|
+
filledValue(sourceColValues, fills, i, d.sourceMinRow, targetRow, 1, "rows", targetRow, column.id));
|
|
225
|
+
});
|
|
209
226
|
}
|
|
210
227
|
if (newMinRow < d.sourceMinRow) {
|
|
211
228
|
// Filling upward - reverse-extrapolate.
|
|
212
229
|
const reversed = sourceColValues.slice().reverse();
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
230
|
+
const targets: number[] = [];
|
|
231
|
+
for (let r = d.sourceMinRow - 1; r >= newMinRow; r -= 1) if (!skipsRow(r)) targets.push(r);
|
|
232
|
+
const fills = pattern(reversed, targets.length);
|
|
233
|
+
targets.forEach((targetRow, i) => {
|
|
217
234
|
if (ctx.isCellEditableAt(targetRow, c))
|
|
218
235
|
writeFilled(steps, targetRow, column.id,
|
|
219
|
-
filledValue(reversed, fills, i, d.sourceMaxRow,
|
|
220
|
-
}
|
|
236
|
+
filledValue(reversed, fills, i, d.sourceMaxRow, targetRow, -1, "rows", targetRow, column.id));
|
|
237
|
+
});
|
|
221
238
|
}
|
|
222
239
|
}
|
|
223
240
|
} else if (horizontalExtension) {
|
|
@@ -232,25 +249,25 @@ export function createClipboard<
|
|
|
232
249
|
}
|
|
233
250
|
if (newMaxCol > d.sourceMaxCol) {
|
|
234
251
|
const targetCols = newMaxCol - d.sourceMaxCol;
|
|
235
|
-
const fills =
|
|
252
|
+
const fills = pattern(sourceRowValues, targetCols);
|
|
236
253
|
for (let i = 0; i < targetCols; i += 1) {
|
|
237
254
|
const targetCol = d.sourceMaxCol + 1 + i;
|
|
238
255
|
const col = ctx.allColumns[targetCol];
|
|
239
256
|
if (col && ctx.isCellEditableAt(r, targetCol))
|
|
240
257
|
writeFilled(steps, r, col.id,
|
|
241
|
-
filledValue(sourceRowValues, fills, i, d.sourceMinCol,
|
|
258
|
+
filledValue(sourceRowValues, fills, i, d.sourceMinCol, targetCol, 1, "cols", r, col.id));
|
|
242
259
|
}
|
|
243
260
|
}
|
|
244
261
|
if (newMinCol < d.sourceMinCol) {
|
|
245
262
|
const reversed = sourceRowValues.slice().reverse();
|
|
246
263
|
const targetCols = d.sourceMinCol - newMinCol;
|
|
247
|
-
const fills =
|
|
264
|
+
const fills = pattern(reversed, targetCols);
|
|
248
265
|
for (let i = 0; i < targetCols; i += 1) {
|
|
249
266
|
const targetCol = d.sourceMinCol - 1 - i;
|
|
250
267
|
const col = ctx.allColumns[targetCol];
|
|
251
268
|
if (col && ctx.isCellEditableAt(r, targetCol))
|
|
252
269
|
writeFilled(steps, r, col.id,
|
|
253
|
-
filledValue(reversed, fills, i, d.sourceMaxCol,
|
|
270
|
+
filledValue(reversed, fills, i, d.sourceMaxCol, targetCol, -1, "cols", r, col.id));
|
|
254
271
|
}
|
|
255
272
|
}
|
|
256
273
|
}
|
|
@@ -277,8 +294,10 @@ export function createClipboard<
|
|
|
277
294
|
const minCol = Math.min(anchor.colIndex, focus.colIndex);
|
|
278
295
|
const maxCol = Math.max(anchor.colIndex, focus.colIndex);
|
|
279
296
|
for (let r = minRow; r <= maxRow; r += 1) {
|
|
297
|
+
if (ctx.isRowCollapsed?.(r)) continue;
|
|
280
298
|
for (let c = minCol; c <= maxCol; c += 1) {
|
|
281
299
|
const col = ctx.allColumns[c];
|
|
300
|
+
if (col && ctx.collapsedColumns?.[col.id]) continue;
|
|
282
301
|
if (col?.columnDef.field && ctx.isCellEditableAt(r, c)) {
|
|
283
302
|
writeCellRaw(r, col.id, null);
|
|
284
303
|
}
|
|
@@ -448,9 +467,15 @@ export function createClipboard<
|
|
|
448
467
|
ctx.fillDrag = { ...ctx.fillDrag, targetRow: hit.row, targetCol: hit.col };
|
|
449
468
|
}
|
|
450
469
|
|
|
451
|
-
|
|
470
|
+
/**
|
|
471
|
+
* The release. Excel's Ctrl turns the fill the other way: a series
|
|
472
|
+
* becomes a copy of the block, and one number, which copies on its own,
|
|
473
|
+
* becomes a series stepping by one. The modifier is read here, at the
|
|
474
|
+
* release, so holding Ctrl only at the end still counts, as in Excel.
|
|
475
|
+
*/
|
|
476
|
+
function onFillPointerUp(event?: PointerEvent) {
|
|
452
477
|
if (!ctx.fillDrag) return;
|
|
453
|
-
applyFillPattern();
|
|
478
|
+
applyFillPattern(!!(event && (event.ctrlKey || event.metaKey)));
|
|
454
479
|
}
|
|
455
480
|
|
|
456
481
|
// ---- Range move / copy (drag the selection border) --------------------
|
|
@@ -837,6 +862,7 @@ export function createClipboard<
|
|
|
837
862
|
const processCell = ctx.props.processCellForClipboard as
|
|
838
863
|
| ((params: { value: unknown; column: unknown; row: unknown; rowIndex: number; columnId: string }) => unknown)
|
|
839
864
|
| undefined;
|
|
865
|
+
const copyCollapsed = ctx.props.includeCollapsedRows as ((rowIndex: number) => boolean) | undefined;
|
|
840
866
|
const blocks: Array<string> = [];
|
|
841
867
|
for (const rect of rects) {
|
|
842
868
|
const lines: Array<string> = [];
|
|
@@ -844,6 +870,7 @@ export function createClipboard<
|
|
|
844
870
|
const header: Array<string> = [];
|
|
845
871
|
for (let c = rect.minCol; c <= rect.maxCol; c += 1) {
|
|
846
872
|
const column = ctx.allColumns[c];
|
|
873
|
+
if (column && ctx.collapsedColumns?.[column.id]) continue;
|
|
847
874
|
header.push(column ? toolPanelHeaderLabel(column) : "");
|
|
848
875
|
}
|
|
849
876
|
lines.push(header.join("\t"));
|
|
@@ -851,6 +878,13 @@ export function createClipboard<
|
|
|
851
878
|
for (let r = rect.minRow; r <= rect.maxRow; r += 1) {
|
|
852
879
|
const row = ctx.allRows[r];
|
|
853
880
|
if (!row || isGroupRow(row)) continue;
|
|
881
|
+
// A collapsed row is folded to nothing: the user cannot see it and
|
|
882
|
+
// the keyboard walks past it, so a copy leaves it out too. That is
|
|
883
|
+
// what a spreadsheet does with a filtered row, and it is what makes
|
|
884
|
+
// "filter, copy, paste" carry the rows that matched. A consumer that
|
|
885
|
+
// tells hidden from filtered answers per row (`includeCollapsedRows`):
|
|
886
|
+
// Excel copies a row hidden by hand.
|
|
887
|
+
if (ctx.isRowCollapsed?.(r) && !copyCollapsed?.(r)) continue;
|
|
854
888
|
const cells: Array<string> = [];
|
|
855
889
|
for (let c = rect.minCol; c <= rect.maxCol; c += 1) {
|
|
856
890
|
const column = ctx.allColumns[c];
|
|
@@ -858,6 +892,7 @@ export function createClipboard<
|
|
|
858
892
|
cells.push("");
|
|
859
893
|
continue;
|
|
860
894
|
}
|
|
895
|
+
if (ctx.collapsedColumns?.[column.id]) continue;
|
|
861
896
|
const base = getColumnBaseValue(row, column);
|
|
862
897
|
let value: unknown = ctx.getCellDisplayValue(row.id, column.id, base);
|
|
863
898
|
if (processCell) {
|
|
@@ -916,6 +951,9 @@ export function createClipboard<
|
|
|
916
951
|
for (let r = startRow; r <= endRow; r += 1) {
|
|
917
952
|
const row = ctx.allRows[r];
|
|
918
953
|
if (!row || isGroupRow(row)) continue;
|
|
954
|
+
// A collapsed row is not there as far as the user is concerned, and a
|
|
955
|
+
// Delete over a filtered block must not wipe what the filter hid.
|
|
956
|
+
if (ctx.isRowCollapsed?.(r)) continue;
|
|
919
957
|
// By identity, not `Number(row.id)` - that is the array index only under
|
|
920
958
|
// the default getRowId, so a custom one cleared the wrong row.
|
|
921
959
|
const dataIndex = dataIndexOf(row);
|
|
@@ -929,6 +967,7 @@ export function createClipboard<
|
|
|
929
967
|
for (let c = startCol; c <= endCol; c += 1) {
|
|
930
968
|
const column = ctx.allColumns[c];
|
|
931
969
|
if (!column?.columnDef.field) continue;
|
|
970
|
+
if (ctx.collapsedColumns?.[column.id]) continue;
|
|
932
971
|
if (!ctx.isCellEditableAt(r, c)) continue;
|
|
933
972
|
// Clear means: empty string for text, undefined for everything
|
|
934
973
|
// else. parseEditorValue handles the per-type coercion.
|
package/src/columns.test.ts
CHANGED
|
@@ -57,14 +57,14 @@ describe('cellPinStyle', () => {
|
|
|
57
57
|
it('returns a left sticky style when the column has a left offset', () => {
|
|
58
58
|
const ctx = makeCtx({ pinnedOffsets: { left: { a: 0, b: 120 }, right: {} } })
|
|
59
59
|
const h = createColumns(ctx)
|
|
60
|
-
expect(h.cellPinStyle('a')).toBe('position: sticky;
|
|
61
|
-
expect(h.cellPinStyle('b')).toBe('position: sticky;
|
|
60
|
+
expect(h.cellPinStyle('a')).toBe('position: sticky; inset-inline-start: 0px; z-index: 30;')
|
|
61
|
+
expect(h.cellPinStyle('b')).toBe('position: sticky; inset-inline-start: 120px; z-index: 30;')
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
it('returns a right sticky style when the column has a right offset', () => {
|
|
65
65
|
const ctx = makeCtx({ pinnedOffsets: { left: {}, right: { d: 0 } } })
|
|
66
66
|
const h = createColumns(ctx)
|
|
67
|
-
expect(h.cellPinStyle('d')).toBe('position: sticky;
|
|
67
|
+
expect(h.cellPinStyle('d')).toBe('position: sticky; inset-inline-end: 0px; z-index: 30;')
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
it('returns empty string for an unpinned column', () => {
|
|
@@ -75,7 +75,7 @@ describe('cellPinStyle', () => {
|
|
|
75
75
|
it('prefers the left offset when a column appears in both maps', () => {
|
|
76
76
|
const ctx = makeCtx({ pinnedOffsets: { left: { a: 5 }, right: { a: 9 } } })
|
|
77
77
|
const h = createColumns(ctx)
|
|
78
|
-
expect(h.cellPinStyle('a')).toBe('position: sticky;
|
|
78
|
+
expect(h.cellPinStyle('a')).toBe('position: sticky; inset-inline-start: 5px; z-index: 30;')
|
|
79
79
|
})
|
|
80
80
|
})
|
|
81
81
|
|
package/src/columns.ts
CHANGED
|
@@ -16,13 +16,18 @@ export function createColumns<
|
|
|
16
16
|
// context (z=20) so a selected cell in the scrollable middle never
|
|
17
17
|
// bleeds over the pinned columns during horizontal scroll. Still
|
|
18
18
|
// sits below dropdowns (z=900+) and the column menu.
|
|
19
|
+
// Logical insets, not `left` / `right`: the offsets are measured from the
|
|
20
|
+
// pinned side in READING order, so under `dir="rtl"` the left-pinned
|
|
21
|
+
// columns have to stick to the right edge. `inset-inline-start` is that
|
|
22
|
+
// edge in either direction; with `left` a right-to-left grid scrolls its
|
|
23
|
+
// pinned columns off the screen.
|
|
19
24
|
const leftOffset = ctx.pinnedOffsets.left[columnId];
|
|
20
25
|
if (leftOffset !== undefined) {
|
|
21
|
-
return `position: sticky;
|
|
26
|
+
return `position: sticky; inset-inline-start: ${leftOffset}px; z-index: 30;`;
|
|
22
27
|
}
|
|
23
28
|
const rightOffset = ctx.pinnedOffsets.right[columnId];
|
|
24
29
|
if (rightOffset !== undefined) {
|
|
25
|
-
return `position: sticky;
|
|
30
|
+
return `position: sticky; inset-inline-end: ${rightOffset}px; z-index: 30;`;
|
|
26
31
|
}
|
|
27
32
|
return "";
|
|
28
33
|
}
|
|
@@ -83,6 +83,14 @@ describe('resolveCellFormat', () => {
|
|
|
83
83
|
expect(resolveCellFormat(1, {}, 'c', fmts, null).background).toBeUndefined()
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
+
it('rule carries its icon while it matches', () => {
|
|
87
|
+
const fmts: ConditionalFormat[] = [
|
|
88
|
+
{ type: 'rule', when: ({ value }) => Number(value) > 100, background: '#dbeafe', icon: '!' },
|
|
89
|
+
]
|
|
90
|
+
expect(resolveCellFormat(150, {}, 'c', fmts, null).icon).toBe('!')
|
|
91
|
+
expect(resolveCellFormat(50, {}, 'c', fmts, null).icon).toBeUndefined()
|
|
92
|
+
})
|
|
93
|
+
|
|
86
94
|
it('respects the columns scope', () => {
|
|
87
95
|
const fmts: ConditionalFormat[] = [
|
|
88
96
|
{ type: 'colorScale', min: '#000000', max: '#ffffff', columns: ['other'] },
|
|
@@ -102,6 +102,8 @@ export type RuleFormat<TData = unknown> = {
|
|
|
102
102
|
background?: string
|
|
103
103
|
color?: string
|
|
104
104
|
fontWeight?: string | number
|
|
105
|
+
/** A short glyph drawn before the value while the rule matches. */
|
|
106
|
+
icon?: string
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
export type ConditionalFormatSpec<TData = unknown> =
|
|
@@ -323,6 +325,7 @@ export function resolveCellFormat<TData = unknown>(
|
|
|
323
325
|
}
|
|
324
326
|
if (fmt.color) out.color = fmt.color
|
|
325
327
|
if (fmt.fontWeight != null) out.fontWeight = fmt.fontWeight
|
|
328
|
+
if (fmt.icon) out.icon = fmt.icon
|
|
326
329
|
}
|
|
327
330
|
continue
|
|
328
331
|
}
|
package/src/core.sort.test.ts
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* snapshot the new output, `referenceSort` below is a literal transcription of
|
|
13
13
|
* the ORIGINAL comparator, and every case asserts the two agree. If the rewrite
|
|
14
14
|
* ever diverges on any input, including the randomised ones, this fails.
|
|
15
|
+
*
|
|
16
|
+
* The oracle calls the pairwise `sortFns`, so it tracks their semantics: since
|
|
17
|
+
* #104 those put blanks and unparseable values first instead of returning NaN,
|
|
18
|
+
* and `auto` compares numbers numerically.
|
|
15
19
|
*/
|
|
16
20
|
import { describe, expect, it } from 'vitest'
|
|
17
21
|
// From './core' rather than './index': `createSvGridCore` is the runes-free
|
|
@@ -233,6 +237,88 @@ describe('createSortedRowModel - equivalence with the original comparator', () =
|
|
|
233
237
|
})
|
|
234
238
|
})
|
|
235
239
|
|
|
240
|
+
describe('built-in comparators keep the Array.sort contract (#104)', () => {
|
|
241
|
+
it('number: non-numeric values sort together ahead of the numbers', () => {
|
|
242
|
+
expect([5, 'n/a', 3, 1, 'n/a', 4, 2].sort(sortFns.number)).toEqual(['n/a', 'n/a', 1, 2, 3, 4, 5])
|
|
243
|
+
expect(sortFns.number('n/a', 'x')).toBe(0)
|
|
244
|
+
expect(Number.isNaN(sortFns.number('n/a', 3))).toBe(false)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('date: blanks and invalid dates sort first and never return NaN', () => {
|
|
248
|
+
// null, not undefined: Array.prototype.sort moves undefined to the end
|
|
249
|
+
// without calling the comparator.
|
|
250
|
+
const sorted = ['2021-01-01', null, 'junk', '2020-01-01'].sort(sortFns.date)
|
|
251
|
+
expect(sorted).toEqual([null, 'junk', '2020-01-01', '2021-01-01'])
|
|
252
|
+
expect(sortFns.date(undefined, 'junk')).toBe(0)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('auto: numbers compare numerically, blanks come first', () => {
|
|
256
|
+
expect([2, 10, 1, 20, 100, 3].sort(sortFns.auto)).toEqual([1, 2, 3, 10, 20, 100])
|
|
257
|
+
expect(['oak', null, 'nuclear', '', 'apple'].sort(sortFns.auto)).toEqual([
|
|
258
|
+
null, '', 'apple', 'nuclear', 'oak',
|
|
259
|
+
])
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('every built-in comparator is antisymmetric and transitive on mixed values', () => {
|
|
263
|
+
const pool: unknown[] = [
|
|
264
|
+
null, undefined, '', 0, -1, 2, 10, NaN, 1e21, '1a', '2', '10', 'n/a', 'apple', 'Apple',
|
|
265
|
+
true, '2020-01-01', 'junk', 1600000000000,
|
|
266
|
+
]
|
|
267
|
+
for (const [name, cmp] of Object.entries(sortFns)) {
|
|
268
|
+
const sign = (a: unknown, b: unknown) => Math.sign(cmp(a, b))
|
|
269
|
+
for (const a of pool) {
|
|
270
|
+
for (const b of pool) {
|
|
271
|
+
const ab = sign(a, b)
|
|
272
|
+
expect(Number.isNaN(ab), `${name}(${String(a)}, ${String(b)}) is NaN`).toBe(false)
|
|
273
|
+
expect(ab, `${name} antisymmetry on ${String(a)}, ${String(b)}`).toBe(-sign(b, a) || 0)
|
|
274
|
+
for (const c of pool) {
|
|
275
|
+
if (ab <= 0 && sign(b, c) <= 0) {
|
|
276
|
+
expect(sign(a, c), `${name}: ${String(a)} <= ${String(b)} <= ${String(c)}`).toBeLessThanOrEqual(0)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('sorts an untyped numeric column numerically in the row model', () => {
|
|
285
|
+
const rows: Row[] = [2, 10, null, 1, 20, 100, 3].map((qty) => ({ qty }))
|
|
286
|
+
const cols = [{ field: 'qty' }]
|
|
287
|
+
expect(actualSort(rows, cols, [{ id: 'qty', desc: false }]).map((r) => r.qty)).toEqual([
|
|
288
|
+
null, 1, 2, 3, 10, 20, 100,
|
|
289
|
+
])
|
|
290
|
+
expect(actualSort(rows, cols, [{ id: 'qty', desc: true }]).map((r) => r.qty)).toEqual([
|
|
291
|
+
100, 20, 10, 3, 2, 1, null,
|
|
292
|
+
])
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('sorts a number column with placeholder text in order', () => {
|
|
296
|
+
const rows: Row[] = [5, 'n/a', 3, 1, 'n/a', 4, 2].map((v) => ({ v }))
|
|
297
|
+
const sorted = actualSort(rows, [{ field: 'v', editorType: 'number' }], [{ id: 'v', desc: false }])
|
|
298
|
+
expect(sorted.map((r) => r.v)).toEqual(['n/a', 'n/a', 1, 2, 3, 4, 5])
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('orders a mixed number and text column by kind, then value', () => {
|
|
302
|
+
const rows: Row[] = [10, 'b', 2, null, 'a', 1].map((v) => ({ v }))
|
|
303
|
+
const sorted = actualSort(rows, [{ field: 'v' }], [{ id: 'v', desc: false }])
|
|
304
|
+
expect(sorted.map((r) => r.v)).toEqual([null, 1, 2, 10, 'a', 'b'])
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('lets the next clause decide when two blanks tie on a numeric clause', () => {
|
|
308
|
+
// Two -Infinity keys subtract to NaN; the multi-clause loop must see 0.
|
|
309
|
+
const rows: Row[] = [
|
|
310
|
+
{ num: null, text: 'b' },
|
|
311
|
+
{ num: 'n/a', text: 'a' },
|
|
312
|
+
{ num: 1, text: 'c' },
|
|
313
|
+
]
|
|
314
|
+
const sorted = actualSort(rows, COLUMNS, [
|
|
315
|
+
{ id: 'num', desc: false },
|
|
316
|
+
{ id: 'text', desc: false },
|
|
317
|
+
])
|
|
318
|
+
expect(sorted.map((r) => r.text)).toEqual(['a', 'b', 'c'])
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
236
322
|
describe('createSortedRowModel - work budget', () => {
|
|
237
323
|
/** Count `getAllColumns` calls the way tools/bench does, but in-process. */
|
|
238
324
|
function countColumnLookups(rowCount: number, sorting: SortingState): number {
|