@trebco/treb 29.5.2 → 29.6.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.
@@ -73,7 +73,7 @@ import type {
73
73
 
74
74
  import {
75
75
  IsArea, ThemeColorTable, ComplexToString, Rectangle, IsComplex, type CellStyle,
76
- Localization, Style, type Color, ResolveThemeColor, IsCellAddress, Area, IsFlatData, IsFlatDataArray, Gradient, DOMContext,
76
+ Localization, Style, type Color, ResolveThemeColor, IsCellAddress, Area, IsFlatData, IsFlatDataArray, Gradient, DOMContext
77
77
  } from 'treb-base-types';
78
78
 
79
79
  import { EventSource, ValidateURI } from 'treb-utils';
@@ -114,6 +114,60 @@ import * as export_worker_script from 'worker:../../treb-export/src/export-worke
114
114
 
115
115
  // --- types -------------------------------------------------------------------
116
116
 
117
+ /**
118
+ * this is a structure for copy/paste data. clipboard data may include
119
+ * relative formauls and resolved styles, so it's suitable for pasting into
120
+ * other areas of the spreadsheet.
121
+ *
122
+ * @privateRemarks
123
+ * work in progress. atm we're not using the system clipboard, although it
124
+ * might be useful to merge this with grid copy/paste routines in the future.
125
+ *
126
+ * if it hits the clipboard this should use mime type `application/x-treb-data`
127
+ *
128
+ */
129
+ export interface ClipboardDataElement {
130
+
131
+ /** calculated cell value */
132
+ calculated: CellValue,
133
+
134
+ /** the actual cell value or formula */
135
+ value: CellValue,
136
+
137
+ /** cell style. this may include row/column styles from the copy source */
138
+ style?: CellStyle,
139
+
140
+ }
141
+
142
+ /** clipboard data is a 2d array */
143
+ export type ClipboardData = ClipboardDataElement[][];
144
+
145
+ /**
146
+ * optional paste options. we can paste formulas or values, and we
147
+ * can use the source style, target style, or just use the source
148
+ * number formats.
149
+ */
150
+ export interface PasteOptions {
151
+
152
+ /**
153
+ * when clipboard data includes formulas, optionally paste calculated
154
+ * values instead of the original formulas. defaults to false.
155
+ */
156
+ values?: boolean;
157
+
158
+ /**
159
+ * when pasting data from the clipboard, we can copy formatting/style
160
+ * from the original data, or we can retain the target range formatting
161
+ * and just paste data. a third option allows pasting source number
162
+ * formats but dropping other style information.
163
+ *
164
+ * defaults to "source", meaning paste source styles.
165
+ */
166
+
167
+ formatting?: 'source'|'target'|'number-formats'
168
+
169
+ }
170
+
117
171
  /**
118
172
  * options for saving files. we add the option for JSON formatting.
119
173
  */
@@ -264,6 +318,9 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
264
318
  /** @internal */
265
319
  public static one_time_warnings: Record<string, boolean> = {};
266
320
 
321
+ /** @internal */
322
+ protected static clipboard?: ClipboardData;
323
+
267
324
  protected DOM = DOMContext.GetInstance(); // default
268
325
 
269
326
  /**
@@ -2009,7 +2066,25 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2009
2066
  * grid stops broadcasting events for the duration of the function call,
2010
2067
  * and collects them instead. After the function call we update as necessary.
2011
2068
  *
2012
- * @public
2069
+ * @privateRemarks
2070
+ *
2071
+ * FIXME: we need to consider the case where this is nested, since we now
2072
+ * call it from the Paste method. that might be batched by a caller. we
2073
+ * need a batch stack, and we need to consolidate any options (paint is the
2074
+ * only option) and keep the top-of-stack last selection.
2075
+ *
2076
+ * Q: why does this work the way it does, anyway? why not just rely
2077
+ * on the actual events? if grid published them after a batch, wouldn't
2078
+ * everything just work? or is the problem that we normally handle events
2079
+ * serially? maybe we need to rethink how we handle events coming from
2080
+ * the grid.
2081
+ *
2082
+ * ---
2083
+ *
2084
+ * OK so now, grid will handle nested batching. it won't return anything
2085
+ * until the last batch is complete. so this should work in the case of
2086
+ * nested calls, because nothing will happen until the last one is complete.
2087
+ *
2013
2088
  */
2014
2089
  public async Batch(func: () => void, paint = false): Promise<void> {
2015
2090
 
@@ -2021,6 +2096,8 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2021
2096
  let recalc = false;
2022
2097
  let reset = false;
2023
2098
 
2099
+ // FIXME (2024): are these FIXMEs below still a thing? (...)
2100
+
2024
2101
  // FIXME: annotation events
2025
2102
  // TODO: annotation events
2026
2103
 
@@ -2822,6 +2899,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2822
2899
  tables: true,
2823
2900
  share_resources: false,
2824
2901
  export_functions: true,
2902
+ apply_row_pattern: true, // if there's a row pattern, set it on rows so they export properly
2825
2903
  });
