@trebco/treb 29.3.4 → 29.4.1

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.
@@ -30,8 +30,8 @@ import type { ParseResult } from 'treb-parser';
30
30
  import { Parser } from 'treb-parser';
31
31
  import type { RangeType, AddressType, HyperlinkType } from './address-type';
32
32
  import { is_range, ShiftRange, InRange, is_address } from './address-type';
33
- import type { ImportedSheetData, AnchoredAnnotation, CellParseResult, AnnotationLayout, Corner as LayoutCorner, ICellAddress, DataValidation, IArea, GradientStop, Color } from 'treb-base-types/src';
34
- import { ValidationType, type SerializedValueType } from 'treb-base-types/src';
33
+ import type { ImportedSheetData, AnchoredAnnotation, CellParseResult, AnnotationLayout, Corner as LayoutCorner, IArea, GradientStop, Color } from 'treb-base-types';
34
+ import type { SerializedValueType } from 'treb-base-types';
35
35
  import type { Sheet} from './workbook-sheet2';
36
36
  import { VisibleState } from './workbook-sheet2';
37
37
  import type { CellAnchor } from './drawing2/drawing2';
@@ -39,7 +39,7 @@ import { XMLUtils } from './xml-utils';
39
39
 
40
40
  // import { one_hundred_pixels } from './constants';
41
41
  import { ColumnWidthToPixels } from './column-width';
42
- import type { AnnotationType } from 'treb-data-model';
42
+ import type { DataValidation, AnnotationType } from 'treb-data-model';
43
43
  import { ZipWrapper } from './zip-wrapper';
44
44
  import type { ConditionalFormat } from 'treb-data-model';
45
45
 
@@ -99,7 +99,7 @@ export class Importer {
99
99
  arrays: RangeType[],
100
100
  merges: RangeType[],
101
101
  links: HyperlinkType[],
102
- validations: Array<{ address: ICellAddress, validation: DataValidation }>,
102
+ // validations: Array<{ address: ICellAddress, validation: DataValidation }>,
103
103
  ): CellParseResult | undefined {
104
104
 
105
105
  // must have, at minimum, an address (must be a single cell? FIXME)
@@ -285,12 +285,14 @@ export class Importer {
285
285
  }
286
286
  }
287
287
 
288
+ /*
288
289
  for (const validation of validations) {
289
290
  if (validation.address.row === shifted.row && validation.address.column === shifted.col) {
290
291
  result.validation = validation.validation;
291
292
  break;
292
293
  }
293
294
  }
295
+ */
294
296
 
