@trebco/treb 27.5.3 → 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 (48) hide show
  1. package/dist/treb-spreadsheet.mjs +14 -14
  2. package/dist/treb.d.ts +37 -23
  3. package/notes/conditional-fomratring.md +29 -0
  4. package/package.json +3 -3
  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 +205 -132
  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 +382 -71
  23. package/treb-embed/style/formula-bar.scss +2 -0
  24. package/treb-embed/style/theme-defaults.scss +46 -15
  25. package/treb-export/src/export-worker/export-worker.ts +0 -13
  26. package/treb-export/src/export2.ts +187 -2
  27. package/treb-export/src/import2.ts +169 -4
  28. package/treb-export/src/workbook-style2.ts +56 -8
  29. package/treb-export/src/workbook2.ts +10 -1
  30. package/treb-grid/src/editors/editor.ts +1276 -0
  31. package/treb-grid/src/editors/external_editor.ts +113 -0
  32. package/treb-grid/src/editors/formula_bar.ts +450 -474
  33. package/treb-grid/src/editors/overlay_editor.ts +437 -512
  34. package/treb-grid/src/index.ts +2 -1
  35. package/treb-grid/src/layout/base_layout.ts +24 -16
  36. package/treb-grid/src/render/tile_renderer.ts +2 -1
  37. package/treb-grid/src/types/conditional_format.ts +168 -0
  38. package/treb-grid/src/types/data_model.ts +130 -3
  39. package/treb-grid/src/types/external_editor_config.ts +47 -0
  40. package/treb-grid/src/types/grid.ts +96 -45
  41. package/treb-grid/src/types/grid_base.ts +187 -35
  42. package/treb-grid/src/types/scale-control.ts +1 -1
  43. package/treb-grid/src/types/sheet.ts +330 -26
  44. package/treb-grid/src/types/sheet_types.ts +4 -0
  45. package/treb-grid/src/util/dom_utilities.ts +58 -25
  46. package/treb-grid/src/editors/formula_editor_base.ts +0 -912
  47. package/treb-grid/src/types/external_editor.ts +0 -27
  48. /package/{README-shadow-DOM.md → notes/shadow-DOM.md} +0 -0
@@ -39,4 +39,5 @@ export type { FunctionDescriptor, ArgumentDescriptor } from './editors/autocompl
39
39
  export { UA } from './util/ua';
40
40
  export type { SetRangeOptions } from './types/set_range_options';
41
41
  export type { AnnotationData, AnnotationType } from './types/annotation';
42
- export type { ExternalEditorType, DependencyList, ExternalEditorCallback } from './types/external_editor';
42
+ export type { ExternalEditorConfig, DependencyList, ExternalEditorCallback } from './types/external_editor_config';
43
+ export * from './types/conditional_format';
@@ -23,7 +23,7 @@ import { DOMUtilities } from '../util/dom_utilities';
23
23
  import type { DataModel, ViewModel } from '../types/data_model';
24
24
 
25
25
  import type { Tile } from '../types/tile';
26
- import type { Theme, Point, Extent, Size, Position, ICellAddress, Table } from 'treb-base-types';
26
+ import type { Theme, Point, Extent, Size, Position, ICellAddress, Table, IArea } from 'treb-base-types';
27
27
  import { Style, Area, Rectangle, ThemeColor } from 'treb-base-types';
28
28
 
29
29
  import { MouseDrag } from '../types/drag_mask';
