@trebco/treb 28.11.1 → 28.13.2

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.
@@ -309,6 +309,17 @@ export const Equals = (a: UnionValue, b: UnionValue): UnionValue => {
309
309
 
310
310
  }
311
311
 
312
+ // this is standard (icase) string equality. we might also need
313
+ // to handle wildcard string matching, although it's not the
314
+ // default case for = operators.
315
+
316
+ if (a.type === ValueType.string && b.type === ValueType.string) {
317
+ return {
318
+ type: ValueType.boolean,
319
+ value: a.value.toLowerCase() === b.value.toLowerCase(),
320
+ };
321
+ }
322
+
312
323
  return { type: ValueType.boolean, value: a.value == b.value }; // note ==
313
324
  };
314
325
 
@@ -302,3 +302,58 @@ export const ApplyAsArray2 = (base: (a: any, b: any, ...rest: any[]) => UnionVal
302
302
  }
303
303
  };
304
304
 
305
+
306
+ /**
307
+ * parse a string with wildcards into a regex pattern
308
+ *
309
+ * from
310
+ * https://exceljet.net/glossary/wildcard
311
+ *
312
+ * Excel has 3 wildcards you can use in your formulas:
313
+ *
314
+ * Asterisk (*) - zero or more characters
315
+ * Question mark (?) - any one character
316
+ * Tilde (~) - escape for literal character (~*) a literal question mark (~?), or a literal tilde (~~)
317
+ *
318
+ * they're pretty liberal with escaping, nothing is an error, just roll with it
319
+ *
320
+ */
321
+ export const ParseWildcards = (text: string): string => {
322
+
323
+ const result: string[] = [];
324
+ const length = text.length;
325
+
326
+ const escaped_chars = '[\\^$.|?*+()';
327
+
328
+ for (let i = 0; i < length; i++) {
329
+ let char = text[i];
330
+ switch (char) {
331
+
332
+ case '*':
333
+ result.push('.', '*');
334
+ break;
335
+
336
+ case '?':
337
+ result.push('.');
338
+ break;
339
+
340
+ case '~':
341
+ char = text[++i] || '';
342
+
343
+ // eslint-disable-next-line no-fallthrough
344
+ default:
345
+ for (let j = 0; j < escaped_chars.length; j++) {
346
+ if (char === escaped_chars[j]) {
347
+ result.push('\\');
348
+ break;
349
+ }
350
+ }
351
+ result.push(char);
352
+ break;
353
+
354
+ }
355
+ }
356
+
357
+ return result.join('');
358
+
359
+ };
@@ -42,6 +42,8 @@ import type {
42
42
  CondifionalFormatExpressionOptions,
43
43
  ConditionalFormatCellMatchOptions,
44
44
  ConditionalFormatCellMatch,
45
+ MacroFunction,
46
+ SerializedNamedExpression,
45
47
  } from 'treb-grid';
46
48
 
47
49
  import {
@@ -413,7 +415,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
413
415
  }
414
416
 
415
417
  /** for destruction */
416
- protected view?: HTMLElement;
418
+ protected view_node?: HTMLElement;
417
419
 
418
420
  /** for destruction */
419
421
  protected key_listener?: (event: KeyboardEvent) => void;
@@ -844,7 +846,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
844
846
  // elements. but we don't add a default; rather we use a template
845
847
 
846
848
  const template = container.querySelector('.treb-view-template') as HTMLTemplateElement;
847
- this.view = template.content.firstElementChild?.cloneNode(true) as HTMLElement;
849
+ this.view_node = template.content.firstElementChild?.cloneNode(true) as HTMLElement;
848
850
 
849
851
  // this is a little weird but we're inserting at the front. the
850
852
  // reason for this is that we only want to use one resize handle,
@@ -852,13 +854,13 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
852
854
 
853
855
  // we could work around this, really we're just being lazy.
854
856
 
855
- container.prepend(this.view);
857
+ container.prepend(this.view_node);
856
858
 
857
859
 
858
860
  // this.node = container;
859
861
  // this.node = this.view;
