@trebco/treb 27.7.6 → 27.9.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.
Files changed (36) hide show
  1. package/dist/treb-spreadsheet.mjs +14 -14
  2. package/dist/treb.d.ts +21 -21
  3. package/notes/conditional-fomratring.md +29 -0
  4. package/package.json +1 -1
  5. package/treb-base-types/src/area.ts +181 -0
  6. package/treb-base-types/src/evaluate-options.ts +21 -0
  7. package/treb-base-types/src/gradient.ts +97 -0
  8. package/treb-base-types/src/import.ts +2 -1
  9. package/treb-base-types/src/index.ts +2 -0
  10. package/treb-calculator/src/calculator.ts +190 -28
  11. package/treb-calculator/src/dag/calculation_leaf_vertex.ts +97 -0
  12. package/treb-calculator/src/dag/graph.ts +10 -22
  13. package/treb-calculator/src/dag/{leaf_vertex.ts → state_leaf_vertex.ts} +3 -3
  14. package/treb-calculator/src/descriptors.ts +10 -3
  15. package/treb-calculator/src/expression-calculator.ts +1 -1
  16. package/treb-calculator/src/function-library.ts +25 -22
  17. package/treb-calculator/src/functions/base-functions.ts +166 -5
  18. package/treb-calculator/src/index.ts +6 -6
  19. package/treb-calculator/src/notifier-types.ts +1 -1
  20. package/treb-calculator/src/utilities.ts +2 -2
  21. package/treb-charts/src/util.ts +2 -2
  22. package/treb-embed/src/embedded-spreadsheet.ts +352 -41
  23. package/treb-export/src/export-worker/export-worker.ts +0 -13
  24. package/treb-export/src/export2.ts +187 -2
  25. package/treb-export/src/import2.ts +169 -4
  26. package/treb-export/src/workbook-style2.ts +56 -8
  27. package/treb-export/src/workbook2.ts +10 -1
  28. package/treb-grid/src/index.ts +2 -1
  29. package/treb-grid/src/layout/base_layout.ts +23 -15
  30. package/treb-grid/src/render/tile_renderer.ts +2 -1
  31. package/treb-grid/src/types/conditional_format.ts +168 -0
  32. package/treb-grid/src/types/grid.ts +5 -6
  33. package/treb-grid/src/types/grid_base.ts +186 -33
  34. package/treb-grid/src/types/sheet.ts +330 -26
  35. package/treb-grid/src/types/sheet_types.ts +4 -0
  36. /package/{README-shadow-DOM.md → notes/shadow-DOM.md} +0 -0
@@ -32,33 +32,47 @@ import type {
32
32
  AnnotationViewData,
33
33
  AnnotationType,
34
34
  ExternalEditorConfig,
35
+ ConditionalFormatDuplicateValuesOptions,
36
+ ConditionalFormatDuplicateValues,
37
+ ConditionalFormatGradientOptions,
38
+ ConditionalFormat,
39
+ ConditionalFormatGradient,
40
+ ConditionalFormatExpression,
41
+ StandardGradient,
42
+ CondifionalFormatExpressionOptions,
43
+ ConditionalFormatCellMatchOptions,
44
+ ConditionalFormatCellMatch,
35
45
  } from 'treb-grid';
36
46
 
37
47
  import {
38
- DataModel, Grid, BorderConstants, Sheet, ErrorCode, UA
48
+ DataModel, Grid, BorderConstants, Sheet, ErrorCode, UA,
49
+ StandardGradientsList,
39
50
  } from 'treb-grid';
40
51
 
41
52
  import {
42
53
  Parser, DecimalMarkType,
43
54
  ArgumentSeparatorType, QuotedSheetNameRegex } from 'treb-parser';
44
55
 
45
- import { Calculator, type EvaluateOptions, type LeafVertex } from 'treb-calculator';
56
+ import { Calculator, type LeafVertex } from 'treb-calculator';
46
57
 