@@ -357,7 +357,7 @@ export abstract class BaseLayout {
357
357
  this.note_node = DOMUtilities.CreateDiv('treb-note');
358
358
  this.title_node = DOMUtilities.CreateDiv('treb-hover-title');
359
359
 
360
- this.sort_button = DOMUtilities.Create<HTMLButtonElement>(
360
+ this.sort_button = DOMUtilities.Create(
361
361
  'button',
362
362
  'treb-sort-button', undefined, undefined, { title: 'Sort table', tabindex: '-1'});
363
363
 
@@ -2160,25 +2160,33 @@ export abstract class BaseLayout {
2160
2160
  }
2161
2161
  }
2162
2162
 
2163
- public DirtyArea(area: Area): void {
2163
+ public DirtyArea(areas: IArea|IArea[]): void {
2164
2164
 
2165
2165
  if (!this.initialized) return;
2166
2166
 
2167
- const start = { row: 0, column: 0 };
2168
- const end = { row: this.grid_tiles[0].length - 1, column: this.grid_tiles.length - 1 };
2169
-
2170
- if (area.start.column !== Infinity) {
2171
- start.column = end.column = this.TileIndexForColumn(area.start.column);
2172
- if (area.end.column !== area.start.column) end.column = this.TileIndexForColumn(area.end.column);
2173
- }
2174
- if (area.start.row !== Infinity) {
2175
- start.row = end.row = this.TileIndexForRow(area.start.row);
2176
- if (area.end.row !== area.start.row) end.row = this.TileIndexForRow(area.end.row);
2167
+ if (!Array.isArray(areas)) {
2168
+ areas = [areas];
2177
2169
  }
2178
- for (let column = start.column; column <= end.column; column++) {
2179
- for (let row = start.row; row <= end.row; row++) {
2180
- this.grid_tiles[column][row].dirty = true;
2170
+
2171
+ for (const area of areas) {
2172
+
2173
+ const start = { row: 0, column: 0 };
2174
+ const end = { row: this.grid_tiles[0].length - 1, column: this.grid_tiles.length - 1 };
2175
+
2176
+ if (area.start.column !== Infinity) {
2177
+ start.column = end.column = this.TileIndexForColumn(area.start.column);
2178
+ if (area.end.column !== area.start.column) end.column = this.TileIndexForColumn(area.end.column);
2181
2179
  }
2180
+ if (area.start.row !== Infinity) {
2181
+ start.row = end.row = this.TileIndexForRow(area.start.row);
2182
+ if (area.end.row !== area.start.row) end.row = this.TileIndexForRow(area.end.row);
2183
+ }
2184
+ for (let column = start.column; column <= end.column; column++) {
2185
+ for (let row = start.row; row <= end.row; row++) {
2186
+ this.grid_tiles[column][row].dirty = true;
2187
+ }
2188
+ }
2189
+
2182
2190
  }
2183
2191
 
2184
2192
  }
@@ -239,7 +239,7 @@ export class TileRenderer {
239
239
  */
240
240
  public OverflowDirty(full_tile = false): void {
241
241
 
242
- const mutated = [];
242
+ const mutated: OverflowRecord[] = [];
243
243
 
244
244
  for (const overflow of this.overflow_areas) {
245
245
  const row = overflow.area.start.row;
@@ -1341,6 +1341,7 @@ export class TileRenderer {
1341
1341
  else {
1342
1342
 
1343
1343
  const fill = ThemeColor2(this.theme, style.fill);
1344
+
1344
1345
  if (fill) {
1345
1346
  context.fillStyle = fill;
1346
1347
  context.fillRect(0, 0, width - 1, height - 1);
@@ -0,0 +1,168 @@
1
+
2
+ import type { CellStyle, EvaluateOptions, IArea, Color, Gradient, GradientStop, UnionValue } from 'treb-base-types';
3
+
4
+ interface VertexPlaceholder {
5
+ result: UnionValue;
6
+ updated: boolean;
7
+ }
8
+
9
+ export interface CondifionalFormatExpressionOptions {
10
+ style: CellStyle;
11
+ expression: string;
12
+ options?: EvaluateOptions;
13
+ }
14
+
15
+ /**
16
+ * conditional format predicated on an expression. if the expression
17
+ * evaluates to true, we apply the style. otherwise no.
18
+ */
19
+ export interface ConditionalFormatExpression extends CondifionalFormatExpressionOptions {
20
+
21
+ type: 'expression';
22
+ area: IArea;
23
+
24
+ /** @internal */
25
+ internal?: {
26
+ vertex?: VertexPlaceholder;
27
+ };
28
+
29
+ }
30
+
31
+ export interface ConditionalFormatGradientOptions {
32
+
33
+ /** property defaults to fill */
34
+ property?: 'fill'|'text';
35
+
36
+ /** defaults to HSL */
37
+ color_space?: 'HSL'|'RGB';
38
+
39
+ /** gradient stops, required */
40
+ stops: Array<{ value: number, color: Color }>;
41
+
42
+ /** min and max are optional. if not provided, we use the min/max of the range of data. */
43
+ min?: number;
44
+
45
+ /** min and max are optional. if not provided, we use the min/max of the range of data. */
46
+ max?: number;
47
+
48
+ }
49
+
50
+
51
+ export const StandardGradientsList = {
52
+ 'red-green': {
53
+ color_space: 'RGB',
54
+ stops: [
55
+ { value: 0, color: { theme: 5, tint: .5 }},
56
+ { value: 1, color: { theme: 9, tint: .5 }},
57
+ ] as GradientStop[],
58
+ },
59
+ 'red-yellow-green': {
60
+ color_space: 'RGB',
61
+ stops: [
62
+ { value: 0, color: { theme: 5, tint: .5 }},
63
+ { value: 0.5, color: { theme: 7, tint: .5 }},
64
+ { value: 1, color: { theme: 9, tint: .5 }},
65
+ ] as GradientStop[],
66
+ },
67
+ 'green-red': {
68
+ color_space: 'RGB',
69
+ stops: [
70
+ { value: 0, color: { theme: 9, tint: .5 }},
71
+ { value: 1, color: { theme: 5, tint: .5 }},
72
+ ] as GradientStop[],
73
+ },
74
+ 'green-yellow-red': {
75
+ color_space: 'RGB',
76
+ stops: [
77
+ { value: 0, color: { theme: 9, tint: .5 }},
78
+ { value: 0.5, color: { theme: 7, tint: .5 }},
79
+ { value: 1, color: { theme: 5, tint: .5 }},
80
+ ] as GradientStop[],
81
+ },
82
+ } as const;
83
+ export type StandardGradient = keyof typeof StandardGradientsList;
84
+
85
+ export interface ConditionalFormatGradient extends ConditionalFormatGradientOptions {
86
+ type: 'gradient';
87
+ area: IArea;
88
+
89
+ /** @internal */
90
+ internal?: {
91
+ gradient?: Gradient;
92
+ vertex?: VertexPlaceholder;
93
+ };
94
+ }
95
+
96
+ export interface ConditionalFormatCellMatchOptions {
97
+ style: CellStyle;
98
+ expression: string;
99
+ options?: EvaluateOptions;
100
+ }
101
+
102
+ export interface ConditionalFormatCellMatch extends ConditionalFormatCellMatchOptions {
103
+ type: 'cell-match';
104
+ area: IArea;
105
+
106
+ /** @internal */
107
+ internal?: {
108
+ vertex?: VertexPlaceholder;
109
+ };
110
+ }
111
+
112
+ export interface ConditionalFormatCellMatchOptions {
113
+ style: CellStyle;
114
+ expression: string;
115
+ options?: EvaluateOptions;
116
+ }
117
+
118
+ export interface ConditionalFormatCellMatch extends ConditionalFormatCellMatchOptions {
119
+ type: 'cell-match';
120
+ area: IArea;
121
+
122
+ /** @internal */
123
+ internal?: {
124
+ vertex?: VertexPlaceholder;
125
+ };
126
+ }
127
+
128
+ export interface ConditionalFormatDuplicateValuesOptions {
129
+ style: CellStyle;
130
+
131
+ /** true to highlight unique cells, false to highlight duplicates. defaults to false. */
132
+ unique?: boolean;
133
+ }
134
+
135
+ export interface ConditionalFormatDuplicateValues extends ConditionalFormatDuplicateValuesOptions {
136
+
137
+ type: 'duplicate-values';
138
+ area: IArea;
139
+
140
+ /** @internal */
141
+ internal?: {
142
+ vertex?: VertexPlaceholder;
143
+ };
144
+
145
+ }
146
+
147
+ /**
148
+ * union, plus we're adding a state used to track application.
149
+ * that state is serialized if it's true.
150
+ * we also add an internal field that will be type-specific, and not serialized.
151
+ *
152
+ * ...everybody has a vertex now, we could standardize it
153
+ *
154
+ */
155
+ export type ConditionalFormat = { internal?: unknown } & (
156
+ ConditionalFormatDuplicateValues |
157
+ ConditionalFormatExpression |
158
+ ConditionalFormatCellMatch |
159
+ ConditionalFormatGradient
160
+ );
161
+
162
+ /**
163
+ * the list of formats, in reverse order of precedence. as a starting
164
+ * point we're using the naive approach, just applying everything in
165
+ * order. that may change.
166
+ */
167
+ export type ConditionalFormatList = ConditionalFormat[];
168
+
@@ -23,8 +23,8 @@ import type { Sheet } from './sheet';
23
23
  import type { IArea, ICellAddress, Table, CellStyle } from 'treb-base-types';
24
24
  import type { SerializedSheet } from './sheet_types';
25
25
  import { NamedRangeCollection } from './named_range';
26
- import type { ExpressionUnit, UnitAddress, UnitStructuredReference, UnitRange } from 'treb-parser';
27
- import { Style } from 'treb-base-types';
26
+ import { type ExpressionUnit, type UnitAddress, type UnitStructuredReference, type UnitRange, Parser, QuotedSheetNameRegex } from 'treb-parser';
27
+ import { Area, IsCellAddress, Style } from 'treb-base-types';
28
28
 
29
29
  export interface SerializedMacroFunction {
30
30
  name: string;
@@ -159,6 +159,8 @@ export class SheetCollection {
159
159
  */
160
160
  export class DataModel {
161
161
 
162
+ public readonly parser: Parser = new Parser();
163
+
162
164
  /** document metadata */
163
165
  public document_name?: string;
164
166
 
@@ -317,7 +319,132 @@ export class DataModel {
317
319
 
318
320
  return undefined;
319
321
  }
320
-
322
+
323
+ /**
324
+ * return an address label for this address (single cell or range)
325
+ *
326
+ * @param address
327
+ * @param active_sheet
328
+ */
329
+ public AddressToLabel(address: ICellAddress|IArea, active_sheet?: Sheet) {
330
+
331
+ const start = IsCellAddress(address) ? address : address.start;
332
+ const parts = IsCellAddress(address) ?
333
+ [Area.CellAddressToLabel(address)] :
334
+ [Area.CellAddressToLabel(address.start), Area.CellAddressToLabel(address.end)];
335
+
336
+ const sheet = this.sheets.Find(start.sheet_id || 0);
337
+ const name = (sheet?.name) ?
338
+ (QuotedSheetNameRegex.test(sheet.name) ? `'${sheet.name}'` : sheet.name) : '';
339
+
340
+ return name + (name ? '!' : '') + (parts[0] === parts[1] ? parts[0] : parts.join(':'));
341
+
342
+ }
343
+
344
+ // --- resolution api, moved from calculator ---------------------------------
345
+
346
+ /**
347
+ * returns false if the sheet cannot be resolved, which probably
348
+ * means the name changed (that's the case we are working on with
349
+ * this fix).
350
+ */
351
+ public ResolveSheetID(expr: UnitAddress|UnitRange, context?: ICellAddress, active_sheet?: Sheet): boolean {
352
+
353
+ const target = expr.type === 'address' ? expr : expr.start;
354
+
355
+ if (target.sheet_id) {
356
+ return true;
357
+ }
358
+
359
+ if (target.sheet) {
360
+ const sheet = this.sheets.Find(target.sheet);
361
+ if (sheet) {
362
+ target.sheet_id = sheet.id;
363
+ return true;
364
+ }
365
+
366
+ /*
367
+ const lc = target.sheet.toLowerCase();
368
+ for (const sheet of this.model.sheets.list) {
369
+ if (sheet.name.toLowerCase() === lc) {
370
+ target.sheet_id = sheet.id;
371
+ return true;
372
+ }
373
+ }
374
+ */
375
+ }
376
+ else if (context?.sheet_id) {
377
+ target.sheet_id = context.sheet_id;
378
+ return true;
379
+ }
380
+ else if (active_sheet?.id) {
381
+ target.sheet_id = active_sheet.id;
382
+ return true;
383
+ }
384
+
385
+ return false; // the error
386
+
387
+ }
388
+
389
+ /** wrapper method ensures it always returns an Area (instance, not interface) */
390
+ public ResolveArea(address: string|ICellAddress|IArea, active_sheet: Sheet): Area {
391
+ const resolved = this.ResolveAddress(address, active_sheet);
392
+ return IsCellAddress(resolved) ? new Area(resolved) : new Area(resolved.start, resolved.end);
393
+ }
394
+
395
+ /**
396
+ * moved from embedded sheet. also modified to preserve ranges, so it
397
+ * might return a range (area). if you are expecting the old behavior
398
+ * you need to check (perhaps we could have a wrapper, or make it optional?)
399
+ *
400
+ * Q: why does this not go in grid? or model? (...)
401
+ * Q: why are we not preserving absoute/relative? (...)
402
+ *
403
+ */
404
+ public ResolveAddress(address: string|ICellAddress|IArea, active_sheet: Sheet): ICellAddress|IArea {
405
+
406
+ if (typeof address === 'string') {
407
+ const parse_result = this.parser.Parse(address);
408
+ if (parse_result.expression && parse_result.expression.type === 'address') {
409
+ this.ResolveSheetID(parse_result.expression, undefined, active_sheet);
410
+ return {
411
+ row: parse_result.expression.row,
412
+ column: parse_result.expression.column,
413
+ sheet_id: parse_result.expression.sheet_id,
414
+ };
415
+ }
416
+ else if (parse_result.expression && parse_result.expression.type === 'range') {
417
+ this.ResolveSheetID(parse_result.expression, undefined, active_sheet);
418
+ return {
419
+ start: {
420
+ row: parse_result.expression.start.row,
421
+ column: parse_result.expression.start.column,
422
+ sheet_id: parse_result.expression.start.sheet_id,
423
+ },
424
+ end: {
425
+ row: parse_result.expression.end.row,
426
+ column: parse_result.expression.end.column,
427
+ }
428
+ };
429
+ }
430
+ else if (parse_result.expression && parse_result.expression.type === 'identifier') {
431
+
432
+ // is named range guaranteed to have a sheet ID? (I think yes...)
433
+
434
+ const named_range = this.named_ranges.Get(parse_result.expression.name);
435
+ if (named_range) {
436
+ return named_range;
437
+ }
438
+ }
439
+
440
+ return { row: 0, column: 0 }; // default for string types -- broken
441
+
442
+ }
443
+
444
+ return address; // already range or address
445
+
446
+ }
447
+
321
448
  }
322
449
 
323
450
  export interface ViewModel {
@@ -0,0 +1,47 @@
1
+
2
+ import type { IArea, ICellAddress } from 'treb-base-types';
3
+
4
+ export type DependencyList = Array<IArea|ICellAddress|undefined>;
5
+ export type ExternalEditorCallback = (selection?: string) => DependencyList|undefined;
6
+
7
+ // FIXME: if you want to keep the old interface, split into to separate types
8
+
9
+ export interface ExternalEditorConfig {
10
+
11
+ // --- old interface ---------------------------------------------------------
12
+
13
+ /**
14
+ * list of dependencies to highlight. we support undefined entries in
15
+ * this list so you can use the result of `EmbeddedSpreadsheet.Resolve`,
16
+ * which may return undefined.
17
+ */
18
+ dependencies: DependencyList;
19
+
20
+ /**
21
+ * this callback will be called when the selection changes in the
22
+ * spreadsheet and this external editor is active. return an updated
23
+ * list of dependencies to highlight.
24
+ *
25
+ * NOTE: this is currently synchronous, but don't rely on that. it
26
+ * might switch to async in the future depending on how it works in
27
+ * practice.
28
+ */
29
+ update: ExternalEditorCallback;
30
+
31
+ // --- new interface ---------------------------------------------------------
32
+
33
+ /**
34
+ * a list of nodes that will serve as editors. when you attach, we will do
35
+ * an initial pass of context highlighting. we highlight on text changes
36
+ * and insert references if you make a selection in the spreadsheet while
37
+ * an editor is focused.
38
+ */
39
+ nodes: HTMLElement[];
40
+
41
+ /**
42
+ * assume that we're editing a formula. does not require leading `=`.
43
+ * defaults to `true` for historical reasons.
44
+ */
45
+ assume_formula?: boolean;
46
+
47
+ }