2826
2904
 
2827
2905
  // why do _we_ put this in, instead of the grid method?
@@ -4288,6 +4366,139 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
4288
4366
 
4289
4367
  }
4290
4368
 
4369
+ /**
4370
+ *
4371
+ * @param target - the target to paste data into. this can be larger
4372
+ * than the clipboard data, in which case values will be recycled in
4373
+ * blocks. if the target is smaller than the source data, we will expand
4374
+ * it.
4375
+ *
4376
+ * @param data - clipboard data to paste.
4377
+ *
4378
+ * @param style - optional paste style. default is to paste formulas and
4379
+ * source formatting. paste options can be usef to paste values, values
4380
+ * and number formats, or retain the target formatting.
4381
+ *
4382
+ * @privateRemarks LLM API
4383
+ */
4384
+ public async Paste(target?: RangeReference, data = EmbeddedSpreadsheet.clipboard, options: PasteOptions = {}) {
4385
+
4386
+ if (!data) {
4387
+ throw new Error('no clipboad data');
4388
+ }
4389
+
4390
+ if (!target) {
4391
+ const selection = this.GetSelectionReference();
4392
+ if (!selection.empty) {
4393
+ target = selection.area;
4394
+ }
4395
+ else {
4396
+ throw new Error('no range and no selection');
4397
+ }
4398
+ }
4399
+
4400
+ const resolved = this.model.ResolveArea(target, this.grid.active_sheet);
4401
+
4402
+ // paste has some special semantics. if the target smaller than the
4403
+ // source data, we write the full data irrespective of size (similar
4404
+ // to "spill"). otherwise, we recycle in blocks.
4405
+
4406
+ // the setrange method will recycle, but we also need to recycle styles.
4407
+
4408
+ // start with data length
4409
+
4410
+ const rows = data.length;
4411
+ const columns = data[0]?.length || 0;
4412
+
4413
+ // target -> block size
4414
+
4415
+ resolved.Resize(
4416
+ Math.max(1, Math.floor(resolved.rows / rows)) * rows,
4417
+ Math.max(1, Math.floor(resolved.columns / columns)) * columns );
4418
+
4419
+ const sheet = (resolved.start.sheet_id ? this.model.sheets.Find(resolved.start.sheet_id) : this.grid.active_sheet) || this.grid.active_sheet;
4420
+
4421
+ const values: CellValue[][] = [];
4422
+
4423
+ // optionally collect calculated values, instead of raw values
4424
+
4425
+ if (options.values) {
4426
+ for (const [index, row] of data.entries()) {
4427
+ values[index] = [];
4428
+ for (const cell of row) {
4429
+ values[index].push(cell.calculated);
4430
+ }
4431
+ }
4432
+ }
4433
+ else {
4434
+ for (const [index, row] of data.entries()) {
4435
+ values[index] = [];
4436
+ for (const cell of row) {
4437
+ values[index].push(cell.value);
4438
+ }
4439
+ }
4440
+ }
4441
+
4442
+ // batch to limit events, sync up undo
4443
+
4444
+ return this.Batch(() => {
4445
+
4446
+ this.grid.SetRange(resolved, values, {
4447
+ r1c1: true, recycle: true,
4448
+ });
4449
+
4450
+ if (options.formatting === 'number-formats') {
4451
+
4452
+ // number format only, and apply delta
4453
+
4454
+ for (const address of resolved) {
4455
+ const r = (address.row - resolved.start.row) % rows;
4456
+ const c = (address.column - resolved.start.column) % columns;
4457
+ const number_format = (data[r][c].style || {}).number_format;
4458
+ sheet.UpdateCellStyle(address, { number_format }, true);
4459
+ }
4460
+ }
4461
+ else if (options.formatting !== 'target') {
4462
+
4463
+ // use source formatting (default)
4464
+ for (const address of resolved) {
4465
+ const r = (address.row - resolved.start.row) % rows;
4466
+ const c = (address.column - resolved.start.column) % columns;
4467
+ sheet.UpdateCellStyle(address, data[r][c].style || {}, false);
4468
+ }
4469
+
4470
+ }
4471
+
4472
+ }, true);
4473
+
4474
+
4475
+ }
4476
+
4477
+ /**
4478
+ * copy data. this method returns the copied data. it does not put it on
4479
+ * the system clipboard. this is for API access when the system clipboard
4480
+ * might not be available.
4481
+ *
4482
+ * @privateRemarks LLM API
4483
+ */
4484
+ public Copy(source?: RangeReference): ClipboardData {
4485
+ return this.CopyInternal(source, 'copy');
4486
+ }
4487
+
4488
+ /**
4489
+ * cut data. this method returns the cut data. it does not put it on the
4490
+ * system clipboard. this method is similar to the Copy method, with
4491
+ * two differences: (1) we remove the source data, effectively clearing
4492
+ * the source range; and (2) the clipboard data retains references, meaning
4493
+ * if you paste the data in a different location it will refer to the same
4494
+ * cells.
4495
+ *
4496
+ * @privateRemarks LLM API
4497
+ */
4498
+ public Cut(source?: RangeReference): ClipboardData {
4499
+ return this.CopyInternal(source, 'cut');
4500
+ }
4501
+
4291
4502
  /**
4292
4503
  *
4293
4504
  * @param range target range. leave undefined to use current selection.
@@ -4440,6 +4651,67 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
4440
4651
 
4441
4652
  // --- internal (protected) methods ------------------------------------------
4442
4653
 
4654
+ /**
4655
+ * internal composite for cut/copy. mostly identical except we
4656
+ * read data as A1 for cut, so it will retain references. also
4657
+ * cut clears the data.
4658
+ *
4659
+ * FIXME: merge with grid cut/copy/paste routines. we already
4660
+ * handle recycling and relative addressing, the only thing missing
4661
+ * is alternate formats.
4662
+ */
4663
+ protected CopyInternal(source?: RangeReference, semantics: 'cut'|'copy' = 'copy'): ClipboardData {
4664
+
4665
+ if (!source) {
4666
+ const selection = this.GetSelectionReference();
4667
+ if (!selection.empty) {
4668
+ source = selection.area;
4669
+ }
4670
+ else {
4671
+ throw new Error('no range and no selection');
4672
+ }
4673
+ }
4674
+
4675
+ // resolve range so we can use it later -> Area
4676
+ const resolved = this.model.ResolveArea(source, this.grid.active_sheet);
4677
+ const sheet = (resolved.start.sheet_id ? this.model.sheets.Find(resolved.start.sheet_id) : this.grid.active_sheet) || this.grid.active_sheet;
4678
+
4679
+ // get cell data as R1C1 for copy but A1 for cut.
4680
+ const r1c1 = this.grid.GetRange(resolved, semantics === 'cut' ? 'A1' : 'R1C1') as CellValue[][];
4681
+
4682
+ // get style data, !apply theme but do apply r/c styles
4683
+ const styles = sheet.GetCellStyle(resolved, false);
4684
+
4685
+ // get calculated values
4686
+ let calculated = sheet.cells.GetRange(resolved.start, resolved.end);
4687
+ if (!Array.isArray(calculated)) {
4688
+ calculated = [[calculated]];
4689
+ }
4690
+
4691
+ const data: ClipboardData = [];
4692
+
4693
+ for (const [r, row] of r1c1.entries()) {
4694
+ data[r] = [];
4695
+ for (const [c, value] of row.entries()) {
4696
+
4697
+ data[r][c] = {
4698
+ value,
4699
+ calculated: calculated[r][c],
4700
+ style: styles[r]?.[c],
4701
+ };
4702
+ }
4703
+ }
4704
+
4705
+ EmbeddedSpreadsheet.clipboard = structuredClone(data);
4706
+
4707
+ if (semantics === 'cut') {
4708
+ this.grid.SetRange(resolved, undefined, { recycle: true }); // clear
4709
+ }
4710
+
4711
+ return data;
4712
+
4713
+ }
4714
+
4443
4715
  // --- moved from grid/grid base ---------------------------------------------