47
58
  import type {
48
59
  ICellAddress,
60
+ EvaluateOptions,
49
61
  IArea, CellValue, Point,
50
62
  Complex, ExtendedUnion, IRectangle,
51
- AddressReference, RangeReference, TableSortOptions, Table, TableTheme,
63
+ AddressReference, RangeReference, TableSortOptions, Table, TableTheme, GradientStop,
52
64
  } from 'treb-base-types';
53
65
 
54
66
  import {
55
67
  IsArea, ThemeColorTable, ComplexToString, Rectangle, IsComplex, type CellStyle,
56
- Localization, Style, type Color, ThemeColor2, IsCellAddress, Area, IsFlatData, IsFlatDataArray,
68
+ Localization, Style, type Color, ThemeColor2, IsCellAddress, Area, IsFlatData, IsFlatDataArray, Gradient, ValueType,
57
69
  } from 'treb-base-types';
58
70
 
59
71
  import { EventSource, Yield, ValidateURI } from 'treb-utils';
60
72
  import { NumberFormatCache, ValueParser, NumberFormat } from 'treb-format';
61
73
 
74
+
75
+
62
76
  // --- local -------------------------------------------------------------------
63
77
 
64
78
  import { Dialog, DialogType } from './progress-dialog';
@@ -81,6 +95,7 @@ import type { SetRangeOptions } from 'treb-grid';
81
95
  * the script so we can run it as a worker.
82
96
  */
83
97
  import * as export_worker_script from 'worker:../../treb-export/src/export-worker/index.worker';
98
+ import { StateLeafVertex } from 'treb-calculator/src/dag/state_leaf_vertex';
84
99
 
85
100
  // --- types -------------------------------------------------------------------
86
101
 
@@ -1251,6 +1266,265 @@ export class EmbeddedSpreadsheet {
1251
1266
 
1252
1267
  }
1253
1268
 
