@trebco/treb 28.7.0 → 28.10.0
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-spreadsheet-light.mjs +14 -14
- package/dist/treb-spreadsheet.mjs +12 -12
- package/dist/treb.d.ts +37 -6
- package/package.json +1 -1
- package/treb-base-types/src/gradient.ts +1 -1
- package/treb-base-types/src/localization.ts +6 -0
- package/treb-calculator/src/calculator.ts +23 -30
- package/treb-calculator/src/dag/calculation_leaf_vertex.ts +7 -0
- package/treb-calculator/src/dag/graph.ts +7 -0
- package/treb-calculator/src/functions/base-functions.ts +30 -1
- package/treb-charts/src/chart-functions.ts +11 -0
- package/treb-charts/src/chart-types.ts +18 -0
- package/treb-charts/src/chart-utils.ts +87 -0
- package/treb-charts/src/chart.ts +4 -0
- package/treb-charts/src/default-chart-renderer.ts +26 -1
- package/treb-charts/src/renderer.ts +81 -0
- package/treb-charts/style/charts.scss +8 -0
- package/treb-embed/markup/toolbar.html +35 -34
- package/treb-embed/src/custom-element/treb-global.ts +10 -2
- package/treb-embed/src/embedded-spreadsheet.ts +57 -101
- package/treb-embed/src/options.ts +7 -0
- package/treb-embed/style/layout.scss +4 -0
- package/treb-embed/style/toolbar.scss +37 -0
- package/treb-grid/src/types/conditional_format.ts +1 -1
- package/treb-grid/src/types/grid.ts +11 -1
- package/treb-grid/src/types/grid_base.ts +127 -5
- package/treb-grid/src/types/grid_command.ts +32 -0
- package/treb-grid/src/types/grid_events.ts +7 -0
- package/treb-grid/src/types/grid_options.ts +8 -0
- package/treb-grid/src/types/sheet.ts +0 -56
- package/treb-grid/src/types/update_flags.ts +1 -0
|
@@ -10,7 +10,7 @@ import type { EmbeddedSpreadsheet } from '../embedded-spreadsheet';
|
|
|
10
10
|
export class TREBGlobal {
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Package version
|
|
14
14
|
*
|
|
15
15
|
* @privateRemarks
|
|
16
16
|
*
|
|
@@ -25,7 +25,15 @@ export class TREBGlobal {
|
|
|
25
25
|
public version = process.env.BUILD_VERSION || '';
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Create a spreadsheet. The `USER_DATA_TYPE` template parameter is the type
|
|
29
|
+
* assigned to the `user_data` field of the spreadsheet instance -- it can
|
|
30
|
+
* help simplify typing if you are storing extra data in spreadsheet
|
|
31
|
+
* files.
|
|
32
|
+
*
|
|
33
|
+
* Just ignore this parameter if you don't need it.
|
|
34
|
+
*
|
|
35
|
+
* @typeParam USER_DATA_TYPE - type for the `user_data` field in the
|
|
36
|
+
* spreadsheet instance
|
|
29
37
|
*/
|
|
30
38
|
public CreateSpreadsheet<USER_DATA_TYPE = unknown>(options: EmbeddedSpreadsheetOptions): EmbeddedSpreadsheet<USER_DATA_TYPE> {
|
|
31
39
|
const container = options.container;
|
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
} from 'treb-base-types';
|
|
70
70
|
|
|
71
71
|
import { EventSource, ValidateURI } from 'treb-utils';
|
|
72
|
-
import { NumberFormatCache, ValueParser, NumberFormat } from 'treb-format';
|
|
72
|
+
import { NumberFormatCache, ValueParser, NumberFormat, LotusDate, UnlotusDate } from 'treb-format';
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
|
|
@@ -486,13 +486,19 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
486
486
|
this.DocumentChange();
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
-
/**
|
|
489
|
+
/**
|
|
490
|
+
* opaque user data (metadata). `USER_DATA_TYPE` is a template
|
|
491
|
+
* parameter you can set when creating the spreadsheet.
|
|
492
|
+
*/
|
|
490
493
|
public get user_data(): USER_DATA_TYPE|undefined {
|
|
491
494
|
return this.grid.model.user_data;
|
|
492
495
|
}
|
|
493
496
|
|
|
494
|
-
/**
|
|
495
|
-
|
|
497
|
+
/**
|
|
498
|
+
* opaque user data (metadata). `USER_DATA_TYPE` is a template
|
|
499
|
+
* parameter you can set when creating the spreadsheet.
|
|
500
|
+
*/
|
|
501
|
+
public set user_data(data: USER_DATA_TYPE|undefined) {
|
|
496
502
|
this.grid.model.user_data = data;
|
|
497
503
|
this.DocumentChange();
|
|
498
504
|
}
|
|
@@ -883,6 +889,8 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
883
889
|
|
|
884
890
|
this.grid.grid_events.Subscribe((event) => {
|
|
885
891
|
|
|
892
|
+
// console.info({event});
|
|
893
|
+
|
|
886
894
|
switch (event.type) {
|
|
887
895
|
|
|
888
896
|
case 'error':
|
|
@@ -986,7 +994,14 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
986
994
|
|
|
987
995
|
case 'structure':
|
|
988
996
|
{
|
|
997
|
+
// console.info("S event", event);
|
|
998
|
+
|
|
989
999
|
const cached_selection = this.last_selection;
|
|
1000
|
+
if (event.conditional_format) {
|
|
1001
|
+
this.calculator.UpdateConditionals();
|
|
1002
|
+
this.ApplyConditionalFormats(this.grid.active_sheet, false);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
990
1005
|
if (event.rebuild_required) {
|
|
991
1006
|
this.calculator.Reset();
|
|
992
1007
|
|
|
@@ -1412,12 +1427,6 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
1412
1427
|
...options,
|
|
1413
1428
|
};
|
|
1414
1429
|
|
|
1415
|
-
// we need to calculate the formula once, to get an initial state
|
|
1416
|
-
// update: internal
|
|
1417
|
-
// let result = this.Evaluate(this.Unresolve(area, true, false) + ' ' + options.expression, options.options);
|
|
1418
|
-
|
|
1419
|
-
// ... apply ...
|
|
1420
|
-
|
|
1421
1430
|
this.AddConditionalFormat(format);
|
|
1422
1431
|
return format;
|
|
1423
1432
|
|
|
@@ -1445,18 +1454,6 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
1445
1454
|
...options,
|
|
1446
1455
|
};
|
|
1447
1456
|
|
|
1448
|
-
/*
|
|
1449
|
-
// we need to calculate the formula once, to get an initial state
|
|
1450
|
-
let result = this.Evaluate(options.expression, options.options);
|
|
1451
|
-
|
|
1452
|
-
if (Array.isArray(result)) {
|
|
1453
|
-
result = result[0][0];
|
|
1454
|
-
}
|
|
1455
|
-
const applied = !!result;
|
|
1456
|
-
|
|
1457
|
-
this.AddConditionalFormat({...format, applied });
|
|
1458
|
-
*/
|
|
1459
|
-
|
|
1460
1457
|
this.AddConditionalFormat(format);
|
|
1461
1458
|
|
|
1462
1459
|
return format;
|
|
@@ -1470,20 +1467,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
1470
1467
|
*/
|
|
1471
1468
|
public AddConditionalFormat(format: ConditionalFormat) {
|
|
1472
1469
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
if (!sheet) {
|
|
1476
|
-
throw new Error('invalid reference in format');
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
sheet.conditional_formats.push(format);
|
|
1480
|
-
|
|
1481
|
-
this.calculator.UpdateConditionals(format, sheet);
|
|
1482
|
-
|
|
1483
|
-
// call update if it's the current sheet
|
|
1484
|
-
this.ApplyConditionalFormats(sheet, sheet === this.grid.active_sheet);
|
|
1485
|
-
|
|
1486
|
-
this.PushUndo();
|
|
1470
|
+
this.grid.AddConditionalFormat(format);
|
|
1487
1471
|
|
|
1488
1472
|
// fluent
|
|
1489
1473
|
return format;
|
|
@@ -1496,40 +1480,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
1496
1480
|
* @internal
|
|
1497
1481
|
*/
|
|
1498
1482
|
public RemoveConditionalFormat(format: ConditionalFormat) {
|
|
1499
|
-
|
|
1500
|
-
const sheet = area.start.sheet_id ? this.model.sheets.Find(area.start.sheet_id) : this.grid.active_sheet;
|
|
1501
|
-
|
|
1502
|
-
if (!sheet) {
|
|
1503
|
-
throw new Error('invalid reference in format');
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
let count = 0;
|
|
1507
|
-
sheet.conditional_formats = sheet.conditional_formats.filter(test => {
|
|
1508
|
-
if (test === format) {
|
|
1509
|
-
count++;
|
|
1510
|
-
this.calculator.RemoveConditional(test);
|
|
1511
|
-
return false;
|
|
1512
|
-
}
|
|
1513
|
-
return true;
|
|
1514
|
-
});
|
|
1515
|
-
|
|
1516
|
-
if (count) {
|
|
1517
|
-
sheet.FlushConditionalFormats();
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
// we want to call update if it's the current sheet,
|
|
1521
|
-
// but we want a full repaint
|
|
1522
|
-
|
|
1523
|
-
this.ApplyConditionalFormats(sheet, false);
|
|
1524
|
-
|
|
1525
|
-
if (sheet === this.grid.active_sheet) {
|
|
1526
|
-
this.grid.Update(true);
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
if (count) {
|
|
1530
|
-
this.PushUndo();
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1483
|
+
this.grid.RemoveConditionalFormat({ format });
|
|
1533
1484
|
}
|
|
1534
1485
|
|
|
1535
1486
|
/**
|
|
@@ -1548,37 +1499,9 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
1548
1499
|
}
|
|
1549
1500
|
range = ref.area;
|
|
1550
1501
|
}
|
|
1551
|
-
const area = this.model.ResolveArea(range, this.grid.active_sheet);
|
|
1552
|
-
const sheet = area.start.sheet_id ? this.model.sheets.Find(area.start.sheet_id) : this.grid.active_sheet;
|
|
1553
|
-
|
|
1554
|
-
if (sheet) {
|
|
1555
|
-
let count = 0;
|
|
1556
|
-
|
|
1557
|
-
sheet.conditional_formats = sheet.conditional_formats.filter(test => {
|
|
1558
|
-
const compare = new Area(test.area.start, test.area.end);
|
|
1559
|
-
if (compare.Intersects(area)) {
|
|
1560
|
-
count++;
|
|
1561
|
-
this.calculator.RemoveConditional(test);
|
|
1562
|
-
return false;
|
|
1563
|
-
}
|
|
1564
|
-
return true;
|
|
1565
|
-
});
|
|
1566
|
-
|
|
1567
|
-
if (count) {
|
|
1568
|
-
|
|
1569
|
-
sheet.FlushConditionalFormats();
|
|
1570
|
-
|
|
1571
|
-
this.ApplyConditionalFormats(sheet, false);
|
|
1572
|
-
|
|
1573
|
-
if (sheet === this.grid.active_sheet) {
|
|
1574
|
-
this.grid.Update(true);
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
this.PushUndo();
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
}
|
|
1581
1502
|
|
|
1503
|
+
const area = this.model.ResolveArea(range, this.grid.active_sheet);
|
|
1504
|
+
this.grid.RemoveConditionalFormat({ area });
|
|
1582
1505
|
|
|
1583
1506
|
}
|
|
1584
1507
|
|
|
@@ -3977,6 +3900,23 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
3977
3900
|
return NumberFormatCache.Get(format).Format(value);
|
|
3978
3901
|
}
|
|
3979
3902
|
|
|
3903
|
+
/**
|
|
3904
|
+
* convert a javascript date (or timestamp) to a spreadsheet date
|
|
3905
|
+
*/
|
|
3906
|
+
public SpreadsheetDate(javascript_date: number|Date) {
|
|
3907
|
+
if (javascript_date instanceof Date) {
|
|
3908
|
+
javascript_date = javascript_date.getTime();
|
|
3909
|
+
}
|
|
3910
|
+
return UnlotusDate(javascript_date, true);
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
/**
|
|
3914
|
+
* convert a spreadsheet date to a javascript date
|
|
3915
|
+
*/
|
|
3916
|
+
public JavascriptDate(spreadsheet_date: number) {
|
|
3917
|
+
return LotusDate(spreadsheet_date).getTime();
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3980
3920
|
/**
|
|
3981
3921
|
* Apply borders to range.
|
|
3982
3922
|
*
|
|
@@ -4611,7 +4551,13 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
4611
4551
|
|
|
4612
4552
|
this.UpdateAnnotations();
|
|
4613
4553
|
|
|
4554
|
+
// we don't really need to call UpdateConditionals here unless
|
|
4555
|
+
// something has changed in a previously inactive sheet -- right?
|
|
4556
|
+
|
|
4614
4557
|
this.calculator.UpdateConditionals();
|
|
4558
|
+
|
|
4559
|
+
// we do need to call apply (I think)
|
|
4560
|
+
|
|
4615
4561
|
this.ApplyConditionalFormats(event.activate, true);
|
|
4616
4562
|
|
|
4617
4563
|
}
|
|
@@ -5798,15 +5744,25 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
5798
5744
|
|
|
5799
5745
|
/**
|
|
5800
5746
|
* handle key down to intercept ctrl+z (undo)
|
|
5801
|
-
*
|
|
5747
|
+
* UPDATE: we're also handling F9 for recalc (optionally)
|
|
5748
|
+
*
|
|
5802
5749
|
* FIXME: redo (ctrl+y or ctrl+shift+z)
|
|
5803
5750
|
*/
|
|
5804
5751
|
protected HandleKeyDown(event: KeyboardEvent): void {
|
|
5752
|
+
|
|
5753
|
+
// can we drop the event.code stuff in 2024? (YES)
|
|
5754
|
+
|
|
5805
5755
|
if (event.ctrlKey && (event.code === 'KeyZ' || event.key === 'z')) {
|
|
5806
5756
|
event.stopPropagation();
|
|
5807
5757
|
event.preventDefault();
|
|
5808
5758
|
this.Undo();
|
|
5809
5759
|
}
|
|
5760
|
+
else if (event.key === 'F9' && this.options.recalculate_on_f9) {
|
|
5761
|
+
event.stopPropagation();
|
|
5762
|
+
event.preventDefault();
|
|
5763
|
+
this.Recalculate();
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5810
5766
|
}
|
|
5811
5767
|
|
|
5812
5768
|
|
|
@@ -304,6 +304,13 @@ export interface EmbeddedSpreadsheetOptions {
|
|
|
304
304
|
*/
|
|
305
305
|
preload?: (instance: unknown) => void;
|
|
306
306
|
|
|
307
|
+
/**
|
|
308
|
+
* handle the F9 key and recalculate the spreadsheet. for compatibility.
|
|
309
|
+
* we're leaving this option to default `false` for now, but that may
|
|
310
|
+
* change in the future. key modifiers have no effect.
|
|
311
|
+
*/
|
|
312
|
+
recalculate_on_f9?: boolean;
|
|
313
|
+
|
|
307
314
|
}
|
|
308
315
|
|
|
309
316
|
/**
|
|
@@ -64,6 +64,10 @@
|
|
|
64
64
|
|
|
65
65
|
font-family: var(--treb-default-font, system-ui, $font-stack);
|
|
66
66
|
|
|
67
|
+
// reset in case we inherit something
|
|
68
|
+
|
|
69
|
+
line-height: normal;
|
|
70
|
+
|
|
67
71
|
div, button, input, ul, ol, li, a, textarea, svg {
|
|
68
72
|
|
|
69
73
|
// maybe this is being too aggressive. we could be a little
|
|
@@ -411,6 +411,43 @@ $swatch-size: 18px;
|
|
|
411
411
|
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
.treb-font-scale {
|
|
415
|
+
padding-left: 2em;
|
|
416
|
+
width: 5em;
|
|
417
|
+
text-align: right;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
[composite][font-scale] {
|
|
421
|
+
position: relative;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.treb-font-scale-icon {
|
|
425
|
+
position: absolute;
|
|
426
|
+
top: 50%;
|
|
427
|
+
left: .5em;
|
|
428
|
+
transform: translateY(-50%);
|
|
429
|
+
opacity: .9;
|
|
430
|
+
// border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
|
|
431
|
+
border-radius: 3px;
|
|
432
|
+
// background: color-mix(in srgb, currentColor 10%, transparent);
|
|
433
|
+
|
|
434
|
+
pointer-events: none;
|
|
435
|
+
line-height: 1;
|
|
436
|
+
|
|
437
|
+
&::before, &::after {
|
|
438
|
+
content: 'A';
|
|
439
|
+
position: relative;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
&::before {
|
|
443
|
+
font-size: 1.2em;
|
|
444
|
+
}
|
|
445
|
+
&::after {
|
|
446
|
+
font-size: .9em;
|
|
447
|
+
left: -.125em;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
414
451
|
}
|
|
415
452
|
|
|
416
453
|
}
|
|
@@ -4627,6 +4627,13 @@ export class Grid extends GridBase {
|
|
|
4627
4627
|
}
|
|
4628
4628
|
}
|
|
4629
4629
|
else {
|
|
4630
|
+
|
|
4631
|
+
// ignore function keys
|
|
4632
|
+
|
|
4633
|
+
if (/^F\d+$/.test(event.key)) {
|
|
4634
|
+
return;
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4630
4637
|
switch (event.key) {
|
|
4631
4638
|
case 'Tab':
|
|
4632
4639
|
if (event.shiftKey) delta.columns--;
|
|
@@ -4687,7 +4694,10 @@ export class Grid extends GridBase {
|
|
|
4687
4694
|
return;
|
|
4688
4695
|
|
|
4689
4696
|
default:
|
|
4690
|
-
|
|
4697
|
+
|
|
4698
|
+
// FIXME: we're handling F9 (optionally) in the embedded
|
|
4699
|
+
// component. this handler should ignore all function keys.
|
|
4700
|
+
// not sure there's a good global for that, though. regex?
|
|
4691
4701
|
|
|
4692
4702
|
if (!selection.empty) {
|
|
4693
4703
|
if (event.key !== 'Escape') {
|
|
@@ -67,6 +67,10 @@ interface PatchOptions extends PatchAreaOptions {
|
|
|
67
67
|
sheet: Sheet;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
const AssertNever = (value: never) => {
|
|
71
|
+
console.error('invalid case');
|
|
72
|
+
};
|
|
73
|
+
|
|
70
74
|
export class GridBase {
|
|
71
75
|
|
|
72
76
|
// --- public members --------------------------------------------------------
|
|
@@ -160,6 +164,24 @@ export class GridBase {
|
|
|
160
164
|
|
|
161
165
|
// --- API methods -----------------------------------------------------------
|
|
162
166
|
|
|
167
|
+
public RemoveConditionalFormat(options: {
|
|
168
|
+
format?: ConditionalFormat;
|
|
169
|
+
area?: IArea;
|
|
170
|
+
}) {
|
|
171
|
+
|
|
172
|
+
this.ExecCommand({
|
|
173
|
+
key: CommandKey.RemoveConditionalFormat,
|
|
174
|
+
...options,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public AddConditionalFormat(format: ConditionalFormat) {
|
|
179
|
+
this.ExecCommand({
|
|
180
|
+
key: CommandKey.AddConditionalFormat,
|
|
181
|
+
format,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
163
185
|
/** remove a table. doesn't remove any data, just removes the overlay. */
|
|
164
186
|
public RemoveTable(table: Table) {
|
|
165
187
|
this.ExecCommand({
|
|
@@ -1887,7 +1909,7 @@ export class GridBase {
|
|
|
1887
1909
|
break;
|
|
1888
1910
|
*/
|
|
1889
1911
|
|
|
1890
|
-
// special
|
|
1912
|
+
// special cases
|
|
1891
1913
|
case CommandKey.SortTable:
|
|
1892
1914
|
case CommandKey.RemoveTable:
|
|
1893
1915
|
if (!command.table.area.start.sheet_id) {
|
|
@@ -1895,6 +1917,12 @@ export class GridBase {
|
|
|
1895
1917
|
}
|
|
1896
1918
|
break;
|
|
1897
1919
|
|
|
1920
|
+
case CommandKey.AddConditionalFormat:
|
|
1921
|
+
if (!command.format.area.start.sheet_id) {
|
|
1922
|
+
command.format.area.start.sheet_id = id;
|
|
1923
|
+
}
|
|
1924
|
+
break;
|
|
1925
|
+
|
|
1898
1926
|
// field
|
|
1899
1927
|
case CommandKey.ResizeRows:
|
|
1900
1928
|
case CommandKey.ResizeColumns:
|
|
@@ -1920,6 +1948,14 @@ export class GridBase {
|
|
|
1920
1948
|
case CommandKey.Select:
|
|
1921
1949
|
case CommandKey.InsertTable:
|
|
1922
1950
|
|
|
1951
|
+
// note that remove conditional format could have a format
|
|
1952
|
+
// object (with an area) instead of an area. but in that case,
|
|
1953
|
+
// the format object must match an existing format, so it would
|
|
1954
|
+
// have to have a proper area. there's no case where we would
|
|
1955
|
+
// want to add it. so we only handle the area case.
|
|
1956
|
+
|
|
1957
|
+
case CommandKey.RemoveConditionalFormat:
|
|
1958
|
+
|
|
1923
1959
|
if (command.area) {
|
|
1924
1960
|
if (IsCellAddress(command.area)) {
|
|
1925
1961
|
if (!command.area.sheet_id) {
|
|
@@ -1934,10 +1970,13 @@ export class GridBase {
|
|
|
1934
1970
|
}
|
|
1935
1971
|
break;
|
|
1936
1972
|
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1973
|
+
default:
|
|
1974
|
+
|
|
1975
|
+
// this is intended to check that we've handled all cases. if you
|
|
1976
|
+
// add additional commands and they're not handled, this should
|
|
1977
|
+
// raise a ts error.
|
|
1978
|
+
|
|
1979
|
+
AssertNever(command);
|
|
1941
1980
|
|
|
1942
1981
|
}
|
|
1943
1982
|
}
|
|
@@ -3534,6 +3573,88 @@ export class GridBase {
|
|
|
3534
3573
|
|
|
3535
3574
|
break;
|
|
3536
3575
|
|
|
3576
|
+
case CommandKey.AddConditionalFormat:
|
|
3577
|
+
{
|
|
3578
|
+
|
|
3579
|
+
const sheet = this.FindSheet(command.format.area);
|
|
3580
|
+
sheet.conditional_formats.push(command.format);
|
|
3581
|
+
|
|
3582
|
+
sheet.Invalidate(new Area(command.format.area.start, command.format.area.end));
|
|
3583
|
+
|
|
3584
|
+
if (sheet === this.active_sheet) {
|
|
3585
|
+
// flags.style_area = Area.Join(command.format.area, flags.style_area);
|
|
3586
|
+
flags.render_area = Area.Join(command.format.area, flags.render_area);
|
|
3587
|
+
}
|
|
3588
|
+
else {
|
|
3589
|
+
// flags.style_event = true;
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
flags.structure_event = true;
|
|
3593
|
+
flags.conditional_formatting_event = true;
|
|
3594
|
+
|
|
3595
|
+
}
|
|
3596
|
+
break;
|
|
3597
|
+
|
|
3598
|
+
case CommandKey.RemoveConditionalFormat:
|
|
3599
|
+
|
|
3600
|
+
{
|
|
3601
|
+
let sheet: Sheet|undefined;
|
|
3602
|
+
let count = 0;
|
|
3603
|
+
|
|
3604
|
+
if (command.format) {
|
|
3605
|
+
|
|
3606
|
+
// we're removing by object equivalence, not strict equality.
|
|
3607
|
+
// this is in case we're switching contexts and the objects
|
|
3608
|
+
// are not strictly equal. may be unecessary. do we need to
|
|
3609
|
+
// normalize in some way? (...)
|
|
3610
|
+
|
|
3611
|
+
const format = JSON.stringify(command.format);
|
|
3612
|
+
|
|
3613
|
+
sheet = this.FindSheet(command.format.area);
|
|
3614
|
+
sheet.conditional_formats = sheet.conditional_formats.filter(test => {
|
|
3615
|
+
// if (test === command.format) {
|
|
3616
|
+
if (JSON.stringify(test) === format) {
|
|
3617
|
+
count++;
|
|
3618
|
+
flags.render_area = Area.Join(test.area, flags.render_area);
|
|
3619
|
+
return false;
|
|
3620
|
+
}
|
|
3621
|
+
return true;
|
|
3622
|
+
});
|
|
3623
|
+
|
|
3624
|
+
}
|
|
3625
|
+
else if (command.area) {
|
|
3626
|
+
const area = new Area(command.area.start, command.area.end);
|
|
3627
|
+
sheet = this.FindSheet(command.area);
|
|
3628
|
+
|
|
3629
|
+
sheet.conditional_formats = sheet.conditional_formats.filter(test => {
|
|
3630
|
+
const compare = new Area(test.area.start, test.area.end);
|
|
3631
|
+
if (compare.Intersects(area)) {
|
|
3632
|
+
count++;
|
|
3633
|
+
flags.render_area = Area.Join(compare, flags.render_area);
|
|
3634
|
+
return false;
|
|
3635
|
+
}
|
|
3636
|
+
return true;
|
|
3637
|
+
});
|
|
3638
|
+
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
if (sheet && count) {
|
|
3642
|
+
sheet.FlushConditionalFormats();
|
|
3643
|
+
flags.structure_event = true;
|
|
3644
|
+
|
|
3645
|
+
// this will flush leaf vertices. but it's expensive because
|
|
3646
|
+
// it's rebuilding the whole graph. we could maybe reduce a
|
|
3647
|
+
// bit... the question is what's worse: rebuilding the graph
|
|
3648
|
+
// or leaving orphans for a while?
|
|
3649
|
+
|
|
3650
|
+
// flags.structure_rebuild_required = true;
|
|
3651
|
+
|
|
3652
|
+
flags.conditional_formatting_event = true;
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
}
|
|
3656
|
+
break;
|
|
3657
|
+
|
|
3537
3658
|
case CommandKey.InsertTable:
|
|
3538
3659
|
|
|
3539
3660
|
// the most important thing here is validating that we can
|
|
@@ -4170,6 +4291,7 @@ export class GridBase {
|
|
|
4170
4291
|
events.push({
|
|
4171
4292
|
type: 'structure',
|
|
4172
4293
|
rebuild_required: flags.structure_rebuild_required,
|
|
4294
|
+
conditional_format: flags.conditional_formatting_event,
|
|
4173
4295
|
});
|
|
4174
4296
|
}
|
|
4175
4297
|
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import type { ICellAddress, IArea, Style, CellStyle, Color, CellValue, Table, TableSortType, TableTheme } from 'treb-base-types';
|
|
23
23
|
import type { ExpressionUnit } from 'treb-parser';
|
|
24
24
|
import type { BorderConstants } from './border_constants';
|
|
25
|
+
import type { ConditionalFormat } from './conditional_format';
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* switching to an exec-command based model, so we can serialize
|
|
@@ -73,6 +74,9 @@ export enum CommandKey {
|
|
|
73
74
|
InsertTable,
|
|
74
75
|
RemoveTable,
|
|
75
76
|
|
|
77
|
+
AddConditionalFormat,
|
|
78
|
+
RemoveConditionalFormat,
|
|
79
|
+
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
/** base type for sheet commands -- can select sheet by name, id or index */
|
|
@@ -418,6 +422,32 @@ export interface ReorderSheetCommand {
|
|
|
418
422
|
move_before: number;
|
|
419
423
|
}
|
|
420
424
|
|
|
425
|
+
/**
|
|
426
|
+
* add conditional format
|
|
427
|
+
*/
|
|
428
|
+
export interface AddConditionalFormatCommand {
|
|
429
|
+
key: CommandKey.AddConditionalFormat;
|
|
430
|
+
format: ConditionalFormat;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* remove conditional format, either as an object or from a target
|
|
435
|
+
* area. as an object, we'll match using object equivalence and not
|
|
436
|
+
* identity.
|
|
437
|
+
*/
|
|
438
|
+
export interface RemoveConditionalFormatCommand {
|
|
439
|
+
|
|
440
|
+
key: CommandKey.RemoveConditionalFormat;
|
|
441
|
+
|
|
442
|
+
/** if format is omitted, we will remove all formats from the target range */
|
|
443
|
+
format?: ConditionalFormat;
|
|
444
|
+
|
|
445
|
+
/** one of area or format should be supplied */
|
|
446
|
+
area?: IArea;
|
|
447
|
+
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
421
451
|
/**
|
|
422
452
|
* ephemeral flag added to commands.
|
|
423
453
|
* /
|
|
@@ -459,6 +489,8 @@ export type Command =
|
|
|
459
489
|
| ActivateSheetCommand
|
|
460
490
|
| DataValidationCommand
|
|
461
491
|
| DuplicateSheetCommand
|
|
492
|
+
| AddConditionalFormatCommand
|
|
493
|
+
| RemoveConditionalFormatCommand
|
|
462
494
|
) ; // & Ephemeral;
|
|
463
495
|
|
|
464
496
|
/**
|
|
@@ -23,6 +23,7 @@ import type { GridSelection } from './grid_selection';
|
|
|
23
23
|
import type { Annotation } from './annotation';
|
|
24
24
|
import type { Sheet } from './sheet';
|
|
25
25
|
import type { Area } from 'treb-base-types';
|
|
26
|
+
import type { ConditionalFormat } from './conditional_format';
|
|
26
27
|
|
|
27
28
|
export enum ErrorCode {
|
|
28
29
|
|
|
@@ -80,6 +81,12 @@ export interface StructureEvent {
|
|
|
80
81
|
* FIXME: merge/unmerge? (...) I think yes
|
|
81
82
|
*/
|
|
82
83
|
rebuild_required?: boolean;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* we can use this when conditional formats are modified/added/removed
|
|
87
|
+
*/
|
|
88
|
+
conditional_format?: boolean;
|
|
89
|
+
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
export interface AnnotationEvent {
|
|
@@ -24,6 +24,14 @@ import type { StatsEntry } from './tab_bar';
|
|
|
24
24
|
|
|
25
25
|
export type StatsFunction = (data: CellValue|CellValue[][]|undefined) => StatsEntry[];
|
|
26
26
|
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @internalRemarks
|
|
30
|
+
*
|
|
31
|
+
* why are there two levels of options? can we consolidate this with
|
|
32
|
+
* the embedded spreadsheet options? they are never (AFAICT) used
|
|
33
|
+
* independently. maybe that's recent.
|
|
34
|
+
*/
|
|
27
35
|
export interface GridOptions {
|
|
28
36
|
|
|
29
37
|
/** can expand rows/columns */
|