@trebco/treb 28.11.1 → 28.15.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 (37) hide show
  1. package/dist/treb-spreadsheet-light.mjs +11 -11
  2. package/dist/treb-spreadsheet.mjs +11 -11
  3. package/dist/treb.d.ts +27 -3
  4. package/package.json +1 -1
  5. package/treb-base-types/src/style.ts +3 -0
  6. package/treb-calculator/src/calculator.ts +235 -68
  7. package/treb-calculator/src/descriptors.ts +5 -0
  8. package/treb-calculator/src/expression-calculator.ts +9 -5
  9. package/treb-calculator/src/functions/base-functions.ts +410 -21
  10. package/treb-calculator/src/functions/text-functions.ts +45 -55
  11. package/treb-calculator/src/primitives.ts +11 -0
  12. package/treb-calculator/src/utilities.ts +55 -0
  13. package/treb-embed/markup/layout.html +15 -10
  14. package/treb-embed/markup/toolbar.html +5 -5
  15. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +38 -2
  16. package/treb-embed/src/embedded-spreadsheet.ts +227 -29
  17. package/treb-embed/src/options.ts +5 -0
  18. package/treb-embed/style/dark-theme.scss +1 -0
  19. package/treb-embed/style/formula-bar.scss +20 -7
  20. package/treb-embed/style/theme-defaults.scss +20 -0
  21. package/treb-export/src/export-worker/export-worker.ts +1 -0
  22. package/treb-export/src/export2.ts +6 -1
  23. package/treb-export/src/import2.ts +76 -6
  24. package/treb-export/src/shared-strings2.ts +1 -1
  25. package/treb-export/src/workbook-style2.ts +89 -52
  26. package/treb-export/src/workbook2.ts +119 -1
  27. package/treb-grid/src/editors/editor.ts +7 -0
  28. package/treb-grid/src/editors/formula_bar.ts +23 -1
  29. package/treb-grid/src/render/tile_renderer.ts +46 -3
  30. package/treb-grid/src/types/annotation.ts +17 -3
  31. package/treb-grid/src/types/grid.ts +28 -9
  32. package/treb-grid/src/types/grid_base.ts +10 -105
  33. package/treb-grid/src/types/grid_options.ts +3 -2
  34. package/treb-grid/src/types/named_range.ts +8 -1
  35. package/treb-grid/src/types/serialize_options.ts +5 -0
  36. package/treb-parser/src/parser-types.ts +27 -4
  37. package/treb-parser/src/parser.ts +74 -36
@@ -34,6 +34,7 @@ import type { BaseLayout, TileRange } from '../layout/base_layout';
34
34
  import type { DataModel, ViewModel } from '../types/data_model';
35
35
  import type { GridOptions } from '../types/grid_options';
36
36
 
37
+ const DEFAULT_INDENT = ' '; // two spaces in the current font
37
38
  const BASELINE = 'bottom';
38
39
  const WK = /webkit/i.test(typeof navigator === 'undefined' ? '' : navigator?.userAgent || '') ? 1 : 0;
39
40
 