1269
+ //////////////////////////////////////////////////////////////////////////////
1270
+ //
1271
+ // conditional formatting API (WIP)
1272
+ //
1273
+
1274
+ /**
1275
+ * list conditional formats. uses the active sheet by default, or pass a
1276
+ * sheet name or id.
1277
+ *
1278
+ * @internal
1279
+ */
1280
+ public ListConditionalFormats(sheet?: number|string) {
1281
+
1282
+ const target = (typeof sheet === 'undefined') ?
1283
+ this.grid.active_sheet :
1284
+ this.model.sheets.Find(sheet);
1285
+
1286
+ return target?.conditional_formats || [];
1287
+
1288
+ }
1289
+
1290
+ /** @internal */
1291
+ public ConditionalFormatDuplicateValues(range: RangeReference|undefined, options: ConditionalFormatDuplicateValuesOptions): ConditionalFormat {
1292
+
1293
+ if (range === undefined) {
1294
+ const ref = this.GetSelectionReference();
1295
+ if (ref.empty) {
1296
+ throw new Error('invalid range (no selection)');
1297
+ }
1298
+ range = ref.area;
1299
+ }
1300
+
1301
+ return this.AddConditionalFormat({
1302
+ type: 'duplicate-values',
1303
+ area: this.model.ResolveArea(range, this.grid.active_sheet),
1304
+ ...options,
1305
+ });
1306
+
1307
+ }
1308
+
1309
+ /**
1310
+ * @internal
1311
+ */
1312
+ public ConditionalFormatGradient(range: RangeReference|undefined, options: ConditionalFormatGradientOptions|StandardGradient): ConditionalFormat {
1313
+
1314
+ if (range === undefined) {
1315
+ const ref = this.GetSelectionReference();
1316
+ if (ref.empty) {
1317
+ throw new Error('invalid range (no selection)');
1318
+ }
1319
+ range = ref.area;
1320
+ }
1321
+
1322
+ const area = this.model.ResolveArea(range, this.grid.active_sheet);
1323
+
1324
+ const format: ConditionalFormatGradient = (typeof options === 'object') ?
1325
+ {
1326
+ type: 'gradient', area, ...options,
1327
+ } :
1328
+ {
1329
+ type: 'gradient', area, ...StandardGradientsList[options]
1330
+ };
1331
+
1332
+ this.AddConditionalFormat(format);
1333
+ return format;
1334
+
1335
+ }
1336
+
1337
+ /** @internal */
1338
+ public ConditionalFormatCellMatch(range: RangeReference|undefined, options: ConditionalFormatCellMatchOptions): ConditionalFormat {
1339
+
1340
+ if (range === undefined) {
1341
+ const ref = this.GetSelectionReference();
1342
+ if (ref.empty) {
1343
+ throw new Error('invalid range (no selection)');
1344
+ }
1345
+ range = ref.area;
1346
+ }
1347
+
1348
+ const area = this.model.ResolveArea(range, this.grid.active_sheet);
1349
+
1350
+ const format: ConditionalFormatCellMatch = {
1351
+ type: 'cell-match',
1352
+ area,
1353
+ ...options,
1354
+ };
1355
+
1356
+ // we need to calculate the formula once, to get an initial state
1357
+ // update: internal
1358
+ // let result = this.Evaluate(this.Unresolve(area, true, false) + ' ' + options.expression, options.options);
1359
+
1360
+ // ... apply ...
1361
+
1362
+ this.AddConditionalFormat(format);
1363
+ return format;
1364
+
1365
+
1366
+ }
1367
+
1368
+ /**
1369
+ * @internal
1370
+ */
1371
+ public ConditionalFormatExpression(range: RangeReference|undefined, options: CondifionalFormatExpressionOptions): ConditionalFormat {
1372
+
1373
+ if (range === undefined) {
1374
+ const ref = this.GetSelectionReference();
1375
+ if (ref.empty) {
1376
+ throw new Error('invalid range (no selection)');
1377
+ }
1378
+ range = ref.area;
1379
+ }
1380
+
1381
+ const area = this.model.ResolveArea(range, this.grid.active_sheet);
1382
+
1383
+ const format: ConditionalFormatExpression = {
1384
+ type: 'expression',
1385
+ area,
1386
+ ...options,
1387
+ };
1388
+
1389
+ /*
1390
+ // we need to calculate the formula once, to get an initial state
1391
+ let result = this.Evaluate(options.expression, options.options);
1392
+
1393
+ if (Array.isArray(result)) {
1394
+ result = result[0][0];
1395
+ }
1396
+ const applied = !!result;
1397
+
1398
+ this.AddConditionalFormat({...format, applied });
1399
+ */
1400
+
1401
+ this.AddConditionalFormat(format);
1402
+
1403
+ return format;
1404
+
1405
+ }
1406
+
1407
+ /**
1408
+ * add a conditional format
1409
+ *
1410
+ * @internal
1411
+ */
1412
+ public AddConditionalFormat(format: ConditionalFormat) {
1413
+
1414
+ const sheet = this.model.sheets.Find(format.area.start.sheet_id||0);
1415
+
1416
+ if (!sheet) {
1417
+ throw new Error('invalid reference in format');
1418
+ }
1419
+
1420
+ sheet.conditional_formats.push(format);
1421
+
1422
+ this.calculator.UpdateConditionals(format, sheet);
1423
+
1424
+ // call update if it's the current sheet
1425
+ this.ApplyConditionalFormats(sheet, sheet === this.grid.active_sheet);
1426
+
1427
+ this.PushUndo();
1428
+
1429
+ // fluent
1430
+ return format;
1431
+
1432
+ }
1433
+
1434
+ /**
1435
+ * remove conditional format
1436
+ *
1437
+ * @internal
1438
+ */
1439
+ public RemoveConditionalFormat(format: ConditionalFormat) {
1440
+ const area = format.area;
1441
+ const sheet = area.start.sheet_id ? this.model.sheets.Find(area.start.sheet_id) : this.grid.active_sheet;
1442
+
1443
+ if (!sheet) {
1444
+ throw new Error('invalid reference in format');
1445
+ }
1446
+
1447
+ let count = 0;
1448
+ sheet.conditional_formats = sheet.conditional_formats.filter(test => {
1449
+ if (test === format) {
1450
+ count++;
1451
+ this.calculator.RemoveConditional(test);
1452
+ return false;
1453
+ }
1454
+ return true;
1455
+ });
1456
+
1457
+ if (count) {
1458
+ sheet.FlushConditionalFormats();
1459
+ }
1460
+
1461
+ // we want to call update if it's the current sheet,
1462
+ // but we want a full repaint
1463
+
1464
+ this.ApplyConditionalFormats(sheet, false);
1465
+
1466
+ if (sheet === this.grid.active_sheet) {
1467
+ this.grid.Update(true);
1468
+ }
1469
+
1470
+ if (count) {
1471
+ this.PushUndo();
1472
+ }
1473
+
1474
+ }
1475
+
1476
+ /**
1477
+ * clear conditional formats from the target range (or currently selected
1478
+ * range). we operate on format objects, meaning we'll remove the whole
1479
+ * format object rather than clip the area.
1480
+ *
1481
+ * @internal
1482
+ */
1483
+ public RemoveConditionalFormats(range?: RangeReference) {
1484
+
1485
+ if (range === undefined) {
1486
+ const ref = this.GetSelectionReference();
1487
+ if (ref.empty) {
1488
+ throw new Error('invalid range (no selection)');
1489
+ }
1490
+ range = ref.area;
1491
+ }
1492
+ const area = this.model.ResolveArea(range, this.grid.active_sheet);
1493
+ const sheet = area.start.sheet_id ? this.model.sheets.Find(area.start.sheet_id) : this.grid.active_sheet;
1494
+
1495
+ if (sheet) {
1496
+ let count = 0;
1497
+
1498
+ sheet.conditional_formats = sheet.conditional_formats.filter(test => {
1499
+ const compare = new Area(test.area.start, test.area.end);
1500
+ if (compare.Intersects(area)) {
1501
+ count++;
1502
+ this.calculator.RemoveConditional(test);
1503
+ return false;
1504
+ }
1505
+ return true;
1506
+ });
1507
+
1508
+ if (count) {
1509
+
1510
+ sheet.FlushConditionalFormats();
1511
+
1512
+ this.ApplyConditionalFormats(sheet, false);
1513
+
1514
+ if (sheet === this.grid.active_sheet) {
1515
+ this.grid.Update(true);
1516
+ }
1517
+
1518
+ this.PushUndo();
1519
+ }
1520
+
1521
+ }
1522
+
1523
+
1524
+ }
1525
+
1526
+ //////////////////////////////////////////////////////////////////////////////
1527
+
1254
1528
  /**
1255
1529
  * @internal
1256
1530
  */
