@trebco/treb 31.1.3 → 31.3.3
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/dist/treb-export-worker.mjs +2 -2
- package/dist/treb-spreadsheet.mjs +13 -13
- package/dist/treb.d.ts +2 -2
- package/package.json +1 -1
- package/test/parser.ts +13 -0
- package/treb-calculator/src/complex-math.ts +104 -0
- package/treb-calculator/src/descriptors.ts +6 -0
- package/treb-calculator/src/functions/base-functions.ts +177 -96
- package/treb-calculator/src/functions/statistics-functions.ts +1 -0
- package/treb-calculator/tsconfig.json +0 -4
- package/treb-embed/src/embedded-spreadsheet.ts +6 -3
- package/treb-embed/style/formula-bar.scss +4 -0
- package/treb-format/src/format.ts +8 -0
- package/treb-grid/src/editors/editor.ts +2 -2
- package/treb-grid/src/editors/formula_bar.ts +110 -10
- package/treb-grid/src/editors/overlay_editor.ts +11 -0
- package/treb-grid/src/types/grid.ts +65 -8
- package/treb-parser/src/parser-types.ts +4 -0
- package/treb-parser/src/parser.ts +44 -2
|
@@ -46,10 +46,16 @@ export interface AddressLabelEvent {
|
|
|
46
46
|
text?: string;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export interface TollEvent {
|
|
50
|
+
type: 'toll';
|
|
51
|
+
value?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
export type FormulaBar2Event
|
|
50
55
|
= FormulaButtonEvent
|
|
51
56
|
// | FormulaBarResizeEvent
|
|
52
57
|
| AddressLabelEvent
|
|
58
|
+
| TollEvent
|
|
53
59
|
;
|
|
54
60
|
|
|
55
61
|
// ---
|
|
@@ -59,6 +65,28 @@ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
|
|
|
59
65
|
|
|
60
66
|
public committed = false;
|
|
61
67
|
|
|
68
|
+
public tolled?: { text: string, substring: string };
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* if we're showing a spill array and it's not the first cell, show
|
|
72
|
+
* the formula using a shadow style (and it's not editable).
|
|
73
|
+
*/
|
|
74
|
+
public shadow_ = false;
|
|
75
|
+
|
|
76
|
+
public get shadow() { return this.shadow_; }
|
|
77
|
+
|
|
78
|
+
public set shadow(shadow: boolean) {
|
|
79
|
+
this.shadow_ = shadow;
|
|
80
|
+
if (this.active_editor?.node) {
|
|
81
|
+
if (shadow) {
|
|
82
|
+
this.active_editor.node.classList.add('treb-editor-shadow');
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.active_editor.node.classList.remove('treb-editor-shadow');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
62
90
|
/** is the _editor_ currently focused */
|
|
63
91
|
// tslint:disable-next-line:variable-name
|
|
64
92
|
public focused_ = false;
|
|
@@ -221,16 +249,27 @@ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
|
|
|
221
249
|
this.active_editor.node.spellcheck = false; // change the default back
|
|
222
250
|
|
|
223
251
|
this.RegisterListener(descriptor, 'focusin', () => {
|
|
224
|
-
|
|
252
|
+
|
|
253
|
+
const restore = this.tolled !== undefined;
|
|
254
|
+
this.tolled = undefined;
|
|
255
|
+
|
|
225
256
|
// this.editor_node.addEventListener('focusin', () => {
|
|
226
257
|
|
|
227
258
|
// can't happen
|
|
228
|
-
if (!this.active_editor) {
|
|
259
|
+
if (!this.active_editor) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
229
262
|
|
|
230
263
|
// console.info('focus in');
|
|
231
264
|
|
|
232
265
|
let text = this.active_editor.node.textContent || '';
|
|
233
266
|
|
|
267
|
+
if (this.shadow) {
|
|
268
|
+
this.shadow = false;
|
|
269
|
+
text = '';
|
|
270
|
+
this.active_editor.node.textContent = text;
|
|
271
|
+
this.active_editor.formatted_text = undefined; // why do we clear this here?
|
|
272
|
+
}
|
|
234
273
|
if (text[0] === '{' && text[text.length - 1] === '}') {
|
|
235
274
|
text = text.substring(1, text.length - 1);
|
|
236
275
|
this.active_editor.node.textContent = text;
|
|
@@ -243,6 +282,18 @@ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
|
|
|
243
282
|
this.UpdateText(this.active_editor);
|
|
244
283
|
this.UpdateColors(undefined, true); // toll update event -- we will send in order, below
|
|
245
284
|
|
|
285
|
+
if (restore) {
|
|
286
|
+
|
|
287
|
+
// FIXME: we probably want to hold the caret so we can restore
|
|
288
|
+
// to a particular place. or set the caret explicitly.
|
|
289
|
+
|
|
290
|
+
const node = this.NodeAtIndex(text.length - 1);
|
|
291
|
+
// console.info({text, node});
|
|
292
|
+
if (node) {
|
|
293
|
+
this.SetCaret({node: node as ChildNode, offset: (node.textContent || '').length});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
246
297
|
this.committed = false;
|
|
247
298
|
|
|
248
299
|
this.Publish([
|
|
@@ -254,7 +305,14 @@ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
|
|
|
254
305
|
|
|
255
306
|
});
|
|
256
307
|
|
|
257
|
-
this.RegisterListener(descriptor, 'focusout', () => {
|
|
308
|
+
this.RegisterListener(descriptor, 'focusout', (event: FocusEvent) => {
|
|
309
|
+
|
|
310
|
+
// this is new, to support an external "insert function" dialog.
|
|
311
|
+
// still working out the semantics, but essentially we won't commit
|
|
312
|
+
// and we'll keep pending text. we need some sort of flag to indicate
|
|
313
|
+
// that we're in the editing state.
|
|
314
|
+
|
|
315
|
+
const toll = (event.relatedTarget instanceof HTMLElement && event.relatedTarget.dataset.tollEditor);
|
|
258
316
|
|
|
259
317
|
if (this.selecting) {
|
|
260
318
|
console.info('focusout, but selecting...');
|
|
@@ -265,7 +323,27 @@ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
|
|
|
265
323
|
const text = (this.active_editor ?
|
|
266
324
|
this.GetTextContent(this.active_editor.node).join('') : '').trim();
|
|
267
325
|
|
|
268
|
-
if (
|
|
326
|
+
if (toll) {
|
|
327
|
+
|
|
328
|
+
let substring = text;
|
|
329
|
+
if (this.active_editor?.node) {
|
|
330
|
+
const s2c = this.SubstringToCaret2(this.active_editor.node, true);
|
|
331
|
+
substring = s2c[0];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.committed = true;
|
|
335
|
+
this.tolled = {
|
|
336
|
+
text,
|
|
337
|
+
substring,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
this.Publish({
|
|
341
|
+
type: 'toll',
|
|
342
|
+
value: text,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
}
|
|
346
|
+
else if (this.committed) {
|
|
269
347
|
this.Publish([
|
|
270
348
|
{ type: 'stop-editing' },
|
|
271
349
|
]);
|
|
@@ -278,14 +356,10 @@ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
|
|
|
278
356
|
});
|
|
279
357
|
}
|
|
280
358
|
|
|
359
|
+
// huh? we're publishing this twice?
|
|
360
|
+
|
|
281
361
|
this.Publish([
|
|
282
362
|
{ type: 'stop-editing' },
|
|
283
|
-
/*
|
|
284
|
-
{
|
|
285
|
-
type: 'commit',
|
|
286
|
-
value: text,
|
|
287
|
-
}
|
|
288
|
-
*/
|
|
289
363
|
]);
|
|
290
364
|
|
|
291
365
|
this.focused_ = false;
|
|
@@ -336,6 +410,31 @@ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
|
|
|
336
410
|
|
|
337
411
|
}
|
|
338
412
|
|
|
413
|
+
/**
|
|
414
|
+
* we might be overthinking this, we don't necessarily need to restore.
|
|
415
|
+
*
|
|
416
|
+
* this method will focus the editor and set the caret (to the end, atm)
|
|
417
|
+
* in case of a tolled editor. the idea is you call Restore() after your
|
|
418
|
+
* dialog is complete and it's like you are back where you started.
|
|
419
|
+
*
|
|
420
|
+
* alternatively, call `Release()` to clean up any saved state.
|
|
421
|
+
*/
|
|
422
|
+
public Restore() {
|
|
423
|
+
const target = this.container_node?.firstElementChild as HTMLDivElement;
|
|
424
|
+
if (target) {
|
|
425
|
+
target.focus();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* release anything that's been tolled. call this if you toll the editor
|
|
431
|
+
* but don't want to restore it when you are done. I think this will be
|
|
432
|
+
* the default.
|
|
433
|
+
*/
|
|
434
|
+
public Release() {
|
|
435
|
+
this.tolled = undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
339
438
|
public IsElement(element: HTMLElement): boolean {
|
|
340
439
|
return element === this.active_editor?.node;
|
|
341
440
|
}
|
|
@@ -482,6 +581,7 @@ export class FormulaBar extends Editor<FormulaBar2Event|FormulaEditorEvent> {
|
|
|
482
581
|
case 'Escape':
|
|
483
582
|
case 'Esc':
|
|
484
583
|
// this.selecting_ = false;
|
|
584
|
+
this.committed = true;
|
|
485
585
|
this.Publish({ type: 'discard' });
|
|
486
586
|
// this.FlushReference();
|
|
487
587
|
break;
|
|
@@ -400,6 +400,17 @@ export class OverlayEditor extends Editor<ResetSelectionEvent> {
|
|
|
400
400
|
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
public GetEditState() {
|
|
404
|
+
const text = this.active_editor?.node?.textContent || '';
|
|
405
|
+
|
|
406
|
+
let substring = '';
|
|
407
|
+
if (this.active_editor?.node) {
|
|
408
|
+
substring = this.SubstringToCaret2(this.active_editor.node, true)[0];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return { text, substring };
|
|
412
|
+
}
|
|
413
|
+
|
|
403
414
|
/**
|
|
404
415
|
* check if we want to handle this key. we have some special cases (tab,
|
|
405
416
|
* enter, escape) where we do take some action but we also let the
|
|
@@ -219,6 +219,9 @@ export class Grid extends GridBase {
|
|
|
219
219
|
/** */
|
|
220
220
|
private editing_annotation?: Annotation;
|
|
221
221
|
|
|
222
|
+
/** */
|
|
223
|
+
private pending_reset_selection = false;
|
|
224
|
+
|
|
222
225
|
/** */
|
|
223
226
|
private view_node?: HTMLElement;
|
|
224
227
|
|
|
@@ -1742,8 +1745,15 @@ export class Grid extends GridBase {
|
|
|
1742
1745
|
if (this.selected_annotation) {
|
|
1743
1746
|
|
|
1744
1747
|
if (click) {
|
|
1745
|
-
|
|
1746
|
-
this.
|
|
1748
|
+
|
|
1749
|
+
if (this.editing_annotation === this.selected_annotation) {
|
|
1750
|
+
this.pending_reset_selection = true;
|
|
1751
|
+
}
|
|
1752
|
+
else {
|
|
1753
|
+
this.selected_annotation = undefined;
|
|
1754
|
+
this.ShowGridSelection();
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1747
1757
|
}
|
|
1748
1758
|
else {
|
|
1749
1759
|
// console.info("reselect annotation...", this.selected_annotation);
|
|
@@ -2964,6 +2974,10 @@ export class Grid extends GridBase {
|
|
|
2964
2974
|
|
|
2965
2975
|
case 'stop-editing':
|
|
2966
2976
|
|
|
2977
|
+
if (this.pending_reset_selection) {
|
|
2978
|
+
this.ShowGridSelection();
|
|
2979
|
+
}
|
|
2980
|
+
this.pending_reset_selection = false;
|
|
2967
2981
|
this.editing_state = EditingState.NotEditing;
|
|
2968
2982
|
break;
|
|
2969
2983
|
|
|
@@ -2986,6 +3000,19 @@ export class Grid extends GridBase {
|
|
|
2986
3000
|
this.editing_selection = { ...this.primary_selection };
|
|
2987
3001
|
break;
|
|
2988
3002
|
|
|
3003
|
+
case 'toll':
|
|
3004
|
+
|
|
3005
|
+
// this is basically "stop editing", with some special
|
|
3006
|
+
// semantics to capture the editing state. it's intended
|
|
3007
|
+
// for external clients, specifically for an "insert function"
|
|
3008
|
+
// dialog.
|
|
3009
|
+
|
|
3010
|
+
this.editing_state = EditingState.NotEditing;
|
|
3011
|
+
this.ClearAdditionalSelections();
|
|
3012
|
+
this.ClearSelection(this.active_selection);
|
|
3013
|
+
this.DelayedRender();
|
|
3014
|
+
break;
|
|
3015
|
+
|
|
2989
3016
|
case 'discard':
|
|
2990
3017
|
|
|
2991
3018
|
this.editing_state = EditingState.NotEditing;
|
|
@@ -3037,10 +3064,14 @@ export class Grid extends GridBase {
|
|
|
3037
3064
|
this.ClearAdditionalSelections();
|
|
3038
3065
|
this.ClearSelection(this.active_selection);
|
|
3039
3066
|
annotation.data.formula = event.value ? this.FixFormula(event.value) : '';
|
|
3040
|
-
|
|
3041
|
-
if (
|
|
3042
|
-
node.
|
|
3067
|
+
|
|
3068
|
+
if (!this.pending_reset_selection) {
|
|
3069
|
+
const node = this.editing_annotation.view[this.view_index]?.node;
|
|
3070
|
+
if (node) {
|
|
3071
|
+
node.focus();
|
|
3072
|
+
}
|
|
3043
3073
|
}
|
|
3074
|
+
|
|
3044
3075
|
this.grid_events.Publish({ type: 'annotation', event: 'update', annotation });
|
|
3045
3076
|
this.editing_annotation = undefined;
|
|
3046
3077
|
this.DelayedRender();
|
|
@@ -4774,6 +4805,26 @@ export class Grid extends GridBase {
|
|
|
4774
4805
|
// || (this.select_argument);
|
|
4775
4806
|
}
|
|
4776
4807
|
|
|
4808
|
+
/**
|
|
4809
|
+
* for external clients. the expected pattern is
|
|
4810
|
+
* - start overlay editor
|
|
4811
|
+
* - click on "insert function"
|
|
4812
|
+
* - do something
|
|
4813
|
+
* - release editor (this function)
|
|
4814
|
+
*/
|
|
4815
|
+
public ReleaseOverlayEditor() {
|
|
4816
|
+
this.editing_state = EditingState.NotEditing;
|
|
4817
|
+
this.DismissEditor();
|
|
4818
|
+
this.DelayedRender();
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
public RestoreOverlayEditor() {
|
|
4822
|
+
|
|
4823
|
+
// ?
|
|
4824
|
+
this.overlay_editor?.FocusEditor();
|
|
4825
|
+
|
|
4826
|
+
}
|
|
4827
|
+
|
|
4777
4828
|
/**
|
|
4778
4829
|
* consolidated event handler for overlay, which both handles grid keys
|
|
4779
4830
|
* and acts as the ICE, depending on state. going to be a little tricky
|
|
@@ -6641,6 +6692,7 @@ export class Grid extends GridBase {
|
|
|
6641
6692
|
|
|
6642
6693
|
if (override) {
|
|
6643
6694
|
if (this.formula_bar) {
|
|
6695
|
+
this.formula_bar.shadow = false;
|
|
6644
6696
|
this.formula_bar.formula = override;
|
|
6645
6697
|
}
|
|
6646
6698
|
return;
|
|
@@ -6648,6 +6700,7 @@ export class Grid extends GridBase {
|
|
|
6648
6700
|
|
|
6649
6701
|
if (this.primary_selection.empty) {
|
|
6650
6702
|
if (this.formula_bar) {
|
|
6703
|
+
this.formula_bar.shadow = false;
|
|
6651
6704
|
this.formula_bar.formula = '';
|
|
6652
6705
|
}
|
|
6653
6706
|
}
|
|
@@ -6657,11 +6710,14 @@ export class Grid extends GridBase {
|
|
|
6657
6710
|
// optimally we would do this check prior to this call, but
|
|
6658
6711
|
// it's the uncommon case... not sure how important that is
|
|
6659
6712
|
|
|
6660
|
-
const head = data.merge_area || data.area;
|
|
6713
|
+
const head = data.merge_area || data.area || data.spill;
|
|
6714
|
+
let shadow = false;
|
|
6715
|
+
|
|
6661
6716
|
if (head) {
|
|
6662
6717
|
if (head.start.column !== this.primary_selection.target.column
|
|
6663
6718
|
|| head.start.row !== this.primary_selection.target.row) {
|
|
6664
6719
|
data = this.active_sheet.CellData(head.start);
|
|
6720
|
+
if (data.spill) { shadow = true; }
|
|
6665
6721
|
}
|
|
6666
6722
|
}
|
|
6667
6723
|
|
|
@@ -6701,12 +6757,13 @@ export class Grid extends GridBase {
|
|
|
6701
6757
|
|
|
6702
6758
|
if (this.formula_bar) {
|
|
6703
6759
|
|
|
6704
|
-
|
|
6760
|
+
this.formula_bar.shadow = shadow;
|
|
6761
|
+
|
|
6705
6762
|
if (data.area) {
|
|
6706
6763
|
this.formula_bar.formula = '{' + (value || '') + '}';
|
|
6707
6764
|
}
|
|
6708
6765
|
else {
|
|
6709
|
-
this.formula_bar.formula = (
|
|
6766
|
+
this.formula_bar.formula = (value ?? '').toString();
|
|
6710
6767
|
}
|
|
6711
6768
|
|
|
6712
6769
|
}
|
|
@@ -36,6 +36,7 @@ import type {
|
|
|
36
36
|
RenderOptions,
|
|
37
37
|
BaseExpressionUnit,
|
|
38
38
|
} from './parser-types';
|
|
39
|
+
|
|
39
40
|
import {
|
|
40
41
|
ArgumentSeparatorType,
|
|
41
42
|
DecimalMarkType,
|
|
@@ -1137,7 +1138,6 @@ export class Parser {
|
|
|
1137
1138
|
|
|
1138
1139
|
}
|
|
1139
1140
|
|
|
1140
|
-
|
|
1141
1141
|
// so we're moving complex handling to post-reordering, to support
|
|
1142
1142
|
// precedence properly. there's still one thing we have to do here,
|
|
1143
1143
|
// though: handle those cases of naked imaginary values "i". these
|
|
@@ -1233,7 +1233,13 @@ export class Parser {
|
|
|
1233
1233
|
|
|
1234
1234
|
// return this.BinaryToRange(this.ArrangeUnits(stream));
|
|
1235
1235
|
// return this.ArrangeUnits(stream);
|
|
1236
|
+
|
|
1237
|
+
// const arranged = this.ArrangeUnits(stream);
|
|
1238
|
+
// const result = this.BinaryToComplex(arranged);
|
|
1239
|
+
// return result;
|
|
1240
|
+
|
|
1236
1241
|
return this.BinaryToComplex(this.ArrangeUnits(stream));
|
|
1242
|
+
|
|
1237
1243
|
}
|
|
1238
1244
|
|
|
1239
1245
|
/**
|
|
@@ -1808,6 +1814,7 @@ export class Parser {
|
|
|
1808
1814
|
* reorders operations for precendence
|
|
1809
1815
|
*/
|
|
1810
1816
|
protected ArrangeUnits(stream: ExpressionUnit[]): ExpressionUnit {
|
|
1817
|
+
|
|
1811
1818
|
// probably should not happen
|
|
1812
1819
|
if (stream.length === 0) return { type: 'missing', id: this.id_counter++ };
|
|
1813
1820
|
|
|
@@ -1908,7 +1915,41 @@ export class Parser {
|
|
|
1908
1915
|
}
|
|
1909
1916
|
}
|
|
1910
1917
|
|
|
1911
|
-
|
|
1918
|
+
//
|
|
1919
|
+
// why is this 2? are we thinking about combining complex numbers?
|
|
1920
|
+
// or ranges? (those would be binary). or was this for dimensioned
|
|
1921
|
+
// quantities? [actually that makes sense] [A: no, it wasn't that]
|
|
1922
|
+
//
|
|
1923
|
+
// actually what's the case where this is triggered and it's _not_
|
|
1924
|
+
// an error? can we find that?
|
|
1925
|
+
//
|
|
1926
|
+
|
|
1927
|
+
if (stack.length < 2) {
|
|
1928
|
+
|
|
1929
|
+
// we know that `element` is not an operator, because we
|
|
1930
|
+
// would have consumed it
|
|
1931
|
+
|
|
1932
|
+
if (stack.length === 1) {
|
|
1933
|
+
const a = stack[0].type;
|
|
1934
|
+
|
|
1935
|
+
if (a !== 'operator') {
|
|
1936
|
+
|
|
1937
|
+
// console.warn("unexpected element", stack[0], element);
|
|
1938
|
+
|
|
1939
|
+
this.error = `unexpected element [3]: ${element.type}`;
|
|
1940
|
+
this.error_position = (element.type === 'missing' || element.type === 'group' || element.type === 'dimensioned') ? -1 : element.position;
|
|
1941
|
+
this.valid = false;
|
|
1942
|
+
return {
|
|
1943
|
+
type: 'group',
|
|
1944
|
+
id: this.id_counter++,
|
|
1945
|
+
elements: stream,
|
|
1946
|
+
explicit: false,
|
|
1947
|
+
};
|
|
1948
|
+
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1912
1953
|
stack.push(element);
|
|
1913
1954
|
}
|
|
1914
1955
|
else if (stack[stack.length - 1].type === 'operator') {
|
|
@@ -2353,6 +2394,7 @@ export class Parser {
|
|
|
2353
2394
|
name: str,
|
|
2354
2395
|
args,
|
|
2355
2396
|
position,
|
|
2397
|
+
end: this.index, // testing
|
|
2356
2398
|
};
|
|
2357
2399
|
}
|
|
2358
2400
|
|