@svgrid/grid 3.0.2 → 3.0.4
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 +2 -1
- package/dist/GridMenus.svelte +44 -2
- package/dist/SvChartMenu.svelte +69 -0
- package/dist/SvChartMenu.svelte.d.ts +11 -0
- package/dist/SvChartPanes.svelte +113 -0
- package/dist/SvChartPanes.svelte.d.ts +20 -0
- package/dist/SvGrid.controller.svelte.d.ts +110 -39
- package/dist/SvGrid.controller.svelte.js +655 -200
- package/dist/SvGrid.css +105 -2
- package/dist/SvGrid.svelte +386 -514
- package/dist/SvGrid.types.d.ts +210 -9
- package/dist/SvGridCellEditor.svelte +17 -0
- package/dist/SvGridChart.svelte +2257 -329
- package/dist/SvGridChart.svelte.d.ts +6 -106
- package/dist/SvGridChart.types.d.ts +338 -0
- package/dist/SvGridChart.types.js +1 -0
- package/dist/SvGridChartBuilder.svelte +323 -0
- package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
- package/dist/SvGridChartPanel.svelte +221 -275
- package/dist/SvGridChartPickers.svelte +417 -0
- package/dist/SvGridChartPickers.svelte.d.ts +70 -0
- package/dist/SvGridChartView.svelte +13 -14
- package/dist/SvModal.svelte +7 -1
- package/dist/SvModal.svelte.d.ts +5 -0
- package/dist/ai.d.ts +40 -8
- package/dist/ai.js +135 -24
- package/dist/aria.d.ts +11 -0
- package/dist/build-api.js +108 -22
- package/dist/cdn/GridMenus-7kbpnnBW.js +635 -0
- package/dist/cdn/GridMenus-BL7ZgQvU.js +636 -0
- package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
- package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
- package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
- package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
- package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
- package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
- package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
- package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
- package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
- package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
- package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
- package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
- package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
- package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
- package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
- package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
- package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
- package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
- package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
- package/dist/cdn/SvModal-CIZWCcad.js +397 -0
- package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
- package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
- package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
- package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
- package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
- package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
- package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
- package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
- package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
- package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
- package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
- package/dist/cdn/dismissable-DAHetSNk.js +44 -0
- package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
- package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
- package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
- package/dist/cdn/{src-DNbz94Ct.js → src-BKhZ6eXd.js} +10227 -9787
- package/dist/cdn/{src-BJ2opd0j.js → src-V1uu8iE9.js} +6568 -6128
- package/dist/cdn/svgrid.js +26 -15
- package/dist/cdn/svgrid.svelte-external.js +26 -15
- package/dist/chart-axes.d.ts +53 -0
- package/dist/chart-axes.js +351 -0
- package/dist/chart-cartesian.d.ts +88 -0
- package/dist/chart-cartesian.js +1862 -0
- package/dist/chart-decimate.d.ts +51 -0
- package/dist/chart-decimate.js +199 -0
- package/dist/chart-export-pdf.d.ts +47 -0
- package/dist/chart-export-pdf.js +187 -0
- package/dist/chart-export.d.ts +2 -1
- package/dist/chart-export.js +18 -3
- package/dist/chart-financial.d.ts +91 -0
- package/dist/chart-financial.js +175 -0
- package/dist/chart-flow.d.ts +10 -0
- package/dist/chart-flow.js +298 -0
- package/dist/chart-format.d.ts +18 -0
- package/dist/chart-format.js +65 -0
- package/dist/chart-grid.d.ts +8 -0
- package/dist/chart-grid.js +190 -0
- package/dist/chart-hierarchy.d.ts +17 -0
- package/dist/chart-hierarchy.js +146 -0
- package/dist/chart-indicators.d.ts +115 -0
- package/dist/chart-indicators.js +389 -0
- package/dist/chart-messages.d.ts +95 -0
- package/dist/chart-messages.js +92 -0
- package/dist/chart-motion.d.ts +16 -0
- package/dist/chart-motion.js +141 -0
- package/dist/chart-panel-messages.d.ts +265 -0
- package/dist/chart-panel-messages.js +285 -0
- package/dist/chart-pivot.d.ts +87 -0
- package/dist/chart-pivot.js +156 -0
- package/dist/chart-polar.d.ts +25 -0
- package/dist/chart-polar.js +700 -0
- package/dist/chart-samples.d.ts +16 -0
- package/dist/chart-samples.js +153 -0
- package/dist/chart-scale.d.ts +113 -0
- package/dist/chart-scale.js +405 -0
- package/dist/chart-stats.d.ts +111 -0
- package/dist/chart-stats.js +419 -0
- package/dist/chart-stream.d.ts +45 -0
- package/dist/chart-stream.js +91 -0
- package/dist/chart-summary.d.ts +27 -0
- package/dist/chart-summary.js +205 -0
- package/dist/chart-sync.svelte.d.ts +32 -0
- package/dist/chart-sync.svelte.js +23 -0
- package/dist/chart-table.d.ts +35 -0
- package/dist/chart-table.js +165 -0
- package/dist/chart-trend.d.ts +19 -0
- package/dist/chart-trend.js +61 -0
- package/dist/chart-types.d.ts +1456 -0
- package/dist/chart-types.js +6 -0
- package/dist/chart-validate.d.ts +43 -0
- package/dist/chart-validate.js +272 -0
- package/dist/chart-zoom.d.ts +40 -0
- package/dist/chart-zoom.js +145 -0
- package/dist/chart.d.ts +186 -865
- package/dist/chart.js +460 -2204
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +137 -18
- package/dist/column-resize.d.ts +3 -0
- package/dist/column-resize.js +4 -1
- package/dist/columns.js +3 -0
- package/dist/command-context.d.ts +19 -0
- package/dist/command-context.js +116 -0
- package/dist/core.d.ts +7 -0
- package/dist/createPopoverSelect.svelte.d.ts +2 -2
- package/dist/editing.d.ts +2 -8
- package/dist/editing.js +262 -33
- package/dist/fill-patterns.d.ts +0 -5
- package/dist/fill-patterns.js +56 -2
- package/dist/grid-messages.d.ts +10 -3
- package/dist/grid-messages.js +5 -0
- package/dist/history.d.ts +83 -0
- package/dist/history.js +133 -0
- package/dist/index.d.ts +24 -6
- package/dist/index.js +20 -2
- package/dist/keyboard-handlers.js +88 -31
- package/dist/keyboard.d.ts +55 -0
- package/dist/keyboard.js +132 -0
- package/dist/menus.d.ts +3 -1
- package/dist/menus.js +7 -2
- package/dist/merges.d.ts +97 -0
- package/dist/merges.js +147 -0
- package/dist/row-resize.d.ts +3 -0
- package/dist/row-resize.js +14 -0
- package/dist/scroll-sync.d.ts +1 -0
- package/dist/scroll-sync.js +24 -0
- package/dist/selection.d.ts +16 -0
- package/dist/selection.js +108 -16
- package/dist/shortcut-registry.d.ts +145 -0
- package/dist/shortcut-registry.js +44 -0
- package/dist/svgrid-wrapper.types.d.ts +213 -22
- package/dist/virtualization/virtualizer.js +38 -17
- package/package.json +6 -1
- package/src/GridMenus.svelte +44 -2
- package/src/SvChartMenu.svelte +69 -0
- package/src/SvChartPanes.svelte +113 -0
- package/src/SvChartPanes.test.ts +66 -0
- package/src/SvGrid.controller.svelte.ts +629 -172
- package/src/SvGrid.css +105 -2
- package/src/SvGrid.svelte +386 -514
- package/src/SvGrid.types.ts +188 -8
- package/src/SvGridCellEditor.svelte +17 -0
- package/src/SvGridChart.svelte +2257 -329
- package/src/SvGridChart.test.ts +1419 -1
- package/src/SvGridChart.types.ts +336 -0
- package/src/SvGridChartBuilder.svelte +323 -0
- package/src/SvGridChartPanel.svelte +221 -275
- package/src/SvGridChartPickers.svelte +417 -0
- package/src/SvGridChartView.svelte +13 -14
- package/src/SvModal.svelte +7 -1
- package/src/SvModal.test.ts +9 -0
- package/src/ai.test.ts +57 -2
- package/src/ai.ts +166 -22
- package/src/aria.d.ts +11 -0
- package/src/build-api.coverage.test.ts +184 -0
- package/src/build-api.ts +107 -18
- package/src/chart-axes.test.ts +573 -0
- package/src/chart-axes.ts +342 -0
- package/src/chart-cartesian.ts +1863 -0
- package/src/chart-decimate.test.ts +249 -0
- package/src/chart-decimate.ts +186 -0
- package/src/chart-export-pdf.test.ts +91 -0
- package/src/chart-export-pdf.ts +207 -0
- package/src/chart-export.test.ts +14 -0
- package/src/chart-export.ts +13 -3
- package/src/chart-financial.ts +208 -0
- package/src/chart-flow.ts +286 -0
- package/src/chart-format.ts +67 -0
- package/src/chart-grid.ts +179 -0
- package/src/chart-hierarchy.ts +135 -0
- package/src/chart-indicators.test.ts +443 -0
- package/src/chart-indicators.ts +384 -0
- package/src/chart-messages.test.ts +21 -0
- package/src/chart-messages.ts +184 -0
- package/src/chart-motion.test.ts +71 -0
- package/src/chart-motion.ts +148 -0
- package/src/chart-panel-messages.test.ts +29 -0
- package/src/chart-panel-messages.ts +550 -0
- package/src/chart-pivot.test.ts +54 -0
- package/src/chart-pivot.ts +225 -0
- package/src/chart-polar.ts +674 -0
- package/src/chart-samples.ts +106 -0
- package/src/chart-scale.ts +394 -0
- package/src/chart-series-types.test.ts +714 -0
- package/src/chart-stats.ts +409 -0
- package/src/chart-stream.test.ts +75 -0
- package/src/chart-stream.ts +114 -0
- package/src/chart-summary.test.ts +90 -0
- package/src/chart-summary.ts +197 -0
- package/src/chart-sync.svelte.ts +41 -0
- package/src/chart-table.test.ts +76 -0
- package/src/chart-table.ts +178 -0
- package/src/chart-trend.ts +47 -0
- package/src/chart-types.ts +1309 -0
- package/src/chart-validate.test.ts +100 -0
- package/src/chart-validate.ts +247 -0
- package/src/chart-zoom.test.ts +137 -0
- package/src/chart-zoom.ts +124 -0
- package/src/chart.coverage.test.ts +81 -0
- package/src/chart.test.ts +56 -3
- package/src/chart.ts +557 -2784
- package/src/clipboard.test.ts +151 -0
- package/src/clipboard.ts +145 -16
- package/src/column-resize.ts +7 -1
- package/src/columns.ts +2 -0
- package/src/command-context.test.ts +200 -0
- package/src/command-context.ts +137 -0
- package/src/core.ts +7 -0
- package/src/editing.test.ts +281 -3
- package/src/editing.ts +256 -38
- package/src/fill-patterns.test.ts +35 -0
- package/src/fill-patterns.ts +56 -2
- package/src/grid-messages.ts +16 -3
- package/src/history.test.ts +196 -0
- package/src/history.ts +162 -0
- package/src/icon-seam.test.ts +3 -2
- package/src/index.ts +150 -2
- package/src/keyboard-handlers.coverage.test.ts +126 -6
- package/src/keyboard-handlers.ts +91 -30
- package/src/keyboard-shortcuts.seam.test.ts +200 -0
- package/src/keyboard.test.ts +124 -1
- package/src/keyboard.ts +142 -0
- package/src/menus.test.ts +26 -0
- package/src/menus.ts +10 -3
- package/src/merges.test.ts +112 -0
- package/src/merges.ts +168 -0
- package/src/pivot.grid.test.ts +40 -5
- package/src/row-resize.test.ts +44 -0
- package/src/row-resize.ts +17 -0
- package/src/scroll-sync.test.ts +42 -1
- package/src/scroll-sync.ts +25 -0
- package/src/selection.multi-range.test.ts +34 -0
- package/src/selection.test.ts +30 -2
- package/src/selection.ts +111 -19
- package/src/shortcut-registry.test.ts +97 -0
- package/src/shortcut-registry.ts +181 -0
- package/src/svgrid-wrapper.types.ts +184 -13
- package/src/svgrid.behavior.test.ts +135 -0
- package/src/svgrid.charting.test.ts +513 -3
- package/src/svgrid.context-menu.test.ts +31 -0
- package/src/svgrid.new-features.wrapper.test.ts +10 -3
- package/src/virtualization/virtualizer.ts +39 -17
- package/dist/cdn/GridMenus-DyFA-QxA.js +0 -602
- package/dist/cdn/GridMenus-M2XP9t0_.js +0 -603
- package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
- package/dist/cdn/SvGridChart-DAb9RbRs.js +0 -1480
- package/dist/cdn/SvGridChart-Yl-IXZFO.js +0 -1481
- package/dist/cdn/SvGridChartPanel-DayQpDsR.js +0 -834
- package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +0 -810
- package/dist/cdn/SvGridChartView-CxfakxR0.js +0 -56
- package/dist/cdn/SvGridChartView-Dm_YEx9L.js +0 -55
- package/dist/cdn/chart-CKJRCPOk.js +0 -1652
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the `GridCommandContext` handed to registered keyboard commands.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately a getter object over the controller `ctx` rather than a
|
|
5
|
+
* snapshot: it is constructed on every keystroke that reaches a registered
|
|
6
|
+
* handler, so copying row and column arrays into it would put an allocation
|
|
7
|
+
* proportional to the grid in the hot path. Getters make construction O(1) and
|
|
8
|
+
* charge only for what a handler actually reads.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is display-indexed, matching `SelectionPoint` and the rest of
|
|
11
|
+
* the selection model, so indices follow sort, filter and pagination. That is
|
|
12
|
+
* why `getCellValue` / `setCellValue` exist here rather than being left to
|
|
13
|
+
* `api.getCellValue`: the api pair indexes the DATA array, so handing a handler
|
|
14
|
+
* a selection index and an api that reads a different index space would give
|
|
15
|
+
* the wrong cell the moment the grid is sorted or filtered. `api.refresh()` has
|
|
16
|
+
* no index in it, so that one is left to the api.
|
|
17
|
+
*/
|
|
18
|
+
import type { GridCommandContext } from "./shortcut-registry";
|
|
19
|
+
import { pushHistory, runHistoryGroup } from "./history";
|
|
20
|
+
|
|
21
|
+
export function buildCommandContext(ctx: any, editing: boolean): GridCommandContext {
|
|
22
|
+
function columnIdAt(colIndex: number): string | null {
|
|
23
|
+
return ctx.allColumns[colIndex]?.id ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
get api() {
|
|
28
|
+
return ctx.buildApi();
|
|
29
|
+
},
|
|
30
|
+
editing,
|
|
31
|
+
get editor() {
|
|
32
|
+
const root: HTMLElement | null = ctx.gridRootEl ?? null;
|
|
33
|
+
const el = root?.querySelector<HTMLInputElement | HTMLTextAreaElement>(".sv-grid-cell-editor") ?? null;
|
|
34
|
+
return el;
|
|
35
|
+
},
|
|
36
|
+
get activeCell() {
|
|
37
|
+
const active = ctx.grid.getState().activeCell;
|
|
38
|
+
if (!active) return null;
|
|
39
|
+
return {
|
|
40
|
+
rowIndex: active.rowIndex,
|
|
41
|
+
colIndex: active.colIndex,
|
|
42
|
+
columnId: columnIdAt(active.colIndex),
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
get selectionFocus() {
|
|
46
|
+
const focus = ctx.selectionRange?.focus;
|
|
47
|
+
return focus ? { rowIndex: focus.rowIndex, colIndex: focus.colIndex } : null;
|
|
48
|
+
},
|
|
49
|
+
get rowCount() {
|
|
50
|
+
return ctx.allRows.length;
|
|
51
|
+
},
|
|
52
|
+
get colCount() {
|
|
53
|
+
return ctx.allColumns.length;
|
|
54
|
+
},
|
|
55
|
+
get ranges() {
|
|
56
|
+
// getSelectionRects() already normalises anchor/focus into rectangles
|
|
57
|
+
// and puts the active range last, which is the order a handler wants
|
|
58
|
+
// when it has to pick one.
|
|
59
|
+
return ctx.getSelectionRects().map(
|
|
60
|
+
(r: { minRow: number; minCol: number; maxRow: number; maxCol: number }) =>
|
|
61
|
+
[r.minRow, r.minCol, r.maxRow, r.maxCol] as const,
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
columnIdAt,
|
|
65
|
+
getCellValue(rowIndex: number, colIndex: number) {
|
|
66
|
+
const columnId = columnIdAt(colIndex);
|
|
67
|
+
return columnId == null ? undefined : ctx.readCellRaw(rowIndex, columnId);
|
|
68
|
+
},
|
|
69
|
+
setCellValue(rowIndex: number, colIndex: number, value: unknown) {
|
|
70
|
+
const columnId = columnIdAt(colIndex);
|
|
71
|
+
if (columnId == null) return;
|
|
72
|
+
// Record the write so Ctrl+Z walks it back. `writeCellRaw` is the raw
|
|
73
|
+
// writer and deliberately keeps no history of its own - the fill handle
|
|
74
|
+
// and paste call it in loops and push their own entries (or, today, none
|
|
75
|
+
// at all). A command has to push here or every multi-cell command it
|
|
76
|
+
// runs would be silently un-undoable.
|
|
77
|
+
//
|
|
78
|
+
// Both reads happen BEFORE the write: writeCellRaw swaps a fresh row
|
|
79
|
+
// object into `internalData`, so afterwards the id lookup would miss.
|
|
80
|
+
const field = ctx.allColumns[colIndex]?.columnDef?.field;
|
|
81
|
+
const rowId = ctx.allRows[rowIndex]?.id;
|
|
82
|
+
const before = ctx.readCellRaw(rowIndex, columnId);
|
|
83
|
+
ctx.writeCellRaw(rowIndex, columnId, value);
|
|
84
|
+
// Only when something actually changed: writeCellRaw no-ops on an equal
|
|
85
|
+
// value, and a step whose before and after match would make Ctrl+Z look
|
|
86
|
+
// like it had swallowed a press.
|
|
87
|
+
if (field != null && rowId != null && before !== value) {
|
|
88
|
+
pushHistory(ctx, [{ rowId, columnId, field, before, after: value }]);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
setActiveCell(rowIndex: number, colIndex: number) {
|
|
92
|
+
ctx.setActiveCell(rowIndex, colIndex);
|
|
93
|
+
},
|
|
94
|
+
setSelection(rowIndex: number, colIndex: number) {
|
|
95
|
+
ctx.setSelection(rowIndex, colIndex);
|
|
96
|
+
},
|
|
97
|
+
extendSelection(rowIndex: number, colIndex: number) {
|
|
98
|
+
ctx.extendSelection(rowIndex, colIndex);
|
|
99
|
+
},
|
|
100
|
+
scrollIntoView(rowIndex: number, colIndex: number) {
|
|
101
|
+
ctx.scrollActiveCellIntoView(rowIndex, colIndex);
|
|
102
|
+
},
|
|
103
|
+
startEditing(rowIndex: number, colIndex: number, seed?: string) {
|
|
104
|
+
if (seed !== undefined) {
|
|
105
|
+
return ctx.startEditingWithChar(rowIndex, colIndex, seed) === true;
|
|
106
|
+
}
|
|
107
|
+
ctx.onCellDoubleClick(rowIndex, colIndex);
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
110
|
+
batch<T>(fn: () => T): T {
|
|
111
|
+
return runHistoryGroup(ctx, fn);
|
|
112
|
+
},
|
|
113
|
+
recordUndo(undo: () => void, redo: () => void) {
|
|
114
|
+
pushHistory(ctx, [
|
|
115
|
+
{ rowId: "", columnId: "", field: "", before: undefined, after: undefined, custom: { undo, redo } },
|
|
116
|
+
]);
|
|
117
|
+
},
|
|
118
|
+
focus() {
|
|
119
|
+
// preventScroll: the grid keeps its own scroll position, and a focus
|
|
120
|
+
// that yanked the viewport to the root's top edge would undo whatever
|
|
121
|
+
// scrollIntoView the command just asked for.
|
|
122
|
+
ctx.gridRootEl?.focus({ preventScroll: true });
|
|
123
|
+
},
|
|
124
|
+
async paste() {
|
|
125
|
+
await ctx.pasteFromClipboard?.();
|
|
126
|
+
},
|
|
127
|
+
copy() {
|
|
128
|
+
ctx.copySelectionToClipboard?.();
|
|
129
|
+
},
|
|
130
|
+
async cut() {
|
|
131
|
+
await ctx.cutSelectionToClipboard?.();
|
|
132
|
+
},
|
|
133
|
+
canEdit(rowIndex: number, colIndex: number) {
|
|
134
|
+
return ctx.isCellEditableAt?.(rowIndex, colIndex) ?? true;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
package/src/core.ts
CHANGED
|
@@ -468,6 +468,13 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
468
468
|
| ((row: TData) => EditorOptionSource | Promise<EditorOptionSource>)
|
|
469
469
|
/** When true, list/chips allow multiple selections. Cell value becomes an array. */
|
|
470
470
|
editorMultiple?: boolean
|
|
471
|
+
/**
|
|
472
|
+
* The text editor accepts line breaks: Alt+Enter inserts one and the
|
|
473
|
+
* editor grows a line per break, while Enter still commits. What a
|
|
474
|
+
* spreadsheet cell with wrapped text wants; `editorType: 'textarea'` is
|
|
475
|
+
* the other shape, where Enter itself is the line break.
|
|
476
|
+
*/
|
|
477
|
+
editorMultiline?: boolean
|
|
471
478
|
/** Separator used when joining array values for the readonly cell display. Defaults to ', '. */
|
|
472
479
|
editorSeparator?: string
|
|
473
480
|
format?: CellFormatConfig
|
package/src/editing.test.ts
CHANGED
|
@@ -77,6 +77,11 @@ function makeCtx(opts: FakeOptions = {}) {
|
|
|
77
77
|
editorSelectAll: false,
|
|
78
78
|
editedCellValues: {},
|
|
79
79
|
selectionRange: { anchor: null, focus: null },
|
|
80
|
+
tabRunOrigin: null,
|
|
81
|
+
activeRangeRect: () => null,
|
|
82
|
+
isRowCollapsed: () => false,
|
|
83
|
+
collapsedColumns: {},
|
|
84
|
+
isCellInSelectedRange: () => false,
|
|
80
85
|
gridRootEl: null,
|
|
81
86
|
props: {},
|
|
82
87
|
history: [] as any[],
|
|
@@ -551,9 +556,10 @@ describe('updateEditingCellValue', () => {
|
|
|
551
556
|
})
|
|
552
557
|
|
|
553
558
|
describe('onEditorKeyDown', () => {
|
|
554
|
-
function fakeEvent(key: string) {
|
|
559
|
+
function fakeEvent(key: string, init: Partial<KeyboardEvent> = {}) {
|
|
555
560
|
return {
|
|
556
561
|
key,
|
|
562
|
+
...init,
|
|
557
563
|
stopPropagation: vi.fn(),
|
|
558
564
|
preventDefault: vi.fn(),
|
|
559
565
|
} as unknown as KeyboardEvent
|
|
@@ -576,6 +582,74 @@ describe('onEditorKeyDown', () => {
|
|
|
576
582
|
expect(focus).toHaveBeenCalledWith({ preventScroll: true })
|
|
577
583
|
})
|
|
578
584
|
|
|
585
|
+
it('Enter saves and moves the active cell one row down, Shift+Enter one up', () => {
|
|
586
|
+
// Typing a column of values: Enter after each one lands on the next row,
|
|
587
|
+
// as in every spreadsheet, rather than leaving the cursor on the cell
|
|
588
|
+
// just committed. The last row is a wall, not a wrap.
|
|
589
|
+
const { ctx, ed } = editingFor({
|
|
590
|
+
columns: [{ id: 'a', field: 'a', editorType: 'text' }],
|
|
591
|
+
data: [{ a: 'a0' }, { a: 'a1' }, { a: 'a2' }],
|
|
592
|
+
})
|
|
593
|
+
ctx.gridRootEl = { focus: vi.fn() }
|
|
594
|
+
ed.onCellDoubleClick(0, 0)
|
|
595
|
+
ctx.editingCell = { ...ctx.editingCell, value: 'new' }
|
|
596
|
+
ed.onEditorKeyDown(fakeEvent('Enter'))
|
|
597
|
+
expect(ctx.internalData[0].a).toBe('new')
|
|
598
|
+
expect(ctx.activeCalls.at(-1)).toEqual([1, 0])
|
|
599
|
+
|
|
600
|
+
ed.onCellDoubleClick(1, 0)
|
|
601
|
+
const up = { ...fakeEvent('Enter'), shiftKey: true } as unknown as KeyboardEvent
|
|
602
|
+
;(up as any).preventDefault = vi.fn()
|
|
603
|
+
;(up as any).stopPropagation = vi.fn()
|
|
604
|
+
ed.onEditorKeyDown(up)
|
|
605
|
+
expect(ctx.activeCalls.at(-1)).toEqual([0, 0])
|
|
606
|
+
|
|
607
|
+
ed.onCellDoubleClick(2, 0)
|
|
608
|
+
ed.onEditorKeyDown(fakeEvent('Enter'))
|
|
609
|
+
expect(ctx.activeCalls.at(-1)).toEqual([2, 0])
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
it('Alt+Enter puts a line break into a multiline editor instead of committing', () => {
|
|
613
|
+
const { ctx, ed } = editingFor({
|
|
614
|
+
columns: [{ id: 'a', field: 'a', editorType: 'text' }],
|
|
615
|
+
data: [{ a: 'old' }],
|
|
616
|
+
})
|
|
617
|
+
ctx.gridRootEl = { focus: vi.fn() }
|
|
618
|
+
ctx.editingCell = { rowId: '0', columnId: 'a', editorType: 'text', value: 'ab' }
|
|
619
|
+
const area = document.createElement('textarea')
|
|
620
|
+
area.value = 'ab'
|
|
621
|
+
area.selectionStart = area.selectionEnd = 1
|
|
622
|
+
const ev = {
|
|
623
|
+
key: 'Enter', altKey: true, currentTarget: area,
|
|
624
|
+
stopPropagation: vi.fn(), preventDefault: vi.fn(),
|
|
625
|
+
} as unknown as KeyboardEvent
|
|
626
|
+
ed.onEditorKeyDown(ev)
|
|
627
|
+
expect(ev.preventDefault).toHaveBeenCalled()
|
|
628
|
+
expect(area.value).toBe('a\nb')
|
|
629
|
+
expect(area.selectionStart).toBe(2)
|
|
630
|
+
expect(ctx.editingCell?.value).toBe('a\nb')
|
|
631
|
+
// Still editing: nothing was saved.
|
|
632
|
+
expect(ctx.internalData[0].a).toBe('old')
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
it('Alt+Enter in a single-line input is just Enter', () => {
|
|
636
|
+
const { ctx, ed } = editingFor({
|
|
637
|
+
columns: [{ id: 'a', field: 'a', editorType: 'text' }],
|
|
638
|
+
data: [{ a: 'old' }, { a: 'next' }],
|
|
639
|
+
})
|
|
640
|
+
ctx.gridRootEl = { focus: vi.fn() }
|
|
641
|
+
ed.onCellDoubleClick(0, 0)
|
|
642
|
+
ctx.editingCell = { ...ctx.editingCell, value: 'new' }
|
|
643
|
+
const input = document.createElement('input')
|
|
644
|
+
const ev = {
|
|
645
|
+
key: 'Enter', altKey: true, currentTarget: input,
|
|
646
|
+
stopPropagation: vi.fn(), preventDefault: vi.fn(),
|
|
647
|
+
} as unknown as KeyboardEvent
|
|
648
|
+
ed.onEditorKeyDown(ev)
|
|
649
|
+
expect(ctx.internalData[0].a).toBe('new')
|
|
650
|
+
expect(ctx.editingCell).toBeNull()
|
|
651
|
+
})
|
|
652
|
+
|
|
579
653
|
it('Escape cancels without saving and refocuses the grid root', () => {
|
|
580
654
|
const { ctx, ed } = editingFor({
|
|
581
655
|
columns: [{ id: 'a', field: 'a', editorType: 'text' }],
|
|
@@ -630,6 +704,89 @@ describe('onEditorKeyDown', () => {
|
|
|
630
704
|
expect(ctx.activeCalls.at(-1)).toEqual([0, 1])
|
|
631
705
|
})
|
|
632
706
|
|
|
707
|
+
it('Enter after a run of Tab commits returns to the column the run began in', () => {
|
|
708
|
+
const { ctx, ed } = editingFor({
|
|
709
|
+
columns: [
|
|
710
|
+
{ id: 'a', field: 'a', editorType: 'text' },
|
|
711
|
+
{ id: 'b', field: 'b', editorType: 'text' },
|
|
712
|
+
{ id: 'c', field: 'c', editorType: 'text' },
|
|
713
|
+
],
|
|
714
|
+
data: [{ a: '', b: '', c: '' }, { a: '', b: '', c: '' }],
|
|
715
|
+
})
|
|
716
|
+
ctx.gridRootEl = { focus: vi.fn() }
|
|
717
|
+
// The real setActiveCell forgets the run on a move to another cell;
|
|
718
|
+
// the fake must too, or the test would pass with the origin never
|
|
719
|
+
// being written back.
|
|
720
|
+
const plain = ctx.setActiveCell
|
|
721
|
+
ctx.setActiveCell = (r: number, c: number) => {
|
|
722
|
+
const prev = ctx.activeCell
|
|
723
|
+
if (!prev || prev.rowIndex !== r || prev.colIndex !== c) ctx.tabRunOrigin = null
|
|
724
|
+
plain(r, c)
|
|
725
|
+
}
|
|
726
|
+
ed.onCellDoubleClick(0, 0)
|
|
727
|
+
ctx.editingCell = { ...ctx.editingCell, value: 'one' }
|
|
728
|
+
ed.onEditorKeyDown(fakeEvent('Tab'))
|
|
729
|
+
ed.onCellDoubleClick(0, 1)
|
|
730
|
+
ctx.editingCell = { ...ctx.editingCell, value: 'two' }
|
|
731
|
+
ed.onEditorKeyDown(fakeEvent('Tab'))
|
|
732
|
+
expect(ctx.activeCalls.at(-1)).toEqual([0, 2])
|
|
733
|
+
expect(ctx.tabRunOrigin).toBe(0)
|
|
734
|
+
ed.onCellDoubleClick(0, 2)
|
|
735
|
+
ctx.editingCell = { ...ctx.editingCell, value: 'three' }
|
|
736
|
+
ed.onEditorKeyDown(fakeEvent('Enter'))
|
|
737
|
+
expect(ctx.internalData[0]).toMatchObject({ a: 'one', b: 'two', c: 'three' })
|
|
738
|
+
expect(ctx.activeCalls.at(-1)).toEqual([1, 0])
|
|
739
|
+
expect(ctx.tabRunOrigin).toBe(null)
|
|
740
|
+
expect(ctx.selectionCalls.at(-1)).toEqual([1, 0])
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
it('Ctrl+Enter writes the entry into every cell of the block, as one undo, and stays put', () => {
|
|
744
|
+
const { ctx, ed } = editingFor({
|
|
745
|
+
columns: [
|
|
746
|
+
{ id: 'a', field: 'a', editorType: 'text' },
|
|
747
|
+
{ id: 'b', field: 'b', editorType: 'text' },
|
|
748
|
+
],
|
|
749
|
+
data: [{ a: '', b: '' }, { a: '', b: '' }, { a: 'keep', b: 'keep' }],
|
|
750
|
+
})
|
|
751
|
+
ctx.gridRootEl = { focus: vi.fn() }
|
|
752
|
+
ctx.activeRangeRect = () => ({ minRow: 0, maxRow: 1, minCol: 0, maxCol: 1 })
|
|
753
|
+
ctx.isCellInSelectedRange = () => true
|
|
754
|
+
ed.onCellDoubleClick(0, 0)
|
|
755
|
+
ctx.editingCell = { ...ctx.editingCell, value: 'x' }
|
|
756
|
+
const ev = fakeEvent('Enter', { ctrlKey: true })
|
|
757
|
+
ed.onEditorKeyDown(ev)
|
|
758
|
+
expect(ev.preventDefault).toHaveBeenCalled()
|
|
759
|
+
expect(ctx.internalData.slice(0, 2)).toEqual([{ a: 'x', b: 'x' }, { a: 'x', b: 'x' }])
|
|
760
|
+
expect(ctx.internalData[2]).toEqual({ a: 'keep', b: 'keep' })
|
|
761
|
+
// The cursor did not move.
|
|
762
|
+
expect(ctx.activeCalls.at(-1)).toEqual([0, 0])
|
|
763
|
+
expect(ctx.editingCell).toBeNull()
|
|
764
|
+
// Four steps, one group.
|
|
765
|
+
const groups = new Set(ctx.history.map((s: any) => s.groupId))
|
|
766
|
+
expect(ctx.history.length).toBe(4)
|
|
767
|
+
expect(groups.size).toBe(1)
|
|
768
|
+
expect([...groups][0]).toBeTruthy()
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
it('a commit inside a selected block keeps the block and moves within it', () => {
|
|
772
|
+
const { ctx, ed } = editingFor({
|
|
773
|
+
columns: [
|
|
774
|
+
{ id: 'a', field: 'a', editorType: 'text' },
|
|
775
|
+
{ id: 'b', field: 'b', editorType: 'text' },
|
|
776
|
+
],
|
|
777
|
+
data: [{ a: '', b: '' }, { a: '', b: '' }],
|
|
778
|
+
})
|
|
779
|
+
ctx.gridRootEl = { focus: vi.fn() }
|
|
780
|
+
ctx.activeRangeRect = () => ({ minRow: 0, maxRow: 1, minCol: 0, maxCol: 1 })
|
|
781
|
+
ctx.isCellInSelectedRange = () => true
|
|
782
|
+
ed.onCellDoubleClick(1, 0)
|
|
783
|
+
ctx.editingCell = { ...ctx.editingCell, value: 'x' }
|
|
784
|
+
ed.onEditorKeyDown(fakeEvent('Enter'))
|
|
785
|
+
// Past the block's last row: the top of the next column.
|
|
786
|
+
expect(ctx.activeCalls.at(-1)).toEqual([0, 1])
|
|
787
|
+
expect(ctx.selectionCalls).toEqual([])
|
|
788
|
+
})
|
|
789
|
+
|
|
633
790
|
it('Shift+Tab moves back a column and wraps to the previous row (#48)', () => {
|
|
634
791
|
const { ctx, ed } = editingFor({
|
|
635
792
|
columns: [
|
|
@@ -841,10 +998,131 @@ describe('onCellDoubleClick', () => {
|
|
|
841
998
|
})
|
|
842
999
|
})
|
|
843
1000
|
|
|
1001
|
+
describe('applyPastedText reports and records what it wrote', () => {
|
|
1002
|
+
function pasteReady(text: string) {
|
|
1003
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
1004
|
+
value: { readText: vi.fn().mockResolvedValue(text) },
|
|
1005
|
+
configurable: true,
|
|
1006
|
+
})
|
|
1007
|
+
const { ctx, ed } = editingFor({
|
|
1008
|
+
columns: [
|
|
1009
|
+
{ id: 'a', field: 'a', editable: true, editorType: 'text' },
|
|
1010
|
+
{ id: 'b', field: 'b', editable: true, editorType: 'text' },
|
|
1011
|
+
],
|
|
1012
|
+
data: [{ a: 'a0', b: 'b0' }, { a: 'a1', b: 'b1' }, { a: 'a2', b: 'b2' }],
|
|
1013
|
+
})
|
|
1014
|
+
ctx.setActiveCell(1, 0)
|
|
1015
|
+
ctx.selectionRange = { anchor: { rowIndex: 1, colIndex: 0 }, focus: { rowIndex: 1, colIndex: 0 } }
|
|
1016
|
+
return { ctx, ed }
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
it('fires onCellValueChange for every cell a block paste changed', async () => {
|
|
1020
|
+
// A formula engine behind the grid only sees a value through this hook;
|
|
1021
|
+
// a paste that skipped it left the engine holding the old cells.
|
|
1022
|
+
const { ctx, ed } = pasteReady('x\ty\nz\tb2')
|
|
1023
|
+
const onCellValueChange = vi.fn()
|
|
1024
|
+
ctx.props.onCellValueChange = onCellValueChange
|
|
1025
|
+
await ed.pasteFromClipboard()
|
|
1026
|
+
expect(ctx.internalData[1]).toEqual({ a: 'x', b: 'y' })
|
|
1027
|
+
expect(ctx.internalData[2]).toEqual({ a: 'z', b: 'b2' })
|
|
1028
|
+
// b2 was already b2: no event for a cell that did not change.
|
|
1029
|
+
expect(onCellValueChange).toHaveBeenCalledTimes(3)
|
|
1030
|
+
expect(onCellValueChange).toHaveBeenCalledWith(
|
|
1031
|
+
expect.objectContaining({ rowIndex: 1, columnId: 'a', oldValue: 'a1', newValue: 'x' }),
|
|
1032
|
+
)
|
|
1033
|
+
expect(onCellValueChange).toHaveBeenCalledWith(
|
|
1034
|
+
expect.objectContaining({ rowIndex: 2, columnId: 'a', oldValue: 'a2', newValue: 'z' }),
|
|
1035
|
+
)
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1038
|
+
it('records the paste as one grouped history entry', async () => {
|
|
1039
|
+
const { ctx, ed } = pasteReady('x\ty\nz\tw')
|
|
1040
|
+
await ed.pasteFromClipboard()
|
|
1041
|
+
expect(ctx.history).toHaveLength(4)
|
|
1042
|
+
const groups = new Set(ctx.history.map((s: { groupId?: string }) => s.groupId))
|
|
1043
|
+
expect(groups.size).toBe(1)
|
|
1044
|
+
expect([...groups][0]).toBeDefined()
|
|
1045
|
+
expect(ctx.history[0]).toMatchObject({ rowId: '1', columnId: 'a', field: 'a', before: 'a1', after: 'x' })
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
it('writes nothing, records nothing, when the paste changes nothing', async () => {
|
|
1049
|
+
const { ctx, ed } = pasteReady('a1\tb1')
|
|
1050
|
+
const onCellValueChange = vi.fn()
|
|
1051
|
+
ctx.props.onCellValueChange = onCellValueChange
|
|
1052
|
+
const before = ctx.internalData
|
|
1053
|
+
await ed.pasteFromClipboard()
|
|
1054
|
+
expect(ctx.internalData).toBe(before)
|
|
1055
|
+
expect(ctx.history).toHaveLength(0)
|
|
1056
|
+
expect(onCellValueChange).not.toHaveBeenCalled()
|
|
1057
|
+
})
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
describe('onPasteClipboard', () => {
|
|
1061
|
+
const grid = () => editingFor({
|
|
1062
|
+
columns: [
|
|
1063
|
+
{ id: 'a', field: 'a', editorType: 'text', editable: true },
|
|
1064
|
+
{ id: 'b', field: 'b', editorType: 'text', editable: true },
|
|
1065
|
+
],
|
|
1066
|
+
data: [{ a: '', b: '' }, { a: '', b: '' }],
|
|
1067
|
+
})
|
|
1068
|
+
|
|
1069
|
+
it('hands the native paste event\'s text and HTML to the handler and applies nothing itself when it returns true', () => {
|
|
1070
|
+
Object.defineProperty(navigator, 'clipboard', { value: { readText: vi.fn() }, configurable: true })
|
|
1071
|
+
const { ctx, ed } = grid()
|
|
1072
|
+
const seen: unknown[] = []
|
|
1073
|
+
ctx.props.onPasteClipboard = (payload: unknown) => { seen.push(payload); return true }
|
|
1074
|
+
const ev = {
|
|
1075
|
+
clipboardData: { getData: (type: string) => (type === 'text/html' ? '<table><tr><td x:fmla="=A1">1</td></tr></table>' : '1') },
|
|
1076
|
+
preventDefault: vi.fn(),
|
|
1077
|
+
} as unknown as ClipboardEvent
|
|
1078
|
+
ed.onGridPaste(ev)
|
|
1079
|
+
expect(ev.preventDefault).toHaveBeenCalled()
|
|
1080
|
+
expect(seen).toEqual([{ text: '1', html: '<table><tr><td x:fmla="=A1">1</td></tr></table>', source: 'event' }])
|
|
1081
|
+
expect(ctx.internalData[0]).toMatchObject({ a: '', b: '' })
|
|
1082
|
+
})
|
|
1083
|
+
|
|
1084
|
+
it('falls through to the plain paste when the handler declines', () => {
|
|
1085
|
+
Object.defineProperty(navigator, 'clipboard', { value: undefined, configurable: true })
|
|
1086
|
+
const { ctx, ed } = grid()
|
|
1087
|
+
ctx.props.onPasteClipboard = () => undefined
|
|
1088
|
+
ctx.selectionRange = { anchor: { rowIndex: 0, colIndex: 0 }, focus: { rowIndex: 0, colIndex: 0 } }
|
|
1089
|
+
const ev = {
|
|
1090
|
+
clipboardData: { getData: (type: string) => (type === 'text/plain' ? 'X\tY' : '') },
|
|
1091
|
+
preventDefault: vi.fn(),
|
|
1092
|
+
} as unknown as ClipboardEvent
|
|
1093
|
+
ed.onGridPaste(ev)
|
|
1094
|
+
expect(ctx.internalData[0]).toMatchObject({ a: 'X', b: 'Y' })
|
|
1095
|
+
})
|
|
1096
|
+
|
|
1097
|
+
it('reads both types through navigator.clipboard.read on the async path', async () => {
|
|
1098
|
+
const blob = (text: string) => ({ text: async () => text })
|
|
1099
|
+
const read = vi.fn().mockResolvedValue([
|
|
1100
|
+
{ types: ['text/plain', 'text/html'], getType: async (t: string) => blob(t === 'text/html' ? '<b>h</b>' : 'p') },
|
|
1101
|
+
])
|
|
1102
|
+
Object.defineProperty(navigator, 'clipboard', { value: { read, readText: vi.fn() }, configurable: true })
|
|
1103
|
+
const { ctx, ed } = grid()
|
|
1104
|
+
const seen: unknown[] = []
|
|
1105
|
+
ctx.props.onPasteClipboard = (payload: unknown) => { seen.push(payload); return true }
|
|
1106
|
+
await ed.pasteFromClipboard()
|
|
1107
|
+
expect(seen).toEqual([{ text: 'p', html: '<b>h</b>', source: 'async' }])
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
it('the paste is one history group even when the handler writes several cells', () => {
|
|
1111
|
+
Object.defineProperty(navigator, 'clipboard', { value: undefined, configurable: true })
|
|
1112
|
+
const { ctx, ed } = grid()
|
|
1113
|
+
let groupSeen: unknown = 'unset'
|
|
1114
|
+
ctx.props.onPasteClipboard = () => { groupSeen = ctx.historyGroupId; return true }
|
|
1115
|
+
ed.onGridPaste({ clipboardData: { getData: () => 'x' }, preventDefault: vi.fn() } as unknown as ClipboardEvent)
|
|
1116
|
+
// The handler ran inside an ambient group, and the group closed after it.
|
|
1117
|
+
expect(groupSeen).toBeTruthy()
|
|
1118
|
+
expect(ctx.historyGroupId).toBeUndefined()
|
|
1119
|
+
})
|
|
1120
|
+
})
|
|
1121
|
+
|
|
844
1122
|
describe('clipboard fallbacks (non-paste branches only)', () => {
|
|
845
1123
|
// onGridPaste and pasteFromClipboard guard on navigator.clipboard.readText.
|
|
846
|
-
// The
|
|
847
|
-
//
|
|
1124
|
+
// The transformation itself is covered just above; here we only assert
|
|
1125
|
+
// the early-return guards.
|
|
848
1126
|
|
|
849
1127
|
it('pasteFromClipboard returns early when the async API is unavailable', async () => {
|
|
850
1128
|
Object.defineProperty(navigator, 'clipboard', {
|