295
297
  for (const range of merges) {
296
298
  if (InRange(range, shifted)) {
@@ -512,10 +514,15 @@ export class Importer {
512
514
  const conditional_formats: ConditionalFormat[] = [];
513
515
  const links: HyperlinkType[] = [];
514
516
  const row_styles: number[] = []; // may be sparse
517
+
518
+ /*
515
519
  const validations: Array<{
516
520
  address: ICellAddress,
517
521
  validation: DataValidation,
518
522
  }> = [];
523
+ */
524
+ const validations: DataValidation[] = [];
525
+
519
526
  const annotations: AnchoredAnnotation[] = [];
520
527
 
521
528
  const FindAll: (path: string) => any[] = XMLUtils.FindAll.bind(XMLUtils, sheet.sheet_data);
@@ -569,33 +576,68 @@ export class Importer {
569
576
  const formula = entry.formula1;
570
577
 
571
578
  if (ref && formula && type === 'list') {
572
- let address: ICellAddress|undefined;
579
+ // let address: ICellAddress|undefined;
573
580
  let validation: DataValidation|undefined;
574
581
  let parse_result = this.parser.Parse(ref);
582
+ const target: IArea[] = [];
575
583
 
576
584
  // apparently these are encoded as ranges for merged cells...
577
585
 
586
+ // NOTE: actually you can have a range, then validation applies
587
+ // to every cell in the range. also you can have multiple ranges,
588
+ // apparently separated by spaces.
589
+
578
590
  if (parse_result.expression) {
579
591
  if (parse_result.expression.type === 'address') {
580
- address = parse_result.expression;
592
+ // address = parse_result.expression;
593
+ target.push({start: parse_result.expression, end: parse_result.expression});
581
594
  }
582
595
  else if (parse_result.expression.type === 'range') {
583
- address = parse_result.expression.start;
596
+ // address = parse_result.expression.start;
597
+ target.push(parse_result.expression);
584
598
  }
585
599
  }
586
600
 
587
601
  parse_result = this.parser.Parse(formula);
602
+
588
603
  if (parse_result.expression) {
589
604
  if (parse_result.expression.type === 'range') {
590
605
  validation = {
591
- type: ValidationType.Range,
606
+ type: 'range',
592
607
  area: parse_result.expression,
608
+ target,
593
609
  };
594
610
  }
595
611
  else if (parse_result.expression.type === 'literal') {
596
612
  validation = {
597
- type: ValidationType.List,
613
+ type: 'list',
614
+ target,
598
615
  list: parse_result.expression.value.toString().split(/,/).map(value => {
616
+
617
+ // there are no formulas here. value is a string, separated
618
+ // by commas. there is no way to escape a comma (AFAICT; not
619
+ // official, but search). if you did want a comma, you'd need
620
+ // to use a range.
621
+
622
+ // but the uptake is split on commas. after that you can try
623
+ // to check for numbers or bools, but they will be in the string.
624
+
625
+ // I think excel might sort the entries? not sure. don't do it
626
+ // for now.
627
+
628
+ const num = Number(value);
629
+ if (!isNaN(num)) {
630
+ return num;
631
+ }
632
+ if (value.toLowerCase() === 'true') {
633
+ return true;
634
+ }
635
+ if (value.toLowerCase() === 'false') {
636
+ return false;
637
+ }
638
+ return value; // string
639
+
640
+ /*
599
641
  const tmp = this.parser.Parse(value);
600
642
 
601
643
  // if type is "group", that means we saw some spaces. this
@@ -612,13 +654,16 @@ export class Importer {
612
654
  return tmp.expression.name;
613
655
  }
614
656
  return undefined;
657
+ */
658
+
615
659
  }),
616
660
  };
617
661
  }
618
662
  }
619
663
 
620
- if (address && validation) {
621
- validations.push({address, validation});
664
+ if (target.length && validation) {
665
+ // validations.push({address, validation});
666
+ validations.push(validation);
622
667
  }
623
668
 
624
669
  }
@@ -730,7 +775,7 @@ export class Importer {
730
775
  if (!Array.isArray(cells)) { cells = [cells]; }
731
776
 
732
777
  for (const element of cells) {
733
- const cell = this.ParseCell(sheet, element, shared_formulae, arrays, merges, links, validations);
778
+ const cell = this.ParseCell(sheet, element, shared_formulae, arrays, merges, links); // , validations);
734
779
  if (cell) {
735
780
  data.push(cell);
736
781
  }
@@ -1178,6 +1223,7 @@ export class Importer {
1178
1223
  row_styles,
1179
1224
  annotations,
1180
1225
  conditional_formats,
1226
+ data_validations: validations,
1181
1227
  styles: this.workbook?.style_cache?.CellXfToStyles() || [],
1182
1228
  };
1183
1229
 
@@ -1295,19 +1295,8 @@ export abstract class BaseLayout {
1295
1295
  }
1296
1296
  }
1297
1297
 
1298
- /*
1299
- this.tooltip.style.fontFamily = theme.tooltip_font_face || '';
1300
- this.tooltip.style.fontSize = theme.tooltip_font_size ? `${theme.tooltip_font_size}pt` : '';
1301
- this.tooltip.style.backgroundColor = theme.tooltip_background || '';
1302
- this.tooltip.style.borderColor = theme.tooltip_background || ''; // for arrow
1303
- this.tooltip.style.color = theme.tooltip_color || '';
1304
- */
1305
-
1306
1298
  // TODO: dropdown caret
1307
1299
 
1308
- // this.dropdown_list.style.fontFamily = theme.cell_font || '';
1309
- // const font_size = (theme.cell_font_size_value || 10) * this.scale;
1310
- // this.dropdown_list.style.fontSize = (font_size) + (theme.cell_font_size_unit || 'pt');
1311
1300
  this.dropdown_list.style.font = Style.Font(theme.grid_cell || {});
1312
1301
 
1313
1302
  }
@@ -1415,6 +1404,8 @@ export abstract class BaseLayout {
1415
1404
  this.dropdown_list.style.left = `${target_rect.left + 2}px`;
1416
1405
  this.dropdown_list.style.minWidth = `${target_rect.width}px`;
1417
1406
 
1407
+ this.dropdown_list.style.fontSize = (this.scale.toFixed(2) + 'em');
1408
+
1418
1409
  this.dropdown_list.textContent = '';
1419
1410
  for (const value of list) {
1420
1411
  const entry = this.DOM.Div(undefined, this.dropdown_list);
@@ -40,7 +40,6 @@ import {
40
40
  ValueType,
41
41
  Localization,
42
42
  IsCellAddress,
43
- ValidationType,
44
43
  LoadThemeProperties,
45
44
  DefaultTheme,
46
45
  ComplexToString,
@@ -1465,20 +1464,22 @@ export class Grid extends GridBase {
1465
1464
  * and will render as a dropdown; the list can be a list of values or
1466
1465
  * a range reference.
1467
1466
  */
1468
- public SetValidation(target?: ICellAddress, data?: CellValue[]|IArea, error?: boolean): void {
1467
+ public SetValidation(target?: IArea, data?: CellValue[]|IArea, error?: boolean): void {
1469
1468
 
1470
1469
  if (!target) {
1471
1470
  if (this.primary_selection.empty) {
1472
1471
  throw new Error('invalid target in set validation');
1473
1472
  }
1474
- target = this.primary_selection.target;
1473
+ target = this.primary_selection.area;
1475
1474
  }
1476
1475
 
1476
+ const area = new Area(target.start, target.end);
1477
+
1477
1478
  // console.info({target, data});
1478
1479
 
1479
1480
  const command: DataValidationCommand = {
1480
1481
  key: CommandKey.DataValidation,
1481
- area: target,
1482
+ area: { start: area.start, end: area.end },
1482
1483
  error,
1483
1484
  };
1484
1485
 
@@ -1502,9 +1503,11 @@ export class Grid extends GridBase {
1502
1503
  //
1503
1504
 
1504
1505
  if (!this.primary_selection.empty &&
1505
- (!target.sheet_id || target.sheet_id === this.active_sheet.id) &&
1506
- (this.primary_selection.target.row === target.row) &&
1507
- (this.primary_selection.target.column === target.column)) {
1506
+ (!target.start.sheet_id || target.start.sheet_id === this.active_sheet.id) &&
1507
+ area.Contains(this.primary_selection.target)) {
1508
+
1509
+ // (this.primary_selection.target.row === target.start.row) &&
1510
+ // (this.primary_selection.target.column === target.start.column)) {
1508
1511
 
1509
1512
  // console.info('repaint selection');
1510
1513
 
@@ -4966,15 +4969,19 @@ export class Grid extends GridBase {
4966
4969
 
4967
4970
  }
4968
4971
 
4969
- if (cell.validation && cell.validation.error) {
4972
+ const validation = this.active_sheet.GetValidation(target)[0];
4973
+
4974
+ // only consider the first result
4975
+
4976
+ if (validation && validation.error) {
4970
4977
 
4971
4978
  let list: CellValue[]|undefined;
4972
4979
 
4973
- if (cell.validation.type === ValidationType.List) {
4974
- list = cell.validation.list;
4980
+ if (validation.type === 'list') {
4981
+ list = validation.list;
4975
4982
  }
4976
- else if (cell.validation.type === ValidationType.Range) {
4977
- list = this.GetValidationRange(cell.validation.area);
4983
+ else if (validation.type === 'range') {
4984
+ list = this.GetValidationRange(validation.area);
4978
4985
  }
4979
4986
 
4980
4987
  if (list && list.length) {
@@ -6216,15 +6223,19 @@ export class Grid extends GridBase {
6216
6223
  // sync up, so it would be a separate function but called at the
6217
6224
  // same time.
6218
6225
 
6219
- if (data.validation && !data.style?.locked) {
6226
+ // less true now that they're maintained separately
6227
+
6228
+ const validation = this.active_sheet.GetValidation(this.primary_selection.target)[0];
6229
+
6230
+ if (validation && !data.style?.locked) {
6220
6231
 
6221
6232
  let list: CellValue[] | undefined;
6222
6233
 
6223
- if (data.validation.type === ValidationType.List) {
6224
- list = data.validation.list;
6234
+ if (validation.type === 'list') {
6235
+ list = validation.list;
6225
6236
  }
6226
- else if (data.validation.type === ValidationType.Range) {
6227
- list = this.GetValidationRange(data.validation.area);
6237
+ else if (validation.type === 'range') {
6238
+ list = this.GetValidationRange(validation.area);
6228
6239
  }
6229
6240
 
6230
6241
  if (list && list.length) {
@@ -6541,7 +6552,7 @@ export class Grid extends GridBase {
6541
6552
  // tsv_row.push(cell.formatted);
6542
6553
 
6543
6554
  let text_value = '';
6544
- if (cell.calculated) {
6555
+ if (cell.calculated !== undefined) {
6545
6556
  if (cell.calculated_type === ValueType.complex) {
6546
6557
  text_value = ComplexToString(cell.calculated as Complex);
6547
6558
  }
@@ -37,11 +37,12 @@
37
37
 
38
38
  import { EventSource } from 'treb-utils';
39
39
  import type { DataModel, ConditionalFormat, // MacroFunction, SerializedModel, SerializedNamedExpression,
40
- ViewModel } from 'treb-data-model';
40
+ ViewModel,
41
+ DataValidation} from 'treb-data-model';
41
42
 
42
43
  import type { Parser, UnitAddress} from 'treb-parser';
43
44
  import { type ExpressionUnit, IllegalSheetNameRegex, ParseCSV, DecimalMarkType } from 'treb-parser';
44
- import { Area, IsCellAddress, ValidationType, ValueType, DefaultTableSortOptions } from 'treb-base-types';
45
+ import { Area, IsCellAddress, ValueType, DefaultTableSortOptions } from 'treb-base-types';
45
46
  import type { ICellAddress, IArea, Cell, CellValue, CellStyle, Table, TableSortOptions, TableTheme, Complex, PatchOptions as PatchAreaOptions } from 'treb-base-types';
46
47
 
47
48
  import { Sheet, type SerializeOptions, type Annotation } from 'treb-data-model';
@@ -922,34 +923,54 @@ export class GridBase {
922
923
 
923
924
  protected SetValidationInternal(command: DataValidationCommand): void {
924
925
 
925
- let cell: Cell|undefined;
926
-
927
926
  const sheet = this.FindSheet(command.area);
928
-
929
- if (sheet) {
930
- cell = sheet.cells.GetCell(command.area, true);
927
+ if (!sheet) {
928
+ throw new Error('invalid sheet in set validation');
931
929
  }
932
930
 
933
- if (!cell) {
934
- throw new Error('invalid cell in set validation');
935
- }
931
+ const target = {start: command.area.start, end: command.area.end };
936
932
 
937
933
  if (command.range) {
934
+
935
+ sheet.AddValidation({
936
+ type: 'range',
937
+ error: !!command.error,
938
+ area: command.range,
939
+ target: [target],
940
+ });
941
+
942
+ /*
938
943
  cell.validation = {
939
944
  type: ValidationType.Range,
940
945
  area: command.range,
941
946
  error: !!command.error,
942
947
  };
948
+ */
949
+
943
950
  }
944
951
  else if (command.list) {
952
+
953
+ sheet.AddValidation({
954
+ type: 'list',
955
+ error: !!command.error,
956
+ list: JSON.parse(JSON.stringify(command.list)),
957
+ target: [target],
958
+ });
959
+
960
+ /*
945
961
  cell.validation = {
946
962
  type: ValidationType.List,
947
963
  list: JSON.parse(JSON.stringify(command.list)),
948
964
  error: !!command.error,
949
965
  }
966
+ */
967
+
950
968
  }
951
969
  else {
952
- cell.validation = undefined;
970
+ // cell.validation = undefined;
971
+
972
+ sheet.RemoveValidations(target);
973
+
953
974
  }
954
975
 
955
976
  }
@@ -2794,8 +2815,54 @@ export class GridBase {
2794
2815
  /**
2795
2816
  * patch sheet conditionals for insert/delete row/column operations.
2796
2817
  * some of them may be deleted.
2818
+ *
2819
+ * UPDATE: using this routine to also patch data validations
2797
2820
  */
2798
- protected PatchConditionals(options: PatchOptions) {
2821
+ protected PatchConditionalsAndValidations(options: PatchOptions) {
2822
+
2823
+ if (options.sheet.data_validation.length) {
2824
+
2825
+ const delete_list: Set<DataValidation> = new Set();
2826
+ for (const validation of options.sheet.data_validation) {
2827
+
2828
+ const targets: IArea[] = [];
2829
+ for (const area of validation.target) {
2830
+ const updated = Area.PatchArea(area, options);
2831
+ if (updated) {
2832
+ targets.push(updated);
2833
+ }
2834
+ }
2835
+
2836
+ if (targets.length > 0) {
2837
+
2838
+ validation.target = targets;
2839
+
2840
+ // format the range, if necessary
2841
+
2842
+ if (validation.type === 'range') {
2843
+ if (validation.area.start.sheet_id === options.sheet.id) {
2844
+ const updated = Area.PatchArea(validation.area, options);
2845
+ if (updated) {
2846
+ validation.area = updated;
2847
+ }
2848
+ else {
2849
+ delete_list.add(validation);
2850
+ }
2851
+ }
2852
+ }
2853
+
2854
+ }
2855
+ else {
2856
+ delete_list.add(validation);
2857
+ }
2858
+
2859
+ }
2860
+
2861
+ if (delete_list.size) {
2862
+ options.sheet.data_validation = options.sheet.data_validation.filter(test => !delete_list.has(test));
2863
+ }
2864
+
2865
+ }
2799
2866
 
2800
2867
  if (options.sheet.conditional_formats?.length) {
2801
2868
 
@@ -2869,7 +2936,7 @@ export class GridBase {
2869
2936
  }
2870
2937
 
2871
2938
  // conditionals
2872
- this.PatchConditionals({
2939
+ this.PatchConditionalsAndValidations({
2873
2940
  sheet: target_sheet,
2874
2941
  before_column: 0,
2875
2942
  column_count: 0,
@@ -3217,7 +3284,7 @@ export class GridBase {
3217
3284
  }
3218
3285
 
3219
3286
  // conditionals
3220
- this.PatchConditionals({
3287
+ this.PatchConditionalsAndValidations({
3221
3288
  sheet: target_sheet,
3222
3289
  before_column: command.before_column,
3223
3290
  column_count: command.count,
@@ -4029,8 +4096,8 @@ export class GridBase {
4029
4096
  // COEDITING: ok
4030
4097
 
4031
4098
  this.SetValidationInternal(command);
4032
- if (!command.area.sheet_id || command.area.sheet_id === this.active_sheet.id) {
4033
- flags.render_area = Area.Join(new Area(command.area), flags.render_area);
4099
+ if (!command.area.start.sheet_id || command.area.start.sheet_id === this.active_sheet.id) {
4100
+ flags.render_area = Area.Join(new Area(command.area.start, command.area.end), flags.render_area);
4034
4101
  }
4035
4102
  break;
4036
4103
 
@@ -225,7 +225,8 @@ export interface SetNameCommand {
225
225
 
226
226
  export interface DataValidationCommand {
227
227
  key: CommandKey.DataValidation;
228
- area: ICellAddress;
228
+ area: IArea;
229
+
229
230
  range?: IArea;
230
231
  list?: CellValue[];
231
232
  error?: boolean;
@@ -409,7 +409,7 @@ export class Parser {
409
409
  for (let i = 0; i < n; i++) {
410
410
  transposed[i] = [];
411
411
  for (let j = 0; j < m; j++) {
412
- transposed[i][j] = arr[j][i];
412
+ transposed[i][j] = arr[j] ? arr[j][i] : undefined;
413
413
  }
414
414
  }
415
415
 
@@ -2415,8 +2415,11 @@ export class Parser {
2415
2415
  if (!c) return null;
2416
2416
  position = c.position;
2417
2417
 
2418
+ // things that look like an address but have row 0 are legal
2419
+ // as names. so this should be a token if r === 0.
2420
+
2418
2421
  const r = this.ConsumeAddressRow(position);
2419
- if (!r) return null;
2422
+ if (!r) return null;
2420
2423
  position = r.position;
2421
2424
 
2422
2425
  const label = sheet ?
@@ -2460,7 +2463,11 @@ export class Parser {
2460
2463
 
2461
2464
  /**
2462
2465
  * consumes a row, possibly absolute ($). returns the numeric row
2463
- * (0-based) and metadata
2466
+ * (0-based) and metadata.
2467
+ *
2468
+ * note that something like "X0" is a legal token, because 0 is not
2469
+ * a valid row. but at the same time it can't have a $ in it. although
2470
+ * maybe "X$0" is a token but not a valid name? dunno
2464
2471
  */
2465
2472
  protected ConsumeAddressRow(position: number):
2466
2473
  {
@@ -2484,7 +2491,18 @@ export class Parser {
2484
2491
  else break;
2485
2492
  }
2486
2493
 
2487
- if (start === position) return false;
2494
+ if (start === position) {
2495
+ return false;
2496
+ }
2497
+
2498
+ // handle token X0. should ~maybe~ handle this only if !absolute
2499
+ // temp leaving this separate from the above test just so it's clear
2500
+ // what we are doing
2501
+
2502
+ if (value === 0) {
2503
+ return false;
2504
+ }
2505
+
2488
2506
  return { absolute, row: value - 1, position };
2489
2507
  }
2490
2508