@trebco/treb 28.7.0 → 28.10.5

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 (37) hide show
  1. package/dist/treb-spreadsheet-light.mjs +14 -14
  2. package/dist/treb-spreadsheet.mjs +12 -12
  3. package/dist/treb.d.ts +39 -6
  4. package/notes/connected-elements.md +37 -0
  5. package/package.json +1 -1
  6. package/treb-base-types/src/gradient.ts +1 -1
  7. package/treb-base-types/src/localization.ts +6 -0
  8. package/treb-calculator/src/calculator.ts +72 -30
  9. package/treb-calculator/src/dag/calculation_leaf_vertex.ts +7 -0
  10. package/treb-calculator/src/dag/graph.ts +8 -0
  11. package/treb-calculator/src/functions/base-functions.ts +30 -1
  12. package/treb-calculator/src/index.ts +1 -1
  13. package/treb-charts/src/chart-functions.ts +14 -0
  14. package/treb-charts/src/chart-types.ts +25 -1
  15. package/treb-charts/src/chart-utils.ts +195 -9
  16. package/treb-charts/src/chart.ts +4 -0
  17. package/treb-charts/src/default-chart-renderer.ts +17 -1
  18. package/treb-charts/src/renderer.ts +182 -9
  19. package/treb-charts/style/charts.scss +39 -0
  20. package/treb-embed/markup/toolbar.html +35 -34
  21. package/treb-embed/src/custom-element/treb-global.ts +10 -2
  22. package/treb-embed/src/embedded-spreadsheet.ts +209 -106
  23. package/treb-embed/src/options.ts +7 -0
  24. package/treb-embed/style/layout.scss +4 -0
  25. package/treb-embed/style/toolbar.scss +37 -0
  26. package/treb-grid/src/index.ts +1 -1
  27. package/treb-grid/src/types/conditional_format.ts +1 -1
  28. package/treb-grid/src/types/data_model.ts +32 -0
  29. package/treb-grid/src/types/grid.ts +11 -1
  30. package/treb-grid/src/types/grid_base.ts +161 -5
  31. package/treb-grid/src/types/grid_command.ts +32 -0
  32. package/treb-grid/src/types/grid_events.ts +7 -0
  33. package/treb-grid/src/types/grid_options.ts +8 -0
  34. package/treb-grid/src/types/sheet.ts +0 -56
  35. package/treb-grid/src/types/update_flags.ts +1 -0
  36. package/treb-parser/src/parser-types.ts +6 -0
  37. package/treb-parser/src/parser.ts +48 -1
@@ -10,7 +10,7 @@ import type { EmbeddedSpreadsheet } from '../embedded-spreadsheet';
10
10
  export class TREBGlobal {
11
11
 
12
12
  /**
13
- * build version
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
- * create a spreadsheet instance
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
 
@@ -87,6 +87,10 @@ import type { BorderToolbarMessage, ToolbarMessage } from './toolbar-message';
87
87
  import { Chart, ChartFunctions } from 'treb-charts';
88
88
  import type { SetRangeOptions } from 'treb-grid';
89
89
 
90
+ import type { StateLeafVertex } from 'treb-calculator';
91
+ import type { ConnectedElementType } from 'treb-grid';
92
+
93
+
90
94
  // --- worker ------------------------------------------------------------------
91
95
 
92
96
  /**
@@ -95,7 +99,6 @@ import type { SetRangeOptions } from 'treb-grid';
95
99
  * the script so we can run it as a worker.
96
100
  */
97
101
  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';
99
102
 
100
103
  // --- types -------------------------------------------------------------------
101
104
 
@@ -486,13 +489,19 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
486
489
  this.DocumentChange();
487
490
  }
488
491
 
489
- /** opaque user data (metadata) */
492
+ /**
493
+ * opaque user data (metadata). `USER_DATA_TYPE` is a template
494
+ * parameter you can set when creating the spreadsheet.
495
+ */
490
496
  public get user_data(): USER_DATA_TYPE|undefined {
491
497
  return this.grid.model.user_data;
492
498
  }
