@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
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,
|
|
@@ -132,11 +128,29 @@ export function createEditing<
|
|
|
132
128
|
rowRef: row.original,
|
|
133
129
|
};
|
|
134
130
|
ctx.setActiveCell(rowIndex, colIndex);
|
|
135
|
-
|
|
131
|
+
selectForEdit(rowIndex, colIndex);
|
|
136
132
|
return true;
|
|
137
133
|
}
|
|
138
134
|
|
|
135
|
+
/**
|
|
136
|
+
* The selection an edit starts with. A cell already inside the selection
|
|
137
|
+
* leaves it standing: typing into the active cell of a selected block
|
|
138
|
+
* used to collapse the block to that one cell, so Enter could never walk
|
|
139
|
+
* on inside it. A cell outside the selection becomes the selection.
|
|
140
|
+
*/
|
|
141
|
+
function selectForEdit(rowIndex: number, colIndex: number) {
|
|
142
|
+
if (ctx.isCellInSelectedRange(rowIndex, colIndex)) return;
|
|
143
|
+
ctx.setSelection(rowIndex, colIndex);
|
|
144
|
+
}
|
|
145
|
+
|
|
139
146
|
function saveEditingCell() {
|
|
147
|
+
// One group: a consumer's onCellValueChange may record a step of its own
|
|
148
|
+
// (a spreadsheet gives "12%" a percent format as it lands), and that
|
|
149
|
+
// belongs to the same Ctrl+Z as the value.
|
|
150
|
+
runHistoryGroup(ctx, () => commitEditingCell());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function commitEditingCell() {
|
|
140
154
|
if (!ctx.editingCell) return;
|
|
141
155
|
const editing = ctx.editingCell;
|
|
142
156
|
// Resolve the row this edit belongs to. `allRows` is only the current page,
|
|
@@ -197,19 +211,7 @@ export function createEditing<
|
|
|
197
211
|
before: oldValue,
|
|
198
212
|
after: finalValue,
|
|
199
213
|
}
|
|
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
|
|
214
|
+
pushHistory(ctx, [step])
|
|
213
215
|
}
|
|
214
216
|
// Notify the consumer AFTER the row has been updated so any callback-
|
|
215
217
|
// driven recompute (cascade totals, server save, undo stack) sees the
|
|
@@ -230,6 +232,11 @@ export function createEditing<
|
|
|
230
232
|
/** Apply an undo / redo step directly to the underlying row, bypassing
|
|
231
233
|
* the editor pipeline so we don't accidentally re-push to the stack. */
|
|
232
234
|
function applyHistoryStep(step: HistoryStep, direction: 'undo' | 'redo') {
|
|
235
|
+
if (step.custom) {
|
|
236
|
+
if (direction === 'undo') step.custom.undo()
|
|
237
|
+
else step.custom.redo()
|
|
238
|
+
return
|
|
239
|
+
}
|
|
233
240
|
const row = ctx.allRows.find((r: any) => r.id === step.rowId)
|
|
234
241
|
const col = ctx.allColumns.find((c: any) => c.id === step.columnId)
|
|
235
242
|
if (!row?.original || !col) return
|
|
@@ -261,26 +268,111 @@ export function createEditing<
|
|
|
261
268
|
* move, Tab committed the edit and then let the browser walk focus out of the
|
|
262
269
|
* grid entirely (#48). Shared by every editor type, including the textarea.
|
|
263
270
|
*/
|
|
264
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Commit the edit and step the cursor the way data entry expects: Tab a
|
|
273
|
+
* column to the right (wrapping at the row's end), Enter a row down, Shift
|
|
274
|
+
* reversing either. Enter used to commit and stay, so a column of numbers
|
|
275
|
+
* took a click or an arrow between every value; Excel and every other
|
|
276
|
+
* sheet move down, and the accessibility page has always said this grid
|
|
277
|
+
* does too. At the last row or column the cursor stays put.
|
|
278
|
+
*/
|
|
279
|
+
function commitAndMove(intent: "tabNext" | "tabPrev" | "moveDown" | "moveUp") {
|
|
265
280
|
const active = ctx.activeCell;
|
|
266
281
|
saveEditingCell();
|
|
267
282
|
ctx.gridRootEl?.focus({ preventScroll: true });
|
|
268
283
|
if (!active) return;
|
|
269
|
-
|
|
284
|
+
// The same stepping as Enter and Tab on the grid root: a Tab run's Enter
|
|
285
|
+
// goes back to the run's first column, and a selected block keeps the
|
|
286
|
+
// cursor inside it.
|
|
287
|
+
const step = getEntryStep(active, intent, {
|
|
270
288
|
maxRow: ctx.allRows.length - 1,
|
|
271
289
|
maxCol: ctx.allColumns.length - 1,
|
|
290
|
+
tabOrigin: ctx.tabRunOrigin,
|
|
291
|
+
range: ctx.activeRangeRect(),
|
|
292
|
+
collapsed: {
|
|
293
|
+
isRowCollapsed: (i: number) => ctx.isRowCollapsed(i) as boolean,
|
|
294
|
+
isColumnCollapsed: (i: number) => !!ctx.collapsedColumns[ctx.allColumns[i]?.id],
|
|
295
|
+
},
|
|
272
296
|
});
|
|
297
|
+
const next = step.cell;
|
|
273
298
|
ctx.setActiveCell(next.rowIndex, next.colIndex);
|
|
274
|
-
ctx.
|
|
299
|
+
ctx.tabRunOrigin = step.tabOrigin;
|
|
300
|
+
if (!step.withinRange) ctx.setSelection(next.rowIndex, next.colIndex);
|
|
275
301
|
ctx.scrollActiveCellIntoView(next.rowIndex, next.colIndex);
|
|
276
302
|
}
|
|
277
303
|
|
|
304
|
+
function commitAndMoveByTab(shiftKey: boolean) {
|
|
305
|
+
commitAndMove(shiftKey ? "tabPrev" : "tabNext");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Ctrl+Enter: the entry goes into every cell of the selected block and the
|
|
310
|
+
* cursor stays where it is, as in Excel, where it is how a block is filled
|
|
311
|
+
* with one value in one go. Each cell takes the same raw entry through the
|
|
312
|
+
* ordinary commit, so its column's parser, the history and
|
|
313
|
+
* `onCellValueChange` all see it, and the whole block is one undo. A cell
|
|
314
|
+
* that cannot be edited is left alone. With nothing but the active cell
|
|
315
|
+
* selected it is a commit that does not move.
|
|
316
|
+
*/
|
|
317
|
+
function commitToSelection() {
|
|
318
|
+
const editing = ctx.editingCell;
|
|
319
|
+
if (!editing) return;
|
|
320
|
+
const raw = editing.value;
|
|
321
|
+
const active = ctx.activeCell;
|
|
322
|
+
const range = ctx.activeRangeRect();
|
|
323
|
+
runHistoryGroup(ctx, () => {
|
|
324
|
+
commitEditingCell();
|
|
325
|
+
if (!active || !range) return;
|
|
326
|
+
for (let r = range.minRow; r <= range.maxRow; r += 1) {
|
|
327
|
+
for (let c = range.minCol; c <= range.maxCol; c += 1) {
|
|
328
|
+
if (r === active.rowIndex && c === active.colIndex) continue;
|
|
329
|
+
const row = ctx.allRows[r];
|
|
330
|
+
const column = ctx.allColumns[c];
|
|
331
|
+
if (!row || !column || isGroupRow(row) || !isCellEditable(column, row)) continue;
|
|
332
|
+
ctx.editingCell = {
|
|
333
|
+
rowId: row.id,
|
|
334
|
+
columnId: column.id,
|
|
335
|
+
editorType: (column.columnDef.editorType ?? "text") as CellEditorType,
|
|
336
|
+
value: raw,
|
|
337
|
+
rowRef: row.original,
|
|
338
|
+
};
|
|
339
|
+
commitEditingCell();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
ctx.gridRootEl?.focus({ preventScroll: true });
|
|
344
|
+
}
|
|
345
|
+
|
|
278
346
|
function onEditorKeyDown(event: KeyboardEvent) {
|
|
279
347
|
event.stopPropagation();
|
|
280
|
-
|
|
348
|
+
// Registered commands get the key first here too, with `editing: true`, so
|
|
349
|
+
// a handler can claim a combination that only means something mid-edit
|
|
350
|
+
// (Alt+Enter for a newline, F4 to cycle a reference between relative and
|
|
351
|
+
// absolute) before the editor's own Enter / Tab / Escape handling runs.
|
|
352
|
+
if (hasGridShortcuts() && runGridShortcuts(event, buildCommandContext(ctx, true))) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (event.key === "Enter" && event.altKey) {
|
|
356
|
+
// A line break, in an editor that can hold one (`editorMultiline`).
|
|
357
|
+
// In a single-line editor Alt+Enter is just Enter.
|
|
358
|
+
const el = event.currentTarget;
|
|
359
|
+
if (el instanceof HTMLTextAreaElement) {
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
const start = el.selectionStart ?? el.value.length;
|
|
362
|
+
const end = el.selectionEnd ?? start;
|
|
363
|
+
el.value = el.value.slice(0, start) + "\n" + el.value.slice(end);
|
|
364
|
+
el.selectionStart = el.selectionEnd = start + 1;
|
|
365
|
+
updateEditingCellValue(el.value);
|
|
366
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
281
371
|
event.preventDefault();
|
|
282
|
-
|
|
283
|
-
|
|
372
|
+
commitToSelection();
|
|
373
|
+
} else if (event.key === "Enter") {
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
commitAndMove(event.shiftKey ? "moveUp" : "moveDown");
|
|
284
376
|
} else if (event.key === "Tab") {
|
|
285
377
|
event.preventDefault();
|
|
286
378
|
commitAndMoveByTab(event.shiftKey);
|
|
@@ -356,7 +448,7 @@ export function createEditing<
|
|
|
356
448
|
rowRef: row.original,
|
|
357
449
|
};
|
|
358
450
|
ctx.setActiveCell(rowIndex, colIndex);
|
|
359
|
-
|
|
451
|
+
selectForEdit(rowIndex, colIndex);
|
|
360
452
|
}
|
|
361
453
|
|
|
362
454
|
async function pasteFromClipboard() {
|
|
@@ -366,6 +458,28 @@ export function createEditing<
|
|
|
366
458
|
// On plain HTTP (a XAMPP/Apache LAN host) `navigator.clipboard` is
|
|
367
459
|
// undefined - there the Ctrl+V keydown handler skips preventDefault and
|
|
368
460
|
// lets the browser deliver a native `paste` event to `onGridPaste`.
|
|
461
|
+
if (ctx.props.onPasteClipboard && navigator.clipboard?.read) {
|
|
462
|
+
// Both types, for a handler that reads the HTML.
|
|
463
|
+
try {
|
|
464
|
+
const items = await navigator.clipboard.read();
|
|
465
|
+
let text = "";
|
|
466
|
+
let html: string | null = null;
|
|
467
|
+
for (const item of items) {
|
|
468
|
+
if (item.types.includes("text/html")) html = await (await item.getType("text/html")).text();
|
|
469
|
+
if (item.types.includes("text/plain")) text = await (await item.getType("text/plain")).text();
|
|
470
|
+
}
|
|
471
|
+
if (text || html) applyPastedPayload({ text, html, source: "async" });
|
|
472
|
+
} catch {
|
|
473
|
+
// No permission or nothing readable: the text-only read below may still work.
|
|
474
|
+
if (!navigator.clipboard.readText) return;
|
|
475
|
+
try {
|
|
476
|
+
applyPastedPayload({ text: await navigator.clipboard.readText(), html: null, source: "async" });
|
|
477
|
+
} catch {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
369
483
|
if (!navigator.clipboard?.readText) return;
|
|
370
484
|
let text: string;
|
|
371
485
|
try {
|
|
@@ -376,6 +490,26 @@ export function createEditing<
|
|
|
376
490
|
applyPastedText(text);
|
|
377
491
|
}
|
|
378
492
|
|
|
493
|
+
/**
|
|
494
|
+
* With `onPasteClipboard` set, Ctrl+V leaves the key to the browser so the
|
|
495
|
+
* `paste` event arrives with the HTML. A browser that does not deliver
|
|
496
|
+
* that event to the grid root (Firefox, a non-editable element) would
|
|
497
|
+
* paste nothing, so the async read is armed to run shortly after unless
|
|
498
|
+
* the event lands first and disarms it.
|
|
499
|
+
*/
|
|
500
|
+
let pasteFallback: ReturnType<typeof setTimeout> | null = null;
|
|
501
|
+
function armPasteFallback() {
|
|
502
|
+
if (pasteFallback) clearTimeout(pasteFallback);
|
|
503
|
+
pasteFallback = setTimeout(() => {
|
|
504
|
+
pasteFallback = null;
|
|
505
|
+
void pasteFromClipboard();
|
|
506
|
+
}, 80);
|
|
507
|
+
}
|
|
508
|
+
function disarmPasteFallback() {
|
|
509
|
+
if (pasteFallback) clearTimeout(pasteFallback);
|
|
510
|
+
pasteFallback = null;
|
|
511
|
+
}
|
|
512
|
+
|
|
379
513
|
/**
|
|
380
514
|
* Native `paste` ClipboardEvent fallback (insecure-context path). A real
|
|
381
515
|
* Ctrl/Cmd+V on the focused grid root fires this with `clipboardData`
|
|
@@ -390,6 +524,17 @@ export function createEditing<
|
|
|
390
524
|
// preventDefault the editor's paste and write to the data cell instead,
|
|
391
525
|
// so the edit "wouldn't update".
|
|
392
526
|
if (ctx.editingCell) return;
|
|
527
|
+
if (ctx.props.onPasteClipboard) {
|
|
528
|
+
// The event is the preferred path for a handler: it carries the HTML
|
|
529
|
+
// as the source wrote it, needs no permission, and works over HTTP.
|
|
530
|
+
disarmPasteFallback();
|
|
531
|
+
const text = event.clipboardData?.getData("text/plain") ?? "";
|
|
532
|
+
const html = event.clipboardData?.getData("text/html") || null;
|
|
533
|
+
if (!text && !html) return;
|
|
534
|
+
event.preventDefault();
|
|
535
|
+
applyPastedPayload({ text, html, source: "event" });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
393
538
|
// When the async API is available the keydown handler already pasted via
|
|
394
539
|
// pasteFromClipboard(); ignore the (suppressed) native event so we don't
|
|
395
540
|
// paste twice.
|
|
@@ -401,6 +546,22 @@ export function createEditing<
|
|
|
401
546
|
}
|
|
402
547
|
|
|
403
548
|
function applyPastedText(text: string) {
|
|
549
|
+
// One history group for the whole paste, so whatever a consumer's
|
|
550
|
+
// processCellFromClipboard records alongside the values (a format, via
|
|
551
|
+
// recordUndo) comes back with them on a single Ctrl+Z.
|
|
552
|
+
runHistoryGroup(ctx, () => applyPastedCells(text));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** A paste with both types: the handler first, the plain text otherwise. */
|
|
556
|
+
function applyPastedPayload(payload: { text: string; html: string | null; source: "event" | "async" }) {
|
|
557
|
+
runHistoryGroup(ctx, () => {
|
|
558
|
+
const handled = ctx.props.onPasteClipboard?.(payload);
|
|
559
|
+
if (handled === true) return;
|
|
560
|
+
if (payload.text) applyPastedCells(payload.text);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function applyPastedCells(text: string) {
|
|
404
565
|
const anchor = ctx.selectionRange.anchor ?? ctx.grid.getState().activeCell;
|
|
405
566
|
if (!anchor) return;
|
|
406
567
|
const focus = ctx.selectionRange.focus ?? anchor;
|
|
@@ -429,6 +590,15 @@ export function createEditing<
|
|
|
429
590
|
|
|
430
591
|
const next = ctx.internalData.slice() as Array<TData>;
|
|
431
592
|
const dataIndexOf = createDataIndexLookup(next);
|
|
593
|
+
// Every cell the paste changes: one grouped history entry for the lot,
|
|
594
|
+
// so Ctrl+Z takes the whole paste back, and one onCellValueChange each,
|
|
595
|
+
// so a consumer keeping its own model behind the grid (a formula engine)
|
|
596
|
+
// learns the values. The loop used to swap the rows in and stop, which
|
|
597
|
+
// left the undo stack blind to a paste and the consumer never told.
|
|
598
|
+
const changed: Array<{
|
|
599
|
+
row: TData; rowId: string; rowIndex: number; columnId: string; field: string;
|
|
600
|
+
before: unknown; after: unknown;
|
|
601
|
+
}> = [];
|
|
432
602
|
for (let i = 0; i < rowSpan; i += 1) {
|
|
433
603
|
const targetRowIndex = startRow + i;
|
|
434
604
|
const row = ctx.allRows[targetRowIndex];
|
|
@@ -445,21 +615,67 @@ export function createEditing<
|
|
|
445
615
|
const updated: Record<string, unknown> = {
|
|
446
616
|
...(originalRow as Record<string, unknown>),
|
|
447
617
|
};
|
|
618
|
+
const rowChanges: typeof changed = [];
|
|
619
|
+
const record = (column: { id: string; columnDef: { field?: string } }, before: unknown, after: unknown) => {
|
|
620
|
+
if (before === after) return;
|
|
621
|
+
rowChanges.push({
|
|
622
|
+
row: updated as TData, rowId: row.id, rowIndex: dataIndex, columnId: column.id,
|
|
623
|
+
field: column.columnDef.field as string, before, after,
|
|
624
|
+
});
|
|
625
|
+
};
|
|
448
626
|
for (let j = 0; j < colSpan; j += 1) {
|
|
449
627
|
const column = ctx.allColumns[startCol + j];
|
|
450
628
|
if (!column?.columnDef.field) continue;
|
|
451
629
|
if (!isCellEditableAt(targetRowIndex, startCol + j)) continue;
|
|
630
|
+
const before = updated[column.columnDef.field];
|
|
452
631
|
const editorType = (column.columnDef.editorType ??
|
|
453
632
|
"text") as CellEditorType;
|
|
454
633
|
const raw = fillRange ? lines[0]! : sourceCells?.[j] ?? "";
|
|
455
|
-
|
|
634
|
+
const parsedValue = parseEditorValue(editorType, raw, {
|
|
456
635
|
dateOnly: column.columnDef.cellDataType === "dateString",
|
|
457
636
|
});
|
|
637
|
+
// The inbound half of the clipboard pair. A consumer (or a feature
|
|
638
|
+
// pack doing paste-special) gets the raw text AND what the grid would
|
|
639
|
+
// have written, and can return either. `undefined` leaves the cell
|
|
640
|
+
// alone, which is how "paste values only" skips a formula column.
|
|
641
|
+
const hook = ctx.props.processCellFromClipboard;
|
|
642
|
+
if (hook) {
|
|
643
|
+
const decided = hook({
|
|
644
|
+
text: raw,
|
|
645
|
+
parsedValue,
|
|
646
|
+
row: originalRow as TData,
|
|
647
|
+
rowIndex: targetRowIndex,
|
|
648
|
+
columnId: column.id as string,
|
|
649
|
+
});
|
|
650
|
+
if (decided === undefined) continue;
|
|
651
|
+
updated[column.columnDef.field] = decided;
|
|
652
|
+
record(column, before, decided);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
updated[column.columnDef.field] = parsedValue;
|
|
656
|
+
record(column, before, parsedValue);
|
|
458
657
|
}
|
|
658
|
+
if (!rowChanges.length) continue;
|
|
459
659
|
next[dataIndex] = updated as TData;
|
|
660
|
+
changed.push(...rowChanges);
|
|
460
661
|
}
|
|
662
|
+
if (!changed.length) return;
|
|
461
663
|
ctx.internalData = next;
|
|
462
664
|
ctx.grid.store.setState((prev: any) => ({ ...prev }));
|
|
665
|
+
pushHistory(
|
|
666
|
+
ctx,
|
|
667
|
+
changed.map((c) => ({
|
|
668
|
+
rowId: c.rowId, columnId: c.columnId, field: c.field, before: c.before, after: c.after,
|
|
669
|
+
})),
|
|
670
|
+
nextGroupId(),
|
|
671
|
+
);
|
|
672
|
+
if (ctx.props.onCellValueChange) {
|
|
673
|
+
for (const c of changed) {
|
|
674
|
+
ctx.props.onCellValueChange({
|
|
675
|
+
rowIndex: c.rowIndex, columnId: c.columnId, oldValue: c.before, newValue: c.after, row: c.row,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
463
679
|
}
|
|
464
680
|
|
|
465
681
|
/** Programmatically begin editing a cell (mirrors a double-click). */
|
|
@@ -563,14 +779,15 @@ export function createEditing<
|
|
|
563
779
|
}
|
|
564
780
|
// Record the whole-row change as consecutive history steps.
|
|
565
781
|
if (changed.length) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
782
|
+
// One full-row commit is ONE undo, not one per changed column.
|
|
783
|
+
pushHistory(
|
|
784
|
+
ctx,
|
|
785
|
+
changed.map((c) => ({
|
|
786
|
+
rowId: fr.rowId, columnId: c.columnId, field: c.field,
|
|
787
|
+
before: c.before, after: c.after,
|
|
788
|
+
})),
|
|
789
|
+
nextGroupId(),
|
|
790
|
+
);
|
|
574
791
|
}
|
|
575
792
|
ctx.grid.store.setState((prev: any) => ({ ...prev }));
|
|
576
793
|
if (ctx.props.onCellValueChange && changed.length) {
|
|
@@ -614,5 +831,6 @@ export function createEditing<
|
|
|
614
831
|
onCellDoubleClick,
|
|
615
832
|
pasteFromClipboard,
|
|
616
833
|
onGridPaste,
|
|
834
|
+
armPasteFallback,
|
|
617
835
|
};
|
|
618
836
|
}
|
|
@@ -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-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
|
|
@@ -102,7 +115,7 @@ export type GridMessages = {
|
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
/** English defaults - the literal strings the grid shipped before localization. */
|
|
105
|
-
export const defaultGridMessages:
|
|
118
|
+
export const defaultGridMessages: GridChromeMessages = {
|
|
106
119
|
noRows: 'No rows to display.',
|
|
107
120
|
loading: 'Loading grid data...',
|
|
108
121
|
columns: 'Columns',
|
|
@@ -184,6 +197,6 @@ export const defaultGridMessages: GridMessages = {
|
|
|
184
197
|
* values fall back to the default (via `resolveMessages`), so a partial map only
|
|
185
198
|
* replaces the keys it sets.
|
|
186
199
|
*/
|
|
187
|
-
export function resolveGridMessages(overrides?: Partial<GridMessages> | null):
|
|
200
|
+
export function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridChromeMessages {
|
|
188
201
|
return resolveMessages(defaultGridMessages, overrides)
|
|
189
202
|
}
|