@trebco/treb 28.10.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.
@@ -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
 
@@ -2197,6 +2200,114 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2197
2200
 
2198
2201
  }
2199
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
+
2200
2311
  /**
2201
2312
  * Insert an annotation node. Usually this means inserting a chart. Regarding
2202
2313
  * the argument separator, see the Evaluate function.
@@ -2226,11 +2337,21 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2226
2337
  target = Rectangle.IsRectangle(rect) ? rect : this.model.ResolveArea(rect, this.grid.active_sheet);
2227
2338
  }
2228
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
+
2229
2345
  if (argument_separator && argument_separator !== this.parser.argument_separator || r1c1) {
2346
+
2347
+ this.parser.Save();
2348
+
2349
+ /*
2230
2350
  const current = {
2231
2351
  argument_separator: this.parser.argument_separator,
2232
2352
  decimal_mark: this.parser.decimal_mark,
2233
2353
  };
2354
+ */
2234
2355
 
2235
2356
  if (argument_separator === ',') {
2236
2357
  this.parser.argument_separator = ArgumentSeparatorType.Comma;
@@ -2241,17 +2362,21 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2241
2362
  this.parser.decimal_mark = DecimalMarkType.Comma;
2242
2363
  }
2243
2364
 
2244
- const r1c1_state = this.parser.flags.r1c1;
2245
- if (r1c1) {
2246
- this.parser.flags.r1c1 = r1c1;
2247
- }
2365
+ // const r1c1_state = this.parser.flags.r1c1;
2366
+ // if (r1c1)
2367
+ // {
2368
+ this.parser.flags.r1c1 = !!r1c1;
2369
+ // }
2248
2370
 
2249
2371
  const result = this.parser.Parse(formula);
2250
2372
 
2373
+ /*
2251
2374
  // reset
2252
2375
  this.parser.argument_separator = current.argument_separator;
2253
2376
  this.parser.decimal_mark = current.decimal_mark;
2254
2377
  this.parser.flags.r1c1 = r1c1_state;
2378
+ */
2379
+ this.parser.Restore();
2255
2380
 
2256
2381
  if (result.expression) {
2257
2382
  formula = '=' + this.parser.Render(result.expression, { missing: '' });
@@ -4856,6 +4981,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
4856
4981
  * (just sparklines atm) and update if necessary.
4857
4982
  */
4858
4983
  protected UpdateAnnotations(): void {
4984
+
4859
4985
  for (const annotation of this.grid.active_sheet.annotations) {
4860
4986
  if (annotation.temp.vertex) {
4861
4987
  const vertex = annotation.temp.vertex as StateLeafVertex;
@@ -4893,6 +5019,27 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
4893
5019
  }
4894
5020
 
4895
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
+ }
4896
5043
  }
4897
5044
 
4898
5045
  /*
@@ -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';
@@ -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
 
@@ -2270,6 +2270,16 @@ export class GridBase {
2270
2270
  }
2271
2271
  }
2272
2272
 
2273
+ for (const element of this.model.connected_elements.values()) {
2274
+ if (element.formula) {
2275
+ const updated = this.PatchExpressionSheetName(element.formula, old_name, name);
2276
+ if (updated) {
2277
+ element.formula = updated;
2278
+ changes++;
2279
+ }
2280
+ }
2281
+ }
2282
+
2273
2283
  return changes;
2274
2284
 
2275
2285
  }
@@ -2850,6 +2860,18 @@ export class GridBase {
2850
2860
  row_count: command.count
2851
2861
  });
2852
2862
 
2863
+ // connected elements
2864
+ for (const external of this.model.connected_elements.values()) {
2865
+ if (external.formula) {
2866
+ const modified = this.PatchFormulasInternal(external.formula,
2867
+ command.before_row, command.count, 0, 0,
2868
+ target_sheet.name.toLowerCase(), false);
2869
+ if (modified) {
2870
+ external.formula = modified;
2871
+ }
2872
+ }
2873
+ }
2874
+
2853
2875
  // see InsertColumnsInternal re: tables. rows are less complicated,
2854
2876
  // except that if you delete the header row we want to remove the
2855
2877
  // table entirely.
@@ -3172,6 +3194,18 @@ export class GridBase {
3172
3194
  before_row: 0,
3173
3195
  row_count: 0 });
3174
3196
 
3197
+ // connected elements
3198
+ for (const element of this.model.connected_elements.values()) {
3199
+ if (element.formula) {
3200
+ const modified = this.PatchFormulasInternal(element.formula,
3201
+ 0, 0, command.before_column, command.count,
3202
+ target_sheet.name.toLowerCase(), false);
3203
+ if (modified) {
3204
+ element.formula = modified;
3205
+ }
3206
+ }
3207
+ }
3208
+
3175
3209
  // patch tables. we removed this from the sheet routine entirely,
3176
3210
  // we need to rebuild any affected tables now.
3177
3211
 
@@ -395,3 +395,9 @@ export interface RenderOptions {
395
395
  long_structured_references: boolean;
396
396
  table_name: string;
397
397
  }
398
+
399
+ export interface PersistedParserConfig {
400
+ flags: Partial<ParserFlags>;
401
+ argument_separator: ArgumentSeparatorType;
402
+ decimal_mark: DecimalMarkType;
403
+ }
@@ -33,7 +33,8 @@ import type {
33
33
  UnitLiteralNumber,
34
34
  ParserFlags,
35
35
  UnitStructuredReference,
36
- RenderOptions} from './parser-types';
36
+ RenderOptions,
37
+ PersistedParserConfig} from './parser-types';
37
38
  import {
38
39
  ArgumentSeparatorType,
39
40
  DecimalMarkType
@@ -248,6 +249,52 @@ export class Parser {
248
249
  */
249
250
  protected full_reference_list: Array<UnitAddress | UnitRange | UnitIdentifier | UnitStructuredReference> = [];
250
251
 
252
+ protected parser_state: string[] = [];
253
+
254
+ /**
255
+ * save local configuration to a buffer, so it can be restored. we're doing
256
+ * this because in a lot of places we're caching parser flagss, changing
257
+ * them, and then restoring them. that's become repetitive, fragile to
258
+ * changes or new flags, and annoying.
259
+ *
260
+ * config is managed in a list with push/pop semantics. we store it as
261
+ * JSON so there's no possibility we'll accidentally mutate.
262
+ *
263
+ * FIXME: while we're at it why not migrate the separators -> flags, so
264
+ * there's a single location for this kind of state? (...TODO)
265
+ *
266
+ */
267
+ public Save() {
268
+ const config: PersistedParserConfig = {
269
+ flags: this.flags,
270
+ argument_separator: this.argument_separator,
271
+ decimal_mark: this.decimal_mark,
272
+ }
273
+ this.parser_state.push(JSON.stringify(config));
274
+ }
275
+
276
+ /**
277
+ * restore persisted config
278
+ * @see Save
279
+ */
280
+ public Restore() {
281
+ const json = this.parser_state.shift();
282
+ if (json) {
283
+ try {
284
+ const config = JSON.parse(json) as PersistedParserConfig;
285
+ this.flags = config.flags;
286
+ this.argument_separator = config.argument_separator;
287
+ this.decimal_mark = config.decimal_mark;
288
+ }
289
+ catch (err) {
290
+ console.error(err);
291
+ }
292
+ }
293
+ else {
294
+ console.warn("No parser state to restore");
295
+ }
296
+ }
297
+
251
298
  /**
252
299
  * recursive tree walk.
253
300
  *