493
499
 
494
- /** opaque user data (metadata) */
495
- public set user_data(data: USER_DATA_TYPE) {
500
+ /**
501
+ * opaque user data (metadata). `USER_DATA_TYPE` is a template
502
+ * parameter you can set when creating the spreadsheet.
503
+ */
504
+ public set user_data(data: USER_DATA_TYPE|undefined) {
496
505
  this.grid.model.user_data = data;
497
506
  this.DocumentChange();
498
507
  }
@@ -883,6 +892,8 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
883
892
 
884
893
  this.grid.grid_events.Subscribe((event) => {
885
894
 
895
+ // console.info({event});
896
+
886
897
  switch (event.type) {
887
898
 
888
899
  case 'error':
@@ -986,7 +997,14 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
986
997
 
987
998
  case 'structure':
988
999
  {
1000
+ // console.info("S event", event);
1001
+
989
1002
  const cached_selection = this.last_selection;
1003
+ if (event.conditional_format) {
1004
+ this.calculator.UpdateConditionals();
1005
+ this.ApplyConditionalFormats(this.grid.active_sheet, false);
1006
+ }
1007
+
990
1008
  if (event.rebuild_required) {
991
1009
  this.calculator.Reset();
992
1010
 
@@ -1412,12 +1430,6 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
1412
1430
  ...options,
1413
1431
  };
1414
1432
 
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
1433
  this.AddConditionalFormat(format);
1422
1434
  return format;
1423
1435
 
@@ -1445,18 +1457,6 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
1445
1457
  ...options,
1446
1458
  };
1447
1459
 
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
1460
  this.AddConditionalFormat(format);
1461
1461
 
1462
1462
  return format;
@@ -1470,20 +1470,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
1470
1470
  */
1471
1471
  public AddConditionalFormat(format: ConditionalFormat) {
1472
1472
 
1473
- const sheet = this.model.sheets.Find(format.area.start.sheet_id||0);
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();
1473
+ this.grid.AddConditionalFormat(format);
1487
1474
 
1488
1475
  // fluent
1489
1476
  return format;
@@ -1496,40 +1483,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
1496
1483
  * @internal
1497
1484
  */
1498
1485
  public RemoveConditionalFormat(format: ConditionalFormat) {
1499
- const area = format.area;
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
-
1486
+ this.grid.RemoveConditionalFormat({ format });
1533
1487
  }
1534
1488
 
1535
1489
  /**
@@ -1548,37 +1502,9 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
1548
1502
  }
1549
1503
  range = ref.area;
1550
1504
  }
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
1505
 
1506
+ const area = this.model.ResolveArea(range, this.grid.active_sheet);
1507
+ this.grid.RemoveConditionalFormat({ area });
1582
1508
 
1583
1509
  }
1584
1510
 
@@ -2274,6 +2200,114 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2274
2200
 
2275
2201
  }
2276
2202
 
2203
+ public RemoveConnectedChart(id: number) {
2204
+ const element = this.model.RemoveConnectedElement(id);
2205
+ if (element) {
2206
+ const removed = this.calculator.RemoveConnectedELement(element);
2207
+ if (removed) {
2208
+ // ... actually don't need to update here
2209
+ }
2210
+ }
2211
+ }
2212
+
2213
+ public UpdateConnectedChart(id: number, formula: string) {
2214
+ const element = this.model.connected_elements.get(id);
2215
+ if (element) {
2216
+ element.formula = formula;
2217
+ const internal = (element.internal) as { vertex: StateLeafVertex, state: any };
2218
+
2219
+ if (internal?.state) {
2220
+ internal.state = undefined;
2221
+ }
2222
+ this.calculator.UpdateConnectedElements(this.grid.active_sheet, element);
2223
+
2224
+ if (element.update) {
2225
+ element.update.call(0, element);
2226
+ }
2227
+
2228
+ }
2229
+ }
2230
+
2231
+ /**
2232
+ * @internal
2233
+ *
2234
+ * @returns an id that can be used to manage the reference
2235
+ */
2236
+ public CreateConnectedChart(formula: string, target: HTMLElement, options: EvaluateOptions): number {
2237
+
2238
+ // FIXME: merge w/ insert annotation?
2239
+
2240
+ let r1c1 = options?.r1c1 || false;
2241
+ let argument_separator = options?.argument_separator || this.parser.argument_separator; // default to current
2242
+
2243
+ this.parser.Save();
2244
+ this.parser.flags.r1c1 = r1c1;
2245
+
2246
+ if (argument_separator === ',') {
2247
+ this.parser.argument_separator = ArgumentSeparatorType.Comma;
2248
+ this.parser.decimal_mark = DecimalMarkType.Period;
2249
+ }
2250
+ else {
2251
+ this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
2252
+ this.parser.decimal_mark = DecimalMarkType.Comma;
2253
+ }
2254
+
2255
+ const result = this.parser.Parse(formula);
2256
+
2257
+ this.parser.Restore();
2258
+
2259
+ if (result.expression) {
2260
+ formula = '=' + this.parser.Render(result.expression, { missing: '' });
2261
+ }
2262
+ else {
2263
+ console.warn("invalid formula", result.error);
2264
+ }
2265
+
2266
+ const chart = this.CreateChart();
2267
+ chart.Initialize(target);
2268
+
2269
+ const id = this.model.AddConnectedElement({
2270
+ formula,
2271
+
2272
+ // this is circular, but I want to leave `this` bound to the sheet
2273
+ // instance in case we need it -- so what's a better approach? pass
2274
+ // in the formula explicitly, and update if we need to make changes?
2275
+
2276
+ update: (instance: ConnectedElementType) => {
2277
+
2278
+ const parse_result = this.parser.Parse(instance.formula);
2279
+
2280
+ if (parse_result &&
2281
+ parse_result.expression &&
2282
+ parse_result.expression.type === 'call') {
2283
+
2284
+ // FIXME: make a method for doing this
2285
+
2286
+ this.parser.Walk(parse_result.expression, (unit) => {
2287
+ if (unit.type === 'address' || unit.type === 'range') {
2288
+ this.model.ResolveSheetID(unit, undefined, this.grid.active_sheet);
2289
+ }
2290
+ return true;
2291
+ });
2292
+
2293
+ const expr_name = parse_result.expression.name.toLowerCase();
2294
+ const result = this.calculator.CalculateExpression(parse_result.expression);
2295
+ chart.Exec(expr_name, result as ExtendedUnion); // FIXME: type?
2296
+ }
2297
+
2298
+ chart.Update();
2299
+
2300
+ },
2301
+
2302
+ });
2303
+
2304
+ this.calculator.UpdateConnectedElements(this.grid.active_sheet);
2305
+ this.UpdateConnectedElements();
2306
+
2307
+ return id;
2308
+
2309
+ }
2310
+
2277
2311
  /**
2278
2312
  * Insert an annotation node. Usually this means inserting a chart. Regarding
2279
2313
  * the argument separator, see the Evaluate function.
@@ -2303,11 +2337,21 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2303
2337
  target = Rectangle.IsRectangle(rect) ? rect : this.model.ResolveArea(rect, this.grid.active_sheet);
2304
2338
  }
2305
2339
 
2340
+ // FIXME: with the new parser save/restore semantics we should
2341
+ // just always do this. also I don't think the r1c1 logic works
2342
+ // properly here... unless we're assuming that the default state
2343
+ // is always off
2344
+
2306
2345
  if (argument_separator && argument_separator !== this.parser.argument_separator || r1c1) {
2346
+
2347
+ this.parser.Save();
2348
+
2349
+ /*
2307
2350
  const current = {
2308
2351
  argument_separator: this.parser.argument_separator,
2309
2352
  decimal_mark: this.parser.decimal_mark,
2310
2353
  };
2354
+ */
2311
2355
 
2312
2356
  if (argument_separator === ',') {
2313
2357
  this.parser.argument_separator = ArgumentSeparatorType.Comma;
@@ -2318,17 +2362,21 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2318
2362
  this.parser.decimal_mark = DecimalMarkType.Comma;
2319
2363
  }
2320
2364
 
2321
- const r1c1_state = this.parser.flags.r1c1;
2322
- if (r1c1) {
2323
- this.parser.flags.r1c1 = r1c1;
2324
- }
2365
+ // const r1c1_state = this.parser.flags.r1c1;
2366
+ // if (r1c1)
2367
+ // {
2368
+ this.parser.flags.r1c1 = !!r1c1;
2369
+ // }
2325
2370
 
2326
2371
  const result = this.parser.Parse(formula);
2327
2372
 
2373
+ /*
2328
2374
  // reset
2329
2375
  this.parser.argument_separator = current.argument_separator;
2330
2376
  this.parser.decimal_mark = current.decimal_mark;
2331
2377
  this.parser.flags.r1c1 = r1c1_state;
2378
+ */
2379
+ this.parser.Restore();
2332
2380
 
2333
2381
  if (result.expression) {
2334
2382
  formula = '=' + this.parser.Render(result.expression, { missing: '' });
@@ -3977,6 +4025,23 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
3977
4025
  return NumberFormatCache.Get(format).Format(value);
3978
4026
  }
3979
4027
 
4028
+ /**
4029
+ * convert a javascript date (or timestamp) to a spreadsheet date
4030
+ */
4031
+ public SpreadsheetDate(javascript_date: number|Date) {
4032
+ if (javascript_date instanceof Date) {
4033
+ javascript_date = javascript_date.getTime();
4034
+ }
4035
+ return UnlotusDate(javascript_date, true);
4036
+ }
4037
+
4038
+ /**
4039
+ * convert a spreadsheet date to a javascript date
4040
+ */
4041
+ public JavascriptDate(spreadsheet_date: number) {
4042
+ return LotusDate(spreadsheet_date).getTime();
4043
+ }
4044
+
3980
4045
  /**
3981
4046
  * Apply borders to range.
3982
4047
  *
@@ -4611,7 +4676,13 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
4611
4676
 
4612
4677
  this.UpdateAnnotations();
4613
4678
 
4679
+ // we don't really need to call UpdateConditionals here unless
4680
+ // something has changed in a previously inactive sheet -- right?
4681
+
4614
4682
  this.calculator.UpdateConditionals();
4683
+
4684
+ // we do need to call apply (I think)
4685
+
4615
4686
  this.ApplyConditionalFormats(event.activate, true);
4616
4687
 
4617
4688
  }
@@ -4910,6 +4981,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
4910
4981
  * (just sparklines atm) and update if necessary.
4911
4982
  */
4912
4983
  protected UpdateAnnotations(): void {
4984
+
4913
4985
  for (const annotation of this.grid.active_sheet.annotations) {
4914
4986
  if (annotation.temp.vertex) {
4915
4987
  const vertex = annotation.temp.vertex as StateLeafVertex;
@@ -4947,6 +5019,27 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
4947
5019
  }
4948
5020
 
4949
5021
  }
5022
+
5023
+ this.UpdateConnectedElements();
5024
+
5025
+ }
5026
+
5027
+ protected UpdateConnectedElements() {
5028
+ for (const element of this.model.connected_elements.values()) {
5029
+ const internal = (element.internal) as { vertex: StateLeafVertex, state: any };
5030
+ if (internal?.vertex && internal.vertex.state_id !== internal.state ) {
5031
+ internal.state = internal.vertex.state_id;
5032
+ const fn = element.update;
5033
+ if (fn) {
5034
+
5035
+ // FIXME: what if there are multiple calls pending? some
5036
+ // kind of a dispatch system might be useful
5037
+
5038
+ Promise.resolve().then(() => fn.call(0, element));
5039
+
5040
+ }
5041
+ }
5042
+ }
4950
5043
  }
4951
5044
 
4952
5045
  /*
@@ -5798,15 +5891,25 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
5798
5891
 
5799
5892
  /**
5800
5893
  * handle key down to intercept ctrl+z (undo)
5801
- *
5894
+ * UPDATE: we're also handling F9 for recalc (optionally)
5895
+ *
5802
5896
  * FIXME: redo (ctrl+y or ctrl+shift+z)
5803
5897
  */
5804
5898
  protected HandleKeyDown(event: KeyboardEvent): void {
5899
+
5900
+ // can we drop the event.code stuff in 2024? (YES)
5901
+
5805
5902
  if (event.ctrlKey && (event.code === 'KeyZ' || event.key === 'z')) {
5806
5903
  event.stopPropagation();
5807
5904
  event.preventDefault();
5808
5905
  this.Undo();
5809
5906
  }
5907
+ else if (event.key === 'F9' && this.options.recalculate_on_f9) {
5908
+ event.stopPropagation();
5909
+ event.preventDefault();
5910
+ this.Recalculate();
5911
+ }
5912
+
5810
5913
  }
5811
5914
 
5812
5915
 
@@ -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
  }
@@ -22,7 +22,7 @@
22
22
  export { Grid } from './types/grid';
23
23
  export { GridBase } from './types/grid_base';
24
24
  export { Sheet } from './types/sheet';
25
- export { DataModel, type MacroFunction } from './types/data_model';
25
+ export { DataModel, type MacroFunction, type ConnectedElementType } from './types/data_model';
26
26
  export type { SerializedNamedExpression, SerializedModel } from './types/data_model';
27
27
  export * from './types/grid_events';
28
28
  export type { SerializedSheet, FreezePane } from './types/sheet_types';
@@ -33,7 +33,7 @@ export interface ConditionalFormatGradientOptions {
33
33
  /** property defaults to fill */
34
34
  property?: 'fill'|'text';
35
35
 
36
- /** defaults to HSL */
36
+ /** defaults to RGB */
37
37
  color_space?: 'HSL'|'RGB';
38
38
 
39
39
  /** gradient stops, required */
@@ -26,6 +26,12 @@ import { NamedRangeCollection } from './named_range';
26
26
  import { type ExpressionUnit, type UnitAddress, type UnitStructuredReference, type UnitRange, Parser, QuotedSheetNameRegex } from 'treb-parser';
27
27
  import { Area, IsCellAddress, Style } from 'treb-base-types';
28
28
 
29
+ export interface ConnectedElementType {
30
+ formula: string;
31
+ update?: (instance: ConnectedElementType) => void;
32
+ internal?: unknown; // opaque type to prevent circular dependencies
33
+ }
34
+
29
35
  export interface SerializedMacroFunction {
30
36
  name: string;
31
37
  function_def: string;
@@ -444,6 +450,32 @@ export class DataModel {
444
450
  return address; // already range or address
445
451
 
446
452
  }
453
+
454
+ public AddConnectedElement(connected_element: ConnectedElementType): number {
455
+ const id = this.connected_element_id++;
456
+ this.connected_elements.set(id, connected_element);
457
+ return id;
458
+ }
459
+
460
+ public RemoveConnectedElement(id: number) {
461
+ const element = this.connected_elements.get(id);
462
+ this.connected_elements.delete(id);
463
+ return element;
464
+ }
465
+
466
+ /**
467
+ * identifier for connected elements, used to manage. these need to be
468
+ * unique in the lifetime of a model instance, but no more than that.
469
+ */
470
+ protected connected_element_id = 100;
471
+
472
+ /**
473
+ * these are intentionally NOT serialized. they're ephemeral, created
474
+ * at runtime and not persistent.
475
+ *
476
+ * @internal
477
+ */
478
+ public connected_elements: Map<number, ConnectedElementType> = new Map();
447
479
 
448
480
  }
449
481
 
@@ -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
- // console.info('ek', event.key);
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') {