@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.
Files changed (281) hide show
  1. package/README.md +2 -1
  2. package/dist/GridMenus.svelte +44 -2
  3. package/dist/SvChartMenu.svelte +69 -0
  4. package/dist/SvChartMenu.svelte.d.ts +11 -0
  5. package/dist/SvChartPanes.svelte +113 -0
  6. package/dist/SvChartPanes.svelte.d.ts +20 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +110 -39
  8. package/dist/SvGrid.controller.svelte.js +655 -200
  9. package/dist/SvGrid.css +105 -2
  10. package/dist/SvGrid.svelte +386 -514
  11. package/dist/SvGrid.types.d.ts +210 -9
  12. package/dist/SvGridCellEditor.svelte +17 -0
  13. package/dist/SvGridChart.svelte +2257 -329
  14. package/dist/SvGridChart.svelte.d.ts +6 -106
  15. package/dist/SvGridChart.types.d.ts +338 -0
  16. package/dist/SvGridChart.types.js +1 -0
  17. package/dist/SvGridChartBuilder.svelte +323 -0
  18. package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
  19. package/dist/SvGridChartPanel.svelte +221 -275
  20. package/dist/SvGridChartPickers.svelte +417 -0
  21. package/dist/SvGridChartPickers.svelte.d.ts +70 -0
  22. package/dist/SvGridChartView.svelte +13 -14
  23. package/dist/SvModal.svelte +7 -1
  24. package/dist/SvModal.svelte.d.ts +5 -0
  25. package/dist/ai.d.ts +40 -8
  26. package/dist/ai.js +135 -24
  27. package/dist/aria.d.ts +11 -0
  28. package/dist/build-api.js +108 -22
  29. package/dist/cdn/GridMenus-7kbpnnBW.js +635 -0
  30. package/dist/cdn/GridMenus-BL7ZgQvU.js +636 -0
  31. package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
  32. package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
  33. package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
  34. package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
  35. package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
  36. package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
  37. package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
  38. package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
  39. package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
  40. package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
  41. package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
  42. package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
  43. package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
  44. package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
  45. package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
  46. package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
  47. package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
  48. package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
  49. package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
  50. package/dist/cdn/SvModal-CIZWCcad.js +397 -0
  51. package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
  52. package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
  53. package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
  54. package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
  55. package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
  56. package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
  57. package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
  58. package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
  59. package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
  60. package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
  61. package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
  62. package/dist/cdn/dismissable-DAHetSNk.js +44 -0
  63. package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
  64. package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
  65. package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
  66. package/dist/cdn/{src-DNbz94Ct.js → src-BKhZ6eXd.js} +10227 -9787
  67. package/dist/cdn/{src-BJ2opd0j.js → src-V1uu8iE9.js} +6568 -6128
  68. package/dist/cdn/svgrid.js +26 -15
  69. package/dist/cdn/svgrid.svelte-external.js +26 -15
  70. package/dist/chart-axes.d.ts +53 -0
  71. package/dist/chart-axes.js +351 -0
  72. package/dist/chart-cartesian.d.ts +88 -0
  73. package/dist/chart-cartesian.js +1862 -0
  74. package/dist/chart-decimate.d.ts +51 -0
  75. package/dist/chart-decimate.js +199 -0
  76. package/dist/chart-export-pdf.d.ts +47 -0
  77. package/dist/chart-export-pdf.js +187 -0
  78. package/dist/chart-export.d.ts +2 -1
  79. package/dist/chart-export.js +18 -3
  80. package/dist/chart-financial.d.ts +91 -0
  81. package/dist/chart-financial.js +175 -0
  82. package/dist/chart-flow.d.ts +10 -0
  83. package/dist/chart-flow.js +298 -0
  84. package/dist/chart-format.d.ts +18 -0
  85. package/dist/chart-format.js +65 -0
  86. package/dist/chart-grid.d.ts +8 -0
  87. package/dist/chart-grid.js +190 -0
  88. package/dist/chart-hierarchy.d.ts +17 -0
  89. package/dist/chart-hierarchy.js +146 -0
  90. package/dist/chart-indicators.d.ts +115 -0
  91. package/dist/chart-indicators.js +389 -0
  92. package/dist/chart-messages.d.ts +95 -0
  93. package/dist/chart-messages.js +92 -0
  94. package/dist/chart-motion.d.ts +16 -0
  95. package/dist/chart-motion.js +141 -0
  96. package/dist/chart-panel-messages.d.ts +265 -0
  97. package/dist/chart-panel-messages.js +285 -0
  98. package/dist/chart-pivot.d.ts +87 -0
  99. package/dist/chart-pivot.js +156 -0
  100. package/dist/chart-polar.d.ts +25 -0
  101. package/dist/chart-polar.js +700 -0
  102. package/dist/chart-samples.d.ts +16 -0
  103. package/dist/chart-samples.js +153 -0
  104. package/dist/chart-scale.d.ts +113 -0
  105. package/dist/chart-scale.js +405 -0
  106. package/dist/chart-stats.d.ts +111 -0
  107. package/dist/chart-stats.js +419 -0
  108. package/dist/chart-stream.d.ts +45 -0
  109. package/dist/chart-stream.js +91 -0
  110. package/dist/chart-summary.d.ts +27 -0
  111. package/dist/chart-summary.js +205 -0
  112. package/dist/chart-sync.svelte.d.ts +32 -0
  113. package/dist/chart-sync.svelte.js +23 -0
  114. package/dist/chart-table.d.ts +35 -0
  115. package/dist/chart-table.js +165 -0
  116. package/dist/chart-trend.d.ts +19 -0
  117. package/dist/chart-trend.js +61 -0
  118. package/dist/chart-types.d.ts +1456 -0
  119. package/dist/chart-types.js +6 -0
  120. package/dist/chart-validate.d.ts +43 -0
  121. package/dist/chart-validate.js +272 -0
  122. package/dist/chart-zoom.d.ts +40 -0
  123. package/dist/chart-zoom.js +145 -0
  124. package/dist/chart.d.ts +186 -865
  125. package/dist/chart.js +460 -2204
  126. package/dist/clipboard.d.ts +1 -0
  127. package/dist/clipboard.js +137 -18
  128. package/dist/column-resize.d.ts +3 -0
  129. package/dist/column-resize.js +4 -1
  130. package/dist/columns.js +3 -0
  131. package/dist/command-context.d.ts +19 -0
  132. package/dist/command-context.js +116 -0
  133. package/dist/core.d.ts +7 -0
  134. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  135. package/dist/editing.d.ts +2 -8
  136. package/dist/editing.js +262 -33
  137. package/dist/fill-patterns.d.ts +0 -5
  138. package/dist/fill-patterns.js +56 -2
  139. package/dist/grid-messages.d.ts +10 -3
  140. package/dist/grid-messages.js +5 -0
  141. package/dist/history.d.ts +83 -0
  142. package/dist/history.js +133 -0
  143. package/dist/index.d.ts +24 -6
  144. package/dist/index.js +20 -2
  145. package/dist/keyboard-handlers.js +88 -31
  146. package/dist/keyboard.d.ts +55 -0
  147. package/dist/keyboard.js +132 -0
  148. package/dist/menus.d.ts +3 -1
  149. package/dist/menus.js +7 -2
  150. package/dist/merges.d.ts +97 -0
  151. package/dist/merges.js +147 -0
  152. package/dist/row-resize.d.ts +3 -0
  153. package/dist/row-resize.js +14 -0
  154. package/dist/scroll-sync.d.ts +1 -0
  155. package/dist/scroll-sync.js +24 -0
  156. package/dist/selection.d.ts +16 -0
  157. package/dist/selection.js +108 -16
  158. package/dist/shortcut-registry.d.ts +145 -0
  159. package/dist/shortcut-registry.js +44 -0
  160. package/dist/svgrid-wrapper.types.d.ts +213 -22
  161. package/dist/virtualization/virtualizer.js +38 -17
  162. package/package.json +6 -1
  163. package/src/GridMenus.svelte +44 -2
  164. package/src/SvChartMenu.svelte +69 -0
  165. package/src/SvChartPanes.svelte +113 -0
  166. package/src/SvChartPanes.test.ts +66 -0
  167. package/src/SvGrid.controller.svelte.ts +629 -172
  168. package/src/SvGrid.css +105 -2
  169. package/src/SvGrid.svelte +386 -514
  170. package/src/SvGrid.types.ts +188 -8
  171. package/src/SvGridCellEditor.svelte +17 -0
  172. package/src/SvGridChart.svelte +2257 -329
  173. package/src/SvGridChart.test.ts +1419 -1
  174. package/src/SvGridChart.types.ts +336 -0
  175. package/src/SvGridChartBuilder.svelte +323 -0
  176. package/src/SvGridChartPanel.svelte +221 -275
  177. package/src/SvGridChartPickers.svelte +417 -0
  178. package/src/SvGridChartView.svelte +13 -14
  179. package/src/SvModal.svelte +7 -1
  180. package/src/SvModal.test.ts +9 -0
  181. package/src/ai.test.ts +57 -2
  182. package/src/ai.ts +166 -22
  183. package/src/aria.d.ts +11 -0
  184. package/src/build-api.coverage.test.ts +184 -0
  185. package/src/build-api.ts +107 -18
  186. package/src/chart-axes.test.ts +573 -0
  187. package/src/chart-axes.ts +342 -0
  188. package/src/chart-cartesian.ts +1863 -0
  189. package/src/chart-decimate.test.ts +249 -0
  190. package/src/chart-decimate.ts +186 -0
  191. package/src/chart-export-pdf.test.ts +91 -0
  192. package/src/chart-export-pdf.ts +207 -0
  193. package/src/chart-export.test.ts +14 -0
  194. package/src/chart-export.ts +13 -3
  195. package/src/chart-financial.ts +208 -0
  196. package/src/chart-flow.ts +286 -0
  197. package/src/chart-format.ts +67 -0
  198. package/src/chart-grid.ts +179 -0
  199. package/src/chart-hierarchy.ts +135 -0
  200. package/src/chart-indicators.test.ts +443 -0
  201. package/src/chart-indicators.ts +384 -0
  202. package/src/chart-messages.test.ts +21 -0
  203. package/src/chart-messages.ts +184 -0
  204. package/src/chart-motion.test.ts +71 -0
  205. package/src/chart-motion.ts +148 -0
  206. package/src/chart-panel-messages.test.ts +29 -0
  207. package/src/chart-panel-messages.ts +550 -0
  208. package/src/chart-pivot.test.ts +54 -0
  209. package/src/chart-pivot.ts +225 -0
  210. package/src/chart-polar.ts +674 -0
  211. package/src/chart-samples.ts +106 -0
  212. package/src/chart-scale.ts +394 -0
  213. package/src/chart-series-types.test.ts +714 -0
  214. package/src/chart-stats.ts +409 -0
  215. package/src/chart-stream.test.ts +75 -0
  216. package/src/chart-stream.ts +114 -0
  217. package/src/chart-summary.test.ts +90 -0
  218. package/src/chart-summary.ts +197 -0
  219. package/src/chart-sync.svelte.ts +41 -0
  220. package/src/chart-table.test.ts +76 -0
  221. package/src/chart-table.ts +178 -0
  222. package/src/chart-trend.ts +47 -0
  223. package/src/chart-types.ts +1309 -0
  224. package/src/chart-validate.test.ts +100 -0
  225. package/src/chart-validate.ts +247 -0
  226. package/src/chart-zoom.test.ts +137 -0
  227. package/src/chart-zoom.ts +124 -0
  228. package/src/chart.coverage.test.ts +81 -0
  229. package/src/chart.test.ts +56 -3
  230. package/src/chart.ts +557 -2784
  231. package/src/clipboard.test.ts +151 -0
  232. package/src/clipboard.ts +145 -16
  233. package/src/column-resize.ts +7 -1
  234. package/src/columns.ts +2 -0
  235. package/src/command-context.test.ts +200 -0
  236. package/src/command-context.ts +137 -0
  237. package/src/core.ts +7 -0
  238. package/src/editing.test.ts +281 -3
  239. package/src/editing.ts +256 -38
  240. package/src/fill-patterns.test.ts +35 -0
  241. package/src/fill-patterns.ts +56 -2
  242. package/src/grid-messages.ts +16 -3
  243. package/src/history.test.ts +196 -0
  244. package/src/history.ts +162 -0
  245. package/src/icon-seam.test.ts +3 -2
  246. package/src/index.ts +150 -2
  247. package/src/keyboard-handlers.coverage.test.ts +126 -6
  248. package/src/keyboard-handlers.ts +91 -30
  249. package/src/keyboard-shortcuts.seam.test.ts +200 -0
  250. package/src/keyboard.test.ts +124 -1
  251. package/src/keyboard.ts +142 -0
  252. package/src/menus.test.ts +26 -0
  253. package/src/menus.ts +10 -3
  254. package/src/merges.test.ts +112 -0
  255. package/src/merges.ts +168 -0
  256. package/src/pivot.grid.test.ts +40 -5
  257. package/src/row-resize.test.ts +44 -0
  258. package/src/row-resize.ts +17 -0
  259. package/src/scroll-sync.test.ts +42 -1
  260. package/src/scroll-sync.ts +25 -0
  261. package/src/selection.multi-range.test.ts +34 -0
  262. package/src/selection.test.ts +30 -2
  263. package/src/selection.ts +111 -19
  264. package/src/shortcut-registry.test.ts +97 -0
  265. package/src/shortcut-registry.ts +181 -0
  266. package/src/svgrid-wrapper.types.ts +184 -13
  267. package/src/svgrid.behavior.test.ts +135 -0
  268. package/src/svgrid.charting.test.ts +513 -3
  269. package/src/svgrid.context-menu.test.ts +31 -0
  270. package/src/svgrid.new-features.wrapper.test.ts +10 -3
  271. package/src/virtualization/virtualizer.ts +39 -17
  272. package/dist/cdn/GridMenus-DyFA-QxA.js +0 -602
  273. package/dist/cdn/GridMenus-M2XP9t0_.js +0 -603
  274. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  275. package/dist/cdn/SvGridChart-DAb9RbRs.js +0 -1480
  276. package/dist/cdn/SvGridChart-Yl-IXZFO.js +0 -1481
  277. package/dist/cdn/SvGridChartPanel-DayQpDsR.js +0 -834
  278. package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +0 -810
  279. package/dist/cdn/SvGridChartView-CxfakxR0.js +0 -56
  280. package/dist/cdn/SvGridChartView-Dm_YEx9L.js +0 -55
  281. package/dist/cdn/chart-CKJRCPOk.js +0 -1652
