@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
@@ -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
+
@@ -1720,15 +1720,16 @@ export class Grid extends GridBase {
1720
1720
  this.RenderSelections();
1721
1721
  }
1722
1722
 
1723
- /**
1723
+ /* *
1724
1724
  * FIXME: who uses this? anyone?
1725
- */
1725
+ * /
1726
1726
  public GetNumberFormat(address: ICellAddress): string|undefined {
1727
1727
  const style = this.active_sheet.CellStyleData(address);
1728
1728
  if (style && style.number_format) {
1729
1729
  return NumberFormatCache.Get(style.number_format).toString();
1730
1730
  }
1731
1731
  }
1732
+ */
1732
1733
 
1733
1734
  /**
1734
1735
  * I can't figure out a way in typescript to overload the GetRange function
@@ -1986,7 +1987,7 @@ export class Grid extends GridBase {
1986
1987
  }
1987
1988
 
1988
1989
  /** repaint after an external event (calculation) */
1989
- public Update(force = false, area?: Area): void {
1990
+ public Update(force = false, area?: IArea|IArea[]): void {
1990
1991
  this.DelayedRender(force, area);
1991
1992
  }
1992
1993
 
@@ -2792,9 +2793,7 @@ export class Grid extends GridBase {
2792
2793
 
2793
2794
  }
2794
2795
 
2795
-
2796
-
2797
- private DelayedRender(force = false, area?: Area, full_tile = false) {
2796
+ private DelayedRender(force = false, area?: IArea|IArea[], full_tile = false) {
2798
2797
 
2799
2798
  // if area is passed, set dirty before calling repaint
2800
2799
 
@@ -40,7 +40,7 @@ import type { DataModel, MacroFunction, SerializedModel, SerializedNamedExpressi
40
40
  import type { Parser, UnitAddress} from 'treb-parser';
41
41
  import { type ExpressionUnit, IllegalSheetNameRegex, ParseCSV, ArgumentSeparatorType, DecimalMarkType } from 'treb-parser';
42
42
  import { Area, IsCellAddress, ValidationType, ValueType, DefaultTableSortOptions } from 'treb-base-types';
43
- import type { ICellAddress, IArea, Cell, CellValue , Style, CellStyle, Table, TableSortOptions, TableTheme, Complex } from 'treb-base-types';
43
+ import type { ICellAddress, IArea, Cell, CellValue , Style, CellStyle, Table, TableSortOptions, TableTheme, Complex, PatchOptions as PatchAreaOptions } from 'treb-base-types';
44
44
  import { Sheet } from './sheet';
45
45
  import type { FunctionDescriptor} from '../editors/autocomplete_matcher';
46
46
  import { AutocompleteMatcher, DescriptorType } from '../editors/autocomplete_matcher';
@@ -61,6 +61,11 @@ import type { UpdateFlags } from './update_flags';
61
61
  import type { FreezePane, LegacySerializedSheet } from './sheet_types';
62
62
  import type { Annotation } from './annotation';
63
63
  import type { ClipboardCellData } from './clipboard_data';
64
+ import type { ConditionalFormat } from './conditional_format';
65
+
66
+ interface PatchOptions extends PatchAreaOptions {
67
+ sheet: Sheet;
68
+ }
64
69
 
65
70
  export class GridBase {
66
71
 
@@ -2146,6 +2151,29 @@ export class GridBase {
2146
2151
 
2147
2152
  }
2148
2153
 
2154
+ protected PatchExpressionSheetName(expression: string, old_name: string, name: string): string|undefined {
2155
+
2156
+ let modified = false;
2157
+ const parsed = this.parser.Parse(expression || '');
2158
+ if (parsed.expression) {
2159
+ this.parser.Walk(parsed.expression, (element: ExpressionUnit) => {
2160
+ if (element.type === 'address') {
2161
+ if (element.sheet && element.sheet.toLowerCase() === old_name) {
2162
+ element.sheet = name;
2163
+ modified = true;
2164
+ }
2165
+ }
2166
+ return true; // continue walk
2167
+ });
2168
+ if (modified) {
2169
+ return '=' + this.parser.Render(parsed.expression, { missing: '' });
2170
+ }
2171
+ }
2172
+
2173
+ return undefined;
2174
+
2175
+ }
2176
+
2149
2177
  /**
2150
2178
  * splitting this logic into a new function so we can reuse it
2151
2179
  * for invalidating broken references. generally we'll call this
@@ -2165,45 +2193,39 @@ export class GridBase {
2165
2193
  // cells
2166
2194
  sheet.cells.IterateAll((cell: Cell) => {
2167
2195
  if (cell.ValueIsFormula()) {
2168
- let modified = false;
2169
- const parsed = this.parser.Parse(cell.value || '');
2170
- if (parsed.expression) {
2171
- this.parser.Walk(parsed.expression, (element: ExpressionUnit) => {
2172
- if (element.type === 'address') {
2173
- if (element.sheet && element.sheet.toLowerCase() === old_name) {
2174
- element.sheet = name;
2175
- modified = true;
2196
+ const updated = this.PatchExpressionSheetName(cell.value||'', old_name, name);
2197
+ if (updated) {
2198
+ cell.value = updated;
2199
+ changes++;
2200
+ }
2201
+ }
2202
+ });
2203
+
2204
+ // conditionals
2205
+ if (sheet.conditional_formats?.length) {
2206
+ for (const format of sheet.conditional_formats) {
2207
+ switch (format.type) {
2208
+ case 'cell-match':
2209
+ case 'expression':
2210
+ {
2211
+ const updated = this.PatchExpressionSheetName(format.expression, old_name, name);
2212
+ if (updated) {
2213
+ format.expression = updated;
2214
+ changes++;
2176
2215
  }
2177
2216
  }
2178
- return true; // continue walk
2179
- });
2180
- if (modified) {
2181
- cell.value = '=' + this.parser.Render(parsed.expression, { missing: '' });
2182
- changes++;
2183
- }
2217
+ break;
2184
2218
  }
2185
2219
  }
2186
- });
2220
+ }
2187
2221
 
2188
2222
  // annotations
2189
2223
  for (const annotation of sheet.annotations) {
2190
2224
  if (annotation.data.formula) {
2191
- let modified = false;
2192
- const parsed = this.parser.Parse(annotation.data.formula || '');
2193
- if (parsed.expression) {
2194
- this.parser.Walk(parsed.expression, (element: ExpressionUnit) => {
2195
- if (element.type === 'address') {
2196
- if (element.sheet && element.sheet.toLowerCase() === old_name) {
2197
- element.sheet = name;
2198
- modified = true;
2199
- }
2200
- }
2201
- return true; // continue walk
2202
- });
2203
- if (modified) {
2204
- annotation.data.formula = '=' + this.parser.Render(parsed.expression, { missing: '' });
2205
- changes++;
2206
- }
2225
+ const updated = this.PatchExpressionSheetName(annotation.data.formula, old_name, name);
2226
+ if (updated) {
2227
+ annotation.data.formula = updated;
2228
+ changes++;
2207
2229
  }
2208
2230
  }
2209
2231
  }
@@ -2634,7 +2656,121 @@ export class GridBase {
2634
2656
 
2635
2657
  }
2636
2658
 
2637
-
2659
+ /**
2660
+ * patch an expression for insert/delete row/column operations.
2661
+ * FIXME: should move, maybe to parser? (...)
2662
+ *
2663
+ * NOTE: did I write this twice? we only need one. check which one is better.
2664
+ * @see PatchFormulasInternal
2665
+ *
2666
+ * @returns the updated expression, or `undefined` if no changes were made.
2667
+ */
2668
+ protected PatchExpression(expression: string, options: PatchOptions) {
2669
+
2670
+ let count = 0;
2671
+ const parse_result = this.parser.Parse(expression);
2672
+ if (parse_result.expression) {
2673
+
2674
+ this.parser.Walk(parse_result.expression, unit => {
2675
+ if (unit.type === 'range') {
2676
+ if (!unit.start.sheet || (unit.start.sheet.toLowerCase() === options.sheet.name.toLowerCase())) {
2677
+ const updated = Area.PatchArea(unit, options);
2678
+ if (updated) {
2679
+ unit.start.row = updated.start.row;
2680
+ unit.start.column = updated.start.column;
2681
+ unit.end.row = updated.end.row;
2682
+ unit.end.column = updated.end.column;
2683
+ }
2684
+ else {
2685
+
2686
+ // FIXME: maybe have options for this? we don't really have a
2687
+ // good way to replace nodes atm
2688
+ unit.start.row = unit.end.row = unit.start.column = unit.end.column = -1;
2689
+
2690
+ }
2691
+ }
2692
+ count++;
2693
+ return false;
2694
+ }
2695
+ else if (unit.type === 'address') {
2696
+ const updated = Area.PatchArea({start: unit, end: unit}, options);
2697
+ if (updated) {
2698
+ unit.row = updated.start.row;
2699
+ unit.column = updated.start.column;
2700
+ }
2701
+ else {
2702
+
2703
+ // see above
2704
+ unit.row = unit.column = -1;
2705
+ }
2706
+ count++;
2707
+ return false;
2708
+ }
2709
+ return true;
2710
+ });
2711
+
2712
+ if (count) {
2713
+ const rendered = this.parser.Render(parse_result.expression, {
2714
+ missing: '',
2715
+ });
2716
+ // console.info("FROM", expression, "TO", rendered);
2717
+ return rendered;
2718
+ }
2719
+
2720
+ }
2721
+
2722
+ return undefined;
2723
+
2724
+ }
2725
+
2726
+ /**
2727
+ * patch sheet conditionals for insert/delete row/column operations.
2728
+ * some of them may be deleted.
2729
+ */
2730
+ protected PatchConditionals(options: PatchOptions) {
2731
+
2732
+ if (options.sheet.conditional_formats?.length) {
2733
+
2734
+ const delete_list: Set<ConditionalFormat> = new Set();
2735
+ for (const format of options.sheet.conditional_formats) {
2736
+
2737
+ // first adjust the format area
2738
+
2739
+ const updated = Area.PatchArea(format.area, options);
2740
+ if (updated) {
2741
+ format.area = JSON.parse(JSON.stringify(updated));
2742
+ }
2743
+ else {
2744
+ delete_list.add(format);
2745
+ continue; // don't bother with formula
2746
+ }
2747
+
2748
+ // next update the formula, if necessary. what do we do if the
2749
+ // area has disappeared? should be a #REF error, not sure we
2750
+ // can encode that properly
2751
+
2752
+ switch (format.type) {
2753
+ case 'expression':
2754
+ case 'cell-match':
2755
+ {
2756
+ const updated = this.PatchExpression(format.expression, options);
2757
+
2758
+ if (updated) {
2759
+ format.expression = updated;
2760
+ }
2761
+ }
2762
+ break;
2763
+ }
2764
+
2765
+ }
2766
+
2767
+ if (delete_list.size) {
2768
+ options.sheet.conditional_formats = options.sheet.conditional_formats.filter(test => !delete_list.has(test));
2769
+ }
2770
+
2771
+ }
2772
+
2773
+ }
2638
2774
 
2639
2775
  /**
2640
2776
  * FIXME: should be API method
@@ -2664,6 +2800,15 @@ export class GridBase {
2664
2800
  return { error: true };
2665
2801
  }
2666
2802
 
2803
+ // conditionals
2804
+ this.PatchConditionals({
2805
+ sheet: target_sheet,
2806
+ before_column: 0,
2807
+ column_count: 0,
2808
+ before_row: command.before_row,
2809
+ row_count: command.count
2810
+ });
2811
+
2667
2812
  // see InsertColumnsInternal re: tables. rows are less complicated,
2668
2813
  // except that if you delete the header row we want to remove the
2669
2814
  // table entirely.
@@ -2978,6 +3123,14 @@ export class GridBase {
2978
3123
  return { error: true };
2979
3124
  }
2980
3125
 
3126
+ // conditionals
3127
+ this.PatchConditionals({
3128
+ sheet: target_sheet,
3129
+ before_column: command.before_column,
3130
+ column_count: command.count,
3131
+ before_row: 0,
3132
+ row_count: 0 });
3133
+
2981
3134
  // patch tables. we removed this from the sheet routine entirely,
2982
3135
  // we need to rebuild any affected tables now.
2983
3136