@@ -1794,6 +2068,19 @@ export class EmbeddedSpreadsheet {
1794
2068
 
1795
2069
  this.UpdateDocumentStyles();
1796
2070
 
2071
+ // we need to flush conditional formats that use theme colors
2072
+ // (I guess we're just flushing everybody?)
2073
+
2074
+ for (const sheet of this.model.sheets.list) {
2075
+ for (const format of sheet.conditional_formats) {
2076
+ format.internal = undefined;
2077
+ }
2078
+ }
2079
+
2080
+ this.calculator.UpdateConditionals();
2081
+ this.ApplyConditionalFormats(this.grid.active_sheet, false);
2082
+ this.grid.Update(true);
2083
+
1797
2084
  }
1798
2085
 
1799
2086
  /**
@@ -2954,11 +3241,11 @@ export class EmbeddedSpreadsheet {
2954
3241
  // FIXME: optional? parameter? (...)
2955
3242
 
2956
3243
  if (data.rendered_values && !options.recalculate) {
2957
- this.grid.Update();
2958
3244
  this.calculator.RebuildClean(true);
3245
+ this.ApplyConditionalFormats(this.grid.active_sheet, false);
3246
+ this.grid.Update();
2959
3247
  }
2960
3248
  else {
2961
- // console.info('load recalc');
2962
3249
  this.Recalculate();
2963
3250
  }
2964
3251
 
@@ -3248,6 +3535,7 @@ export class EmbeddedSpreadsheet {
3248
3535
  }
3249
3536
 
3250
3537
  this.calculator.Calculate(area);
3538
+ this.ApplyConditionalFormats(this.grid.active_sheet, false);
3251
3539
 
3252
3540
  this.grid.Update(true); // , area);
3253
3541
  this.UpdateAnnotations();
@@ -3432,6 +3720,9 @@ export class EmbeddedSpreadsheet {
3432
3720
  ref = resolved;
3433
3721
  }
3434
3722
 
3723
+ return this.calculator.Unresolve(ref, this.grid.active_sheet, qualified, named);
3724
+
3725
+ /*
3435
3726
  let range = '';
3436
3727
  const area = IsCellAddress(ref) ? new Area(ref) : new Area(ref.start, ref.end);
3437
3728
 
@@ -3457,10 +3748,11 @@ export class EmbeddedSpreadsheet {
3457
3748
  // the active selection must be on the active sheet? (...)
3458
3749
 
3459
3750
  const sheet_id = area.start.sheet_id || this.grid.active_sheet.id;
3460
- const sheet_name = this.ResolveSheetName(sheet_id, true);
3751
+ const sheet_name = this.calculator.ResolveSheetName(sheet_id, true);
3461
3752
 
3462
3753
  return sheet_name ? sheet_name + '!' + range : range;
3463
-
3754
+ */
3755
+
3464
3756
  }