4444
4716
 
4445
4717
 
@@ -611,9 +611,13 @@ export class Exporter {
611
611
 
612
612
  const list: CellStyle[] = [sheet.sheet_style];
613
613
 
614
+ /*
615
+ // should apply to rows, not cells
616
+
614
617
  if (sheet.row_pattern && sheet.row_pattern.length) {
615
618
  list.push(sheet.row_pattern[row % sheet.row_pattern.length]);
616
619
  }
620
+ */
617
621
 
618
622
  // is this backwards, vis a vis our rendering? I think it might be...
619
623
  // YES: should be row pattern -> row -> column -> cell [corrected]
@@ -2176,21 +2176,40 @@ export class Grid extends GridBase {
2176
2176
  /**
2177
2177
  * batch updates. returns all the events that _would_ have been sent.
2178
2178
  * also does a paint (can disable).
2179
+ *
2180
+ * update for nesting/stacking. we won't return events until the last
2181
+ * batch is complete. paint will similarly toll until the last batch
2182
+ * is complete, and we'll carry forward any paint requirement from
2183
+ * inner batch funcs.
2184
+ *
2179
2185
  * @param func
2180
2186
  */
2181
2187
  public Batch(func: () => void, paint = true): GridEvent[] {
2182
2188
 
2183
- this.batch = true;
2189
+ if (this.batch === 0) {
2190
+ this.batch_paint = paint; // clear any old setting
2191
+ }
2192
+ else {
2193
+ this.batch_paint = this.batch_paint || paint; // ensure we honor it at the end
2194
+ }
2195
+
2196
+ this.batch++;
2184
2197
  func();
2185
- this.batch = false;
2186
- const events = this.batch_events.slice(0);
2187
- this.batch_events = [];
2198
+ this.batch--;
2199
+
2200
+ if (this.batch === 0) {
2201
+ const events = this.batch_events.slice(0);
2202
+ this.batch_events = [];
2188
2203
 
2189
- if (paint) {
2190
- this.DelayedRender(false);
2204
+ if (this.batch_paint) {
2205
+ this.DelayedRender(false);
2206
+ }
2207
+
2208
+ return events;
2191
2209
  }
2192
2210
 
2193
- return events;
2211
+ return []; // either nothing happened or we're not done
2212
+
2194
2213
  }
2195
2214
 
2196
2215
 
@@ -109,7 +109,19 @@ export class GridBase {
109
109
 
110
110
  // --- protected members -----------------------------------------------------
111
111
 
112
- protected batch = false;
112
+ /**
113
+ * switching to a stack, in case batching is nested. we don't need
114
+ * actual data so (atm) just count the depth.
115
+ */
116
+ protected batch = 0; // false;
117
+
118
+ /**
119
+ * if any batch method along the way requests a paint update, we
120
+ * want to toll it until the last batch call is complete, but we don't
121
+ * want to lose it. just remember to reset. [FIXME: isn't tolling this
122
+ * paint implicit, since it's async? ...]
123
+ */
124
+ protected batch_paint = false;
113
125
 
114
126
  protected batch_events: GridEvent[] = [];
115
127
 
@@ -4474,7 +4486,7 @@ export class GridBase {
4474
4486
  });
4475
4487
  }
4476
4488
 
4477
- if (this.batch) {
4489
+ if (this.batch > 0) {
4478
4490
  this.batch_events.push(...events);
4479
4491
  }
4480
4492
  else {