@pagent-libs/core 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/README.md +32 -0
- package/dist/canvas/cell-renderer.d.ts +45 -0
- package/dist/canvas/cell-renderer.d.ts.map +1 -0
- package/dist/canvas/grid-renderer.d.ts +29 -0
- package/dist/canvas/grid-renderer.d.ts.map +1 -0
- package/dist/canvas/header-renderer.d.ts +58 -0
- package/dist/canvas/header-renderer.d.ts.map +1 -0
- package/dist/canvas/hit-testing.d.ts +81 -0
- package/dist/canvas/hit-testing.d.ts.map +1 -0
- package/dist/canvas/index.d.ts +9 -0
- package/dist/canvas/index.d.ts.map +1 -0
- package/dist/canvas/renderer.d.ts +140 -0
- package/dist/canvas/renderer.d.ts.map +1 -0
- package/dist/canvas/selection-renderer.d.ts +55 -0
- package/dist/canvas/selection-renderer.d.ts.map +1 -0
- package/dist/canvas/text-renderer.d.ts +49 -0
- package/dist/canvas/text-renderer.d.ts.map +1 -0
- package/dist/canvas/types.d.ts +200 -0
- package/dist/canvas/types.d.ts.map +1 -0
- package/dist/collaboration/firebase-provider.d.ts +13 -0
- package/dist/collaboration/firebase-provider.d.ts.map +1 -0
- package/dist/collaboration/index.d.ts +3 -0
- package/dist/collaboration/index.d.ts.map +1 -0
- package/dist/collaboration/types.d.ts +34 -0
- package/dist/collaboration/types.d.ts.map +1 -0
- package/dist/event-emitter.d.ts +13 -0
- package/dist/event-emitter.d.ts.map +1 -0
- package/dist/export/csv.d.ts +5 -0
- package/dist/export/csv.d.ts.map +1 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/features/filter.d.ts +58 -0
- package/dist/features/filter.d.ts.map +1 -0
- package/dist/features/freeze.d.ts +86 -0
- package/dist/features/freeze.d.ts.map +1 -0
- package/dist/features/index.d.ts +4 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/sort.d.ts +15 -0
- package/dist/features/sort.d.ts.map +1 -0
- package/dist/format-pool.d.ts +17 -0
- package/dist/format-pool.d.ts.map +1 -0
- package/dist/formula-graph.d.ts +12 -0
- package/dist/formula-graph.d.ts.map +1 -0
- package/dist/formula-parser/cell-reference.d.ts +7 -0
- package/dist/formula-parser/cell-reference.d.ts.map +1 -0
- package/dist/formula-parser/formula-adjust.d.ts +13 -0
- package/dist/formula-parser/formula-adjust.d.ts.map +1 -0
- package/dist/formula-parser/formula-ranges.d.ts +22 -0
- package/dist/formula-parser/formula-ranges.d.ts.map +1 -0
- package/dist/formula-parser/index.d.ts +6 -0
- package/dist/formula-parser/index.d.ts.map +1 -0
- package/dist/formula-parser/parser.d.ts +18 -0
- package/dist/formula-parser/parser.d.ts.map +1 -0
- package/dist/formula-parser/types.d.ts +33 -0
- package/dist/formula-parser/types.d.ts.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +5823 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +5885 -0
- package/dist/index.js.map +1 -0
- package/dist/sheet.d.ts +119 -0
- package/dist/sheet.d.ts.map +1 -0
- package/dist/style-pool.d.ts +17 -0
- package/dist/style-pool.d.ts.map +1 -0
- package/dist/types.d.ts +260 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/cell-key.d.ts +7 -0
- package/dist/utils/cell-key.d.ts.map +1 -0
- package/dist/utils/format-utils.d.ts +75 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/range.d.ts +13 -0
- package/dist/utils/range.d.ts.map +1 -0
- package/dist/workbook.d.ts +155 -0
- package/dist/workbook.d.ts.map +1 -0
- package/package.json +46 -0
- package/src/canvas/cell-renderer.ts +181 -0
- package/src/canvas/grid-renderer.ts +238 -0
- package/src/canvas/header-renderer.ts +402 -0
- package/src/canvas/hit-testing.ts +537 -0
- package/src/canvas/index.ts +16 -0
- package/src/canvas/renderer.ts +1056 -0
- package/src/canvas/selection-renderer.ts +604 -0
- package/src/canvas/text-renderer.ts +321 -0
- package/src/canvas/types.ts +289 -0
- package/src/collaboration/firebase-provider.ts +48 -0
- package/src/collaboration/index.ts +5 -0
- package/src/collaboration/types.ts +38 -0
- package/src/event-emitter.ts +73 -0
- package/src/export/csv.ts +101 -0
- package/src/export/index.ts +4 -0
- package/src/features/filter.ts +231 -0
- package/src/features/freeze.ts +271 -0
- package/src/features/index.ts +5 -0
- package/src/features/sort.ts +282 -0
- package/src/format-pool.ts +61 -0
- package/src/formula-graph.ts +84 -0
- package/src/formula-parser/cell-reference.ts +99 -0
- package/src/formula-parser/formula-adjust.ts +129 -0
- package/src/formula-parser/formula-ranges.ts +159 -0
- package/src/formula-parser/index.ts +8 -0
- package/src/formula-parser/parser.ts +438 -0
- package/src/formula-parser/types.ts +39 -0
- package/src/index.ts +25 -0
- package/src/sheet.ts +502 -0
- package/src/style-pool.ts +62 -0
- package/src/types.ts +291 -0
- package/src/utils/cell-key.ts +19 -0
- package/src/utils/format-utils.ts +515 -0
- package/src/utils/range.ts +53 -0
- package/src/workbook.ts +1031 -0
package/src/workbook.ts
ADDED
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
// Workbook model
|
|
2
|
+
|
|
3
|
+
import type { Workbook, Sheet, Cell, Selection, CellValue, SortOrder, CellFormat, CellStyle, EventType, EventHandler } from './types';
|
|
4
|
+
import { SheetImpl } from './sheet';
|
|
5
|
+
import { EventEmitter } from './event-emitter';
|
|
6
|
+
import { FormulaGraphImpl } from './formula-graph';
|
|
7
|
+
import { StylePool } from './style-pool';
|
|
8
|
+
import { FormatPool } from './format-pool';
|
|
9
|
+
import { SortManager } from './features/sort';
|
|
10
|
+
import { getCellKey, parseCellKey } from './utils/cell-key';
|
|
11
|
+
import { FormulaParser } from './formula-parser';
|
|
12
|
+
import type { RangeReference } from './formula-parser';
|
|
13
|
+
|
|
14
|
+
// Snapshot for undo/redo
|
|
15
|
+
interface WorkbookSnapshot {
|
|
16
|
+
sheets: Map<string, SheetSnapshot>;
|
|
17
|
+
activeSheetId: string;
|
|
18
|
+
selection: Selection;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SheetSnapshot {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
cells: Map<string, Cell>;
|
|
25
|
+
config: Sheet['config'];
|
|
26
|
+
rowCount: number;
|
|
27
|
+
colCount: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class WorkbookImpl implements Workbook {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
sheets: Map<string, Sheet> = new Map();
|
|
34
|
+
activeSheetId: string;
|
|
35
|
+
defaultRowHeight: number;
|
|
36
|
+
defaultColWidth: number;
|
|
37
|
+
|
|
38
|
+
private events: EventEmitter = new EventEmitter();
|
|
39
|
+
private formulaGraph: FormulaGraphImpl = new FormulaGraphImpl();
|
|
40
|
+
private stylePool: StylePool = new StylePool();
|
|
41
|
+
private formatPool: FormatPool = new FormatPool();
|
|
42
|
+
private formulaParser: FormulaParser = new FormulaParser();
|
|
43
|
+
private selection: Selection = {
|
|
44
|
+
ranges: [],
|
|
45
|
+
activeCell: { row: 0, col: 0 },
|
|
46
|
+
};
|
|
47
|
+
private evaluatingCells: Set<string> = new Set(); // Track cells being evaluated to detect circular references
|
|
48
|
+
private undoStack: WorkbookSnapshot[] = [];
|
|
49
|
+
private redoStack: WorkbookSnapshot[] = [];
|
|
50
|
+
private maxHistorySize = 50; // Limit history size to prevent memory issues
|
|
51
|
+
private isUndoing = false; // Flag to prevent recording history during undo/redo
|
|
52
|
+
private isRedoing = false;
|
|
53
|
+
private isBatching = false; // Flag to track if we're in a batch operation
|
|
54
|
+
|
|
55
|
+
constructor(id: string, name: string) {
|
|
56
|
+
this.id = id;
|
|
57
|
+
this.name = name;
|
|
58
|
+
this.defaultRowHeight = 20;
|
|
59
|
+
this.defaultColWidth = 100;
|
|
60
|
+
|
|
61
|
+
// Create default sheet
|
|
62
|
+
const defaultSheet = this.addSheet('Sheet1');
|
|
63
|
+
this.activeSheetId = defaultSheet.id;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
addSheet(name: string): Sheet {
|
|
67
|
+
const id = `sheet_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
68
|
+
const sheet = new SheetImpl(id, name);
|
|
69
|
+
this.sheets.set(id, sheet);
|
|
70
|
+
this.events.emit('sheetAdd', { sheetId: id, name });
|
|
71
|
+
return sheet;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
deleteSheet(sheetId: string): void {
|
|
75
|
+
if (this.sheets.size <= 1) {
|
|
76
|
+
throw new Error('Cannot delete the last sheet');
|
|
77
|
+
}
|
|
78
|
+
this.sheets.delete(sheetId);
|
|
79
|
+
if (this.activeSheetId === sheetId) {
|
|
80
|
+
// Switch to first available sheet
|
|
81
|
+
this.activeSheetId = Array.from(this.sheets.keys())[0];
|
|
82
|
+
}
|
|
83
|
+
this.events.emit('sheetDelete', { sheetId });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getSheet(sheetId?: string): SheetImpl {
|
|
87
|
+
const id = sheetId ?? this.activeSheetId;
|
|
88
|
+
const sheet = this.sheets.get(id);
|
|
89
|
+
if (!sheet) {
|
|
90
|
+
throw new Error(`Sheet not found: ${id}`);
|
|
91
|
+
}
|
|
92
|
+
return sheet as SheetImpl;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setActiveSheet(sheetId: string): void {
|
|
96
|
+
if (!this.sheets.has(sheetId)) {
|
|
97
|
+
throw new Error(`Sheet not found: ${sheetId}`);
|
|
98
|
+
}
|
|
99
|
+
this.activeSheetId = sheetId;
|
|
100
|
+
this.events.emit('sheetChange', { sheetId });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
renameSheet(sheetId: string, newName: string): void {
|
|
104
|
+
const sheet = this.getSheet(sheetId);
|
|
105
|
+
const oldName = sheet.name;
|
|
106
|
+
sheet.name = newName;
|
|
107
|
+
this.events.emit('sheetRename', { sheetId, oldName, newName });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getSheetIdByName(sheetName: string): string | undefined {
|
|
111
|
+
for (const [id, sheet] of this.sheets.entries()) {
|
|
112
|
+
if (sheet.name === sheetName) {
|
|
113
|
+
return id;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getCell(sheetId: string | undefined, row: number, col: number): Cell | undefined {
|
|
120
|
+
const sheet = this.getSheet(sheetId);
|
|
121
|
+
return sheet.getCell(row, col);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
setCell(
|
|
125
|
+
sheetId: string | undefined,
|
|
126
|
+
row: number,
|
|
127
|
+
col: number,
|
|
128
|
+
cell: Partial<Cell>
|
|
129
|
+
): void {
|
|
130
|
+
// Handle style pooling before storing
|
|
131
|
+
if (cell.styleId) {
|
|
132
|
+
// Style already pooled
|
|
133
|
+
} else if (cell.styleId === undefined && 'style' in cell) {
|
|
134
|
+
// Need to pool the style
|
|
135
|
+
const styleId = this.stylePool.getOrCreate(cell.style as CellStyle);
|
|
136
|
+
cell.styleId = styleId;
|
|
137
|
+
delete (cell as Partial<Cell> & { style?: CellStyle }).style;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle format pooling before storing
|
|
141
|
+
if (cell.formatId) {
|
|
142
|
+
// Format already pooled
|
|
143
|
+
} else if (cell.formatId === undefined && 'format' in cell) {
|
|
144
|
+
// Clean the format before pooling to ensure consistent keys
|
|
145
|
+
const cleanedFormat = this.cleanFormat(cell.format as CellFormat);
|
|
146
|
+
const formatId = this.formatPool.getOrCreate(cleanedFormat);
|
|
147
|
+
cell.formatId = formatId;
|
|
148
|
+
delete (cell as Partial<Cell> & { format?: CellFormat }).format;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const sheet = this.getSheet(sheetId);
|
|
152
|
+
sheet.setCell(row, col, cell);
|
|
153
|
+
|
|
154
|
+
this.events.emit('cellChange', {
|
|
155
|
+
sheetId: sheet.id,
|
|
156
|
+
row,
|
|
157
|
+
col,
|
|
158
|
+
cellKey: getCellKey(row, col),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setCellValue(sheetId: string | undefined, row: number, col: number, value: unknown): void {
|
|
163
|
+
// Record history before making changes (unless we're undoing/redoing or in a batch)
|
|
164
|
+
if (!this.isUndoing && !this.isRedoing && !this.isBatching) {
|
|
165
|
+
const currentCell = this.getCell(sheetId, row, col);
|
|
166
|
+
const currentValue = currentCell?.value;
|
|
167
|
+
if (currentValue !== value) {
|
|
168
|
+
this.recordHistory();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const cellKey = getCellKey(row, col);
|
|
173
|
+
const hadFormula = this.formulaGraph.nodes.has(cellKey);
|
|
174
|
+
|
|
175
|
+
// Remove formula if setting a direct value
|
|
176
|
+
if (hadFormula) {
|
|
177
|
+
this.formulaGraph.removeFormula(cellKey);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.setCell(sheetId, row, col, { value: value as string | number | boolean | null });
|
|
181
|
+
|
|
182
|
+
// Invalidate dependents of this cell
|
|
183
|
+
if (hadFormula || this.getCell(sheetId, row, col)?.value !== undefined) {
|
|
184
|
+
this.formulaGraph.invalidate(cellKey);
|
|
185
|
+
this.recalculateDependents(cellKey, sheetId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
getCellValue(sheetId: string | undefined, row: number, col: number): unknown {
|
|
190
|
+
const cell = this.getCell(sheetId, row, col);
|
|
191
|
+
return cell?.value ?? null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
setFormula(sheetId: string | undefined, row: number, col: number, formula: string): void {
|
|
195
|
+
// Record history before making changes (unless we're undoing/redoing or in a batch)
|
|
196
|
+
if (!this.isUndoing && !this.isRedoing && !this.isBatching) {
|
|
197
|
+
const currentCell = this.getCell(sheetId, row, col);
|
|
198
|
+
const currentFormula = currentCell?.formula;
|
|
199
|
+
if (currentFormula !== formula) {
|
|
200
|
+
this.recordHistory();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const cellKey = getCellKey(row, col);
|
|
205
|
+
|
|
206
|
+
// Parse formula to get dependencies
|
|
207
|
+
const parseResult = this.formulaParser.parse(formula, row, col);
|
|
208
|
+
|
|
209
|
+
if (parseResult.error) {
|
|
210
|
+
// Store error in cell value
|
|
211
|
+
this.setCell(sheetId, row, col, { formula, value: parseResult.error as CellValue });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Update formula graph with dependencies
|
|
216
|
+
this.formulaGraph.addFormula(cellKey, formula, parseResult.dependencies);
|
|
217
|
+
|
|
218
|
+
// Set cell with formula
|
|
219
|
+
this.setCell(sheetId, row, col, { formula });
|
|
220
|
+
|
|
221
|
+
// Evaluate and store result
|
|
222
|
+
this.evaluateFormula(sheetId, row, col);
|
|
223
|
+
|
|
224
|
+
// Recalculate dependents
|
|
225
|
+
this.recalculateDependents(cellKey, sheetId);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
getSelection(): Selection {
|
|
229
|
+
return { ...this.selection };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
setSelection(selection: Selection): void {
|
|
233
|
+
this.selection = selection;
|
|
234
|
+
this.events.emit('cellSelection', { selection });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
batch(operations: () => void): void {
|
|
238
|
+
// Record history before batch operations (unless we're undoing/redoing)
|
|
239
|
+
if (!this.isUndoing && !this.isRedoing) {
|
|
240
|
+
this.isBatching = true;
|
|
241
|
+
this.recordHistory();
|
|
242
|
+
}
|
|
243
|
+
this.events.batch(operations);
|
|
244
|
+
this.isBatching = false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
on(event: EventType, handler: EventHandler): () => void {
|
|
248
|
+
return this.events.on(event, handler);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
off(event: EventType, handler: EventHandler): void {
|
|
252
|
+
this.events.off(event, handler);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getFormulaGraph(): FormulaGraphImpl {
|
|
256
|
+
return this.formulaGraph;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
getStylePool(): StylePool {
|
|
260
|
+
return this.stylePool;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
getFormatPool(): FormatPool {
|
|
264
|
+
return this.formatPool;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get the calculated value of a cell (evaluates formulas)
|
|
269
|
+
*/
|
|
270
|
+
getCellCalculatedValue(sheetId: string | undefined, row: number, col: number): unknown {
|
|
271
|
+
const cell = this.getCell(sheetId, row, col);
|
|
272
|
+
const cellKey = getCellKey(row, col);
|
|
273
|
+
|
|
274
|
+
// If cell has a formula, evaluate it
|
|
275
|
+
if (cell?.formula) {
|
|
276
|
+
const formulaNode = this.formulaGraph.nodes.get(cellKey);
|
|
277
|
+
|
|
278
|
+
// Return cached value if available and not dirty
|
|
279
|
+
if (formulaNode && !formulaNode.isDirty && formulaNode.cachedValue !== undefined) {
|
|
280
|
+
return formulaNode.cachedValue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check for circular reference BEFORE evaluating
|
|
284
|
+
if (this.evaluatingCells.has(cellKey)) {
|
|
285
|
+
// This cell is already being evaluated - circular reference!
|
|
286
|
+
return '#CIRCULAR!';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Evaluate formula
|
|
290
|
+
return this.evaluateFormula(sheetId, row, col);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Return direct value
|
|
294
|
+
// For empty cells, return 0 (standard spreadsheet behavior for numeric formulas)
|
|
295
|
+
// For cells with values, return the actual value (string, number, boolean, etc.)
|
|
296
|
+
if (!cell) return 0;
|
|
297
|
+
|
|
298
|
+
// Return the actual value - don't convert null/undefined to 0 here
|
|
299
|
+
// The formula parser will handle type conversion as needed
|
|
300
|
+
if (cell.value === null || cell.value === undefined) {
|
|
301
|
+
return 0; // Empty cells return 0 for numeric calculations
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return cell.value;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Evaluate a formula and store the result
|
|
309
|
+
*/
|
|
310
|
+
private evaluateFormula(sheetId: string | undefined, row: number, col: number): unknown {
|
|
311
|
+
const cellKey = getCellKey(row, col);
|
|
312
|
+
const cell = this.getCell(sheetId, row, col);
|
|
313
|
+
|
|
314
|
+
if (!cell?.formula) {
|
|
315
|
+
return cell?.value ?? null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check for circular reference BEFORE adding to evaluatingCells
|
|
319
|
+
if (this.evaluatingCells.has(cellKey)) {
|
|
320
|
+
const error = '#CIRCULAR!';
|
|
321
|
+
this.formulaGraph.markClean(cellKey, error);
|
|
322
|
+
this.setCell(sheetId, row, col, { value: error });
|
|
323
|
+
return error;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Add to evaluating cells to detect circular references
|
|
327
|
+
this.evaluatingCells.add(cellKey);
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
// Parse formula (parser resolves relative references during parsing)
|
|
331
|
+
const parseResult = this.formulaParser.parse(cell.formula, row, col);
|
|
332
|
+
|
|
333
|
+
if (parseResult.error) {
|
|
334
|
+
this.evaluatingCells.delete(cellKey);
|
|
335
|
+
this.formulaGraph.markClean(cellKey, parseResult.error as CellValue);
|
|
336
|
+
this.setCell(sheetId, row, col, { value: parseResult.error as CellValue });
|
|
337
|
+
return parseResult.error;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Create evaluation context with current formula cell position captured in closure
|
|
341
|
+
const evaluationContext = {
|
|
342
|
+
getCellValue: (r: number, c: number, sId?: string, sheetName?: string) => {
|
|
343
|
+
// Resolve sheet name to sheet ID if provided
|
|
344
|
+
let targetSheetId = sId ?? sheetId;
|
|
345
|
+
if (sheetName) {
|
|
346
|
+
const resolvedSheetId = this.getSheetIdByName(sheetName);
|
|
347
|
+
if (resolvedSheetId) {
|
|
348
|
+
targetSheetId = resolvedSheetId;
|
|
349
|
+
} else {
|
|
350
|
+
// Sheet not found - return error
|
|
351
|
+
return '#REF!';
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return this.getCellCalculatedValue(targetSheetId, r, c);
|
|
355
|
+
},
|
|
356
|
+
getRangeValues: (range: RangeReference, sId?: string, sheetName?: string) => {
|
|
357
|
+
// Resolve sheet name to sheet ID if provided
|
|
358
|
+
let targetSheetId = sId ?? sheetId;
|
|
359
|
+
if (sheetName) {
|
|
360
|
+
const resolvedSheetId = this.getSheetIdByName(sheetName);
|
|
361
|
+
if (resolvedSheetId) {
|
|
362
|
+
targetSheetId = resolvedSheetId;
|
|
363
|
+
} else {
|
|
364
|
+
// Sheet not found - return empty array
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Use captured row/col from the formula cell being evaluated
|
|
369
|
+
return this.getRangeValues(targetSheetId, range, row, col);
|
|
370
|
+
},
|
|
371
|
+
getSheetIdByName: (sheetName: string) => {
|
|
372
|
+
return this.getSheetIdByName(sheetName);
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// Evaluate formula
|
|
377
|
+
const result = this.formulaParser.evaluate(parseResult.ast, evaluationContext, row, col);
|
|
378
|
+
|
|
379
|
+
// Handle division by zero
|
|
380
|
+
if (typeof result === 'number' && !isFinite(result)) {
|
|
381
|
+
const error = result === Infinity || result === -Infinity ? '#DIV/0!' : '#NUM!';
|
|
382
|
+
this.evaluatingCells.delete(cellKey);
|
|
383
|
+
this.formulaGraph.markClean(cellKey, error);
|
|
384
|
+
this.setCell(sheetId, row, col, { value: error });
|
|
385
|
+
return error;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Store result
|
|
389
|
+
const cellValue = result as CellValue;
|
|
390
|
+
this.evaluatingCells.delete(cellKey);
|
|
391
|
+
this.formulaGraph.markClean(cellKey, cellValue);
|
|
392
|
+
|
|
393
|
+
// Update cell value (but keep formula)
|
|
394
|
+
const currentCell = this.getCell(sheetId, row, col);
|
|
395
|
+
if (currentCell) {
|
|
396
|
+
this.setCell(sheetId, row, col, { ...currentCell, value: cellValue });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return result;
|
|
400
|
+
} catch (error) {
|
|
401
|
+
this.evaluatingCells.delete(cellKey);
|
|
402
|
+
const errorMsg = error instanceof Error ? error.message : '#ERROR!';
|
|
403
|
+
this.formulaGraph.markClean(cellKey, errorMsg as CellValue);
|
|
404
|
+
this.setCell(sheetId, row, col, { value: errorMsg as CellValue });
|
|
405
|
+
return errorMsg;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get values from a range for formula evaluation
|
|
411
|
+
* Note: Range references are already resolved to absolute positions during parsing,
|
|
412
|
+
* but we keep the currentRow/currentCol parameters for consistency with the API
|
|
413
|
+
*/
|
|
414
|
+
private getRangeValues(
|
|
415
|
+
sheetId: string | undefined,
|
|
416
|
+
range: RangeReference,
|
|
417
|
+
currentRow: number,
|
|
418
|
+
currentCol: number
|
|
419
|
+
): unknown[][] {
|
|
420
|
+
const { start, end } = range;
|
|
421
|
+
|
|
422
|
+
// Resolve relative references to absolute positions
|
|
423
|
+
const startRow = start.rowAbsolute ? start.row : currentRow + start.row;
|
|
424
|
+
const endRow = end.rowAbsolute ? end.row : currentRow + end.row;
|
|
425
|
+
const startCol = start.colAbsolute ? start.col : currentCol + start.col;
|
|
426
|
+
const endCol = end.colAbsolute ? end.col : currentCol + end.col;
|
|
427
|
+
|
|
428
|
+
const minRow = Math.min(startRow, endRow);
|
|
429
|
+
const maxRow = Math.max(startRow, endRow);
|
|
430
|
+
const minCol = Math.min(startCol, endCol);
|
|
431
|
+
const maxCol = Math.max(startCol, endCol);
|
|
432
|
+
|
|
433
|
+
const values: unknown[][] = [];
|
|
434
|
+
for (let row = minRow; row <= maxRow; row++) {
|
|
435
|
+
const rowValues: unknown[] = [];
|
|
436
|
+
for (let col = minCol; col <= maxCol; col++) {
|
|
437
|
+
rowValues.push(this.getCellCalculatedValue(sheetId, row, col));
|
|
438
|
+
}
|
|
439
|
+
values.push(rowValues);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return values;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Recalculate all cells that depend on the given cell
|
|
447
|
+
*/
|
|
448
|
+
private recalculateDependents(cellKey: string, sheetId: string | undefined): void {
|
|
449
|
+
const dependents = this.formulaGraph.getDependents(cellKey);
|
|
450
|
+
|
|
451
|
+
for (const dependentKey of dependents) {
|
|
452
|
+
// Invalidate dependent
|
|
453
|
+
this.formulaGraph.invalidate(dependentKey);
|
|
454
|
+
|
|
455
|
+
// Recalculate dependent
|
|
456
|
+
const { row, col } = parseCellKey(dependentKey);
|
|
457
|
+
this.evaluateFormula(sheetId, row, col);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Create a snapshot of the current workbook state
|
|
463
|
+
*/
|
|
464
|
+
private createSnapshot(): WorkbookSnapshot {
|
|
465
|
+
const sheets = new Map<string, SheetSnapshot>();
|
|
466
|
+
|
|
467
|
+
for (const [sheetId, sheet] of this.sheets.entries()) {
|
|
468
|
+
// Deep clone cells
|
|
469
|
+
const cells = new Map<string, Cell>();
|
|
470
|
+
for (const [key, cell] of sheet.cells.entries()) {
|
|
471
|
+
cells.set(key, { ...cell });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Deep clone config
|
|
475
|
+
const config = {
|
|
476
|
+
...sheet.config,
|
|
477
|
+
rowHeights: sheet.config.rowHeights ? new Map(sheet.config.rowHeights) : undefined,
|
|
478
|
+
colWidths: sheet.config.colWidths ? new Map(sheet.config.colWidths) : undefined,
|
|
479
|
+
hiddenRows: sheet.config.hiddenRows ? new Set(sheet.config.hiddenRows) : undefined,
|
|
480
|
+
hiddenCols: sheet.config.hiddenCols ? new Set(sheet.config.hiddenCols) : undefined,
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
sheets.set(sheetId, {
|
|
484
|
+
id: sheet.id,
|
|
485
|
+
name: sheet.name,
|
|
486
|
+
cells,
|
|
487
|
+
config,
|
|
488
|
+
rowCount: sheet.rowCount,
|
|
489
|
+
colCount: sheet.colCount,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
sheets,
|
|
495
|
+
activeSheetId: this.activeSheetId,
|
|
496
|
+
selection: { ...this.selection },
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Restore workbook state from a snapshot
|
|
502
|
+
*/
|
|
503
|
+
private restoreSnapshot(snapshot: WorkbookSnapshot): void {
|
|
504
|
+
// Clear existing sheets
|
|
505
|
+
this.sheets.clear();
|
|
506
|
+
|
|
507
|
+
// Restore sheets
|
|
508
|
+
for (const [sheetId, sheetSnapshot] of snapshot.sheets.entries()) {
|
|
509
|
+
const sheet = new SheetImpl(sheetSnapshot.id, sheetSnapshot.name, sheetSnapshot.config);
|
|
510
|
+
sheet.rowCount = sheetSnapshot.rowCount;
|
|
511
|
+
sheet.colCount = sheetSnapshot.colCount;
|
|
512
|
+
|
|
513
|
+
// Restore cells
|
|
514
|
+
for (const [key, cell] of sheetSnapshot.cells.entries()) {
|
|
515
|
+
sheet.cells.set(key, { ...cell });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
this.sheets.set(sheetId, sheet);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Restore active sheet
|
|
522
|
+
this.activeSheetId = snapshot.activeSheetId;
|
|
523
|
+
|
|
524
|
+
// Restore selection
|
|
525
|
+
this.selection = { ...snapshot.selection };
|
|
526
|
+
|
|
527
|
+
// Rebuild formula graph
|
|
528
|
+
this.formulaGraph = new FormulaGraphImpl();
|
|
529
|
+
for (const [, sheet] of this.sheets.entries()) {
|
|
530
|
+
for (const [key, cell] of sheet.cells.entries()) {
|
|
531
|
+
if (cell.formula) {
|
|
532
|
+
const { row, col } = parseCellKey(key);
|
|
533
|
+
const parseResult = this.formulaParser.parse(cell.formula, row, col);
|
|
534
|
+
if (!parseResult.error) {
|
|
535
|
+
this.formulaGraph.addFormula(key, cell.formula, parseResult.dependencies);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Recalculate all formulas
|
|
542
|
+
for (const [sheetId, sheet] of this.sheets.entries()) {
|
|
543
|
+
for (const [key, cell] of sheet.cells.entries()) {
|
|
544
|
+
if (cell.formula) {
|
|
545
|
+
const { row, col } = parseCellKey(key);
|
|
546
|
+
this.evaluateFormula(sheetId, row, col);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Emit change events
|
|
552
|
+
this.events.emit('workbookChange', {});
|
|
553
|
+
this.events.emit('sheetChange', { sheetId: this.activeSheetId });
|
|
554
|
+
this.events.emit('cellSelection', { selection: this.selection });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Record current state for undo
|
|
559
|
+
*/
|
|
560
|
+
recordHistory(): void {
|
|
561
|
+
if (this.isUndoing || this.isRedoing) {
|
|
562
|
+
return; // Don't record history during undo/redo operations
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const snapshot = this.createSnapshot();
|
|
566
|
+
this.undoStack.push(snapshot);
|
|
567
|
+
|
|
568
|
+
// Limit history size
|
|
569
|
+
if (this.undoStack.length > this.maxHistorySize) {
|
|
570
|
+
this.undoStack.shift();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Clear redo stack when new action is performed
|
|
574
|
+
this.redoStack = [];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Undo the last operation
|
|
579
|
+
*/
|
|
580
|
+
undo(): boolean {
|
|
581
|
+
if (this.undoStack.length === 0) {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Save current state to redo stack
|
|
586
|
+
const currentSnapshot = this.createSnapshot();
|
|
587
|
+
this.redoStack.push(currentSnapshot);
|
|
588
|
+
|
|
589
|
+
// Restore previous state
|
|
590
|
+
const previousSnapshot = this.undoStack.pop()!;
|
|
591
|
+
this.isUndoing = true;
|
|
592
|
+
try {
|
|
593
|
+
this.restoreSnapshot(previousSnapshot);
|
|
594
|
+
} finally {
|
|
595
|
+
this.isUndoing = false;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Redo the last undone operation
|
|
603
|
+
*/
|
|
604
|
+
redo(): boolean {
|
|
605
|
+
if (this.redoStack.length === 0) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Save current state to undo stack
|
|
610
|
+
const currentSnapshot = this.createSnapshot();
|
|
611
|
+
this.undoStack.push(currentSnapshot);
|
|
612
|
+
|
|
613
|
+
// Restore next state
|
|
614
|
+
const nextSnapshot = this.redoStack.pop()!;
|
|
615
|
+
this.isRedoing = true;
|
|
616
|
+
try {
|
|
617
|
+
this.restoreSnapshot(nextSnapshot);
|
|
618
|
+
} finally {
|
|
619
|
+
this.isRedoing = false;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Check if undo is available
|
|
627
|
+
*/
|
|
628
|
+
canUndo(): boolean {
|
|
629
|
+
return this.undoStack.length > 0;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Check if redo is available
|
|
634
|
+
*/
|
|
635
|
+
canRedo(): boolean {
|
|
636
|
+
return this.redoStack.length > 0;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ============================================
|
|
640
|
+
// Data Serialization Methods
|
|
641
|
+
// ============================================
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Get complete workbook data for serialization
|
|
645
|
+
*/
|
|
646
|
+
getData(): import('./types').WorkbookData {
|
|
647
|
+
// Serialize style pool
|
|
648
|
+
const stylePool: Record<string, import('./types').CellStyle> = {};
|
|
649
|
+
for (const [styleId, style] of this.stylePool.getAllStyles()) {
|
|
650
|
+
stylePool[styleId] = style;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Serialize format pool
|
|
654
|
+
const formatPool: Record<string, import('./types').CellFormat> = {};
|
|
655
|
+
for (const [formatId, format] of this.formatPool.getAllFormats()) {
|
|
656
|
+
formatPool[formatId] = format;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Serialize sheets
|
|
660
|
+
const sheets: import('./types').SheetData[] = [];
|
|
661
|
+
for (const sheet of this.sheets.values()) {
|
|
662
|
+
// Serialize cells
|
|
663
|
+
const cells: Array<{ key: string; cell: import('./types').Cell }> = [];
|
|
664
|
+
for (const [key, cell] of sheet.cells.entries()) {
|
|
665
|
+
cells.push({ key, cell });
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Serialize config (convert Maps/Sets to arrays)
|
|
669
|
+
const config = {
|
|
670
|
+
defaultRowHeight: sheet.config.defaultRowHeight,
|
|
671
|
+
defaultColWidth: sheet.config.defaultColWidth,
|
|
672
|
+
rowHeights: sheet.config.rowHeights ? Array.from(sheet.config.rowHeights.entries()) : undefined,
|
|
673
|
+
colWidths: sheet.config.colWidths ? Array.from(sheet.config.colWidths.entries()) : undefined,
|
|
674
|
+
hiddenRows: sheet.config.hiddenRows ? Array.from(sheet.config.hiddenRows) : undefined,
|
|
675
|
+
hiddenCols: sheet.config.hiddenCols ? Array.from(sheet.config.hiddenCols) : undefined,
|
|
676
|
+
frozenRows: sheet.config.frozenRows,
|
|
677
|
+
frozenCols: sheet.config.frozenCols,
|
|
678
|
+
showGridLines: sheet.config.showGridLines,
|
|
679
|
+
sortOrder: sheet.config.sortOrder,
|
|
680
|
+
filters: sheet.config.filters ? Array.from(sheet.config.filters.entries()) : undefined,
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
sheets.push({
|
|
684
|
+
id: sheet.id,
|
|
685
|
+
name: sheet.name,
|
|
686
|
+
cells,
|
|
687
|
+
config,
|
|
688
|
+
rowCount: sheet.rowCount,
|
|
689
|
+
colCount: sheet.colCount,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
id: this.id,
|
|
695
|
+
name: this.name,
|
|
696
|
+
activeSheetId: this.activeSheetId,
|
|
697
|
+
defaultRowHeight: this.defaultRowHeight,
|
|
698
|
+
defaultColWidth: this.defaultColWidth,
|
|
699
|
+
stylePool,
|
|
700
|
+
formatPool,
|
|
701
|
+
sheets,
|
|
702
|
+
selection: { ...this.selection },
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Set complete workbook data from serialized data
|
|
708
|
+
*/
|
|
709
|
+
setData(data: import('./types').WorkbookData): void {
|
|
710
|
+
// Clear existing state
|
|
711
|
+
this.sheets.clear();
|
|
712
|
+
this.formulaGraph = new FormulaGraphImpl();
|
|
713
|
+
this.stylePool.clear();
|
|
714
|
+
this.undoStack.length = 0;
|
|
715
|
+
this.redoStack.length = 0;
|
|
716
|
+
|
|
717
|
+
// Restore workbook metadata
|
|
718
|
+
this.id = data.id;
|
|
719
|
+
this.name = data.name;
|
|
720
|
+
this.activeSheetId = data.activeSheetId;
|
|
721
|
+
this.defaultRowHeight = data.defaultRowHeight;
|
|
722
|
+
this.defaultColWidth = data.defaultColWidth;
|
|
723
|
+
|
|
724
|
+
// Restore selection if provided
|
|
725
|
+
if (data.selection) {
|
|
726
|
+
this.selection = { ...data.selection };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Restore style pool
|
|
730
|
+
for (const [styleId, style] of Object.entries(data.stylePool)) {
|
|
731
|
+
// We need to manually set the style since we cleared the pool
|
|
732
|
+
(this.stylePool as StylePool).setStyles(new Map([[styleId, style]]));
|
|
733
|
+
(this.stylePool as StylePool).setStyleToId(new Map([[this.stylePool.getStyleKey(style), styleId]]));
|
|
734
|
+
// Update nextId to avoid conflicts
|
|
735
|
+
const idNum = parseInt(styleId.split('_')[1] || '0');
|
|
736
|
+
if (idNum >= (this.stylePool as StylePool).getNextId()) {
|
|
737
|
+
(this.stylePool as StylePool).setNextId(idNum + 1);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Restore format pool (if present - for backward compatibility)
|
|
742
|
+
if (data.formatPool) {
|
|
743
|
+
for (const [formatId, format] of Object.entries(data.formatPool)) {
|
|
744
|
+
// We need to manually set the format since we cleared the pool
|
|
745
|
+
(this.formatPool as FormatPool).setFormats(new Map([[formatId, format]]));
|
|
746
|
+
(this.formatPool as FormatPool).setFormatToId(new Map([[this.formatPool.getFormatKey(format), formatId]]));
|
|
747
|
+
// Update nextId to avoid conflicts
|
|
748
|
+
const idNum = parseInt(formatId.split('_')[1] || '0');
|
|
749
|
+
if (idNum >= (this.formatPool as FormatPool).getNextId()) {
|
|
750
|
+
(this.formatPool as FormatPool).setNextId(idNum + 1);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Restore sheets
|
|
756
|
+
for (const sheetData of data.sheets) {
|
|
757
|
+
// Convert config arrays back to Maps/Sets
|
|
758
|
+
const config = {
|
|
759
|
+
defaultRowHeight: sheetData.config.defaultRowHeight,
|
|
760
|
+
defaultColWidth: sheetData.config.defaultColWidth,
|
|
761
|
+
rowHeights: sheetData.config.rowHeights ? new Map(sheetData.config.rowHeights) : undefined,
|
|
762
|
+
colWidths: sheetData.config.colWidths ? new Map(sheetData.config.colWidths) : undefined,
|
|
763
|
+
hiddenRows: sheetData.config.hiddenRows ? new Set(sheetData.config.hiddenRows) : undefined,
|
|
764
|
+
hiddenCols: sheetData.config.hiddenCols ? new Set(sheetData.config.hiddenCols) : undefined,
|
|
765
|
+
frozenRows: sheetData.config.frozenRows,
|
|
766
|
+
frozenCols: sheetData.config.frozenCols,
|
|
767
|
+
showGridLines: sheetData.config.showGridLines,
|
|
768
|
+
sortOrder: sheetData.config.sortOrder,
|
|
769
|
+
filters: sheetData.config.filters ? new Map(sheetData.config.filters) : undefined,
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// Create sheet
|
|
773
|
+
const sheet = new SheetImpl(sheetData.id, sheetData.name, config);
|
|
774
|
+
sheet.rowCount = sheetData.rowCount;
|
|
775
|
+
sheet.colCount = sheetData.colCount;
|
|
776
|
+
|
|
777
|
+
// Restore cells (convert format to formatId if needed)
|
|
778
|
+
for (const { key, cell } of sheetData.cells) {
|
|
779
|
+
const cellToStore = { ...cell };
|
|
780
|
+
|
|
781
|
+
// Handle format conversion for backward compatibility
|
|
782
|
+
if ('format' in cellToStore && cellToStore.format && !cellToStore.formatId) {
|
|
783
|
+
// Clean and pool the format
|
|
784
|
+
const cleanedFormat = this.cleanFormat(cellToStore.format as CellFormat);
|
|
785
|
+
const formatId = this.formatPool.getOrCreate(cleanedFormat);
|
|
786
|
+
cellToStore.formatId = formatId;
|
|
787
|
+
delete (cellToStore as Partial<Cell> & { format?: CellFormat }).format;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
sheet.cells.set(key, cellToStore);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
this.sheets.set(sheetData.id, sheet);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Rebuild formula graph and evaluate formulas
|
|
797
|
+
this.rebuildFormulaGraph();
|
|
798
|
+
|
|
799
|
+
// Emit change events
|
|
800
|
+
this.events.emit('workbookChange', {});
|
|
801
|
+
this.events.emit('sheetChange', { sheetId: this.activeSheetId });
|
|
802
|
+
this.events.emit('cellSelection', { selection: this.selection });
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Clean up format object to only include properties relevant to the format type
|
|
807
|
+
*/
|
|
808
|
+
private cleanFormat(format: CellFormat): CellFormat {
|
|
809
|
+
if (!format.type) {
|
|
810
|
+
return format;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const cleaned: CellFormat = { type: format.type };
|
|
814
|
+
|
|
815
|
+
switch (format.type) {
|
|
816
|
+
case 'number':
|
|
817
|
+
cleaned.decimalPlaces = format.decimalPlaces;
|
|
818
|
+
cleaned.useThousandsSeparator = format.useThousandsSeparator;
|
|
819
|
+
cleaned.negativeFormat = format.negativeFormat;
|
|
820
|
+
break;
|
|
821
|
+
|
|
822
|
+
case 'currency':
|
|
823
|
+
cleaned.decimalPlaces = format.decimalPlaces;
|
|
824
|
+
cleaned.currencyCode = format.currencyCode;
|
|
825
|
+
cleaned.currencySymbolPosition = format.currencySymbolPosition;
|
|
826
|
+
cleaned.negativeFormat = format.negativeFormat;
|
|
827
|
+
break;
|
|
828
|
+
|
|
829
|
+
case 'accounting':
|
|
830
|
+
cleaned.decimalPlaces = format.decimalPlaces;
|
|
831
|
+
cleaned.currencyCode = format.currencyCode;
|
|
832
|
+
cleaned.negativeFormat = format.negativeFormat;
|
|
833
|
+
break;
|
|
834
|
+
|
|
835
|
+
case 'percentage':
|
|
836
|
+
cleaned.decimalPlaces = format.decimalPlaces;
|
|
837
|
+
break;
|
|
838
|
+
|
|
839
|
+
case 'scientific':
|
|
840
|
+
cleaned.decimalPlaces = format.decimalPlaces;
|
|
841
|
+
break;
|
|
842
|
+
|
|
843
|
+
case 'fraction':
|
|
844
|
+
cleaned.fractionType = format.fractionType;
|
|
845
|
+
break;
|
|
846
|
+
|
|
847
|
+
case 'date':
|
|
848
|
+
cleaned.dateFormat = format.dateFormat;
|
|
849
|
+
break;
|
|
850
|
+
|
|
851
|
+
case 'time':
|
|
852
|
+
cleaned.timeFormat = format.timeFormat;
|
|
853
|
+
break;
|
|
854
|
+
|
|
855
|
+
case 'datetime':
|
|
856
|
+
cleaned.dateFormat = format.dateFormat;
|
|
857
|
+
cleaned.timeFormat = format.timeFormat;
|
|
858
|
+
break;
|
|
859
|
+
|
|
860
|
+
case 'duration':
|
|
861
|
+
cleaned.durationFormat = format.durationFormat;
|
|
862
|
+
break;
|
|
863
|
+
|
|
864
|
+
case 'custom':
|
|
865
|
+
cleaned.pattern = format.pattern;
|
|
866
|
+
break;
|
|
867
|
+
|
|
868
|
+
case 'text':
|
|
869
|
+
default:
|
|
870
|
+
// Text format doesn't need additional properties
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return cleaned;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Rebuild formula graph from current cells and evaluate all formulas
|
|
879
|
+
*/
|
|
880
|
+
private rebuildFormulaGraph(): void {
|
|
881
|
+
for (const [sheetId, sheet] of this.sheets.entries()) {
|
|
882
|
+
for (const [key, cell] of sheet.cells.entries()) {
|
|
883
|
+
if (cell.formula) {
|
|
884
|
+
const { row, col } = parseCellKey(key);
|
|
885
|
+
const parseResult = this.formulaParser.parse(cell.formula, row, col);
|
|
886
|
+
if (!parseResult.error) {
|
|
887
|
+
this.formulaGraph.addFormula(key, cell.formula, parseResult.dependencies);
|
|
888
|
+
// Evaluate the formula
|
|
889
|
+
this.evaluateFormula(sheetId, row, col);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// ============================================
|
|
897
|
+
// Sorting Methods
|
|
898
|
+
// ============================================
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Set the sort order for a sheet
|
|
902
|
+
* @param sortOrder Sort order to apply
|
|
903
|
+
* @param sheetId Target sheet ID (defaults to active sheet)
|
|
904
|
+
*/
|
|
905
|
+
setSortOrder(sortOrder: SortOrder[], sheetId?: string): void {
|
|
906
|
+
const targetSheetId = sheetId ?? this.activeSheetId;
|
|
907
|
+
const sheet = this.sheets.get(targetSheetId);
|
|
908
|
+
if (sheet) {
|
|
909
|
+
// Record history before making changes
|
|
910
|
+
if (!this.isUndoing && !this.isRedoing && !this.isBatching) {
|
|
911
|
+
this.recordHistory();
|
|
912
|
+
}
|
|
913
|
+
sheet.setSortOrder(sortOrder);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Get the sort order for a sheet
|
|
919
|
+
* @param sheetId Target sheet ID (defaults to active sheet)
|
|
920
|
+
* @returns Current sort order
|
|
921
|
+
*/
|
|
922
|
+
getSortOrder(sheetId?: string): SortOrder[] {
|
|
923
|
+
const targetSheetId = sheetId ?? this.activeSheetId;
|
|
924
|
+
const sheet = this.sheets.get(targetSheetId);
|
|
925
|
+
return sheet ? sheet.getSortOrder() : [];
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Clear sorting for a sheet
|
|
930
|
+
* @param sheetId Target sheet ID (defaults to active sheet)
|
|
931
|
+
*/
|
|
932
|
+
clearSort(sheetId?: string): void {
|
|
933
|
+
const targetSheetId = sheetId ?? this.activeSheetId;
|
|
934
|
+
const sheet = this.sheets.get(targetSheetId);
|
|
935
|
+
if (sheet) {
|
|
936
|
+
// Record history before making changes
|
|
937
|
+
if (!this.isUndoing && !this.isRedoing && !this.isBatching) {
|
|
938
|
+
this.recordHistory();
|
|
939
|
+
}
|
|
940
|
+
sheet.clearSort();
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Sort the sheet data according to current sort order
|
|
946
|
+
* @param sheetId Target sheet ID (defaults to active sheet)
|
|
947
|
+
*/
|
|
948
|
+
sortSheet(sheetId?: string): void {
|
|
949
|
+
const targetSheetId = sheetId ?? this.activeSheetId;
|
|
950
|
+
const sheet = this.sheets.get(targetSheetId);
|
|
951
|
+
if (sheet) {
|
|
952
|
+
const sortOrder = sheet.getSortOrder();
|
|
953
|
+
if (sortOrder.length > 0) {
|
|
954
|
+
// Record history before sorting
|
|
955
|
+
if (!this.isUndoing && !this.isRedoing && !this.isBatching) {
|
|
956
|
+
this.recordHistory();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
SortManager.sortRows(sheet, sortOrder);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// ============================================
|
|
965
|
+
// Filtering Methods
|
|
966
|
+
// ============================================
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Set a filter for a column
|
|
970
|
+
* @param column Column index
|
|
971
|
+
* @param filter Filter configuration
|
|
972
|
+
* @param sheetId Target sheet ID (defaults to active sheet)
|
|
973
|
+
*/
|
|
974
|
+
setFilter(column: number, filter: import('./types').ColumnFilter, sheetId?: string): void {
|
|
975
|
+
const targetSheetId = sheetId ?? this.activeSheetId;
|
|
976
|
+
const sheet = this.sheets.get(targetSheetId);
|
|
977
|
+
if (sheet) {
|
|
978
|
+
// Record history before making changes
|
|
979
|
+
if (!this.isUndoing && !this.isRedoing && !this.isBatching) {
|
|
980
|
+
this.recordHistory();
|
|
981
|
+
}
|
|
982
|
+
sheet.setFilter(column, filter);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Clear filter for a specific column
|
|
988
|
+
* @param column Column index
|
|
989
|
+
* @param sheetId Target sheet ID (defaults to active sheet)
|
|
990
|
+
*/
|
|
991
|
+
clearFilter(column: number, sheetId?: string): void {
|
|
992
|
+
const targetSheetId = sheetId ?? this.activeSheetId;
|
|
993
|
+
const sheet = this.sheets.get(targetSheetId);
|
|
994
|
+
if (sheet) {
|
|
995
|
+
// Record history before making changes
|
|
996
|
+
if (!this.isUndoing && !this.isRedoing && !this.isBatching) {
|
|
997
|
+
this.recordHistory();
|
|
998
|
+
}
|
|
999
|
+
sheet.clearFilter(column);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Get all active filters for a sheet
|
|
1005
|
+
* @param sheetId Target sheet ID (defaults to active sheet)
|
|
1006
|
+
* @returns Map of column -> filter
|
|
1007
|
+
*/
|
|
1008
|
+
getFilters(sheetId?: string): Map<number, import('./types').ColumnFilter> {
|
|
1009
|
+
const targetSheetId = sheetId ?? this.activeSheetId;
|
|
1010
|
+
const sheet = this.sheets.get(targetSheetId);
|
|
1011
|
+
return sheet ? sheet.getFilters() : new Map();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Clear all filters for a sheet
|
|
1016
|
+
* @param sheetId Target sheet ID (defaults to active sheet)
|
|
1017
|
+
*/
|
|
1018
|
+
clearAllFilters(sheetId?: string): void {
|
|
1019
|
+
const targetSheetId = sheetId ?? this.activeSheetId;
|
|
1020
|
+
const sheet = this.sheets.get(targetSheetId);
|
|
1021
|
+
if (sheet) {
|
|
1022
|
+
// Record history before making changes
|
|
1023
|
+
if (!this.isUndoing && !this.isRedoing && !this.isBatching) {
|
|
1024
|
+
this.recordHistory();
|
|
1025
|
+
}
|
|
1026
|
+
sheet.clearAllFilters();
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
}
|
|
1031
|
+
|