@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,635 @@
|
|
|
1
|
+
import * as owl from "@odoo/owl";
|
|
2
|
+
import {
|
|
3
|
+
BACKGROUND_GRAY_COLOR,
|
|
4
|
+
DEFAULT_CELL_HEIGHT,
|
|
5
|
+
HEADER_HEIGHT,
|
|
6
|
+
HEADER_WIDTH,
|
|
7
|
+
SCROLLBAR_WIDTH,
|
|
8
|
+
} from "../constants";
|
|
9
|
+
import { isEqual, isInside, toCartesian, toXC } from "../helpers/index";
|
|
10
|
+
import { Model } from "../model";
|
|
11
|
+
import { cellMenuRegistry } from "../registries/menus/cell_menu_registry";
|
|
12
|
+
import { colMenuRegistry } from "../registries/menus/col_menu_registry";
|
|
13
|
+
import { rowMenuRegistry } from "../registries/menus/row_menu_registry";
|
|
14
|
+
import { SpreadsheetEnv, Viewport } from "../types/index";
|
|
15
|
+
import { Autofill } from "./autofill";
|
|
16
|
+
import { Composer } from "./composer/composer";
|
|
17
|
+
import { FiguresContainer } from "./figures/container";
|
|
18
|
+
import { startDnd } from "./helpers/drag_and_drop";
|
|
19
|
+
import { Menu, MenuState } from "./menu";
|
|
20
|
+
import { Overlay } from "./overlay";
|
|
21
|
+
import { ScrollBar } from "./scrollbar";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The Grid component is the main part of the spreadsheet UI. It is responsible
|
|
25
|
+
* for displaying the actual grid, rendering it, managing events, ...
|
|
26
|
+
*
|
|
27
|
+
* The grid is rendered on a canvas. 3 sub components are (sometimes) displayed
|
|
28
|
+
* on top of the canvas:
|
|
29
|
+
* - a composer (to edit the cell content)
|
|
30
|
+
* - a horizontal resizer (to resize columns)
|
|
31
|
+
* - a vertical resizer (same, for rows)
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const { Component, useState } = owl;
|
|
35
|
+
const { xml, css } = owl.tags;
|
|
36
|
+
const { useRef, onMounted, onWillUnmount } = owl.hooks;
|
|
37
|
+
export type ContextMenuType = "ROW" | "COL" | "CELL";
|
|
38
|
+
|
|
39
|
+
const registries = {
|
|
40
|
+
ROW: rowMenuRegistry,
|
|
41
|
+
COL: colMenuRegistry,
|
|
42
|
+
CELL: cellMenuRegistry,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// copy and paste are specific events that should not be managed by the keydown event,
|
|
46
|
+
// but they shouldn't be preventDefault and stopped (else copy and paste events will not trigger)
|
|
47
|
+
// and also should not result in typing the character C or V in the composer
|
|
48
|
+
const keyDownMappingIgnore: string[] = ["CTRL+C", "CTRL+V"];
|
|
49
|
+
|
|
50
|
+
// -----------------------------------------------------------------------------
|
|
51
|
+
// Error Tooltip Hook
|
|
52
|
+
// -----------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
interface ErrorTooltip {
|
|
55
|
+
isOpen: boolean;
|
|
56
|
+
text: string;
|
|
57
|
+
style: string;
|
|
58
|
+
}
|
|
59
|
+
function useErrorTooltip(env: SpreadsheetEnv, getViewPort: () => Viewport): ErrorTooltip {
|
|
60
|
+
const { browser, getters } = env;
|
|
61
|
+
const { Date, setInterval, clearInterval } = browser;
|
|
62
|
+
let x = 0;
|
|
63
|
+
let y = 0;
|
|
64
|
+
let lastMoved = 0;
|
|
65
|
+
let tooltipCol: number, tooltipRow: number;
|
|
66
|
+
const canvasRef = useRef("canvas");
|
|
67
|
+
const tooltip = useState({ isOpen: false, text: "", style: "" });
|
|
68
|
+
let interval;
|
|
69
|
+
function updateMousePosition(e: MouseEvent) {
|
|
70
|
+
x = e.offsetX;
|
|
71
|
+
y = e.offsetY;
|
|
72
|
+
lastMoved = Date.now();
|
|
73
|
+
}
|
|
74
|
+
function getPosition(): [number, number] {
|
|
75
|
+
const viewport = getViewPort();
|
|
76
|
+
const col = getters.getColIndex(x, viewport.left);
|
|
77
|
+
const row = getters.getRowIndex(y, viewport.top);
|
|
78
|
+
return [col, row];
|
|
79
|
+
}
|
|
80
|
+
function checkTiming() {
|
|
81
|
+
if (tooltip.isOpen) {
|
|
82
|
+
const [col, row] = getPosition();
|
|
83
|
+
if (col !== tooltipCol || row !== tooltipRow) {
|
|
84
|
+
tooltip.isOpen = false;
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
const delta = Date.now() - lastMoved;
|
|
88
|
+
if (400 < delta && delta < 600) {
|
|
89
|
+
// mouse did not move for a short while
|
|
90
|
+
const [col, row] = getPosition();
|
|
91
|
+
if (col < 0 || row < 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const mainXc = getters.getMainCell(toXC(col, row));
|
|
95
|
+
const cell = getters.getCell(...toCartesian(mainXc));
|
|
96
|
+
if (cell && cell.error) {
|
|
97
|
+
tooltip.isOpen = true;
|
|
98
|
+
tooltip.text = cell.error;
|
|
99
|
+
tooltipCol = col;
|
|
100
|
+
tooltipRow = row;
|
|
101
|
+
const viewport = getViewPort();
|
|
102
|
+
const [x, y, width, height] = env.getters.getRect(
|
|
103
|
+
{ left: col, top: row, right: col, bottom: row },
|
|
104
|
+
viewport
|
|
105
|
+
);
|
|
106
|
+
const hAlign = x + width + 200 < viewport.width ? "left" : "right";
|
|
107
|
+
const hOffset =
|
|
108
|
+
hAlign === "left" ? x + width : viewport.width - x + (SCROLLBAR_WIDTH + 2);
|
|
109
|
+
const vAlign = y + 120 < viewport.height ? "top" : "bottom";
|
|
110
|
+
const vOffset =
|
|
111
|
+
vAlign === "top" ? y : viewport.height - y - height + (SCROLLBAR_WIDTH + 2);
|
|
112
|
+
tooltip.style = `${hAlign}:${hOffset}px;${vAlign}:${vOffset}px`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onMounted(() => {
|
|
119
|
+
canvasRef.el!.addEventListener("mousemove", updateMousePosition);
|
|
120
|
+
interval = setInterval(checkTiming, 200);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
onWillUnmount(() => {
|
|
124
|
+
canvasRef.el!.removeEventListener("mousemove", updateMousePosition);
|
|
125
|
+
clearInterval(interval);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return tooltip;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function useTouchMove(handler: (deltaX: number, deltaY: number) => void, canMoveUp: () => boolean) {
|
|
132
|
+
const canvasRef = useRef("canvas");
|
|
133
|
+
let x = null as number | null;
|
|
134
|
+
let y = null as number | null;
|
|
135
|
+
function onTouchStart(ev: TouchEvent) {
|
|
136
|
+
if (ev.touches.length !== 1) return;
|
|
137
|
+
x = ev.touches[0].clientX;
|
|
138
|
+
y = ev.touches[0].clientY;
|
|
139
|
+
}
|
|
140
|
+
function onTouchEnd() {
|
|
141
|
+
x = null;
|
|
142
|
+
y = null;
|
|
143
|
+
}
|
|
144
|
+
function onTouchMove(ev: TouchEvent) {
|
|
145
|
+
if (ev.touches.length !== 1) return;
|
|
146
|
+
// On mobile browsers, swiping down is often associated with "pull to refresh".
|
|
147
|
+
// We only want this behavior if the grid is already at the top.
|
|
148
|
+
// Otherwise we only want to move the canvas up, without triggering any refresh.
|
|
149
|
+
if (canMoveUp()) {
|
|
150
|
+
ev.preventDefault();
|
|
151
|
+
ev.stopPropagation();
|
|
152
|
+
}
|
|
153
|
+
const currentX = ev.touches[0].clientX;
|
|
154
|
+
const currentY = ev.touches[0].clientY;
|
|
155
|
+
handler(x! - currentX, y! - currentY);
|
|
156
|
+
x = currentX;
|
|
157
|
+
y = currentY;
|
|
158
|
+
}
|
|
159
|
+
onMounted(() => {
|
|
160
|
+
canvasRef.el!.addEventListener("touchstart", onTouchStart);
|
|
161
|
+
canvasRef.el!.addEventListener("touchend", onTouchEnd);
|
|
162
|
+
canvasRef.el!.addEventListener("touchmove", onTouchMove);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
onWillUnmount(() => {
|
|
166
|
+
canvasRef.el!.removeEventListener("touchstart", onTouchStart);
|
|
167
|
+
canvasRef.el!.removeEventListener("touchend", onTouchEnd);
|
|
168
|
+
canvasRef.el!.removeEventListener("touchmove", onTouchMove);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// -----------------------------------------------------------------------------
|
|
173
|
+
// TEMPLATE
|
|
174
|
+
// -----------------------------------------------------------------------------
|
|
175
|
+
const TEMPLATE = xml/* xml */ `
|
|
176
|
+
<div class="o-grid" t-on-click="focus" t-on-keydown="onKeydown" t-on-wheel="onMouseWheel">
|
|
177
|
+
<t t-if="getters.getEditionMode() !== 'inactive'">
|
|
178
|
+
<Composer t-ref="composer" t-on-composer-unmounted="focus" viewport="snappedViewport"/>
|
|
179
|
+
</t>
|
|
180
|
+
<canvas t-ref="canvas"
|
|
181
|
+
t-on-mousedown="onMouseDown"
|
|
182
|
+
t-on-dblclick="onDoubleClick"
|
|
183
|
+
tabindex="-1"
|
|
184
|
+
t-on-contextmenu="onCanvasContextMenu"
|
|
185
|
+
/>
|
|
186
|
+
<t t-if="errorTooltip.isOpen">
|
|
187
|
+
<div class="o-error-tooltip" t-esc="errorTooltip.text" t-att-style="errorTooltip.style"/>
|
|
188
|
+
</t>
|
|
189
|
+
<t t-if="getters.getEditionMode() === 'inactive'">
|
|
190
|
+
<Autofill position="getAutofillPosition()" viewport="snappedViewport"/>
|
|
191
|
+
</t>
|
|
192
|
+
<Overlay t-on-open-contextmenu="onOverlayContextMenu" viewport="snappedViewport"/>
|
|
193
|
+
<Menu t-if="menuState.isOpen"
|
|
194
|
+
menuItems="menuState.menuItems"
|
|
195
|
+
position="menuState.position"
|
|
196
|
+
t-on-close.stop="menuState.isOpen=false"/>
|
|
197
|
+
<t t-set="gridSize" t-value="getters.getGridSize()"/>
|
|
198
|
+
<FiguresContainer viewport="snappedViewport" model="props.model" t-on-figure-deleted="focus" />
|
|
199
|
+
<div class="o-scrollbar vertical" t-on-scroll="onScroll" t-ref="vscrollbar">
|
|
200
|
+
<div t-attf-style="width:1px;height:{{gridSize[1]}}px"/>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="o-scrollbar horizontal" t-on-scroll="onScroll" t-ref="hscrollbar">
|
|
203
|
+
<div t-attf-style="height:1px;width:{{gridSize[0]}}px"/>
|
|
204
|
+
</div>
|
|
205
|
+
</div>`;
|
|
206
|
+
|
|
207
|
+
// -----------------------------------------------------------------------------
|
|
208
|
+
// STYLE
|
|
209
|
+
// -----------------------------------------------------------------------------
|
|
210
|
+
const CSS = css/* scss */ `
|
|
211
|
+
.o-grid {
|
|
212
|
+
position: relative;
|
|
213
|
+
overflow: hidden;
|
|
214
|
+
background-color: ${BACKGROUND_GRAY_COLOR};
|
|
215
|
+
|
|
216
|
+
> canvas {
|
|
217
|
+
border-top: 1px solid #e2e3e3;
|
|
218
|
+
border-bottom: 1px solid #e2e3e3;
|
|
219
|
+
|
|
220
|
+
&:focus {
|
|
221
|
+
outline: none;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
.o-error-tooltip {
|
|
225
|
+
position: absolute;
|
|
226
|
+
font-size: 13px;
|
|
227
|
+
width: 180px;
|
|
228
|
+
height: 80px;
|
|
229
|
+
background-color: white;
|
|
230
|
+
box-shadow: 0 1px 4px 3px rgba(60, 64, 67, 0.15);
|
|
231
|
+
border-left: 3px solid red;
|
|
232
|
+
padding: 10px;
|
|
233
|
+
}
|
|
234
|
+
.o-scrollbar {
|
|
235
|
+
position: absolute;
|
|
236
|
+
overflow: auto;
|
|
237
|
+
z-index: 2;
|
|
238
|
+
&.vertical {
|
|
239
|
+
right: 0;
|
|
240
|
+
top: ${HEADER_HEIGHT}px;
|
|
241
|
+
bottom: ${SCROLLBAR_WIDTH}px;
|
|
242
|
+
width: ${SCROLLBAR_WIDTH}px;
|
|
243
|
+
overflow-x: hidden;
|
|
244
|
+
}
|
|
245
|
+
&.horizontal {
|
|
246
|
+
bottom: 0;
|
|
247
|
+
height: ${SCROLLBAR_WIDTH}px;
|
|
248
|
+
right: ${SCROLLBAR_WIDTH + 1}px;
|
|
249
|
+
left: ${HEADER_WIDTH}px;
|
|
250
|
+
overflow-y: hidden;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
// -----------------------------------------------------------------------------
|
|
257
|
+
// JS
|
|
258
|
+
// -----------------------------------------------------------------------------
|
|
259
|
+
export class Grid extends Component<{ model: Model }, SpreadsheetEnv> {
|
|
260
|
+
static template = TEMPLATE;
|
|
261
|
+
static style = CSS;
|
|
262
|
+
static components = { Composer, Overlay, Menu, Autofill, FiguresContainer };
|
|
263
|
+
|
|
264
|
+
private menuState: MenuState = useState({
|
|
265
|
+
isOpen: false,
|
|
266
|
+
position: null,
|
|
267
|
+
menuItems: [],
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
private composer = useRef("composer");
|
|
271
|
+
|
|
272
|
+
private vScrollbarRef = useRef("vscrollbar");
|
|
273
|
+
private hScrollbarRef = useRef("hscrollbar");
|
|
274
|
+
private vScrollbar: ScrollBar;
|
|
275
|
+
private hScrollbar: ScrollBar;
|
|
276
|
+
private canvas = useRef("canvas");
|
|
277
|
+
private getters = this.env.getters;
|
|
278
|
+
private dispatch = this.env.dispatch;
|
|
279
|
+
private currentPosition = this.getters.getPosition();
|
|
280
|
+
private currentSheet = this.getters.getActiveSheet();
|
|
281
|
+
|
|
282
|
+
private currentPositionInViewport = false;
|
|
283
|
+
private clickedCol = 0;
|
|
284
|
+
private clickedRow = 0;
|
|
285
|
+
private viewport: Viewport = {
|
|
286
|
+
width: 0,
|
|
287
|
+
height: 0,
|
|
288
|
+
offsetX: 0,
|
|
289
|
+
offsetY: 0,
|
|
290
|
+
left: 0,
|
|
291
|
+
top: 0,
|
|
292
|
+
right: 0,
|
|
293
|
+
bottom: 0,
|
|
294
|
+
};
|
|
295
|
+
// this viewport represent the same area as the previous one, but 'snapped' to
|
|
296
|
+
// the col/row structure, so, the offsets are correct for computations necessary
|
|
297
|
+
// to align elements to the grid.
|
|
298
|
+
private snappedViewport: Viewport = this.viewport;
|
|
299
|
+
errorTooltip = useErrorTooltip(this.env, () => this.snappedViewport);
|
|
300
|
+
|
|
301
|
+
// this map will handle most of the actions that should happen on key down. The arrow keys are managed in the key
|
|
302
|
+
// down itself
|
|
303
|
+
private keyDownMapping: { [key: string]: Function } = {
|
|
304
|
+
ENTER: () => this.dispatch("START_EDITION"),
|
|
305
|
+
TAB: () => this.dispatch("MOVE_POSITION", { deltaX: 1, deltaY: 0 }),
|
|
306
|
+
"SHIFT+TAB": () => this.dispatch("MOVE_POSITION", { deltaX: -1, deltaY: 0 }),
|
|
307
|
+
F2: () => this.dispatch("START_EDITION"),
|
|
308
|
+
DELETE: () => {
|
|
309
|
+
this.dispatch("DELETE_CONTENT", {
|
|
310
|
+
sheet: this.getters.getActiveSheet(),
|
|
311
|
+
target: this.getters.getSelectedZones(),
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
"CTRL+A": () => this.dispatch("SELECT_ALL"),
|
|
315
|
+
"CTRL+S": () => {
|
|
316
|
+
this.trigger("save-requested");
|
|
317
|
+
},
|
|
318
|
+
"CTRL+Z": () => this.dispatch("UNDO"),
|
|
319
|
+
"CTRL+Y": () => this.dispatch("REDO"),
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
constructor() {
|
|
323
|
+
super(...arguments);
|
|
324
|
+
this.vScrollbar = new ScrollBar(this.vScrollbarRef.el, "vertical");
|
|
325
|
+
this.hScrollbar = new ScrollBar(this.hScrollbarRef.el, "horizontal");
|
|
326
|
+
useTouchMove(this.moveCanvas.bind(this), () => this.vScrollbar.scroll > 0);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
mounted() {
|
|
330
|
+
this.vScrollbar.el = this.vScrollbarRef.el!;
|
|
331
|
+
this.hScrollbar.el = this.hScrollbarRef.el!;
|
|
332
|
+
this.focus();
|
|
333
|
+
this.drawGrid();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async willUpdateProps() {
|
|
337
|
+
const sheet = this.getters.getActiveSheet();
|
|
338
|
+
if (this.currentSheet !== sheet) {
|
|
339
|
+
// We need to reset the viewport as the sheet is changed
|
|
340
|
+
this.currentPositionInViewport = true;
|
|
341
|
+
this.viewport.offsetX = 0;
|
|
342
|
+
this.viewport.offsetY = 0;
|
|
343
|
+
this.hScrollbar.scroll = 0;
|
|
344
|
+
this.vScrollbar.scroll = 0;
|
|
345
|
+
this.viewport = this.getters.adjustViewportZone(this.viewport);
|
|
346
|
+
this.viewport = this.getters.adjustViewportPosition(this.viewport);
|
|
347
|
+
this.snappedViewport = this.getters.snapViewportToCell(this.viewport);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
patched() {
|
|
352
|
+
this.drawGrid();
|
|
353
|
+
}
|
|
354
|
+
focus() {
|
|
355
|
+
if (this.getters.getEditionMode() !== "selecting" && !this.getters.getSelectedFigureId()) {
|
|
356
|
+
this.canvas.el!.focus();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
onScroll() {
|
|
361
|
+
this.viewport.offsetX = this.hScrollbar.scroll;
|
|
362
|
+
this.viewport.offsetY = this.vScrollbar.scroll;
|
|
363
|
+
const viewport = this.getters.adjustViewportZone(this.viewport);
|
|
364
|
+
if (!isEqual(viewport, this.viewport)) {
|
|
365
|
+
this.viewport = viewport;
|
|
366
|
+
this.render();
|
|
367
|
+
}
|
|
368
|
+
this.snappedViewport = this.getters.snapViewportToCell(this.viewport);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
checkChanges(): boolean {
|
|
372
|
+
const [col, row] = this.getters.getPosition();
|
|
373
|
+
const [curCol, curRow] = this.currentPosition;
|
|
374
|
+
const currentSheet = this.getters.getActiveSheet();
|
|
375
|
+
const changed = currentSheet !== this.currentSheet || col !== curCol || row !== curRow;
|
|
376
|
+
if (changed) {
|
|
377
|
+
this.currentPosition = [col, row];
|
|
378
|
+
}
|
|
379
|
+
if (currentSheet !== this.currentSheet) {
|
|
380
|
+
this.focus();
|
|
381
|
+
this.currentSheet = currentSheet;
|
|
382
|
+
}
|
|
383
|
+
return changed;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
getAutofillPosition() {
|
|
387
|
+
const zone = this.getters.getSelectedZone();
|
|
388
|
+
const sheet = this.getters.getActiveSheet();
|
|
389
|
+
return {
|
|
390
|
+
left:
|
|
391
|
+
this.getters.getCol(sheet, zone.right).end -
|
|
392
|
+
4 +
|
|
393
|
+
HEADER_WIDTH -
|
|
394
|
+
this.snappedViewport.offsetX,
|
|
395
|
+
top:
|
|
396
|
+
this.getters.getRow(sheet, zone.bottom).end -
|
|
397
|
+
4 +
|
|
398
|
+
HEADER_HEIGHT -
|
|
399
|
+
this.snappedViewport.offsetY,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
drawGrid() {
|
|
404
|
+
// update viewport dimensions
|
|
405
|
+
// resize window
|
|
406
|
+
this.viewport.width = this.el!.clientWidth - SCROLLBAR_WIDTH;
|
|
407
|
+
this.viewport.height = this.el!.clientHeight - SCROLLBAR_WIDTH;
|
|
408
|
+
|
|
409
|
+
// scrollbar scrolled
|
|
410
|
+
this.viewport.offsetX = this.hScrollbar.scroll;
|
|
411
|
+
this.viewport.offsetY = this.vScrollbar.scroll;
|
|
412
|
+
|
|
413
|
+
// needed to reset the bottom and the right on the current viewport to the one of the new
|
|
414
|
+
// active sheet or in any case, the number of cols & rows might have changed.
|
|
415
|
+
this.viewport = this.getters.adjustViewportZone(this.viewport);
|
|
416
|
+
|
|
417
|
+
// check for position changes
|
|
418
|
+
if (this.checkChanges() && this.currentPositionInViewport) {
|
|
419
|
+
this.currentPositionInViewport = false;
|
|
420
|
+
this.viewport = this.getters.adjustViewportPosition(this.viewport);
|
|
421
|
+
this.hScrollbar.scroll = this.viewport.offsetX;
|
|
422
|
+
this.vScrollbar.scroll = this.viewport.offsetY;
|
|
423
|
+
}
|
|
424
|
+
this.snappedViewport = this.getters.snapViewportToCell(this.viewport);
|
|
425
|
+
|
|
426
|
+
// drawing grid on canvas
|
|
427
|
+
const canvas = this.canvas.el as HTMLCanvasElement;
|
|
428
|
+
const dpr = window.devicePixelRatio || 1;
|
|
429
|
+
const ctx = canvas.getContext("2d", { alpha: false })!;
|
|
430
|
+
const thinLineWidth = 0.4 * dpr;
|
|
431
|
+
const renderingContext = { ctx, viewport: this.viewport, dpr, thinLineWidth };
|
|
432
|
+
const { width, height } = this.viewport;
|
|
433
|
+
canvas.style.width = `${width}px`;
|
|
434
|
+
canvas.style.height = `${height}px`;
|
|
435
|
+
canvas.width = width * dpr;
|
|
436
|
+
canvas.height = height * dpr;
|
|
437
|
+
canvas.setAttribute("style", `width:${width}px;height:${height}px;`);
|
|
438
|
+
ctx.translate(-0.5, -0.5);
|
|
439
|
+
ctx.scale(dpr, dpr);
|
|
440
|
+
this.props.model.drawGrid(renderingContext);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private moveCanvas(deltaX, deltaY) {
|
|
444
|
+
this.vScrollbar.scroll = this.vScrollbar.scroll + deltaY;
|
|
445
|
+
this.hScrollbar.scroll = this.hScrollbar.scroll + deltaX;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
onMouseWheel(ev: WheelEvent) {
|
|
449
|
+
function normalize(val: number): number {
|
|
450
|
+
return val * (ev.deltaMode === 0 ? 1 : DEFAULT_CELL_HEIGHT);
|
|
451
|
+
}
|
|
452
|
+
this.moveCanvas(normalize(ev.deltaX), normalize(ev.deltaY));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
// Zone selection with mouse
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
getCartesianCoordinates(ev: MouseEvent): [number, number] {
|
|
460
|
+
const rect = this.el!.getBoundingClientRect();
|
|
461
|
+
const x = ev.pageX - rect.left;
|
|
462
|
+
const y = ev.pageY - rect.top;
|
|
463
|
+
const colIndex = this.getters.getColIndex(x, this.snappedViewport.left);
|
|
464
|
+
const rowIndex = this.getters.getRowIndex(y, this.snappedViewport.top);
|
|
465
|
+
return [colIndex, rowIndex];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
onMouseDown(ev: MouseEvent) {
|
|
469
|
+
if (ev.button > 0) {
|
|
470
|
+
// not main button, probably a context menu
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const [col, row] = this.getCartesianCoordinates(ev);
|
|
474
|
+
if (col < 0 || row < 0) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
this.clickedCol = col;
|
|
478
|
+
this.clickedRow = row;
|
|
479
|
+
|
|
480
|
+
this.dispatch(ev.ctrlKey ? "START_SELECTION_EXPANSION" : "START_SELECTION");
|
|
481
|
+
if (ev.shiftKey) {
|
|
482
|
+
this.dispatch("ALTER_SELECTION", { cell: [col, row] });
|
|
483
|
+
} else {
|
|
484
|
+
this.dispatch("SELECT_CELL", { col, row });
|
|
485
|
+
this.checkChanges();
|
|
486
|
+
}
|
|
487
|
+
let prevCol = col;
|
|
488
|
+
let prevRow = row;
|
|
489
|
+
const onMouseMove = (ev: MouseEvent) => {
|
|
490
|
+
const [col, row] = this.getCartesianCoordinates(ev);
|
|
491
|
+
if (col < 0 || row < 0) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (col !== prevCol || row !== prevRow) {
|
|
495
|
+
prevCol = col;
|
|
496
|
+
prevRow = row;
|
|
497
|
+
this.dispatch("ALTER_SELECTION", { cell: [col, row] });
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
const onMouseUp = (ev: MouseEvent) => {
|
|
501
|
+
this.dispatch(ev.ctrlKey ? "PREPARE_SELECTION_EXPANSION" : "STOP_SELECTION");
|
|
502
|
+
if (this.getters.getEditionMode() === "selecting") {
|
|
503
|
+
if (this.composer.comp) {
|
|
504
|
+
(this.composer.comp as Composer).addTextFromSelection();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
this.canvas.el!.removeEventListener("mousemove", onMouseMove);
|
|
508
|
+
if (this.getters.isPaintingFormat()) {
|
|
509
|
+
this.dispatch("PASTE", {
|
|
510
|
+
target: this.getters.getSelectedZones(),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
startDnd(onMouseMove, onMouseUp);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
onDoubleClick(ev) {
|
|
519
|
+
const [col, row] = this.getCartesianCoordinates(ev);
|
|
520
|
+
if (this.clickedCol === col && this.clickedRow === row) {
|
|
521
|
+
this.dispatch("START_EDITION");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
// Keyboard interactions
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
|
|
528
|
+
processTabKey(ev: KeyboardEvent) {
|
|
529
|
+
ev.preventDefault();
|
|
530
|
+
const deltaX = ev.shiftKey ? -1 : 1;
|
|
531
|
+
this.dispatch("MOVE_POSITION", { deltaX, deltaY: 0 });
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
processArrows(ev: KeyboardEvent) {
|
|
535
|
+
ev.preventDefault();
|
|
536
|
+
ev.stopPropagation();
|
|
537
|
+
const deltaMap = {
|
|
538
|
+
ArrowDown: [0, 1],
|
|
539
|
+
ArrowLeft: [-1, 0],
|
|
540
|
+
ArrowRight: [1, 0],
|
|
541
|
+
ArrowUp: [0, -1],
|
|
542
|
+
};
|
|
543
|
+
this.currentPositionInViewport = true;
|
|
544
|
+
const delta = deltaMap[ev.key];
|
|
545
|
+
if (ev.shiftKey) {
|
|
546
|
+
this.dispatch("ALTER_SELECTION", { delta });
|
|
547
|
+
} else {
|
|
548
|
+
this.dispatch("MOVE_POSITION", { deltaX: delta[0], deltaY: delta[1] });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (this.getters.getEditionMode() === "selecting" && this.composer.comp) {
|
|
552
|
+
(this.composer.comp as Composer).addTextFromSelection();
|
|
553
|
+
} else if (this.getters.isPaintingFormat()) {
|
|
554
|
+
this.dispatch("PASTE", {
|
|
555
|
+
target: this.getters.getSelectedZones(),
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
onKeydown(ev: KeyboardEvent) {
|
|
561
|
+
if (ev.key.startsWith("Arrow")) {
|
|
562
|
+
this.processArrows(ev);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
let keyDownString = "";
|
|
567
|
+
if (ev.ctrlKey) keyDownString += "CTRL+";
|
|
568
|
+
if (ev.metaKey) keyDownString += "CTRL+";
|
|
569
|
+
if (ev.altKey) keyDownString += "ALT+";
|
|
570
|
+
if (ev.shiftKey) keyDownString += "SHIFT+";
|
|
571
|
+
keyDownString += ev.key.toUpperCase();
|
|
572
|
+
|
|
573
|
+
let handler = this.keyDownMapping[keyDownString];
|
|
574
|
+
if (handler) {
|
|
575
|
+
ev.preventDefault();
|
|
576
|
+
ev.stopPropagation();
|
|
577
|
+
handler();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (!keyDownMappingIgnore.includes(keyDownString)) {
|
|
581
|
+
if (ev.key.length === 1 && !ev.ctrlKey && !ev.metaKey && !ev.altKey) {
|
|
582
|
+
// if the user types a character on the grid, it means he wants to start composing the selected cell with that
|
|
583
|
+
// character
|
|
584
|
+
ev.preventDefault();
|
|
585
|
+
ev.stopPropagation();
|
|
586
|
+
this.dispatch("START_EDITION", { text: ev.key });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
// Context Menu
|
|
593
|
+
// ---------------------------------------------------------------------------
|
|
594
|
+
|
|
595
|
+
onCanvasContextMenu(ev: MouseEvent) {
|
|
596
|
+
ev.preventDefault();
|
|
597
|
+
const [col, row] = this.getCartesianCoordinates(ev);
|
|
598
|
+
if (col < 0 || row < 0) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const zones = this.getters.getSelectedZones();
|
|
602
|
+
const lastZone = zones[zones.length - 1];
|
|
603
|
+
let type: ContextMenuType = "CELL";
|
|
604
|
+
if (!isInside(col, row, lastZone)) {
|
|
605
|
+
this.dispatch("SELECT_CELL", { col, row });
|
|
606
|
+
} else {
|
|
607
|
+
if (this.getters.getActiveCols().has(col)) {
|
|
608
|
+
type = "COL";
|
|
609
|
+
} else if (this.getters.getActiveRows().has(row)) {
|
|
610
|
+
type = "ROW";
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
this.toggleContextMenu(type, ev.offsetX, ev.offsetY);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
onOverlayContextMenu(ev: CustomEvent) {
|
|
617
|
+
const type = ev.detail.type as ContextMenuType;
|
|
618
|
+
const x = ev.detail.x;
|
|
619
|
+
const y = ev.detail.y;
|
|
620
|
+
this.toggleContextMenu(type, x, y);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
toggleContextMenu(type: ContextMenuType, x: number, y: number) {
|
|
624
|
+
this.menuState.isOpen = true;
|
|
625
|
+
this.menuState.position = {
|
|
626
|
+
x,
|
|
627
|
+
y,
|
|
628
|
+
width: this.el!.clientWidth,
|
|
629
|
+
height: this.el!.clientHeight,
|
|
630
|
+
};
|
|
631
|
+
this.menuState.menuItems = registries[type]
|
|
632
|
+
.getAll()
|
|
633
|
+
.filter((item) => !item.isVisible || item.isVisible(this.env));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type EventFn = (ev: MouseEvent) => void;
|
|
2
|
+
|
|
3
|
+
export function startDnd(onMouseMove: EventFn, onMouseUp: EventFn) {
|
|
4
|
+
const _onMouseUp = (ev: MouseEvent) => {
|
|
5
|
+
onMouseUp(ev);
|
|
6
|
+
window.removeEventListener("mouseup", _onMouseUp);
|
|
7
|
+
window.removeEventListener("dragstart", _onDragStart);
|
|
8
|
+
window.removeEventListener("mousemove", onMouseMove);
|
|
9
|
+
};
|
|
10
|
+
function _onDragStart(ev: DragEvent) {
|
|
11
|
+
ev.preventDefault();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
window.addEventListener("mouseup", _onMouseUp);
|
|
15
|
+
window.addEventListener("dragstart", _onDragStart);
|
|
16
|
+
window.addEventListener("mousemove", onMouseMove);
|
|
17
|
+
}
|