@dxos/plugin-sheet 0.6.8-main.046e6cf
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/LICENSE +8 -0
- package/README.md +14 -0
- package/dist/lib/browser/SheetContainer-H22IDJ43.mjs +3740 -0
- package/dist/lib/browser/SheetContainer-H22IDJ43.mjs.map +7 -0
- package/dist/lib/browser/chunk-6VPEAUG6.mjs +82 -0
- package/dist/lib/browser/chunk-6VPEAUG6.mjs.map +7 -0
- package/dist/lib/browser/chunk-AT2FJXQX.mjs +861 -0
- package/dist/lib/browser/chunk-AT2FJXQX.mjs.map +7 -0
- package/dist/lib/browser/chunk-JRL5LGCE.mjs +18 -0
- package/dist/lib/browser/chunk-JRL5LGCE.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +213 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/meta.mjs +9 -0
- package/dist/lib/browser/meta.mjs.map +7 -0
- package/dist/lib/browser/types.mjs +22 -0
- package/dist/lib/browser/types.mjs.map +7 -0
- package/dist/lib/node/SheetContainer-S32KTNZ6.cjs +3731 -0
- package/dist/lib/node/SheetContainer-S32KTNZ6.cjs.map +7 -0
- package/dist/lib/node/chunk-4CE6FK5Z.cjs +108 -0
- package/dist/lib/node/chunk-4CE6FK5Z.cjs.map +7 -0
- package/dist/lib/node/chunk-BJ6ZD7MN.cjs +51 -0
- package/dist/lib/node/chunk-BJ6ZD7MN.cjs.map +7 -0
- package/dist/lib/node/chunk-FCKJ4QRM.cjs +881 -0
- package/dist/lib/node/chunk-FCKJ4QRM.cjs.map +7 -0
- package/dist/lib/node/index.cjs +226 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.cjs +30 -0
- package/dist/lib/node/meta.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/lib/node/types.cjs +44 -0
- package/dist/lib/node/types.cjs.map +7 -0
- package/dist/types/src/SheetPlugin.d.ts +4 -0
- package/dist/types/src/SheetPlugin.d.ts.map +1 -0
- package/dist/types/src/components/CellEditor/CellEditor.d.ts +14 -0
- package/dist/types/src/components/CellEditor/CellEditor.d.ts.map +1 -0
- package/dist/types/src/components/CellEditor/CellEditor.stories.d.ts +29 -0
- package/dist/types/src/components/CellEditor/CellEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/CellEditor/extension.d.ts +18 -0
- package/dist/types/src/components/CellEditor/extension.d.ts.map +1 -0
- package/dist/types/src/components/CellEditor/extension.test.d.ts +2 -0
- package/dist/types/src/components/CellEditor/extension.test.d.ts.map +1 -0
- package/dist/types/src/components/CellEditor/functions.d.ts +66 -0
- package/dist/types/src/components/CellEditor/functions.d.ts.map +1 -0
- package/dist/types/src/components/CellEditor/index.d.ts +3 -0
- package/dist/types/src/components/CellEditor/index.d.ts.map +1 -0
- package/dist/types/src/components/ComputeGraph/async-function.d.ts +52 -0
- package/dist/types/src/components/ComputeGraph/async-function.d.ts.map +1 -0
- package/dist/types/src/components/ComputeGraph/custom.d.ts +21 -0
- package/dist/types/src/components/ComputeGraph/custom.d.ts.map +1 -0
- package/dist/types/src/components/ComputeGraph/edge-function.d.ts +20 -0
- package/dist/types/src/components/ComputeGraph/edge-function.d.ts.map +1 -0
- package/dist/types/src/components/ComputeGraph/graph-context.d.ts +11 -0
- package/dist/types/src/components/ComputeGraph/graph-context.d.ts.map +1 -0
- package/dist/types/src/components/ComputeGraph/graph.browser.test.d.ts +2 -0
- package/dist/types/src/components/ComputeGraph/graph.browser.test.d.ts.map +1 -0
- package/dist/types/src/components/ComputeGraph/graph.d.ts +21 -0
- package/dist/types/src/components/ComputeGraph/graph.d.ts.map +1 -0
- package/dist/types/src/components/ComputeGraph/index.d.ts +4 -0
- package/dist/types/src/components/ComputeGraph/index.d.ts.map +1 -0
- package/dist/types/src/components/Sheet/Sheet.d.ts +55 -0
- package/dist/types/src/components/Sheet/Sheet.d.ts.map +1 -0
- package/dist/types/src/components/Sheet/Sheet.stories.d.ts +54 -0
- package/dist/types/src/components/Sheet/Sheet.stories.d.ts.map +1 -0
- package/dist/types/src/components/Sheet/formatting.d.ts +14 -0
- package/dist/types/src/components/Sheet/formatting.d.ts.map +1 -0
- package/dist/types/src/components/Sheet/grid.d.ts +52 -0
- package/dist/types/src/components/Sheet/grid.d.ts.map +1 -0
- package/dist/types/src/components/Sheet/index.d.ts +2 -0
- package/dist/types/src/components/Sheet/index.d.ts.map +1 -0
- package/dist/types/src/components/Sheet/nav.d.ts +29 -0
- package/dist/types/src/components/Sheet/nav.d.ts.map +1 -0
- package/dist/types/src/components/Sheet/sheet-context.d.ts +24 -0
- package/dist/types/src/components/Sheet/sheet-context.d.ts.map +1 -0
- package/dist/types/src/components/Sheet/util.d.ts +18 -0
- package/dist/types/src/components/Sheet/util.d.ts.map +1 -0
- package/dist/types/src/components/SheetContainer.d.ts +9 -0
- package/dist/types/src/components/SheetContainer.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +21 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +35 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/common.d.ts +20 -0
- package/dist/types/src/components/Toolbar/common.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/index.d.ts +2 -0
- package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +7 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +15 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/model/index.d.ts +3 -0
- package/dist/types/src/model/index.d.ts.map +1 -0
- package/dist/types/src/model/model.browser.test.d.ts +2 -0
- package/dist/types/src/model/model.browser.test.d.ts.map +1 -0
- package/dist/types/src/model/model.d.ts +142 -0
- package/dist/types/src/model/model.d.ts.map +1 -0
- package/dist/types/src/model/types.d.ts +17 -0
- package/dist/types/src/model/types.d.ts.map +1 -0
- package/dist/types/src/model/types.test.d.ts +2 -0
- package/dist/types/src/model/types.test.d.ts.map +1 -0
- package/dist/types/src/model/util.d.ts +15 -0
- package/dist/types/src/model/util.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +16 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +94 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/package.json +122 -0
- package/src/SheetPlugin.tsx +150 -0
- package/src/components/CellEditor/CellEditor.stories.tsx +88 -0
- package/src/components/CellEditor/CellEditor.tsx +113 -0
- package/src/components/CellEditor/extension.test.ts +42 -0
- package/src/components/CellEditor/extension.ts +286 -0
- package/src/components/CellEditor/functions.ts +2017 -0
- package/src/components/CellEditor/index.ts +6 -0
- package/src/components/ComputeGraph/async-function.ts +148 -0
- package/src/components/ComputeGraph/custom.ts +70 -0
- package/src/components/ComputeGraph/edge-function.ts +60 -0
- package/src/components/ComputeGraph/graph-context.tsx +37 -0
- package/src/components/ComputeGraph/graph.browser.test.ts +49 -0
- package/src/components/ComputeGraph/graph.ts +52 -0
- package/src/components/ComputeGraph/index.ts +7 -0
- package/src/components/Sheet/Sheet.stories.tsx +329 -0
- package/src/components/Sheet/Sheet.tsx +1164 -0
- package/src/components/Sheet/formatting.ts +106 -0
- package/src/components/Sheet/grid.ts +191 -0
- package/src/components/Sheet/index.ts +5 -0
- package/src/components/Sheet/nav.ts +157 -0
- package/src/components/Sheet/sheet-context.tsx +101 -0
- package/src/components/Sheet/util.ts +56 -0
- package/src/components/SheetContainer.tsx +30 -0
- package/src/components/Toolbar/Toolbar.stories.tsx +36 -0
- package/src/components/Toolbar/Toolbar.tsx +198 -0
- package/src/components/Toolbar/common.tsx +72 -0
- package/src/components/Toolbar/index.ts +5 -0
- package/src/components/index.ts +10 -0
- package/src/index.ts +9 -0
- package/src/meta.tsx +18 -0
- package/src/model/index.ts +6 -0
- package/src/model/model.browser.test.ts +100 -0
- package/src/model/model.ts +480 -0
- package/src/model/types.test.ts +92 -0
- package/src/model/types.ts +71 -0
- package/src/model/util.ts +36 -0
- package/src/translations.ts +22 -0
- package/src/types.ts +110 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { DetailedCellError, ExportedCellChange } from 'hyperformula';
|
|
6
|
+
import { type SimpleCellRange } from 'hyperformula/typings/AbsoluteCellRange';
|
|
7
|
+
import { type SimpleCellAddress } from 'hyperformula/typings/Cell';
|
|
8
|
+
import { type SimpleDate, type SimpleDateTime } from 'hyperformula/typings/DateTimeHelper';
|
|
9
|
+
|
|
10
|
+
import { Event } from '@dxos/async';
|
|
11
|
+
import { Context } from '@dxos/context';
|
|
12
|
+
import { invariant } from '@dxos/invariant';
|
|
13
|
+
import { PublicKey } from '@dxos/keys';
|
|
14
|
+
import { log } from '@dxos/log';
|
|
15
|
+
|
|
16
|
+
import { addressFromA1Notation, addressToA1Notation, type CellAddress, type CellRange } from './types';
|
|
17
|
+
import { createIndices, RangeException, ReadonlyException } from './util';
|
|
18
|
+
import { type ComputeGraph } from '../components';
|
|
19
|
+
import { type CellScalarValue, type CellValue, type SheetType, ValueTypeEnum } from '../types';
|
|
20
|
+
|
|
21
|
+
// TODO(burdon): Defaults or Max?
|
|
22
|
+
const DEFAULT_ROWS = 500;
|
|
23
|
+
const DEFAULT_COLUMNS = 26 * 2;
|
|
24
|
+
|
|
25
|
+
export type CellIndex = string;
|
|
26
|
+
|
|
27
|
+
export type CellContentValue = number | string | boolean | null;
|
|
28
|
+
|
|
29
|
+
export type SheetModelOptions = {
|
|
30
|
+
readonly?: boolean;
|
|
31
|
+
rows: number;
|
|
32
|
+
columns: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const typeMap: Record<string, ValueTypeEnum> = {
|
|
36
|
+
BOOLEAN: ValueTypeEnum.Boolean,
|
|
37
|
+
NUMBER_RAW: ValueTypeEnum.Number,
|
|
38
|
+
NUMBER_PERCENT: ValueTypeEnum.Percent,
|
|
39
|
+
NUMBER_CURRENCY: ValueTypeEnum.Currency,
|
|
40
|
+
NUMBER_DATETIME: ValueTypeEnum.DateTime,
|
|
41
|
+
NUMBER_DATE: ValueTypeEnum.Date,
|
|
42
|
+
NUMBER_TIME: ValueTypeEnum.Time,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const defaultOptions: SheetModelOptions = {
|
|
46
|
+
rows: 50,
|
|
47
|
+
columns: 26,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getTopLeft = (range: CellRange) => {
|
|
51
|
+
const to = range.to ?? range.from;
|
|
52
|
+
return { row: Math.min(range.from.row, to.row), column: Math.min(range.from.column, to.column) };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const toSimpleCellAddress = (sheet: number, cell: CellAddress): SimpleCellAddress => ({
|
|
56
|
+
sheet,
|
|
57
|
+
row: cell.row,
|
|
58
|
+
col: cell.column,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const toModelRange = (sheet: number, range: CellRange): SimpleCellRange => ({
|
|
62
|
+
start: toSimpleCellAddress(sheet, range.from),
|
|
63
|
+
end: toSimpleCellAddress(sheet, range.to ?? range.from),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Spreadsheet data model.
|
|
68
|
+
*
|
|
69
|
+
* [ComputeGraphContext] > [SheetContext]:[SheetModel] > [Sheet.Root]
|
|
70
|
+
*/
|
|
71
|
+
export class SheetModel {
|
|
72
|
+
public readonly id = `model-${PublicKey.random().truncate()}`;
|
|
73
|
+
private _ctx?: Context = undefined;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Formula engine.
|
|
77
|
+
* Acts as a write through cache for scalar and computed values.
|
|
78
|
+
*/
|
|
79
|
+
private readonly _sheetId: number;
|
|
80
|
+
private readonly _options: SheetModelOptions;
|
|
81
|
+
|
|
82
|
+
public readonly update = new Event();
|
|
83
|
+
|
|
84
|
+
constructor(
|
|
85
|
+
private readonly _graph: ComputeGraph,
|
|
86
|
+
private readonly _sheet: SheetType,
|
|
87
|
+
options: Partial<SheetModelOptions> = {},
|
|
88
|
+
) {
|
|
89
|
+
// Sheet for this object.
|
|
90
|
+
const name = this._sheet.id;
|
|
91
|
+
if (!this._graph.hf.doesSheetExist(name)) {
|
|
92
|
+
this._graph.hf.addSheet(name);
|
|
93
|
+
}
|
|
94
|
+
this._sheetId = this._graph.hf.getSheetId(name)!;
|
|
95
|
+
this._options = { ...defaultOptions, ...options };
|
|
96
|
+
this.reset();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get graph() {
|
|
100
|
+
return this._graph;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get sheet() {
|
|
104
|
+
return this._sheet;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get readonly() {
|
|
108
|
+
return this._options.readonly;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get bounds() {
|
|
112
|
+
return {
|
|
113
|
+
rows: this._sheet.rows.length,
|
|
114
|
+
columns: this._sheet.columns.length,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get functions(): string[] {
|
|
119
|
+
return this._graph.hf.getRegisteredFunctionNames();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get initialized(): boolean {
|
|
123
|
+
return !!this._ctx;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Initialize sheet and engine.
|
|
128
|
+
*/
|
|
129
|
+
async initialize() {
|
|
130
|
+
log('initialize', { id: this.id });
|
|
131
|
+
invariant(!this.initialized, 'Already initialized.');
|
|
132
|
+
this._ctx = new Context();
|
|
133
|
+
if (!this._sheet.rows.length) {
|
|
134
|
+
this._insertIndices(this._sheet.rows, 0, this._options.rows, DEFAULT_ROWS);
|
|
135
|
+
}
|
|
136
|
+
if (!this._sheet.columns.length) {
|
|
137
|
+
this._insertIndices(this._sheet.columns, 0, this._options.columns, DEFAULT_COLUMNS);
|
|
138
|
+
}
|
|
139
|
+
this.reset();
|
|
140
|
+
|
|
141
|
+
// Listen for model updates (e.g., async calculations).
|
|
142
|
+
const unsubscribe = this._graph.update.on(() => this.update.emit());
|
|
143
|
+
this._ctx.onDispose(unsubscribe);
|
|
144
|
+
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async destroy() {
|
|
149
|
+
log('destroy', { id: this.id });
|
|
150
|
+
if (this._ctx) {
|
|
151
|
+
await this._ctx.dispose();
|
|
152
|
+
this._ctx = undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Update engine.
|
|
158
|
+
* NOTE: This resets the undo history.
|
|
159
|
+
* @deprecated
|
|
160
|
+
*/
|
|
161
|
+
reset() {
|
|
162
|
+
this._graph.hf.clearSheet(this._sheetId);
|
|
163
|
+
Object.entries(this._sheet.cells).forEach(([key, { value }]) => {
|
|
164
|
+
const { column, row } = this.addressFromIndex(key);
|
|
165
|
+
if (typeof value === 'string' && value.charAt(0) === '=') {
|
|
166
|
+
value = this.mapFormulaIndicesToRefs(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this._graph.hf.setCellContents({ sheet: this._sheetId, row, col: column }, value);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Recalculate formulas.
|
|
175
|
+
* NOTE: This resets the undo history.
|
|
176
|
+
* https://hyperformula.handsontable.com/guide/volatile-functions.html#volatile-actions
|
|
177
|
+
* @deprecated
|
|
178
|
+
*/
|
|
179
|
+
// TODO(burdon): Remove.
|
|
180
|
+
recalculate() {
|
|
181
|
+
this._graph.hf.rebuildAndRecalculate();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
insertRows(i: number, n = 1) {
|
|
185
|
+
this._insertIndices(this._sheet.rows, i, n, DEFAULT_ROWS);
|
|
186
|
+
this.reset();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
insertColumns(i: number, n = 1) {
|
|
190
|
+
this._insertIndices(this._sheet.columns, i, n, DEFAULT_COLUMNS);
|
|
191
|
+
this.reset();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
//
|
|
195
|
+
// Undoable actions.
|
|
196
|
+
// TODO(burdon): Group undoable methods; consistently update hf/sheet.
|
|
197
|
+
//
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Clear range of values.
|
|
201
|
+
*/
|
|
202
|
+
clear(range: CellRange) {
|
|
203
|
+
const topLeft = getTopLeft(range);
|
|
204
|
+
const values = this._iterRange(range, () => null);
|
|
205
|
+
this._graph.hf.setCellContents(toSimpleCellAddress(this._sheetId, topLeft), values);
|
|
206
|
+
this._iterRange(range, (cell) => {
|
|
207
|
+
const idx = this.addressToIndex(cell);
|
|
208
|
+
delete this._sheet.cells[idx];
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
cut(range: CellRange) {
|
|
213
|
+
this._graph.hf.cut(toModelRange(this._sheetId, range));
|
|
214
|
+
this._iterRange(range, (cell) => {
|
|
215
|
+
const idx = this.addressToIndex(cell);
|
|
216
|
+
delete this._sheet.cells[idx];
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
copy(range: CellRange) {
|
|
221
|
+
this._graph.hf.copy(toModelRange(this._sheetId, range));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
paste(cell: CellAddress) {
|
|
225
|
+
if (!this._graph.hf.isClipboardEmpty()) {
|
|
226
|
+
const changes = this._graph.hf.paste(toSimpleCellAddress(this._sheetId, cell));
|
|
227
|
+
for (const change of changes) {
|
|
228
|
+
if (change instanceof ExportedCellChange) {
|
|
229
|
+
const { address, newValue } = change;
|
|
230
|
+
const idx = this.addressToIndex({ row: address.row, column: address.col });
|
|
231
|
+
this._sheet.cells[idx] = { value: newValue };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// TODO(burdon): Display undo/redo state.
|
|
238
|
+
undo() {
|
|
239
|
+
if (this._graph.hf.isThereSomethingToUndo()) {
|
|
240
|
+
this._graph.hf.undo();
|
|
241
|
+
this.update.emit();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
redo() {
|
|
246
|
+
if (this._graph.hf.isThereSomethingToRedo()) {
|
|
247
|
+
this._graph.hf.redo();
|
|
248
|
+
this.update.emit();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get value from sheet.
|
|
254
|
+
*/
|
|
255
|
+
getCellValue(cell: CellAddress): CellScalarValue {
|
|
256
|
+
const idx = this.addressToIndex(cell);
|
|
257
|
+
return this._sheet.cells[idx]?.value ?? null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get value as a string for editing.
|
|
262
|
+
*/
|
|
263
|
+
getCellText(cell: CellAddress): string | undefined {
|
|
264
|
+
const value = this.getCellValue(cell);
|
|
265
|
+
if (value == null) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (typeof value === 'string' && value.charAt(0) === '=') {
|
|
270
|
+
return this.mapFormulaIndicesToRefs(value);
|
|
271
|
+
} else {
|
|
272
|
+
return String(value);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get array of raw values from sheet.
|
|
278
|
+
*/
|
|
279
|
+
getCellValues(range: CellRange): CellScalarValue[][] {
|
|
280
|
+
return this._iterRange(range, (cell) => this.getCellValue(cell));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Gets the regular or computed value from the engine.
|
|
285
|
+
*/
|
|
286
|
+
getValue(cell: CellAddress): CellScalarValue {
|
|
287
|
+
// Applies rounding and post-processing.
|
|
288
|
+
const value = this._graph.hf.getCellValue(toSimpleCellAddress(this._sheetId, cell));
|
|
289
|
+
if (value instanceof DetailedCellError) {
|
|
290
|
+
return value.toString();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get value type.
|
|
298
|
+
*/
|
|
299
|
+
getValueType(cell: CellAddress): ValueTypeEnum {
|
|
300
|
+
const addr = toSimpleCellAddress(this._sheetId, cell);
|
|
301
|
+
const type = this._graph.hf.getCellValueDetailedType(addr);
|
|
302
|
+
return typeMap[type];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Sets the value, updating the sheet and engine.
|
|
307
|
+
*/
|
|
308
|
+
setValue(cell: CellAddress, value: CellScalarValue) {
|
|
309
|
+
if (this._options.readonly) {
|
|
310
|
+
throw new ReadonlyException();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Reallocate if > current bounds.
|
|
314
|
+
let refresh = false;
|
|
315
|
+
if (cell.row >= this._sheet.rows.length) {
|
|
316
|
+
this._insertIndices(this._sheet.rows, cell.row, 1, DEFAULT_ROWS);
|
|
317
|
+
refresh = true;
|
|
318
|
+
}
|
|
319
|
+
if (cell.column >= this._sheet.columns.length) {
|
|
320
|
+
this._insertIndices(this._sheet.columns, cell.column, 1, DEFAULT_COLUMNS);
|
|
321
|
+
refresh = true;
|
|
322
|
+
}
|
|
323
|
+
if (refresh) {
|
|
324
|
+
// TODO(burdon): Remove.
|
|
325
|
+
this.reset();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Insert into engine.
|
|
329
|
+
this._graph.hf.setCellContents({ sheet: this._sheetId, row: cell.row, col: cell.column }, [[value]]);
|
|
330
|
+
|
|
331
|
+
// Insert into sheet.
|
|
332
|
+
const idx = this.addressToIndex(cell);
|
|
333
|
+
if (value === undefined || value === null) {
|
|
334
|
+
delete this._sheet.cells[idx];
|
|
335
|
+
} else {
|
|
336
|
+
if (typeof value === 'string' && value.charAt(0) === '=') {
|
|
337
|
+
value = this.mapFormulaRefsToIndices(value);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this._sheet.cells[idx] = { value };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Sets values from a simple map.
|
|
346
|
+
*/
|
|
347
|
+
setValues(values: Record<string, CellValue>) {
|
|
348
|
+
Object.entries(values).forEach(([key, { value }]) => {
|
|
349
|
+
this.setValue(addressFromA1Notation(key), value);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Iterate range.
|
|
355
|
+
*/
|
|
356
|
+
private _iterRange(range: CellRange, cb: (cell: CellAddress) => CellScalarValue | void): CellScalarValue[][] {
|
|
357
|
+
const to = range.to ?? range.from;
|
|
358
|
+
const rowRange = [Math.min(range.from.row, to.row), Math.max(range.from.row, to.row)];
|
|
359
|
+
const columnRange = [Math.min(range.from.column, to.column), Math.max(range.from.column, to.column)];
|
|
360
|
+
const rows: CellScalarValue[][] = [];
|
|
361
|
+
for (let row = rowRange[0]; row <= rowRange[1]; row++) {
|
|
362
|
+
const rowCells: CellScalarValue[] = [];
|
|
363
|
+
for (let column = columnRange[0]; column <= columnRange[1]; column++) {
|
|
364
|
+
const value = cb({ row, column });
|
|
365
|
+
if (value !== undefined) {
|
|
366
|
+
rowCells.push(value);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
rows.push(rowCells);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return rows;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
*
|
|
377
|
+
*/
|
|
378
|
+
// TODO(burdon): Insert indices into sheet.
|
|
379
|
+
private _insertIndices(indices: string[], i: number, n: number, max: number) {
|
|
380
|
+
if (i + n > max) {
|
|
381
|
+
throw new RangeException(i + n);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const idx = createIndices(n);
|
|
385
|
+
indices.splice(i, 0, ...idx);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// TODO(burdon): Delete index.
|
|
389
|
+
private _deleteIndices(indices: string[], i: number, n: number) {
|
|
390
|
+
throw new Error('Not implemented');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// TODO(burdon): Move. Cannot use fractional without changing. Switch back to using unique IDs?
|
|
394
|
+
private _moveIndices(indices: string[], i: number, j: number, n: number) {
|
|
395
|
+
throw new Error('Not implemented');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
//
|
|
399
|
+
// Indices.
|
|
400
|
+
//
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* E.g., "A1" => "x1@y1".
|
|
404
|
+
*/
|
|
405
|
+
addressToIndex(cell: CellAddress): CellIndex {
|
|
406
|
+
return `${this._sheet.columns[cell.column]}@${this._sheet.rows[cell.row]}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* E.g., "x1@y1" => "A1".
|
|
411
|
+
*/
|
|
412
|
+
addressFromIndex(idx: CellIndex): CellAddress {
|
|
413
|
+
const [column, row] = idx.split('@');
|
|
414
|
+
return {
|
|
415
|
+
column: this._sheet.columns.indexOf(column),
|
|
416
|
+
row: this._sheet.rows.indexOf(row),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* E.g., "A1:B2" => "x1@y1:x2@y2".
|
|
422
|
+
*/
|
|
423
|
+
rangeToIndex(range: CellRange): string {
|
|
424
|
+
return [range.from, range.to ?? range.from].map((cell) => this.addressToIndex(cell)).join(':');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* E.g., "x1@y1:x2@y2" => "A1:B2".
|
|
429
|
+
*/
|
|
430
|
+
rangeFromIndex(idx: string): CellRange {
|
|
431
|
+
const [from, to] = idx.split(':').map((idx) => this.addressFromIndex(idx));
|
|
432
|
+
return { from, to };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Map from A1 notation to indices.
|
|
437
|
+
*/
|
|
438
|
+
mapFormulaRefsToIndices(formula: string): string {
|
|
439
|
+
invariant(formula.charAt(0) === '=');
|
|
440
|
+
return formula.replace(/([a-zA-Z]+)([0-9]+)/g, (match) => {
|
|
441
|
+
return this.addressToIndex(addressFromA1Notation(match));
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Map from indices to A1 notation.
|
|
447
|
+
*/
|
|
448
|
+
mapFormulaIndicesToRefs(formula: string): string {
|
|
449
|
+
invariant(formula.charAt(0) === '=');
|
|
450
|
+
return formula.replace(/([a-zA-Z0-9]+)@([a-zA-Z0-9]+)/g, (idx) => {
|
|
451
|
+
return addressToA1Notation(this.addressFromIndex(idx));
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
//
|
|
456
|
+
// Values
|
|
457
|
+
//
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* https://hyperformula.handsontable.com/guide/date-and-time-handling.html#example
|
|
461
|
+
* https://hyperformula.handsontable.com/api/interfaces/configparams.html#nulldate
|
|
462
|
+
* NOTE: TODAY() is number of FULL days since nullDate. It will typically be -1 days from NOW().
|
|
463
|
+
*/
|
|
464
|
+
toLocalDate(num: number): Date {
|
|
465
|
+
const { year, month, day, hours, minutes, seconds } = this.toDateTime(num);
|
|
466
|
+
return new Date(year, month - 1, day, hours, minutes, seconds);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
toDateTime(num: number): SimpleDateTime {
|
|
470
|
+
return this._graph.hf.numberToDateTime(num) as SimpleDateTime;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
toDate(num: number): SimpleDate {
|
|
474
|
+
return this._graph.hf.numberToDate(num) as SimpleDate;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
toTime(num: number): SimpleDate {
|
|
478
|
+
return this._graph.hf.numberToTime(num) as SimpleDate;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { getIndices, sortByIndex, getIndicesBelow, getIndicesAbove, getIndicesBetween } from '@tldraw/indices';
|
|
6
|
+
import { expect } from 'chai';
|
|
7
|
+
import { describe, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { inRange, addressFromA1Notation, addressToA1Notation, rangeFromA1Notation, rangeToA1Notation } from './types';
|
|
10
|
+
|
|
11
|
+
describe('cell', () => {
|
|
12
|
+
test('posToA1Notation', () => {
|
|
13
|
+
expect(addressToA1Notation({ column: 0, row: 0 })).to.eq('A1');
|
|
14
|
+
expect(addressFromA1Notation('C2')).to.deep.eq({ column: 2, row: 1 });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('rangeToA1Notation', () => {
|
|
18
|
+
expect(rangeToA1Notation({ from: addressFromA1Notation('A1'), to: addressFromA1Notation('A5') })).to.eq('A1:A5');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('inRange', () => {
|
|
22
|
+
const range = rangeFromA1Notation('A1:C5');
|
|
23
|
+
expect(inRange(range, addressFromA1Notation('A1'))).to.be.true;
|
|
24
|
+
expect(inRange(range, addressFromA1Notation('C5'))).to.be.true;
|
|
25
|
+
expect(inRange(range, addressFromA1Notation('A6'))).to.be.false;
|
|
26
|
+
expect(inRange(range, addressFromA1Notation('D5'))).to.be.false;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// TODO(burdon): Move to model.test.ts
|
|
30
|
+
test('index', () => {
|
|
31
|
+
// Pre-allocated grid.
|
|
32
|
+
const n = 5;
|
|
33
|
+
const columns = getIndices(n - 1);
|
|
34
|
+
const rows = getIndices(n - 1);
|
|
35
|
+
|
|
36
|
+
const pickOne = (indices: string[]) => indices[Math.floor(Math.random() * indices.length)];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Insert an index into the allocated list.
|
|
40
|
+
* Randomly picks from n values between indexes to support probabilistic concurrency.
|
|
41
|
+
*/
|
|
42
|
+
const insertIndex = (indices: string[], i: number, n = 20) => {
|
|
43
|
+
if (i === 0) {
|
|
44
|
+
const idx = pickOne(getIndicesBelow(indices[0], n));
|
|
45
|
+
indices.splice(0, 0, idx);
|
|
46
|
+
} else if (i >= indices.length) {
|
|
47
|
+
// Reallocate if > current bounds.
|
|
48
|
+
// TODO(burdon): Is this OK if this happens concurrently?
|
|
49
|
+
indices.splice(indices.length, 0, ...getIndicesAbove(indices[indices.length - 1], i + 1 - indices.length));
|
|
50
|
+
} else {
|
|
51
|
+
const idx = pickOne(getIndicesBetween(indices[i - 1], indices[i], n));
|
|
52
|
+
indices.splice(i, 0, idx);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Values.
|
|
57
|
+
const cells: Record<string, any> = {};
|
|
58
|
+
const setCell = (cell: string, value: any) => {
|
|
59
|
+
const { column, row } = addressFromA1Notation(cell);
|
|
60
|
+
// Reallocate if > current bounds.
|
|
61
|
+
if (column >= columns.length) {
|
|
62
|
+
insertIndex(columns, column);
|
|
63
|
+
}
|
|
64
|
+
if (row >= rows.length) {
|
|
65
|
+
insertIndex(rows, row);
|
|
66
|
+
}
|
|
67
|
+
const index = `${columns[column]}@${rows[row]}`;
|
|
68
|
+
cells[index] = value;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
expect(addressFromA1Notation('A1')).to.deep.eq({ column: 0, row: 0 });
|
|
72
|
+
|
|
73
|
+
expect(columns).to.deep.eq(['a1', 'a2', 'a3', 'a4', 'a5']);
|
|
74
|
+
insertIndex(columns, 7);
|
|
75
|
+
expect(columns).to.deep.eq(['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8']);
|
|
76
|
+
|
|
77
|
+
setCell('A1', 100);
|
|
78
|
+
setCell('B1', 101);
|
|
79
|
+
|
|
80
|
+
insertIndex(columns, 1);
|
|
81
|
+
setCell('B1', 102);
|
|
82
|
+
|
|
83
|
+
setCell('J10', 104);
|
|
84
|
+
expect(columns).to.have.length(10);
|
|
85
|
+
expect(rows).to.have.length(10);
|
|
86
|
+
|
|
87
|
+
const entries = Object.entries(cells).map(([key, value]) => ({ index: key.split('@')[0], value }));
|
|
88
|
+
const sorted = entries.sort(sortByIndex);
|
|
89
|
+
const values = sorted.map(({ value }) => value);
|
|
90
|
+
expect(values).to.deep.eq([100, 102, 101, 104]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { invariant } from '@dxos/invariant';
|
|
6
|
+
|
|
7
|
+
export const MAX_COLUMNS = 26 * 26;
|
|
8
|
+
|
|
9
|
+
export type CellAddress = { column: number; row: number };
|
|
10
|
+
export type CellRange = { from: CellAddress; to?: CellAddress };
|
|
11
|
+
|
|
12
|
+
export const posEquals = (a: CellAddress | undefined, b: CellAddress | undefined) => {
|
|
13
|
+
return a?.column === b?.column && a?.row === b?.row;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const columnLetter = (column: number): string => {
|
|
17
|
+
invariant(column < MAX_COLUMNS, `Invalid column: ${column}`);
|
|
18
|
+
return (
|
|
19
|
+
(column >= 26 ? String.fromCharCode('A'.charCodeAt(0) + Math.floor(column / 26) - 1) : '') +
|
|
20
|
+
String.fromCharCode('A'.charCodeAt(0) + (column % 26))
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const addressToA1Notation = ({ column, row }: CellAddress): string => {
|
|
25
|
+
return `${columnLetter(column)}${row + 1}`;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const addressFromA1Notation = (ref: string): CellAddress => {
|
|
29
|
+
const match = ref.match(/([A-Z]+)(\d+)/);
|
|
30
|
+
invariant(match, `Invalid notation: ${ref}`);
|
|
31
|
+
return {
|
|
32
|
+
row: parseInt(match[2], 10) - 1,
|
|
33
|
+
column: match[1].split('').reduce((acc, c) => acc * 26 + c.charCodeAt(0) - 'A'.charCodeAt(0) + 1, 0) - 1,
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const rangeToA1Notation = (range: CellRange) => {
|
|
38
|
+
return [range?.from && addressToA1Notation(range?.from), range?.to && addressToA1Notation(range?.to)]
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join(':');
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const rangeFromA1Notation = (ref: string): CellRange => {
|
|
44
|
+
const [from, to] = ref.split(':').map(addressFromA1Notation);
|
|
45
|
+
return { from, to };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const inRange = (range: CellRange | undefined, cell: CellAddress): boolean => {
|
|
49
|
+
if (!range) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { from, to } = range;
|
|
54
|
+
if ((from && posEquals(from, cell)) || (to && posEquals(to, cell))) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!from || !to) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { column: c1, row: r1 } = from;
|
|
63
|
+
const { column: c2, row: r2 } = to;
|
|
64
|
+
const cMin = Math.min(c1, c2);
|
|
65
|
+
const cMax = Math.max(c1, c2);
|
|
66
|
+
const rMin = Math.min(r1, r2);
|
|
67
|
+
const rMax = Math.max(r1, r2);
|
|
68
|
+
|
|
69
|
+
const { column, row } = cell;
|
|
70
|
+
return column >= cMin && column <= cMax && row >= rMin && row <= rMax;
|
|
71
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { randomBytes } from '@dxos/crypto';
|
|
6
|
+
|
|
7
|
+
// TODO(burdon): Factor out from dxos/protocols to new common package.
|
|
8
|
+
export class ApiError extends Error {}
|
|
9
|
+
export class ReadonlyException extends ApiError {}
|
|
10
|
+
export class RangeException extends ApiError {
|
|
11
|
+
constructor(n: number) {
|
|
12
|
+
super();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* With a string length of 8, the chance of a collision is 0.02% for a sheet with 10,000 strings.
|
|
18
|
+
*/
|
|
19
|
+
export const createIndex = (length = 8): string => {
|
|
20
|
+
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
21
|
+
const charactersLength = characters.length;
|
|
22
|
+
const randomBuffer = randomBytes(length);
|
|
23
|
+
return Array.from(randomBuffer, (byte) => characters[byte % charactersLength]).join('');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const createIndices = (length: number): string[] => Array.from({ length }).map(() => createIndex());
|
|
27
|
+
|
|
28
|
+
// TODO(burdon): Factor out.
|
|
29
|
+
export const pickOne = <T>(values: T[]): T => values[Math.floor(Math.random() * values.length)];
|
|
30
|
+
export const pickSome = <T>(values: T[], n = 1): T[] => {
|
|
31
|
+
const result = new Set<T>();
|
|
32
|
+
while (result.size < n) {
|
|
33
|
+
result.add(pickOne(values));
|
|
34
|
+
}
|
|
35
|
+
return Array.from(result.values());
|
|
36
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { SHEET_PLUGIN } from './meta';
|
|
6
|
+
|
|
7
|
+
export default [
|
|
8
|
+
{
|
|
9
|
+
'en-US': {
|
|
10
|
+
[SHEET_PLUGIN]: {
|
|
11
|
+
'plugin name': 'Sheets',
|
|
12
|
+
'sheet title placeholder': 'New sheet',
|
|
13
|
+
'create sheet label': 'Create sheet',
|
|
14
|
+
'create sheet section label': 'Create sheet',
|
|
15
|
+
'cell placeholder': 'Cell value...',
|
|
16
|
+
'toolbar left label': 'Align left',
|
|
17
|
+
'toolbar left center': 'Align center',
|
|
18
|
+
'toolbar left right': 'Align right',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
];
|