@svgrid/enterprise 3.0.1 → 3.0.2

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 (197) hide show
  1. package/dist/SvFormulaBar.svelte +606 -0
  2. package/dist/SvFormulaBar.svelte.d.ts +62 -0
  3. package/dist/SvPivotDesigner.svelte +65 -14
  4. package/dist/SvRibbonIcon.svelte +46 -0
  5. package/dist/SvRibbonIcon.svelte.d.ts +17 -0
  6. package/dist/SvSchemaChart.svelte +4 -2
  7. package/dist/SvSheet.svelte +4114 -0
  8. package/dist/SvSheet.svelte.d.ts +142 -0
  9. package/dist/SvSheetComment.svelte +98 -0
  10. package/dist/SvSheetComment.svelte.d.ts +22 -0
  11. package/dist/SvSheetConditionalFormat.svelte +162 -0
  12. package/dist/SvSheetConditionalFormat.svelte.d.ts +14 -0
  13. package/dist/SvSheetDataValidation.svelte +249 -0
  14. package/dist/SvSheetDataValidation.svelte.d.ts +14 -0
  15. package/dist/SvSheetFilterMenu.svelte +217 -0
  16. package/dist/SvSheetFilterMenu.svelte.d.ts +17 -0
  17. package/dist/SvSheetFindReplace.svelte +132 -0
  18. package/dist/SvSheetFindReplace.svelte.d.ts +10 -0
  19. package/dist/SvSheetFormatCells.svelte +421 -0
  20. package/dist/SvSheetFormatCells.svelte.d.ts +17 -0
  21. package/dist/SvSheetGoalSeek.svelte +144 -0
  22. package/dist/SvSheetGoalSeek.svelte.d.ts +12 -0
  23. package/dist/SvSheetInsertFunction.svelte +147 -0
  24. package/dist/SvSheetInsertFunction.svelte.d.ts +8 -0
  25. package/dist/SvSheetListPicker.svelte +89 -0
  26. package/dist/SvSheetListPicker.svelte.d.ts +16 -0
  27. package/dist/SvSheetManageRules.svelte +185 -0
  28. package/dist/SvSheetManageRules.svelte.d.ts +16 -0
  29. package/dist/SvSheetNameManager.svelte +225 -0
  30. package/dist/SvSheetNameManager.svelte.d.ts +11 -0
  31. package/dist/SvSheetPasteSpecial.svelte +105 -0
  32. package/dist/SvSheetPasteSpecial.svelte.d.ts +12 -0
  33. package/dist/SvSheetRemoveDuplicates.svelte +149 -0
  34. package/dist/SvSheetRemoveDuplicates.svelte.d.ts +12 -0
  35. package/dist/SvSheetRibbon.svelte +1201 -0
  36. package/dist/SvSheetRibbon.svelte.d.ts +34 -0
  37. package/dist/SvSheetSizeDialog.svelte +58 -0
  38. package/dist/SvSheetSizeDialog.svelte.d.ts +11 -0
  39. package/dist/SvSheetTabs.svelte +441 -0
  40. package/dist/SvSheetTabs.svelte.d.ts +36 -0
  41. package/dist/SvSheetTabs.test-harness.svelte.d.ts +17 -0
  42. package/dist/SvSheetTabs.test-harness.svelte.js +22 -0
  43. package/dist/SvSheetTextToColumns.svelte +172 -0
  44. package/dist/SvSheetTextToColumns.svelte.d.ts +13 -0
  45. package/dist/SvSheetValidationAlert.svelte +83 -0
  46. package/dist/SvSheetValidationAlert.svelte.d.ts +15 -0
  47. package/dist/cdn/svgrid-enterprise.svelte-external.js +18997 -5396
  48. package/dist/designer/assets/GridMenus-BlsvL_aU.js +8 -0
  49. package/dist/designer/assets/SvChartMenu-BYO9cYdy.css +1 -0
  50. package/dist/designer/assets/SvChartMenu-D9hEeHar.js +1 -0
  51. package/dist/designer/assets/SvDateRangeInput-BBRzrQeg.js +1 -0
  52. package/dist/designer/assets/SvDateTimePicker-DARvKUL2.js +1 -0
  53. package/dist/designer/assets/SvGridCellEditor-C7uQXYds.js +4 -0
  54. package/dist/designer/assets/SvGridChart-B8j1SSy3.js +3 -0
  55. package/dist/designer/assets/SvGridChart-BgLuPO7I.css +1 -0
  56. package/dist/designer/assets/SvGridChartBuilder-08YWWeqY.css +1 -0
  57. package/dist/designer/assets/SvGridChartBuilder-kvyIU_Rl.js +1 -0
  58. package/dist/designer/assets/SvGridChartPanel-4Pc8y5yO.css +1 -0
  59. package/dist/designer/assets/SvGridChartPanel-pT6ITc2P.js +4 -0
  60. package/dist/designer/assets/{SvGridChartView-DUc-1RUf.css → SvGridChartView-7msfnWZT.css} +1 -1
  61. package/dist/designer/assets/SvGridChartView-bg281UA1.js +1 -0
  62. package/dist/designer/assets/SvListBox-BDS66lmZ.js +110 -0
  63. package/dist/designer/assets/SvMenuList-DJ47ba1h.css +1 -0
  64. package/dist/designer/assets/SvMenuList-Dgq2D5VN.js +1 -0
  65. package/dist/designer/assets/SvTabs-DKmwb5p1.css +1 -0
  66. package/dist/designer/assets/SvTabs-UFCMTHXS.js +1 -0
  67. package/dist/designer/assets/chart-cIPUsbM5.js +1 -0
  68. package/dist/designer/assets/chart-export-B6bkQ3X8.js +11 -0
  69. package/dist/designer/assets/chart-export-pdf-BbA28-gt.js +11 -0
  70. package/dist/designer/assets/chart-panel-messages-DtTxpBUU.js +1 -0
  71. package/dist/designer/assets/chart-summary-Czu_5epb.js +1 -0
  72. package/dist/designer/assets/chart-trend-D5I5VEe8.js +1 -0
  73. package/dist/designer/assets/chart-validate-BMqM-qkF.js +1 -0
  74. package/dist/designer/assets/column-resize-BYDzR1Ve.js +1 -0
  75. package/dist/designer/assets/date-format-B8JbEtIG.js +1 -0
  76. package/dist/designer/assets/date-format-DKS-z0xS.css +1 -0
  77. package/dist/designer/assets/editor-contract-Bpoa7A5g.js +1 -0
  78. package/dist/designer/assets/focus-trap-CAreAhlU.js +1 -0
  79. package/dist/designer/assets/index-CMFgAoVV.css +1 -0
  80. package/dist/designer/assets/{index-C_a9Cj8Y.js → index-DwU7CAMm.js} +291 -290
  81. package/dist/designer/assets/list-option-BP_dWS66.js +1 -0
  82. package/dist/designer/assets/row-resize-DLktcqUK.js +1 -0
  83. package/dist/designer/assets/src-CITpcm6b.js +1 -0
  84. package/dist/designer/assets/src-EdI3Xqds.js +58 -0
  85. package/dist/designer/index.html +23 -14
  86. package/dist/export-pdf.d.ts +89 -0
  87. package/dist/export-pdf.js +130 -0
  88. package/dist/export.js +17 -10
  89. package/dist/index.d.ts +64 -1
  90. package/dist/index.js +68 -1
  91. package/dist/install.js +5 -0
  92. package/dist/node/studio.js +64 -2
  93. package/dist/pivot-chart.d.ts +25 -1
  94. package/dist/pivot-chart.js +25 -69
  95. package/dist/sheet/address.d.ts +43 -0
  96. package/dist/sheet/address.js +81 -0
  97. package/dist/sheet/ast.d.ts +94 -0
  98. package/dist/sheet/ast.js +56 -0
  99. package/dist/sheet/auto-filter.d.ts +66 -0
  100. package/dist/sheet/auto-filter.js +147 -0
  101. package/dist/sheet/autocomplete.d.ts +42 -0
  102. package/dist/sheet/autocomplete.js +162 -0
  103. package/dist/sheet/coerce.d.ts +31 -0
  104. package/dist/sheet/coerce.js +122 -0
  105. package/dist/sheet/commands.d.ts +89 -0
  106. package/dist/sheet/commands.js +230 -0
  107. package/dist/sheet/comments.d.ts +29 -0
  108. package/dist/sheet/comments.js +73 -0
  109. package/dist/sheet/conditional-formats.d.ts +144 -0
  110. package/dist/sheet/conditional-formats.js +316 -0
  111. package/dist/sheet/deps.d.ts +60 -0
  112. package/dist/sheet/deps.js +199 -0
  113. package/dist/sheet/document.d.ts +150 -0
  114. package/dist/sheet/document.js +264 -0
  115. package/dist/sheet/edit-keys.d.ts +14 -0
  116. package/dist/sheet/edit-keys.js +40 -0
  117. package/dist/sheet/entry.d.ts +31 -0
  118. package/dist/sheet/entry.js +76 -0
  119. package/dist/sheet/evaluate.d.ts +61 -0
  120. package/dist/sheet/evaluate.js +337 -0
  121. package/dist/sheet/find-replace.d.ts +62 -0
  122. package/dist/sheet/find-replace.js +152 -0
  123. package/dist/sheet/format-store.d.ts +99 -0
  124. package/dist/sheet/format-store.js +233 -0
  125. package/dist/sheet/freeze.d.ts +54 -0
  126. package/dist/sheet/freeze.js +58 -0
  127. package/dist/sheet/function-catalog.d.ts +10 -0
  128. package/dist/sheet/function-catalog.js +94 -0
  129. package/dist/sheet/functions.d.ts +28 -0
  130. package/dist/sheet/functions.js +362 -0
  131. package/dist/sheet/goal-seek.d.ts +73 -0
  132. package/dist/sheet/goal-seek.js +153 -0
  133. package/dist/sheet/index.d.ts +53 -0
  134. package/dist/sheet/index.js +53 -0
  135. package/dist/sheet/merges.d.ts +71 -0
  136. package/dist/sheet/merges.js +137 -0
  137. package/dist/sheet/names.d.ts +31 -0
  138. package/dist/sheet/names.js +87 -0
  139. package/dist/sheet/navigate.d.ts +52 -0
  140. package/dist/sheet/navigate.js +135 -0
  141. package/dist/sheet/number-format.d.ts +63 -0
  142. package/dist/sheet/number-format.js +508 -0
  143. package/dist/sheet/palette.d.ts +19 -0
  144. package/dist/sheet/palette.js +84 -0
  145. package/dist/sheet/parse.d.ts +13 -0
  146. package/dist/sheet/parse.js +108 -0
  147. package/dist/sheet/paste-special.d.ts +164 -0
  148. package/dist/sheet/paste-special.js +592 -0
  149. package/dist/sheet/protection.d.ts +21 -0
  150. package/dist/sheet/protection.js +48 -0
  151. package/dist/sheet/rects.d.ts +45 -0
  152. package/dist/sheet/rects.js +112 -0
  153. package/dist/sheet/refs.d.ts +82 -0
  154. package/dist/sheet/refs.js +266 -0
  155. package/dist/sheet/ribbon-icons.d.ts +31 -0
  156. package/dist/sheet/ribbon-icons.js +350 -0
  157. package/dist/sheet/ribbon.d.ts +173 -0
  158. package/dist/sheet/ribbon.js +753 -0
  159. package/dist/sheet/shortcuts.d.ts +164 -0
  160. package/dist/sheet/shortcuts.js +623 -0
  161. package/dist/sheet/structure.d.ts +87 -0
  162. package/dist/sheet/structure.js +166 -0
  163. package/dist/sheet/tables.d.ts +70 -0
  164. package/dist/sheet/tables.js +147 -0
  165. package/dist/sheet/tokenize.d.ts +54 -0
  166. package/dist/sheet/tokenize.js +304 -0
  167. package/dist/sheet/transforms.d.ts +76 -0
  168. package/dist/sheet/transforms.js +154 -0
  169. package/dist/sheet/validation.d.ts +92 -0
  170. package/dist/sheet/validation.js +241 -0
  171. package/dist/sheet/workbook.d.ts +72 -0
  172. package/dist/sheet/workbook.js +409 -0
  173. package/dist/sheet-enable.d.ts +6 -0
  174. package/dist/sheet-enable.js +46 -0
  175. package/dist/studio/ui-components.generated.js +63 -1
  176. package/dist/version.d.ts +1 -1
  177. package/dist/version.js +1 -1
  178. package/package.json +9 -4
  179. package/dist/designer/assets/GridMenus-B1JtIN21.js +0 -8
  180. package/dist/designer/assets/SvDateRangeInput-CjO7nn_x.js +0 -1
  181. package/dist/designer/assets/SvDateTimePicker-ByBD_ov6.js +0 -1
  182. package/dist/designer/assets/SvField-YDLipR6O.css +0 -1
  183. package/dist/designer/assets/SvField-cj1PFNgZ.js +0 -1
  184. package/dist/designer/assets/SvGridCellEditor-0W1L2hJ1.js +0 -2
  185. package/dist/designer/assets/SvGridChart-DBOR4xcz.js +0 -2
  186. package/dist/designer/assets/SvGridChart-fkMWeh1J.css +0 -1
  187. package/dist/designer/assets/SvGridChartPanel-symrhNnR.js +0 -11
  188. package/dist/designer/assets/SvGridChartView-BmRxq6mU.js +0 -1
  189. package/dist/designer/assets/SvListBox-1oUF6Iav.js +0 -110
  190. package/dist/designer/assets/chart-DXFganU8.js +0 -1
  191. package/dist/designer/assets/column-resize-C_inr5zT.js +0 -1
  192. package/dist/designer/assets/date-format-DGagAqRc.css +0 -1
  193. package/dist/designer/assets/date-format-onlYDsrY.js +0 -1
  194. package/dist/designer/assets/easing-zaypKrr0.js +0 -3
  195. package/dist/designer/assets/index-k2stW6L8.css +0 -1
  196. package/dist/designer/assets/row-resize-B0FWHcNr.js +0 -1
  197. package/dist/designer/assets/src-D9p27OGC.js +0 -58
