@odoo/o-spreadsheet 14.0.0 → 14.0.1
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/.eslintrc.json +20 -0
- package/.husky/post-checkout +9 -0
- package/.husky/pre-commit +39 -0
- package/.prettierignore +1 -0
- package/astToFormula.patch +0 -0
- package/demo/data.js +255 -0
- package/demo/index.html +9 -0
- package/demo/libs/Chart.bundle.js +20755 -0
- package/demo/main.js +101 -0
- package/doc/readme.md +46 -0
- package/package.json +3 -17
- package/rollup.config.js +23 -0
- package/src/base_plugin.ts +102 -0
- package/src/components/autofill.ts +146 -0
- package/src/components/bottom_bar.ts +199 -0
- package/src/components/color_picker.ts +150 -0
- package/src/components/composer/autocomplete_dropdown.ts +135 -0
- package/src/components/composer/composer.ts +487 -0
- package/src/components/composer/content_editable_helper.ts +146 -0
- package/src/components/figures/chart.ts +70 -0
- package/src/components/figures/container.ts +268 -0
- package/src/components/figures/text_figure.ts +36 -0
- package/src/components/grid.ts +635 -0
- package/src/components/helpers/dom_helpers.ts +7 -0
- package/src/components/helpers/drag_and_drop.ts +17 -0
- package/src/components/icons.ts +36 -0
- package/src/components/index.ts +1 -0
- package/src/components/menu.ts +288 -0
- package/src/components/overlay.ts +457 -0
- package/src/components/scrollbar.ts +22 -0
- package/src/components/selection_input.ts +168 -0
- package/src/components/side_panel/cell_is_rule_editor.ts +169 -0
- package/src/components/side_panel/chart_panel.ts +104 -0
- package/src/components/side_panel/color_scale_rule_editor.ts +145 -0
- package/src/components/side_panel/conditional_formatting.ts +396 -0
- package/src/components/side_panel/side_panel.ts +127 -0
- package/src/components/side_panel/translations_terms.ts +45 -0
- package/src/components/spreadsheet.ts +198 -0
- package/src/components/top_bar.ts +535 -0
- package/src/constants.ts +26 -0
- package/src/data.ts +149 -0
- package/src/fonts.ts +41 -0
- package/src/formulas/compiler.ts +258 -0
- package/src/formulas/composer_tokenizer.ts +70 -0
- package/src/formulas/index.ts +15 -0
- package/src/formulas/normalize.ts +52 -0
- package/src/formulas/parser.ts +280 -0
- package/src/formulas/range_tokenizer.ts +97 -0
- package/src/formulas/tokenizer.ts +242 -0
- package/src/functions/arguments.ts +114 -0
- package/src/functions/dates.ts +302 -0
- package/src/functions/helpers.ts +592 -0
- package/src/functions/index.ts +52 -0
- package/src/functions/module_database.ts +282 -0
- package/src/functions/module_date.ts +743 -0
- package/src/functions/module_info.ts +69 -0
- package/src/functions/module_logical.ts +208 -0
- package/src/functions/module_lookup.ts +185 -0
- package/src/functions/module_math.ts +1252 -0
- package/src/functions/module_operators.ts +291 -0
- package/src/functions/module_statistical.ts +882 -0
- package/src/functions/module_text.ts +370 -0
- package/src/helpers/color.ts +30 -0
- package/src/helpers/coordinates.ts +71 -0
- package/src/helpers/grid_manipulation.ts +68 -0
- package/src/helpers/index.ts +7 -0
- package/src/helpers/misc.ts +76 -0
- package/src/helpers/numbers.ts +117 -0
- package/src/helpers/uuid.ts +19 -0
- package/src/helpers/zones.ts +263 -0
- package/src/history.ts +186 -0
- package/src/index.ts +84 -0
- package/src/model.ts +261 -0
- package/src/plugins/autofill.ts +412 -0
- package/src/plugins/chart.ts +404 -0
- package/src/plugins/clipboard.ts +461 -0
- package/src/plugins/conditional_format.ts +445 -0
- package/src/plugins/core.ts +1502 -0
- package/src/plugins/edition.ts +157 -0
- package/src/plugins/evaluation.ts +428 -0
- package/src/plugins/figures.ts +123 -0
- package/src/plugins/formatting.ts +630 -0
- package/src/plugins/highlight.ts +187 -0
- package/src/plugins/index.ts +32 -0
- package/src/plugins/merge.ts +524 -0
- package/src/plugins/renderer.ts +571 -0
- package/src/plugins/selection.ts +562 -0
- package/src/plugins/selection_inputs.ts +326 -0
- package/src/registries/autofill_modifiers.ts +76 -0
- package/src/registries/autofill_rules.ts +98 -0
- package/src/registries/figure_registry.ts +30 -0
- package/src/registries/index.ts +7 -0
- package/src/registries/menu_items_registry.ts +110 -0
- package/src/registries/menus/cell_menu_registry.ts +79 -0
- package/src/registries/menus/col_menu_registry.ts +63 -0
- package/src/registries/menus/index.ts +6 -0
- package/src/registries/menus/menu_items_actions.ts +425 -0
- package/src/registries/menus/row_menu_registry.ts +63 -0
- package/src/registries/menus/sheet_menu_registry.ts +75 -0
- package/src/registries/menus/topbar_menu_registry.ts +204 -0
- package/src/registries/side_panel_registry.ts +26 -0
- package/src/registries/topbar_component_registry.ts +12 -0
- package/src/registry.ts +51 -0
- package/src/translation.ts +44 -0
- package/src/types/autofill.ts +48 -0
- package/src/types/chart.ts +26 -0
- package/src/types/commands.ts +716 -0
- package/src/types/conditional_formatting.ts +124 -0
- package/src/types/env.ts +13 -0
- package/src/types/functions.ts +35 -0
- package/src/types/getters.ts +103 -0
- package/src/types/index.ts +20 -0
- package/src/types/misc.ts +143 -0
- package/src/types/rendering.ts +41 -0
- package/src/types/workbook_data.ts +62 -0
- package/tests/__mocks__/uuid.ts +7 -0
- package/tests/canvas.mock.ts +34 -0
- package/tests/components/__mocks__/content_editable_helper.ts +79 -0
- package/tests/components/__mocks__/scrollbar.ts +18 -0
- package/tests/components/__snapshots__/autocomplete_dropdown_test.ts.snap +29 -0
- package/tests/components/__snapshots__/bottom_bar_test.ts.snap +75 -0
- package/tests/components/__snapshots__/context_menu_test.ts.snap +88 -0
- package/tests/components/__snapshots__/grid_test.ts.snap +60 -0
- package/tests/components/__snapshots__/spreadsheet_test.ts.snap +455 -0
- package/tests/components/__snapshots__/top_bar_test.ts.snap +705 -0
- package/tests/components/autocomplete_dropdown_test.ts +315 -0
- package/tests/components/autofill_test.ts +167 -0
- package/tests/components/bottom_bar_test.ts +262 -0
- package/tests/components/chart_side_panel_test.ts +123 -0
- package/tests/components/composer_test.ts +400 -0
- package/tests/components/context_menu_test.ts +825 -0
- package/tests/components/grid_manipulation_test.ts +163 -0
- package/tests/components/grid_test.ts +581 -0
- package/tests/components/overlay_test.ts +541 -0
- package/tests/components/selection_input_test.ts +259 -0
- package/tests/components/side_panel_test.ts +148 -0
- package/tests/components/spreadsheet_test.ts +194 -0
- package/tests/components/top_bar_test.ts +427 -0
- package/tests/data_test.ts +175 -0
- package/tests/dom_helper.ts +42 -0
- package/tests/formulas/__snapshots__/compiler_test.ts.snap +302 -0
- package/tests/formulas/compiler_test.ts +266 -0
- package/tests/formulas/composer_tokenizer_test.ts +137 -0
- package/tests/formulas/expressions_test.ts +57 -0
- package/tests/formulas/formulas_test.ts +80 -0
- package/tests/formulas/normalize_test.ts +82 -0
- package/tests/formulas/parser_test.ts +224 -0
- package/tests/formulas/range_tokenizer_test.ts +187 -0
- package/tests/formulas/tokenizer_test.ts +164 -0
- package/tests/functions/arguments_test.ts +141 -0
- package/tests/functions/dates_test.ts +1081 -0
- package/tests/functions/functions_test.ts +53 -0
- package/tests/functions/helper_test.ts +98 -0
- package/tests/functions/module_database_test.ts +311 -0
- package/tests/functions/module_date_test.ts +941 -0
- package/tests/functions/module_info_test.ts +139 -0
- package/tests/functions/module_logical_test.ts +369 -0
- package/tests/functions/module_lookup_test.ts +541 -0
- package/tests/functions/module_math_test.ts +2482 -0
- package/tests/functions/module_operators_test.ts +955 -0
- package/tests/functions/module_statistical_test.ts +2014 -0
- package/tests/functions/module_text_test.ts +667 -0
- package/tests/helpers/coordinates_test.ts +25 -0
- package/tests/helpers/misc_test.ts +95 -0
- package/tests/helpers/numbers_test.ts +196 -0
- package/tests/helpers/zones_test.ts +97 -0
- package/tests/helpers.ts +384 -0
- package/tests/menu_items_registry_test.ts +540 -0
- package/tests/model/plugins/__snapshots__/chart_test.ts.snap +90 -0
- package/tests/model/plugins/chart_test.ts +562 -0
- package/tests/model_test.ts +82 -0
- package/tests/plugins/__snapshots__/conditional_formatting_test.ts.snap +131 -0
- package/tests/plugins/__snapshots__/renderer_test.ts.snap +371 -0
- package/tests/plugins/autofill_test.ts +457 -0
- package/tests/plugins/borders_test.ts +295 -0
- package/tests/plugins/clipboard_test.ts +1180 -0
- package/tests/plugins/conditional_formatting_test.ts +1253 -0
- package/tests/plugins/core_test.ts +614 -0
- package/tests/plugins/edition_test.ts +100 -0
- package/tests/plugins/evaluation_async_test.ts +332 -0
- package/tests/plugins/evaluation_dates_test.ts +79 -0
- package/tests/plugins/evaluation_test.ts +901 -0
- package/tests/plugins/figures_test.ts +327 -0
- package/tests/plugins/formatting_test.ts +391 -0
- package/tests/plugins/grid_manipulation_test.ts +1366 -0
- package/tests/plugins/highlight_test.ts +413 -0
- package/tests/plugins/history_test.ts +142 -0
- package/tests/plugins/import_export_test.ts +276 -0
- package/tests/plugins/merges_test.ts +490 -0
- package/tests/plugins/navigation_test.ts +181 -0
- package/tests/plugins/plugins/evaluation_test.ts +111 -0
- package/tests/plugins/renderer_test.ts +598 -0
- package/tests/plugins/resizing_test.ts +152 -0
- package/tests/plugins/selection_input_test.ts +409 -0
- package/tests/plugins/selection_test.ts +442 -0
- package/tests/plugins/sheets_test.ts +643 -0
- package/tests/plugins/style_test.ts +90 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,1502 @@
|
|
|
1
|
+
import { BasePlugin } from "../base_plugin";
|
|
2
|
+
import { DEFAULT_CELL_HEIGHT, DEFAULT_CELL_WIDTH } from "../constants";
|
|
3
|
+
import { cellReference, compile, normalize, rangeTokenize } from "../formulas/index";
|
|
4
|
+
import { formatDateTime, InternalDate, parseDateTime } from "../functions/dates";
|
|
5
|
+
import {
|
|
6
|
+
formatNumber,
|
|
7
|
+
formatStandardNumber,
|
|
8
|
+
getComposerSheetName,
|
|
9
|
+
getUnquotedSheetName,
|
|
10
|
+
isNumber,
|
|
11
|
+
mapCellsInZone,
|
|
12
|
+
numberToLetters,
|
|
13
|
+
parseNumber,
|
|
14
|
+
recomputeZones,
|
|
15
|
+
toCartesian,
|
|
16
|
+
toXC,
|
|
17
|
+
toZone,
|
|
18
|
+
} from "../helpers/index";
|
|
19
|
+
import { _lt } from "../translation";
|
|
20
|
+
import {
|
|
21
|
+
CancelledReason,
|
|
22
|
+
Cell,
|
|
23
|
+
CellData,
|
|
24
|
+
Col,
|
|
25
|
+
Command,
|
|
26
|
+
CommandResult,
|
|
27
|
+
HeaderData,
|
|
28
|
+
NormalizedFormula,
|
|
29
|
+
RenameSheetCommand,
|
|
30
|
+
Row,
|
|
31
|
+
Sheet,
|
|
32
|
+
SheetData,
|
|
33
|
+
WorkbookData,
|
|
34
|
+
Zone,
|
|
35
|
+
} from "../types/index";
|
|
36
|
+
|
|
37
|
+
const nbspRegexp = new RegExp(String.fromCharCode(160), "g");
|
|
38
|
+
const MIN_PADDING = 3;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Core Plugin
|
|
42
|
+
*
|
|
43
|
+
* This is the most fundamental of all plugins. It defines how to interact with
|
|
44
|
+
* cell and sheet content.
|
|
45
|
+
*/
|
|
46
|
+
export class CorePlugin extends BasePlugin {
|
|
47
|
+
static getters = [
|
|
48
|
+
"applyOffset",
|
|
49
|
+
"getColsZone",
|
|
50
|
+
"getRowsZone",
|
|
51
|
+
"getCell",
|
|
52
|
+
"getCellText",
|
|
53
|
+
"zoneToXC",
|
|
54
|
+
"getActiveSheet",
|
|
55
|
+
"getSheetName",
|
|
56
|
+
"getSheetIdByName",
|
|
57
|
+
"getSheets",
|
|
58
|
+
"getCol",
|
|
59
|
+
"getRow",
|
|
60
|
+
"getColCells",
|
|
61
|
+
"getNumberCols",
|
|
62
|
+
"getNumberRows",
|
|
63
|
+
"getGridSize",
|
|
64
|
+
"shouldShowFormulas",
|
|
65
|
+
"getRangeValues",
|
|
66
|
+
"getRangeFormattedValues",
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
private sheetIds: { [name: string]: string } = {};
|
|
70
|
+
private showFormulas: boolean = false;
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Command Handling
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
allowDispatch(cmd: Command): CommandResult {
|
|
77
|
+
switch (cmd.type) {
|
|
78
|
+
case "REMOVE_COLUMNS":
|
|
79
|
+
return this.workbook.sheets[cmd.sheet].cols.length > cmd.columns.length
|
|
80
|
+
? { status: "SUCCESS" }
|
|
81
|
+
: { status: "CANCELLED", reason: CancelledReason.NotEnoughColumns };
|
|
82
|
+
case "REMOVE_ROWS":
|
|
83
|
+
return this.workbook.sheets[cmd.sheet].rows.length > cmd.rows.length
|
|
84
|
+
? { status: "SUCCESS" }
|
|
85
|
+
: { status: "CANCELLED", reason: CancelledReason.NotEnoughRows };
|
|
86
|
+
case "CREATE_SHEET":
|
|
87
|
+
case "DUPLICATE_SHEET":
|
|
88
|
+
const { visibleSheets, sheets } = this.workbook;
|
|
89
|
+
return !cmd.name || !visibleSheets.find((id) => sheets[id].name === cmd.name)
|
|
90
|
+
? { status: "SUCCESS" }
|
|
91
|
+
: { status: "CANCELLED", reason: CancelledReason.WrongSheetName };
|
|
92
|
+
case "MOVE_SHEET":
|
|
93
|
+
const currentIndex = this.workbook.visibleSheets.findIndex((id) => id === cmd.sheet);
|
|
94
|
+
if (currentIndex === -1) {
|
|
95
|
+
return { status: "CANCELLED", reason: CancelledReason.WrongSheetName };
|
|
96
|
+
}
|
|
97
|
+
return (cmd.direction === "left" && currentIndex === 0) ||
|
|
98
|
+
(cmd.direction === "right" && currentIndex === this.workbook.visibleSheets.length - 1)
|
|
99
|
+
? { status: "CANCELLED", reason: CancelledReason.WrongSheetMove }
|
|
100
|
+
: { status: "SUCCESS" };
|
|
101
|
+
case "RENAME_SHEET":
|
|
102
|
+
return this.isRenameAllowed(cmd);
|
|
103
|
+
case "DELETE_SHEET_CONFIRMATION":
|
|
104
|
+
case "DELETE_SHEET":
|
|
105
|
+
return this.workbook.visibleSheets.length > 1
|
|
106
|
+
? { status: "SUCCESS" }
|
|
107
|
+
: { status: "CANCELLED", reason: CancelledReason.NotEnoughSheets };
|
|
108
|
+
default:
|
|
109
|
+
return { status: "SUCCESS" };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
handle(cmd: Command) {
|
|
114
|
+
switch (cmd.type) {
|
|
115
|
+
case "ACTIVATE_SHEET":
|
|
116
|
+
this.history.updateState(["activeSheet"], this.workbook.sheets[cmd.to]);
|
|
117
|
+
break;
|
|
118
|
+
case "CREATE_SHEET":
|
|
119
|
+
const sheet = this.createSheet(
|
|
120
|
+
cmd.id,
|
|
121
|
+
cmd.name || this.generateSheetName(),
|
|
122
|
+
cmd.cols || 26,
|
|
123
|
+
cmd.rows || 100
|
|
124
|
+
);
|
|
125
|
+
this.sheetIds[this.workbook.sheets[sheet].name] = sheet;
|
|
126
|
+
if (cmd.activate) {
|
|
127
|
+
this.dispatch("ACTIVATE_SHEET", { from: this.workbook.activeSheet.id, to: sheet });
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
case "MOVE_SHEET":
|
|
131
|
+
this.moveSheet(cmd.sheet, cmd.direction);
|
|
132
|
+
break;
|
|
133
|
+
case "RENAME_SHEET":
|
|
134
|
+
if (cmd.interactive) {
|
|
135
|
+
this.interactiveRenameSheet(cmd.sheet, _lt("Rename Sheet"));
|
|
136
|
+
} else {
|
|
137
|
+
this.renameSheet(cmd.sheet, cmd.name!);
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
case "DUPLICATE_SHEET":
|
|
141
|
+
this.duplicateSheet(cmd.sheet, cmd.id, cmd.name);
|
|
142
|
+
break;
|
|
143
|
+
case "DELETE_SHEET_CONFIRMATION":
|
|
144
|
+
this.interactiveDeleteSheet(cmd.sheet);
|
|
145
|
+
break;
|
|
146
|
+
case "DELETE_SHEET":
|
|
147
|
+
this.deleteSheet(cmd.sheet);
|
|
148
|
+
break;
|
|
149
|
+
case "DELETE_CONTENT":
|
|
150
|
+
this.clearZones(cmd.sheet, cmd.target);
|
|
151
|
+
break;
|
|
152
|
+
case "SET_VALUE":
|
|
153
|
+
const [col, row] = toCartesian(cmd.xc);
|
|
154
|
+
this.dispatch("UPDATE_CELL", {
|
|
155
|
+
sheet: cmd.sheetId ? cmd.sheetId : this.workbook.activeSheet.id,
|
|
156
|
+
col,
|
|
157
|
+
row,
|
|
158
|
+
content: cmd.text,
|
|
159
|
+
});
|
|
160
|
+
break;
|
|
161
|
+
case "UPDATE_CELL":
|
|
162
|
+
this.updateCell(cmd.sheet, cmd.col, cmd.row, cmd);
|
|
163
|
+
break;
|
|
164
|
+
case "CLEAR_CELL":
|
|
165
|
+
this.dispatch("UPDATE_CELL", {
|
|
166
|
+
sheet: cmd.sheet,
|
|
167
|
+
col: cmd.col,
|
|
168
|
+
row: cmd.row,
|
|
169
|
+
content: "",
|
|
170
|
+
border: 0,
|
|
171
|
+
style: 0,
|
|
172
|
+
format: "",
|
|
173
|
+
});
|
|
174
|
+
break;
|
|
175
|
+
case "AUTORESIZE_COLUMNS":
|
|
176
|
+
for (let col of cmd.cols) {
|
|
177
|
+
const size = this.getColMaxWidth(col);
|
|
178
|
+
if (size !== 0) {
|
|
179
|
+
this.setColSize(cmd.sheet, col, size + 2 * MIN_PADDING);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
case "AUTORESIZE_ROWS":
|
|
184
|
+
for (let col of cmd.rows) {
|
|
185
|
+
const size = this.getRowMaxHeight(col);
|
|
186
|
+
if (size !== 0) {
|
|
187
|
+
this.setRowSize(cmd.sheet, col, size + 2 * MIN_PADDING);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
case "RESIZE_COLUMNS":
|
|
192
|
+
for (let col of cmd.cols) {
|
|
193
|
+
this.setColSize(cmd.sheet, col, cmd.size);
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
case "RESIZE_ROWS":
|
|
197
|
+
for (let row of cmd.rows) {
|
|
198
|
+
this.setRowSize(cmd.sheet, row, cmd.size);
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
case "REMOVE_COLUMNS":
|
|
202
|
+
this.removeColumns(cmd.sheet, cmd.columns);
|
|
203
|
+
this.history.updateState(
|
|
204
|
+
["sheets", cmd.sheet, "colNumber"],
|
|
205
|
+
this.workbook.sheets[cmd.sheet].colNumber - cmd.columns.length
|
|
206
|
+
);
|
|
207
|
+
break;
|
|
208
|
+
case "REMOVE_ROWS":
|
|
209
|
+
this.removeRows(cmd.sheet, cmd.rows);
|
|
210
|
+
this.history.updateState(
|
|
211
|
+
["sheets", cmd.sheet, "rowNumber"],
|
|
212
|
+
this.workbook.sheets[cmd.sheet].rowNumber - cmd.rows.length
|
|
213
|
+
);
|
|
214
|
+
break;
|
|
215
|
+
case "ADD_COLUMNS":
|
|
216
|
+
this.addColumns(cmd.sheet, cmd.column, cmd.position, cmd.quantity);
|
|
217
|
+
this.history.updateState(
|
|
218
|
+
["activeSheet", "colNumber"],
|
|
219
|
+
this.workbook.activeSheet.colNumber + cmd.quantity
|
|
220
|
+
);
|
|
221
|
+
break;
|
|
222
|
+
case "ADD_ROWS":
|
|
223
|
+
this.addRows(cmd.sheet, cmd.row, cmd.position, cmd.quantity);
|
|
224
|
+
this.history.updateState(
|
|
225
|
+
["activeSheet", "rowNumber"],
|
|
226
|
+
this.workbook.activeSheet.rowNumber + cmd.quantity
|
|
227
|
+
);
|
|
228
|
+
break;
|
|
229
|
+
case "SET_FORMULA_VISIBILITY":
|
|
230
|
+
this.showFormulas = cmd.show;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Getters
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
applyOffset(formula: string, offsetX: number, offsetY: number): string {
|
|
240
|
+
return rangeTokenize(formula)
|
|
241
|
+
.map((t) => {
|
|
242
|
+
if (t.type === "SYMBOL" && cellReference.test(t.value)) {
|
|
243
|
+
const [xcs, sheetName] = t.value.split("!").reverse();
|
|
244
|
+
const sheetId = this.getSheetIdByName(sheetName);
|
|
245
|
+
if (xcs.includes(":")) {
|
|
246
|
+
return this.updateRange(xcs, offsetX, offsetY, sheetId);
|
|
247
|
+
}
|
|
248
|
+
return this.updateReference(xcs, offsetX, offsetY, sheetId, true, false);
|
|
249
|
+
}
|
|
250
|
+
return t.value;
|
|
251
|
+
})
|
|
252
|
+
.join("");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getCell(col: number, row: number, sheetName?: string): Cell | null {
|
|
256
|
+
let r;
|
|
257
|
+
if (!sheetName) {
|
|
258
|
+
r = this.workbook.activeSheet.rows[row];
|
|
259
|
+
} else {
|
|
260
|
+
const sheet = Object.values(this.workbook.sheets).find((x) => x.name === sheetName);
|
|
261
|
+
if (sheet) {
|
|
262
|
+
r = sheet.rows[row];
|
|
263
|
+
} else {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return r ? r.cells[col] || null : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
getCellText(cell: Cell): string {
|
|
271
|
+
const value: unknown = this.showFormulas ? cell.content : cell.value;
|
|
272
|
+
const shouldFormat = (value || value === 0) && cell.format && !cell.error && !cell.pending;
|
|
273
|
+
const dateTimeFormat = shouldFormat && cell.format!.match(/y|m|d|:/);
|
|
274
|
+
const numberFormat = shouldFormat && !dateTimeFormat;
|
|
275
|
+
switch (typeof value) {
|
|
276
|
+
case "string":
|
|
277
|
+
return value;
|
|
278
|
+
case "boolean":
|
|
279
|
+
return value ? "TRUE" : "FALSE";
|
|
280
|
+
case "number":
|
|
281
|
+
if (dateTimeFormat) {
|
|
282
|
+
return formatDateTime({ value, format: cell.format! });
|
|
283
|
+
}
|
|
284
|
+
if (numberFormat) {
|
|
285
|
+
return formatNumber(value, cell.format!);
|
|
286
|
+
}
|
|
287
|
+
return formatStandardNumber(value);
|
|
288
|
+
case "object":
|
|
289
|
+
if (!value) return "0";
|
|
290
|
+
if ("value" in value && "format" in value) {
|
|
291
|
+
if (dateTimeFormat) {
|
|
292
|
+
return formatDateTime(value as InternalDate, cell.format);
|
|
293
|
+
}
|
|
294
|
+
if (numberFormat) {
|
|
295
|
+
return formatNumber((value as any).value, cell.format!);
|
|
296
|
+
}
|
|
297
|
+
if (value && (value as InternalDate).format.match(/y|m|d|:/)) {
|
|
298
|
+
return formatDateTime(value as InternalDate);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return value.toString();
|
|
302
|
+
}
|
|
303
|
+
return "";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Converts a zone to a XC coordinate system
|
|
308
|
+
*
|
|
309
|
+
* The conversion also treats merges as one single cell
|
|
310
|
+
*
|
|
311
|
+
* Examples:
|
|
312
|
+
* {top:0,left:0,right:0,bottom:0} ==> A1
|
|
313
|
+
* {top:0,left:0,right:1,bottom:1} ==> A1:B2
|
|
314
|
+
*
|
|
315
|
+
* if A1:B2 is a merge:
|
|
316
|
+
* {top:0,left:0,right:1,bottom:1} ==> A1
|
|
317
|
+
* {top:1,left:0,right:1,bottom:2} ==> A1:B3
|
|
318
|
+
*
|
|
319
|
+
* if A1:B2 and A4:B5 are merges:
|
|
320
|
+
* {top:1,left:0,right:1,bottom:3} ==> A1:A5
|
|
321
|
+
*/
|
|
322
|
+
zoneToXC(zone: Zone): string {
|
|
323
|
+
zone = this.getters.expandZone(zone);
|
|
324
|
+
const topLeft = toXC(zone.left, zone.top);
|
|
325
|
+
const botRight = toXC(zone.right, zone.bottom);
|
|
326
|
+
if (
|
|
327
|
+
topLeft != botRight &&
|
|
328
|
+
this.getters.getMainCell(topLeft) !== this.getters.getMainCell(botRight)
|
|
329
|
+
) {
|
|
330
|
+
return topLeft + ":" + botRight;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return topLeft;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Returns the id (not the name) of the currently active sheet
|
|
338
|
+
*/
|
|
339
|
+
getActiveSheet(): string {
|
|
340
|
+
return this.workbook.activeSheet.id;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
getSheetName(sheetId: string): string | undefined {
|
|
344
|
+
return this.workbook.sheets[sheetId] && this.workbook.sheets[sheetId].name;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
getSheetIdByName(name: string | undefined): string | undefined {
|
|
348
|
+
return name && this.sheetIds[getUnquotedSheetName(name)];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
getSheets(): Sheet[] {
|
|
352
|
+
const { visibleSheets, sheets } = this.workbook;
|
|
353
|
+
return visibleSheets.map((id) => sheets[id]);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
getCol(sheetId: string, index: number): Col {
|
|
357
|
+
return this.workbook.sheets[sheetId].cols[index];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
getRow(sheetId: string, index: number): Row {
|
|
361
|
+
return this.workbook.sheets[sheetId].rows[index];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Returns all the cells of a col
|
|
366
|
+
*/
|
|
367
|
+
getColCells(col: number): Cell[] {
|
|
368
|
+
return this.workbook.activeSheet.rows.reduce(
|
|
369
|
+
(acc: Cell[], cur) => (cur.cells[col] ? acc.concat(cur.cells[col]) : acc),
|
|
370
|
+
[]
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
getNumberCols(sheetId: string): number {
|
|
375
|
+
return this.workbook.sheets[sheetId].cols.length;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
getNumberRows(sheetId: string): number {
|
|
379
|
+
return this.workbook.sheets[sheetId].rows.length;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
getColsZone(start: number, end: number): Zone {
|
|
383
|
+
return {
|
|
384
|
+
top: 0,
|
|
385
|
+
bottom: this.workbook.activeSheet.rows.length - 1,
|
|
386
|
+
left: start,
|
|
387
|
+
right: end,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
getRowsZone(start: number, end: number): Zone {
|
|
392
|
+
return {
|
|
393
|
+
top: start,
|
|
394
|
+
bottom: end,
|
|
395
|
+
left: 0,
|
|
396
|
+
right: this.workbook.activeSheet.cols.length - 1,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
getGridSize(): [number, number] {
|
|
401
|
+
const activeSheet = this.workbook.activeSheet;
|
|
402
|
+
const height = activeSheet.rows[activeSheet.rows.length - 1].end + DEFAULT_CELL_HEIGHT + 5;
|
|
403
|
+
const width = activeSheet.cols[activeSheet.cols.length - 1].end + DEFAULT_CELL_WIDTH;
|
|
404
|
+
|
|
405
|
+
return [width, height];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
shouldShowFormulas(): boolean {
|
|
409
|
+
return this.showFormulas;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
getRangeValues(reference: string, defaultSheetId: string): any[][] {
|
|
413
|
+
const [range, sheetName] = reference.split("!").reverse();
|
|
414
|
+
const sheetId = sheetName ? this.sheetIds[sheetName] : defaultSheetId;
|
|
415
|
+
return mapCellsInZone(toZone(range), this.workbook.sheets[sheetId], (cell) => cell.value);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
getRangeFormattedValues(reference: string, defaultSheetId: string): string[][] {
|
|
419
|
+
const [range, sheetName] = reference.split("!").reverse();
|
|
420
|
+
const sheetId = sheetName ? this.sheetIds[sheetName] : defaultSheetId;
|
|
421
|
+
return mapCellsInZone(
|
|
422
|
+
toZone(range),
|
|
423
|
+
this.workbook.sheets[sheetId],
|
|
424
|
+
this.getters.getCellText,
|
|
425
|
+
""
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
// Row/Col manipulation
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
private getColMaxWidth(index: number): number {
|
|
434
|
+
const cells = this.workbook.activeSheet.rows.reduce(
|
|
435
|
+
(acc: Cell[], cur) => (cur.cells[index] ? acc.concat(cur.cells[index]) : acc),
|
|
436
|
+
[]
|
|
437
|
+
);
|
|
438
|
+
const sizes = cells.map(this.getters.getCellWidth);
|
|
439
|
+
return Math.max(0, ...sizes);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private getRowMaxHeight(index: number): number {
|
|
443
|
+
const cells = Object.values(this.workbook.activeSheet.rows[index].cells);
|
|
444
|
+
const sizes = cells.map(this.getters.getCellHeight);
|
|
445
|
+
return Math.max(0, ...sizes);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private setColSize(sheetId: string, index: number, size: number) {
|
|
449
|
+
const cols = this.workbook.sheets[sheetId].cols;
|
|
450
|
+
const col = cols[index];
|
|
451
|
+
const delta = size - col.size;
|
|
452
|
+
this.history.updateState(["sheets", sheetId, "cols", index, "size"], size);
|
|
453
|
+
this.history.updateState(["sheets", sheetId, "cols", index, "end"], col.end + delta);
|
|
454
|
+
for (let i = index + 1; i < cols.length; i++) {
|
|
455
|
+
const col = cols[i];
|
|
456
|
+
this.history.updateState(["sheets", sheetId, "cols", i, "start"], col.start + delta);
|
|
457
|
+
this.history.updateState(["sheets", sheetId, "cols", i, "end"], col.end + delta);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private setRowSize(sheetId: string, index: number, size: number) {
|
|
462
|
+
const rows = this.workbook.sheets[sheetId].rows;
|
|
463
|
+
const row = rows[index];
|
|
464
|
+
const delta = size - row.size;
|
|
465
|
+
this.history.updateState(["sheets", sheetId, "rows", index, "size"], size);
|
|
466
|
+
this.history.updateState(["sheets", sheetId, "rows", index, "end"], row.end + delta);
|
|
467
|
+
for (let i = index + 1; i < rows.length; i++) {
|
|
468
|
+
const row = rows[i];
|
|
469
|
+
this.history.updateState(["sheets", sheetId, "rows", i, "start"], row.start + delta);
|
|
470
|
+
this.history.updateState(["sheets", sheetId, "rows", i, "end"], row.end + delta);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Delete column. This requires a lot of handling:
|
|
476
|
+
* - Update all the formulas in all sheets
|
|
477
|
+
* - Move the cells
|
|
478
|
+
* - Update the cols/rows (size, number, (cells), ...)
|
|
479
|
+
* - Reevaluate the cells
|
|
480
|
+
*
|
|
481
|
+
* @param sheetID ID of the sheet on which deletion should be applied
|
|
482
|
+
* @param columns Columns to delete
|
|
483
|
+
*/
|
|
484
|
+
private removeColumns(sheetID: string, columns: number[]) {
|
|
485
|
+
// This is necessary because we have to delete elements in correct order:
|
|
486
|
+
// begin with the end.
|
|
487
|
+
columns.sort((a, b) => b - a);
|
|
488
|
+
for (let column of columns) {
|
|
489
|
+
// Update all the formulas.
|
|
490
|
+
this.updateColumnsFormulas(column, -1, sheetID);
|
|
491
|
+
|
|
492
|
+
// Move the cells.
|
|
493
|
+
this.moveCellsHorizontally(column, -1, sheetID);
|
|
494
|
+
|
|
495
|
+
// Effectively delete the element and recompute the left-right.
|
|
496
|
+
this.manageColumnsHeaders(column, -1, sheetID);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Delete row. This requires a lot of handling:
|
|
502
|
+
* - Update the merges
|
|
503
|
+
* - Update all the formulas in all sheets
|
|
504
|
+
* - Move the cells
|
|
505
|
+
* - Update the cols/rows (size, number, (cells), ...)
|
|
506
|
+
* - Reevaluate the cells
|
|
507
|
+
*
|
|
508
|
+
* @param sheetID ID of the sheet on which deletion should be applied
|
|
509
|
+
* @param rows Rows to delete
|
|
510
|
+
*/
|
|
511
|
+
private removeRows(sheetID: string, rows: number[]) {
|
|
512
|
+
// This is necessary because we have to delete elements in correct order:
|
|
513
|
+
// begin with the end.
|
|
514
|
+
rows.sort((a, b) => b - a);
|
|
515
|
+
|
|
516
|
+
const consecutiveRows = rows.reduce((groups, currentRow, index, rows) => {
|
|
517
|
+
if (currentRow - rows[index - 1] === -1) {
|
|
518
|
+
const lastGroup = groups[groups.length - 1];
|
|
519
|
+
lastGroup.push(currentRow);
|
|
520
|
+
} else {
|
|
521
|
+
groups.push([currentRow]);
|
|
522
|
+
}
|
|
523
|
+
return groups;
|
|
524
|
+
}, [] as number[][]);
|
|
525
|
+
|
|
526
|
+
for (let group of consecutiveRows) {
|
|
527
|
+
// Update all the formulas.
|
|
528
|
+
this.updateRowsFormulas(group[0], -group.length, sheetID);
|
|
529
|
+
|
|
530
|
+
// Move the cells.
|
|
531
|
+
this.moveCellVerticallyBatched(group[group.length - 1], group[0], sheetID);
|
|
532
|
+
|
|
533
|
+
// Effectively delete the element and recompute the left-right/top-bottom.
|
|
534
|
+
group.map((row) => this.processRowsHeaderDelete(row, sheetID));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private addColumns(
|
|
539
|
+
sheetID: string,
|
|
540
|
+
column: number,
|
|
541
|
+
position: "before" | "after",
|
|
542
|
+
quantity: number
|
|
543
|
+
) {
|
|
544
|
+
// Update all the formulas.
|
|
545
|
+
this.updateColumnsFormulas(position === "before" ? column - 1 : column, quantity, sheetID);
|
|
546
|
+
|
|
547
|
+
// Move the cells.
|
|
548
|
+
this.moveCellsHorizontally(position === "before" ? column : column + 1, quantity, sheetID);
|
|
549
|
+
|
|
550
|
+
// Recompute the left-right/top-bottom.
|
|
551
|
+
this.manageColumnsHeaders(column, quantity, sheetID);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private addRows(sheetID: string, row: number, position: "before" | "after", quantity: number) {
|
|
555
|
+
for (let i = 0; i < quantity; i++) {
|
|
556
|
+
this.addEmptyRow();
|
|
557
|
+
}
|
|
558
|
+
// Update all the formulas.
|
|
559
|
+
this.updateRowsFormulas(position === "before" ? row - 1 : row, quantity, sheetID);
|
|
560
|
+
|
|
561
|
+
// Move the cells.
|
|
562
|
+
this.moveCellsVertically(position === "before" ? row : row + 1, quantity, sheetID);
|
|
563
|
+
|
|
564
|
+
// Recompute the left-right/top-bottom.
|
|
565
|
+
this.processRowsHeaderAdd(row, quantity);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private moveCellsHorizontally(base: number, step: number, sheetID: string) {
|
|
569
|
+
return this.processCellsToMove(
|
|
570
|
+
(cell) => cell.col >= base,
|
|
571
|
+
(cell) => cell.col !== base || step !== -1,
|
|
572
|
+
(cell) => {
|
|
573
|
+
return {
|
|
574
|
+
type: "UPDATE_CELL",
|
|
575
|
+
sheet: sheetID,
|
|
576
|
+
col: cell.col + step,
|
|
577
|
+
row: cell.row,
|
|
578
|
+
content: cell.content,
|
|
579
|
+
border: cell.border,
|
|
580
|
+
style: cell.style,
|
|
581
|
+
format: cell.format,
|
|
582
|
+
};
|
|
583
|
+
},
|
|
584
|
+
sheetID
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Move all the cells that are from the row under `deleteToRow` up to `deleteFromRow`
|
|
590
|
+
*
|
|
591
|
+
* b.e.
|
|
592
|
+
* move vertically with delete from 3 and delete to 5 will first clear all the cells from lines 3 to 5,
|
|
593
|
+
* then take all the row starting at index 6 and add them back at index 3
|
|
594
|
+
*
|
|
595
|
+
* @param deleteFromRow the row index from which to start deleting
|
|
596
|
+
* @param deleteToRow the row index until which the deleting must continue
|
|
597
|
+
* @param the sheet from which to remove
|
|
598
|
+
*/
|
|
599
|
+
private moveCellVerticallyBatched(deleteFromRow: number, deleteToRow: number, sheetID: string) {
|
|
600
|
+
return this.processCellsToMove(
|
|
601
|
+
({ row }) => row >= deleteFromRow,
|
|
602
|
+
({ row }) => row > deleteToRow,
|
|
603
|
+
(cell) => {
|
|
604
|
+
return {
|
|
605
|
+
type: "UPDATE_CELL",
|
|
606
|
+
sheet: this.workbook.sheets[sheetID].id,
|
|
607
|
+
col: cell.col,
|
|
608
|
+
row: cell.row - (deleteToRow - deleteFromRow + 1),
|
|
609
|
+
content: cell.content,
|
|
610
|
+
border: cell.border,
|
|
611
|
+
style: cell.style,
|
|
612
|
+
format: cell.format,
|
|
613
|
+
};
|
|
614
|
+
},
|
|
615
|
+
sheetID
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private moveCellsVertically(base: number, step: number, sheetID: string) {
|
|
620
|
+
return this.processCellsToMove(
|
|
621
|
+
(cell) => cell.row >= base,
|
|
622
|
+
(cell) => cell.row !== base || step !== -1,
|
|
623
|
+
(cell) => {
|
|
624
|
+
return {
|
|
625
|
+
type: "UPDATE_CELL",
|
|
626
|
+
sheet: sheetID,
|
|
627
|
+
col: cell.col,
|
|
628
|
+
row: cell.row + step,
|
|
629
|
+
content: cell.content,
|
|
630
|
+
border: cell.border,
|
|
631
|
+
style: cell.style,
|
|
632
|
+
format: cell.format,
|
|
633
|
+
};
|
|
634
|
+
},
|
|
635
|
+
sheetID
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private manageColumnsHeaders(base: number, step: number, sheetID: string) {
|
|
640
|
+
const cols: Col[] = [];
|
|
641
|
+
let start = 0;
|
|
642
|
+
let colIndex = 0;
|
|
643
|
+
const sheet = this.workbook.sheets[sheetID];
|
|
644
|
+
for (let i in sheet.cols) {
|
|
645
|
+
if (parseInt(i, 10) === base) {
|
|
646
|
+
if (step !== -1) {
|
|
647
|
+
const { size } = sheet.cols[colIndex];
|
|
648
|
+
for (let a = 0; a < step; a++) {
|
|
649
|
+
cols.push({
|
|
650
|
+
name: numberToLetters(colIndex),
|
|
651
|
+
size,
|
|
652
|
+
start,
|
|
653
|
+
end: start + size,
|
|
654
|
+
});
|
|
655
|
+
start += size;
|
|
656
|
+
colIndex++;
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const { size } = sheet.cols[i];
|
|
663
|
+
cols.push({
|
|
664
|
+
name: numberToLetters(colIndex),
|
|
665
|
+
size,
|
|
666
|
+
start,
|
|
667
|
+
end: start + size,
|
|
668
|
+
});
|
|
669
|
+
start += size;
|
|
670
|
+
colIndex++;
|
|
671
|
+
}
|
|
672
|
+
this.history.updateState(["sheets", sheetID, "cols"], cols);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private processRowsHeaderDelete(index: number, sheetID: string) {
|
|
676
|
+
const rows: Row[] = [];
|
|
677
|
+
let start = 0;
|
|
678
|
+
let rowIndex = 0;
|
|
679
|
+
const sheet = this.workbook.sheets[sheetID];
|
|
680
|
+
const cellsQueue = sheet.rows.map((row) => row.cells);
|
|
681
|
+
for (let i in sheet.rows) {
|
|
682
|
+
const row = sheet.rows[i];
|
|
683
|
+
const { size } = row;
|
|
684
|
+
if (parseInt(i, 10) === index) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
rowIndex++;
|
|
688
|
+
rows.push({
|
|
689
|
+
start,
|
|
690
|
+
end: start + size,
|
|
691
|
+
size,
|
|
692
|
+
cells: cellsQueue.shift()!,
|
|
693
|
+
name: String(rowIndex),
|
|
694
|
+
});
|
|
695
|
+
start += size;
|
|
696
|
+
}
|
|
697
|
+
this.history.updateState(["sheets", sheetID, "rows"], rows);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private processRowsHeaderAdd(index: number, quantity: number) {
|
|
701
|
+
const rows: Row[] = [];
|
|
702
|
+
let start = 0;
|
|
703
|
+
let rowIndex = 0;
|
|
704
|
+
let sizeIndex = 0;
|
|
705
|
+
const cellsQueue = this.workbook.activeSheet.rows.map((row) => row.cells);
|
|
706
|
+
for (let i in this.workbook.activeSheet.rows) {
|
|
707
|
+
const { size } = this.workbook.activeSheet.rows[sizeIndex];
|
|
708
|
+
if (parseInt(i, 10) < index || parseInt(i, 10) >= index + quantity) {
|
|
709
|
+
sizeIndex++;
|
|
710
|
+
}
|
|
711
|
+
rowIndex++;
|
|
712
|
+
rows.push({
|
|
713
|
+
start,
|
|
714
|
+
end: start + size,
|
|
715
|
+
size,
|
|
716
|
+
cells: cellsQueue.shift()!,
|
|
717
|
+
name: String(rowIndex),
|
|
718
|
+
});
|
|
719
|
+
start += size;
|
|
720
|
+
}
|
|
721
|
+
this.history.updateState(["activeSheet", "rows"], rows);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private addEmptyRow() {
|
|
725
|
+
const lastEnd = this.workbook.activeSheet.rows[this.workbook.activeSheet.rows.length - 1].end;
|
|
726
|
+
const name = (this.workbook.activeSheet.rows.length + 1).toString();
|
|
727
|
+
const newRows: Row[] = this.workbook.activeSheet.rows.slice();
|
|
728
|
+
const size = 0;
|
|
729
|
+
newRows.push({
|
|
730
|
+
start: lastEnd,
|
|
731
|
+
end: lastEnd + size,
|
|
732
|
+
size,
|
|
733
|
+
name,
|
|
734
|
+
cells: {},
|
|
735
|
+
});
|
|
736
|
+
const path = ["activeSheet", "rows"];
|
|
737
|
+
this.history.updateState(path, newRows);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private updateColumnsFormulas(base: number, step: number, sheetID: string) {
|
|
741
|
+
return this.visitFormulas(
|
|
742
|
+
this.workbook.sheets[sheetID].name,
|
|
743
|
+
(value: string, sheet: string | undefined): string => {
|
|
744
|
+
if (value.includes(":")) {
|
|
745
|
+
return this.updateColumnsRange(value, sheet, base, step);
|
|
746
|
+
}
|
|
747
|
+
return this.updateColumnsRef(value, sheet, base, step);
|
|
748
|
+
}
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
private updateRowsFormulas(base: number, step: number, sheetID: string) {
|
|
753
|
+
return this.visitFormulas(
|
|
754
|
+
this.workbook.sheets[sheetID].name,
|
|
755
|
+
(value: string, sheet: string | undefined): string => {
|
|
756
|
+
if (value.includes(":")) {
|
|
757
|
+
return this.updateRowsRange(value, sheet, base, step);
|
|
758
|
+
}
|
|
759
|
+
return this.updateRowsRef(value, sheet, base, step);
|
|
760
|
+
}
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private processCellsToMove(
|
|
765
|
+
shouldDelete: (cell: Cell) => boolean,
|
|
766
|
+
shouldAdd: (cell: Cell) => boolean,
|
|
767
|
+
buildCellToAdd: (cell: Cell) => Command,
|
|
768
|
+
sheetId
|
|
769
|
+
) {
|
|
770
|
+
const deleteCommands: Command[] = [];
|
|
771
|
+
const addCommands: Command[] = [];
|
|
772
|
+
|
|
773
|
+
const sheet = this.workbook.sheets[sheetId];
|
|
774
|
+
|
|
775
|
+
for (let xc in sheet.cells) {
|
|
776
|
+
let cell = sheet.cells[xc];
|
|
777
|
+
if (shouldDelete(cell)) {
|
|
778
|
+
const [col, row] = toCartesian(xc);
|
|
779
|
+
deleteCommands.push({
|
|
780
|
+
type: "CLEAR_CELL",
|
|
781
|
+
sheet: sheet.id,
|
|
782
|
+
col,
|
|
783
|
+
row,
|
|
784
|
+
});
|
|
785
|
+
if (shouldAdd(cell)) {
|
|
786
|
+
addCommands.push(buildCellToAdd(cell));
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
for (let cmd of deleteCommands) {
|
|
791
|
+
this.dispatch(cmd.type, cmd);
|
|
792
|
+
}
|
|
793
|
+
for (let cmd of addCommands) {
|
|
794
|
+
this.dispatch(cmd.type, cmd);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
// ---------------------------------------------------------------------------
|
|
798
|
+
// Cells
|
|
799
|
+
// ---------------------------------------------------------------------------
|
|
800
|
+
|
|
801
|
+
private updateCell(sheet: string, col: number, row: number, data: CellData) {
|
|
802
|
+
const _sheet = this.workbook.sheets[sheet];
|
|
803
|
+
const current = _sheet.rows[row].cells[col];
|
|
804
|
+
const xc = (current && current.xc) || toXC(col, row);
|
|
805
|
+
const hasContent = "content" in data;
|
|
806
|
+
|
|
807
|
+
// Compute the new cell properties
|
|
808
|
+
const dataContent = data.content ? data.content.replace(nbspRegexp, "") : "";
|
|
809
|
+
let content = hasContent ? dataContent : (current && current.content) || "";
|
|
810
|
+
const style = "style" in data ? data.style : (current && current.style) || 0;
|
|
811
|
+
const border = "border" in data ? data.border : (current && current.border) || 0;
|
|
812
|
+
let format = "format" in data ? data.format : (current && current.format) || "";
|
|
813
|
+
|
|
814
|
+
// if all are empty, we need to delete the underlying cell object
|
|
815
|
+
if (!content && !style && !border && !format) {
|
|
816
|
+
if (current) {
|
|
817
|
+
// todo: make this work on other sheets
|
|
818
|
+
this.history.updateSheet(_sheet, ["cells", xc], undefined);
|
|
819
|
+
this.history.updateSheet(_sheet, ["rows", row, "cells", col], undefined);
|
|
820
|
+
}
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// compute the new cell value
|
|
825
|
+
const didContentChange =
|
|
826
|
+
(!current && dataContent) || (hasContent && current && current.content !== dataContent);
|
|
827
|
+
let cell: Cell;
|
|
828
|
+
if (current && !didContentChange) {
|
|
829
|
+
cell = { col, row, xc, content, value: current.value, type: current.type };
|
|
830
|
+
if (cell.type === "formula") {
|
|
831
|
+
cell.error = current.error;
|
|
832
|
+
cell.pending = current.pending;
|
|
833
|
+
cell.formula = current.formula;
|
|
834
|
+
}
|
|
835
|
+
} else {
|
|
836
|
+
// the current content cannot be reused, so we need to recompute the
|
|
837
|
+
// derived values
|
|
838
|
+
let type: Cell["type"] = content[0] === "=" ? "formula" : "text";
|
|
839
|
+
let value: Cell["value"] = content;
|
|
840
|
+
if (isNumber(content)) {
|
|
841
|
+
value = parseNumber(content);
|
|
842
|
+
type = "number";
|
|
843
|
+
if (content.includes("%")) {
|
|
844
|
+
format = content.includes(".") ? "0.00%" : "0%";
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
let date = parseDateTime(content);
|
|
848
|
+
if (date) {
|
|
849
|
+
type = "date";
|
|
850
|
+
value = date;
|
|
851
|
+
content = formatDateTime(date);
|
|
852
|
+
}
|
|
853
|
+
const contentUpperCase = content.toUpperCase();
|
|
854
|
+
if (contentUpperCase === "TRUE") {
|
|
855
|
+
value = true;
|
|
856
|
+
}
|
|
857
|
+
if (contentUpperCase === "FALSE") {
|
|
858
|
+
value = false;
|
|
859
|
+
}
|
|
860
|
+
cell = { col, row, xc, content, value, type };
|
|
861
|
+
if (cell.type === "formula") {
|
|
862
|
+
cell.error = undefined;
|
|
863
|
+
try {
|
|
864
|
+
let formulaString: NormalizedFormula = normalize(cell.content || "");
|
|
865
|
+
let compiledFormula = compile(formulaString);
|
|
866
|
+
cell.formula = {
|
|
867
|
+
compiledFormula: compiledFormula,
|
|
868
|
+
dependencies: formulaString.dependencies,
|
|
869
|
+
text: formulaString.text,
|
|
870
|
+
};
|
|
871
|
+
} catch (e) {
|
|
872
|
+
cell.value = "#BAD_EXPR";
|
|
873
|
+
cell.error = e.message || _lt("Invalid Expression");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (style) {
|
|
878
|
+
cell.style = style;
|
|
879
|
+
}
|
|
880
|
+
if (border) {
|
|
881
|
+
cell.border = border;
|
|
882
|
+
}
|
|
883
|
+
if (format) {
|
|
884
|
+
cell.format = format;
|
|
885
|
+
}
|
|
886
|
+
// todo: make this work on other sheets
|
|
887
|
+
this.history.updateSheet(_sheet, ["cells", xc], cell);
|
|
888
|
+
this.history.updateSheet(_sheet, ["rows", row, "cells", col], cell);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private generateSheetName(): string {
|
|
892
|
+
let i = 1;
|
|
893
|
+
const names = this.getSheets().map((s) => s.name);
|
|
894
|
+
const baseName = _lt("Sheet");
|
|
895
|
+
let name = `${baseName}${i}`;
|
|
896
|
+
while (names.includes(name)) {
|
|
897
|
+
name = `${baseName}${i}`;
|
|
898
|
+
i++;
|
|
899
|
+
}
|
|
900
|
+
return name;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private createSheet(id: string, name: string, cols: number, rows: number): string {
|
|
904
|
+
const sheet: Sheet = {
|
|
905
|
+
id,
|
|
906
|
+
name,
|
|
907
|
+
cells: {},
|
|
908
|
+
colNumber: cols,
|
|
909
|
+
rowNumber: rows,
|
|
910
|
+
cols: createDefaultCols(cols),
|
|
911
|
+
rows: createDefaultRows(rows),
|
|
912
|
+
merges: {},
|
|
913
|
+
mergeCellMap: {},
|
|
914
|
+
};
|
|
915
|
+
const visibleSheets = this.workbook.visibleSheets.slice();
|
|
916
|
+
const index = visibleSheets.findIndex((id) => this.workbook.activeSheet.id === id);
|
|
917
|
+
visibleSheets.splice(index + 1, 0, sheet.id);
|
|
918
|
+
const sheets = this.workbook.sheets;
|
|
919
|
+
this.history.updateState(["visibleSheets"], visibleSheets);
|
|
920
|
+
this.history.updateState(["sheets"], Object.assign({}, sheets, { [sheet.id]: sheet }));
|
|
921
|
+
return sheet.id;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private moveSheet(sheetId: string, direction: "left" | "right") {
|
|
925
|
+
const visibleSheets = this.workbook.visibleSheets.slice();
|
|
926
|
+
const currentIndex = visibleSheets.findIndex((id) => id === sheetId);
|
|
927
|
+
const sheet = visibleSheets.splice(currentIndex, 1);
|
|
928
|
+
visibleSheets.splice(currentIndex + (direction === "left" ? -1 : 1), 0, sheet[0]);
|
|
929
|
+
this.history.updateState(["visibleSheets"], visibleSheets);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
private isRenameAllowed(cmd: RenameSheetCommand): CommandResult {
|
|
933
|
+
if (cmd.interactive) {
|
|
934
|
+
return { status: "SUCCESS" };
|
|
935
|
+
}
|
|
936
|
+
const name = cmd.name && cmd.name.trim().toLowerCase();
|
|
937
|
+
if (!name) {
|
|
938
|
+
return { status: "CANCELLED", reason: CancelledReason.WrongSheetName };
|
|
939
|
+
}
|
|
940
|
+
return this.workbook.visibleSheets.findIndex(
|
|
941
|
+
(id) => this.workbook.sheets[id].name.toLowerCase() === name
|
|
942
|
+
) === -1
|
|
943
|
+
? { status: "SUCCESS" }
|
|
944
|
+
: { status: "CANCELLED", reason: CancelledReason.WrongSheetName };
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
private interactiveRenameSheet(sheet: string, title: string) {
|
|
948
|
+
const placeholder = this.getSheetName(sheet)!;
|
|
949
|
+
this.ui.editText(title, placeholder, (name: string | null) => {
|
|
950
|
+
if (!name) {
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const result = this.dispatch("RENAME_SHEET", { sheet, name });
|
|
954
|
+
const sheetName = this.getSheetName(sheet);
|
|
955
|
+
if (result.status === "CANCELLED" && sheetName !== name) {
|
|
956
|
+
this.interactiveRenameSheet(sheet, _lt("Please enter a valid sheet name"));
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
private renameSheet(sheetId: string, name: string) {
|
|
962
|
+
const sheet = this.workbook.sheets[sheetId];
|
|
963
|
+
const oldName = sheet.name;
|
|
964
|
+
this.history.updateSheet(sheet, ["name"], name.trim());
|
|
965
|
+
const sheetIds = Object.assign({}, this.sheetIds);
|
|
966
|
+
sheetIds[name] = sheet.id;
|
|
967
|
+
delete sheetIds[oldName];
|
|
968
|
+
this.history.updateLocalState(["sheetIds"], sheetIds);
|
|
969
|
+
this.visitAllFormulasSymbols((value: string) => {
|
|
970
|
+
let [val, sheetRef] = value.split("!").reverse();
|
|
971
|
+
if (sheetRef) {
|
|
972
|
+
sheetRef = getUnquotedSheetName(sheetRef);
|
|
973
|
+
if (sheetRef === oldName) {
|
|
974
|
+
if (val.includes(":")) {
|
|
975
|
+
return this.updateRange(val, 0, 0, sheet.id);
|
|
976
|
+
}
|
|
977
|
+
return this.updateReference(val, 0, 0, sheet.id);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return value;
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
private duplicateSheet(fromId: string, toId: string, toName: string) {
|
|
985
|
+
const sheet = this.workbook.sheets[fromId];
|
|
986
|
+
const newSheet = JSON.parse(JSON.stringify(sheet));
|
|
987
|
+
newSheet.id = toId;
|
|
988
|
+
newSheet.name = toName;
|
|
989
|
+
const visibleSheets = this.workbook.visibleSheets.slice();
|
|
990
|
+
const currentIndex = visibleSheets.findIndex((id) => id === fromId);
|
|
991
|
+
visibleSheets.splice(currentIndex + 1, 0, newSheet.id);
|
|
992
|
+
this.history.updateState(["visibleSheets"], visibleSheets);
|
|
993
|
+
this.history.updateState(
|
|
994
|
+
["sheets"],
|
|
995
|
+
Object.assign({}, this.workbook.sheets, { [newSheet.id]: newSheet })
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
const sheetIds = Object.assign({}, this.sheetIds);
|
|
999
|
+
sheetIds[newSheet.name] = newSheet.id;
|
|
1000
|
+
this.history.updateLocalState(["sheetIds"], sheetIds);
|
|
1001
|
+
this.dispatch("ACTIVATE_SHEET", { from: this.workbook.activeSheet.id, to: toId });
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private interactiveDeleteSheet(sheetId: string) {
|
|
1005
|
+
this.ui.askConfirmation(_lt("Are you sure you want to delete this sheet ?"), () => {
|
|
1006
|
+
this.dispatch("DELETE_SHEET", { sheet: sheetId });
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
private deleteSheet(sheetId: string) {
|
|
1011
|
+
const name = this.workbook.sheets[sheetId].name;
|
|
1012
|
+
const sheets = Object.assign({}, this.workbook.sheets);
|
|
1013
|
+
delete sheets[sheetId];
|
|
1014
|
+
this.history.updateState(["sheets"], sheets);
|
|
1015
|
+
|
|
1016
|
+
const visibleSheets = this.workbook.visibleSheets.slice();
|
|
1017
|
+
const currentIndex = visibleSheets.findIndex((id) => id === sheetId);
|
|
1018
|
+
visibleSheets.splice(currentIndex, 1);
|
|
1019
|
+
this.history.updateState(["visibleSheets"], visibleSheets);
|
|
1020
|
+
|
|
1021
|
+
const sheetIds = Object.assign({}, this.sheetIds);
|
|
1022
|
+
delete sheetIds[name];
|
|
1023
|
+
this.history.updateLocalState(["sheetIds"], sheetIds);
|
|
1024
|
+
this.visitAllFormulasSymbols((value: string) => {
|
|
1025
|
+
let [, sheetRef] = value.split("!").reverse();
|
|
1026
|
+
if (sheetRef) {
|
|
1027
|
+
sheetRef = getUnquotedSheetName(sheetRef);
|
|
1028
|
+
if (sheetRef === name) {
|
|
1029
|
+
return "#REF";
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return value;
|
|
1033
|
+
});
|
|
1034
|
+
if (this.getActiveSheet() === sheetId) {
|
|
1035
|
+
this.dispatch("ACTIVATE_SHEET", {
|
|
1036
|
+
from: sheetId,
|
|
1037
|
+
to: visibleSheets[Math.max(0, currentIndex - 1)],
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
private clearZones(sheet: string, zones: Zone[]) {
|
|
1043
|
+
// TODO: get cells from the actual sheet
|
|
1044
|
+
const cells = this.workbook.activeSheet.cells;
|
|
1045
|
+
for (let zone of zones) {
|
|
1046
|
+
for (let col = zone.left; col <= zone.right; col++) {
|
|
1047
|
+
for (let row = zone.top; row <= zone.bottom; row++) {
|
|
1048
|
+
const xc = toXC(col, row);
|
|
1049
|
+
if (xc in cells) {
|
|
1050
|
+
this.dispatch("UPDATE_CELL", {
|
|
1051
|
+
sheet,
|
|
1052
|
+
content: "",
|
|
1053
|
+
col,
|
|
1054
|
+
row,
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// ---------------------------------------------------------------------------
|
|
1063
|
+
// Cols/Rows addition/deletion offsets manipulation
|
|
1064
|
+
// ---------------------------------------------------------------------------
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Update a reference by applying an offset to the column
|
|
1068
|
+
*
|
|
1069
|
+
* @param ref Reference to update
|
|
1070
|
+
* @param sheet Id of the sheet, if cross-sheet reference
|
|
1071
|
+
* @param base Index of the element added/removed
|
|
1072
|
+
* @param step Number of elements added or -1 if removed
|
|
1073
|
+
*/
|
|
1074
|
+
private updateColumnsRef = (
|
|
1075
|
+
ref: string,
|
|
1076
|
+
sheet: string | undefined,
|
|
1077
|
+
base: number,
|
|
1078
|
+
step: number
|
|
1079
|
+
): string => {
|
|
1080
|
+
let x = toCartesian(ref)[0];
|
|
1081
|
+
if (x === base && step === -1) {
|
|
1082
|
+
return "#REF";
|
|
1083
|
+
}
|
|
1084
|
+
return this.updateReference(ref, x > base ? step : 0, 0, this.getSheetIdByName(sheet), false);
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Update a full range by appling an offset.
|
|
1089
|
+
*
|
|
1090
|
+
* @param ref Reference to update
|
|
1091
|
+
* @param sheet Id of the sheet, if cross-sheet reference
|
|
1092
|
+
* @param base Index of the element added/removed
|
|
1093
|
+
* @param step Number of elements added or -1 if removed
|
|
1094
|
+
*/
|
|
1095
|
+
private updateColumnsRange = (
|
|
1096
|
+
ref: string,
|
|
1097
|
+
sheet: string | undefined,
|
|
1098
|
+
base: number,
|
|
1099
|
+
step: number
|
|
1100
|
+
): string => {
|
|
1101
|
+
const currentZone = toZone(ref);
|
|
1102
|
+
const recomputedRefs =
|
|
1103
|
+
step < 0
|
|
1104
|
+
? recomputeZones(
|
|
1105
|
+
[ref],
|
|
1106
|
+
[this.zoneToXC({ ...currentZone, left: base + step + 1, right: base })]
|
|
1107
|
+
)
|
|
1108
|
+
: [ref];
|
|
1109
|
+
|
|
1110
|
+
if (recomputedRefs.length === 0) {
|
|
1111
|
+
return "#REF";
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const leftZone = toZone(recomputedRefs[0]);
|
|
1115
|
+
|
|
1116
|
+
const rightZone =
|
|
1117
|
+
step < 0 && currentZone.left < base + step + 1 && base < currentZone.right
|
|
1118
|
+
? toZone(recomputedRefs[1])
|
|
1119
|
+
: leftZone;
|
|
1120
|
+
|
|
1121
|
+
const left = base < leftZone.left ? leftZone.left + step : leftZone.left;
|
|
1122
|
+
const right = base < rightZone.right ? rightZone.right + step : rightZone.right;
|
|
1123
|
+
|
|
1124
|
+
const [leftXc, rightXc] = ref.split(":");
|
|
1125
|
+
const sheetId = this.getters.getSheetIdByName(sheet);
|
|
1126
|
+
const newLeft = this.updateReference(leftXc, left - currentZone.left, 0, sheetId, false, false);
|
|
1127
|
+
const newRight = this.updateReference(
|
|
1128
|
+
rightXc,
|
|
1129
|
+
right - currentZone.right,
|
|
1130
|
+
0,
|
|
1131
|
+
sheetId,
|
|
1132
|
+
false,
|
|
1133
|
+
false
|
|
1134
|
+
);
|
|
1135
|
+
if (newLeft === newRight) {
|
|
1136
|
+
return newLeft;
|
|
1137
|
+
}
|
|
1138
|
+
//As updateReference put the sheet in the ref, we need to remove it from the right part
|
|
1139
|
+
return `${newLeft}:${newRight.split("!").pop()!}`;
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Update a reference by applying an offset to the row
|
|
1144
|
+
*
|
|
1145
|
+
* @param ref Reference to update
|
|
1146
|
+
* @param sheet Id of the sheet, if cross-sheet reference
|
|
1147
|
+
* @param base Index of the element added/removed
|
|
1148
|
+
* @param step Number of elements added or -1 if removed
|
|
1149
|
+
*/
|
|
1150
|
+
private updateRowsRef = (
|
|
1151
|
+
ref: string,
|
|
1152
|
+
sheet: string | undefined,
|
|
1153
|
+
base: number,
|
|
1154
|
+
step: number
|
|
1155
|
+
): string => {
|
|
1156
|
+
let y = toCartesian(ref)[1];
|
|
1157
|
+
if (base + step < y && y <= base) {
|
|
1158
|
+
return "#REF";
|
|
1159
|
+
}
|
|
1160
|
+
return this.updateReference(ref, 0, y > base ? step : 0, this.getSheetIdByName(sheet), false);
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Update a full range by appling an offset.
|
|
1165
|
+
*
|
|
1166
|
+
* @param ref Reference to update
|
|
1167
|
+
* @param sheet Id of the sheet, if cross-sheet reference
|
|
1168
|
+
* @param base Index of the element added/removed
|
|
1169
|
+
* @param step Number of elements added/removed (negative when removed)
|
|
1170
|
+
*/
|
|
1171
|
+
private updateRowsRange = (
|
|
1172
|
+
ref: string,
|
|
1173
|
+
sheet: string | undefined,
|
|
1174
|
+
base: number,
|
|
1175
|
+
step: number
|
|
1176
|
+
): string => {
|
|
1177
|
+
const currentZone = toZone(ref);
|
|
1178
|
+
// base+step+1 === left element of the zone to remove
|
|
1179
|
+
const recomputedRefs =
|
|
1180
|
+
step < 0
|
|
1181
|
+
? recomputeZones(
|
|
1182
|
+
[ref],
|
|
1183
|
+
[this.zoneToXC({ ...currentZone, top: base + step + 1, bottom: base })]
|
|
1184
|
+
)
|
|
1185
|
+
: [ref];
|
|
1186
|
+
|
|
1187
|
+
if (recomputedRefs.length === 0) {
|
|
1188
|
+
return "#REF";
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const topZone = toZone(recomputedRefs[0]);
|
|
1192
|
+
|
|
1193
|
+
// base+step+1 === top element of the zone to remove
|
|
1194
|
+
const bottomZone =
|
|
1195
|
+
step < 0 && currentZone.top < base + step + 1 && base < currentZone.bottom
|
|
1196
|
+
? toZone(recomputedRefs[1])
|
|
1197
|
+
: topZone;
|
|
1198
|
+
|
|
1199
|
+
const top = base < topZone.top ? topZone.top + step : topZone.top;
|
|
1200
|
+
const bottom = base < bottomZone.bottom ? bottomZone.bottom + step : bottomZone.bottom;
|
|
1201
|
+
|
|
1202
|
+
const [left, right] = ref.split(":");
|
|
1203
|
+
const sheetId = this.getters.getSheetIdByName(sheet);
|
|
1204
|
+
const newLeft = this.updateReference(left, 0, top - currentZone.top, sheetId, false, false);
|
|
1205
|
+
const newRight = this.updateReference(
|
|
1206
|
+
right,
|
|
1207
|
+
0,
|
|
1208
|
+
bottom - currentZone.bottom,
|
|
1209
|
+
sheetId,
|
|
1210
|
+
false,
|
|
1211
|
+
false
|
|
1212
|
+
);
|
|
1213
|
+
if (newLeft === newRight) {
|
|
1214
|
+
return newLeft;
|
|
1215
|
+
}
|
|
1216
|
+
//As updateReference put the sheet in the ref, we need to remove it from the right part
|
|
1217
|
+
return `${newLeft}:${newRight.split("!").pop()!}`;
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
// ---------------------------------------------------------------------------
|
|
1221
|
+
// Helpers
|
|
1222
|
+
// ---------------------------------------------------------------------------
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Update a range with some offsets
|
|
1226
|
+
*/
|
|
1227
|
+
private updateRange(
|
|
1228
|
+
symbol: string,
|
|
1229
|
+
offsetX: number,
|
|
1230
|
+
offsetY: number,
|
|
1231
|
+
sheetId: string | undefined
|
|
1232
|
+
): string {
|
|
1233
|
+
let [left, right] = symbol.split(":");
|
|
1234
|
+
left = this.updateReference(left, offsetX, offsetY, sheetId, true, false);
|
|
1235
|
+
right = this.updateReference(right, offsetX, offsetY, sheetId, true, false);
|
|
1236
|
+
if (left === "#REF" || right === "#REF") {
|
|
1237
|
+
return "#REF";
|
|
1238
|
+
}
|
|
1239
|
+
//As updateReference put the sheet in the ref, we need to remove it from the right part
|
|
1240
|
+
right = right.split("!").pop()!;
|
|
1241
|
+
return `${left}:${right}`;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Update a reference with some offsets.
|
|
1246
|
+
*/
|
|
1247
|
+
private updateReference(
|
|
1248
|
+
symbol: string,
|
|
1249
|
+
offsetX: number,
|
|
1250
|
+
offsetY: number,
|
|
1251
|
+
sheetId: string | undefined,
|
|
1252
|
+
updateFreeze: boolean = true,
|
|
1253
|
+
checkSheetBoundary: boolean = true
|
|
1254
|
+
): string {
|
|
1255
|
+
const xc = symbol.replace(/\$/g, "");
|
|
1256
|
+
let [x, y] = toCartesian(xc);
|
|
1257
|
+
const freezeCol = symbol.startsWith("$");
|
|
1258
|
+
const freezeRow = symbol.includes("$", 1);
|
|
1259
|
+
x += freezeCol && updateFreeze ? 0 : offsetX;
|
|
1260
|
+
y += freezeRow && updateFreeze ? 0 : offsetY;
|
|
1261
|
+
if (
|
|
1262
|
+
x < 0 ||
|
|
1263
|
+
y < 0 ||
|
|
1264
|
+
(checkSheetBoundary &&
|
|
1265
|
+
(x >= this.getters.getNumberCols(sheetId || this.getters.getActiveSheet()) ||
|
|
1266
|
+
y >= this.getters.getNumberRows(sheetId || this.getters.getActiveSheet())))
|
|
1267
|
+
) {
|
|
1268
|
+
return "#REF";
|
|
1269
|
+
}
|
|
1270
|
+
const sheetName = sheetId && getComposerSheetName(this.getters.getSheetName(sheetId)!);
|
|
1271
|
+
return (
|
|
1272
|
+
(sheetName ? `${sheetName}!` : "") +
|
|
1273
|
+
(freezeCol ? "$" : "") +
|
|
1274
|
+
numberToLetters(x) +
|
|
1275
|
+
(freezeRow ? "$" : "") +
|
|
1276
|
+
String(y + 1)
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
private visitAllFormulasSymbols(cb: (value: string, sheetId: string) => string) {
|
|
1281
|
+
for (let sheetId in this.workbook.sheets) {
|
|
1282
|
+
const sheet = this.workbook.sheets[sheetId];
|
|
1283
|
+
for (let [xc, cell] of Object.entries(sheet.cells)) {
|
|
1284
|
+
if (cell.type === "formula") {
|
|
1285
|
+
const content = rangeTokenize(cell.content!)
|
|
1286
|
+
.map((t) => {
|
|
1287
|
+
if (t.type === "SYMBOL" && cellReference.test(t.value)) {
|
|
1288
|
+
return cb(t.value, sheet.id);
|
|
1289
|
+
}
|
|
1290
|
+
return t.value;
|
|
1291
|
+
})
|
|
1292
|
+
.join("");
|
|
1293
|
+
if (content !== cell.content) {
|
|
1294
|
+
const [col, row] = toCartesian(xc);
|
|
1295
|
+
this.dispatch("UPDATE_CELL", {
|
|
1296
|
+
sheet: sheet.id,
|
|
1297
|
+
col,
|
|
1298
|
+
row,
|
|
1299
|
+
content,
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Apply a function to update the formula on every cells of every sheets which
|
|
1309
|
+
* contains a formula
|
|
1310
|
+
* @param cb Update formula function to apply
|
|
1311
|
+
*/
|
|
1312
|
+
private visitFormulas(
|
|
1313
|
+
sheetNameToFind: string,
|
|
1314
|
+
cb: (value: string, sheet: string | undefined) => string
|
|
1315
|
+
) {
|
|
1316
|
+
this.visitAllFormulasSymbols((content: string, sheetId: string): string => {
|
|
1317
|
+
let [value, sheetRef] = content.split("!").reverse();
|
|
1318
|
+
if (sheetRef) {
|
|
1319
|
+
sheetRef = getUnquotedSheetName(sheetRef);
|
|
1320
|
+
if (sheetRef === sheetNameToFind) {
|
|
1321
|
+
return cb(value, sheetRef);
|
|
1322
|
+
}
|
|
1323
|
+
} else if (this.sheetIds[sheetNameToFind] === sheetId) {
|
|
1324
|
+
return cb(value, undefined);
|
|
1325
|
+
}
|
|
1326
|
+
return content;
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// ---------------------------------------------------------------------------
|
|
1331
|
+
// Import/Export
|
|
1332
|
+
// ---------------------------------------------------------------------------
|
|
1333
|
+
|
|
1334
|
+
import(data: WorkbookData) {
|
|
1335
|
+
// we need to fill the sheetIds mapping first, because otherwise formulas
|
|
1336
|
+
// that depends on a sheet not already imported will not be able to be
|
|
1337
|
+
// compiled
|
|
1338
|
+
for (let sheet of data.sheets) {
|
|
1339
|
+
this.sheetIds[sheet.name] = sheet.id;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
for (let sheet of data.sheets) {
|
|
1343
|
+
this.importSheet(sheet);
|
|
1344
|
+
}
|
|
1345
|
+
this.workbook.activeSheet = this.workbook.sheets[data.activeSheet];
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
importSheet(data: SheetData) {
|
|
1349
|
+
let { sheets, visibleSheets } = this.workbook;
|
|
1350
|
+
const name = data.name || `Sheet${Object.keys(sheets).length + 1}`;
|
|
1351
|
+
const { colNumber, rowNumber } = getSheetSize(data);
|
|
1352
|
+
const sheet: Sheet = {
|
|
1353
|
+
id: data.id,
|
|
1354
|
+
name: name,
|
|
1355
|
+
cells: {},
|
|
1356
|
+
colNumber,
|
|
1357
|
+
rowNumber,
|
|
1358
|
+
cols: createCols(data.cols || {}, colNumber),
|
|
1359
|
+
rows: createRows(data.rows || {}, rowNumber),
|
|
1360
|
+
merges: {},
|
|
1361
|
+
mergeCellMap: {},
|
|
1362
|
+
};
|
|
1363
|
+
visibleSheets = visibleSheets.slice();
|
|
1364
|
+
visibleSheets.push(sheet.id);
|
|
1365
|
+
this.history.updateState(["visibleSheets"], visibleSheets);
|
|
1366
|
+
this.history.updateState(["sheets"], Object.assign({}, sheets, { [sheet.id]: sheet }));
|
|
1367
|
+
// cells
|
|
1368
|
+
for (let xc in data.cells) {
|
|
1369
|
+
const cell = data.cells[xc];
|
|
1370
|
+
const [col, row] = toCartesian(xc);
|
|
1371
|
+
this.updateCell(data.id, col, row, cell);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
export(data: WorkbookData) {
|
|
1376
|
+
data.sheets = this.workbook.visibleSheets.map((id) => {
|
|
1377
|
+
const sheet = this.workbook.sheets[id];
|
|
1378
|
+
const cells: { [key: string]: CellData } = {};
|
|
1379
|
+
for (let [key, cell] of Object.entries(sheet.cells)) {
|
|
1380
|
+
cells[key] = {
|
|
1381
|
+
content: cell.content,
|
|
1382
|
+
border: cell.border,
|
|
1383
|
+
style: cell.style,
|
|
1384
|
+
format: cell.format,
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
return {
|
|
1388
|
+
id: sheet.id,
|
|
1389
|
+
name: sheet.name,
|
|
1390
|
+
colNumber: sheet.colNumber,
|
|
1391
|
+
rowNumber: sheet.rowNumber,
|
|
1392
|
+
rows: exportRows(sheet.rows),
|
|
1393
|
+
cols: exportCols(sheet.cols),
|
|
1394
|
+
merges: [], //exportMerges(sheet.merges),
|
|
1395
|
+
cells: cells,
|
|
1396
|
+
conditionalFormats: [],
|
|
1397
|
+
figures: [],
|
|
1398
|
+
};
|
|
1399
|
+
});
|
|
1400
|
+
data.activeSheet = this.workbook.activeSheet.id;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function getSheetSize(data: SheetData): { rowNumber: number; colNumber: number } {
|
|
1405
|
+
const positions = Object.keys(data.cells).map(toCartesian);
|
|
1406
|
+
return {
|
|
1407
|
+
rowNumber: Math.max(data.rowNumber, ...new Set(positions.map(([col, row]) => row + 1))),
|
|
1408
|
+
colNumber: Math.max(data.colNumber, ...new Set(positions.map(([col, row]) => col + 1))),
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function createDefaultCols(colNumber: number): Col[] {
|
|
1413
|
+
const cols: Col[] = [];
|
|
1414
|
+
let current = 0;
|
|
1415
|
+
for (let i = 0; i < colNumber; i++) {
|
|
1416
|
+
const size = DEFAULT_CELL_WIDTH;
|
|
1417
|
+
const col = {
|
|
1418
|
+
start: current,
|
|
1419
|
+
end: current + size,
|
|
1420
|
+
size: size,
|
|
1421
|
+
name: numberToLetters(i),
|
|
1422
|
+
};
|
|
1423
|
+
cols.push(col);
|
|
1424
|
+
current = col.end;
|
|
1425
|
+
}
|
|
1426
|
+
return cols;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function createDefaultRows(rowNumber: number): Row[] {
|
|
1430
|
+
const rows: Row[] = [];
|
|
1431
|
+
let current = 0;
|
|
1432
|
+
for (let i = 0; i < rowNumber; i++) {
|
|
1433
|
+
const size = DEFAULT_CELL_HEIGHT;
|
|
1434
|
+
const row = {
|
|
1435
|
+
start: current,
|
|
1436
|
+
end: current + size,
|
|
1437
|
+
size: size,
|
|
1438
|
+
name: String(i + 1),
|
|
1439
|
+
cells: {},
|
|
1440
|
+
};
|
|
1441
|
+
rows.push(row);
|
|
1442
|
+
current = row.end;
|
|
1443
|
+
}
|
|
1444
|
+
return rows;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function createCols(savedCols: { [key: number]: HeaderData }, colNumber: number): Col[] {
|
|
1448
|
+
const cols: Col[] = [];
|
|
1449
|
+
let current = 0;
|
|
1450
|
+
for (let i = 0; i < colNumber; i++) {
|
|
1451
|
+
const size = savedCols[i] ? savedCols[i].size || DEFAULT_CELL_WIDTH : DEFAULT_CELL_WIDTH;
|
|
1452
|
+
const col = {
|
|
1453
|
+
start: current,
|
|
1454
|
+
end: current + size,
|
|
1455
|
+
size: size,
|
|
1456
|
+
name: numberToLetters(i),
|
|
1457
|
+
};
|
|
1458
|
+
cols.push(col);
|
|
1459
|
+
current = col.end;
|
|
1460
|
+
}
|
|
1461
|
+
return cols;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function createRows(savedRows: { [key: number]: HeaderData }, rowNumber: number): Row[] {
|
|
1465
|
+
const rows: Row[] = [];
|
|
1466
|
+
let current = 0;
|
|
1467
|
+
for (let i = 0; i < rowNumber; i++) {
|
|
1468
|
+
const size = savedRows[i] ? savedRows[i].size || DEFAULT_CELL_HEIGHT : DEFAULT_CELL_HEIGHT;
|
|
1469
|
+
const row = {
|
|
1470
|
+
start: current,
|
|
1471
|
+
end: current + size,
|
|
1472
|
+
size: size,
|
|
1473
|
+
name: String(i + 1),
|
|
1474
|
+
cells: {},
|
|
1475
|
+
};
|
|
1476
|
+
rows.push(row);
|
|
1477
|
+
current = row.end;
|
|
1478
|
+
}
|
|
1479
|
+
return rows;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function exportCols(cols: Col[]): { [key: number]: HeaderData } {
|
|
1483
|
+
const exportedCols: { [key: number]: HeaderData } = {};
|
|
1484
|
+
for (let i in cols) {
|
|
1485
|
+
const col = cols[i];
|
|
1486
|
+
if (col.size !== DEFAULT_CELL_WIDTH) {
|
|
1487
|
+
exportedCols[i] = { size: col.size };
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return exportedCols;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function exportRows(rows: Row[]): { [key: number]: HeaderData } {
|
|
1494
|
+
const exportedRows: { [key: number]: HeaderData } = {};
|
|
1495
|
+
for (let i in rows) {
|
|
1496
|
+
const row = rows[i];
|
|
1497
|
+
if (row.size !== DEFAULT_CELL_HEIGHT) {
|
|
1498
|
+
exportedRows[i] = { size: row.size };
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return exportedRows;
|
|
1502
|
+
}
|