@lotics/xlsx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +30 -0
- package/src/auto_sum.test.ts +71 -0
- package/src/auto_sum.ts +83 -0
- package/src/canvas_cf.ts +135 -0
- package/src/canvas_chart.ts +687 -0
- package/src/canvas_fill.ts +156 -0
- package/src/canvas_images.ts +99 -0
- package/src/canvas_layout.test.ts +159 -0
- package/src/canvas_layout.ts +239 -0
- package/src/canvas_renderer.ts +1010 -0
- package/src/canvas_shape.ts +242 -0
- package/src/canvas_sparkline.ts +163 -0
- package/src/canvas_text.ts +454 -0
- package/src/cell_editor.ts +558 -0
- package/src/clipboard.test.ts +145 -0
- package/src/clipboard.ts +341 -0
- package/src/command_history.ts +102 -0
- package/src/csv_parser.test.ts +130 -0
- package/src/csv_parser.ts +172 -0
- package/src/data_validation.test.ts +95 -0
- package/src/data_validation.ts +114 -0
- package/src/editing_layer.test.ts +309 -0
- package/src/excel_cf_evaluate.test.ts +293 -0
- package/src/excel_cf_evaluate.ts +719 -0
- package/src/excel_cf_icons.ts +108 -0
- package/src/excel_cf_types.ts +67 -0
- package/src/excel_numfmt.test.ts +607 -0
- package/src/excel_numfmt.ts +1033 -0
- package/src/excel_parser.test.ts +1061 -0
- package/src/excel_parser.ts +393 -0
- package/src/excel_pattern_fills.ts +64 -0
- package/src/excel_table_styles.ts +160 -0
- package/src/excel_utils.test.ts +108 -0
- package/src/excel_utils.ts +162 -0
- package/src/fast-formula-parser.d.ts +53 -0
- package/src/fill_logic.ts +257 -0
- package/src/fill_patterns.test.ts +90 -0
- package/src/fill_patterns.ts +71 -0
- package/src/flash_fill.test.ts +75 -0
- package/src/flash_fill.ts +221 -0
- package/src/formula_deps.ts +189 -0
- package/src/formula_engine.test.ts +348 -0
- package/src/formula_engine.ts +401 -0
- package/src/formula_highlight.test.ts +81 -0
- package/src/formula_highlight.ts +139 -0
- package/src/formula_suggest.ts +100 -0
- package/src/hidden_sheets.test.ts +49 -0
- package/src/index.ts +149 -0
- package/src/keyboard_nav.test.ts +394 -0
- package/src/keyboard_nav.ts +891 -0
- package/src/move_logic.ts +63 -0
- package/src/numfmt_type.test.ts +158 -0
- package/src/numfmt_type.ts +231 -0
- package/src/ooxml_cell_format.ts +70 -0
- package/src/ooxml_chart.test.ts +85 -0
- package/src/ooxml_chart.ts +347 -0
- package/src/ooxml_chart_types.ts +207 -0
- package/src/ooxml_color.ts +339 -0
- package/src/ooxml_drawing.test.ts +87 -0
- package/src/ooxml_drawing.ts +287 -0
- package/src/ooxml_pivot.test.ts +195 -0
- package/src/ooxml_pivot.ts +468 -0
- package/src/ooxml_pivot_types.ts +165 -0
- package/src/ooxml_shape.ts +355 -0
- package/src/ooxml_sheet.ts +1271 -0
- package/src/ooxml_styles.ts +556 -0
- package/src/ooxml_table.test.ts +70 -0
- package/src/ooxml_table.ts +131 -0
- package/src/ooxml_workbook.test.ts +40 -0
- package/src/ooxml_workbook.ts +259 -0
- package/src/ooxml_zip.ts +33 -0
- package/src/pivot_model.ts +237 -0
- package/src/pivot_recompute.test.ts +210 -0
- package/src/pivot_recompute.ts +413 -0
- package/src/selection_model.ts +364 -0
- package/src/spreadsheet_commands.test.ts +772 -0
- package/src/spreadsheet_commands.ts +1805 -0
- package/src/structured_refs.test.ts +128 -0
- package/src/structured_refs.ts +160 -0
- package/src/style_helpers.test.ts +33 -0
- package/src/style_helpers.ts +30 -0
- package/src/template_builder.test.ts +139 -0
- package/src/template_builder.ts +36 -0
- package/src/types.ts +239 -0
- package/src/workbook_model.test.ts +536 -0
- package/src/workbook_model.ts +911 -0
- package/src/xlsx_round_trip.test.ts +279 -0
- package/src/xlsx_writer.test.ts +488 -0
- package/src/xlsx_writer.ts +1549 -0
- package/src/xml_entities.ts +23 -0
|
@@ -0,0 +1,1805 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Spreadsheet Commands
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Reversible command implementations for all spreadsheet mutations.
|
|
6
|
+
// Each command captures enough state to undo itself.
|
|
7
|
+
// Used with CommandHistory for undo/redo support.
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
import type { Command } from "./command_history";
|
|
11
|
+
import { SheetModel } from "./workbook_model";
|
|
12
|
+
import type { WorkbookModel, CellValue } from "./workbook_model";
|
|
13
|
+
import type { CellStyle, CellContent } from "./types";
|
|
14
|
+
import type { ParsedConditionalFormat } from "./excel_cf_types";
|
|
15
|
+
import type { FormulaEngine } from "./formula_engine";
|
|
16
|
+
import type { PivotTableModel } from "./pivot_model";
|
|
17
|
+
import type { ParsedPivotTable } from "./ooxml_pivot_types";
|
|
18
|
+
import { rowColToRef, refToRowCol } from "./excel_utils";
|
|
19
|
+
import { adjustFormulaRefsForRowShift, adjustFormulaRefsForColShift } from "./clipboard";
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Set Cell Value
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
export function setCellValueCommand(
|
|
26
|
+
workbook: WorkbookModel,
|
|
27
|
+
engine: FormulaEngine | undefined,
|
|
28
|
+
sheetIndex: number,
|
|
29
|
+
row: number,
|
|
30
|
+
col: number,
|
|
31
|
+
newValue: string,
|
|
32
|
+
): Command {
|
|
33
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
34
|
+
const ref = rowColToRef(row, col);
|
|
35
|
+
const oldCell = sheet.getCell(ref);
|
|
36
|
+
const oldValue = oldCell?.value ?? null;
|
|
37
|
+
const oldFormula = oldCell?.formula;
|
|
38
|
+
// Determine if this is a formula
|
|
39
|
+
const isFormula = newValue.startsWith("=");
|
|
40
|
+
const formula = isFormula ? newValue.slice(1) : undefined;
|
|
41
|
+
const value: CellValue = isFormula
|
|
42
|
+
? null // Will be computed by formula engine
|
|
43
|
+
: parseInputValue(newValue);
|
|
44
|
+
|
|
45
|
+
const styleIndex = oldCell?.styleIndex ?? 0;
|
|
46
|
+
const numFmtCode = oldCell?.numFmtCode ?? "General";
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
description: `Set ${ref} = ${newValue}`,
|
|
50
|
+
execute() {
|
|
51
|
+
sheet.set(ref, value, sheet.styles?.get(styleIndex), numFmtCode, formula);
|
|
52
|
+
if (engine) {
|
|
53
|
+
engine.onCellChanged(sheet.name, ref, formula);
|
|
54
|
+
}
|
|
55
|
+
workbook.emit({
|
|
56
|
+
type: "cell_value",
|
|
57
|
+
sheet: sheetIndex,
|
|
58
|
+
ref,
|
|
59
|
+
oldValue,
|
|
60
|
+
newValue: value,
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
undo() {
|
|
64
|
+
if (oldValue === null && !oldFormula) {
|
|
65
|
+
sheet.deleteCell(ref);
|
|
66
|
+
} else {
|
|
67
|
+
sheet.set(ref, oldValue, sheet.styles?.get(styleIndex), numFmtCode, oldFormula);
|
|
68
|
+
}
|
|
69
|
+
if (engine) {
|
|
70
|
+
engine.onCellChanged(sheet.name, ref, oldFormula);
|
|
71
|
+
}
|
|
72
|
+
workbook.emit({
|
|
73
|
+
type: "cell_value",
|
|
74
|
+
sheet: sheetIndex,
|
|
75
|
+
ref,
|
|
76
|
+
oldValue: value,
|
|
77
|
+
newValue: oldValue,
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Set Cell Style
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
export function setCellStyleCommand(
|
|
88
|
+
workbook: WorkbookModel,
|
|
89
|
+
sheetIndex: number,
|
|
90
|
+
row: number,
|
|
91
|
+
col: number,
|
|
92
|
+
styleOverride: Partial<CellStyle>,
|
|
93
|
+
): Command {
|
|
94
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
95
|
+
const ref = rowColToRef(row, col);
|
|
96
|
+
const cell = sheet.getCell(ref);
|
|
97
|
+
const oldStyleIndex = cell?.styleIndex ?? 0;
|
|
98
|
+
const newStyleIndex = workbook.styles.merge(oldStyleIndex, styleOverride);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
description: `Style ${ref}`,
|
|
102
|
+
execute() {
|
|
103
|
+
let c = sheet.getCell(ref);
|
|
104
|
+
if (!c) {
|
|
105
|
+
c = sheet.set(ref, null, sheet.styles?.get(newStyleIndex));
|
|
106
|
+
} else {
|
|
107
|
+
c.styleIndex = newStyleIndex;
|
|
108
|
+
}
|
|
109
|
+
workbook.emit({
|
|
110
|
+
type: "cell_style",
|
|
111
|
+
sheet: sheetIndex,
|
|
112
|
+
ref,
|
|
113
|
+
oldStyle: oldStyleIndex,
|
|
114
|
+
newStyle: newStyleIndex,
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
undo() {
|
|
118
|
+
let c = sheet.getCell(ref);
|
|
119
|
+
if (!c) {
|
|
120
|
+
c = sheet.set(ref, null, sheet.styles?.get(oldStyleIndex));
|
|
121
|
+
} else {
|
|
122
|
+
c.styleIndex = oldStyleIndex;
|
|
123
|
+
}
|
|
124
|
+
workbook.emit({
|
|
125
|
+
type: "cell_style",
|
|
126
|
+
sheet: sheetIndex,
|
|
127
|
+
ref,
|
|
128
|
+
oldStyle: newStyleIndex,
|
|
129
|
+
newStyle: oldStyleIndex,
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// Set Range Style (apply style to all cells in a range)
|
|
137
|
+
// =============================================================================
|
|
138
|
+
|
|
139
|
+
export function setRangeStyleCommand(
|
|
140
|
+
workbook: WorkbookModel,
|
|
141
|
+
sheetIndex: number,
|
|
142
|
+
startRow: number,
|
|
143
|
+
startCol: number,
|
|
144
|
+
endRow: number,
|
|
145
|
+
endCol: number,
|
|
146
|
+
styleOverride: Partial<CellStyle>,
|
|
147
|
+
): Command {
|
|
148
|
+
const commands: Command[] = [];
|
|
149
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
150
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
151
|
+
commands.push(setCellStyleCommand(workbook, sheetIndex, r, c, styleOverride));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const rangeRef = `${rowColToRef(startRow, startCol)}:${rowColToRef(endRow, endCol)}`;
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
description: `Style ${rangeRef}`,
|
|
159
|
+
execute() {
|
|
160
|
+
for (const cmd of commands) cmd.execute();
|
|
161
|
+
},
|
|
162
|
+
undo() {
|
|
163
|
+
for (let i = commands.length - 1; i >= 0; i--) commands[i].undo();
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// Delete Cell
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
export function deleteCellCommand(
|
|
173
|
+
workbook: WorkbookModel,
|
|
174
|
+
engine: FormulaEngine | undefined,
|
|
175
|
+
sheetIndex: number,
|
|
176
|
+
row: number,
|
|
177
|
+
col: number,
|
|
178
|
+
): Command {
|
|
179
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
180
|
+
const ref = rowColToRef(row, col);
|
|
181
|
+
const oldCell = sheet.getCell(ref);
|
|
182
|
+
const snapshot = oldCell
|
|
183
|
+
? {
|
|
184
|
+
value: oldCell.value,
|
|
185
|
+
displayValue: oldCell.displayValue,
|
|
186
|
+
formula: oldCell.formula,
|
|
187
|
+
styleIndex: oldCell.styleIndex,
|
|
188
|
+
numFmtCode: oldCell.numFmtCode,
|
|
189
|
+
content: oldCell.content,
|
|
190
|
+
richText: oldCell.richText,
|
|
191
|
+
}
|
|
192
|
+
: null;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
description: `Clear ${ref}`,
|
|
196
|
+
execute() {
|
|
197
|
+
sheet.deleteCell(ref);
|
|
198
|
+
if (engine) {
|
|
199
|
+
engine.onCellChanged(sheet.name, ref);
|
|
200
|
+
}
|
|
201
|
+
workbook.emit({
|
|
202
|
+
type: "cell_value",
|
|
203
|
+
sheet: sheetIndex,
|
|
204
|
+
ref,
|
|
205
|
+
oldValue: snapshot?.value ?? null,
|
|
206
|
+
newValue: null,
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
undo() {
|
|
210
|
+
if (snapshot) {
|
|
211
|
+
sheet.set(ref, snapshot.value, sheet.styles?.get(snapshot.styleIndex), snapshot.numFmtCode, snapshot.formula);
|
|
212
|
+
}
|
|
213
|
+
if (engine) {
|
|
214
|
+
engine.onCellChanged(sheet.name, ref, snapshot?.formula);
|
|
215
|
+
}
|
|
216
|
+
workbook.emit({
|
|
217
|
+
type: "cell_value",
|
|
218
|
+
sheet: sheetIndex,
|
|
219
|
+
ref,
|
|
220
|
+
oldValue: null,
|
|
221
|
+
newValue: snapshot?.value ?? null,
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// =============================================================================
|
|
228
|
+
// Clear Range (delete all cells in a range)
|
|
229
|
+
// =============================================================================
|
|
230
|
+
|
|
231
|
+
export function clearRangeCommand(
|
|
232
|
+
workbook: WorkbookModel,
|
|
233
|
+
engine: FormulaEngine | undefined,
|
|
234
|
+
sheetIndex: number,
|
|
235
|
+
startRow: number,
|
|
236
|
+
startCol: number,
|
|
237
|
+
endRow: number,
|
|
238
|
+
endCol: number,
|
|
239
|
+
): Command {
|
|
240
|
+
const commands: Command[] = [];
|
|
241
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
242
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
243
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
244
|
+
const ref = rowColToRef(r, c);
|
|
245
|
+
if (sheet.getCell(ref)) {
|
|
246
|
+
commands.push(deleteCellCommand(workbook, engine, sheetIndex, r, c));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const rangeRef = `${rowColToRef(startRow, startCol)}:${rowColToRef(endRow, endCol)}`;
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
description: `Clear ${rangeRef}`,
|
|
255
|
+
execute() {
|
|
256
|
+
for (const cmd of commands) cmd.execute();
|
|
257
|
+
},
|
|
258
|
+
undo() {
|
|
259
|
+
for (let i = commands.length - 1; i >= 0; i--) commands[i].undo();
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// =============================================================================
|
|
265
|
+
// Merge Cells
|
|
266
|
+
// =============================================================================
|
|
267
|
+
|
|
268
|
+
export function mergeCellsCommand(
|
|
269
|
+
workbook: WorkbookModel,
|
|
270
|
+
sheetIndex: number,
|
|
271
|
+
startRow: number,
|
|
272
|
+
startCol: number,
|
|
273
|
+
endRow: number,
|
|
274
|
+
endCol: number,
|
|
275
|
+
): Command {
|
|
276
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
277
|
+
const mergeRef = `${rowColToRef(startRow, startCol)}:${rowColToRef(endRow, endCol)}`;
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
description: `Merge ${mergeRef}`,
|
|
281
|
+
execute() {
|
|
282
|
+
if (!sheet.mergedCells.includes(mergeRef)) {
|
|
283
|
+
sheet.mergedCells.push(mergeRef);
|
|
284
|
+
}
|
|
285
|
+
workbook.emit({ type: "merge", sheet: sheetIndex, ref: mergeRef });
|
|
286
|
+
},
|
|
287
|
+
undo() {
|
|
288
|
+
const idx = sheet.mergedCells.indexOf(mergeRef);
|
|
289
|
+
if (idx >= 0) sheet.mergedCells.splice(idx, 1);
|
|
290
|
+
workbook.emit({ type: "unmerge", sheet: sheetIndex, ref: mergeRef });
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// =============================================================================
|
|
296
|
+
// Unmerge Cells
|
|
297
|
+
// =============================================================================
|
|
298
|
+
|
|
299
|
+
export function unmergeCellsCommand(
|
|
300
|
+
workbook: WorkbookModel,
|
|
301
|
+
sheetIndex: number,
|
|
302
|
+
mergeRef: string,
|
|
303
|
+
): Command {
|
|
304
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
description: `Unmerge ${mergeRef}`,
|
|
308
|
+
execute() {
|
|
309
|
+
const idx = sheet.mergedCells.indexOf(mergeRef);
|
|
310
|
+
if (idx >= 0) sheet.mergedCells.splice(idx, 1);
|
|
311
|
+
workbook.emit({ type: "unmerge", sheet: sheetIndex, ref: mergeRef });
|
|
312
|
+
},
|
|
313
|
+
undo() {
|
|
314
|
+
if (!sheet.mergedCells.includes(mergeRef)) {
|
|
315
|
+
sheet.mergedCells.push(mergeRef);
|
|
316
|
+
}
|
|
317
|
+
workbook.emit({ type: "merge", sheet: sheetIndex, ref: mergeRef });
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// =============================================================================
|
|
323
|
+
// Insert Rows
|
|
324
|
+
// =============================================================================
|
|
325
|
+
|
|
326
|
+
export function insertRowsCommand(
|
|
327
|
+
workbook: WorkbookModel,
|
|
328
|
+
sheetIndex: number,
|
|
329
|
+
at: number,
|
|
330
|
+
count: number,
|
|
331
|
+
): Command {
|
|
332
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
333
|
+
// Snapshot merged ranges so undo can restore them. Cheaper than recomputing
|
|
334
|
+
// the inverse of `adjustMergedCellsForRowInsert` and avoids edge cases when
|
|
335
|
+
// inserts split existing ranges.
|
|
336
|
+
let mergeSnapshot: string[] = [];
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
description: `Insert ${count} row${count > 1 ? "s" : ""} at ${at}`,
|
|
340
|
+
execute() {
|
|
341
|
+
mergeSnapshot = [...sheet.mergedCells];
|
|
342
|
+
// Shift existing cells down
|
|
343
|
+
const toMove: Array<{ ref: string; row: number; col: number }> = [];
|
|
344
|
+
for (const [ref] of sheet.cells) {
|
|
345
|
+
const rc = refToRowColSafe(ref);
|
|
346
|
+
if (rc && rc.row >= at) toMove.push({ ref, ...rc });
|
|
347
|
+
}
|
|
348
|
+
toMove.sort((a, b) => b.row - a.row); // bottom-up to avoid collisions
|
|
349
|
+
for (const { ref, row, col } of toMove) {
|
|
350
|
+
const cell = sheet.cells.get(ref)!;
|
|
351
|
+
sheet.cells.delete(ref);
|
|
352
|
+
sheet.cells.set(rowColToRef(row + count, col), cell);
|
|
353
|
+
}
|
|
354
|
+
// Shift row heights
|
|
355
|
+
const heightsToShift = Array.from(sheet.rowHeights.entries()).filter(([r]) => r >= at).sort((a, b) => b[0] - a[0]);
|
|
356
|
+
for (const [r, h] of heightsToShift) {
|
|
357
|
+
sheet.rowHeights.delete(r);
|
|
358
|
+
sheet.rowHeights.set(r + count, h);
|
|
359
|
+
}
|
|
360
|
+
// Adjust formula references and merged-cell ranges in lock-step.
|
|
361
|
+
adjustAllFormulasForRowShift(sheet, at, count);
|
|
362
|
+
adjustMergedCellsForRowInsert(sheet, at, count);
|
|
363
|
+
workbook.emit({ type: "rows_inserted", sheet: sheetIndex, at, count });
|
|
364
|
+
},
|
|
365
|
+
undo() {
|
|
366
|
+
sheet.mergedCells = [...mergeSnapshot];
|
|
367
|
+
// Shift cells back up
|
|
368
|
+
const toMove: Array<{ ref: string; row: number; col: number }> = [];
|
|
369
|
+
for (const [ref] of sheet.cells) {
|
|
370
|
+
const rc = refToRowColSafe(ref);
|
|
371
|
+
if (rc && rc.row >= at + count) toMove.push({ ref, ...rc });
|
|
372
|
+
}
|
|
373
|
+
// Delete inserted empty rows
|
|
374
|
+
for (const [ref] of sheet.cells) {
|
|
375
|
+
const rc = refToRowColSafe(ref);
|
|
376
|
+
if (rc && rc.row >= at && rc.row < at + count) sheet.cells.delete(ref);
|
|
377
|
+
}
|
|
378
|
+
toMove.sort((a, b) => a.row - b.row); // top-down
|
|
379
|
+
for (const { ref, row, col } of toMove) {
|
|
380
|
+
const cell = sheet.cells.get(ref)!;
|
|
381
|
+
sheet.cells.delete(ref);
|
|
382
|
+
sheet.cells.set(rowColToRef(row - count, col), cell);
|
|
383
|
+
}
|
|
384
|
+
// Shift row heights back
|
|
385
|
+
const heightsToShift = Array.from(sheet.rowHeights.entries()).filter(([r]) => r >= at + count).sort((a, b) => a[0] - b[0]);
|
|
386
|
+
for (const [r, h] of heightsToShift) {
|
|
387
|
+
sheet.rowHeights.delete(r);
|
|
388
|
+
sheet.rowHeights.set(r - count, h);
|
|
389
|
+
}
|
|
390
|
+
for (let r = at; r < at + count; r++) sheet.rowHeights.delete(r);
|
|
391
|
+
workbook.emit({ type: "rows_deleted", sheet: sheetIndex, at, count });
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// =============================================================================
|
|
397
|
+
// Delete Rows
|
|
398
|
+
// =============================================================================
|
|
399
|
+
|
|
400
|
+
export function deleteRowsCommand(
|
|
401
|
+
workbook: WorkbookModel,
|
|
402
|
+
sheetIndex: number,
|
|
403
|
+
at: number,
|
|
404
|
+
count: number,
|
|
405
|
+
): Command {
|
|
406
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
407
|
+
// Snapshot deleted cells
|
|
408
|
+
const snapshot = new Map<string, { value: CellValue; displayValue: string; formula?: string; styleIndex: number; numFmtCode: string; content: CellContent }>();
|
|
409
|
+
for (const [ref, cell] of sheet.cells) {
|
|
410
|
+
const rc = refToRowColSafe(ref);
|
|
411
|
+
if (rc && rc.row >= at && rc.row < at + count) {
|
|
412
|
+
snapshot.set(ref, { value: cell.value, displayValue: cell.displayValue, formula: cell.formula, styleIndex: cell.styleIndex, numFmtCode: cell.numFmtCode, content: cell.content });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
let mergeSnapshot: string[] = [];
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
description: `Delete ${count} row${count > 1 ? "s" : ""} at ${at}`,
|
|
419
|
+
execute() {
|
|
420
|
+
mergeSnapshot = [...sheet.mergedCells];
|
|
421
|
+
// Delete cells in the range
|
|
422
|
+
for (const ref of snapshot.keys()) sheet.cells.delete(ref);
|
|
423
|
+
// Shift cells up
|
|
424
|
+
const toMove: Array<{ ref: string; row: number; col: number }> = [];
|
|
425
|
+
for (const [ref] of sheet.cells) {
|
|
426
|
+
const rc = refToRowColSafe(ref);
|
|
427
|
+
if (rc && rc.row >= at + count) toMove.push({ ref, ...rc });
|
|
428
|
+
}
|
|
429
|
+
toMove.sort((a, b) => a.row - b.row);
|
|
430
|
+
for (const { ref, row, col } of toMove) {
|
|
431
|
+
const cell = sheet.cells.get(ref)!;
|
|
432
|
+
sheet.cells.delete(ref);
|
|
433
|
+
sheet.cells.set(rowColToRef(row - count, col), cell);
|
|
434
|
+
}
|
|
435
|
+
adjustAllFormulasForRowShift(sheet, at, -count);
|
|
436
|
+
adjustMergedCellsForRowDelete(sheet, at, count);
|
|
437
|
+
workbook.emit({ type: "rows_deleted", sheet: sheetIndex, at, count });
|
|
438
|
+
},
|
|
439
|
+
undo() {
|
|
440
|
+
sheet.mergedCells = [...mergeSnapshot];
|
|
441
|
+
// Shift cells back down
|
|
442
|
+
const toMove: Array<{ ref: string; row: number; col: number }> = [];
|
|
443
|
+
for (const [ref] of sheet.cells) {
|
|
444
|
+
const rc = refToRowColSafe(ref);
|
|
445
|
+
if (rc && rc.row >= at) toMove.push({ ref, ...rc });
|
|
446
|
+
}
|
|
447
|
+
toMove.sort((a, b) => b.row - a.row);
|
|
448
|
+
for (const { ref, row, col } of toMove) {
|
|
449
|
+
const cell = sheet.cells.get(ref)!;
|
|
450
|
+
sheet.cells.delete(ref);
|
|
451
|
+
sheet.cells.set(rowColToRef(row + count, col), cell);
|
|
452
|
+
}
|
|
453
|
+
// Restore snapshot
|
|
454
|
+
for (const [ref, snap] of snapshot) {
|
|
455
|
+
sheet.set(ref, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
|
|
456
|
+
}
|
|
457
|
+
workbook.emit({ type: "rows_inserted", sheet: sheetIndex, at, count });
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// =============================================================================
|
|
463
|
+
// Insert Columns
|
|
464
|
+
// =============================================================================
|
|
465
|
+
|
|
466
|
+
export function insertColsCommand(
|
|
467
|
+
workbook: WorkbookModel,
|
|
468
|
+
sheetIndex: number,
|
|
469
|
+
at: number,
|
|
470
|
+
count: number,
|
|
471
|
+
): Command {
|
|
472
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
473
|
+
let mergeSnapshot: string[] = [];
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
description: `Insert ${count} column${count > 1 ? "s" : ""} at ${at}`,
|
|
477
|
+
execute() {
|
|
478
|
+
mergeSnapshot = [...sheet.mergedCells];
|
|
479
|
+
const toMove: Array<{ ref: string; row: number; col: number }> = [];
|
|
480
|
+
for (const [ref] of sheet.cells) {
|
|
481
|
+
const rc = refToRowColSafe(ref);
|
|
482
|
+
if (rc && rc.col >= at) toMove.push({ ref, ...rc });
|
|
483
|
+
}
|
|
484
|
+
toMove.sort((a, b) => b.col - a.col);
|
|
485
|
+
for (const { ref, row, col } of toMove) {
|
|
486
|
+
const cell = sheet.cells.get(ref)!;
|
|
487
|
+
sheet.cells.delete(ref);
|
|
488
|
+
sheet.cells.set(rowColToRef(row, col + count), cell);
|
|
489
|
+
}
|
|
490
|
+
const widthsToShift = Array.from(sheet.colWidths.entries()).filter(([c]) => c >= at).sort((a, b) => b[0] - a[0]);
|
|
491
|
+
for (const [c, w] of widthsToShift) {
|
|
492
|
+
sheet.colWidths.delete(c);
|
|
493
|
+
sheet.colWidths.set(c + count, w);
|
|
494
|
+
}
|
|
495
|
+
adjustAllFormulasForColShift(sheet, at, count);
|
|
496
|
+
adjustMergedCellsForColInsert(sheet, at, count);
|
|
497
|
+
workbook.emit({ type: "cols_inserted", sheet: sheetIndex, at, count });
|
|
498
|
+
},
|
|
499
|
+
undo() {
|
|
500
|
+
sheet.mergedCells = [...mergeSnapshot];
|
|
501
|
+
const toMove: Array<{ ref: string; row: number; col: number }> = [];
|
|
502
|
+
for (const [ref] of sheet.cells) {
|
|
503
|
+
const rc = refToRowColSafe(ref);
|
|
504
|
+
if (rc && rc.col >= at + count) toMove.push({ ref, ...rc });
|
|
505
|
+
}
|
|
506
|
+
for (const [ref] of sheet.cells) {
|
|
507
|
+
const rc = refToRowColSafe(ref);
|
|
508
|
+
if (rc && rc.col >= at && rc.col < at + count) sheet.cells.delete(ref);
|
|
509
|
+
}
|
|
510
|
+
toMove.sort((a, b) => a.col - b.col);
|
|
511
|
+
for (const { ref, row, col } of toMove) {
|
|
512
|
+
const cell = sheet.cells.get(ref)!;
|
|
513
|
+
sheet.cells.delete(ref);
|
|
514
|
+
sheet.cells.set(rowColToRef(row, col - count), cell);
|
|
515
|
+
}
|
|
516
|
+
const widthsToShift = Array.from(sheet.colWidths.entries()).filter(([c]) => c >= at + count).sort((a, b) => a[0] - b[0]);
|
|
517
|
+
for (const [c, w] of widthsToShift) {
|
|
518
|
+
sheet.colWidths.delete(c);
|
|
519
|
+
sheet.colWidths.set(c - count, w);
|
|
520
|
+
}
|
|
521
|
+
for (let c = at; c < at + count; c++) sheet.colWidths.delete(c);
|
|
522
|
+
workbook.emit({ type: "cols_deleted", sheet: sheetIndex, at, count });
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// =============================================================================
|
|
528
|
+
// Delete Columns
|
|
529
|
+
// =============================================================================
|
|
530
|
+
|
|
531
|
+
export function deleteColsCommand(
|
|
532
|
+
workbook: WorkbookModel,
|
|
533
|
+
sheetIndex: number,
|
|
534
|
+
at: number,
|
|
535
|
+
count: number,
|
|
536
|
+
): Command {
|
|
537
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
538
|
+
const snapshot = new Map<string, { value: CellValue; displayValue: string; formula?: string; styleIndex: number; numFmtCode: string; content: CellContent }>();
|
|
539
|
+
for (const [ref, cell] of sheet.cells) {
|
|
540
|
+
const rc = refToRowColSafe(ref);
|
|
541
|
+
if (rc && rc.col >= at && rc.col < at + count) {
|
|
542
|
+
snapshot.set(ref, { value: cell.value, displayValue: cell.displayValue, formula: cell.formula, styleIndex: cell.styleIndex, numFmtCode: cell.numFmtCode, content: cell.content });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
let mergeSnapshot: string[] = [];
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
description: `Delete ${count} column${count > 1 ? "s" : ""} at ${at}`,
|
|
549
|
+
execute() {
|
|
550
|
+
mergeSnapshot = [...sheet.mergedCells];
|
|
551
|
+
for (const ref of snapshot.keys()) sheet.cells.delete(ref);
|
|
552
|
+
const toMove: Array<{ ref: string; row: number; col: number }> = [];
|
|
553
|
+
for (const [ref] of sheet.cells) {
|
|
554
|
+
const rc = refToRowColSafe(ref);
|
|
555
|
+
if (rc && rc.col >= at + count) toMove.push({ ref, ...rc });
|
|
556
|
+
}
|
|
557
|
+
toMove.sort((a, b) => a.col - b.col);
|
|
558
|
+
for (const { ref, row, col } of toMove) {
|
|
559
|
+
const cell = sheet.cells.get(ref)!;
|
|
560
|
+
sheet.cells.delete(ref);
|
|
561
|
+
sheet.cells.set(rowColToRef(row, col - count), cell);
|
|
562
|
+
}
|
|
563
|
+
adjustAllFormulasForColShift(sheet, at, -count);
|
|
564
|
+
adjustMergedCellsForColDelete(sheet, at, count);
|
|
565
|
+
workbook.emit({ type: "cols_deleted", sheet: sheetIndex, at, count });
|
|
566
|
+
},
|
|
567
|
+
undo() {
|
|
568
|
+
sheet.mergedCells = [...mergeSnapshot];
|
|
569
|
+
const toMove: Array<{ ref: string; row: number; col: number }> = [];
|
|
570
|
+
for (const [ref] of sheet.cells) {
|
|
571
|
+
const rc = refToRowColSafe(ref);
|
|
572
|
+
if (rc && rc.col >= at) toMove.push({ ref, ...rc });
|
|
573
|
+
}
|
|
574
|
+
toMove.sort((a, b) => b.col - a.col);
|
|
575
|
+
for (const { ref, row, col } of toMove) {
|
|
576
|
+
const cell = sheet.cells.get(ref)!;
|
|
577
|
+
sheet.cells.delete(ref);
|
|
578
|
+
sheet.cells.set(rowColToRef(row, col + count), cell);
|
|
579
|
+
}
|
|
580
|
+
for (const [ref, snap] of snapshot) {
|
|
581
|
+
sheet.set(ref, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
|
|
582
|
+
}
|
|
583
|
+
workbook.emit({ type: "cols_inserted", sheet: sheetIndex, at, count });
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// =============================================================================
|
|
589
|
+
// Set Column Width
|
|
590
|
+
// =============================================================================
|
|
591
|
+
|
|
592
|
+
export function setColWidthCommand(
|
|
593
|
+
workbook: WorkbookModel,
|
|
594
|
+
sheetIndex: number,
|
|
595
|
+
col: number,
|
|
596
|
+
newWidth: number,
|
|
597
|
+
): Command {
|
|
598
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
599
|
+
const oldWidth = sheet.colWidths.get(col) ?? sheet.defaultColWidth;
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
description: `Resize column ${col}`,
|
|
603
|
+
execute() {
|
|
604
|
+
sheet.colWidths.set(col, newWidth);
|
|
605
|
+
},
|
|
606
|
+
undo() {
|
|
607
|
+
if (oldWidth === sheet.defaultColWidth) {
|
|
608
|
+
sheet.colWidths.delete(col);
|
|
609
|
+
} else {
|
|
610
|
+
sheet.colWidths.set(col, oldWidth);
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// =============================================================================
|
|
617
|
+
// Set Row Height
|
|
618
|
+
// =============================================================================
|
|
619
|
+
|
|
620
|
+
export function setRowHeightCommand(
|
|
621
|
+
workbook: WorkbookModel,
|
|
622
|
+
sheetIndex: number,
|
|
623
|
+
row: number,
|
|
624
|
+
newHeight: number,
|
|
625
|
+
): Command {
|
|
626
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
627
|
+
const oldHeight = sheet.rowHeights.get(row) ?? sheet.defaultRowHeight;
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
description: `Resize row ${row}`,
|
|
631
|
+
execute() {
|
|
632
|
+
sheet.rowHeights.set(row, newHeight);
|
|
633
|
+
},
|
|
634
|
+
undo() {
|
|
635
|
+
if (oldHeight === sheet.defaultRowHeight) {
|
|
636
|
+
sheet.rowHeights.delete(row);
|
|
637
|
+
} else {
|
|
638
|
+
sheet.rowHeights.set(row, oldHeight);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// =============================================================================
|
|
645
|
+
// Set Named Range
|
|
646
|
+
// =============================================================================
|
|
647
|
+
|
|
648
|
+
export function setNamedRangeCommand(
|
|
649
|
+
workbook: WorkbookModel,
|
|
650
|
+
name: string,
|
|
651
|
+
expr: string,
|
|
652
|
+
): Command {
|
|
653
|
+
const oldExpr = workbook.namedRanges.get(name);
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
description: `Define name ${name} = ${expr}`,
|
|
657
|
+
execute() {
|
|
658
|
+
workbook.namedRanges.set(name, expr);
|
|
659
|
+
},
|
|
660
|
+
undo() {
|
|
661
|
+
if (oldExpr === undefined) {
|
|
662
|
+
workbook.namedRanges.delete(name);
|
|
663
|
+
} else {
|
|
664
|
+
workbook.namedRanges.set(name, oldExpr);
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// =============================================================================
|
|
671
|
+
// Set Range Number Format
|
|
672
|
+
// =============================================================================
|
|
673
|
+
|
|
674
|
+
export function setRangeNumFmtCommand(
|
|
675
|
+
workbook: WorkbookModel,
|
|
676
|
+
sheetIndex: number,
|
|
677
|
+
startRow: number,
|
|
678
|
+
startCol: number,
|
|
679
|
+
endRow: number,
|
|
680
|
+
endCol: number,
|
|
681
|
+
numFmtCode: string,
|
|
682
|
+
): Command {
|
|
683
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
684
|
+
const snapshot = new Map<string, { value: CellValue; styleIndex: number; numFmtCode: string; formula?: string } | null>();
|
|
685
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
686
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
687
|
+
const ref = rowColToRef(r, c);
|
|
688
|
+
const cell = sheet.getCell(ref);
|
|
689
|
+
snapshot.set(ref, cell ? { value: cell.value, styleIndex: cell.styleIndex, numFmtCode: cell.numFmtCode, formula: cell.formula } : null);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const rangeRef = `${rowColToRef(startRow, startCol)}:${rowColToRef(endRow, endCol)}`;
|
|
693
|
+
|
|
694
|
+
return {
|
|
695
|
+
description: `Format ${rangeRef} as ${numFmtCode}`,
|
|
696
|
+
execute() {
|
|
697
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
698
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
699
|
+
const ref = rowColToRef(r, c);
|
|
700
|
+
const cell = sheet.getCell(ref);
|
|
701
|
+
if (cell) {
|
|
702
|
+
sheet.set(ref, cell.value, sheet.styles?.get(cell.styleIndex), numFmtCode, cell.formula);
|
|
703
|
+
} else {
|
|
704
|
+
sheet.set(ref, null, undefined, numFmtCode);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
undo() {
|
|
710
|
+
for (const [ref, snap] of snapshot) {
|
|
711
|
+
if (snap) {
|
|
712
|
+
sheet.set(ref, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
|
|
713
|
+
} else {
|
|
714
|
+
sheet.deleteCell(ref);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// =============================================================================
|
|
722
|
+
// Sheet Management
|
|
723
|
+
// =============================================================================
|
|
724
|
+
|
|
725
|
+
export function addSheetCommand(workbook: WorkbookModel, name: string): Command {
|
|
726
|
+
return {
|
|
727
|
+
description: `Add sheet ${name}`,
|
|
728
|
+
execute() {
|
|
729
|
+
workbook.sheets.push(new SheetModel(name, workbook.styles));
|
|
730
|
+
workbook.emit({ type: "sheet_added", index: workbook.sheets.length - 1 });
|
|
731
|
+
},
|
|
732
|
+
undo() {
|
|
733
|
+
const index = workbook.sheets.length - 1;
|
|
734
|
+
workbook.sheets.pop();
|
|
735
|
+
workbook.emit({ type: "sheet_deleted", index });
|
|
736
|
+
},
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export function deleteSheetCommand(workbook: WorkbookModel, index: number): Command {
|
|
741
|
+
const snapshot = workbook.sheets[index];
|
|
742
|
+
return {
|
|
743
|
+
description: `Delete sheet ${snapshot.name}`,
|
|
744
|
+
execute() {
|
|
745
|
+
workbook.sheets.splice(index, 1);
|
|
746
|
+
workbook.emit({ type: "sheet_deleted", index });
|
|
747
|
+
},
|
|
748
|
+
undo() {
|
|
749
|
+
workbook.sheets.splice(index, 0, snapshot);
|
|
750
|
+
workbook.emit({ type: "sheet_added", index });
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export function renameSheetCommand(workbook: WorkbookModel, index: number, newName: string): Command {
|
|
756
|
+
const oldName = workbook.sheets[index].name;
|
|
757
|
+
return {
|
|
758
|
+
description: `Rename sheet ${oldName} → ${newName}`,
|
|
759
|
+
execute() {
|
|
760
|
+
workbook.sheets[index].name = newName;
|
|
761
|
+
workbook.emit({ type: "sheet_renamed", index, oldName, newName });
|
|
762
|
+
},
|
|
763
|
+
undo() {
|
|
764
|
+
workbook.sheets[index].name = oldName;
|
|
765
|
+
workbook.emit({ type: "sheet_renamed", index, oldName: newName, newName: oldName });
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// =============================================================================
|
|
771
|
+
// Batch Command
|
|
772
|
+
// =============================================================================
|
|
773
|
+
|
|
774
|
+
export function batchCommand(
|
|
775
|
+
description: string,
|
|
776
|
+
commands: Command[],
|
|
777
|
+
): Command {
|
|
778
|
+
return {
|
|
779
|
+
description,
|
|
780
|
+
execute() {
|
|
781
|
+
for (const cmd of commands) cmd.execute();
|
|
782
|
+
},
|
|
783
|
+
undo() {
|
|
784
|
+
for (let i = commands.length - 1; i >= 0; i--) commands[i].undo();
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// =============================================================================
|
|
790
|
+
// Set Hidden Rows
|
|
791
|
+
// =============================================================================
|
|
792
|
+
|
|
793
|
+
// =============================================================================
|
|
794
|
+
// Add Table — convert a range to a styled Excel Table
|
|
795
|
+
// =============================================================================
|
|
796
|
+
|
|
797
|
+
export function addTableCommand(
|
|
798
|
+
workbook: WorkbookModel,
|
|
799
|
+
sheetIndex: number,
|
|
800
|
+
table: import("./ooxml_chart_types").ParsedTable,
|
|
801
|
+
): Command {
|
|
802
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
803
|
+
return {
|
|
804
|
+
description: `Insert table ${table.name}`,
|
|
805
|
+
execute() {
|
|
806
|
+
sheet.tables.push(table);
|
|
807
|
+
},
|
|
808
|
+
undo() {
|
|
809
|
+
const idx = sheet.tables.findIndex((t) => t.name === table.name);
|
|
810
|
+
if (idx >= 0) sheet.tables.splice(idx, 1);
|
|
811
|
+
},
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export function removeTableCommand(
|
|
816
|
+
workbook: WorkbookModel,
|
|
817
|
+
sheetIndex: number,
|
|
818
|
+
tableName: string,
|
|
819
|
+
): Command {
|
|
820
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
821
|
+
let savedIdx = -1;
|
|
822
|
+
let savedTable: import("./ooxml_chart_types").ParsedTable | undefined;
|
|
823
|
+
return {
|
|
824
|
+
description: `Convert table ${tableName} to range`,
|
|
825
|
+
execute() {
|
|
826
|
+
savedIdx = sheet.tables.findIndex((t) => t.name === tableName);
|
|
827
|
+
if (savedIdx >= 0) {
|
|
828
|
+
savedTable = sheet.tables[savedIdx];
|
|
829
|
+
sheet.tables.splice(savedIdx, 1);
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
undo() {
|
|
833
|
+
if (savedTable && savedIdx >= 0) {
|
|
834
|
+
sheet.tables.splice(savedIdx, 0, savedTable);
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function setSheetTabColorCommand(
|
|
841
|
+
workbook: WorkbookModel,
|
|
842
|
+
sheetIndex: number,
|
|
843
|
+
newColor: string | undefined,
|
|
844
|
+
): Command {
|
|
845
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
846
|
+
const oldColor = sheet.view.tabColor;
|
|
847
|
+
|
|
848
|
+
return {
|
|
849
|
+
description: `Set tab color`,
|
|
850
|
+
execute() {
|
|
851
|
+
sheet.view.tabColor = newColor;
|
|
852
|
+
},
|
|
853
|
+
undo() {
|
|
854
|
+
sheet.view.tabColor = oldColor;
|
|
855
|
+
},
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// =============================================================================
|
|
860
|
+
// Set Hidden Rows
|
|
861
|
+
// =============================================================================
|
|
862
|
+
|
|
863
|
+
export function setHiddenRowsCommand(
|
|
864
|
+
workbook: WorkbookModel,
|
|
865
|
+
sheetIndex: number,
|
|
866
|
+
newHidden: Set<number>,
|
|
867
|
+
): Command {
|
|
868
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
869
|
+
const oldHidden = new Set(sheet.hiddenRows);
|
|
870
|
+
|
|
871
|
+
return {
|
|
872
|
+
description: `Set hidden rows`,
|
|
873
|
+
execute() {
|
|
874
|
+
sheet.hiddenRows = new Set(newHidden);
|
|
875
|
+
},
|
|
876
|
+
undo() {
|
|
877
|
+
sheet.hiddenRows = new Set(oldHidden);
|
|
878
|
+
},
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// =============================================================================
|
|
883
|
+
// Set Hidden Cols
|
|
884
|
+
// =============================================================================
|
|
885
|
+
|
|
886
|
+
export function setHiddenColsCommand(
|
|
887
|
+
workbook: WorkbookModel,
|
|
888
|
+
sheetIndex: number,
|
|
889
|
+
newHidden: Set<number>,
|
|
890
|
+
): Command {
|
|
891
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
892
|
+
const oldHidden = new Set(sheet.hiddenCols);
|
|
893
|
+
|
|
894
|
+
return {
|
|
895
|
+
description: `Set hidden cols`,
|
|
896
|
+
execute() {
|
|
897
|
+
sheet.hiddenCols = new Set(newHidden);
|
|
898
|
+
},
|
|
899
|
+
undo() {
|
|
900
|
+
sheet.hiddenCols = new Set(oldHidden);
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// =============================================================================
|
|
906
|
+
// Set Pivot Config
|
|
907
|
+
// =============================================================================
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Replace a pivot's `config` (axis assignments, value aggregations, layout)
|
|
911
|
+
* and recompute. Captures the old config so undo restores it. The caller
|
|
912
|
+
* builds the next config; this command swaps it atomically.
|
|
913
|
+
*/
|
|
914
|
+
export function setPivotConfigCommand(
|
|
915
|
+
workbook: WorkbookModel,
|
|
916
|
+
pivot: PivotTableModel,
|
|
917
|
+
newConfig: ParsedPivotTable,
|
|
918
|
+
): Command {
|
|
919
|
+
const oldConfig = pivot.config;
|
|
920
|
+
|
|
921
|
+
return {
|
|
922
|
+
description: `Update pivot ${oldConfig.name}`,
|
|
923
|
+
execute() {
|
|
924
|
+
pivot.config = newConfig;
|
|
925
|
+
pivot.recompute(workbook);
|
|
926
|
+
},
|
|
927
|
+
undo() {
|
|
928
|
+
pivot.config = oldConfig;
|
|
929
|
+
pivot.recompute(workbook);
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// =============================================================================
|
|
935
|
+
// Sort Column
|
|
936
|
+
// =============================================================================
|
|
937
|
+
|
|
938
|
+
export function sortColumnCommand(
|
|
939
|
+
workbook: WorkbookModel,
|
|
940
|
+
sheetIndex: number,
|
|
941
|
+
col: number,
|
|
942
|
+
ascending: boolean,
|
|
943
|
+
): Command {
|
|
944
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
945
|
+
const used = sheet.getUsedRange();
|
|
946
|
+
|
|
947
|
+
// Snapshot ALL cells in the used range before sorting
|
|
948
|
+
const snapshot = new Map<string, { value: CellValue; displayValue: string; formula?: string; styleIndex: number; numFmtCode: string; content: CellContent } | undefined>();
|
|
949
|
+
for (let r = used.minRow; r <= used.maxRow; r++) {
|
|
950
|
+
for (let c = used.minCol; c <= used.maxCol; c++) {
|
|
951
|
+
const ref = rowColToRef(r, c);
|
|
952
|
+
const cell = sheet.getCell(ref);
|
|
953
|
+
if (cell) {
|
|
954
|
+
snapshot.set(ref, { value: cell.value, displayValue: cell.displayValue, formula: cell.formula, styleIndex: cell.styleIndex, numFmtCode: cell.numFmtCode, content: cell.content });
|
|
955
|
+
} else {
|
|
956
|
+
snapshot.set(ref, undefined);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
description: `Sort column ${col} ${ascending ? "asc" : "desc"}`,
|
|
963
|
+
execute() {
|
|
964
|
+
sortColumnInPlace(sheet, col, ascending);
|
|
965
|
+
},
|
|
966
|
+
undo() {
|
|
967
|
+
// Restore all cells from snapshot
|
|
968
|
+
for (const [ref, snap] of snapshot) {
|
|
969
|
+
if (snap) {
|
|
970
|
+
sheet.set(ref, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
|
|
971
|
+
} else {
|
|
972
|
+
sheet.deleteCell(ref);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// =============================================================================
|
|
980
|
+
// Multi-Column Sort
|
|
981
|
+
// =============================================================================
|
|
982
|
+
|
|
983
|
+
export interface SortSpec {
|
|
984
|
+
col: number;
|
|
985
|
+
ascending: boolean;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
export function multiSortCommand(
|
|
989
|
+
workbook: WorkbookModel,
|
|
990
|
+
sheetIndex: number,
|
|
991
|
+
sortSpecs: SortSpec[],
|
|
992
|
+
hasHeader: boolean,
|
|
993
|
+
): Command {
|
|
994
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
995
|
+
const used = sheet.getUsedRange();
|
|
996
|
+
|
|
997
|
+
// Snapshot ALL cells in the used range before sorting
|
|
998
|
+
const snapshot = new Map<string, { value: CellValue; displayValue: string; formula?: string; styleIndex: number; numFmtCode: string; content: CellContent } | undefined>();
|
|
999
|
+
for (let r = used.minRow; r <= used.maxRow; r++) {
|
|
1000
|
+
for (let c = used.minCol; c <= used.maxCol; c++) {
|
|
1001
|
+
const ref = rowColToRef(r, c);
|
|
1002
|
+
const cell = sheet.getCell(ref);
|
|
1003
|
+
if (cell) {
|
|
1004
|
+
snapshot.set(ref, { value: cell.value, displayValue: cell.displayValue, formula: cell.formula, styleIndex: cell.styleIndex, numFmtCode: cell.numFmtCode, content: cell.content });
|
|
1005
|
+
} else {
|
|
1006
|
+
snapshot.set(ref, undefined);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return {
|
|
1012
|
+
description: `Multi-sort by ${sortSpecs.length} keys`,
|
|
1013
|
+
execute() {
|
|
1014
|
+
multiSortInPlace(sheet, sortSpecs, hasHeader ? used.minRow + 1 : used.minRow);
|
|
1015
|
+
},
|
|
1016
|
+
undo() {
|
|
1017
|
+
for (const [ref, snap] of snapshot) {
|
|
1018
|
+
if (snap) {
|
|
1019
|
+
sheet.set(ref, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
|
|
1020
|
+
} else {
|
|
1021
|
+
sheet.deleteCell(ref);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// =============================================================================
|
|
1029
|
+
// Set Freeze Panes
|
|
1030
|
+
// =============================================================================
|
|
1031
|
+
|
|
1032
|
+
export function setFreezePanesCommand(
|
|
1033
|
+
workbook: WorkbookModel,
|
|
1034
|
+
sheetIndex: number,
|
|
1035
|
+
newFreeze: { row: number; col: number } | undefined,
|
|
1036
|
+
): Command {
|
|
1037
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
1038
|
+
const oldFreeze = sheet.freeze ? { ...sheet.freeze } : undefined;
|
|
1039
|
+
|
|
1040
|
+
return {
|
|
1041
|
+
description: newFreeze ? `Freeze at row ${newFreeze.row}, col ${newFreeze.col}` : "Unfreeze panes",
|
|
1042
|
+
execute() {
|
|
1043
|
+
sheet.freeze = newFreeze ? { ...newFreeze } : undefined;
|
|
1044
|
+
},
|
|
1045
|
+
undo() {
|
|
1046
|
+
sheet.freeze = oldFreeze ? { ...oldFreeze } : undefined;
|
|
1047
|
+
},
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// =============================================================================
|
|
1052
|
+
// Set Zoom
|
|
1053
|
+
// =============================================================================
|
|
1054
|
+
|
|
1055
|
+
export function setZoomCommand(
|
|
1056
|
+
workbook: WorkbookModel,
|
|
1057
|
+
sheetIndex: number,
|
|
1058
|
+
newZoom: number,
|
|
1059
|
+
): Command {
|
|
1060
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
1061
|
+
const oldZoom = sheet.view.zoomScale;
|
|
1062
|
+
|
|
1063
|
+
return {
|
|
1064
|
+
description: `Zoom ${oldZoom}% → ${newZoom}%`,
|
|
1065
|
+
execute() {
|
|
1066
|
+
sheet.view.zoomScale = newZoom;
|
|
1067
|
+
},
|
|
1068
|
+
undo() {
|
|
1069
|
+
sheet.view.zoomScale = oldZoom;
|
|
1070
|
+
},
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// =============================================================================
|
|
1075
|
+
// Add Conditional Format
|
|
1076
|
+
// =============================================================================
|
|
1077
|
+
|
|
1078
|
+
export function addConditionalFormatCommand(
|
|
1079
|
+
workbook: WorkbookModel,
|
|
1080
|
+
sheetIndex: number,
|
|
1081
|
+
cf: ParsedConditionalFormat,
|
|
1082
|
+
): Command {
|
|
1083
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
1084
|
+
|
|
1085
|
+
return {
|
|
1086
|
+
description: `Add CF rule on ${cf.ref}`,
|
|
1087
|
+
execute() {
|
|
1088
|
+
sheet.conditionalFormats.push(cf);
|
|
1089
|
+
},
|
|
1090
|
+
undo() {
|
|
1091
|
+
sheet.conditionalFormats.pop();
|
|
1092
|
+
},
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// =============================================================================
|
|
1097
|
+
// Delete Conditional Format
|
|
1098
|
+
// =============================================================================
|
|
1099
|
+
|
|
1100
|
+
export function deleteConditionalFormatCommand(
|
|
1101
|
+
workbook: WorkbookModel,
|
|
1102
|
+
sheetIndex: number,
|
|
1103
|
+
index: number,
|
|
1104
|
+
): Command {
|
|
1105
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
1106
|
+
const snapshot = sheet.conditionalFormats[index];
|
|
1107
|
+
|
|
1108
|
+
return {
|
|
1109
|
+
description: `Delete CF rule #${index}`,
|
|
1110
|
+
execute() {
|
|
1111
|
+
sheet.conditionalFormats.splice(index, 1);
|
|
1112
|
+
},
|
|
1113
|
+
undo() {
|
|
1114
|
+
sheet.conditionalFormats.splice(index, 0, snapshot);
|
|
1115
|
+
},
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// =============================================================================
|
|
1120
|
+
// Reorder Sheets
|
|
1121
|
+
// =============================================================================
|
|
1122
|
+
|
|
1123
|
+
export function reorderSheetsCommand(
|
|
1124
|
+
workbook: WorkbookModel,
|
|
1125
|
+
fromIndex: number,
|
|
1126
|
+
toIndex: number,
|
|
1127
|
+
): Command {
|
|
1128
|
+
return {
|
|
1129
|
+
description: `Move sheet ${fromIndex} → ${toIndex}`,
|
|
1130
|
+
execute() {
|
|
1131
|
+
const [sheet] = workbook.sheets.splice(fromIndex, 1);
|
|
1132
|
+
workbook.sheets.splice(toIndex, 0, sheet);
|
|
1133
|
+
},
|
|
1134
|
+
undo() {
|
|
1135
|
+
const [sheet] = workbook.sheets.splice(toIndex, 1);
|
|
1136
|
+
workbook.sheets.splice(fromIndex, 0, sheet);
|
|
1137
|
+
},
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// =============================================================================
|
|
1142
|
+
// Insert Cells (Shift Down)
|
|
1143
|
+
// =============================================================================
|
|
1144
|
+
|
|
1145
|
+
export function insertCellsShiftDownCommand(
|
|
1146
|
+
workbook: WorkbookModel,
|
|
1147
|
+
sheetIndex: number,
|
|
1148
|
+
startRow: number,
|
|
1149
|
+
startCol: number,
|
|
1150
|
+
endRow: number,
|
|
1151
|
+
endCol: number,
|
|
1152
|
+
): Command {
|
|
1153
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
1154
|
+
const height = endRow - startRow + 1;
|
|
1155
|
+
|
|
1156
|
+
return {
|
|
1157
|
+
description: `Insert cells shift down ${rowColToRef(startRow, startCol)}:${rowColToRef(endRow, endCol)}`,
|
|
1158
|
+
execute() {
|
|
1159
|
+
// For each column in the range, shift cells down by `height` starting from startRow
|
|
1160
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
1161
|
+
// Collect cells at or below startRow in this column
|
|
1162
|
+
const toMove: Array<{ row: number; ref: string }> = [];
|
|
1163
|
+
for (const [ref] of sheet.cells) {
|
|
1164
|
+
const rc = refToRowColSafe(ref);
|
|
1165
|
+
if (rc && rc.col === c && rc.row >= startRow) toMove.push({ row: rc.row, ref });
|
|
1166
|
+
}
|
|
1167
|
+
toMove.sort((a, b) => b.row - a.row); // bottom-up
|
|
1168
|
+
for (const { ref, row } of toMove) {
|
|
1169
|
+
const cell = sheet.cells.get(ref)!;
|
|
1170
|
+
sheet.cells.delete(ref);
|
|
1171
|
+
sheet.cells.set(rowColToRef(row + height, c), cell);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
},
|
|
1175
|
+
undo() {
|
|
1176
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
1177
|
+
// Delete the inserted empty cells
|
|
1178
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
1179
|
+
sheet.deleteCell(rowColToRef(r, c));
|
|
1180
|
+
}
|
|
1181
|
+
// Shift cells back up
|
|
1182
|
+
const toMove: Array<{ row: number; ref: string }> = [];
|
|
1183
|
+
for (const [ref] of sheet.cells) {
|
|
1184
|
+
const rc = refToRowColSafe(ref);
|
|
1185
|
+
if (rc && rc.col === c && rc.row >= startRow + height) toMove.push({ row: rc.row, ref });
|
|
1186
|
+
}
|
|
1187
|
+
toMove.sort((a, b) => a.row - b.row); // top-down
|
|
1188
|
+
for (const { ref, row } of toMove) {
|
|
1189
|
+
const cell = sheet.cells.get(ref)!;
|
|
1190
|
+
sheet.cells.delete(ref);
|
|
1191
|
+
sheet.cells.set(rowColToRef(row - height, c), cell);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// =============================================================================
|
|
1199
|
+
// Insert Cells (Shift Right)
|
|
1200
|
+
// =============================================================================
|
|
1201
|
+
|
|
1202
|
+
export function insertCellsShiftRightCommand(
|
|
1203
|
+
workbook: WorkbookModel,
|
|
1204
|
+
sheetIndex: number,
|
|
1205
|
+
startRow: number,
|
|
1206
|
+
startCol: number,
|
|
1207
|
+
endRow: number,
|
|
1208
|
+
endCol: number,
|
|
1209
|
+
): Command {
|
|
1210
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
1211
|
+
const width = endCol - startCol + 1;
|
|
1212
|
+
|
|
1213
|
+
return {
|
|
1214
|
+
description: `Insert cells shift right ${rowColToRef(startRow, startCol)}:${rowColToRef(endRow, endCol)}`,
|
|
1215
|
+
execute() {
|
|
1216
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
1217
|
+
const toMove: Array<{ col: number; ref: string }> = [];
|
|
1218
|
+
for (const [ref] of sheet.cells) {
|
|
1219
|
+
const rc = refToRowColSafe(ref);
|
|
1220
|
+
if (rc && rc.row === r && rc.col >= startCol) toMove.push({ col: rc.col, ref });
|
|
1221
|
+
}
|
|
1222
|
+
toMove.sort((a, b) => b.col - a.col); // right-to-left
|
|
1223
|
+
for (const { ref, col } of toMove) {
|
|
1224
|
+
const cell = sheet.cells.get(ref)!;
|
|
1225
|
+
sheet.cells.delete(ref);
|
|
1226
|
+
sheet.cells.set(rowColToRef(r, col + width), cell);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
},
|
|
1230
|
+
undo() {
|
|
1231
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
1232
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
1233
|
+
sheet.deleteCell(rowColToRef(r, c));
|
|
1234
|
+
}
|
|
1235
|
+
const toMove: Array<{ col: number; ref: string }> = [];
|
|
1236
|
+
for (const [ref] of sheet.cells) {
|
|
1237
|
+
const rc = refToRowColSafe(ref);
|
|
1238
|
+
if (rc && rc.row === r && rc.col >= startCol + width) toMove.push({ col: rc.col, ref });
|
|
1239
|
+
}
|
|
1240
|
+
toMove.sort((a, b) => a.col - b.col); // left-to-right
|
|
1241
|
+
for (const { ref, col } of toMove) {
|
|
1242
|
+
const cell = sheet.cells.get(ref)!;
|
|
1243
|
+
sheet.cells.delete(ref);
|
|
1244
|
+
sheet.cells.set(rowColToRef(r, col - width), cell);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// =============================================================================
|
|
1252
|
+
// Delete Cells (Shift Up)
|
|
1253
|
+
// =============================================================================
|
|
1254
|
+
|
|
1255
|
+
export function deleteCellsShiftUpCommand(
|
|
1256
|
+
workbook: WorkbookModel,
|
|
1257
|
+
sheetIndex: number,
|
|
1258
|
+
startRow: number,
|
|
1259
|
+
startCol: number,
|
|
1260
|
+
endRow: number,
|
|
1261
|
+
endCol: number,
|
|
1262
|
+
): Command {
|
|
1263
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
1264
|
+
const height = endRow - startRow + 1;
|
|
1265
|
+
|
|
1266
|
+
// Snapshot deleted cells
|
|
1267
|
+
const snapshot = new Map<string, { value: CellValue; displayValue: string; formula?: string; styleIndex: number; numFmtCode: string; content: CellContent }>();
|
|
1268
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
1269
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
1270
|
+
const ref = rowColToRef(r, c);
|
|
1271
|
+
const cell = sheet.getCell(ref);
|
|
1272
|
+
if (cell) {
|
|
1273
|
+
snapshot.set(ref, { value: cell.value, displayValue: cell.displayValue, formula: cell.formula, styleIndex: cell.styleIndex, numFmtCode: cell.numFmtCode, content: cell.content });
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
return {
|
|
1279
|
+
description: `Delete cells shift up ${rowColToRef(startRow, startCol)}:${rowColToRef(endRow, endCol)}`,
|
|
1280
|
+
execute() {
|
|
1281
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
1282
|
+
// Delete cells in the range
|
|
1283
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
1284
|
+
sheet.deleteCell(rowColToRef(r, c));
|
|
1285
|
+
}
|
|
1286
|
+
// Shift cells up
|
|
1287
|
+
const toMove: Array<{ row: number; ref: string }> = [];
|
|
1288
|
+
for (const [ref] of sheet.cells) {
|
|
1289
|
+
const rc = refToRowColSafe(ref);
|
|
1290
|
+
if (rc && rc.col === c && rc.row > endRow) toMove.push({ row: rc.row, ref });
|
|
1291
|
+
}
|
|
1292
|
+
toMove.sort((a, b) => a.row - b.row); // top-down
|
|
1293
|
+
for (const { ref, row } of toMove) {
|
|
1294
|
+
const cell = sheet.cells.get(ref)!;
|
|
1295
|
+
sheet.cells.delete(ref);
|
|
1296
|
+
sheet.cells.set(rowColToRef(row - height, c), cell);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
undo() {
|
|
1301
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
1302
|
+
// Shift cells back down
|
|
1303
|
+
const toMove: Array<{ row: number; ref: string }> = [];
|
|
1304
|
+
for (const [ref] of sheet.cells) {
|
|
1305
|
+
const rc = refToRowColSafe(ref);
|
|
1306
|
+
if (rc && rc.col === c && rc.row >= startRow) toMove.push({ row: rc.row, ref });
|
|
1307
|
+
}
|
|
1308
|
+
toMove.sort((a, b) => b.row - a.row); // bottom-up
|
|
1309
|
+
for (const { ref, row } of toMove) {
|
|
1310
|
+
const cell = sheet.cells.get(ref)!;
|
|
1311
|
+
sheet.cells.delete(ref);
|
|
1312
|
+
sheet.cells.set(rowColToRef(row + height, c), cell);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
// Restore snapshot
|
|
1316
|
+
for (const [ref, snap] of snapshot) {
|
|
1317
|
+
sheet.set(ref, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
|
|
1318
|
+
}
|
|
1319
|
+
},
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// =============================================================================
|
|
1324
|
+
// Delete Cells (Shift Left)
|
|
1325
|
+
// =============================================================================
|
|
1326
|
+
|
|
1327
|
+
export function deleteCellsShiftLeftCommand(
|
|
1328
|
+
workbook: WorkbookModel,
|
|
1329
|
+
sheetIndex: number,
|
|
1330
|
+
startRow: number,
|
|
1331
|
+
startCol: number,
|
|
1332
|
+
endRow: number,
|
|
1333
|
+
endCol: number,
|
|
1334
|
+
): Command {
|
|
1335
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
1336
|
+
const width = endCol - startCol + 1;
|
|
1337
|
+
|
|
1338
|
+
// Snapshot deleted cells
|
|
1339
|
+
const snapshot = new Map<string, { value: CellValue; displayValue: string; formula?: string; styleIndex: number; numFmtCode: string; content: CellContent }>();
|
|
1340
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
1341
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
1342
|
+
const ref = rowColToRef(r, c);
|
|
1343
|
+
const cell = sheet.getCell(ref);
|
|
1344
|
+
if (cell) {
|
|
1345
|
+
snapshot.set(ref, { value: cell.value, displayValue: cell.displayValue, formula: cell.formula, styleIndex: cell.styleIndex, numFmtCode: cell.numFmtCode, content: cell.content });
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
return {
|
|
1351
|
+
description: `Delete cells shift left ${rowColToRef(startRow, startCol)}:${rowColToRef(endRow, endCol)}`,
|
|
1352
|
+
execute() {
|
|
1353
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
1354
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
1355
|
+
sheet.deleteCell(rowColToRef(r, c));
|
|
1356
|
+
}
|
|
1357
|
+
const toMove: Array<{ col: number; ref: string }> = [];
|
|
1358
|
+
for (const [ref] of sheet.cells) {
|
|
1359
|
+
const rc = refToRowColSafe(ref);
|
|
1360
|
+
if (rc && rc.row === r && rc.col > endCol) toMove.push({ col: rc.col, ref });
|
|
1361
|
+
}
|
|
1362
|
+
toMove.sort((a, b) => a.col - b.col); // left-to-right
|
|
1363
|
+
for (const { ref, col } of toMove) {
|
|
1364
|
+
const cell = sheet.cells.get(ref)!;
|
|
1365
|
+
sheet.cells.delete(ref);
|
|
1366
|
+
sheet.cells.set(rowColToRef(r, col - width), cell);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
undo() {
|
|
1371
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
1372
|
+
const toMove: Array<{ col: number; ref: string }> = [];
|
|
1373
|
+
for (const [ref] of sheet.cells) {
|
|
1374
|
+
const rc = refToRowColSafe(ref);
|
|
1375
|
+
if (rc && rc.row === r && rc.col >= startCol) toMove.push({ col: rc.col, ref });
|
|
1376
|
+
}
|
|
1377
|
+
toMove.sort((a, b) => b.col - a.col); // right-to-left
|
|
1378
|
+
for (const { ref, col } of toMove) {
|
|
1379
|
+
const cell = sheet.cells.get(ref)!;
|
|
1380
|
+
sheet.cells.delete(ref);
|
|
1381
|
+
sheet.cells.set(rowColToRef(r, col + width), cell);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
for (const [ref, snap] of snapshot) {
|
|
1385
|
+
sheet.set(ref, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// =============================================================================
|
|
1392
|
+
// Set Show Grid Lines
|
|
1393
|
+
// =============================================================================
|
|
1394
|
+
|
|
1395
|
+
export function setShowGridLinesCommand(
|
|
1396
|
+
workbook: WorkbookModel,
|
|
1397
|
+
sheetIndex: number,
|
|
1398
|
+
show: boolean,
|
|
1399
|
+
): Command {
|
|
1400
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
1401
|
+
const oldValue = sheet.view.showGridLines;
|
|
1402
|
+
|
|
1403
|
+
return {
|
|
1404
|
+
description: show ? "Show grid lines" : "Hide grid lines",
|
|
1405
|
+
execute() {
|
|
1406
|
+
sheet.view.showGridLines = show;
|
|
1407
|
+
},
|
|
1408
|
+
undo() {
|
|
1409
|
+
sheet.view.showGridLines = oldValue;
|
|
1410
|
+
},
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// =============================================================================
|
|
1415
|
+
// Set Show Headers
|
|
1416
|
+
// =============================================================================
|
|
1417
|
+
|
|
1418
|
+
export function setShowHeadersCommand(
|
|
1419
|
+
workbook: WorkbookModel,
|
|
1420
|
+
sheetIndex: number,
|
|
1421
|
+
show: boolean,
|
|
1422
|
+
): Command {
|
|
1423
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
1424
|
+
const oldValue = sheet.view.showHeaders;
|
|
1425
|
+
|
|
1426
|
+
return {
|
|
1427
|
+
description: show ? "Show headers" : "Hide headers",
|
|
1428
|
+
execute() {
|
|
1429
|
+
sheet.view.showHeaders = show;
|
|
1430
|
+
},
|
|
1431
|
+
undo() {
|
|
1432
|
+
sheet.view.showHeaders = oldValue;
|
|
1433
|
+
},
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// =============================================================================
|
|
1438
|
+
// Set Hyperlink
|
|
1439
|
+
// =============================================================================
|
|
1440
|
+
|
|
1441
|
+
export function setHyperlinkCommand(
|
|
1442
|
+
workbook: WorkbookModel,
|
|
1443
|
+
sheetIndex: number,
|
|
1444
|
+
ref: string,
|
|
1445
|
+
url: string | undefined,
|
|
1446
|
+
): Command {
|
|
1447
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
1448
|
+
const oldUrl = sheet.hyperlinks.get(ref);
|
|
1449
|
+
const cell = sheet.getCell(ref);
|
|
1450
|
+
const oldContent = cell?.content;
|
|
1451
|
+
|
|
1452
|
+
return {
|
|
1453
|
+
description: url ? `Set hyperlink on ${ref}` : `Remove hyperlink from ${ref}`,
|
|
1454
|
+
execute() {
|
|
1455
|
+
if (url) {
|
|
1456
|
+
sheet.hyperlinks.set(ref, url);
|
|
1457
|
+
} else {
|
|
1458
|
+
sheet.hyperlinks.delete(ref);
|
|
1459
|
+
}
|
|
1460
|
+
// Rebuild content to reflect hyperlink change
|
|
1461
|
+
const c = sheet.getCell(ref);
|
|
1462
|
+
if (c) {
|
|
1463
|
+
const displayValue = c.displayValue;
|
|
1464
|
+
if (url) {
|
|
1465
|
+
c.content = { type: "hyperlink", text: displayValue, url };
|
|
1466
|
+
} else {
|
|
1467
|
+
c.content = { type: "plain", text: displayValue };
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
},
|
|
1471
|
+
undo() {
|
|
1472
|
+
if (oldUrl) {
|
|
1473
|
+
sheet.hyperlinks.set(ref, oldUrl);
|
|
1474
|
+
} else {
|
|
1475
|
+
sheet.hyperlinks.delete(ref);
|
|
1476
|
+
}
|
|
1477
|
+
const c = sheet.getCell(ref);
|
|
1478
|
+
if (c && oldContent) {
|
|
1479
|
+
c.content = oldContent;
|
|
1480
|
+
} else if (c) {
|
|
1481
|
+
c.content = { type: "plain", text: c.displayValue };
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// =============================================================================
|
|
1488
|
+
// Helpers
|
|
1489
|
+
// =============================================================================
|
|
1490
|
+
|
|
1491
|
+
/** Parse user input into a typed CellValue. */
|
|
1492
|
+
function parseInputValue(input: string): CellValue {
|
|
1493
|
+
if (input === "") return null;
|
|
1494
|
+
if (input.toUpperCase() === "TRUE") return true;
|
|
1495
|
+
if (input.toUpperCase() === "FALSE") return false;
|
|
1496
|
+
const num = Number(input);
|
|
1497
|
+
if (!isNaN(num) && input.trim() !== "") return num;
|
|
1498
|
+
return input;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function refToRowColSafe(ref: string): { row: number; col: number } | undefined {
|
|
1502
|
+
return refToRowCol(ref) ?? undefined;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/** Adjust all formula references in a sheet for a row insert/delete. */
|
|
1506
|
+
function adjustAllFormulasForRowShift(sheet: SheetModel, atRow: number, delta: number): void {
|
|
1507
|
+
for (const cell of sheet.cells.values()) {
|
|
1508
|
+
if (cell.formula) {
|
|
1509
|
+
cell.formula = adjustFormulaRefsForRowShift(cell.formula, atRow, delta);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
/** Adjust all formula references in a sheet for a column insert/delete. */
|
|
1515
|
+
function adjustAllFormulasForColShift(sheet: SheetModel, atCol: number, delta: number): void {
|
|
1516
|
+
for (const cell of sheet.cells.values()) {
|
|
1517
|
+
if (cell.formula) {
|
|
1518
|
+
cell.formula = adjustFormulaRefsForColShift(cell.formula, atCol, delta);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/** Sort sheet rows in-place by a column value. */
|
|
1524
|
+
function sortColumnInPlace(sheet: SheetModel, col: number, ascending: boolean): void {
|
|
1525
|
+
const used = sheet.getUsedRange();
|
|
1526
|
+
const rows: { row: number; val: number | string | null }[] = [];
|
|
1527
|
+
for (let r = used.minRow; r <= used.maxRow; r++) {
|
|
1528
|
+
const ref = rowColToRef(r, col);
|
|
1529
|
+
const cell = sheet.getCell(ref);
|
|
1530
|
+
const val = cell?.value ?? null;
|
|
1531
|
+
rows.push({ row: r, val: typeof val === "boolean" ? (val ? 1 : 0) : val });
|
|
1532
|
+
}
|
|
1533
|
+
rows.sort((a, b) => {
|
|
1534
|
+
if (a.val === null && b.val === null) return 0;
|
|
1535
|
+
if (a.val === null) return 1;
|
|
1536
|
+
if (b.val === null) return -1;
|
|
1537
|
+
if (typeof a.val === "number" && typeof b.val === "number") return ascending ? a.val - b.val : b.val - a.val;
|
|
1538
|
+
const sa = String(a.val).toLowerCase();
|
|
1539
|
+
const sb = String(b.val).toLowerCase();
|
|
1540
|
+
return ascending ? sa.localeCompare(sb) : sb.localeCompare(sa);
|
|
1541
|
+
});
|
|
1542
|
+
// Snapshot all rows, then write back in new order
|
|
1543
|
+
const allCells = new Map<string, Map<number, ReturnType<typeof sheet.getCell>>>();
|
|
1544
|
+
for (let r = used.minRow; r <= used.maxRow; r++) {
|
|
1545
|
+
const rowCells = new Map<number, ReturnType<typeof sheet.getCell>>();
|
|
1546
|
+
for (let c = used.minCol; c <= used.maxCol; c++) {
|
|
1547
|
+
const ref = rowColToRef(r, c);
|
|
1548
|
+
const cell = sheet.getCell(ref);
|
|
1549
|
+
if (cell) rowCells.set(c, { ...cell });
|
|
1550
|
+
sheet.deleteCell(ref);
|
|
1551
|
+
}
|
|
1552
|
+
allCells.set(String(r), rowCells);
|
|
1553
|
+
}
|
|
1554
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1555
|
+
const sourceRow = rows[i].row;
|
|
1556
|
+
const targetRow = used.minRow + i;
|
|
1557
|
+
const rowCells = allCells.get(String(sourceRow));
|
|
1558
|
+
if (!rowCells) continue;
|
|
1559
|
+
for (const [c, cell] of rowCells) {
|
|
1560
|
+
if (cell) {
|
|
1561
|
+
const ref = rowColToRef(targetRow, c);
|
|
1562
|
+
sheet.set(ref, cell.value, sheet.styles?.get(cell.styleIndex), cell.numFmtCode, cell.formula);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/** Sort sheet rows in-place by multiple column keys (cascading comparison). */
|
|
1569
|
+
function multiSortInPlace(sheet: SheetModel, sortSpecs: SortSpec[], startRow: number): void {
|
|
1570
|
+
const used = sheet.getUsedRange();
|
|
1571
|
+
|
|
1572
|
+
// Build row index with sort key values
|
|
1573
|
+
const rowEntries: { row: number; vals: (number | string | null)[] }[] = [];
|
|
1574
|
+
for (let r = startRow; r <= used.maxRow; r++) {
|
|
1575
|
+
const vals = sortSpecs.map((spec) => {
|
|
1576
|
+
const ref = rowColToRef(r, spec.col);
|
|
1577
|
+
const cell = sheet.getCell(ref);
|
|
1578
|
+
const val = cell?.value ?? null;
|
|
1579
|
+
return typeof val === "boolean" ? (val ? 1 : 0) : val;
|
|
1580
|
+
});
|
|
1581
|
+
rowEntries.push({ row: r, vals });
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Cascading comparison: try each sort key in order
|
|
1585
|
+
rowEntries.sort((a, b) => {
|
|
1586
|
+
for (let k = 0; k < sortSpecs.length; k++) {
|
|
1587
|
+
const aVal = a.vals[k];
|
|
1588
|
+
const bVal = b.vals[k];
|
|
1589
|
+
const asc = sortSpecs[k].ascending;
|
|
1590
|
+
if (aVal === null && bVal === null) continue;
|
|
1591
|
+
if (aVal === null) return 1;
|
|
1592
|
+
if (bVal === null) return -1;
|
|
1593
|
+
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
1594
|
+
const diff = asc ? aVal - bVal : bVal - aVal;
|
|
1595
|
+
if (diff !== 0) return diff;
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
const sa = String(aVal).toLowerCase();
|
|
1599
|
+
const sb = String(bVal).toLowerCase();
|
|
1600
|
+
const cmp = asc ? sa.localeCompare(sb) : sb.localeCompare(sa);
|
|
1601
|
+
if (cmp !== 0) return cmp;
|
|
1602
|
+
}
|
|
1603
|
+
return 0;
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
// Snapshot all rows, then write back in new order
|
|
1607
|
+
const allCells = new Map<string, Map<number, ReturnType<typeof sheet.getCell>>>();
|
|
1608
|
+
for (let r = startRow; r <= used.maxRow; r++) {
|
|
1609
|
+
const rowCells = new Map<number, ReturnType<typeof sheet.getCell>>();
|
|
1610
|
+
for (let c = used.minCol; c <= used.maxCol; c++) {
|
|
1611
|
+
const ref = rowColToRef(r, c);
|
|
1612
|
+
const cell = sheet.getCell(ref);
|
|
1613
|
+
if (cell) rowCells.set(c, { ...cell });
|
|
1614
|
+
sheet.deleteCell(ref);
|
|
1615
|
+
}
|
|
1616
|
+
allCells.set(String(r), rowCells);
|
|
1617
|
+
}
|
|
1618
|
+
for (let i = 0; i < rowEntries.length; i++) {
|
|
1619
|
+
const sourceRow = rowEntries[i].row;
|
|
1620
|
+
const targetRow = startRow + i;
|
|
1621
|
+
const rowCells = allCells.get(String(sourceRow));
|
|
1622
|
+
if (!rowCells) continue;
|
|
1623
|
+
for (const [c, cell] of rowCells) {
|
|
1624
|
+
if (cell) {
|
|
1625
|
+
const ref = rowColToRef(targetRow, c);
|
|
1626
|
+
sheet.set(ref, cell.value, sheet.styles?.get(cell.styleIndex), cell.numFmtCode, cell.formula);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// =============================================================================
|
|
1633
|
+
// Merged-cell range adjustment
|
|
1634
|
+
// =============================================================================
|
|
1635
|
+
//
|
|
1636
|
+
// `insertRowsCommand` and `deleteRowsCommand` now own merged-cell range
|
|
1637
|
+
// shifting and contraction. Previously this lived as separate functions
|
|
1638
|
+
// callers had to remember to invoke (e.g. excel_placeholder_engine).
|
|
1639
|
+
|
|
1640
|
+
/** Shift merged-cell ranges to accommodate a row insertion at `atRow`. */
|
|
1641
|
+
export function adjustMergedCellsForRowInsert(sheet: SheetModel, atRow: number, count: number): void {
|
|
1642
|
+
sheet.mergedCells = sheet.mergedCells.map((range) => {
|
|
1643
|
+
const [startRef, endRef] = range.split(":");
|
|
1644
|
+
const start = refToRowCol(startRef);
|
|
1645
|
+
const end = refToRowCol(endRef);
|
|
1646
|
+
if (!start || !end) return range;
|
|
1647
|
+
|
|
1648
|
+
let r1 = start.row;
|
|
1649
|
+
let r2 = end.row;
|
|
1650
|
+
|
|
1651
|
+
if (r1 >= atRow) {
|
|
1652
|
+
r1 += count;
|
|
1653
|
+
r2 += count;
|
|
1654
|
+
} else if (r2 >= atRow) {
|
|
1655
|
+
r2 += count;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
return `${rowColToRef(r1, start.col)}:${rowColToRef(r2, end.col)}`;
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* Adjust merged-cell ranges after a row deletion. Ranges fully inside the
|
|
1664
|
+
* deleted span are dropped, overlapping ranges are contracted, and ranges
|
|
1665
|
+
* entirely after the deletion shift up.
|
|
1666
|
+
*/
|
|
1667
|
+
export function adjustMergedCellsForRowDelete(sheet: SheetModel, atRow: number, count: number): void {
|
|
1668
|
+
const endRow = atRow + count - 1;
|
|
1669
|
+
const updated: string[] = [];
|
|
1670
|
+
for (const range of sheet.mergedCells) {
|
|
1671
|
+
const [startRef, endRef] = range.split(":");
|
|
1672
|
+
const start = refToRowCol(startRef);
|
|
1673
|
+
const end = refToRowCol(endRef);
|
|
1674
|
+
if (!start || !end) {
|
|
1675
|
+
updated.push(range);
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
let r1 = start.row;
|
|
1680
|
+
let r2 = end.row;
|
|
1681
|
+
|
|
1682
|
+
if (r1 > endRow) {
|
|
1683
|
+
r1 -= count;
|
|
1684
|
+
r2 -= count;
|
|
1685
|
+
} else if (r2 < atRow) {
|
|
1686
|
+
// unchanged
|
|
1687
|
+
} else if (r1 >= atRow && r2 <= endRow) {
|
|
1688
|
+
continue;
|
|
1689
|
+
} else if (r1 < atRow && r2 > endRow) {
|
|
1690
|
+
r2 -= count;
|
|
1691
|
+
} else if (r1 < atRow) {
|
|
1692
|
+
r2 = atRow - 1;
|
|
1693
|
+
} else {
|
|
1694
|
+
r1 = atRow;
|
|
1695
|
+
r2 -= count;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (r2 < r1) continue;
|
|
1699
|
+
updated.push(`${rowColToRef(r1, start.col)}:${rowColToRef(r2, end.col)}`);
|
|
1700
|
+
}
|
|
1701
|
+
sheet.mergedCells = updated;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/** Shift merged-cell ranges to accommodate a column insertion at `atCol`. */
|
|
1705
|
+
export function adjustMergedCellsForColInsert(sheet: SheetModel, atCol: number, count: number): void {
|
|
1706
|
+
sheet.mergedCells = sheet.mergedCells.map((range) => {
|
|
1707
|
+
const [startRef, endRef] = range.split(":");
|
|
1708
|
+
const start = refToRowCol(startRef);
|
|
1709
|
+
const end = refToRowCol(endRef);
|
|
1710
|
+
if (!start || !end) return range;
|
|
1711
|
+
|
|
1712
|
+
let c1 = start.col;
|
|
1713
|
+
let c2 = end.col;
|
|
1714
|
+
|
|
1715
|
+
if (c1 >= atCol) {
|
|
1716
|
+
c1 += count;
|
|
1717
|
+
c2 += count;
|
|
1718
|
+
} else if (c2 >= atCol) {
|
|
1719
|
+
c2 += count;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
return `${rowColToRef(start.row, c1)}:${rowColToRef(end.row, c2)}`;
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/** Adjust merged-cell ranges after a column deletion. Mirrors the row variant. */
|
|
1727
|
+
export function adjustMergedCellsForColDelete(sheet: SheetModel, atCol: number, count: number): void {
|
|
1728
|
+
const endCol = atCol + count - 1;
|
|
1729
|
+
const updated: string[] = [];
|
|
1730
|
+
for (const range of sheet.mergedCells) {
|
|
1731
|
+
const [startRef, endRef] = range.split(":");
|
|
1732
|
+
const start = refToRowCol(startRef);
|
|
1733
|
+
const end = refToRowCol(endRef);
|
|
1734
|
+
if (!start || !end) {
|
|
1735
|
+
updated.push(range);
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
let c1 = start.col;
|
|
1740
|
+
let c2 = end.col;
|
|
1741
|
+
|
|
1742
|
+
if (c1 > endCol) {
|
|
1743
|
+
c1 -= count;
|
|
1744
|
+
c2 -= count;
|
|
1745
|
+
} else if (c2 < atCol) {
|
|
1746
|
+
// unchanged
|
|
1747
|
+
} else if (c1 >= atCol && c2 <= endCol) {
|
|
1748
|
+
continue;
|
|
1749
|
+
} else if (c1 < atCol && c2 > endCol) {
|
|
1750
|
+
c2 -= count;
|
|
1751
|
+
} else if (c1 < atCol) {
|
|
1752
|
+
c2 = atCol - 1;
|
|
1753
|
+
} else {
|
|
1754
|
+
c1 = atCol;
|
|
1755
|
+
c2 -= count;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (c2 < c1) continue;
|
|
1759
|
+
updated.push(`${rowColToRef(start.row, c1)}:${rowColToRef(end.row, c2)}`);
|
|
1760
|
+
}
|
|
1761
|
+
sheet.mergedCells = updated;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// =============================================================================
|
|
1765
|
+
// Formula range expansion
|
|
1766
|
+
// =============================================================================
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* After inserting rows inside a region used by a range formula (e.g.
|
|
1770
|
+
* `SUM(C5:C5)` with template row 5 + 9 inserted rows → `SUM(C5:C14)`),
|
|
1771
|
+
* widen any range whose end exactly matches the template's last row.
|
|
1772
|
+
*
|
|
1773
|
+
* Used by the Excel template placeholder engine when expanding `{{#each}}`
|
|
1774
|
+
* blocks. Lives in the command layer so future callers don't have to
|
|
1775
|
+
* reimplement it.
|
|
1776
|
+
*/
|
|
1777
|
+
export function expandFormulaRangesForRowInsert(
|
|
1778
|
+
sheet: SheetModel,
|
|
1779
|
+
templateStartRow: number,
|
|
1780
|
+
templateEndRow: number,
|
|
1781
|
+
insertedCount: number,
|
|
1782
|
+
): void {
|
|
1783
|
+
const rangeRe = /(\$?)([A-Z]+)(\$?)(\d+):(\$?)([A-Z]+)(\$?)(\d+)/g;
|
|
1784
|
+
|
|
1785
|
+
for (const [ref, cell] of sheet.cells) {
|
|
1786
|
+
if (!cell.formula) continue;
|
|
1787
|
+
|
|
1788
|
+
const newFormula = cell.formula.replace(
|
|
1789
|
+
rangeRe,
|
|
1790
|
+
(match, ca1, c1, ra1, r1s, ca2, c2, ra2, r2s) => {
|
|
1791
|
+
const r1 = parseInt(r1s, 10);
|
|
1792
|
+
const r2 = parseInt(r2s, 10);
|
|
1793
|
+
|
|
1794
|
+
if (r1 <= templateStartRow && r2 === templateEndRow) {
|
|
1795
|
+
return `${ca1}${c1}${ra1}${r1}:${ca2}${c2}${ra2}${r2 + insertedCount}`;
|
|
1796
|
+
}
|
|
1797
|
+
return match;
|
|
1798
|
+
},
|
|
1799
|
+
);
|
|
1800
|
+
|
|
1801
|
+
if (newFormula !== cell.formula) {
|
|
1802
|
+
sheet.set(ref, cell.value, sheet.styles?.get(cell.styleIndex), cell.numFmtCode, newFormula);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|