860
862
 
861
- this.view.addEventListener('focusin', () => {
863
+ this.view_node.addEventListener('focusin', () => {
862
864
  if (this.focus_target !== this) {
863
865
  this.Publish({ type: 'focus-view' });
864
866
  this.focus_target = this;
@@ -878,14 +880,14 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
878
880
 
879
881
  // const view = container.querySelector('.treb-view') as HTMLElement;
880
882
 
881
- this.grid.Initialize(this.view, toll_initial_render);
883
+ this.grid.Initialize(this.view_node, toll_initial_render);
882
884
 
883
885
  // dnd
884
886
 
885
887
  if (this.options.dnd) {
886
- this.view.addEventListener('dragenter', (event) => this.HandleDrag(event));
887
- this.view.addEventListener('dragover', (event) => this.HandleDrag(event));
888
- this.view.addEventListener('drop', (event) => this.HandleDrop(event));
888
+ this.view_node.addEventListener('dragenter', (event) => this.HandleDrag(event));
889
+ this.view_node.addEventListener('dragover', (event) => this.HandleDrag(event));
890
+ this.view_node.addEventListener('drop', (event) => this.HandleDrop(event));
889
891
  }
890
892
 
891
893
  // set up grid events
@@ -1244,19 +1246,19 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
1244
1246
  sheet.grid.grid_events.CancelAll();
1245
1247
  sheet.events.CancelAll();
1246
1248
 
1247
- if (sheet.view?.parentElement) {
1249
+ if (sheet.view_node?.parentElement) {
1248
1250
 
1249
1251
  // remove listener
1250
1252
  if (sheet.key_listener) {
1251
- sheet.view.parentElement.removeEventListener('keydown', sheet.key_listener);
1253
+ sheet.view_node.parentElement.removeEventListener('keydown', sheet.key_listener);
1252
1254
  }
1253
1255
 
1254
1256
  // remove node
1255
- sheet.view.parentElement.removeChild(sheet.view);
1257
+ sheet.view_node.parentElement.removeChild(sheet.view_node);
1256
1258
  }
1257
1259
 
1258
1260
  // in case other view was focused
1259
- this.view?.focus();
1261
+ this.view_node?.focus();
1260
1262
 
1261
1263
  // usually this results in us getting larger, we may need to update
1262
1264
  this.Resize();
@@ -1285,7 +1287,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
1285
1287
  const view = this.CreateView();
1286
1288
  view.grid.EnsureActiveSheet(true);
1287
1289
 
1288
- view.view?.addEventListener('focusin', () => {
1290
+ view.view_node?.addEventListener('focusin', () => {
1289
1291
  if (this.focus_target !== view) {
1290
1292
  this.Publish({ type: 'focus-view' });
1291
1293
  this.focus_target = view;
@@ -2244,12 +2246,16 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2244
2246
  this.parser.flags.r1c1 = r1c1;
2245
2247
 
2246
2248
  if (argument_separator === ',') {
2247
- this.parser.argument_separator = ArgumentSeparatorType.Comma;
2248
- this.parser.decimal_mark = DecimalMarkType.Period;
2249
+ this.parser.SetLocaleSettings(DecimalMarkType.Period);
2250
+
2251
+ // this.parser.argument_separator = ArgumentSeparatorType.Comma;
2252
+ // this.parser.decimal_mark = DecimalMarkType.Period;
2249
2253
  }
2250
2254
  else {
2251
- this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
2252
- this.parser.decimal_mark = DecimalMarkType.Comma;
2255
+ this.parser.SetLocaleSettings(DecimalMarkType.Comma);
2256
+
2257
+ // this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
2258
+ // this.parser.decimal_mark = DecimalMarkType.Comma;
2253
2259
  }
2254
2260
 
2255
2261
  const result = this.parser.Parse(formula);
@@ -2354,12 +2360,16 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2354
2360
  */
2355
2361
 
2356
2362
  if (argument_separator === ',') {
2357
- this.parser.argument_separator = ArgumentSeparatorType.Comma;
2358
- this.parser.decimal_mark = DecimalMarkType.Period;
2363
+ this.parser.SetLocaleSettings(DecimalMarkType.Period);
2364
+
2365
+ // this.parser.argument_separator = ArgumentSeparatorType.Comma;
2366
+ // this.parser.decimal_mark = DecimalMarkType.Period;
2359
2367
  }
2360
2368
  else {
2361
- this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
2362
- this.parser.decimal_mark = DecimalMarkType.Comma;
2369
+ this.parser.SetLocaleSettings(DecimalMarkType.Comma);
2370
+
2371
+ // this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
2372
+ // this.parser.decimal_mark = DecimalMarkType.Comma;
2363
2373
  }
2364
2374
 
2365
2375
  // const r1c1_state = this.parser.flags.r1c1;
@@ -2749,13 +2759,14 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2749
2759
 
2750
2760
  // FIXME: type
2751
2761
 
2752
- const serialized: SerializedModel = this.grid.Serialize({
2762
+ const serialized: SerializedModel = this.Serialize({ // this.grid.Serialize({
2753
2763
  rendered_values: true,
2754
2764
  expand_arrays: true,
2755
2765
  export_colors: true,
2756
2766
  decorated_cells: true,
2757
2767
  tables: true,
2758
2768
  share_resources: false,
2769
+ export_functions: true,
2759
2770
  });
2760
2771
 
2761
2772
  // why do _we_ put this in, instead of the grid method?
@@ -3523,6 +3534,9 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
3523
3534
  /**
3524
3535
  * Create a macro function.
3525
3536
  *
3537
+ * FIXME: this needs a control for argument separator, like other
3538
+ * functions that use formulas (@see SetRange)
3539
+ *
3526
3540
  * @public
3527
3541
  */
3528
3542
  public DefineFunction(name: string, argument_names: string | string[] = '', function_def = '0'): void {
@@ -3592,7 +3606,8 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
3592
3606
  ...options,
3593
3607
  };
3594
3608
 
3595
- const grid_data = this.grid.Serialize(options);
3609
+ // const grid_data = this.grid.Serialize(options);
3610
+ const grid_data = this.Serialize(options);
3596
3611
 
3597
3612
  // NOTE: these are not really env vars. we replace them at build time
3598
3613
  // via a webpack plugin. using the env syntax lets them look "real" at
@@ -4405,6 +4420,121 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
4405
4420
 
4406
4421
  // --- internal (protected) methods ------------------------------------------
4407
4422
 
4423
+ // --- moved from grid/grid base ---------------------------------------------
4424
+
4425
+
4426
+ /**
4427
+ * serialize data model. moved from grid/grid base. this is moved so we
4428
+ * have access to the calculator, which we want so we can do function
4429
+ * translation on some new functions that don't necessarily map 1:1 to
4430
+ * XLSX functions. we can also do cleanup on functions where we're less
4431
+ * strict about arguments (ROUND, for example).
4432
+ *
4433
+ */
4434
+ protected Serialize(options: SerializeOptions = {}): SerializedModel {
4435
+
4436
+ const active_sheet = this.grid.active_sheet; // I thought this was in view? (...)
4437
+
4438
+ active_sheet.selection = JSON.parse(JSON.stringify(this.grid.GetSelection()));
4439
+
4440
+ // same for scroll offset
4441
+
4442
+ const scroll_offset = this.grid.ScrollOffset();
4443
+ if (scroll_offset) {
4444
+ active_sheet.scroll_offset = scroll_offset; // this.grid.layout.scroll_offset;
4445
+ }
4446
+
4447
+ // NOTE: annotations moved to sheets, they will be serialized in the sheets
4448
+
4449
+ const sheet_data = this.model.sheets.list.map((sheet) => sheet.toJSON(options));
4450
+
4451
+ // OK, not serializing tables in cells anymore. old comment about this:
4452
+ //
4453
+ // at the moment, tables are being serialized in cells. if we put them
4454
+ // in here, then we have two records of the same data. that would be bad.
4455
+ // I think this is probably the correct place, but if we put them here
4456
+ // we need to stop serializing in cells. and I'm not sure that there are
4457
+ // not some side-effects to that. hopefully not, but (...)
4458
+ //
4459
+
4460
+ let tables: Table[] | undefined;
4461
+ if (this.model.tables.size > 0) {
4462
+ tables = Array.from(this.model.tables.values());
4463
+ }
4464
+
4465
+ // NOTE: moving into a structured object (the sheet data is also structured,
4466
+ // of course) but we are moving things out of sheet (just named ranges atm))
4467
+
4468
+ let macro_functions: MacroFunction[] | undefined;
4469
+
4470
+ if (this.model.macro_functions.size) {
4471
+ macro_functions = [];
4472
+ for (const macro of this.model.macro_functions.values()) {
4473
+ macro_functions.push({
4474
+ ...macro,
4475
+ expression: undefined,
4476
+ });
4477
+ }
4478
+ }
4479
+
4480
+ // when serializing named expressions, we have to make sure
4481
+ // that there's a sheet name in any address/range.
4482
+
4483
+ const named_expressions: SerializedNamedExpression[] = [];
4484
+ if (this.model.named_expressions) {
4485
+
4486
+ for (const [name, expr] of this.model.named_expressions) {
4487
+ this.parser.Walk(expr, unit => {
4488
+ if (unit.type === 'address' || unit.type === 'range') {
4489
+ const test = unit.type === 'range' ? unit.start : unit;
4490
+
4491
+ test.absolute_column = test.absolute_row = true;
4492
+
4493
+ if (!test.sheet) {
4494
+ if (test.sheet_id) {
4495
+ const sheet = this.model.sheets.Find(test.sheet_id);
4496
+ if (sheet) {
4497
+ test.sheet = sheet.name;
4498
+ }
4499
+ }
4500
+ if (!test.sheet) {
4501
+ test.sheet = active_sheet.name;
4502
+ }
4503
+ }
4504
+
4505
+ if (unit.type === 'range') {
4506
+ unit.end.absolute_column = unit.end.absolute_row = true;
4507
+ }
4508
+
4509
+ return false;
4510
+ }
4511
+ else if (unit.type === 'call' && options.export_functions) {
4512
+ // ...
4513
+ }
4514
+ return true;
4515
+ });
4516
+ const rendered = this.parser.Render(expr, { missing: '' });
4517
+ named_expressions.push({
4518
+ name, expression: rendered
4519
+ });
4520
+ }
4521
+ }
4522
+
4523
+ return {
4524
+ sheet_data,
4525
+ active_sheet: active_sheet.id,
4526
+ named_ranges: this.model.named_ranges.Count() ?
4527
+ this.model.named_ranges.Serialize() :
4528
+ undefined,
4529
+ macro_functions,
4530
+ tables,
4531
+ named_expressions: named_expressions.length ? named_expressions : undefined,
4532
+ };
4533
+
4534
+ }
4535
+
4536
+ // --- /moved ----------------------------------------------------------------
4537
+
4408
4538
  /**
4409
4539
  *
4410
4540
  */
@@ -5531,14 +5661,18 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
5531
5661
  // FIXME: also we should unify on types for decimal, argument separator
5532
5662
 
5533
5663
  if (data.decimal_mark === '.') {
5534
- parser.decimal_mark = DecimalMarkType.Period;
5535
- parser.argument_separator = ArgumentSeparatorType.Comma;
5664
+ // parser.decimal_mark = DecimalMarkType.Period;
5665
+ // parser.argument_separator = ArgumentSeparatorType.Comma;
5666
+ parser.SetLocaleSettings(DecimalMarkType.Period);
5667
+
5536
5668
  target_decimal_mark = DecimalMarkType.Comma;
5537
5669
  target_argument_separator = ArgumentSeparatorType.Semicolon;
5538
5670
  }
5539
5671
  else {
5540
- parser.decimal_mark = DecimalMarkType.Comma;
5541
- parser.argument_separator = ArgumentSeparatorType.Semicolon;
5672
+ // parser.decimal_mark = DecimalMarkType.Comma;
5673
+ // parser.argument_separator = ArgumentSeparatorType.Semicolon;
5674
+ parser.SetLocaleSettings(DecimalMarkType.Comma);
5675
+
5542
5676
  target_decimal_mark = DecimalMarkType.Period;
5543
5677
  target_argument_separator = ArgumentSeparatorType.Comma;
5544
5678
  }
@@ -33,6 +33,7 @@
33
33
  --treb-autocomplete-tooltip-color: #fff;
34
34
  --treb-chart-background: #000;
35
35
  --treb-chart-grid-color: #976;
36
+ --treb-chart-grid-zero-color: #fdd;
36
37
  --treb-chart-text-color: #fff;
37
38
  --treb-dialog-background: #000;
38
39
  --treb-dialog-color: #fff;
@@ -57,6 +57,7 @@ const ImportSheet = (data: any) => {
57
57
  results.sheets.push(result);
58
58
  }
59
59
  }
60
+
60
61
  ctx.postMessage({ status: 'complete', results });
61
62
 
62
63
  }
@@ -81,7 +81,10 @@ export class Importer {
81
81
  t?: string;
82
82
  s?: string;
83
83
  };
84
- v?: string|number; // is this never an object? (note: booleans are numbers in Excel)
84
+ v?: string|number|{
85
+ t$: string;
86
+ a$?: any;
87
+ };
85
88
  f?: string|{
86
89
  t$: string;
87
90
  a$?: {
@@ -214,14 +217,17 @@ export class Importer {
214
217
  }
215
218
 
216
219
  if (typeof element.v !== 'undefined') {
217
- const num = Number(element.v.toString());
220
+
221
+ const V = (typeof element.v === 'object') ? element.v?.t$ : element.v;
222
+
223
+ const num = Number(V.toString());
218
224
  if (!isNaN(num)) {
219
225
  calculated_type = 'number'; // ValueType.number;
220
226
  calculated_value = num;
221
227
  }
222
228
  else {
223
229
  calculated_type = 'string'; // ValueType.string;
224
- calculated_value = element.v.toString();
230
+ calculated_value = V.toString();
225
231
  }
226
232
  }
227
233
 
@@ -29,6 +29,9 @@ import { XMLUtils } from './xml-utils';
29
29
 
30
30
  import { Unescape } from './unescape_xml';
31
31
 
32
+ // what's the default font size? ... 11pt?
33
+ const DEFAULT_FONT_SIZE = 11;
34
+
32
35
  export interface Font {
33
36
  size?: number;
34
37
  name?: string;
@@ -269,13 +272,24 @@ export class StyleCache {
269
272
 
270
273
  }
271
274
 
275
+
272
276
  if (composite.font_size?.unit && composite.font_size.value) {
273
- if (composite.font_size.unit !== 'pt') {
274
- console.warn(`can't handle non-point font (FIXME)`);
277
+ if (composite.font_size.unit === 'em') {
278
+ font.size = composite.font_size.value * DEFAULT_FONT_SIZE;
275
279
  }
276
- else {
280
+ else if (composite.font_size.unit === '%') {
281
+ font.size = composite.font_size.value * DEFAULT_FONT_SIZE / 100;
282
+ }
283
+ else if (composite.font_size.unit === 'pt' ){
277
284
  font.size = composite.font_size.value;
278
285
  }
286
+ else if (composite.font_size.unit === 'px' ){
287
+ font.size = composite.font_size.value * .75; // ?
288
+ }
289
+ else {
290
+ console.warn(`Unhandled font size unit`, composite.font_size);
291
+ }
292
+
279
293
  }
280
294
 
281
295
  if (composite.bold) font.bold = true;
@@ -657,6 +671,19 @@ export class StyleCache {
657
671
 
658
672
  // borders
659
673
 
674
+ const BorderEdgeToColor = (edge: BorderEdge): Color|undefined => {
675
+
676
+ // TODO: indexed
677
+
678
+ if (typeof edge.theme !== 'undefined') {
679
+ return {
680
+ theme: edge.theme,
681
+ tint: edge.tint,
682
+ };
683
+ }
684
+
685
+ };
686
+
660
687
  const border = this.borders[xf.border || 0];
661
688
  if (border) {
662
689
  if (border.bottom.style) {
@@ -666,10 +693,20 @@ export class StyleCache {
666
693
  else {
667
694
  props.border_bottom = 1;
668
695
  }
696
+ props.border_bottom_fill = BorderEdgeToColor(border.bottom);
697
+ }
698
+ if (border.left.style) {
699
+ props.border_left = 1;
700
+ props.border_left_fill = BorderEdgeToColor(border.left);
701
+ }
702
+ if (border.top.style) {
703
+ props.border_top = 1;
704
+ props.border_top_fill = BorderEdgeToColor(border.top);
705
+ }
706
+ if (border.right.style) {
707
+ props.border_right = 1;
708
+ props.border_right_fill = BorderEdgeToColor(border.right);
669
709
  }
670
- if (border.left.style) props.border_left = 1;
671
- if (border.top.style) props.border_top = 1;
672
- if (border.right.style) props.border_right = 1;
673
710
  }
674
711
 
675
712
  return props;
@@ -1144,31 +1181,58 @@ export class StyleCache {
1144
1181
 
1145
1182
  composite = FindAll('styleSheet/borders/border');
1146
1183
 
1184
+ const ElementToBorderEdge = (element: any, edge: BorderEdge) => {
1185
+
1186
+ if (element?.a$) {
1187
+ edge.style = element.a$.style;
1188
+ if (typeof element.color === 'object') {
1189
+ if (typeof element.color.a$?.indexed !== 'undefined') {
1190
+ edge.color = Number(element.color.a$.indexed);
1191
+ }
1192
+ if (typeof element.color.a$?.theme !== 'undefined') {
1193
+ edge.theme = Number(element.color.a$.theme);
1194
+ }
1195
+ if (typeof element.color.a$?.tint !== 'undefined') {
1196
+ edge.tint = Number(element.color.a$.tint);
1197
+ }
1198
+ }
1199
+ }
1200
+
1201
+ };
1202
+
1203
+
1147
1204
  this.borders = composite.map(element => {
1148
1205
 
1149
1206
  const border: BorderStyle = JSON.parse(JSON.stringify(default_border));
1150
1207
 
1208
+ /*
1151
1209
  // we're relying on these being empty strings -> falsy, not a good look
1152
1210
 
1153
1211
  if (element.left) {
1154
- border.left.style = element.left.a$.style;
1155
- border.left.color = Number(element.left.color?.a$?.indexed);
1212
+ // border.left.style = element.left.a$.style;
1213
+ // border.left.color = Number(element.left.color?.a$?.indexed);
1156
1214
  }
1157
1215
 
1158
1216
  if (element.right) {
1159
- border.right.style = element.right.a$.style;
1160
- border.right.color = Number(element.right.color?.a$?.indexed);
1217
+ // border.right.style = element.right.a$.style;
1218
+ // border.right.color = Number(element.right.color?.a$?.indexed);
1161
1219
  }
1162
1220
 
1163
1221
  if (element.top) {
1164
- border.top.style = element.top.a$.style;
1165
- border.top.color = Number(element.top.color?.a$?.indexed);
1222
+ // border.top.style = element.top.a$.style;
1223
+ // border.top.color = Number(element.top.color?.a$?.indexed);
1166
1224
  }
1167
1225
 
1168
1226
  if (element.bottom) {
1169
- border.bottom.style = element.bottom.a$.style;
1170
- border.bottom.color = Number(element.bottom.color?.a$?.indexed);
1227
+ // border.bottom.style = element.bottom.a$.style;
1228
+ // border.bottom.color = Number(element.bottom.color?.a$?.indexed);
1171
1229
  }
1230
+ */
1231
+
1232
+ ElementToBorderEdge(element.left, border.left);
1233
+ ElementToBorderEdge(element.right, border.right);
1234
+ ElementToBorderEdge(element.top, border.top);
1235
+ ElementToBorderEdge(element.bottom, border.bottom);
1172
1236
 
1173
1237
  return border;
1174
1238
 
@@ -789,6 +789,13 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
789
789
 
790
790
  if (this.active_editor && !this.assume_formula) {
791
791
  this.active_editor.node.spellcheck = !(this.text_formula);
792
+
793
+ // if not assuming formula, and it's not a formula, then exit.
794
+
795
+ if (!this.text_formula) {
796
+ return;
797
+ }
798
+
792
799
  }
793
800
 
794
801
  // this is a short-circuit so we don't format the same text twice.
@@ -835,9 +835,9 @@ export class Grid extends GridBase {
835
835
  }
836
836
  }
837
837
 
838
- /**
838
+ /* *
839
839
  * specialization: update selection, scroll offset
840
- */
840
+ * /
841
841
  public Serialize(options: SerializeOptions = {}): SerializedModel {
842
842
 
843
843
  // selection moved to sheet, but it's not "live"; so we need to
@@ -855,6 +855,7 @@ export class Grid extends GridBase {
855
855
  return super.Serialize(options);
856
856
 
857
857
  }
858
+ */
858
859
 
859
860
  /**
860
861
  * show or hide headers
@@ -1830,18 +1831,23 @@ export class Grid extends GridBase {
1830
1831
  argument_separator: this.parser.argument_separator,
1831
1832
  decimal_mark: this.parser.decimal_mark,
1832
1833
  }
1834
+
1835
+ this.parser.Save();
1836
+
1833
1837
  let convert = false;
1834
1838
 
1835
1839
  if (options.argument_separator === ',' && this.parser.argument_separator !== ArgumentSeparatorType.Comma) {
1836
- this.parser.argument_separator = ArgumentSeparatorType.Comma;
1837
- this.parser.decimal_mark = DecimalMarkType.Period;
1840
+ // this.parser.argument_separator = ArgumentSeparatorType.Comma;
1841
+ // this.parser.decimal_mark = DecimalMarkType.Period;
1842
+ this.parser.SetLocaleSettings(DecimalMarkType.Period);
1838
1843
 
1839
1844
  convert = true;
1840
1845
  }
1841
1846
 
1842
1847
  if (options.argument_separator === ';' && this.parser.argument_separator !== ArgumentSeparatorType.Semicolon) {
1843
- this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
1844
- this.parser.decimal_mark = DecimalMarkType.Comma;
1848
+ // this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
1849
+ // this.parser.decimal_mark = DecimalMarkType.Comma;
1850
+ this.parser.SetLocaleSettings(DecimalMarkType.Comma);
1845
1851
 
1846
1852
  convert = true;
1847
1853
  }
@@ -1882,8 +1888,10 @@ export class Grid extends GridBase {
1882
1888
 
1883
1889
  // reset
1884
1890
 
1885
- this.parser.argument_separator = current.argument_separator;
1886
- this.parser.decimal_mark = current.decimal_mark;
1891
+ // this.parser.argument_separator = current.argument_separator;
1892
+ // this.parser.decimal_mark = current.decimal_mark;
1893
+
1894
+ this.parser.Restore();
1887
1895
 
1888
1896
  }
1889
1897
 
@@ -4439,7 +4447,10 @@ export class Grid extends GridBase {
4439
4447
 
4440
4448
  if (this.overlay_editor?.selection) {
4441
4449
  const value = this.overlay_editor?.edit_node.textContent || undefined;
4442
- const array = (event.key === 'Enter' && event.ctrlKey && event.shiftKey);
4450
+
4451
+ // let's support command+shift+enter on mac
4452
+ const array = (event.key === 'Enter' && (event.ctrlKey || (UA.is_mac && event.metaKey)) && event.shiftKey);
4453
+
4443
4454
  this.SetInferredType(this.overlay_editor.selection, value, array);
4444
4455
  }
4445
4456