3465
3757
 
3466
3758
  /**
@@ -3482,7 +3774,7 @@ export class EmbeddedSpreadsheet {
3482
3774
 
3483
3775
  return this.calculator.Evaluate(
3484
3776
  expression, this.grid.active_sheet, options);
3485
-
3777
+
3486
3778
  }
3487
3779
 
3488
3780
  /**
@@ -3522,7 +3814,7 @@ export class EmbeddedSpreadsheet {
3522
3814
  // the active selection must be on the active sheet? (...)
3523
3815
 
3524
3816
  const sheet_id = ref.area.start.sheet_id || this.grid.active_sheet.id;
3525
- const sheet_name = this.ResolveSheetName(sheet_id, true);
3817
+ const sheet_name = this.calculator.ResolveSheetName(sheet_id, true);
3526
3818
 
3527
3819
  return sheet_name ? sheet_name + '!' + range : range;
3528
3820
 
@@ -3940,6 +4232,47 @@ export class EmbeddedSpreadsheet {
3940
4232
 
3941
4233
  // --- internal (protected) methods ------------------------------------------
3942
4234
 
4235
+ /**
4236
+ *
4237
+ */
4238
+ protected ApplyConditionalFormats(sheet: Sheet, call_update: boolean) {
4239
+
4240
+ // const sheet = this.grid.active_sheet;
4241
+ const areas: IArea[] = [];
4242
+
4243
+ if (sheet.conditional_formats.length || sheet.flush_conditional_formats) {
4244
+
4245
+ for (const entry of sheet.conditional_formats) {
4246
+ areas.push(entry.area);
4247
+
4248
+ // NOTE: we're (optionally) adding the gradient here, instead
4249
+ // of when it's created, because we might want to flush this
4250
+ // from time to time. specifically, because gradient might use
4251
+ // theme colors, we need to flush it when the theme is updated.
4252
+
4253
+ // so don't move it, even though it is tempting. or rewrite to
4254
+ // update on theme changes.
4255
+
4256
+ if (entry.type === 'gradient') {
4257
+ if (!entry.internal) {
4258
+ entry.internal = {};
4259
+ }
4260
+ if (!entry.internal.gradient) {
4261
+ entry.internal.gradient = new Gradient(entry.stops, this.grid.theme, entry.color_space);
4262
+ }
4263
+ }
4264
+ }
4265
+
4266
+ sheet.ApplyConditionalFormats();
4267
+
4268
+ }
4269
+
4270
+ if (call_update) {
4271
+ this.grid.Update(true, areas);
4272
+ }
4273
+
4274
+ }
4275
+
3943
4276
  protected ResolveTable(reference: RangeReference): Table|undefined {
3944
4277
 
3945
4278
  let table: Table|undefined = undefined;
@@ -4044,6 +4377,11 @@ export class EmbeddedSpreadsheet {
4044
4377
 
4045
4378
  this.InflateAnnotations();
4046
4379
 
4380
+ // and conditional formats
4381
+
4382
+ this.calculator.UpdateConditionals();
4383
+ this.ApplyConditionalFormats(this.grid.active_sheet, false);
4384
+
4047
4385
  }
4048
4386
  else {
4049
4387
  return reject('unknown error (missing data)');
@@ -4164,6 +4502,9 @@ export class EmbeddedSpreadsheet {
4164
4502
 
4165
4503
  this.UpdateAnnotations();
4166
4504
 
4505
+ this.calculator.UpdateConditionals();
4506
+ this.ApplyConditionalFormats(event.activate, true);
4507
+
4167
4508
  }
4168
4509
 
4169
4510
  protected HandleDrag(event: DragEvent): void {
@@ -4556,7 +4897,7 @@ export class EmbeddedSpreadsheet {
4556
4897
  protected UpdateAnnotations(): void {
4557
4898
  for (const annotation of this.grid.active_sheet.annotations) {
4558
4899
  if (annotation.temp.vertex) {
4559
- const vertex = annotation.temp.vertex as LeafVertex;
4900
+ const vertex = annotation.temp.vertex as StateLeafVertex;
4560
4901
  if (vertex.state_id !== annotation.temp.state) {
4561
4902
  annotation.temp.state = vertex.state_id;
4562
4903
 
@@ -5436,35 +5777,5 @@ export class EmbeddedSpreadsheet {
5436
5777
  }
5437
5778
  }
5438
5779
 
5439
- /**
5440
- * this is only used in one place, can we just inline?
5441
- * [A: seems like it might be useful, though]
5442
- *
5443
- * @param id
5444
- * @param quote
5445
- * @returns
5446
- */
5447
- protected ResolveSheetName(id: number, quote = false): string | undefined {
5448
- const sheet = this.model.sheets.Find(id);
5449
- if (sheet) {
5450
- if (quote && QuotedSheetNameRegex.test(sheet.name)) {
5451
- return `'${sheet.name}'`;
5452
- }
5453
- return sheet.name;
5454
- }
5455
-
5456
- /*
5457
- for (const sheet of this.grid.model.sheets) {
5458
- if (sheet.id === id) {
5459
- if (quote && QuotedSheetNameRegex.test(sheet.name)) {
5460
- return `'${sheet.name}'`;
5461
- }
5462
- return sheet.name;
5463
- }
5464
- }
5465
- */
5466
-
5467
- return undefined;
5468
- }
5469
5780
 
5470
5781
  }
@@ -40,19 +40,6 @@ const ExportSheets = async (data: any) => {
40
40
  ctx.postMessage({ status: 'complete', blob: corrected });
41
41
  }
42
42
 
43
- /*
44
- if (data.sheet) {
45
- await exporter.Init();
46
- await exporter.ExportSheets(data.sheet);
47
- const blob = await exporter.AsBlob(1);
48
-
49
- // correct the mime type for firefox
50
- const corrected = (blob as Blob).slice(0, (blob as Blob).size,
51
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
52
-
53
- ctx.postMessage({ status: 'complete', blob: corrected });
54
- }
55
- */
56
43
  };
57
44
 
58
45
  const ImportSheet = async (data: any) => {