@svgrid/grid 3.0.3 → 3.0.5
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 +6 -5
- 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 +846 -207
- package/dist/SvGrid.css +178 -2
- package/dist/SvGrid.svelte +717 -720
- package/dist/SvGrid.types.d.ts +566 -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-CID62rlI.js +636 -0
- package/dist/cdn/GridMenus-DLL-qZeu.js +635 -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/server-block-cache-CGoWz-87.js +237 -0
- package/dist/cdn/{src-D6XvA1Ij.js → src-Bklofsad.js} +10615 -10155
- package/dist/cdn/{src-Bugf5XlI.js → src-jcFMkBnN.js} +7111 -6651
- package/dist/cdn/svgrid.js +27 -15
- package/dist/cdn/svgrid.svelte-external.js +27 -15
- package/dist/cdn/validate-C7Rn_Bns.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/gantt-view.svelte.d.ts +24 -0
- package/dist/gantt-view.svelte.js +13 -0
- package/dist/grid-icons.d.ts +2 -2
- package/dist/grid-icons.js +2 -2
- package/dist/grid-messages.d.ts +16 -3
- package/dist/grid-messages.js +10 -0
- package/dist/history.d.ts +83 -0
- package/dist/history.js +133 -0
- package/dist/index.d.ts +29 -10
- package/dist/index.js +23 -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 +4 -0
- package/dist/validate.js +21 -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 +819 -179
- package/src/SvGrid.css +178 -2
- package/src/SvGrid.svelte +717 -720
- package/src/SvGrid.types.ts +564 -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/gantt-stub.test.svelte +38 -0
- package/src/gantt-view.svelte.ts +35 -0
- package/src/grid-icons.ts +2 -2
- package/src/grid-messages.ts +28 -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 +186 -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.gantt-seam.test.ts +199 -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/svgrid.upsell-license.test.ts +7 -4
- package/src/validate.test.ts +29 -0
- package/src/validate.ts +28 -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/GridMenus-DhdPF700.js +0 -603
- 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/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,16 +106,23 @@ 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
|
|
115
|
+
ganttUpsellTitle: string
|
|
116
|
+
ganttUpsellBody: string
|
|
117
|
+
/** Placeholder in the Gantt view's search box. */
|
|
118
|
+
ganttSearchPlaceholder: string
|
|
99
119
|
/** Shared licensing line under every Enterprise upsell note. */
|
|
100
120
|
upsellLicense: string
|
|
101
121
|
upsellLicenseLink: string
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
/** English defaults - the literal strings the grid shipped before localization. */
|
|
105
|
-
export const defaultGridMessages:
|
|
125
|
+
export const defaultGridMessages: GridChromeMessages = {
|
|
106
126
|
noRows: 'No rows to display.',
|
|
107
127
|
loading: 'Loading grid data...',
|
|
108
128
|
columns: 'Columns',
|
|
@@ -172,8 +192,13 @@ export const defaultGridMessages: GridMessages = {
|
|
|
172
192
|
opBefore: 'Before',
|
|
173
193
|
opAfter: 'After',
|
|
174
194
|
chartRange: 'Chart selected range',
|
|
195
|
+
rowLoadFailed: 'Could not load these rows.',
|
|
196
|
+
rowLoadRetry: 'Retry',
|
|
175
197
|
pivotUpsellTitle: 'Pivot mode',
|
|
176
198
|
pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',
|
|
199
|
+
ganttUpsellTitle: 'Gantt view',
|
|
200
|
+
ganttUpsellBody: 'The Gantt view is an Enterprise feature. Install @svgrid/enterprise and call enableGanttView() to render it.',
|
|
201
|
+
ganttSearchPlaceholder: 'Search tasks...',
|
|
177
202
|
upsellLicense:
|
|
178
203
|
'A license key is required for production use. Without one the feature still works, but the grid renders an unlicensed watermark.',
|
|
179
204
|
upsellLicenseLink: 'View licensing',
|
|
@@ -184,6 +209,6 @@ export const defaultGridMessages: GridMessages = {
|
|
|
184
209
|
* values fall back to the default (via `resolveMessages`), so a partial map only
|
|
185
210
|
* replaces the keys it sets.
|
|
186
211
|
*/
|
|
187
|
-
export function resolveGridMessages(overrides?: Partial<GridMessages> | null):
|
|
212
|
+
export function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridChromeMessages {
|
|
188
213
|
return resolveMessages(defaultGridMessages, overrides)
|
|
189
214
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
undoHistory,
|
|
4
|
+
redoHistory,
|
|
5
|
+
pushHistory,
|
|
6
|
+
runHistoryGroup,
|
|
7
|
+
nextGroupId,
|
|
8
|
+
type HistoryStep,
|
|
9
|
+
} from './history'
|
|
10
|
+
|
|
11
|
+
function step(n: number, over: Partial<HistoryStep> = {}): HistoryStep {
|
|
12
|
+
return { rowId: `r${n}`, columnId: 'c', field: 'c', before: n, after: n * 10, ...over }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function makeCtx(history: HistoryStep[] = [], limit = 200) {
|
|
16
|
+
const applied: Array<{ rowId: string; direction: string }> = []
|
|
17
|
+
const ctx = {
|
|
18
|
+
history,
|
|
19
|
+
historyPtr: history.length - 1,
|
|
20
|
+
historyVersion: 0,
|
|
21
|
+
historyGroupId: undefined as string | undefined,
|
|
22
|
+
UNDO_LIMIT: limit,
|
|
23
|
+
applyHistoryStep: vi.fn((s: HistoryStep, direction: 'undo' | 'redo') => {
|
|
24
|
+
applied.push({ rowId: s.rowId, direction })
|
|
25
|
+
}),
|
|
26
|
+
}
|
|
27
|
+
return { ctx, applied }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('undo / redo without groups', () => {
|
|
31
|
+
it('walks one step per call', () => {
|
|
32
|
+
const { ctx, applied } = makeCtx([step(1), step(2)])
|
|
33
|
+
expect(undoHistory(ctx)).toBe(true)
|
|
34
|
+
expect(applied).toEqual([{ rowId: 'r2', direction: 'undo' }])
|
|
35
|
+
expect(ctx.historyPtr).toBe(0)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('returns false at the bottom and the top', () => {
|
|
39
|
+
const { ctx } = makeCtx([])
|
|
40
|
+
expect(undoHistory(ctx)).toBe(false)
|
|
41
|
+
expect(redoHistory(ctx)).toBe(false)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('round-trips undo then redo', () => {
|
|
45
|
+
const { ctx, applied } = makeCtx([step(1)])
|
|
46
|
+
undoHistory(ctx)
|
|
47
|
+
expect(ctx.historyPtr).toBe(-1)
|
|
48
|
+
redoHistory(ctx)
|
|
49
|
+
expect(ctx.historyPtr).toBe(0)
|
|
50
|
+
expect(applied.map((a) => a.direction)).toEqual(['undo', 'redo'])
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('undo / redo with groups', () => {
|
|
55
|
+
it('undoes a whole group in one call', () => {
|
|
56
|
+
const g = 'g1'
|
|
57
|
+
const { ctx, applied } = makeCtx([
|
|
58
|
+
step(1),
|
|
59
|
+
step(2, { groupId: g }), step(3, { groupId: g }), step(4, { groupId: g }),
|
|
60
|
+
])
|
|
61
|
+
expect(undoHistory(ctx)).toBe(true)
|
|
62
|
+
expect(applied.map((a) => a.rowId)).toEqual(['r4', 'r3', 'r2'])
|
|
63
|
+
// The pointer lands before the group, so the next undo takes the lone step.
|
|
64
|
+
expect(ctx.historyPtr).toBe(0)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('undoes a group newest-first so repeated writes to one cell unwind right', () => {
|
|
68
|
+
// Both steps touch r1. Applying oldest-first would leave the cell holding
|
|
69
|
+
// the intermediate value instead of the original.
|
|
70
|
+
const g = 'g1'
|
|
71
|
+
const { ctx, applied } = makeCtx([
|
|
72
|
+
{ rowId: 'r1', columnId: 'c', field: 'c', before: 'orig', after: 'mid', groupId: g },
|
|
73
|
+
{ rowId: 'r1', columnId: 'c', field: 'c', before: 'mid', after: 'final', groupId: g },
|
|
74
|
+
])
|
|
75
|
+
undoHistory(ctx)
|
|
76
|
+
expect(applied).toEqual([
|
|
77
|
+
{ rowId: 'r1', direction: 'undo' },
|
|
78
|
+
{ rowId: 'r1', direction: 'undo' },
|
|
79
|
+
])
|
|
80
|
+
expect(ctx.applyHistoryStep.mock.calls[0]![0].before).toBe('mid')
|
|
81
|
+
expect(ctx.applyHistoryStep.mock.calls[1]![0].before).toBe('orig')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('redoes a whole group in order', () => {
|
|
85
|
+
const g = 'g1'
|
|
86
|
+
const { ctx, applied } = makeCtx([
|
|
87
|
+
step(1, { groupId: g }), step(2, { groupId: g }), step(3, { groupId: g }),
|
|
88
|
+
])
|
|
89
|
+
ctx.historyPtr = -1
|
|
90
|
+
expect(redoHistory(ctx)).toBe(true)
|
|
91
|
+
expect(applied.map((a) => a.rowId)).toEqual(['r1', 'r2', 'r3'])
|
|
92
|
+
expect(ctx.historyPtr).toBe(2)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('keeps two adjacent groups separate', () => {
|
|
96
|
+
const { ctx, applied } = makeCtx([
|
|
97
|
+
step(1, { groupId: 'g1' }), step(2, { groupId: 'g1' }),
|
|
98
|
+
step(3, { groupId: 'g2' }), step(4, { groupId: 'g2' }),
|
|
99
|
+
])
|
|
100
|
+
undoHistory(ctx)
|
|
101
|
+
expect(applied.map((a) => a.rowId)).toEqual(['r4', 'r3'])
|
|
102
|
+
expect(ctx.historyPtr).toBe(1)
|
|
103
|
+
undoHistory(ctx)
|
|
104
|
+
expect(applied.map((a) => a.rowId)).toEqual(['r4', 'r3', 'r2', 'r1'])
|
|
105
|
+
expect(ctx.historyPtr).toBe(-1)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('pushHistory', () => {
|
|
110
|
+
it('truncates forward history so an edit invalidates redo', () => {
|
|
111
|
+
const { ctx } = makeCtx([step(1), step(2), step(3)])
|
|
112
|
+
ctx.historyPtr = 0
|
|
113
|
+
pushHistory(ctx, [step(9)])
|
|
114
|
+
expect(ctx.history.map((s) => s.rowId)).toEqual(['r1', 'r9'])
|
|
115
|
+
expect(ctx.historyPtr).toBe(1)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('drops the OLDEST entries when the buffer is full', () => {
|
|
119
|
+
const { ctx } = makeCtx([step(1), step(2), step(3)], 3)
|
|
120
|
+
pushHistory(ctx, [step(4)])
|
|
121
|
+
expect(ctx.history.map((s) => s.rowId)).toEqual(['r2', 'r3', 'r4'])
|
|
122
|
+
expect(ctx.historyPtr).toBe(2)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('does not tag a group of one', () => {
|
|
126
|
+
const { ctx } = makeCtx()
|
|
127
|
+
pushHistory(ctx, [step(1)], nextGroupId())
|
|
128
|
+
expect(ctx.history[0]!.groupId).toBeUndefined()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('tags every step when several are pushed together', () => {
|
|
132
|
+
const { ctx } = makeCtx()
|
|
133
|
+
pushHistory(ctx, [step(1), step(2)], nextGroupId())
|
|
134
|
+
const ids = ctx.history.map((s) => s.groupId)
|
|
135
|
+
expect(ids[0]).toBeDefined()
|
|
136
|
+
expect(new Set(ids).size).toBe(1)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('ignores an empty push', () => {
|
|
140
|
+
const { ctx } = makeCtx()
|
|
141
|
+
pushHistory(ctx, [])
|
|
142
|
+
expect(ctx.history).toHaveLength(0)
|
|
143
|
+
expect(ctx.historyVersion).toBe(0)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('runHistoryGroup', () => {
|
|
148
|
+
it('joins separate pushes into one undoable action', () => {
|
|
149
|
+
const { ctx, applied } = makeCtx()
|
|
150
|
+
runHistoryGroup(ctx, () => {
|
|
151
|
+
pushHistory(ctx, [step(1)])
|
|
152
|
+
pushHistory(ctx, [step(2)])
|
|
153
|
+
pushHistory(ctx, [step(3)])
|
|
154
|
+
})
|
|
155
|
+
expect(new Set(ctx.history.map((s) => s.groupId)).size).toBe(1)
|
|
156
|
+
undoHistory(ctx)
|
|
157
|
+
expect(applied.map((a) => a.rowId)).toEqual(['r3', 'r2', 'r1'])
|
|
158
|
+
expect(ctx.historyPtr).toBe(-1)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('survives the buffer overflowing mid-group', () => {
|
|
162
|
+
// This is why the group id is ambient rather than stamped afterwards: the
|
|
163
|
+
// cap drops entries from the front, so any index captured before the call
|
|
164
|
+
// would point at the wrong steps by the end of it.
|
|
165
|
+
const { ctx } = makeCtx([step(0)], 3)
|
|
166
|
+
runHistoryGroup(ctx, () => {
|
|
167
|
+
pushHistory(ctx, [step(1)])
|
|
168
|
+
pushHistory(ctx, [step(2)])
|
|
169
|
+
pushHistory(ctx, [step(3)])
|
|
170
|
+
})
|
|
171
|
+
expect(ctx.history.map((s) => s.rowId)).toEqual(['r1', 'r2', 'r3'])
|
|
172
|
+
expect(new Set(ctx.history.map((s) => s.groupId)).size).toBe(1)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('nests into the outer group so one action is one undo', () => {
|
|
176
|
+
const { ctx } = makeCtx()
|
|
177
|
+
runHistoryGroup(ctx, () => {
|
|
178
|
+
pushHistory(ctx, [step(1)])
|
|
179
|
+
runHistoryGroup(ctx, () => pushHistory(ctx, [step(2)]))
|
|
180
|
+
})
|
|
181
|
+
expect(new Set(ctx.history.map((s) => s.groupId)).size).toBe(1)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('clears the ambient id when the body throws', () => {
|
|
185
|
+
const { ctx } = makeCtx()
|
|
186
|
+
expect(() => runHistoryGroup(ctx, () => { throw new Error('boom') })).toThrow('boom')
|
|
187
|
+
expect(ctx.historyGroupId).toBeUndefined()
|
|
188
|
+
pushHistory(ctx, [step(1)])
|
|
189
|
+
expect(ctx.history[0]!.groupId).toBeUndefined()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('returns the body result', () => {
|
|
193
|
+
const { ctx } = makeCtx()
|
|
194
|
+
expect(runHistoryGroup(ctx, () => 42)).toBe(42)
|
|
195
|
+
})
|
|
196
|
+
})
|
package/src/history.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undo / redo over the grid's history array, group-aware.
|
|
3
|
+
*
|
|
4
|
+
* The history is one ordered array of per-cell steps plus a pointer to the
|
|
5
|
+
* last applied step (the model lives in SvGrid.controller.svelte.ts). A step
|
|
6
|
+
* describes ONE cell, which is right for a single edit and wrong for anything
|
|
7
|
+
* that writes many cells at once: `applyMoveRange` in clipboard.ts pushes one
|
|
8
|
+
* step per moved cell, so undoing a 15x20 range move takes 300 presses of
|
|
9
|
+
* Ctrl+Z, and with UNDO_LIMIT at 200 the same move evicts the entire history
|
|
10
|
+
* behind it.
|
|
11
|
+
*
|
|
12
|
+
* `groupId` fixes that without changing the storage model. Steps written
|
|
13
|
+
* inside one logical action share an id; undo and redo walk the whole run of
|
|
14
|
+
* steps carrying that id and apply them together, so a paste, a fill-down or
|
|
15
|
+
* a range move is one press, the way it is in a spreadsheet. Steps with no
|
|
16
|
+
* groupId behave exactly as before, one step per press.
|
|
17
|
+
*
|
|
18
|
+
* Undo applies a group in REVERSE order. Two steps that touched the same cell
|
|
19
|
+
* inside one action have to be unwound newest-first or the older `before`
|
|
20
|
+
* value is overwritten by the newer one and the cell lands on the wrong value.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type HistoryStep = {
|
|
24
|
+
rowId: string
|
|
25
|
+
columnId: string
|
|
26
|
+
field: string
|
|
27
|
+
before: unknown
|
|
28
|
+
after: unknown
|
|
29
|
+
/** Set when this step is part of a multi-cell action. Steps sharing an id
|
|
30
|
+
* are contiguous in the array and undo / redo as one. */
|
|
31
|
+
groupId?: string
|
|
32
|
+
/**
|
|
33
|
+
* A step that is not a cell write: something outside the grid's data that
|
|
34
|
+
* still belongs in the same Ctrl+Z stream, such as a cell format kept in a
|
|
35
|
+
* consumer's store. The cell fields are empty on such a step and the two
|
|
36
|
+
* functions do the work. Never serialised, since a closure cannot be.
|
|
37
|
+
*/
|
|
38
|
+
custom?: { undo: () => void; redo: () => void }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Ctx surface these helpers touch. The controller satisfies it structurally. */
|
|
42
|
+
type HistoryCtx = {
|
|
43
|
+
history: HistoryStep[]
|
|
44
|
+
historyPtr: number
|
|
45
|
+
historyVersion: number
|
|
46
|
+
UNDO_LIMIT: number
|
|
47
|
+
/** Set while a `runHistoryGroup` call is on the stack. Every step pushed
|
|
48
|
+
* inside it joins that group, so a command does not have to thread a group
|
|
49
|
+
* id down through the writers it calls. */
|
|
50
|
+
historyGroupId: string | undefined
|
|
51
|
+
applyHistoryStep(step: HistoryStep, direction: 'undo' | 'redo'): void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Index of the first step in the group that ends at `end`, or `end` itself
|
|
55
|
+
* when that step is ungrouped. */
|
|
56
|
+
function groupStart(history: ReadonlyArray<HistoryStep>, end: number): number {
|
|
57
|
+
const id = history[end]?.groupId
|
|
58
|
+
if (!id) return end
|
|
59
|
+
let i = end
|
|
60
|
+
while (i > 0 && history[i - 1]?.groupId === id) i -= 1
|
|
61
|
+
return i
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Index of the last step in the group that begins at `start`, or `start`
|
|
65
|
+
* itself when that step is ungrouped. */
|
|
66
|
+
function groupEnd(history: ReadonlyArray<HistoryStep>, start: number): number {
|
|
67
|
+
const id = history[start]?.groupId
|
|
68
|
+
if (!id) return start
|
|
69
|
+
let i = start
|
|
70
|
+
while (i < history.length - 1 && history[i + 1]?.groupId === id) i += 1
|
|
71
|
+
return i
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Undo one action. Returns false when there is nothing left to undo. */
|
|
75
|
+
export function undoHistory(ctx: HistoryCtx): boolean {
|
|
76
|
+
if (ctx.historyPtr < 0) return false
|
|
77
|
+
const end = ctx.historyPtr
|
|
78
|
+
if (!ctx.history[end]) return false
|
|
79
|
+
const start = groupStart(ctx.history, end)
|
|
80
|
+
// Reverse order: see the note at the top of the file.
|
|
81
|
+
for (let i = end; i >= start; i -= 1) {
|
|
82
|
+
const step = ctx.history[i]
|
|
83
|
+
if (step) ctx.applyHistoryStep(step, 'undo')
|
|
84
|
+
}
|
|
85
|
+
ctx.historyPtr = start - 1
|
|
86
|
+
ctx.historyVersion += 1
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Redo one action. Returns false when there is nothing left to redo. */
|
|
91
|
+
export function redoHistory(ctx: HistoryCtx): boolean {
|
|
92
|
+
if (ctx.historyPtr >= ctx.history.length - 1) return false
|
|
93
|
+
const start = ctx.historyPtr + 1
|
|
94
|
+
if (!ctx.history[start]) return false
|
|
95
|
+
const end = groupEnd(ctx.history, start)
|
|
96
|
+
for (let i = start; i <= end; i += 1) {
|
|
97
|
+
const step = ctx.history[i]
|
|
98
|
+
if (step) ctx.applyHistoryStep(step, 'redo')
|
|
99
|
+
}
|
|
100
|
+
ctx.historyPtr = end
|
|
101
|
+
ctx.historyVersion += 1
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Append steps at the pointer, truncating any forward history (the standard
|
|
107
|
+
* "an edit invalidates redo" rule) and capping the buffer at UNDO_LIMIT by
|
|
108
|
+
* dropping the OLDEST entries.
|
|
109
|
+
*
|
|
110
|
+
* Every writer goes through here rather than open-coding the slice / push /
|
|
111
|
+
* cap dance, which editing.ts and clipboard.ts previously did three times
|
|
112
|
+
* with three slightly different shapes.
|
|
113
|
+
*/
|
|
114
|
+
export function pushHistory(
|
|
115
|
+
ctx: HistoryCtx,
|
|
116
|
+
steps: ReadonlyArray<HistoryStep>,
|
|
117
|
+
groupId?: string,
|
|
118
|
+
): void {
|
|
119
|
+
if (!steps.length) return
|
|
120
|
+
// An ambient group wins: inside runHistoryGroup even a single step has to be
|
|
121
|
+
// tagged, because a later push in the same action must join it.
|
|
122
|
+
//
|
|
123
|
+
// Outside one, a group of one is just a step. Tagging it would make undo do
|
|
124
|
+
// the same work through a slower path and would show up in serialized state.
|
|
125
|
+
const id = ctx.historyGroupId ?? (groupId && steps.length > 1 ? groupId : undefined)
|
|
126
|
+
let next = ctx.history.slice(0, ctx.historyPtr + 1)
|
|
127
|
+
for (const step of steps) next.push(id ? { ...step, groupId: id } : step)
|
|
128
|
+
if (next.length > ctx.UNDO_LIMIT) next = next.slice(next.length - ctx.UNDO_LIMIT)
|
|
129
|
+
ctx.history = next
|
|
130
|
+
ctx.historyPtr = ctx.history.length - 1
|
|
131
|
+
ctx.historyVersion += 1
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let groupCounter = 0
|
|
135
|
+
|
|
136
|
+
/** A fresh group id. Monotonic, not random: ids never leave the session and a
|
|
137
|
+
* counter is comparable in a test snapshot. */
|
|
138
|
+
export function nextGroupId(): string {
|
|
139
|
+
groupCounter += 1
|
|
140
|
+
return `g${groupCounter}`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Run `fn` with every history step it writes tagged as one undoable action.
|
|
145
|
+
*
|
|
146
|
+
* This is ambient rather than a wrapper that re-tags afterwards, because
|
|
147
|
+
* `pushHistory` can drop the oldest entries when the buffer hits UNDO_LIMIT.
|
|
148
|
+
* A re-tagging implementation would compute its slice bounds from a length
|
|
149
|
+
* that had shifted underneath it and stamp the wrong steps.
|
|
150
|
+
*
|
|
151
|
+
* Nested calls join the outer group: one user action is one undo, however many
|
|
152
|
+
* layers of helper it goes through.
|
|
153
|
+
*/
|
|
154
|
+
export function runHistoryGroup<T>(ctx: HistoryCtx, fn: () => T): T {
|
|
155
|
+
const outer = ctx.historyGroupId
|
|
156
|
+
ctx.historyGroupId = outer ?? nextGroupId()
|
|
157
|
+
try {
|
|
158
|
+
return fn()
|
|
159
|
+
} finally {
|
|
160
|
+
ctx.historyGroupId = outer
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/icon-seam.test.ts
CHANGED
|
@@ -20,26 +20,25 @@ const SRC = join(process.cwd(), 'src')
|
|
|
20
20
|
const read = (f: string) => readFileSync(join(SRC, f), 'utf8')
|
|
21
21
|
|
|
22
22
|
/** Files that render grid chrome and therefore must go through the seam. */
|
|
23
|
-
const CHROME = ['SvGrid.svelte', 'GridMenus.svelte', 'GridFooter.svelte'
|
|
23
|
+
const CHROME = ['SvGrid.svelte', 'GridMenus.svelte', 'GridFooter.svelte']
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Drop the regions allowed to contain raw icon markup: the `icon` snippet's own
|
|
27
|
-
* body (which IS the built-in set)
|
|
28
|
-
* every `<script>` block.
|
|
27
|
+
* body (which IS the built-in set) and every `<script>` block.
|
|
29
28
|
*/
|
|
30
29
|
function stripAllowedRegions(src: string): string {
|
|
31
30
|
let out = src.replace(/<script[\s\S]*?<\/script>/g, '')
|
|
32
31
|
// The built-in catalogue: from the snippet header to its closing tag.
|
|
33
32
|
out = out.replace(/\{#snippet icon\(name: GridIconName\)\}[\s\S]*?\n\{\/snippet\}/g, '')
|
|
34
|
-
out = out.replace(/\{#snippet ic\(name: GridIconName\)\}[\s\S]*?\{\/snippet\}/g, '')
|
|
35
33
|
return out
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
describe('icon seam', () => {
|
|
39
37
|
it('leaves no inline <svg> outside the built-in catalogue', () => {
|
|
40
38
|
// Data visualisation is not chrome: these draw values, not affordances, and
|
|
41
|
-
// there is nothing for a consumer's icon set to say about them.
|
|
42
|
-
|
|
39
|
+
// there is nothing for a consumer's icon set to say about them. Nor is a
|
|
40
|
+
// context-menu item's own path icon: that IS the consumer's drawing.
|
|
41
|
+
const ALLOWED = ['sv-grid-sparkline', 'sv-grid-menu-icon-own']
|
|
43
42
|
|
|
44
43
|
const offenders: string[] = []
|
|
45
44
|
for (const file of CHROME) {
|