package/dist/editing.js CHANGED
@@ -3,9 +3,12 @@
3
3
  // ($state/$derived/$effect) stays in the controller.
4
4
  import { parseEditorValue, } from "./index.js";
5
5
  import "./sv-grid-scrollbar.js";
6
- import { getNextActiveCell } from "./keyboard.js";
6
+ import { getEntryStep } from "./keyboard.js";
7
7
  import { getCellKey, toValueArray, } from "./SvGrid.helpers.js";
8
8
  import { createDataIndexLookup, getColumnBaseValue, isGroupRow, } from "./cell-values.js";
9
+ import { pushHistory, nextGroupId, runHistoryGroup } from "./history.js";
10
+ import { hasGridShortcuts, runGridShortcuts } from "./shortcut-registry.js";
11
+ import { buildCommandContext } from "./command-context.js";
9
12
  export function createEditing(ctx) {
10
13
  /**
11
14
  * Resolve `columnDef.editable` for one row × column pair.
@@ -100,10 +103,27 @@ export function createEditing(ctx) {
100
103
  rowRef: row.original,
101
104
  };
102
105
  ctx.setActiveCell(rowIndex, colIndex);
103
- ctx.setSelection(rowIndex, colIndex);
106
+ selectForEdit(rowIndex, colIndex);
104
107
  return true;
105
108
  }
109
+ /**
110
+ * The selection an edit starts with. A cell already inside the selection
111
+ * leaves it standing: typing into the active cell of a selected block
112
+ * used to collapse the block to that one cell, so Enter could never walk
113
+ * on inside it. A cell outside the selection becomes the selection.
114
+ */
115
+ function selectForEdit(rowIndex, colIndex) {
116
+ if (ctx.isCellInSelectedRange(rowIndex, colIndex))
117
+ return;
118
+ ctx.setSelection(rowIndex, colIndex);
119
+ }
106
120
  function saveEditingCell() {
121
+ // One group: a consumer's onCellValueChange may record a step of its own
122
+ // (a spreadsheet gives "12%" a percent format as it lands), and that
123
+ // belongs to the same Ctrl+Z as the value.
124
+ runHistoryGroup(ctx, () => commitEditingCell());
125
+ }
126
+ function commitEditingCell() {
107
127
  if (!ctx.editingCell)
108
128
  return;
109
129
  const editing = ctx.editingCell;
@@ -154,20 +174,7 @@ export function createEditing(ctx) {
154
174
  before: oldValue,
155
175
  after: finalValue,
156
176
  };
157
- const truncated = ctx.history.slice(0, ctx.historyPtr + 1);
158
- truncated.push(step);
159
- // Cap the buffer at UNDO_LIMIT; drop the OLDEST entries so the
160
- // pointer stays valid relative to the newest steps.
161
- if (truncated.length > ctx.UNDO_LIMIT) {
162
- const drop = truncated.length - ctx.UNDO_LIMIT;
163
- ctx.history = truncated.slice(drop);
164
- ctx.historyPtr = ctx.history.length - 1;
165
- }
166
- else {
167
- ctx.history = truncated;
168
- ctx.historyPtr = ctx.history.length - 1;
169
- }
170
- ctx.historyVersion += 1;
177
+ pushHistory(ctx, [step]);
171
178
  }
172
179
  // Notify the consumer AFTER the row has been updated so any callback-
173
180
  // driven recompute (cascade totals, server save, undo stack) sees the
@@ -187,6 +194,13 @@ export function createEditing(ctx) {
187
194
  /** Apply an undo / redo step directly to the underlying row, bypassing
188
195
  * the editor pipeline so we don't accidentally re-push to the stack. */
189
196
  function applyHistoryStep(step, direction) {
197
+ if (step.custom) {
198
+ if (direction === 'undo')
199
+ step.custom.undo();
200
+ else
201
+ step.custom.redo();
202
+ return;
203
+ }
190
204
  const row = ctx.allRows.find((r) => r.id === step.rowId);
191
205
  const col = ctx.allColumns.find((c) => c.id === step.columnId);
192
206
  if (!row?.original || !col)
@@ -217,26 +231,115 @@ export function createEditing(ctx) {
217
231
  * move, Tab committed the edit and then let the browser walk focus out of the
218
232
  * grid entirely (#48). Shared by every editor type, including the textarea.
219
233
  */
220
- function commitAndMoveByTab(shiftKey) {
234
+ /**
235
+ * Commit the edit and step the cursor the way data entry expects: Tab a
236
+ * column to the right (wrapping at the row's end), Enter a row down, Shift
237
+ * reversing either. Enter used to commit and stay, so a column of numbers
238
+ * took a click or an arrow between every value; Excel and every other
239
+ * sheet move down, and the accessibility page has always said this grid
240
+ * does too. At the last row or column the cursor stays put.
241
+ */
242
+ function commitAndMove(intent) {
221
243
  const active = ctx.activeCell;
222
244
  saveEditingCell();
223
245
  ctx.gridRootEl?.focus({ preventScroll: true });
224
246
  if (!active)
225
247
  return;
226
- const next = getNextActiveCell(active, shiftKey ? "tabPrev" : "tabNext", {
248
+ // The same stepping as Enter and Tab on the grid root: a Tab run's Enter
249
+ // goes back to the run's first column, and a selected block keeps the
250
+ // cursor inside it.
251
+ const step = getEntryStep(active, intent, {
227
252
  maxRow: ctx.allRows.length - 1,
228
253
  maxCol: ctx.allColumns.length - 1,
254
+ tabOrigin: ctx.tabRunOrigin,
255
+ range: ctx.activeRangeRect(),
256
+ collapsed: {
257
+ isRowCollapsed: (i) => ctx.isRowCollapsed(i),
258
+ isColumnCollapsed: (i) => !!ctx.collapsedColumns[ctx.allColumns[i]?.id],
259
+ },
229
260
  });
261
+ const next = step.cell;
230
262
  ctx.setActiveCell(next.rowIndex, next.colIndex);
231
- ctx.setSelection(next.rowIndex, next.colIndex);
263
+ ctx.tabRunOrigin = step.tabOrigin;
264
+ if (!step.withinRange)
265
+ ctx.setSelection(next.rowIndex, next.colIndex);
232
266
  ctx.scrollActiveCellIntoView(next.rowIndex, next.colIndex);
233
267
  }
268
+ function commitAndMoveByTab(shiftKey) {
269
+ commitAndMove(shiftKey ? "tabPrev" : "tabNext");
270
+ }
271
+ /**
272
+ * Ctrl+Enter: the entry goes into every cell of the selected block and the
273
+ * cursor stays where it is, as in Excel, where it is how a block is filled
274
+ * with one value in one go. Each cell takes the same raw entry through the
275
+ * ordinary commit, so its column's parser, the history and
276
+ * `onCellValueChange` all see it, and the whole block is one undo. A cell
277
+ * that cannot be edited is left alone. With nothing but the active cell
278
+ * selected it is a commit that does not move.
279
+ */
280
+ function commitToSelection() {
281
+ const editing = ctx.editingCell;
282
+ if (!editing)
283
+ return;
284
+ const raw = editing.value;
285
+ const active = ctx.activeCell;
286
+ const range = ctx.activeRangeRect();
287
+ runHistoryGroup(ctx, () => {
288
+ commitEditingCell();
289
+ if (!active || !range)
290
+ return;
291
+ for (let r = range.minRow; r <= range.maxRow; r += 1) {
292
+ for (let c = range.minCol; c <= range.maxCol; c += 1) {
293
+ if (r === active.rowIndex && c === active.colIndex)
294
+ continue;
295
+ const row = ctx.allRows[r];
296
+ const column = ctx.allColumns[c];
297
+ if (!row || !column || isGroupRow(row) || !isCellEditable(column, row))
298
+ continue;
299
+ ctx.editingCell = {
300
+ rowId: row.id,
301
+ columnId: column.id,
302
+ editorType: (column.columnDef.editorType ?? "text"),
303
+ value: raw,
304
+ rowRef: row.original,
305
+ };
306
+ commitEditingCell();
307
+ }
308
+ }
309
+ });
310
+ ctx.gridRootEl?.focus({ preventScroll: true });
311
+ }
234
312
  function onEditorKeyDown(event) {
235
313
  event.stopPropagation();
236
- if (event.key === "Enter") {
314
+ // Registered commands get the key first here too, with `editing: true`, so
315
+ // a handler can claim a combination that only means something mid-edit
316
+ // (Alt+Enter for a newline, F4 to cycle a reference between relative and
317
+ // absolute) before the editor's own Enter / Tab / Escape handling runs.
318
+ if (hasGridShortcuts() && runGridShortcuts(event, buildCommandContext(ctx, true))) {
319
+ return;
320
+ }
321
+ if (event.key === "Enter" && event.altKey) {
322
+ // A line break, in an editor that can hold one (`editorMultiline`).
323
+ // In a single-line editor Alt+Enter is just Enter.
324
+ const el = event.currentTarget;
325
+ if (el instanceof HTMLTextAreaElement) {
326
+ event.preventDefault();
327
+ const start = el.selectionStart ?? el.value.length;
328
+ const end = el.selectionEnd ?? start;
329
+ el.value = el.value.slice(0, start) + "\n" + el.value.slice(end);
330
+ el.selectionStart = el.selectionEnd = start + 1;
331
+ updateEditingCellValue(el.value);
332
+ el.dispatchEvent(new Event("input", { bubbles: true }));
333
+ return;
334
+ }
335
+ }
336
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
237
337
  event.preventDefault();
238
- saveEditingCell();
239
- ctx.gridRootEl?.focus({ preventScroll: true });
338
+ commitToSelection();
339
+ }
340
+ else if (event.key === "Enter") {
341
+ event.preventDefault();
342
+ commitAndMove(event.shiftKey ? "moveUp" : "moveDown");
240
343
  }
241
344
  else if (event.key === "Tab") {
242
345
  event.preventDefault();
@@ -316,7 +419,7 @@ export function createEditing(ctx) {
316
419
  rowRef: row.original,
317
420
  };
318
421
  ctx.setActiveCell(rowIndex, colIndex);
319
- ctx.setSelection(rowIndex, colIndex);
422
+ selectForEdit(rowIndex, colIndex);
320
423
  }
321
424
  async function pasteFromClipboard() {
322
425
  // Preferred path: the async Clipboard API. Requires a secure context
@@ -325,6 +428,34 @@ export function createEditing(ctx) {
325
428
  // On plain HTTP (a XAMPP/Apache LAN host) `navigator.clipboard` is
326
429
  // undefined - there the Ctrl+V keydown handler skips preventDefault and
327
430
  // lets the browser deliver a native `paste` event to `onGridPaste`.
431
+ if (ctx.props.onPasteClipboard && navigator.clipboard?.read) {
432
+ // Both types, for a handler that reads the HTML.
433
+ try {
434
+ const items = await navigator.clipboard.read();
435
+ let text = "";
436
+ let html = null;
437
+ for (const item of items) {
438
+ if (item.types.includes("text/html"))
439
+ html = await (await item.getType("text/html")).text();
440
+ if (item.types.includes("text/plain"))
441
+ text = await (await item.getType("text/plain")).text();
442
+ }
443
+ if (text || html)
444
+ applyPastedPayload({ text, html, source: "async" });
445
+ }
446
+ catch {
447
+ // No permission or nothing readable: the text-only read below may still work.
448
+ if (!navigator.clipboard.readText)
449
+ return;
450
+ try {
451
+ applyPastedPayload({ text: await navigator.clipboard.readText(), html: null, source: "async" });
452
+ }
453
+ catch {
454
+ return;
455
+ }
456
+ }
457
+ return;
458
+ }
328
459
  if (!navigator.clipboard?.readText)
329
460
  return;
330
461
  let text;
@@ -336,6 +467,27 @@ export function createEditing(ctx) {
336
467
  }
337
468
  applyPastedText(text);
338
469
  }
470
+ /**
471
+ * With `onPasteClipboard` set, Ctrl+V leaves the key to the browser so the
472
+ * `paste` event arrives with the HTML. A browser that does not deliver
473
+ * that event to the grid root (Firefox, a non-editable element) would
474
+ * paste nothing, so the async read is armed to run shortly after unless
475
+ * the event lands first and disarms it.
476
+ */
477
+ let pasteFallback = null;
478
+ function armPasteFallback() {
479
+ if (pasteFallback)
480
+ clearTimeout(pasteFallback);
481
+ pasteFallback = setTimeout(() => {
482
+ pasteFallback = null;
483
+ void pasteFromClipboard();
484
+ }, 80);
485
+ }
486
+ function disarmPasteFallback() {
487
+ if (pasteFallback)
488
+ clearTimeout(pasteFallback);
489
+ pasteFallback = null;
490
+ }
339
491
  /**
340
492
  * Native `paste` ClipboardEvent fallback (insecure-context path). A real
341
493
  * Ctrl/Cmd+V on the focused grid root fires this with `clipboardData`
@@ -351,6 +503,18 @@ export function createEditing(ctx) {
351
503
  // so the edit "wouldn't update".
352
504
  if (ctx.editingCell)
353
505
  return;
506
+ if (ctx.props.onPasteClipboard) {
507
+ // The event is the preferred path for a handler: it carries the HTML
508
+ // as the source wrote it, needs no permission, and works over HTTP.
509
+ disarmPasteFallback();
510
+ const text = event.clipboardData?.getData("text/plain") ?? "";
511
+ const html = event.clipboardData?.getData("text/html") || null;
512
+ if (!text && !html)
513
+ return;
514
+ event.preventDefault();
515
+ applyPastedPayload({ text, html, source: "event" });
516
+ return;
517
+ }
354
518
  // When the async API is available the keydown handler already pasted via
355
519
  // pasteFromClipboard(); ignore the (suppressed) native event so we don't
356
520
  // paste twice.
@@ -363,6 +527,22 @@ export function createEditing(ctx) {
363
527
  applyPastedText(text);
364
528
  }
365
529
  function applyPastedText(text) {
530
+ // One history group for the whole paste, so whatever a consumer's
531
+ // processCellFromClipboard records alongside the values (a format, via
532
+ // recordUndo) comes back with them on a single Ctrl+Z.
533
+ runHistoryGroup(ctx, () => applyPastedCells(text));
534
+ }
535
+ /** A paste with both types: the handler first, the plain text otherwise. */
536
+ function applyPastedPayload(payload) {
537
+ runHistoryGroup(ctx, () => {
538
+ const handled = ctx.props.onPasteClipboard?.(payload);
539
+ if (handled === true)
540
+ return;
541
+ if (payload.text)
542
+ applyPastedCells(payload.text);
543
+ });
544
+ }
545
+ function applyPastedCells(text) {
366
546
  const anchor = ctx.selectionRange.anchor ?? ctx.grid.getState().activeCell;
367
547
  if (!anchor)
368
548
  return;
@@ -390,6 +570,12 @@ export function createEditing(ctx) {
390
570
  : Math.max(...lines.map((l) => l.split("\t").length));
391
571
  const next = ctx.internalData.slice();
392
572
  const dataIndexOf = createDataIndexLookup(next);
573
+ // Every cell the paste changes: one grouped history entry for the lot,
574
+ // so Ctrl+Z takes the whole paste back, and one onCellValueChange each,
575
+ // so a consumer keeping its own model behind the grid (a formula engine)
576
+ // learns the values. The loop used to swap the rows in and stop, which
577
+ // left the undo stack blind to a paste and the consumer never told.
578
+ const changed = [];
393
579
  for (let i = 0; i < rowSpan; i += 1) {
394
580
  const targetRowIndex = startRow + i;
395
581
  const row = ctx.allRows[targetRowIndex];
@@ -409,23 +595,69 @@ export function createEditing(ctx) {
409
595
  const updated = {
410
596
  ...originalRow,
411
597
  };
598
+ const rowChanges = [];
599
+ const record = (column, before, after) => {
600
+ if (before === after)
601
+ return;
602
+ rowChanges.push({
603
+ row: updated, rowId: row.id, rowIndex: dataIndex, columnId: column.id,
604
+ field: column.columnDef.field, before, after,
605
+ });
606
+ };
412
607
  for (let j = 0; j < colSpan; j += 1) {
413
608
  const column = ctx.allColumns[startCol + j];
414
609
  if (!column?.columnDef.field)
415
610
  continue;
416
611
  if (!isCellEditableAt(targetRowIndex, startCol + j))
417
612
  continue;
613
+ const before = updated[column.columnDef.field];
418
614
  const editorType = (column.columnDef.editorType ??
419
615
  "text");
420
616
  const raw = fillRange ? lines[0] : sourceCells?.[j] ?? "";
421
- updated[column.columnDef.field] = parseEditorValue(editorType, raw, {
617
+ const parsedValue = parseEditorValue(editorType, raw, {
422
618
  dateOnly: column.columnDef.cellDataType === "dateString",
423
619
  });
620
+ // The inbound half of the clipboard pair. A consumer (or a feature
621
+ // pack doing paste-special) gets the raw text AND what the grid would
622
+ // have written, and can return either. `undefined` leaves the cell
623
+ // alone, which is how "paste values only" skips a formula column.
624
+ const hook = ctx.props.processCellFromClipboard;
625
+ if (hook) {
626
+ const decided = hook({
627
+ text: raw,
628
+ parsedValue,
629
+ row: originalRow,
630
+ rowIndex: targetRowIndex,
631
+ columnId: column.id,
632
+ });
633
+ if (decided === undefined)
634
+ continue;
635
+ updated[column.columnDef.field] = decided;
636
+ record(column, before, decided);
637
+ continue;
638
+ }
639
+ updated[column.columnDef.field] = parsedValue;
640
+ record(column, before, parsedValue);
424
641
  }
642
+ if (!rowChanges.length)
643
+ continue;
425
644
  next[dataIndex] = updated;
645
+ changed.push(...rowChanges);
426
646
  }
647
+ if (!changed.length)
648
+ return;
427
649
  ctx.internalData = next;
428
650
  ctx.grid.store.setState((prev) => ({ ...prev }));
651
+ pushHistory(ctx, changed.map((c) => ({
652
+ rowId: c.rowId, columnId: c.columnId, field: c.field, before: c.before, after: c.after,
653
+ })), nextGroupId());
654
+ if (ctx.props.onCellValueChange) {
655
+ for (const c of changed) {
656
+ ctx.props.onCellValueChange({
657
+ rowIndex: c.rowIndex, columnId: c.columnId, oldValue: c.before, newValue: c.after, row: c.row,
658
+ });
659
+ }
660
+ }
429
661
  }
430
662
  /** Programmatically begin editing a cell (mirrors a double-click). */
431
663
  function startEditing(rowIndex, columnId) {
@@ -531,15 +763,11 @@ export function createEditing(ctx) {
531
763
  }
532
764
  // Record the whole-row change as consecutive history steps.
533
765
  if (changed.length) {
534
- let hist = ctx.history.slice(0, ctx.historyPtr + 1);
535
- for (const c of changed) {
536
- hist.push({ rowId: fr.rowId, columnId: c.columnId, field: c.field, before: c.before, after: c.after });
537
- }
538
- if (hist.length > ctx.UNDO_LIMIT)
539
- hist = hist.slice(hist.length - ctx.UNDO_LIMIT);
540
- ctx.history = hist;
541
- ctx.historyPtr = ctx.history.length - 1;
542
- ctx.historyVersion += 1;
766
+ // One full-row commit is ONE undo, not one per changed column.
767
+ pushHistory(ctx, changed.map((c) => ({
768
+ rowId: fr.rowId, columnId: c.columnId, field: c.field,
769
+ before: c.before, after: c.after,
770
+ })), nextGroupId());
543
771
  }
544
772
  ctx.grid.store.setState((prev) => ({ ...prev }));
545
773
  if (ctx.props.onCellValueChange && changed.length) {
@@ -581,5 +809,6 @@ export function createEditing(ctx) {
581
809
  onCellDoubleClick,
582
810
  pasteFromClipboard,
583
811
  onGridPaste,
812
+ armPasteFallback,
584
813
  };
585
814
  }
@@ -22,9 +22,4 @@
22
22
  * A single source value falls through to the cycle case (repeats), which
23
23
  * matches Excel's default fill behavior.
24
24
  */
25
- /**
26
- * Compute the values to write into `targetCount` new cells given the
27
- * `sourceValues` already in the selection. See module header for the
28
- * pattern-detection rules.
29
- */
30
25
  export declare function buildFillPattern(sourceValues: ReadonlyArray<unknown>, targetCount: number): unknown[];
@@ -149,10 +149,48 @@ function arithmeticStep(nums) {
149
149
  * `sourceValues` already in the selection. See module header for the
150
150
  * pattern-detection rules.
151
151
  */
152
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
153
+ /**
154
+ * The sources as day stamps when every one is a date: `Date` objects, or
155
+ * `YYYY-MM-DD` text (read as UTC so a fill never crosses a day boundary
156
+ * with the time zone). Anything else means "not a date series".
157
+ */
158
+ function datesOf(values) {
159
+ if (values.every((v) => v instanceof Date && Number.isFinite(v.getTime()))) {
160
+ return { stamps: values.map((v) => v.getTime()), iso: false };
161
+ }
162
+ if (values.every((v) => typeof v === 'string' && ISO_DATE.test(v))) {
163
+ const stamps = values.map((v) => Date.parse(`${v}T00:00:00Z`));
164
+ return stamps.every((t) => Number.isFinite(t)) ? { stamps, iso: true } : null;
165
+ }
166
+ return null;
167
+ }
152
168
  export function buildFillPattern(sourceValues, targetCount) {
153
169
  if (sourceValues.length === 0 || targetCount <= 0)
154
170
  return [];
155
- // -------- 1) Numeric arithmetic progression (2+ values) -----------
171
+ // -------- 0) Dates step by the day ---------------------------------
172
+ // Before the numeric rule, since a Date coerces to a finite number of
173
+ // milliseconds, and before the prefix rule, which read 2024-01-15 as the
174
+ // number 2024 with a suffix and filled 2025-01-15. Two dates set the step
175
+ // (a week apart fills weekly); one date steps by a day, as Excel's does.
176
+ const dates = datesOf(sourceValues);
177
+ if (dates) {
178
+ const DAY = 86400000;
179
+ const days = dates.stamps.map((t) => Math.round(t / DAY));
180
+ const step = days.length >= 2 ? arithmeticStep(days) : 1;
181
+ if (step !== null) {
182
+ const last = days[days.length - 1];
183
+ return Array.from({ length: targetCount }, (_, i) => {
184
+ const stamp = (last + step * (i + 1)) * DAY;
185
+ return dates.iso ? new Date(stamp).toISOString().slice(0, 10) : new Date(stamp);
186
+ });
187
+ }
188
+ }
189
+ // -------- 1) Numbers: the linear trend (2+ values) -----------------
190
+ // Excel's AutoFill of two or more numbers continues their least-squares
191
+ // line: 1, 2, 3 goes on 4, 5, 6, and 1, 2, 4 goes on 5.33, 6.83, ...
192
+ // rather than repeating. An even step is the same line, so the common
193
+ // case is unchanged.
156
194
  const nums = sourceValues.map((v) => Number(v));
157
195
  const allNumericLike = sourceValues.every((v) => v !== null && v !== '' && v !== undefined) &&
158
196
  nums.every((n) => Number.isFinite(n));
@@ -162,6 +200,20 @@ export function buildFillPattern(sourceValues, targetCount) {
162
200
  const last = nums[nums.length - 1];
163
201
  return Array.from({ length: targetCount }, (_, i) => last + step * (i + 1));
164
202
  }
203
+ const n = nums.length;
204
+ const meanX = (n - 1) / 2;
205
+ const meanY = nums.reduce((a, b) => a + b, 0) / n;
206
+ let sxy = 0;
207
+ let sxx = 0;
208
+ for (let i = 0; i < n; i += 1) {
209
+ sxy += (i - meanX) * (nums[i] - meanY);
210
+ sxx += (i - meanX) ** 2;
211
+ }
212
+ const slope = sxx === 0 ? 0 : sxy / sxx;
213
+ const intercept = meanY - slope * meanX;
214
+ // Rounded to the precision floating point gives 5.333333333333333, so
215
+ // 10, 20, 40 fills 50, 60 and not 50.00000000000001.
216
+ return Array.from({ length: targetCount }, (_, i) => Number((intercept + slope * (n + i)).toPrecision(15)));
165
217
  }
166
218
  // -------- 2) Known string sequence (days / months / quarters) -----
167
219
  const allStrings = sourceValues.every((v) => typeof v === 'string');
@@ -179,7 +231,9 @@ export function buildFillPattern(sourceValues, targetCount) {
179
231
  if (allStrings && sourceValues.length >= 1) {
180
232
  const stringSources = sourceValues;
181
233
  const split = findPrefixNumberSuffix(stringSources);
182
- if (split) {
234
+ // A bare number as text ('5') is a number, not 'Item 5': one of them
235
+ // copies, as a numeric 5 does, and two or more were a progression above.
236
+ if (split && (split.prefix !== '' || split.suffix !== '')) {
183
237
  const step = split.numbers.length >= 2
184
238
  ? arithmeticStep(split.numbers) ?? 1
185
239
  : 1;
@@ -1,5 +1,12 @@
1
+ import type { GridChartPanelMessages } from './chart-panel-messages.js';
2
+ /**
3
+ * Every string `localization.text` accepts: the grid chrome's own
4
+ * ({@link GridChromeMessages}) and the chart panel's
5
+ * ({@link GridChartPanelMessages}).
6
+ */
7
+ export type GridMessages = GridChromeMessages & GridChartPanelMessages;
1
8
  /** Every localizable chrome string, grouped by area in the comments. */
2
- export type GridMessages = {
9
+ export type GridChromeMessages = {
3
10
  noRows: string;
4
11
  loading: string;
5
12
  columns: string;
@@ -74,10 +81,10 @@ export type GridMessages = {
74
81
  upsellLicenseLink: string;
75
82
  };
76
83
  /** English defaults - the literal strings the grid shipped before localization. */
77
- export declare const defaultGridMessages: GridMessages;
84
+ export declare const defaultGridMessages: GridChromeMessages;
78
85
  /**
79
86
  * Merge a consumer's `localeText` over the English defaults. Undefined/empty
80
87
  * values fall back to the default (via `resolveMessages`), so a partial map only
81
88
  * replaces the keys it sets.
82
89
  */
83
- export declare function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridMessages;
90
+ export declare function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridChromeMessages;
@@ -7,6 +7,11 @@
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.js';
12
17
  /** English defaults - the literal strings the grid shipped before localization. */
@@ -0,0 +1,83 @@
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
+ export type HistoryStep = {
23
+ rowId: string;
24
+ columnId: string;
25
+ field: string;
26
+ before: unknown;
27
+ after: unknown;
28
+ /** Set when this step is part of a multi-cell action. Steps sharing an id
29
+ * are contiguous in the array and undo / redo as one. */
30
+ groupId?: string;
31
+ /**
32
+ * A step that is not a cell write: something outside the grid's data that
33
+ * still belongs in the same Ctrl+Z stream, such as a cell format kept in a
34
+ * consumer's store. The cell fields are empty on such a step and the two
35
+ * functions do the work. Never serialised, since a closure cannot be.
36
+ */
37
+ custom?: {
38
+ undo: () => void;
39
+ redo: () => void;
40
+ };
41
+ };
42
+ /** Ctx surface these helpers touch. The controller satisfies it structurally. */
43
+ type HistoryCtx = {
44
+ history: HistoryStep[];
45
+ historyPtr: number;
46
+ historyVersion: number;
47
+ UNDO_LIMIT: number;
48
+ /** Set while a `runHistoryGroup` call is on the stack. Every step pushed
49
+ * inside it joins that group, so a command does not have to thread a group
50
+ * id down through the writers it calls. */
51
+ historyGroupId: string | undefined;
52
+ applyHistoryStep(step: HistoryStep, direction: 'undo' | 'redo'): void;
53
+ };
54
+ /** Undo one action. Returns false when there is nothing left to undo. */
55
+ export declare function undoHistory(ctx: HistoryCtx): boolean;
56
+ /** Redo one action. Returns false when there is nothing left to redo. */
57
+ export declare function redoHistory(ctx: HistoryCtx): boolean;
58
+ /**
59
+ * Append steps at the pointer, truncating any forward history (the standard
60
+ * "an edit invalidates redo" rule) and capping the buffer at UNDO_LIMIT by
61
+ * dropping the OLDEST entries.
62
+ *
63
+ * Every writer goes through here rather than open-coding the slice / push /
64
+ * cap dance, which editing.ts and clipboard.ts previously did three times
65
+ * with three slightly different shapes.
66
+ */
67
+ export declare function pushHistory(ctx: HistoryCtx, steps: ReadonlyArray<HistoryStep>, groupId?: string): void;
68
+ /** A fresh group id. Monotonic, not random: ids never leave the session and a
69
+ * counter is comparable in a test snapshot. */
70
+ export declare function nextGroupId(): string;
71
+ /**
72
+ * Run `fn` with every history step it writes tagged as one undoable action.
73
+ *
74
+ * This is ambient rather than a wrapper that re-tags afterwards, because
75
+ * `pushHistory` can drop the oldest entries when the buffer hits UNDO_LIMIT.
76
+ * A re-tagging implementation would compute its slice bounds from a length
77
+ * that had shifted underneath it and stamp the wrong steps.
78
+ *
79
+ * Nested calls join the outer group: one user action is one undo, however many
80
+ * layers of helper it goes through.
81
+ */
82
+ export declare function runHistoryGroup<T>(ctx: HistoryCtx, fn: () => T): T;
83
+ export {};