@@ -723,6 +724,15 @@ export class TileRenderer {
723
724
  let override_formatting: string | undefined;
724
725
  let formatted = cell.editing ? '' : cell.formatted; // <-- empty on editing, to remove overflows
725
726
 
727
+ // precalculate indent as string so we can use layout
728
+
729
+ let indent = '';
730
+ if (style.indent) {
731
+ for (let i = 0; i < style.indent; i++) {
732
+ indent += DEFAULT_INDENT;
733
+ }
734
+ }
735
+
726
736
  if (Array.isArray(formatted)) {
727
737
 
728
738
  // type 1 is a multi-part formatted string; used for number formats.
@@ -733,6 +743,15 @@ export class TileRenderer {
733
743
 
734
744
  // this is a single line, with number formatting
735
745
 
746
+ if (indent) {
747
+ if (style.horizontal_align === 'left') {
748
+ formatted.unshift({ text: indent });
749
+ }
750
+ else if (style.horizontal_align === 'right') {
751
+ formatted.push({ text: indent });
752
+ }
753
+ }
754
+
736
755
  for (const part of formatted) {
737
756
  if (part.flag === TextPartFlag.formatting) {
738
757
  override_formatting = part.text;
@@ -810,11 +829,14 @@ export class TileRenderer {
810
829
 
811
830
  // for wrapping
812
831
 
813
- const bound = cell_width - (2 * this.cell_edge_buffer);
832
+ let bound = cell_width - (2 * this.cell_edge_buffer);
814
833
  const strings: RenderTextPart[][] = [];
815
834
 
816
835
  if (style.wrap) {
817
836
 
837
+ const indent_width = (indent && style.horizontal_align !== 'center') ? context.measureText(indent).width : 0;
838
+ bound -= indent_width;
839
+
818
840
  for (const line of md) {
819
841
 
820
842
  // we should probably normalize whitespace -- because formatting
@@ -921,14 +943,25 @@ export class TileRenderer {
921
943
 
922
944
  max_width = Math.max(max_width, last.width);
923
945
 
924
- strings.push(line2.map((metric) => {
946
+ const line_string = line2.map((metric) => {
925
947
  return {
926
948
  ...metric.part,
927
949
  hidden: false,
928
950
  width: metric.width,
929
951
  text: metric.text,
930
952
  };
931
- }));
953
+ });
954
+
955
+ if (style.indent) {
956
+ if (style.horizontal_align === 'left') {
957
+ line_string.unshift({ text: indent, hidden: false, width: indent_width });
958
+ }
959
+ else if (style.horizontal_align === 'right') {
960
+ line_string.push({ text: indent, hidden: false, width: indent_width });
961
+ }
962
+ }
963
+
964
+ strings.push(line_string);
932
965
 
933
966
  }
934
967
 
@@ -939,9 +972,19 @@ export class TileRenderer {
939
972
 
940
973
  // simple case
941
974
 
975
+
942
976
  for (const line of md) {
943
977
  const parts: RenderTextPart[] = [];
944
978
 
979
+ if (style.indent) {
980
+ if (style.horizontal_align === 'left') {
981
+ line.unshift({ text: indent });
982
+ }
983
+ else if (style.horizontal_align === 'right') {
984
+ line.push({ text: indent });
985
+ }
986
+ }
987
+
945
988
  let line_width = 0;
946
989
 
947
990
  for (const element of line) {
@@ -19,7 +19,7 @@
19
19
  *
20
20
  */
21
21
 
22
- import type { ICellAddress, AnnotationLayout, IRectangle } from 'treb-base-types';
22
+ import type { ICellAddress, AnnotationLayout, IRectangle, CellStyle } from 'treb-base-types';
23
23
  import { Rectangle } from 'treb-base-types';
24
24
 
25
25
  /**
@@ -97,7 +97,7 @@ export interface ImageAnnotationData {
97
97
 
98
98
  }
99
99
 
100
- export type AnnotationType = 'treb-chart'|'image'|'external';
100
+ export type AnnotationType = 'treb-chart'|'image'|'textbox'|'external';
101
101
 
102
102
  /**
103
103
  * splitting persisted data from the annotation class. that class might
@@ -182,12 +182,26 @@ export interface AnnotationChartData extends AnnotationDataBase {
182
182
  type: 'treb-chart';
183
183
  }
184
184
 
185
+ export interface AnnotationTextBoxData extends AnnotationDataBase {
186
+ type: 'textbox';
187
+ data: {
188
+ style?: CellStyle;
189
+ paragraphs: {
190
+ style?: CellStyle,
191
+ content: {
192
+ text: string,
193
+ style?: CellStyle
194
+ }[],
195
+ }[];
196
+ };
197
+ };
198
+
185
199
  export interface AnnotationExternalData extends AnnotationDataBase {
186
200
  type: 'external';
187
201
  data: Record<string, string>;
188
202
  }
189
203
 
190
- export type AnnotationData = AnnotationChartData | AnnotationImageData | AnnotationExternalData;
204
+ export type AnnotationData = AnnotationChartData | AnnotationImageData | AnnotationExternalData | AnnotationTextBoxData;
191
205
 
192
206
  /**
193
207
  * why is this a class? it doesn't do anything.
@@ -634,6 +634,14 @@ export class Grid extends GridBase {
634
634
  this.editing_annotation = annotation;
635
635
  this.layout.ShowSelections(true);
636
636
  }
637
+ else if (this.formula_bar?.IsExpandButton(event.relatedTarget as HTMLElement)) {
638
+
639
+ // for this particular case, do nothing. basically you are
640
+ // expanding/contracting the formula bar. we want to preserve
641
+ // the selected annotation, if any. after the operation we'll
642
+ // restore focus.
643
+
644
+ }
637
645
  else {
638
646
  if (this.selected_annotation === annotation) {
639
647
  this.selected_annotation = undefined;
@@ -835,9 +843,9 @@ export class Grid extends GridBase {
835
843
  }
836
844
  }
837
845
 
838
- /**
846
+ /* *
839
847
  * specialization: update selection, scroll offset
840
- */
848
+ * /
841
849
  public Serialize(options: SerializeOptions = {}): SerializedModel {
842
850
 
843
851
  // selection moved to sheet, but it's not "live"; so we need to
@@ -855,6 +863,7 @@ export class Grid extends GridBase {
855
863
  return super.Serialize(options);
856
864
 
857
865
  }
866
+ */
858
867
 
859
868
  /**
860
869
  * show or hide headers
@@ -1830,18 +1839,23 @@ export class Grid extends GridBase {
1830
1839
  argument_separator: this.parser.argument_separator,
1831
1840
  decimal_mark: this.parser.decimal_mark,
1832
1841
  }
1842
+
1843
+ this.parser.Save();
1844
+
1833
1845
  let convert = false;
1834
1846
 
1835
1847
  if (options.argument_separator === ',' && this.parser.argument_separator !== ArgumentSeparatorType.Comma) {
1836
- this.parser.argument_separator = ArgumentSeparatorType.Comma;
1837
- this.parser.decimal_mark = DecimalMarkType.Period;
1848
+ // this.parser.argument_separator = ArgumentSeparatorType.Comma;
1849
+ // this.parser.decimal_mark = DecimalMarkType.Period;
1850
+ this.parser.SetLocaleSettings(DecimalMarkType.Period);
1838
1851
 
1839
1852
  convert = true;
1840
1853
  }
1841
1854
 
1842
1855
  if (options.argument_separator === ';' && this.parser.argument_separator !== ArgumentSeparatorType.Semicolon) {
1843
- this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
1844
- this.parser.decimal_mark = DecimalMarkType.Comma;
1856
+ // this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
1857
+ // this.parser.decimal_mark = DecimalMarkType.Comma;
1858
+ this.parser.SetLocaleSettings(DecimalMarkType.Comma);
1845
1859
 
1846
1860
  convert = true;
1847
1861
  }
@@ -1882,8 +1896,10 @@ export class Grid extends GridBase {
1882
1896
 
1883
1897
  // reset
1884
1898
 
1885
- this.parser.argument_separator = current.argument_separator;
1886
- this.parser.decimal_mark = current.decimal_mark;
1899
+ // this.parser.argument_separator = current.argument_separator;
1900
+ // this.parser.decimal_mark = current.decimal_mark;
1901
+
1902
+ this.parser.Restore();
1887
1903
 
1888
1904
  }
1889
1905
 
@@ -4439,7 +4455,10 @@ export class Grid extends GridBase {
4439
4455
 
4440
4456
  if (this.overlay_editor?.selection) {
4441
4457
  const value = this.overlay_editor?.edit_node.textContent || undefined;
4442
- const array = (event.key === 'Enter' && event.ctrlKey && event.shiftKey);
4458
+
4459
+ // let's support command+shift+enter on mac
4460
+ const array = (event.key === 'Enter' && (event.ctrlKey || (UA.is_mac && event.metaKey)) && event.shiftKey);
4461
+
4443
4462
  this.SetInferredType(this.overlay_editor.selection, value, array);
4444
4463
  }
4445
4464
 
@@ -461,13 +461,19 @@ export class GridBase {
461
461
  // CSV assumes dot-decimal, correct? if we want to use the
462
462
  // parser we will have to check (and set/reset) the separator
463
463
 
464
+ this.parser.Save();
465
+ this.parser.SetLocaleSettings(DecimalMarkType.Period);
466
+
467
+ /*
464
468
  const toggle_separator = this.parser.decimal_mark === DecimalMarkType.Comma;
465
469
 
466
- if (toggle_separator) {
470
+ if (toggle_separator)
471
+ {
467
472
  // swap
468
473
  this.parser.argument_separator = ArgumentSeparatorType.Comma;
469
474
  this.parser.decimal_mark = DecimalMarkType.Period;
470
475
  }
476
+ */
471
477
 
472
478
  const records = ParseCSV(text);
473
479
  const arr = records.map((record) =>
@@ -481,11 +487,14 @@ export class GridBase {
481
487
  return ValueParser.TryParse(field).value;
482
488
  }));
483
489
 
490
+ /*
484
491
  if (toggle_separator) {
485
492
  // reset
486
493
  this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
487
494
  this.parser.decimal_mark = DecimalMarkType.Comma;
488
495
  }
496
+ */
497
+ this.parser.Restore();
489
498
 
490
499
  const end = {
491
500
  row: Math.max(0, arr.length - 1),
@@ -735,110 +744,6 @@ export class GridBase {
735
744
  this.model.user_data = undefined;
736
745
  }
737
746
 
738
- /**
739
- * serialize data. this function used to (optionally) stringify
740
- * by typescript has a problem figuring this out, so we will simplify
741
- * the function.
742
- */
743
- public Serialize(options: SerializeOptions = {}): SerializedModel {
744
-
745
- // (removed UI stuff, that goes in subclass)
746
-
747
- // selection moved to sheet, but it's not "live"; so we need to
748
- // capture the primary selection in the current active sheet before
749
- // we serialize it
750
-
751
- // this.active_sheet.selection = JSON.parse(JSON.stringify(this.primary_selection));
752
-
753
- // same for scroll offset
754
-
755
- // this.active_sheet.scroll_offset = this.layout.scroll_offset;
756
-
757
- // NOTE: annotations moved to sheets, they will be serialized in the sheets
758
-
759
- const sheet_data = this.model.sheets.list.map((sheet) => sheet.toJSON(options));
760
-
761
- // OK, not serializing tables in cells anymore. old comment about this:
762
- //
763
- // at the moment, tables are being serialized in cells. if we put them
764
- // in here, then we have two records of the same data. that would be bad.
765
- // I think this is probably the correct place, but if we put them here
766
- // we need to stop serializing in cells. and I'm not sure that there are
767
- // not some side-effects to that. hopefully not, but (...)
768
- //
769
-
770
- let tables: Table[] | undefined;
771
- if (this.model.tables.size > 0) {
772
- tables = Array.from(this.model.tables.values());
773
- }
774
-
775
- // NOTE: moving into a structured object (the sheet data is also structured,
776
- // of course) but we are moving things out of sheet (just named ranges atm))
777
-
778
- let macro_functions: MacroFunction[] | undefined;
779
-
780
- if (this.model.macro_functions.size) {
781
- macro_functions = [];
782
- for (const macro of this.model.macro_functions.values()) {
783
- macro_functions.push({
784
- ...macro,
785
- expression: undefined,
786
- });
787
- }
788
- }
789
-
790
- // when serializing named expressions, we have to make sure
791
- // that there's a sheet name in any address/range.
792
-
793
- const named_expressions: SerializedNamedExpression[] = [];
794
- if (this.model.named_expressions) {
795
-
796
- for (const [name, expr] of this.model.named_expressions) {
797
- this.parser.Walk(expr, unit => {
798
- if (unit.type === 'address' || unit.type === 'range') {
799
- const test = unit.type === 'range' ? unit.start : unit;
800
-
801
- test.absolute_column = test.absolute_row = true;
802
-
803
- if (!test.sheet) {
804
- if (test.sheet_id) {
805
- const sheet = this.model.sheets.Find(test.sheet_id);
806
- if (sheet) {
807
- test.sheet = sheet.name;
808
- }
809
- }
810
- if (!test.sheet) {
811
- test.sheet = this.active_sheet.name;
812
- }
813
- }
814
-
815
- if (unit.type === 'range') {
816
- unit.end.absolute_column = unit.end.absolute_row = true;
817
- }
818
-
819
- return false;
820
- }
821
- return true;
822
- });
823
- const rendered = this.parser.Render(expr, { missing: '' });
824
- named_expressions.push({
825
- name, expression: rendered
826
- });
827
- }
828
- }
829
-
830
- return {
831
- sheet_data,
832
- active_sheet: this.active_sheet.id,
833
- named_ranges: this.model.named_ranges.Count() ?
834
- this.model.named_ranges.Serialize() :
835
- undefined,
836
- macro_functions,
837
- tables,
838
- named_expressions: named_expressions.length ? named_expressions : undefined,
839
- };
840
-
841
- }
842
747
 
843
748
  // --- protected methods -----------------------------------------------------
844
749
 
@@ -68,8 +68,9 @@ export interface GridOptions {
68
68
  /* * show delete tab in the tab bar */
69
69
  // delete_tab?: boolean;
70
70
 
71
- /** show the "insert function" button. requires formula bar. */
71
+ /* * show the "insert function" button. requires formula bar. * /
72
72
  insert_function_button?: boolean;
73
+ */
73
74
 
74
75
  /** button to increase/reduce size of formula editor */
75
76
  expand_formula_button?: boolean;
@@ -97,7 +98,7 @@ export const DefaultGridOptions: GridOptions = {
97
98
  formula_bar: true,
98
99
  add_tab: false,
99
100
  tab_bar: 'auto',
100
- insert_function_button: false,
101
+ // insert_function_button: false,
101
102
  expand_formula_button: false,
102
103
  expand: true,
103
104
  repaint_on_cell_change: true,
@@ -92,10 +92,17 @@ export class NamedRangeCollection {
92
92
  console.warn('invalid name');
93
93
  return false;
94
94
  }
95
+
96
+ // why is this considered invalid here? I've seen it done.
97
+ // maybe something we're doing with these ranges doesn't
98
+ // collapse them? (...)
99
+
95
100
  if (range.entire_column || range.entire_row) {
96
- console.warn('invalid range');
101
+ console.info({range});
102
+ console.warn(`invalid range`);
97
103
  return false;
98
104
  }
105
+
99
106
  this.forward[validated] = range;
100
107
  if (apply) {
101
108
  this.RebuildList();
@@ -69,4 +69,9 @@ export interface SerializeOptions {
69
69
  /** share resources (images, for now) to prevent writing data URIs more than once */
70
70
  share_resources?: boolean;
71
71
 
72
+ /**
73
+ * if a function has an export() handler, call that
74
+ */
75
+ export_functions?: boolean;
76
+
72
77
  }
@@ -305,9 +305,19 @@ export interface ParseResult {
305
305
  full_reference_list?: Array<UnitRange | UnitAddress | UnitIdentifier | UnitStructuredReference>;
306
306
  }
307
307
 
308
+ /**
309
+ * moving these settings into flags, but we want to ensure that
310
+ * they're available so we'll have some required flags
311
+ *
312
+ */
313
+ export interface RequiredParserFlags {
314
+ decimal_mark: DecimalMarkType;
315
+ argument_separator: ArgumentSeparatorType;
316
+ }
317
+
308
318
  //
309
319
 
310
- export interface ParserFlags {
320
+ export interface OptionalParserFlags {
311
321
 
312
322
  /**
313
323
  * flag: support spreadsheet addresses (e.g. "A1"). this is the default,
@@ -386,6 +396,8 @@ export interface ParserFlags {
386
396
 
387
397
  }
388
398
 
399
+ export type ParserFlags = Partial<OptionalParserFlags> & RequiredParserFlags;
400
+
389
401
  export interface RenderOptions {
390
402
  offset: { rows: number; columns: number };
391
403
  missing: string;
@@ -396,8 +408,19 @@ export interface RenderOptions {
396
408
  table_name: string;
397
409
  }
398
410
 
411
+ /*
399
412
  export interface PersistedParserConfig {
400
- flags: Partial<ParserFlags>;
401
- argument_separator: ArgumentSeparatorType;
402
- decimal_mark: DecimalMarkType;
413
+ flags: Partial<ParserFlags> & RequiredParserFlags;
414
+ // argument_separator: ArgumentSeparatorType;
415
+ // decimal_mark: DecimalMarkType;
403
416
  }
417
+ */
418
+
419
+ export const DefaultParserConfig: ParserFlags = {
420
+ spreadsheet_semantics: true,
421
+ dimensioned_quantities: false,
422
+ fractions: true,
423
+ decimal_mark: DecimalMarkType.Period,
424
+ argument_separator: ArgumentSeparatorType.Comma,
425
+ };
426
+