@@ -0,0 +1,623 @@
1
+ import { edgeOfRegion, currentRegion, isWholeSheet, wholeSheet, isBlankValue, } from './navigate.js';
2
+ import { fillDown, fillRight, stampDate, copyFromAbove, copyValueFromAbove, targetRect, guessSumRange, looksNumeric, } from './commands.js';
3
+ import { FORMAT_PRESETS } from './number-format.js';
4
+ import { formatA1 } from './address.js';
5
+ import { insertRows, deleteRows, insertColumns, deleteColumns, axisForSelection, getStructureTarget, } from './structure.js';
6
+ import { getFindTarget } from './find-replace.js';
7
+ import { cycleReference } from './edit-keys.js';
8
+ /** The guard's answer, with the refusal reported when it is no. */
9
+ export function formatAllowed(target, rects) {
10
+ if (target.guard?.(rects) === false) {
11
+ target.refused?.();
12
+ return false;
13
+ }
14
+ return true;
15
+ }
16
+ let formatTarget = null;
17
+ /** Attach the store the formatting shortcuts write to. */
18
+ export function setFormatTarget(target) {
19
+ formatTarget = target;
20
+ }
21
+ export function getFormatTarget() {
22
+ return formatTarget;
23
+ }
24
+ /**
25
+ * The workbook the sheet-switching keys act on.
26
+ *
27
+ * Optional, like everything else here: a single-sheet grid never sets one and
28
+ * Ctrl+PageDown falls through to the browser's own tab switching, which is
29
+ * what someone with one sheet expects it to do.
30
+ */
31
+ let workbook = null;
32
+ let onWorkbookChange = null;
33
+ export function setWorkbook(next, onChange) {
34
+ workbook = next;
35
+ onWorkbookChange = onChange ?? null;
36
+ }
37
+ export function getWorkbook() {
38
+ return workbook;
39
+ }
40
+ /** Move `delta` sheets from the active one. Excel does NOT wrap at the ends,
41
+ * so neither does this: hitting the last sheet and pressing again should
42
+ * leave you there rather than teleport you to the first. */
43
+ export function switchSheet(delta) {
44
+ const wb = workbook;
45
+ if (!wb)
46
+ return false;
47
+ const sheets = wb.sheets;
48
+ const at = sheets.indexOf(wb.active);
49
+ if (at < 0)
50
+ return false;
51
+ const next = at + delta;
52
+ if (next < 0 || next >= sheets.length)
53
+ return false;
54
+ wb.setActive(sheets[next]);
55
+ onWorkbookChange?.();
56
+ return true;
57
+ }
58
+ /** Called when Ctrl+Shift+V fires, so a consumer can open its own Paste
59
+ * Special dialog. Same reasoning as the other two: the keyboard layer owns
60
+ * the key, the consumer owns the chrome. */
61
+ let onPasteSpecial = null;
62
+ export function setPasteSpecialHandler(fn) {
63
+ onPasteSpecial = fn;
64
+ }
65
+ /** Called when Ctrl+H fires, so a consumer can open its own Find and Replace
66
+ * panel. Same reasoning as the Format Cells dialog: the keyboard layer owns
67
+ * the key, the consumer owns the chrome. */
68
+ let onFindReplace = null;
69
+ export function setFindReplaceHandler(fn) {
70
+ onFindReplace = fn;
71
+ }
72
+ let onRibbonAction = null;
73
+ export function setRibbonActionHandler(fn) {
74
+ onRibbonAction = fn;
75
+ }
76
+ const raise = (action) => (cmd) => onRibbonAction?.(action, cmd) ?? false;
77
+ /** Raise a ribbon action from a button whose face is not an entry of its own. */
78
+ export function raiseRibbonAction(action, cmd) {
79
+ return onRibbonAction?.(action, cmd) ?? false;
80
+ }
81
+ /** Excel's size ladder. Pixels here, since that is what the cell renders. */
82
+ export const FONT_SIZES = [8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72];
83
+ /** A cell with no size of its own renders at the grid's 13px. */
84
+ export const DEFAULT_FONT_SIZE = 13;
85
+ /** The format entry under the active cell, or undefined. */
86
+ export function activeEntry(cmd) {
87
+ const target = formatTarget;
88
+ const active = cmd.activeCell;
89
+ if (!target || !active)
90
+ return undefined;
91
+ const rowId = target.lookup.rowIdAt(active.rowIndex);
92
+ const columnId = target.lookup.columnIdAt(active.colIndex);
93
+ return rowId != null && columnId != null ? target.store.get(rowId, columnId) : undefined;
94
+ }
95
+ /** Excel's Increase / Decrease Font Size: the next rung of the ladder. */
96
+ export function nudgeFontSize(direction) {
97
+ return (cmd) => {
98
+ const current = activeEntry(cmd)?.fontSize ?? DEFAULT_FONT_SIZE;
99
+ const larger = FONT_SIZES.filter((s) => s > current);
100
+ const smaller = FONT_SIZES.filter((s) => s < current);
101
+ const next = direction > 0 ? larger[0] : smaller[smaller.length - 1];
102
+ if (next === undefined)
103
+ return false;
104
+ return applyFormat(cmd, { fontSize: next === DEFAULT_FONT_SIZE ? undefined : next });
105
+ };
106
+ }
107
+ let onFormatDialog = null;
108
+ export function setFormatDialogHandler(fn) {
109
+ onFormatDialog = fn;
110
+ }
111
+ /** Read the grid through the command context as a blank-aware matrix. */
112
+ export function gridOf(cmd) {
113
+ return {
114
+ rowCount: cmd.rowCount,
115
+ colCount: cmd.colCount,
116
+ isBlank: (r, c) => isBlankValue(cmd.getCellValue(r, c)),
117
+ };
118
+ }
119
+ /**
120
+ * Ctrl+End: the bottom-right corner of what has been typed, as in Excel,
121
+ * rather than the grid's last cell. A sheet has thousands of empty rows
122
+ * below the data, and the last of them is never where the user wanted to
123
+ * go. Ctrl+Home is the grid's own and already lands on A1.
124
+ */
125
+ export function lastUsedCell(cmd) {
126
+ let row = -1;
127
+ let col = -1;
128
+ for (let r = 0; r < cmd.rowCount; r += 1) {
129
+ for (let c = 0; c < cmd.colCount; c += 1) {
130
+ if (isBlankValue(cmd.getCellValue(r, c)))
131
+ continue;
132
+ if (r > row)
133
+ row = r;
134
+ if (c > col)
135
+ col = c;
136
+ }
137
+ }
138
+ return row < 0 ? { row: 0, col: 0 } : { row, col };
139
+ }
140
+ export const goToLastUsed = (cmd) => {
141
+ const { row, col } = lastUsedCell(cmd);
142
+ cmd.setActiveCell(row, col);
143
+ cmd.setSelection(row, col);
144
+ cmd.scrollIntoView(row, col);
145
+ return true;
146
+ };
147
+ /** Ctrl+Shift+End / Home: extend the selection to the last used cell, or to A1. */
148
+ function extendTo(where) {
149
+ return (cmd) => {
150
+ if (!cmd.activeCell)
151
+ return false;
152
+ const { row, col } = where === 'end' ? lastUsedCell(cmd) : { row: 0, col: 0 };
153
+ cmd.extendSelection(row, col);
154
+ cmd.scrollIntoView(row, col);
155
+ return true;
156
+ };
157
+ }
158
+ /** F4 while editing: turn the reference at the caret through its anchorings. */
159
+ export const pinReference = (cmd) => {
160
+ const el = cmd.editor;
161
+ if (!el)
162
+ return false;
163
+ const edit = cycleReference(el.value, el.selectionStart ?? el.value.length);
164
+ if (!edit)
165
+ return false;
166
+ el.value = edit.text;
167
+ el.setSelectionRange(edit.caret, edit.caret);
168
+ // The grid keeps its own copy of the draft and reads it from input events.
169
+ el.dispatchEvent(new Event('input', { bubbles: true }));
170
+ return true;
171
+ };
172
+ export function move(dir, extend) {
173
+ return (cmd) => {
174
+ const active = cmd.activeCell;
175
+ if (!active)
176
+ return false;
177
+ if (extend) {
178
+ // The range grows from its far corner and the active cell stays put,
179
+ // as in Excel; stepping from the active cell would reach the same
180
+ // edge on every press.
181
+ const from = cmd.selectionFocus ?? { rowIndex: active.rowIndex, colIndex: active.colIndex };
182
+ const to = pastHidden(cmd, { row: from.rowIndex, col: from.colIndex }, edgeOfRegion(gridOf(cmd), { row: from.rowIndex, col: from.colIndex }, dir), dir);
183
+ cmd.extendSelection(to.row, to.col);
184
+ // Along the axis of the key only, as Excel scrolls: a sideways jump
185
+ // keeps the rows where they are.
186
+ const sideways = dir === 'left' || dir === 'right';
187
+ cmd.scrollIntoView(sideways ? active.rowIndex : to.row, sideways ? to.col : active.colIndex);
188
+ return true;
189
+ }
190
+ const to = pastHidden(cmd, { row: active.rowIndex, col: active.colIndex }, edgeOfRegion(gridOf(cmd), { row: active.rowIndex, col: active.colIndex }, dir), dir);
191
+ cmd.setActiveCell(to.row, to.col);
192
+ cmd.scrollIntoView(to.row, to.col);
193
+ cmd.setSelection(to.row, to.col);
194
+ return true;
195
+ };
196
+ }
197
+ /**
198
+ * A jump never rests on a hidden row or column, as Excel's does not. The
199
+ * edge it found is the end of a run of data, or the first cell of the next
200
+ * run; a hidden edge stands in for the nearest cell of the same run that
201
+ * shows, which lies back toward the start when the run continues behind
202
+ * the landing, and further on when the landing began a new run. With no
203
+ * such cell the cursor goes on to the next line that shows at all. Only the
204
+ * grid's api knows what is hidden; a context without one (a test's fake)
205
+ * hides nothing.
206
+ */
207
+ function pastHidden(cmd, from, to, dir) {
208
+ const api = cmd.api;
209
+ const vertical = dir === 'up' || dir === 'down';
210
+ const hidden = vertical
211
+ ? (i) => api?.isRowCollapsed?.(i) ?? false
212
+ : (i) => { const id = cmd.columnIdAt(i); return id != null && (api?.isColumnCollapsed?.(id) ?? false); };
213
+ const at = vertical ? to.row : to.col;
214
+ if (!hidden(at))
215
+ return to;
216
+ const grid = gridOf(cmd);
217
+ const count = vertical ? cmd.rowCount : cmd.colCount;
218
+ const step = dir === 'down' || dir === 'right' ? 1 : -1;
219
+ const cell = (i) => (vertical ? { row: i, col: to.col } : { row: to.row, col: i });
220
+ const blank = (i) => i < 0 || i >= count || grid.isBlank(cell(i).row, cell(i).col);
221
+ const origin = vertical ? from.row : from.col;
222
+ // The run continues back toward the start: the last of it that shows,
223
+ // unless that is where the jump began, in which case the run is done and
224
+ // the jump goes on from its hidden end to the next run.
225
+ if (!blank(at - step)) {
226
+ for (let i = at - step; i !== origin && i >= 0 && i < count; i -= step) {
227
+ if (blank(i))
228
+ break;
229
+ if (!hidden(i))
230
+ return cell(i);
231
+ }
232
+ const next = edgeOfRegion(grid, to, dir);
233
+ if (next.row !== to.row || next.col !== to.col)
234
+ return pastHidden(cmd, to, next, dir);
235
+ }
236
+ // The landing began a run: the first of it that shows.
237
+ for (let i = at + step; i >= 0 && i < count && !blank(i); i += step) {
238
+ if (!hidden(i))
239
+ return cell(i);
240
+ }
241
+ // Nothing of the run shows: the next line that shows at all, else stay.
242
+ for (let i = at + step; i >= 0 && i < count; i += step)
243
+ if (!hidden(i))
244
+ return cell(i);
245
+ for (let i = at - step; i !== origin - step && i >= 0 && i < count; i -= step)
246
+ if (!hidden(i))
247
+ return cell(i);
248
+ return to;
249
+ }
250
+ /** Ctrl+A: the current region, then the whole sheet on a second press. */
251
+ export const selectRegion = (cmd) => {
252
+ const active = cmd.activeCell;
253
+ if (!active)
254
+ return false;
255
+ const g = gridOf(cmd);
256
+ const current = targetRect(cmd);
257
+ const region = currentRegion(g, { row: active.rowIndex, col: active.colIndex });
258
+ const already = current !== null &&
259
+ current[0] === region[0] && current[1] === region[1] &&
260
+ current[2] === region[2] && current[3] === region[3];
261
+ const next = already && !isWholeSheet(g, region) ? wholeSheet(g) : region;
262
+ cmd.setSelection(next[0], next[1]);
263
+ cmd.extendSelection(next[2], next[3]);
264
+ return true;
265
+ };
266
+ /**
267
+ * Ctrl+Space / Shift+Space: the whole columns or rows the selection
268
+ * touches (B2:C3 gives rows 2:3), as in Excel; the active cell stays.
269
+ */
270
+ export function selectLine(axis) {
271
+ return (cmd) => {
272
+ const rect = targetRect(cmd);
273
+ if (!rect)
274
+ return false;
275
+ const [r1, c1, r2, c2] = rect;
276
+ if (axis === 'column') {
277
+ cmd.setSelection(0, c1);
278
+ cmd.extendSelection(Math.max(cmd.rowCount - 1, 0), c2);
279
+ }
280
+ else {
281
+ cmd.setSelection(r1, 0);
282
+ cmd.extendSelection(r2, Math.max(cmd.colCount - 1, 0));
283
+ }
284
+ return true;
285
+ };
286
+ }
287
+ /** Ctrl+Shift+8 (Ctrl+*): the current region, and only that. */
288
+ export const selectCurrentRegion = (cmd) => {
289
+ const active = cmd.activeCell;
290
+ if (!active)
291
+ return false;
292
+ const region = currentRegion(gridOf(cmd), { row: active.rowIndex, col: active.colIndex });
293
+ cmd.setSelection(region[0], region[1]);
294
+ cmd.extendSelection(region[2], region[3]);
295
+ return true;
296
+ };
297
+ /**
298
+ * Run a format change over `rects` as ONE undo step.
299
+ *
300
+ * The format store is not the grid's data, so a write to it is invisible
301
+ * to Ctrl+Z on its own: Bold would land and the Undo button would stay
302
+ * grey, which is not how a spreadsheet behaves. This snapshots every cell
303
+ * the change touches before and after, and hands the grid a step that puts
304
+ * either snapshot back. A cell's whole entry is restored rather than the
305
+ * one field, so undoing "bold" on a cell that was also made red does not
306
+ * have to know which of the two the press was about.
307
+ */
308
+ export function withFormatUndo(cmd, target, rects, mutate) {
309
+ const snapshot = () => {
310
+ const cells = [];
311
+ for (const [minRow, minCol, maxRow, maxCol] of rects) {
312
+ for (let r = minRow; r <= maxRow; r += 1) {
313
+ for (let c = minCol; c <= maxCol; c += 1) {
314
+ const rowId = target.lookup.rowIdAt(r);
315
+ const columnId = target.lookup.columnIdAt(c);
316
+ if (rowId == null || columnId == null)
317
+ continue;
318
+ const entry = target.store.get(rowId, columnId);
319
+ cells.push({ r, c, entry: entry ? { ...entry, border: entry.border ? { ...entry.border } : undefined } : undefined });
320
+ }
321
+ }
322
+ }
323
+ return cells;
324
+ };
325
+ const restore = (cells) => () => {
326
+ for (const { r, c, entry } of cells) {
327
+ target.store.clear([[r, c, r, c]], target.lookup);
328
+ if (entry)
329
+ target.store.set([[r, c, r, c]], entry, target.lookup);
330
+ }
331
+ target.onChange?.();
332
+ };
333
+ const before = snapshot();
334
+ mutate();
335
+ const after = snapshot();
336
+ // Optional at runtime: a context built by hand (a demo, an older integration)
337
+ // may predate the seam, and a format that cannot be undone is still a
338
+ // format that landed.
339
+ cmd.recordUndo?.(restore(before), restore(after));
340
+ }
341
+ /** Apply a patch to the selection through the attached store. */
342
+ export function applyFormat(cmd, patch) {
343
+ const target = formatTarget;
344
+ if (!target)
345
+ return false;
346
+ const rects = cmd.ranges.length ? cmd.ranges : rectOfActive(cmd);
347
+ if (!rects.length)
348
+ return false;
349
+ if (!formatAllowed(target, rects))
350
+ return false;
351
+ withFormatUndo(cmd, target, rects, () => target.store.set(rects, patch, target.lookup));
352
+ target.onChange?.();
353
+ return true;
354
+ }
355
+ export function toggleFormat(cmd, field) {
356
+ const target = formatTarget;
357
+ if (!target)
358
+ return false;
359
+ const rects = cmd.ranges.length ? cmd.ranges : rectOfActive(cmd);
360
+ if (!rects.length)
361
+ return false;
362
+ if (!formatAllowed(target, rects))
363
+ return false;
364
+ withFormatUndo(cmd, target, rects, () => target.store.toggle(rects, field, target.lookup));
365
+ target.onChange?.();
366
+ return true;
367
+ }
368
+ /**
369
+ * Excel's Borders menu, applied the way Excel applies it: "Bottom Border"
370
+ * puts a line under the bottom edge of the SELECTION, not under every
371
+ * cell; "All Borders" lines every cell; "Outside" frames the block. Sides
372
+ * merge with what a cell already has, so a top border does not erase a
373
+ * bottom one, and "No Border" clears all four.
374
+ */
375
+ export function applyBorders(cmd, preset) {
376
+ const target = formatTarget;
377
+ if (!target)
378
+ return false;
379
+ const rects = cmd.ranges.length ? cmd.ranges : rectOfActive(cmd);
380
+ if (!rects.length)
381
+ return false;
382
+ if (!formatAllowed(target, rects))
383
+ return false;
384
+ const line = { width: preset === 'thick-bottom' ? 2 : 1 };
385
+ const { store, lookup } = target;
386
+ withFormatUndo(cmd, target, rects, () => {
387
+ for (const [minRow, minCol, maxRow, maxCol] of rects) {
388
+ for (let r = minRow; r <= maxRow; r += 1) {
389
+ for (let c = minCol; c <= maxCol; c += 1) {
390
+ const rowId = lookup.rowIdAt(r);
391
+ const columnId = lookup.columnIdAt(c);
392
+ if (rowId == null || columnId == null)
393
+ continue;
394
+ const sides = new Set();
395
+ if (preset === 'all') {
396
+ sides.add('top');
397
+ sides.add('right');
398
+ sides.add('bottom');
399
+ sides.add('left');
400
+ }
401
+ if (preset === 'outside' || preset === 'top') {
402
+ if (r === minRow)
403
+ sides.add('top');
404
+ }
405
+ if (preset === 'outside' || preset === 'bottom' || preset === 'thick-bottom') {
406
+ if (r === maxRow)
407
+ sides.add('bottom');
408
+ }
409
+ if (preset === 'outside' || preset === 'left') {
410
+ if (c === minCol)
411
+ sides.add('left');
412
+ }
413
+ if (preset === 'outside' || preset === 'right') {
414
+ if (c === maxCol)
415
+ sides.add('right');
416
+ }
417
+ if (preset === 'none') {
418
+ store.set([[r, c, r, c]], { border: undefined }, lookup);
419
+ continue;
420
+ }
421
+ if (sides.size === 0)
422
+ continue;
423
+ const border = { ...(store.get(rowId, columnId)?.border ?? {}) };
424
+ for (const side of sides)
425
+ border[side] = line;
426
+ store.set([[r, c, r, c]], { border }, lookup);
427
+ }
428
+ }
429
+ }
430
+ });
431
+ target.onChange?.();
432
+ return true;
433
+ }
434
+ /** Excel's Clear Formats: drop every format on the selection, keep the values. */
435
+ export function clearFormats(cmd) {
436
+ const target = formatTarget;
437
+ if (!target)
438
+ return false;
439
+ const rects = cmd.ranges.length ? cmd.ranges : rectOfActive(cmd);
440
+ if (!rects.length)
441
+ return false;
442
+ if (!formatAllowed(target, rects))
443
+ return false;
444
+ withFormatUndo(cmd, target, rects, () => target.store.clear(rects, target.lookup));
445
+ target.onChange?.();
446
+ return true;
447
+ }
448
+ function rectOfActive(cmd) {
449
+ const rect = targetRect(cmd);
450
+ return rect ? [rect] : [];
451
+ }
452
+ export function preset(name) {
453
+ return (cmd) => applyFormat(cmd, { numFmt: FORMAT_PRESETS[name] });
454
+ }
455
+ /** Alt+=. Inserts =SUM(range) over the run Excel would guess. */
456
+ export const autoSum = (cmd) => {
457
+ const active = cmd.activeCell;
458
+ if (!active)
459
+ return false;
460
+ const range = guessSumRange(cmd, looksNumeric);
461
+ if (!range)
462
+ return false;
463
+ if (cmd.canEdit?.(active.rowIndex, active.colIndex) === false)
464
+ return false;
465
+ const [minRow, minCol, maxRow, maxCol] = range;
466
+ const ref = (r, c) => formatA1({ col: c, colAbs: false, row: r, rowAbs: false, sheet: null });
467
+ cmd.setCellValue(active.rowIndex, active.colIndex, `=SUM(${ref(minRow, minCol)}:${ref(maxRow, maxCol)})`);
468
+ return true;
469
+ };
470
+ export const SHEET_BINDINGS = [
471
+ // Navigation
472
+ { key: 'ArrowUp', mod: true, run: move('up', false), label: 'Jump to the edge of the data region' },
473
+ { key: 'ArrowDown', mod: true, run: move('down', false), label: 'Jump to the edge of the data region' },
474
+ { key: 'ArrowLeft', mod: true, run: move('left', false), label: 'Jump to the edge of the data region' },
475
+ { key: 'ArrowRight', mod: true, run: move('right', false), label: 'Jump to the edge of the data region' },
476
+ { key: 'ArrowUp', mod: true, shift: true, run: move('up', true), label: 'Extend the selection to the edge' },
477
+ { key: 'ArrowDown', mod: true, shift: true, run: move('down', true), label: 'Extend the selection to the edge' },
478
+ { key: 'ArrowLeft', mod: true, shift: true, run: move('left', true), label: 'Extend the selection to the edge' },
479
+ { key: 'ArrowRight', mod: true, shift: true, run: move('right', true), label: 'Extend the selection to the edge' },
480
+ { key: 'End', mod: true, run: goToLastUsed, label: 'Go to the last used cell' },
481
+ { key: 'End', mod: true, shift: true, run: extendTo('end'), label: 'Extend the selection to the last used cell' },
482
+ { key: 'Home', mod: true, shift: true, run: extendTo('home'), label: 'Extend the selection to A1' },
483
+ // While editing
484
+ { key: 'F4', editing: true, run: pinReference, label: 'Pin the reference at the caret ($A$1, A$1, $A1)' },
485
+ // Selection
486
+ { key: 'a', mod: true, run: selectRegion, label: 'Select the current region, then the sheet' },
487
+ { key: ' ', mod: true, shift: true, run: selectRegion, label: 'Select the current region, then the sheet' },
488
+ { key: '*', code: 'Digit8', mod: true, shift: true, run: selectCurrentRegion, label: 'Select the current region' },
489
+ { key: ' ', mod: true, run: selectLine('column'), label: 'Select the column' },
490
+ { key: ' ', shift: true, run: selectLine('row'), label: 'Select the row' },
491
+ // Fill and entry
492
+ { key: 'd', mod: true, run: (cmd) => fillDown(cmd), label: 'Fill down' },
493
+ { key: 'r', mod: true, run: (cmd) => fillRight(cmd), label: 'Fill right' },
494
+ { key: ';', mod: true, run: (cmd) => stampDate(cmd, 'date'), label: "Insert today's date" },
495
+ { key: ';', code: 'Semicolon', mod: true, shift: true, run: (cmd) => stampDate(cmd, 'time'), label: 'Insert the current time' },
496
+ { key: "'", mod: true, run: (cmd) => copyFromAbove(cmd), label: 'Copy the cell above, unchanged' },
497
+ { key: '"', code: 'Quote', mod: true, shift: true, run: (cmd) => copyValueFromAbove(cmd), label: 'Copy the value of the cell above' },
498
+ { key: '&', code: 'Digit7', mod: true, shift: true, run: (cmd) => applyBorders(cmd, 'outside'), label: 'Outline border' },
499
+ { key: '_', code: 'Minus', mod: true, shift: true, run: (cmd) => applyBorders(cmd, 'none'), label: 'Remove borders' },
500
+ { key: '=', alt: true, run: autoSum, label: 'AutoSum the run above or to the left' },
501
+ // Formatting. These decline when no format store is attached, so the key
502
+ // falls through instead of looking broken.
503
+ { key: 'b', mod: true, run: (cmd) => toggleFormat(cmd, 'bold'), label: 'Bold' },
504
+ { key: 'i', mod: true, run: (cmd) => toggleFormat(cmd, 'italic'), label: 'Italic' },
505
+ { key: 'u', mod: true, run: (cmd) => toggleFormat(cmd, 'underline'), label: 'Underline' },
506
+ { key: '5', mod: true, run: (cmd) => toggleFormat(cmd, 'strike'), label: 'Strikethrough' },
507
+ { key: '1', mod: true, run: (cmd) => {
508
+ if (!onFormatDialog)
509
+ return false;
510
+ onFormatDialog(cmd);
511
+ return true;
512
+ }, label: 'Open Format Cells' },
513
+ { key: '1', code: 'Digit1', mod: true, shift: true, run: preset('number'), label: 'Number format' },
514
+ { key: '2', code: 'Digit2', mod: true, shift: true, run: preset('time'), label: 'Time format' },
515
+ { key: '3', code: 'Digit3', mod: true, shift: true, run: preset('date'), label: 'Date format' },
516
+ { key: '4', code: 'Digit4', mod: true, shift: true, run: preset('currency'), label: 'Currency format' },
517
+ { key: '5', code: 'Digit5', mod: true, shift: true, run: preset('percent'), label: 'Percent format' },
518
+ { key: '6', code: 'Digit6', mod: true, shift: true, run: preset('scientific'), label: 'Scientific format' },
519
+ { key: '`', code: 'Backquote', mod: true, shift: true, run: preset('general'), label: 'General format' },
520
+ { key: '>', code: 'Period', mod: true, shift: true, run: nudgeFontSize(1), label: 'Increase font size' },
521
+ { key: '<', code: 'Comma', mod: true, shift: true, run: nudgeFontSize(-1), label: 'Decrease font size' },
522
+ // What the ribbon's tooltips promise: the same actions from the keyboard.
523
+ { key: 'F9', run: raise('recalculate'), label: 'Recalculate' },
524
+ { key: '`', code: 'Backquote', mod: true, run: raise('toggle-formulas'), label: 'Show formulas' },
525
+ { key: 'l', mod: true, shift: true, run: raise('toggle-filter'), label: 'Filter' },
526
+ { key: 'F3', shift: true, run: raise('insert-function'), label: 'Insert Function' },
527
+ { key: 'F2', shift: true, run: raise('edit-comment'), label: 'Insert or edit the comment on the active cell' },
528
+ { key: 'ArrowDown', alt: true, run: raise('open-list'), label: 'Open the list a validated cell offers' },
529
+ { key: 'F1', mod: true, run: raise('toggle-ribbon'), label: 'Collapse or expand the ribbon' },
530
+ { key: 'F3', mod: true, run: raise('name-manager'), label: 'Name Manager' },
531
+ { key: 't', mod: true, run: raise('insert-table'), label: 'Format as Table' },
532
+ // Structure. Like the format bindings, these decline when nothing is
533
+ // attached. Excel opens a dialog for an ambiguous selection; deciding what
534
+ // that looks like is the consumer's, so an ambiguous selection declines and
535
+ // the consumer can bind its own dialog.
536
+ { key: '+', code: 'Equal', mod: true, shift: true, run: (cmd) => structural(cmd, 'insert'), label: 'Insert rows or columns' },
537
+ { key: '-', mod: true, run: (cmd) => structural(cmd, 'delete'), label: 'Delete rows or columns' },
538
+ // Hide and Unhide are raised, as Freeze is: the sheet shell keeps what
539
+ // is hidden per sheet and moves it with an insert or delete.
540
+ { key: '9', code: 'Digit9', mod: true, run: raise('hide-rows'), label: 'Hide the selected rows' },
541
+ { key: '0', code: 'Digit0', mod: true, run: raise('hide-columns'), label: 'Hide the selected columns' },
542
+ { key: '(', code: 'Digit9', mod: true, shift: true, run: raise('unhide-rows'), label: 'Unhide rows in the selection' },
543
+ { key: ')', code: 'Digit0', mod: true, shift: true, run: raise('unhide-columns'), label: 'Unhide columns in the selection' },
544
+ { key: 'h', mod: true, run: (cmd) => {
545
+ if (!onFindReplace || !getFindTarget())
546
+ return false;
547
+ onFindReplace(cmd);
548
+ return true;
549
+ }, label: 'Find and Replace' },
550
+ { key: 'v', mod: true, shift: true, run: (cmd) => {
551
+ if (!onPasteSpecial)
552
+ return false;
553
+ onPasteSpecial(cmd);
554
+ return true;
555
+ }, label: 'Paste Special' },
556
+ // Workbook. These decline without a workbook attached, so a single-sheet
557
+ // grid leaves Ctrl+PageDown to the browser.
558
+ { key: 'PageDown', mod: true, run: () => switchSheet(1), label: 'Next sheet' },
559
+ { key: 'PageUp', mod: true, run: () => switchSheet(-1), label: 'Previous sheet' },
560
+ { key: 'F11', shift: true, run: () => {
561
+ const wb = workbook;
562
+ if (!wb)
563
+ return false;
564
+ wb.addSheet();
565
+ onWorkbookChange?.();
566
+ return true;
567
+ }, label: 'New sheet' },
568
+ ];
569
+ export function structural(cmd, kind) {
570
+ if (!getStructureTarget())
571
+ return false;
572
+ const axis = axisForSelection(cmd);
573
+ if (axis === 'ambiguous')
574
+ return false;
575
+ if (kind === 'insert') {
576
+ return axis === 'rows' ? insertRows(cmd) : insertColumns(cmd);
577
+ }
578
+ return axis === 'rows' ? deleteRows(cmd) : deleteColumns(cmd);
579
+ }
580
+ function matches(binding, event) {
581
+ if (binding.code) {
582
+ // Fall back to `key` when `code` is absent, which is how synthesized
583
+ // events in tests and some assistive tech behave.
584
+ const seen = event.code || '';
585
+ if (seen ? seen !== binding.code : event.key.toLowerCase() !== binding.key.toLowerCase()) {
586
+ return false;
587
+ }
588
+ }
589
+ else if (event.key.toLowerCase() !== binding.key.toLowerCase()) {
590
+ return false;
591
+ }
592
+ const mod = event.ctrlKey || event.metaKey;
593
+ if (mod !== (binding.mod ?? false))
594
+ return false;
595
+ if (event.shiftKey !== (binding.shift ?? false))
596
+ return false;
597
+ if (event.altKey !== (binding.alt ?? false))
598
+ return false;
599
+ return true;
600
+ }
601
+ /**
602
+ * The handler registered with the grid. Returns true when a binding ran, which
603
+ * is what tells the grid to stop and not interpret the key itself.
604
+ *
605
+ * A binding marked `editing` runs only inside the editor and the others
606
+ * only outside it: Ctrl+D while someone is typing in a cell is a keystroke
607
+ * they meant for the text, and F4 on the grid root has nothing to pin.
608
+ * Alt+Enter needs no binding: the grid's multiline text editor takes it as
609
+ * a line break itself.
610
+ */
611
+ export function handleSheetKey(event, cmd) {
612
+ for (const binding of SHEET_BINDINGS) {
613
+ if ((binding.editing ?? false) !== cmd.editing)
614
+ continue;
615
+ if (!matches(binding, event))
616
+ continue;
617
+ if (!binding.run(cmd, event))
618
+ return false;
619
+ event.preventDefault();
620
+ return true;
621
+ }
622
+ return false;
623
+ }