@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.
Files changed (198) hide show
  1. package/.eslintrc.json +20 -0
  2. package/.husky/post-checkout +9 -0
  3. package/.husky/pre-commit +39 -0
  4. package/.prettierignore +1 -0
  5. package/astToFormula.patch +0 -0
  6. package/demo/data.js +255 -0
  7. package/demo/index.html +9 -0
  8. package/demo/libs/Chart.bundle.js +20755 -0
  9. package/demo/main.js +101 -0
  10. package/doc/readme.md +46 -0
  11. package/package.json +3 -17
  12. package/rollup.config.js +23 -0
  13. package/src/base_plugin.ts +102 -0
  14. package/src/components/autofill.ts +146 -0
  15. package/src/components/bottom_bar.ts +199 -0
  16. package/src/components/color_picker.ts +150 -0
  17. package/src/components/composer/autocomplete_dropdown.ts +135 -0
  18. package/src/components/composer/composer.ts +487 -0
  19. package/src/components/composer/content_editable_helper.ts +146 -0
  20. package/src/components/figures/chart.ts +70 -0
  21. package/src/components/figures/container.ts +268 -0
  22. package/src/components/figures/text_figure.ts +36 -0
  23. package/src/components/grid.ts +635 -0
  24. package/src/components/helpers/dom_helpers.ts +7 -0
  25. package/src/components/helpers/drag_and_drop.ts +17 -0
  26. package/src/components/icons.ts +36 -0
  27. package/src/components/index.ts +1 -0
  28. package/src/components/menu.ts +288 -0
  29. package/src/components/overlay.ts +457 -0
  30. package/src/components/scrollbar.ts +22 -0
  31. package/src/components/selection_input.ts +168 -0
  32. package/src/components/side_panel/cell_is_rule_editor.ts +169 -0
  33. package/src/components/side_panel/chart_panel.ts +104 -0
  34. package/src/components/side_panel/color_scale_rule_editor.ts +145 -0
  35. package/src/components/side_panel/conditional_formatting.ts +396 -0
  36. package/src/components/side_panel/side_panel.ts +127 -0
  37. package/src/components/side_panel/translations_terms.ts +45 -0
  38. package/src/components/spreadsheet.ts +198 -0
  39. package/src/components/top_bar.ts +535 -0
  40. package/src/constants.ts +26 -0
  41. package/src/data.ts +149 -0
  42. package/src/fonts.ts +41 -0
  43. package/src/formulas/compiler.ts +258 -0
  44. package/src/formulas/composer_tokenizer.ts +70 -0
  45. package/src/formulas/index.ts +15 -0
  46. package/src/formulas/normalize.ts +52 -0
  47. package/src/formulas/parser.ts +280 -0
  48. package/src/formulas/range_tokenizer.ts +97 -0
  49. package/src/formulas/tokenizer.ts +242 -0
  50. package/src/functions/arguments.ts +114 -0
  51. package/src/functions/dates.ts +302 -0
  52. package/src/functions/helpers.ts +592 -0
  53. package/src/functions/index.ts +52 -0
  54. package/src/functions/module_database.ts +282 -0
  55. package/src/functions/module_date.ts +743 -0
  56. package/src/functions/module_info.ts +69 -0
  57. package/src/functions/module_logical.ts +208 -0
  58. package/src/functions/module_lookup.ts +185 -0
  59. package/src/functions/module_math.ts +1252 -0
  60. package/src/functions/module_operators.ts +291 -0
  61. package/src/functions/module_statistical.ts +882 -0
  62. package/src/functions/module_text.ts +370 -0
  63. package/src/helpers/color.ts +30 -0
  64. package/src/helpers/coordinates.ts +71 -0
  65. package/src/helpers/grid_manipulation.ts +68 -0
  66. package/src/helpers/index.ts +7 -0
  67. package/src/helpers/misc.ts +76 -0
  68. package/src/helpers/numbers.ts +117 -0
  69. package/src/helpers/uuid.ts +19 -0
  70. package/src/helpers/zones.ts +263 -0
  71. package/src/history.ts +186 -0
  72. package/src/index.ts +84 -0
  73. package/src/model.ts +261 -0
  74. package/src/plugins/autofill.ts +412 -0
  75. package/src/plugins/chart.ts +404 -0
  76. package/src/plugins/clipboard.ts +461 -0
  77. package/src/plugins/conditional_format.ts +445 -0
  78. package/src/plugins/core.ts +1502 -0
  79. package/src/plugins/edition.ts +157 -0
  80. package/src/plugins/evaluation.ts +428 -0
  81. package/src/plugins/figures.ts +123 -0
  82. package/src/plugins/formatting.ts +630 -0
  83. package/src/plugins/highlight.ts +187 -0
  84. package/src/plugins/index.ts +32 -0
  85. package/src/plugins/merge.ts +524 -0
  86. package/src/plugins/renderer.ts +571 -0
  87. package/src/plugins/selection.ts +562 -0
  88. package/src/plugins/selection_inputs.ts +326 -0
  89. package/src/registries/autofill_modifiers.ts +76 -0
  90. package/src/registries/autofill_rules.ts +98 -0
  91. package/src/registries/figure_registry.ts +30 -0
  92. package/src/registries/index.ts +7 -0
  93. package/src/registries/menu_items_registry.ts +110 -0
  94. package/src/registries/menus/cell_menu_registry.ts +79 -0
  95. package/src/registries/menus/col_menu_registry.ts +63 -0
  96. package/src/registries/menus/index.ts +6 -0
  97. package/src/registries/menus/menu_items_actions.ts +425 -0
  98. package/src/registries/menus/row_menu_registry.ts +63 -0
  99. package/src/registries/menus/sheet_menu_registry.ts +75 -0
  100. package/src/registries/menus/topbar_menu_registry.ts +204 -0
  101. package/src/registries/side_panel_registry.ts +26 -0
  102. package/src/registries/topbar_component_registry.ts +12 -0
  103. package/src/registry.ts +51 -0
  104. package/src/translation.ts +44 -0
  105. package/src/types/autofill.ts +48 -0
  106. package/src/types/chart.ts +26 -0
  107. package/src/types/commands.ts +716 -0
  108. package/src/types/conditional_formatting.ts +124 -0
  109. package/src/types/env.ts +13 -0
  110. package/src/types/functions.ts +35 -0
  111. package/src/types/getters.ts +103 -0
  112. package/src/types/index.ts +20 -0
  113. package/src/types/misc.ts +143 -0
  114. package/src/types/rendering.ts +41 -0
  115. package/src/types/workbook_data.ts +62 -0
  116. package/tests/__mocks__/uuid.ts +7 -0
  117. package/tests/canvas.mock.ts +34 -0
  118. package/tests/components/__mocks__/content_editable_helper.ts +79 -0
  119. package/tests/components/__mocks__/scrollbar.ts +18 -0
  120. package/tests/components/__snapshots__/autocomplete_dropdown_test.ts.snap +29 -0
  121. package/tests/components/__snapshots__/bottom_bar_test.ts.snap +75 -0
  122. package/tests/components/__snapshots__/context_menu_test.ts.snap +88 -0
  123. package/tests/components/__snapshots__/grid_test.ts.snap +60 -0
  124. package/tests/components/__snapshots__/spreadsheet_test.ts.snap +455 -0
  125. package/tests/components/__snapshots__/top_bar_test.ts.snap +705 -0
  126. package/tests/components/autocomplete_dropdown_test.ts +315 -0
  127. package/tests/components/autofill_test.ts +167 -0
  128. package/tests/components/bottom_bar_test.ts +262 -0
  129. package/tests/components/chart_side_panel_test.ts +123 -0
  130. package/tests/components/composer_test.ts +400 -0
  131. package/tests/components/context_menu_test.ts +825 -0
  132. package/tests/components/grid_manipulation_test.ts +163 -0
  133. package/tests/components/grid_test.ts +581 -0
  134. package/tests/components/overlay_test.ts +541 -0
  135. package/tests/components/selection_input_test.ts +259 -0
  136. package/tests/components/side_panel_test.ts +148 -0
  137. package/tests/components/spreadsheet_test.ts +194 -0
  138. package/tests/components/top_bar_test.ts +427 -0
  139. package/tests/data_test.ts +175 -0
  140. package/tests/dom_helper.ts +42 -0
  141. package/tests/formulas/__snapshots__/compiler_test.ts.snap +302 -0
  142. package/tests/formulas/compiler_test.ts +266 -0
  143. package/tests/formulas/composer_tokenizer_test.ts +137 -0
  144. package/tests/formulas/expressions_test.ts +57 -0
  145. package/tests/formulas/formulas_test.ts +80 -0
  146. package/tests/formulas/normalize_test.ts +82 -0
  147. package/tests/formulas/parser_test.ts +224 -0
  148. package/tests/formulas/range_tokenizer_test.ts +187 -0
  149. package/tests/formulas/tokenizer_test.ts +164 -0
  150. package/tests/functions/arguments_test.ts +141 -0
  151. package/tests/functions/dates_test.ts +1081 -0
  152. package/tests/functions/functions_test.ts +53 -0
  153. package/tests/functions/helper_test.ts +98 -0
  154. package/tests/functions/module_database_test.ts +311 -0
  155. package/tests/functions/module_date_test.ts +941 -0
  156. package/tests/functions/module_info_test.ts +139 -0
  157. package/tests/functions/module_logical_test.ts +369 -0
  158. package/tests/functions/module_lookup_test.ts +541 -0
  159. package/tests/functions/module_math_test.ts +2482 -0
  160. package/tests/functions/module_operators_test.ts +955 -0
  161. package/tests/functions/module_statistical_test.ts +2014 -0
  162. package/tests/functions/module_text_test.ts +667 -0
  163. package/tests/helpers/coordinates_test.ts +25 -0
  164. package/tests/helpers/misc_test.ts +95 -0
  165. package/tests/helpers/numbers_test.ts +196 -0
  166. package/tests/helpers/zones_test.ts +97 -0
  167. package/tests/helpers.ts +384 -0
  168. package/tests/menu_items_registry_test.ts +540 -0
  169. package/tests/model/plugins/__snapshots__/chart_test.ts.snap +90 -0
  170. package/tests/model/plugins/chart_test.ts +562 -0
  171. package/tests/model_test.ts +82 -0
  172. package/tests/plugins/__snapshots__/conditional_formatting_test.ts.snap +131 -0
  173. package/tests/plugins/__snapshots__/renderer_test.ts.snap +371 -0
  174. package/tests/plugins/autofill_test.ts +457 -0
  175. package/tests/plugins/borders_test.ts +295 -0
  176. package/tests/plugins/clipboard_test.ts +1180 -0
  177. package/tests/plugins/conditional_formatting_test.ts +1253 -0
  178. package/tests/plugins/core_test.ts +614 -0
  179. package/tests/plugins/edition_test.ts +100 -0
  180. package/tests/plugins/evaluation_async_test.ts +332 -0
  181. package/tests/plugins/evaluation_dates_test.ts +79 -0
  182. package/tests/plugins/evaluation_test.ts +901 -0
  183. package/tests/plugins/figures_test.ts +327 -0
  184. package/tests/plugins/formatting_test.ts +391 -0
  185. package/tests/plugins/grid_manipulation_test.ts +1366 -0
  186. package/tests/plugins/highlight_test.ts +413 -0
  187. package/tests/plugins/history_test.ts +142 -0
  188. package/tests/plugins/import_export_test.ts +276 -0
  189. package/tests/plugins/merges_test.ts +490 -0
  190. package/tests/plugins/navigation_test.ts +181 -0
  191. package/tests/plugins/plugins/evaluation_test.ts +111 -0
  192. package/tests/plugins/renderer_test.ts +598 -0
  193. package/tests/plugins/resizing_test.ts +152 -0
  194. package/tests/plugins/selection_input_test.ts +409 -0
  195. package/tests/plugins/selection_test.ts +442 -0
  196. package/tests/plugins/sheets_test.ts +643 -0
  197. package/tests/plugins/style_test.ts +90 -0
  198. package/tsconfig.json +22 -0
