@svgrid/grid 3.0.3 → 4.0.0
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 +5 -4
- 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 +122 -41
- package/dist/SvGrid.controller.svelte.js +845 -207
- package/dist/SvGrid.css +178 -2
- package/dist/SvGrid.svelte +467 -516
- package/dist/SvGrid.types.d.ts +315 -12
- 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 +119 -28
- package/dist/cdn/GridMenus-CLI93jke.js +635 -0
- package/dist/cdn/{GridMenus-DhdPF700.js → GridMenus-CwooL3QA.js} +240 -207
- 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/server-block-cache-CGoWz-87.js +237 -0
- package/dist/cdn/{src-D6XvA1Ij.js → src-B6HXdfHY.js} +10448 -10049
- package/dist/cdn/{src-Bugf5XlI.js → src-C6Iowdvs.js} +6908 -6509
- package/dist/cdn/svgrid.js +27 -15
- package/dist/cdn/svgrid.svelte-external.js +27 -15
- package/dist/cdn/validate-CchXzyrX.js +76 -0
- 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/core.js +36 -13
- package/dist/createPopoverSelect.svelte.d.ts +2 -2
- package/dist/editing.d.ts +2 -8
- package/dist/editing.js +272 -33
- package/dist/fill-patterns.d.ts +0 -5
- package/dist/fill-patterns.js +56 -2
- package/dist/grid-icons.d.ts +2 -2
- package/dist/grid-icons.js +2 -2
- package/dist/grid-messages.d.ts +12 -3
- package/dist/grid-messages.js +7 -0
- package/dist/history.d.ts +83 -0
- package/dist/history.js +133 -0
- package/dist/index.d.ts +27 -10
- package/dist/index.js +22 -5
- 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-model.d.ts +133 -0
- package/dist/row-model.js +16 -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 +18 -1
- package/dist/selection.js +138 -18
- package/dist/server-block-cache.d.ts +214 -0
- package/dist/server-block-cache.js +531 -0
- package/dist/server-data-source.d.ts +240 -6
- package/dist/server-data-source.js +220 -20
- package/dist/server.d.ts +13 -0
- package/dist/server.js +13 -0
- 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/validate.d.ts +2 -0
- package/dist/validate.js +8 -3
- package/dist/virtualization/virtualizer.js +38 -17
- package/package.json +11 -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 +818 -179
- package/src/SvGrid.css +178 -2
- package/src/SvGrid.svelte +467 -516
- package/src/SvGrid.types.ts +293 -11
- 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 +118 -22
- 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.rowmodel-cache.test.ts +51 -0
- package/src/core.ts +40 -12
- package/src/editing.test.ts +281 -3
- package/src/editing.ts +264 -38
- package/src/fill-patterns.test.ts +35 -0
- package/src/fill-patterns.ts +56 -2
- package/src/grid-icons.ts +2 -2
- package/src/grid-messages.ts +21 -3
- package/src/history.test.ts +196 -0
- package/src/history.ts +162 -0
- package/src/icon-seam.test.ts +5 -6
- package/src/index.ts +169 -12
- 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-model.ts +146 -0
- 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 +33 -2
- package/src/selection.ts +143 -21
- package/src/server-block-cache.test.ts +645 -0
- package/src/server-block-cache.ts +677 -0
- package/src/server-data-source.infinite.test.ts +343 -0
- package/src/server-data-source.ts +450 -27
- package/src/server.ts +47 -0
- 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/svgrid.row-model-prop.svelte.test.ts +352 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +289 -0
- package/src/validate.test.ts +10 -0
- package/src/validate.ts +10 -3
- package/src/virtualization/virtualizer.ts +39 -17
- 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-CHOMnW-c.js +0 -602
- package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
- package/dist/cdn/SvGridChart-1PMSp2LZ.js +0 -1481
- package/dist/cdn/SvGridChart-D02avhyr.js +0 -1480
- package/dist/cdn/SvGridChartPanel-DJnViOv4.js +0 -834
- package/dist/cdn/SvGridChartPanel-vPTNerFZ.js +0 -810
- package/dist/cdn/SvGridChartView-C--HhdUo.js +0 -56
- package/dist/cdn/SvGridChartView-KyyFG-wY.js +0 -55
- package/dist/cdn/chart-DLg3_zTQ.js +0 -1652
- 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
package/src/editing.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type TableFeatures,
|
|
12
12
|
} from "./index";
|
|
13
13
|
import "./sv-grid-scrollbar";
|
|
14
|
-
import {
|
|
14
|
+
import { getEntryStep } from "./keyboard";
|
|
15
15
|
import {
|
|
16
16
|
getCellKey,
|
|
17
17
|
toValueArray,
|
|
@@ -21,14 +21,10 @@ import {
|
|
|
21
21
|
getColumnBaseValue,
|
|
22
22
|
isGroupRow,
|
|
23
23
|
} from "./cell-values";
|
|
24
|
+
import { pushHistory, nextGroupId, runHistoryGroup, type HistoryStep } from "./history";
|
|
25
|
+
import { hasGridShortcuts, runGridShortcuts } from "./shortcut-registry";
|
|
26
|
+
import { buildCommandContext } from "./command-context";
|
|
24
27
|
|
|
25
|
-
type HistoryStep = {
|
|
26
|
-
rowId: string;
|
|
27
|
-
columnId: string;
|
|
28
|
-
field: string;
|
|
29
|
-
before: unknown;
|
|
30
|
-
after: unknown;
|
|
31
|
-
};
|
|
32
28
|
|
|
33
29
|
export function createEditing<
|
|
34
30
|
TFeatures extends TableFeatures = TableFeatures,
|
|
@@ -51,6 +47,14 @@ export function createEditing<
|
|
|
51
47
|
function isCellEditable(column: Column<TData>, row?: Row<TData>): boolean {
|
|
52
48
|
const editable = column.columnDef.editable;
|
|
53
49
|
if (editable === false) return false;
|
|
50
|
+
if (row) {
|
|
51
|
+
// A server-side group row is a key with aggregates under the leaf
|
|
52
|
+
// columns, and a placeholder row has no data yet: neither takes an
|
|
53
|
+
// edit, whatever the column says.
|
|
54
|
+
const serverGroup = ctx.props.serverGroup;
|
|
55
|
+
if (serverGroup && row.original !== undefined && serverGroup.isGroup(row.original)) return false;
|
|
56
|
+
if (ctx.placeholderStateOf?.(row)) return false;
|
|
57
|
+
}
|
|
54
58
|
if (typeof editable !== "function") return true;
|
|
55
59
|
if (!row) return true;
|
|
56
60
|
const cellCtx: CellContext<TData> = {
|
|
@@ -132,11 +136,29 @@ export function createEditing<
|
|
|
132
136
|
rowRef: row.original,
|
|
133
137
|
};
|
|
134
138
|
ctx.setActiveCell(rowIndex, colIndex);
|
|
135
|
-
|
|
139
|
+
selectForEdit(rowIndex, colIndex);
|
|
136
140
|
return true;
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The selection an edit starts with. A cell already inside the selection
|
|
145
|
+
* leaves it standing: typing into the active cell of a selected block
|
|
146
|
+
* used to collapse the block to that one cell, so Enter could never walk
|
|
147
|
+
* on inside it. A cell outside the selection becomes the selection.
|
|
148
|
+
*/
|
|
149
|
+
function selectForEdit(rowIndex: number, colIndex: number) {
|
|
150
|
+
if (ctx.isCellInSelectedRange(rowIndex, colIndex)) return;
|
|
151
|
+
ctx.setSelection(rowIndex, colIndex);
|
|
152
|
+
}
|
|
153
|
+
|
|
139
154
|
function saveEditingCell() {
|
|
155
|
+
// One group: a consumer's onCellValueChange may record a step of its own
|
|
156
|
+
// (a spreadsheet gives "12%" a percent format as it lands), and that
|
|
157
|
+
// belongs to the same Ctrl+Z as the value.
|
|
158
|
+
runHistoryGroup(ctx, () => commitEditingCell());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function commitEditingCell() {
|
|
140
162
|
if (!ctx.editingCell) return;
|
|
141
163
|
const editing = ctx.editingCell;
|
|
142
164
|
// Resolve the row this edit belongs to. `allRows` is only the current page,
|
|
@@ -197,19 +219,7 @@ export function createEditing<
|
|
|
197
219
|
before: oldValue,
|
|
198
220
|
after: finalValue,
|
|
199
221
|
}
|
|
200
|
-
|
|
201
|
-
truncated.push(step)
|
|
202
|
-
// Cap the buffer at UNDO_LIMIT; drop the OLDEST entries so the
|
|
203
|
-
// pointer stays valid relative to the newest steps.
|
|
204
|
-
if (truncated.length > ctx.UNDO_LIMIT) {
|
|
205
|
-
const drop = truncated.length - ctx.UNDO_LIMIT
|
|
206
|
-
ctx.history = truncated.slice(drop)
|
|
207
|
-
ctx.historyPtr = ctx.history.length - 1
|
|
208
|
-
} else {
|
|
209
|
-
ctx.history = truncated
|
|
210
|
-
ctx.historyPtr = ctx.history.length - 1
|
|
211
|
-
}
|
|
212
|
-
ctx.historyVersion += 1
|
|
222
|
+
pushHistory(ctx, [step])
|
|
213
223
|
}
|
|
214
224
|
// Notify the consumer AFTER the row has been updated so any callback-
|
|
215
225
|
// driven recompute (cascade totals, server save, undo stack) sees the
|
|
@@ -230,6 +240,11 @@ export function createEditing<
|
|
|
230
240
|
/** Apply an undo / redo step directly to the underlying row, bypassing
|
|
231
241
|
* the editor pipeline so we don't accidentally re-push to the stack. */
|
|
232
242
|
function applyHistoryStep(step: HistoryStep, direction: 'undo' | 'redo') {
|
|
243
|
+
if (step.custom) {
|
|
244
|
+
if (direction === 'undo') step.custom.undo()
|
|
245
|
+
else step.custom.redo()
|
|
246
|
+
return
|
|
247
|
+
}
|
|
233
248
|
const row = ctx.allRows.find((r: any) => r.id === step.rowId)
|
|
234
249
|
const col = ctx.allColumns.find((c: any) => c.id === step.columnId)
|
|
235
250
|
if (!row?.original || !col) return
|
|
@@ -261,26 +276,111 @@ export function createEditing<
|
|
|
261
276
|
* move, Tab committed the edit and then let the browser walk focus out of the
|
|
262
277
|
* grid entirely (#48). Shared by every editor type, including the textarea.
|
|
263
278
|
*/
|
|
264
|
-
|
|
279
|
+
/**
|
|
280
|
+
* Commit the edit and step the cursor the way data entry expects: Tab a
|
|
281
|
+
* column to the right (wrapping at the row's end), Enter a row down, Shift
|
|
282
|
+
* reversing either. Enter used to commit and stay, so a column of numbers
|
|
283
|
+
* took a click or an arrow between every value; Excel and every other
|
|
284
|
+
* sheet move down, and the accessibility page has always said this grid
|
|
285
|
+
* does too. At the last row or column the cursor stays put.
|
|
286
|
+
*/
|
|
287
|
+
function commitAndMove(intent: "tabNext" | "tabPrev" | "moveDown" | "moveUp") {
|
|
265
288
|
const active = ctx.activeCell;
|
|
266
289
|
saveEditingCell();
|
|
267
290
|
ctx.gridRootEl?.focus({ preventScroll: true });
|
|
268
291
|
if (!active) return;
|
|
269
|
-
|
|
292
|
+
// The same stepping as Enter and Tab on the grid root: a Tab run's Enter
|
|
293
|
+
// goes back to the run's first column, and a selected block keeps the
|
|
294
|
+
// cursor inside it.
|
|
295
|
+
const step = getEntryStep(active, intent, {
|
|
270
296
|
maxRow: ctx.allRows.length - 1,
|
|
271
297
|
maxCol: ctx.allColumns.length - 1,
|
|
298
|
+
tabOrigin: ctx.tabRunOrigin,
|
|
299
|
+
range: ctx.activeRangeRect(),
|
|
300
|
+
collapsed: {
|
|
301
|
+
isRowCollapsed: (i: number) => ctx.isRowCollapsed(i) as boolean,
|
|
302
|
+
isColumnCollapsed: (i: number) => !!ctx.collapsedColumns[ctx.allColumns[i]?.id],
|
|
303
|
+
},
|
|
272
304
|
});
|
|
305
|
+
const next = step.cell;
|
|
273
306
|
ctx.setActiveCell(next.rowIndex, next.colIndex);
|
|
274
|
-
ctx.
|
|
307
|
+
ctx.tabRunOrigin = step.tabOrigin;
|
|
308
|
+
if (!step.withinRange) ctx.setSelection(next.rowIndex, next.colIndex);
|
|
275
309
|
ctx.scrollActiveCellIntoView(next.rowIndex, next.colIndex);
|
|
276
310
|
}
|
|
277
311
|
|
|
312
|
+
function commitAndMoveByTab(shiftKey: boolean) {
|
|
313
|
+
commitAndMove(shiftKey ? "tabPrev" : "tabNext");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Ctrl+Enter: the entry goes into every cell of the selected block and the
|
|
318
|
+
* cursor stays where it is, as in Excel, where it is how a block is filled
|
|
319
|
+
* with one value in one go. Each cell takes the same raw entry through the
|
|
320
|
+
* ordinary commit, so its column's parser, the history and
|
|
321
|
+
* `onCellValueChange` all see it, and the whole block is one undo. A cell
|
|
322
|
+
* that cannot be edited is left alone. With nothing but the active cell
|
|
323
|
+
* selected it is a commit that does not move.
|
|
324
|
+
*/
|
|
325
|
+
function commitToSelection() {
|
|
326
|
+
const editing = ctx.editingCell;
|
|
327
|
+
if (!editing) return;
|
|
328
|
+
const raw = editing.value;
|
|
329
|
+
const active = ctx.activeCell;
|
|
330
|
+
const range = ctx.activeRangeRect();
|
|
331
|
+
runHistoryGroup(ctx, () => {
|
|
332
|
+
commitEditingCell();
|
|
333
|
+
if (!active || !range) return;
|
|
334
|
+
for (let r = range.minRow; r <= range.maxRow; r += 1) {
|
|
335
|
+
for (let c = range.minCol; c <= range.maxCol; c += 1) {
|
|
336
|
+
if (r === active.rowIndex && c === active.colIndex) continue;
|
|
337
|
+
const row = ctx.allRows[r];
|
|
338
|
+
const column = ctx.allColumns[c];
|
|
339
|
+
if (!row || !column || isGroupRow(row) || !isCellEditable(column, row)) continue;
|
|
340
|
+
ctx.editingCell = {
|
|
341
|
+
rowId: row.id,
|
|
342
|
+
columnId: column.id,
|
|
343
|
+
editorType: (column.columnDef.editorType ?? "text") as CellEditorType,
|
|
344
|
+
value: raw,
|
|
345
|
+
rowRef: row.original,
|
|
346
|
+
};
|
|
347
|
+
commitEditingCell();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
ctx.gridRootEl?.focus({ preventScroll: true });
|
|
352
|
+
}
|
|
353
|
+
|
|
278
354
|
function onEditorKeyDown(event: KeyboardEvent) {
|
|
279
355
|
event.stopPropagation();
|
|
280
|
-
|
|
356
|
+
// Registered commands get the key first here too, with `editing: true`, so
|
|
357
|
+
// a handler can claim a combination that only means something mid-edit
|
|
358
|
+
// (Alt+Enter for a newline, F4 to cycle a reference between relative and
|
|
359
|
+
// absolute) before the editor's own Enter / Tab / Escape handling runs.
|
|
360
|
+
if (hasGridShortcuts() && runGridShortcuts(event, buildCommandContext(ctx, true))) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (event.key === "Enter" && event.altKey) {
|
|
364
|
+
// A line break, in an editor that can hold one (`editorMultiline`).
|
|
365
|
+
// In a single-line editor Alt+Enter is just Enter.
|
|
366
|
+
const el = event.currentTarget;
|
|
367
|
+
if (el instanceof HTMLTextAreaElement) {
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
const start = el.selectionStart ?? el.value.length;
|
|
370
|
+
const end = el.selectionEnd ?? start;
|
|
371
|
+
el.value = el.value.slice(0, start) + "\n" + el.value.slice(end);
|
|
372
|
+
el.selectionStart = el.selectionEnd = start + 1;
|
|
373
|
+
updateEditingCellValue(el.value);
|
|
374
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
281
379
|
event.preventDefault();
|
|
282
|
-
|
|
283
|
-
|
|
380
|
+
commitToSelection();
|
|
381
|
+
} else if (event.key === "Enter") {
|
|
382
|
+
event.preventDefault();
|
|
383
|
+
commitAndMove(event.shiftKey ? "moveUp" : "moveDown");
|
|
284
384
|
} else if (event.key === "Tab") {
|
|
285
385
|
event.preventDefault();
|
|
286
386
|
commitAndMoveByTab(event.shiftKey);
|
|
@@ -356,7 +456,7 @@ export function createEditing<
|
|
|
356
456
|
rowRef: row.original,
|
|
357
457
|
};
|
|
358
458
|
ctx.setActiveCell(rowIndex, colIndex);
|
|
359
|
-
|
|
459
|
+
selectForEdit(rowIndex, colIndex);
|
|
360
460
|
}
|
|
361
461
|
|
|
362
462
|
async function pasteFromClipboard() {
|
|
@@ -366,6 +466,28 @@ export function createEditing<
|
|
|
366
466
|
// On plain HTTP (a XAMPP/Apache LAN host) `navigator.clipboard` is
|
|
367
467
|
// undefined - there the Ctrl+V keydown handler skips preventDefault and
|
|
368
468
|
// lets the browser deliver a native `paste` event to `onGridPaste`.
|
|
469
|
+
if (ctx.props.onPasteClipboard && navigator.clipboard?.read) {
|
|
470
|
+
// Both types, for a handler that reads the HTML.
|
|
471
|
+
try {
|
|
472
|
+
const items = await navigator.clipboard.read();
|
|
473
|
+
let text = "";
|
|
474
|
+
let html: string | null = null;
|
|
475
|
+
for (const item of items) {
|
|
476
|
+
if (item.types.includes("text/html")) html = await (await item.getType("text/html")).text();
|
|
477
|
+
if (item.types.includes("text/plain")) text = await (await item.getType("text/plain")).text();
|
|
478
|
+
}
|
|
479
|
+
if (text || html) applyPastedPayload({ text, html, source: "async" });
|
|
480
|
+
} catch {
|
|
481
|
+
// No permission or nothing readable: the text-only read below may still work.
|
|
482
|
+
if (!navigator.clipboard.readText) return;
|
|
483
|
+
try {
|
|
484
|
+
applyPastedPayload({ text: await navigator.clipboard.readText(), html: null, source: "async" });
|
|
485
|
+
} catch {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
369
491
|
if (!navigator.clipboard?.readText) return;
|
|
370
492
|
let text: string;
|
|
371
493
|
try {
|
|
@@ -376,6 +498,26 @@ export function createEditing<
|
|
|
376
498
|
applyPastedText(text);
|
|
377
499
|
}
|
|
378
500
|
|
|
501
|
+
/**
|
|
502
|
+
* With `onPasteClipboard` set, Ctrl+V leaves the key to the browser so the
|
|
503
|
+
* `paste` event arrives with the HTML. A browser that does not deliver
|
|
504
|
+
* that event to the grid root (Firefox, a non-editable element) would
|
|
505
|
+
* paste nothing, so the async read is armed to run shortly after unless
|
|
506
|
+
* the event lands first and disarms it.
|
|
507
|
+
*/
|
|
508
|
+
let pasteFallback: ReturnType<typeof setTimeout> | null = null;
|
|
509
|
+
function armPasteFallback() {
|
|
510
|
+
if (pasteFallback) clearTimeout(pasteFallback);
|
|
511
|
+
pasteFallback = setTimeout(() => {
|
|
512
|
+
pasteFallback = null;
|
|
513
|
+
void pasteFromClipboard();
|
|
514
|
+
}, 80);
|
|
515
|
+
}
|
|
516
|
+
function disarmPasteFallback() {
|
|
517
|
+
if (pasteFallback) clearTimeout(pasteFallback);
|
|
518
|
+
pasteFallback = null;
|
|
519
|
+
}
|
|
520
|
+
|
|
379
521
|
/**
|
|
380
522
|
* Native `paste` ClipboardEvent fallback (insecure-context path). A real
|
|
381
523
|
* Ctrl/Cmd+V on the focused grid root fires this with `clipboardData`
|
|
@@ -390,6 +532,17 @@ export function createEditing<
|
|
|
390
532
|
// preventDefault the editor's paste and write to the data cell instead,
|
|
391
533
|
// so the edit "wouldn't update".
|
|
392
534
|
if (ctx.editingCell) return;
|
|
535
|
+
if (ctx.props.onPasteClipboard) {
|
|
536
|
+
// The event is the preferred path for a handler: it carries the HTML
|
|
537
|
+
// as the source wrote it, needs no permission, and works over HTTP.
|
|
538
|
+
disarmPasteFallback();
|
|
539
|
+
const text = event.clipboardData?.getData("text/plain") ?? "";
|
|
540
|
+
const html = event.clipboardData?.getData("text/html") || null;
|
|
541
|
+
if (!text && !html) return;
|
|
542
|
+
event.preventDefault();
|
|
543
|
+
applyPastedPayload({ text, html, source: "event" });
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
393
546
|
// When the async API is available the keydown handler already pasted via
|
|
394
547
|
// pasteFromClipboard(); ignore the (suppressed) native event so we don't
|
|
395
548
|
// paste twice.
|
|
@@ -401,6 +554,22 @@ export function createEditing<
|
|
|
401
554
|
}
|
|
402
555
|
|
|
403
556
|
function applyPastedText(text: string) {
|
|
557
|
+
// One history group for the whole paste, so whatever a consumer's
|
|
558
|
+
// processCellFromClipboard records alongside the values (a format, via
|
|
559
|
+
// recordUndo) comes back with them on a single Ctrl+Z.
|
|
560
|
+
runHistoryGroup(ctx, () => applyPastedCells(text));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** A paste with both types: the handler first, the plain text otherwise. */
|
|
564
|
+
function applyPastedPayload(payload: { text: string; html: string | null; source: "event" | "async" }) {
|
|
565
|
+
runHistoryGroup(ctx, () => {
|
|
566
|
+
const handled = ctx.props.onPasteClipboard?.(payload);
|
|
567
|
+
if (handled === true) return;
|
|
568
|
+
if (payload.text) applyPastedCells(payload.text);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function applyPastedCells(text: string) {
|
|
404
573
|
const anchor = ctx.selectionRange.anchor ?? ctx.grid.getState().activeCell;
|
|
405
574
|
if (!anchor) return;
|
|
406
575
|
const focus = ctx.selectionRange.focus ?? anchor;
|
|
@@ -429,6 +598,15 @@ export function createEditing<
|
|
|
429
598
|
|
|
430
599
|
const next = ctx.internalData.slice() as Array<TData>;
|
|
431
600
|
const dataIndexOf = createDataIndexLookup(next);
|
|
601
|
+
// Every cell the paste changes: one grouped history entry for the lot,
|
|
602
|
+
// so Ctrl+Z takes the whole paste back, and one onCellValueChange each,
|
|
603
|
+
// so a consumer keeping its own model behind the grid (a formula engine)
|
|
604
|
+
// learns the values. The loop used to swap the rows in and stop, which
|
|
605
|
+
// left the undo stack blind to a paste and the consumer never told.
|
|
606
|
+
const changed: Array<{
|
|
607
|
+
row: TData; rowId: string; rowIndex: number; columnId: string; field: string;
|
|
608
|
+
before: unknown; after: unknown;
|
|
609
|
+
}> = [];
|
|
432
610
|
for (let i = 0; i < rowSpan; i += 1) {
|
|
433
611
|
const targetRowIndex = startRow + i;
|
|
434
612
|
const row = ctx.allRows[targetRowIndex];
|
|
@@ -445,21 +623,67 @@ export function createEditing<
|
|
|
445
623
|
const updated: Record<string, unknown> = {
|
|
446
624
|
...(originalRow as Record<string, unknown>),
|
|
447
625
|
};
|
|
626
|
+
const rowChanges: typeof changed = [];
|
|
627
|
+
const record = (column: { id: string; columnDef: { field?: string } }, before: unknown, after: unknown) => {
|
|
628
|
+
if (before === after) return;
|
|
629
|
+
rowChanges.push({
|
|
630
|
+
row: updated as TData, rowId: row.id, rowIndex: dataIndex, columnId: column.id,
|
|
631
|
+
field: column.columnDef.field as string, before, after,
|
|
632
|
+
});
|
|
633
|
+
};
|
|
448
634
|
for (let j = 0; j < colSpan; j += 1) {
|
|
449
635
|
const column = ctx.allColumns[startCol + j];
|
|
450
636
|
if (!column?.columnDef.field) continue;
|
|
451
637
|
if (!isCellEditableAt(targetRowIndex, startCol + j)) continue;
|
|
638
|
+
const before = updated[column.columnDef.field];
|
|
452
639
|
const editorType = (column.columnDef.editorType ??
|
|
453
640
|
"text") as CellEditorType;
|
|
454
641
|
const raw = fillRange ? lines[0]! : sourceCells?.[j] ?? "";
|
|
455
|
-
|
|
642
|
+
const parsedValue = parseEditorValue(editorType, raw, {
|
|
456
643
|
dateOnly: column.columnDef.cellDataType === "dateString",
|
|
457
644
|
});
|
|
645
|
+
// The inbound half of the clipboard pair. A consumer (or a feature
|
|
646
|
+
// pack doing paste-special) gets the raw text AND what the grid would
|
|
647
|
+
// have written, and can return either. `undefined` leaves the cell
|
|
648
|
+
// alone, which is how "paste values only" skips a formula column.
|
|
649
|
+
const hook = ctx.props.processCellFromClipboard;
|
|
650
|
+
if (hook) {
|
|
651
|
+
const decided = hook({
|
|
652
|
+
text: raw,
|
|
653
|
+
parsedValue,
|
|
654
|
+
row: originalRow as TData,
|
|
655
|
+
rowIndex: targetRowIndex,
|
|
656
|
+
columnId: column.id as string,
|
|
657
|
+
});
|
|
658
|
+
if (decided === undefined) continue;
|
|
659
|
+
updated[column.columnDef.field] = decided;
|
|
660
|
+
record(column, before, decided);
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
updated[column.columnDef.field] = parsedValue;
|
|
664
|
+
record(column, before, parsedValue);
|
|
458
665
|
}
|
|
666
|
+
if (!rowChanges.length) continue;
|
|
459
667
|
next[dataIndex] = updated as TData;
|
|
668
|
+
changed.push(...rowChanges);
|
|
460
669
|
}
|
|
670
|
+
if (!changed.length) return;
|
|
461
671
|
ctx.internalData = next;
|
|
462
672
|
ctx.grid.store.setState((prev: any) => ({ ...prev }));
|
|
673
|
+
pushHistory(
|
|
674
|
+
ctx,
|
|
675
|
+
changed.map((c) => ({
|
|
676
|
+
rowId: c.rowId, columnId: c.columnId, field: c.field, before: c.before, after: c.after,
|
|
677
|
+
})),
|
|
678
|
+
nextGroupId(),
|
|
679
|
+
);
|
|
680
|
+
if (ctx.props.onCellValueChange) {
|
|
681
|
+
for (const c of changed) {
|
|
682
|
+
ctx.props.onCellValueChange({
|
|
683
|
+
rowIndex: c.rowIndex, columnId: c.columnId, oldValue: c.before, newValue: c.after, row: c.row,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
463
687
|
}
|
|
464
688
|
|
|
465
689
|
/** Programmatically begin editing a cell (mirrors a double-click). */
|
|
@@ -563,14 +787,15 @@ export function createEditing<
|
|
|
563
787
|
}
|
|
564
788
|
// Record the whole-row change as consecutive history steps.
|
|
565
789
|
if (changed.length) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
790
|
+
// One full-row commit is ONE undo, not one per changed column.
|
|
791
|
+
pushHistory(
|
|
792
|
+
ctx,
|
|
793
|
+
changed.map((c) => ({
|
|
794
|
+
rowId: fr.rowId, columnId: c.columnId, field: c.field,
|
|
795
|
+
before: c.before, after: c.after,
|
|
796
|
+
})),
|
|
797
|
+
nextGroupId(),
|
|
798
|
+
);
|
|
574
799
|
}
|
|
575
800
|
ctx.grid.store.setState((prev: any) => ({ ...prev }));
|
|
576
801
|
if (ctx.props.onCellValueChange && changed.length) {
|
|
@@ -614,5 +839,6 @@ export function createEditing<
|
|
|
614
839
|
onCellDoubleClick,
|
|
615
840
|
pasteFromClipboard,
|
|
616
841
|
onGridPaste,
|
|
842
|
+
armPasteFallback,
|
|
617
843
|
};
|
|
618
844
|
}
|
|
@@ -8,6 +8,14 @@ describe('buildFillPattern - numeric', () => {
|
|
|
8
8
|
expect(buildFillPattern([100, 90, 80], 3)).toEqual([70, 60, 50])
|
|
9
9
|
})
|
|
10
10
|
|
|
11
|
+
it('continues an uneven run of numbers along its linear trend, as Excel does', () => {
|
|
12
|
+
// 1, 2, 4: slope 1.5, intercept -2/3, so the next three are 5.33, 6.83, 8.33.
|
|
13
|
+
const next = buildFillPattern([1, 2, 4], 3) as number[]
|
|
14
|
+
expect(next.map((v) => Number(v.toFixed(4)))).toEqual([5.3333, 6.8333, 8.3333])
|
|
15
|
+
expect((buildFillPattern([10, 20, 40], 2) as number[]).map((v) => Number(v.toFixed(4)))).toEqual([53.3333, 68.3333])
|
|
16
|
+
expect(buildFillPattern([18200, 4200, 1200], 2)).toEqual([-9133.33333333333, -17633.3333333333])
|
|
17
|
+
})
|
|
18
|
+
|
|
11
19
|
it('repeats a single numeric value (Excel default)', () => {
|
|
12
20
|
expect(buildFillPattern([42], 4)).toEqual([42, 42, 42, 42])
|
|
13
21
|
})
|
|
@@ -87,7 +95,34 @@ describe('buildFillPattern - fallback cycle', () => {
|
|
|
87
95
|
])
|
|
88
96
|
})
|
|
89
97
|
|
|
98
|
+
it('repeats a single number held as text, the way a numeric one repeats', () => {
|
|
99
|
+
// A sheet stores what was typed, so 5 arrives as '5'. It is a number,
|
|
100
|
+
// not 'Item 5' with an empty prefix: one copy, no 6, 7, 8.
|
|
101
|
+
expect(buildFillPattern(['5'], 3)).toEqual(['5', '5', '5'])
|
|
102
|
+
expect(buildFillPattern(['Item 5'], 2)).toEqual(['Item 6', 'Item 7'])
|
|
103
|
+
})
|
|
104
|
+
|
|
90
105
|
it('repeats a single non-numeric string', () => {
|
|
91
106
|
expect(buildFillPattern(['hello'], 3)).toEqual(['hello', 'hello', 'hello'])
|
|
92
107
|
})
|
|
108
|
+
|
|
109
|
+
it('steps an ISO date by a day, and by the gap two dates set', () => {
|
|
110
|
+
// Used to read 2024-01-15 as the number 2024 with a suffix and fill
|
|
111
|
+
// 2025-01-15, 2026-01-15.
|
|
112
|
+
expect(buildFillPattern(['2024-01-15'], 3)).toEqual(['2024-01-16', '2024-01-17', '2024-01-18'])
|
|
113
|
+
expect(buildFillPattern(['2024-01-01', '2024-01-08'], 2)).toEqual(['2024-01-15', '2024-01-22'])
|
|
114
|
+
expect(buildFillPattern(['2024-02-28'], 2)).toEqual(['2024-02-29', '2024-03-01'])
|
|
115
|
+
expect(buildFillPattern(['2024-03-03', '2024-03-02'], 2)).toEqual(['2024-03-01', '2024-02-29'])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('steps Date objects by the day and keeps them Dates', () => {
|
|
119
|
+
const out = buildFillPattern([new Date(Date.UTC(2024, 0, 31))], 2)
|
|
120
|
+
expect(out.every((d) => d instanceof Date)).toBe(true)
|
|
121
|
+
expect((out as Date[]).map((d) => d.toISOString().slice(0, 10))).toEqual(['2024-02-01', '2024-02-02'])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('still counts a prefixed number that happens to hold digits and dashes', () => {
|
|
125
|
+
expect(buildFillPattern(['Run-001'], 2)).toEqual(['Run-002', 'Run-003'])
|
|
126
|
+
expect(buildFillPattern(['2024-1-5'], 1)).not.toEqual(['2024-01-06'])
|
|
127
|
+
})
|
|
93
128
|
})
|
package/src/fill-patterns.ts
CHANGED
|
@@ -164,13 +164,54 @@ function arithmeticStep(nums: ReadonlyArray<number>): number | null {
|
|
|
164
164
|
* `sourceValues` already in the selection. See module header for the
|
|
165
165
|
* pattern-detection rules.
|
|
166
166
|
*/
|
|
167
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The sources as day stamps when every one is a date: `Date` objects, or
|
|
171
|
+
* `YYYY-MM-DD` text (read as UTC so a fill never crosses a day boundary
|
|
172
|
+
* with the time zone). Anything else means "not a date series".
|
|
173
|
+
*/
|
|
174
|
+
function datesOf(values: ReadonlyArray<unknown>): { stamps: number[]; iso: boolean } | null {
|
|
175
|
+
if (values.every((v) => v instanceof Date && Number.isFinite(v.getTime()))) {
|
|
176
|
+
return { stamps: values.map((v) => (v as Date).getTime()), iso: false }
|
|
177
|
+
}
|
|
178
|
+
if (values.every((v) => typeof v === 'string' && ISO_DATE.test(v))) {
|
|
179
|
+
const stamps = values.map((v) => Date.parse(`${v}T00:00:00Z`))
|
|
180
|
+
return stamps.every((t) => Number.isFinite(t)) ? { stamps, iso: true } : null
|
|
181
|
+
}
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
|
|
167
185
|
export function buildFillPattern(
|
|
168
186
|
sourceValues: ReadonlyArray<unknown>,
|
|
169
187
|
targetCount: number,
|
|
170
188
|
): unknown[] {
|
|
171
189
|
if (sourceValues.length === 0 || targetCount <= 0) return []
|
|
172
190
|
|
|
173
|
-
// --------
|
|
191
|
+
// -------- 0) Dates step by the day ---------------------------------
|
|
192
|
+
// Before the numeric rule, since a Date coerces to a finite number of
|
|
193
|
+
// milliseconds, and before the prefix rule, which read 2024-01-15 as the
|
|
194
|
+
// number 2024 with a suffix and filled 2025-01-15. Two dates set the step
|
|
195
|
+
// (a week apart fills weekly); one date steps by a day, as Excel's does.
|
|
196
|
+
const dates = datesOf(sourceValues)
|
|
197
|
+
if (dates) {
|
|
198
|
+
const DAY = 86_400_000
|
|
199
|
+
const days = dates.stamps.map((t) => Math.round(t / DAY))
|
|
200
|
+
const step = days.length >= 2 ? arithmeticStep(days) : 1
|
|
201
|
+
if (step !== null) {
|
|
202
|
+
const last = days[days.length - 1]!
|
|
203
|
+
return Array.from({ length: targetCount }, (_, i) => {
|
|
204
|
+
const stamp = (last + step * (i + 1)) * DAY
|
|
205
|
+
return dates.iso ? new Date(stamp).toISOString().slice(0, 10) : new Date(stamp)
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// -------- 1) Numbers: the linear trend (2+ values) -----------------
|
|
211
|
+
// Excel's AutoFill of two or more numbers continues their least-squares
|
|
212
|
+
// line: 1, 2, 3 goes on 4, 5, 6, and 1, 2, 4 goes on 5.33, 6.83, ...
|
|
213
|
+
// rather than repeating. An even step is the same line, so the common
|
|
214
|
+
// case is unchanged.
|
|
174
215
|
const nums = sourceValues.map((v) => Number(v))
|
|
175
216
|
const allNumericLike =
|
|
176
217
|
sourceValues.every((v) => v !== null && v !== '' && v !== undefined) &&
|
|
@@ -181,6 +222,17 @@ export function buildFillPattern(
|
|
|
181
222
|
const last = nums[nums.length - 1]!
|
|
182
223
|
return Array.from({ length: targetCount }, (_, i) => last + step * (i + 1))
|
|
183
224
|
}
|
|
225
|
+
const n = nums.length
|
|
226
|
+
const meanX = (n - 1) / 2
|
|
227
|
+
const meanY = nums.reduce((a, b) => a + b, 0) / n
|
|
228
|
+
let sxy = 0
|
|
229
|
+
let sxx = 0
|
|
230
|
+
for (let i = 0; i < n; i += 1) { sxy += (i - meanX) * (nums[i]! - meanY); sxx += (i - meanX) ** 2 }
|
|
231
|
+
const slope = sxx === 0 ? 0 : sxy / sxx
|
|
232
|
+
const intercept = meanY - slope * meanX
|
|
233
|
+
// Rounded to the precision floating point gives 5.333333333333333, so
|
|
234
|
+
// 10, 20, 40 fills 50, 60 and not 50.00000000000001.
|
|
235
|
+
return Array.from({ length: targetCount }, (_, i) => Number((intercept + slope * (n + i)).toPrecision(15)))
|
|
184
236
|
}
|
|
185
237
|
|
|
186
238
|
// -------- 2) Known string sequence (days / months / quarters) -----
|
|
@@ -200,7 +252,9 @@ export function buildFillPattern(
|
|
|
200
252
|
if (allStrings && sourceValues.length >= 1) {
|
|
201
253
|
const stringSources = sourceValues as string[]
|
|
202
254
|
const split = findPrefixNumberSuffix(stringSources)
|
|
203
|
-
|
|
255
|
+
// A bare number as text ('5') is a number, not 'Item 5': one of them
|
|
256
|
+
// copies, as a numeric 5 does, and two or more were a progression above.
|
|
257
|
+
if (split && (split.prefix !== '' || split.suffix !== '')) {
|
|
204
258
|
const step = split.numbers.length >= 2
|
|
205
259
|
? arithmeticStep(split.numbers) ?? 1
|
|
206
260
|
: 1
|
package/src/grid-icons.ts
CHANGED
|
@@ -97,8 +97,8 @@ export type GridIcons = Partial<Record<GridIconName, Snippet>>
|
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* Icons whose built-in form is a character rather than an SVG path. Kept as
|
|
100
|
-
* data so `<SvGrid>`, its footer and the
|
|
101
|
-
* the same defaults instead of three copies drifting apart.
|
|
100
|
+
* data so `<SvGrid>`, its footer and the row-group panel in @svgrid/enterprise
|
|
101
|
+
* render the same defaults instead of three copies drifting apart.
|
|
102
102
|
*
|
|
103
103
|
* Each is the exact character that shipped inline before, so a grid that sets
|
|
104
104
|
* no `icons` renders byte-for-byte what it always did.
|
package/src/grid-messages.ts
CHANGED
|
@@ -7,11 +7,24 @@
|
|
|
7
7
|
*
|
|
8
8
|
* This mirrors the editor kit's `resolveMessages` pattern (editor-contract.ts) so
|
|
9
9
|
* chrome and editors share one localization idiom.
|
|
10
|
+
*
|
|
11
|
+
* The chart panel's strings are the other half of `GridMessages`
|
|
12
|
+
* (`GridChartPanelMessages` in chart-panel-messages.ts): same map, same
|
|
13
|
+
* `localization.text`, but their English defaults load with the panel, not
|
|
14
|
+
* with every grid.
|
|
10
15
|
*/
|
|
11
16
|
import { resolveMessages } from './editor-contract'
|
|
17
|
+
import type { GridChartPanelMessages } from './chart-panel-messages'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Every string `localization.text` accepts: the grid chrome's own
|
|
21
|
+
* ({@link GridChromeMessages}) and the chart panel's
|
|
22
|
+
* ({@link GridChartPanelMessages}).
|
|
23
|
+
*/
|
|
24
|
+
export type GridMessages = GridChromeMessages & GridChartPanelMessages
|
|
12
25
|
|
|
13
26
|
/** Every localizable chrome string, grouped by area in the comments. */
|
|
14
|
-
export type
|
|
27
|
+
export type GridChromeMessages = {
|
|
15
28
|
// Empty / loading state
|
|
16
29
|
noRows: string
|
|
17
30
|
loading: string
|
|
@@ -93,6 +106,9 @@ export type GridMessages = {
|
|
|
93
106
|
bulkEditCancel: string
|
|
94
107
|
bulkEditNoFields: string
|
|
95
108
|
chartRange: string
|
|
109
|
+
// Rows whose data has not arrived (server-side / infinite loading)
|
|
110
|
+
rowLoadFailed: string
|
|
111
|
+
rowLoadRetry: string
|
|
96
112
|
// Upsell notes (enterprise views not installed)
|
|
97
113
|
pivotUpsellTitle: string
|
|
98
114
|
pivotUpsellBody: string
|
|
@@ -102,7 +118,7 @@ export type GridMessages = {
|
|
|
102
118
|
}
|
|
103
119
|
|
|
104
120
|
/** English defaults - the literal strings the grid shipped before localization. */
|
|
105
|
-
export const defaultGridMessages:
|
|
121
|
+
export const defaultGridMessages: GridChromeMessages = {
|
|
106
122
|
noRows: 'No rows to display.',
|
|
107
123
|
loading: 'Loading grid data...',
|
|
108
124
|
columns: 'Columns',
|
|
@@ -172,6 +188,8 @@ export const defaultGridMessages: GridMessages = {
|
|
|
172
188
|
opBefore: 'Before',
|
|
173
189
|
opAfter: 'After',
|
|
174
190
|
chartRange: 'Chart selected range',
|
|
191
|
+
rowLoadFailed: 'Could not load these rows.',
|
|
192
|
+
rowLoadRetry: 'Retry',
|
|
175
193
|
pivotUpsellTitle: 'Pivot mode',
|
|
176
194
|
pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',
|
|
177
195
|
upsellLicense:
|
|
@@ -184,6 +202,6 @@ export const defaultGridMessages: GridMessages = {
|
|
|
184
202
|
* values fall back to the default (via `resolveMessages`), so a partial map only
|
|
185
203
|
* replaces the keys it sets.
|
|
186
204
|
*/
|
|
187
|
-
export function resolveGridMessages(overrides?: Partial<GridMessages> | null):
|
|
205
|
+
export function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridChromeMessages {
|
|
188
206
|
return resolveMessages(defaultGridMessages, overrides)
|
|
189
207
|
}
|