@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,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
|
+
}
|