@@ -0,0 +1,157 @@
1
+ import { BasePlugin } from "../base_plugin";
2
+ import { tokenize } from "../formulas/index";
3
+ import { toCartesian, toXC } from "../helpers/index";
4
+ import { Mode } from "../model";
5
+ import { Command, LAYERS } from "../types/index";
6
+
7
+ export type EditionMode = "editing" | "selecting" | "inactive";
8
+
9
+ export class EditionPlugin extends BasePlugin {
10
+ static layers = [LAYERS.Highlights];
11
+ static getters = ["getEditionMode", "getCurrentContent", "getEditionSheet"];
12
+ static modes: Mode[] = ["normal", "readonly"];
13
+
14
+ private col: number = 0;
15
+ private row: number = 0;
16
+ private mode: EditionMode = "inactive";
17
+ private sheet: string = "";
18
+ private currentContent: string = "";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Command Handling
22
+ // ---------------------------------------------------------------------------
23
+
24
+ beforeHandle(cmd: Command) {
25
+ switch (cmd.type) {
26
+ case "ACTIVATE_SHEET":
27
+ if (this.mode !== "selecting") {
28
+ this.stopEdition();
29
+ }
30
+ break;
31
+ }
32
+ }
33
+
34
+ handle(cmd: Command) {
35
+ switch (cmd.type) {
36
+ case "START_COMPOSER_SELECTION":
37
+ this.mode = "selecting";
38
+ this.dispatch("SET_SELECTION", {
39
+ zones: this.getters.getSelectedZones(),
40
+ anchor: this.getters.getPosition(),
41
+ });
42
+ break;
43
+ case "STOP_COMPOSER_SELECTION":
44
+ if (this.mode === "selecting") {
45
+ this.mode = "editing";
46
+ }
47
+ break;
48
+ case "START_EDITION":
49
+ this.startEdition(cmd.text);
50
+ break;
51
+ case "STOP_EDITION":
52
+ if (cmd.cancel) {
53
+ this.cancelEdition();
54
+ } else {
55
+ this.stopEdition();
56
+ }
57
+ break;
58
+ case "SET_CURRENT_CONTENT":
59
+ this.currentContent = cmd.content;
60
+ break;
61
+ case "SELECT_CELL":
62
+ case "MOVE_POSITION":
63
+ if (this.mode === "editing") {
64
+ this.stopEdition();
65
+ }
66
+ break;
67
+ }
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Getters
72
+ // ---------------------------------------------------------------------------
73
+
74
+ getEditionMode(): EditionMode {
75
+ return this.mode;
76
+ }
77
+
78
+ getCurrentContent(): string {
79
+ return this.currentContent;
80
+ }
81
+
82
+ getEditionSheet(): string {
83
+ return this.sheet;
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Misc
88
+ // ---------------------------------------------------------------------------
89
+
90
+ private startEdition(str?: string) {
91
+ if (!str) {
92
+ const cell = this.getters.getActiveCell();
93
+ str = cell ? cell.content || "" : "";
94
+ }
95
+ this.mode = "editing";
96
+ this.currentContent = str || "";
97
+ this.dispatch("REMOVE_ALL_HIGHLIGHTS");
98
+ const [col, row] = this.getters.getPosition();
99
+ this.col = col;
100
+ this.row = row;
101
+ this.sheet = this.getters.getActiveSheet();
102
+ }
103
+
104
+ private stopEdition() {
105
+ if (this.mode !== "inactive") {
106
+ this.cancelEdition();
107
+ let xc = toXC(this.col, this.row);
108
+ const { mergeCellMap, merges, cells } = this.getters
109
+ .getSheets()
110
+ .find((sheet) => sheet.id === this.sheet)!;
111
+ if (xc in mergeCellMap) {
112
+ const mergeId = mergeCellMap[xc];
113
+ xc = merges[mergeId].topLeft;
114
+ }
115
+ let content = this.currentContent;
116
+ this.currentContent = "";
117
+ const cell = cells[xc];
118
+ const didChange = cell ? cell.content !== content : content !== "";
119
+ if (!didChange) {
120
+ return;
121
+ }
122
+ const [col, row] = toCartesian(xc);
123
+ if (content) {
124
+ if (content.startsWith("=")) {
125
+ const tokens = tokenize(content);
126
+ const left = tokens.filter((t) => t.type === "LEFT_PAREN").length;
127
+ const right = tokens.filter((t) => t.type === "RIGHT_PAREN").length;
128
+ const missing = left - right;
129
+ if (missing > 0) {
130
+ content += new Array(missing).fill(")").join("");
131
+ }
132
+ }
133
+ this.dispatch("UPDATE_CELL", {
134
+ sheet: this.sheet,
135
+ col,
136
+ row,
137
+ content,
138
+ });
139
+ } else {
140
+ this.dispatch("UPDATE_CELL", {
141
+ sheet: this.sheet,
142
+ content: "",
143
+ col,
144
+ row,
145
+ });
146
+ }
147
+ if (this.getters.getActiveSheet() !== this.sheet) {
148
+ this.dispatch("ACTIVATE_SHEET", { from: this.getters.getActiveSheet(), to: this.sheet });
149
+ }
150
+ }
151
+ }
152
+
153
+ private cancelEdition() {
154
+ this.mode = "inactive";
155
+ this.dispatch("REMOVE_ALL_HIGHLIGHTS");
156
+ }
157
+ }
@@ -0,0 +1,428 @@
1
+ import { BasePlugin } from "../base_plugin";
2
+ import { compile, normalize } from "../formulas/index";
3
+ import { functionRegistry } from "../functions/index";
4
+ import { mapCellsInZone, toCartesian, toXC, toZone } from "../helpers/index";
5
+ import { WHistory } from "../history";
6
+ import { Mode, ModelConfig } from "../model";
7
+ import { _lt } from "../translation";
8
+ import {
9
+ Cell,
10
+ Command,
11
+ CommandDispatcher,
12
+ EnsureRange,
13
+ EvalContext,
14
+ Getters,
15
+ NormalizedFormula,
16
+ ReferenceDenormalizer,
17
+ UID,
18
+ Workbook,
19
+ } from "../types";
20
+
21
+ function* makeObjectIterator(obj: Object) {
22
+ for (let i in obj) {
23
+ yield obj[i];
24
+ }
25
+ }
26
+
27
+ function* makeSetIterator(set: Set<any>) {
28
+ for (let elem of set) {
29
+ yield elem;
30
+ }
31
+ }
32
+
33
+ const functionMap = functionRegistry.mapping;
34
+
35
+ type FormulaParameters = [ReferenceDenormalizer, EnsureRange, EvalContext];
36
+
37
+ export const LOADING = "Loading...";
38
+
39
+ export class EvaluationPlugin extends BasePlugin {
40
+ static getters = ["evaluateFormula", "isIdle"];
41
+ static modes: Mode[] = ["normal", "readonly"];
42
+
43
+ private isUptodate: Set<string> = new Set();
44
+ private loadingCells: number = 0;
45
+ private isStarted: boolean = false;
46
+ private evalContext: EvalContext;
47
+
48
+ /**
49
+ * For all cells that are being currently computed (asynchronously).
50
+ *
51
+ * For example: =Wait(3)
52
+ */
53
+ private PENDING: Set<Cell> = new Set();
54
+
55
+ /**
56
+ * For all cells that are NOT being currently computed, but depend on another
57
+ * asynchronous computation.
58
+ *
59
+ * For example: A2 is in WAITING (initially) and A1 in PENDING
60
+ * A1: =Wait(3)
61
+ * A2: =A1
62
+ */
63
+ private WAITING: { [key: string]: Set<Cell> } = {};
64
+
65
+ /**
66
+ * For all cells that have been async computed.
67
+ *
68
+ * For example:
69
+ * A1: =Wait(3)
70
+ * A2: =A1
71
+ *
72
+ * When A1 is computed, A1 is moved in COMPUTED
73
+ */
74
+ private COMPUTED: Set<Cell> = new Set();
75
+
76
+ constructor(
77
+ workbook: Workbook,
78
+ getters: Getters,
79
+ history: WHistory,
80
+ dispatch: CommandDispatcher["dispatch"],
81
+ config: ModelConfig
82
+ ) {
83
+ super(workbook, getters, history, dispatch, config);
84
+ this.evalContext = config.evalContext;
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Command Handling
89
+ // ---------------------------------------------------------------------------
90
+
91
+ handle(cmd: Command) {
92
+ switch (cmd.type) {
93
+ case "START":
94
+ this.evaluate();
95
+ break;
96
+ case "UPDATE_CELL":
97
+ if ("content" in cmd) {
98
+ this.isUptodate.clear();
99
+ }
100
+ break;
101
+ case "EVALUATE_CELLS":
102
+ if (cmd.onlyWaiting) {
103
+ for (const sheetId in this.WAITING) {
104
+ const cells = new Set(this.WAITING[sheetId]);
105
+ this.WAITING[sheetId].clear();
106
+ this.evaluateCells(makeSetIterator(cells), sheetId);
107
+ }
108
+ } else {
109
+ for (const sheetId in this.WAITING) {
110
+ delete this.WAITING[sheetId];
111
+ }
112
+ this.evaluate();
113
+ }
114
+ this.isUptodate.add(this.workbook.activeSheet.id);
115
+ break;
116
+ case "UNDO":
117
+ case "REDO":
118
+ this.isUptodate.clear();
119
+ break;
120
+ }
121
+ }
122
+
123
+ finalize() {
124
+ if (!this.isUptodate.has(this.workbook.activeSheet.id)) {
125
+ this.evaluate();
126
+ this.isUptodate.add(this.workbook.activeSheet.id);
127
+ }
128
+ if (this.loadingCells > 0) {
129
+ this.startScheduler();
130
+ }
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Getters
135
+ // ---------------------------------------------------------------------------
136
+
137
+ evaluateFormula(formula: string, sheet: UID = this.workbook.activeSheet.id): any {
138
+ let formulaString: NormalizedFormula = normalize(formula);
139
+
140
+ const compiledFormula = compile(formulaString);
141
+ const params = this.getFormulaParameters(() => {});
142
+ return compiledFormula(formulaString.dependencies, sheet, ...params);
143
+ }
144
+
145
+ isIdle() {
146
+ return this.loadingCells === 0;
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Scheduler
151
+ // ---------------------------------------------------------------------------
152
+
153
+ private startScheduler() {
154
+ if (!this.isStarted) {
155
+ this.isStarted = true;
156
+ let current = this.loadingCells;
157
+ const recomputeCells = () => {
158
+ if (this.loadingCells !== current) {
159
+ this.dispatch("EVALUATE_CELLS", { onlyWaiting: true });
160
+ current = this.loadingCells;
161
+ if (current === 0) {
162
+ this.isStarted = false;
163
+ }
164
+ }
165
+ if (current > 0) {
166
+ window.setTimeout(recomputeCells, 15);
167
+ }
168
+ };
169
+ window.setTimeout(recomputeCells, 5);
170
+ }
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Evaluator
175
+ // ---------------------------------------------------------------------------
176
+
177
+ private evaluate() {
178
+ this.COMPUTED.clear();
179
+ this.evaluateCells(
180
+ makeObjectIterator(this.workbook.activeSheet.cells),
181
+ this.workbook.activeSheet.id
182
+ );
183
+ }
184
+
185
+ private evaluateCells(cells: Generator<Cell>, sheetId: string) {
186
+ const self = this;
187
+ const { COMPUTED, PENDING, WAITING } = this;
188
+ const params = this.getFormulaParameters(computeValue);
189
+ const visited: { [sheetId: string]: { [xc: string]: boolean | null } } = {};
190
+
191
+ const _cells = [...cells];
192
+
193
+ for (let cell of _cells) {
194
+ // Do not reset error when it's during the compilation
195
+ if (cell.value !== "#BAD_EXPR") {
196
+ cell.error = undefined;
197
+ }
198
+ }
199
+
200
+ for (let cell of _cells) {
201
+ computeValue(cell, sheetId);
202
+ }
203
+
204
+ function handleError(e: Error | any, currentCell: Cell, currentSheetId: string) {
205
+ if (!(e instanceof Error)) {
206
+ e = new Error(e);
207
+ }
208
+ if (PENDING.has(currentCell)) {
209
+ PENDING.delete(currentCell);
210
+ self.loadingCells--;
211
+ }
212
+ if (e.message === "not ready") {
213
+ if (!WAITING[currentSheetId]) {
214
+ WAITING[currentSheetId] = new Set<Cell>();
215
+ }
216
+ WAITING[currentSheetId].add(currentCell);
217
+ currentCell.pending = true;
218
+ currentCell.value = LOADING;
219
+ } else if (!currentCell.error) {
220
+ currentCell.value = "#ERROR";
221
+ const __lastFnCalled = params[2].__lastFnCalled || "";
222
+ currentCell.error = e.message.replace("[[FUNCTION_NAME]]", __lastFnCalled);
223
+ }
224
+ }
225
+
226
+ function computeValue(cell: Cell, sheetId: string) {
227
+ if (cell.type !== "formula" || !cell.formula) {
228
+ return;
229
+ }
230
+ const xc = cell.xc;
231
+ visited[sheetId] = visited[sheetId] || {};
232
+ if (xc in visited[sheetId]) {
233
+ if (visited[sheetId][xc] === null) {
234
+ cell.value = "#CYCLE";
235
+ cell.error = _lt("Circular reference");
236
+ }
237
+ return;
238
+ }
239
+ if (COMPUTED.has(cell) || PENDING.has(cell)) {
240
+ return;
241
+ }
242
+ visited[sheetId][xc] = null;
243
+ cell.error = undefined;
244
+ try {
245
+ params[2].__originCellXC = xc;
246
+ // todo: move formatting in grid and formatters.js
247
+ if (cell.formula.compiledFormula.async) {
248
+ cell.value = LOADING;
249
+ cell.pending = true;
250
+ PENDING.add(cell);
251
+
252
+ cell.formula
253
+ .compiledFormula(cell.formula.dependencies, sheetId, ...params)
254
+ .then((val) => {
255
+ cell.value = val;
256
+ self.loadingCells--;
257
+ if (PENDING.has(cell)) {
258
+ PENDING.delete(cell);
259
+ cell.pending = false;
260
+ COMPUTED.add(cell);
261
+ }
262
+ })
263
+ .catch((e: Error) => handleError(e, cell, sheetId));
264
+ self.loadingCells++;
265
+ } else {
266
+ cell.value = cell.formula.compiledFormula(cell.formula.dependencies, sheetId, ...params);
267
+ cell.pending = false;
268
+ }
269
+ if (Array.isArray(cell.value)) {
270
+ // if a value returns an array (like =A1:A3)
271
+ throw new Error(_lt("This formula depends on invalid values"));
272
+ } else {
273
+ cell.error = undefined;
274
+ }
275
+ } catch (e) {
276
+ handleError(e, cell, sheetId);
277
+ }
278
+ visited[sheetId][xc] = true;
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Return all functions necessary to properly evaluate a formula:
284
+ * - a refFn function to read any reference, cell or range of a normalized formula
285
+ * - a range function to convert any reference to a proper value array
286
+ * - an evaluation context
287
+ */
288
+ private getFormulaParameters(computeValue: Function): FormulaParameters {
289
+ const evalContext = Object.assign(Object.create(functionMap), this.evalContext, {
290
+ getters: this.getters,
291
+ });
292
+ const sheets = this.workbook.sheets;
293
+ const PENDING = this.PENDING;
294
+ function readCell(xc: string, sheetId: UID): any {
295
+ let cell;
296
+ const s = sheets[sheetId];
297
+ if (s) {
298
+ cell = s.cells[xc.toUpperCase().replace(/\$/g, "")];
299
+ } else {
300
+ throw new Error(_lt("Invalid sheet name"));
301
+ }
302
+ if (!cell || cell.content === "") {
303
+ return null;
304
+ }
305
+ const value = getCellValue(cell, sheetId);
306
+ if (value === LOADING) {
307
+ throw new Error("not ready");
308
+ }
309
+ return value;
310
+ }
311
+
312
+ function getCellValue(cell: Cell, sheetId: UID): any {
313
+ if (cell.formula && cell.formula.compiledFormula.async && cell.error && !PENDING.has(cell)) {
314
+ throw new Error(_lt("This formula depends on invalid values"));
315
+ }
316
+ computeValue(cell, sheetId);
317
+ if (cell.error) {
318
+ throw new Error(_lt("This formula depends on invalid values"));
319
+ }
320
+ return cell.value;
321
+ }
322
+
323
+ /**
324
+ * Return a range of values. It is a list of col values.
325
+ *
326
+ * Note that each col is possibly sparse: it only contain the values of cells
327
+ * that are actually present in the grid.
328
+ */
329
+ function _range(v1: string, v2: string, sheetId: UID): any[][] {
330
+ const sheet = sheets[sheetId];
331
+ let [left, top] = toCartesian(v1);
332
+ let [right, bottom] = toCartesian(v2);
333
+ right = Math.min(right, sheet.colNumber - 1);
334
+ bottom = Math.min(bottom, sheet.rowNumber - 1);
335
+
336
+ if (left > right) {
337
+ const tmp = left;
338
+ left = right;
339
+ right = tmp;
340
+ }
341
+ if (top > bottom) {
342
+ const tmp = top;
343
+ top = bottom;
344
+ bottom = tmp;
345
+ }
346
+
347
+ const zone = { left, top, right, bottom };
348
+ const result = mapCellsInZone(zone, sheet, (cell) => getCellValue(cell, sheetId));
349
+ for (const col of result) {
350
+ for (const value of col) {
351
+ if (value === LOADING) {
352
+ throw new Error("not ready");
353
+ }
354
+ }
355
+ }
356
+ return result;
357
+ }
358
+
359
+ /**
360
+ * Returns the value of the cell(s) used in reference
361
+ *
362
+ * @param position the index in the references array
363
+ * @param references all the references used in the current formula
364
+ * @param sheetId the sheet that is currently being evaluated, if a reference does not
365
+ * include a sheet, it is the id of the sheet of the reference to be used
366
+ */
367
+ function refFn(
368
+ position: number,
369
+ references: string[],
370
+ sheetId: UID,
371
+ functionName: string,
372
+ paramNumber: number
373
+ ): any | any[][] {
374
+ const referenceText = references[position];
375
+
376
+ const [reference, sheetName] = referenceText.split("!").reverse();
377
+ const referenceSheetId = sheetName
378
+ ? evalContext.getters.getSheetIdByName(sheetName)
379
+ : sheetId;
380
+
381
+ const zone = toZone(referenceText);
382
+
383
+ if (zone.top > zone.bottom || zone.left > zone.right) {
384
+ throw new Error(
385
+ _lt("invalid range %s:%s", toXC(zone.left, zone.top), toXC(zone.right, zone.bottom))
386
+ );
387
+ }
388
+
389
+ // if the formula definition could have accepted a range, we would pass through the _range function and not here
390
+ if (zone.bottom !== zone.top || zone.left !== zone.right) {
391
+ throw new Error(
392
+ _lt(
393
+ "Function %s expects the parameter %s to be a single value or a single cell reference, not a range.",
394
+ functionName.toString(),
395
+ paramNumber.toString()
396
+ )
397
+ );
398
+ }
399
+ //it's a cell
400
+ return readCell(reference, referenceSheetId);
401
+ }
402
+
403
+ /**
404
+ * Return the values of the cell(s) used in reference, but always in the format of a range even
405
+ * if a single cell is referenced. This is useful for the formulas that describe parameters as
406
+ * range<number> etc.
407
+ *
408
+ * the parameters are the same as refFn, except that these parameters cannot be Meta
409
+ */
410
+ function range(position: number, references: string[], sheetId: UID): any[][] {
411
+ const referenceText = references[position];
412
+ const [reference, sheetName] = referenceText.split("!").reverse();
413
+ const referenceSheetId = sheetName
414
+ ? evalContext.getters.getSheetIdByName(sheetName)
415
+ : sheetId;
416
+
417
+ if (references[position] && !references[position].includes(":")) {
418
+ //it's a cell reference, but it must be treated as a range
419
+ return _range(reference, reference, referenceSheetId);
420
+ } else {
421
+ const [left, right] = reference.split(":");
422
+ return _range(left, right, referenceSheetId);
423
+ }
424
+ }
425
+
426
+ return [refFn, range, evalContext];
427
+ }
428
+ }
@@ -0,0 +1,123 @@
1
+ import { BasePlugin } from "../base_plugin";
2
+ import { Command, Figure, Viewport, WorkbookData } from "../types/index";
3
+
4
+ export class FigurePlugin extends BasePlugin {
5
+ static getters = ["getFigures", "getSelectedFigureId", "getFigure"];
6
+
7
+ private selectedFigureId: string | null = null;
8
+
9
+ private figures: { [figId: string]: Figure<any> } = {};
10
+ private sheetFigures: { [sheetId: string]: Figure<any>[] } = {};
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Command Handling
14
+ // ---------------------------------------------------------------------------
15
+ handle(cmd: Command) {
16
+ switch (cmd.type) {
17
+ case "DELETE_SHEET":
18
+ this.deleteSheet(cmd.sheet);
19
+ break;
20
+ case "CREATE_FIGURE":
21
+ this.history.updateLocalState(["figures", cmd.figure.id], cmd.figure);
22
+ const sheetFigures = (this.sheetFigures[cmd.sheet] || []).slice();
23
+ sheetFigures.push(cmd.figure);
24
+ this.history.updateLocalState(["sheetFigures", cmd.sheet], sheetFigures);
25
+ break;
26
+ case "UPDATE_FIGURE":
27
+ if (cmd.x !== undefined) {
28
+ this.history.updateLocalState(["figures", cmd.id, "x"], Math.max(cmd.x, 0));
29
+ }
30
+ if (cmd.y !== undefined) {
31
+ this.history.updateLocalState(["figures", cmd.id, "y"], Math.max(cmd.y, 0));
32
+ }
33
+ if (cmd.width !== undefined) {
34
+ this.history.updateLocalState(["figures", cmd.id, "width"], cmd.width);
35
+ }
36
+ if (cmd.height !== undefined) {
37
+ this.history.updateLocalState(["figures", cmd.id, "height"], cmd.height);
38
+ }
39
+ if (cmd.data !== undefined) {
40
+ this.history.updateLocalState(["figures", cmd.id, "data"], cmd.data);
41
+ }
42
+ break;
43
+ case "SELECT_FIGURE":
44
+ this.selectedFigureId = cmd.id;
45
+ break;
46
+ case "DELETE_FIGURE":
47
+ this.history.updateLocalState(["figures", cmd.id], undefined);
48
+ for (let s in this.sheetFigures) {
49
+ let deletedFigureIndex = this.sheetFigures[s].findIndex((f) => f.id === cmd.id);
50
+ if (deletedFigureIndex > -1) {
51
+ const copy = this.sheetFigures[s].slice();
52
+ copy.splice(deletedFigureIndex, 1);
53
+ this.history.updateLocalState(["sheetFigures", s], copy);
54
+ this.selectedFigureId = null;
55
+ }
56
+ }
57
+ break;
58
+ // some commands should not remove the current selection
59
+ case "EVALUATE_CELLS":
60
+ break;
61
+ default:
62
+ this.selectedFigureId = null;
63
+ }
64
+ }
65
+
66
+ deleteSheet(sheet: string) {
67
+ for (let figure of this.sheetFigures[sheet] || []) {
68
+ this.history.updateLocalState(["figures", figure.id], undefined);
69
+ }
70
+ const sheetFigures = Object.assign({}, this.sheetFigures);
71
+ delete sheetFigures[sheet];
72
+ this.history.updateLocalState(["sheetFigures"], sheetFigures);
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Getters
77
+ // ---------------------------------------------------------------------------
78
+
79
+ getFigures(viewport: Viewport): Figure<any>[] {
80
+ const result: Figure<any>[] = [];
81
+ const figures = this.sheetFigures[this.workbook.activeSheet.id] || [];
82
+ const { offsetX, offsetY, width, height } = viewport;
83
+ for (let figure of figures) {
84
+ if (figure.x >= offsetX + width || figure.x + figure.width <= offsetX) {
85
+ continue;
86
+ }
87
+ if (figure.y >= offsetY + height || figure.y + figure.height <= offsetY) {
88
+ continue;
89
+ }
90
+ result.push(figure);
91
+ }
92
+ return result;
93
+ }
94
+
95
+ getSelectedFigureId(): string | null {
96
+ return this.selectedFigureId;
97
+ }
98
+
99
+ getFigure<T>(figureId: string): Figure<T> {
100
+ return this.figures[figureId];
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Import/Export
105
+ // ---------------------------------------------------------------------------
106
+
107
+ import(data: WorkbookData) {
108
+ for (let sheet of data.sheets) {
109
+ const figList: Figure<any>[] = [];
110
+ for (let f of sheet.figures) {
111
+ this.figures[f.id] = f;
112
+ figList.push(f);
113
+ }
114
+ this.sheetFigures[sheet.id] = figList;
115
+ }
116
+ }
117
+
118
+ export(data: WorkbookData) {
119
+ for (let sheetData of data.sheets) {
120
+ sheetData.figures = this.sheetFigures[sheetData.id] || [];
121
+ }
122
+ }
123
+ }