@dxos/lit-grid 0.6.10-main.3cfcc89 → 0.6.10-main.bbdfaa4

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.
package/src/dx-grid.ts CHANGED
@@ -2,56 +2,23 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { LitElement, html } from 'lit';
5
+ import { LitElement, html, type PropertyValues } from 'lit';
6
6
  import { customElement, state, property } from 'lit/decorators.js';
7
7
  import { ref, createRef, type Ref } from 'lit/directives/ref.js';
8
8
 
9
- /**
10
- * The size in pixels of the gap between cells
11
- */
12
- const gap = 1;
9
+ import { colToA1Notation, posFromNumericNotation, rowToA1Notation } from './position';
13
10
 
14
- /**
15
- * This should be about the width of the `1` numeral so resize is triggered as the row header column’s intrinsic size
16
- * changes when scrolling vertically.
17
- */
18
- const resizeTolerance = 8;
11
+ const colSize = 64;
19
12
 
20
- //
21
- // `overscan` is the number of columns or rows to render outside of the viewport
22
- //
23
- const overscanCol = 1;
24
- const overscanRow = 1;
13
+ const rowSize = 20;
25
14
 
26
- //
27
- // `size`, when suffixed with ‘row’ or ‘col’, are limits on size applied when resizing
28
- //
29
- const sizeColMin = 32;
30
- const sizeColMax = 1024;
31
- const sizeRowMin = 16;
32
- const sizeRowMax = 1024;
33
-
34
- /**
35
- * Separator for serializing cell position vectors
36
- */
37
- const separator = ',';
38
-
39
- //
40
- // A1 notation is the fallback for numbering columns and rows.
41
- //
42
-
43
- const colToA1Notation = (col: number): string => {
44
- return (
45
- (col >= 26 ? String.fromCharCode('A'.charCodeAt(0) + Math.floor(col / 26) - 1) : '') +
46
- String.fromCharCode('A'.charCodeAt(0) + (col % 26))
47
- );
48
- };
49
-
50
- const rowToA1Notation = (row: number): string => {
51
- return `${row + 1}`;
52
- };
15
+ const gap = 0;
53
16
 
54
17
  export type CellValue = {
18
+ /**
19
+ * The position (or topleft-most of the range) in numeric notation
20
+ */
21
+ pos: string;
55
22
  /**
56
23
  * The content value
57
24
  */
@@ -66,39 +33,10 @@ export type CellValue = {
66
33
  style?: string;
67
34
  };
68
35
 
69
- type AxisMeta = {
70
- size: number;
71
- description?: string;
72
- resizeable?: boolean;
73
- };
74
-
75
- export type DxGridProps = Pick<DxGrid, 'cells' | 'rows' | 'columns' | 'rowDefault' | 'columnDefault'>;
76
-
77
- const localChId = (c0: number) => `ch--${c0}`;
78
- const localRhId = (r0: number) => `rh--${r0}`;
79
-
80
- const getPage = (axis: string, event: PointerEvent) => (axis === 'col' ? event.pageX : event.pageY);
81
-
82
36
  @customElement('dx-grid')
83
37
  export class DxGrid extends LitElement {
84
38
  @property({ type: Object })
85
- rowDefault: AxisMeta = { size: 32 };
86
-
87
- @property({ type: Object })
88
- columnDefault: AxisMeta = { size: 180 };
89
-
90
- @property({ type: Object })
91
- rows: Record<string, AxisMeta> = {};
92
-
93
- @property({ type: Object })
94
- columns: Record<string, AxisMeta> = {};
95
-
96
- @property({ type: Object })
97
- cells: Record<string, CellValue> = {};
98
-
99
- //
100
- // `pos`, short for ‘position’, is the position in pixels of the viewport from the origin.
101
- //
39
+ values: Record<string, CellValue> = {};
102
40
 
103
41
  @state()
104
42
  posInline = 0;
@@ -106,136 +44,14 @@ export class DxGrid extends LitElement {
106
44
  @state()
107
45
  posBlock = 0;
108
46
 
109
- //
110
- // `size` (when not suffixed with ‘row’ or ‘col’, see above) is the size in pixels of the viewport.
111
- //
112
-
113
47
  @state()
114
48
  sizeInline = 0;
115
49
 
116
50
  @state()
117
51
  sizeBlock = 0;
118
52
 
119
- //
120
- // `overscan` is the amount in pixels to offset the grid content due to the number of overscanned columns or rows.
121
- //
122
-
123
- @state()
124
- overscanInline = 0;
125
-
126
- @state()
127
- overscanBlock = 0;
128
-
129
- //
130
- // `bin`, not short for anything, is the range in pixels within which virtualization does not need to reassess.
131
- //
132
-
133
- @state()
134
- binInlineMin = 0;
135
-
136
- @state()
137
- binInlineMax = this.colSize(0);
138
-
139
- @state()
140
- binBlockMin = 0;
141
-
142
- @state()
143
- binBlockMax = this.rowSize(0);
144
-
145
- //
146
- // `vis`, short for ‘visible’, is the range in numeric index of the columns or rows which should be rendered within
147
- // the viewport. These start with naïve values that are updated before first contentful render.
148
- //
149
-
150
- @state()
151
- visColMin = 0;
152
-
153
- @state()
154
- visColMax = 1;
155
-
156
- @state()
157
- visRowMin = 0;
158
-
159
- @state()
160
- visRowMax = 1;
161
-
162
- //
163
- // `template` is the rendered value of `grid-{axis}-template`.
164
- //
165
53
  @state()
166
- templateColumns = `${this.colSize(0)}px`;
167
-
168
- @state()
169
- templateRows = `${this.rowSize(0)}px`;
170
-
171
- //
172
- // Resize state and handlers
173
- //
174
-
175
- @state()
176
- colSizes: Record<string, number> = {};
177
-
178
- @state()
179
- rowSizes: Record<string, number> = {};
180
-
181
- @state()
182
- resizing: null | { axis: 'col' | 'row'; page: number; size: number; index: string } = null;
183
-
184
- handlePointerDown = (event: PointerEvent) => {
185
- const actionEl = (event.target as HTMLElement)?.closest('[data-dx-grid-action]');
186
- const action = actionEl?.getAttribute('data-dx-grid-action');
187
- if (action) {
188
- if (action.startsWith('resize')) {
189
- const [resize, index] = action.split(',');
190
- const [_, axis] = resize.split('-');
191
- this.resizing = {
192
- axis: axis as 'col' | 'row',
193
- size: axis === 'col' ? this.colSize(index) : this.rowSize(index),
194
- page: getPage(axis, event),
195
- index,
196
- };
197
- }
198
- }
199
- };
200
-
201
- handlePointerUp = (_event: PointerEvent) => {
202
- this.resizing = null;
203
- };
204
-
205
- handlePointerMove = (event: PointerEvent) => {
206
- if (this.resizing) {
207
- const delta = getPage(this.resizing.axis, event) - this.resizing.page;
208
- if (this.resizing.axis === 'col') {
209
- const nextSize = Math.max(sizeColMin, Math.min(sizeColMax, this.resizing.size + delta));
210
- this.colSizes = { ...this.colSizes, [this.resizing.index]: nextSize };
211
- this.updateVisInline();
212
- } else {
213
- const nextSize = Math.max(sizeRowMin, Math.min(sizeRowMax, this.resizing.size + delta));
214
- this.rowSizes = { ...this.rowSizes, [this.resizing.index]: nextSize };
215
- this.updateVisBlock();
216
- }
217
- }
218
- };
219
-
220
- //
221
- // Accessors
222
- //
223
-
224
- private colSize(c: number | string) {
225
- return this.colSizes?.[c] ?? this.columnDefault.size;
226
- }
227
-
228
- private rowSize(r: number | string) {
229
- return this.rowSizes?.[r] ?? this.rowDefault.size;
230
- }
231
-
232
- private getCell(c: number | string, r: number | string) {
233
- return this.cells[`${c}${separator}${r}`];
234
- }
235
-
236
- //
237
- // Resize & reposition handlers, observer, ref
238
- //
54
+ cellByPosition: Record<string, string> = {};
239
55
 
240
56
  @state()
241
57
  observer = new ResizeObserver((entries) => {
@@ -243,15 +59,8 @@ export class DxGrid extends LitElement {
243
59
  inlineSize: 0,
244
60
  blockSize: 0,
245
61
  };
246
- if (
247
- Math.abs(inlineSize - this.sizeInline) > resizeTolerance ||
248
- Math.abs(blockSize - this.sizeBlock) > resizeTolerance
249
- ) {
250
- // console.info('[updating bounds]', 'resize', [inlineSize - this.sizeInline, blockSize - this.sizeBlock]);
251
- this.sizeInline = inlineSize;
252
- this.sizeBlock = blockSize;
253
- this.updateVis();
254
- }
62
+ this.sizeInline = inlineSize;
63
+ this.sizeBlock = blockSize;
255
64
  });
256
65
 
257
66
  viewportRef: Ref<HTMLDivElement> = createRef();
@@ -259,135 +68,111 @@ export class DxGrid extends LitElement {
259
68
  handleWheel = ({ deltaX, deltaY }: WheelEvent) => {
260
69
  this.posInline = Math.max(0, this.posInline + deltaX);
261
70
  this.posBlock = Math.max(0, this.posBlock + deltaY);
262
- if (
263
- this.posInline >= this.binInlineMin &&
264
- this.posInline < this.binInlineMax &&
265
- this.posBlock >= this.binBlockMin &&
266
- this.posBlock < this.binBlockMax
267
- ) {
268
- // do nothing
269
- } else {
270
- // console.info(
271
- // '[updating bounds]',
272
- // 'wheel',
273
- // [this.binInlineMin, this.posInline, this.binInlineMax],
274
- // [this.binBlockMin, this.posBlock, this.binBlockMax],
275
- // );
276
- this.updateVis();
277
- }
278
71
  };
279
72
 
280
- private updateVisInline() {
281
- // todo: avoid starting from zero
282
- let colIndex = 0;
283
- let pxInline = this.colSize(colIndex);
284
-
285
- while (pxInline < this.posInline) {
286
- colIndex += 1;
287
- pxInline += this.colSize(colIndex) + gap;
288
- }
289
-
290
- this.visColMin = colIndex - overscanCol;
291
-
292
- this.binInlineMin = pxInline - this.colSize(colIndex) - gap;
293
- this.binInlineMax = pxInline + gap;
294
-
295
- this.overscanInline =
296
- [...Array(overscanCol)].reduce((acc, _, c0) => {
297
- acc += this.colSize(this.visColMin + c0);
73
+ override willUpdate(changed: PropertyValues<this>) {
74
+ if (changed.has('values')) {
75
+ // console.log('[computing cellByPosition]');
76
+ this.cellByPosition = Object.entries(this.values).reduce((acc: Record<string, string>, [id, { pos, end }]) => {
77
+ const { i: i1, j: j1 } = posFromNumericNotation(pos);
78
+ if (end) {
79
+ const { i: i2, j: j2 } = posFromNumericNotation(end);
80
+ for (let ci = i1; ci <= i2; ci += 1) {
81
+ for (let cj = j1; cj <= j2; cj += 1) {
82
+ acc[`${ci},${cj}`] = id;
83
+ }
84
+ }
85
+ } else {
86
+ acc[`${i1},${j1}`] = id;
87
+ }
298
88
  return acc;
299
- }, 0) +
300
- gap * (overscanCol - 1);
301
-
302
- while (pxInline < this.binInlineMax + this.sizeInline) {
303
- colIndex += 1;
304
- pxInline += this.colSize(colIndex) + gap;
89
+ }, {});
305
90
  }
306
-
307
- this.visColMax = colIndex + overscanCol + 1;
308
-
309
- this.templateColumns = [...Array(this.visColMax - this.visColMin)]
310
- .map((_, c0) => `${this.colSize(this.visColMin + c0)}px`)
311
- .join(' ');
312
91
  }
313
92
 
314
- private updateVisBlock() {
315
- // todo: avoid starting from zero
316
- let rowIndex = 0;
317
- let pxBlock = this.rowSize(rowIndex);
318
-
319
- while (pxBlock < this.posBlock) {
320
- rowIndex += 1;
321
- pxBlock += this.rowSize(rowIndex) + gap;
322
- }
323
-
324
- this.visRowMin = rowIndex - overscanRow;
325
-
326
- this.binBlockMin = pxBlock - this.rowSize(rowIndex) - gap;
327
- this.binBlockMax = pxBlock + gap;
93
+ private getCell(i: number, j: number) {
94
+ const pos = `${i},${j}`;
95
+ const cellId = this.cellByPosition[pos];
96
+ return cellId ? this.values[cellId] : undefined;
97
+ }
328
98
 
329
- this.overscanBlock =
330
- [...Array(overscanRow)].reduce((acc, _, r0) => {
331
- acc += this.rowSize(this.visRowMin + r0);
99
+ private computeExtrema() {
100
+ const colVisMin = Math.floor(this.posInline / (colSize + gap));
101
+ const colVisMax = Math.ceil((this.sizeInline + this.posInline) / (colSize + gap));
102
+
103
+ const rowVisMin = Math.floor(this.posBlock / (rowSize + gap));
104
+ const rowVisMax = Math.ceil((this.sizeBlock + this.posBlock) / (rowSize + gap));
105
+
106
+ const { colExtMin, colExtMax } = [...Array(rowVisMax - rowVisMin)].reduce(
107
+ (acc, _, j) => {
108
+ const colVisMinCell = this.getCell(colVisMin, j + rowVisMin);
109
+ if (colVisMinCell?.end) {
110
+ const { i: iStart } = posFromNumericNotation(colVisMinCell.pos);
111
+ acc.colExtMin = Math.min(acc.colExtMin, iStart);
112
+ }
113
+ const colVisMaxCell = this.getCell(colVisMax, j + rowVisMin);
114
+ if (colVisMaxCell?.end) {
115
+ const { i: iEnd } = posFromNumericNotation(colVisMaxCell.end);
116
+ acc.colExtMax = Math.max(acc.colExtMax, iEnd);
117
+ }
332
118
  return acc;
333
- }, 0) +
334
- gap * (overscanRow - 1);
335
-
336
- while (pxBlock < this.binBlockMax + this.sizeBlock) {
337
- rowIndex += 1;
338
- pxBlock += this.rowSize(rowIndex) + gap;
339
- }
340
-
341
- this.visRowMax = rowIndex + overscanRow + 1;
342
-
343
- this.templateRows = [...Array(this.visRowMax - this.visRowMin)]
344
- .map((_, r0) => `${this.rowSize(this.visRowMin + r0)}px`)
345
- .join(' ');
119
+ },
120
+ { colExtMin: colVisMin, colExtMax: colVisMax },
121
+ );
122
+
123
+ const { rowExtMin, rowExtMax } = [...Array(colVisMax - colVisMin)].reduce(
124
+ (acc, _, i) => {
125
+ const rowVisMinCell = this.getCell(i + colVisMin, rowVisMin);
126
+ if (rowVisMinCell?.end) {
127
+ const { j: jStart } = posFromNumericNotation(rowVisMinCell.pos);
128
+ acc.rowExtMin = Math.min(acc.rowExtMin, jStart);
129
+ }
130
+ const rowVisMaxCell = this.getCell(i + colVisMin, rowVisMax);
131
+ if (rowVisMaxCell?.end) {
132
+ const { j: jEnd } = posFromNumericNotation(rowVisMaxCell.end);
133
+ acc.rowExtMax = Math.max(acc.rowExtMax, jEnd);
134
+ }
135
+ return acc;
136
+ },
137
+ { rowExtMin: rowVisMin, rowExtMax: rowVisMax },
138
+ );
139
+
140
+ return {
141
+ colVisMin,
142
+ colExtMin,
143
+ colVisMax,
144
+ colExtMax,
145
+ rowVisMin,
146
+ rowExtMin,
147
+ rowVisMax,
148
+ rowExtMax,
149
+ };
346
150
  }
347
151
 
348
- private updateVis() {
349
- this.updateVisInline();
350
- this.updateVisBlock();
351
- }
152
+ override render() {
153
+ const { colVisMin, colVisMax, rowVisMin, rowVisMax } = this.computeExtrema();
352
154
 
353
- //
354
- // Render and other lifecycle methods
355
- //
155
+ const visibleCols = colVisMax - colVisMin;
156
+ const visibleRows = rowVisMax - rowVisMin;
356
157
 
357
- override render() {
358
- const visibleCols = this.visColMax - this.visColMin;
359
- const visibleRows = this.visRowMax - this.visRowMin;
360
- // TODO NEXT -> ensure offset is using the right components
361
- const offsetInline = this.binInlineMin - this.posInline - this.overscanInline;
362
- const offsetBlock = this.binBlockMin - this.posBlock - this.overscanBlock;
158
+ // TODO(thure): now compute the offset based on the extrema.
159
+ const offsetInline = colVisMin * colSize - this.posInline;
160
+ const offsetBlock = rowVisMin * rowSize - this.posBlock;
363
161
 
364
- return html`<div
365
- role="none"
366
- class="dx-grid"
367
- @pointerdown=${this.handlePointerDown}
368
- @pointerup=${this.handlePointerUp}
369
- @pointermove=${this.handlePointerMove}
370
- >
162
+ return html`<div role="none" class="dx-grid">
371
163
  <div role="none" class="dx-grid__corner"></div>
372
164
  <div role="none" class="dx-grid__columnheader">
373
165
  <div
374
166
  role="none"
375
167
  class="dx-grid__columnheader__content"
376
- style="transform:translate3d(${offsetInline}px,0,0);grid-template-columns:${this.templateColumns};"
168
+ style="transform:translate3d(${offsetInline}px,0,0);grid-template-columns:repeat(${visibleCols},${colSize}px);"
377
169
  >
378
- ${[...Array(visibleCols)].map((_, c0) => {
379
- const c = this.visColMin + c0;
170
+ ${[...Array(visibleCols)].map((_, i) => {
380
171
  return html`<div
381
- role="columnheader"
382
- ?inert=${c < 0}
383
- style="inline-size:${this.colSize(c)}px;block-size:${this.rowDefault.size}px;grid-column:${c0 + 1}/${c0 +
384
- 2};"
172
+ role="gridcell"
173
+ style="inline-size:${colSize}px;block-size:${rowSize}px;grid-column:${i + 1}/${i + 2};"
385
174
  >
386
- <span id=${localChId(c0)}>${colToA1Notation(c)}</span>
387
- ${(this.columns[c]?.resizeable ?? this.columnDefault.resizeable) &&
388
- html`<button class="dx-grid__resize-handle" data-dx-grid-action=${`resize-col,${c}`}>
389
- <span class="sr-only">Resize</span>
390
- </button>`}
175
+ ${colToA1Notation(colVisMin + i)}
391
176
  </div>`;
392
177
  })}
393
178
  </div>
@@ -395,18 +180,9 @@ export class DxGrid extends LitElement {
395
180
  <div role="none" class="dx-grid__corner"></div>
396
181
  <div role="none" class="dx-grid__rowheader">
397
182
  <div role="none" class="dx-grid__rowheader__content" style="transform:translate3d(0,${offsetBlock}px,0);">
398
- ${[...Array(visibleRows)].map((_, r0) => {
399
- const r = this.visRowMin + r0;
400
- return html`<div
401
- role="rowheader"
402
- ?inert=${r < 0}
403
- style="block-size:${this.rowSize(r)}px;grid-row:${r0 + 1}/${r0 + 2}"
404
- >
405
- <span id=${localRhId(r0)}>${rowToA1Notation(r)}</span>
406
- ${(this.rows[r]?.resizeable ?? this.rowDefault.resizeable) &&
407
- html`<button class="dx-grid__resize-handle" data-dx-grid-action=${`resize-row,${r}`}>
408
- <span class="sr-only">Resize</span>
409
- </button>`}
183
+ ${[...Array(visibleRows)].map((_, j) => {
184
+ return html`<div role="gridcell" style="block-size:${rowSize}px;grid-row:${j + 1}/${j + 2}">
185
+ ${rowToA1Notation(rowVisMin + j)}
410
186
  </div>`;
411
187
  })}
412
188
  </div>
@@ -415,24 +191,33 @@ export class DxGrid extends LitElement {
415
191
  <div
416
192
  role="grid"
417
193
  class="dx-grid__content"
418
- style="transform:translate3d(${offsetInline}px,${offsetBlock}px,0);grid-template-columns:${this
419
- .templateColumns};grid-template-rows:${this.templateRows};"
194
+ style="transform:translate3d(${offsetInline}px,${offsetBlock}px,0);grid-template-columns:repeat(${visibleCols},${colSize}px);grid-template-rows:repeat(${visibleRows},${rowSize}px);"
420
195
  >
421
- ${[...Array(visibleCols)].map((_, c0) => {
422
- return [...Array(visibleRows)].map((_, r0) => {
423
- const c = c0 + this.visColMin;
424
- const r = r0 + this.visRowMin;
425
- const cell = this.getCell(c, r);
426
- return html`<div
427
- role="gridcell"
428
- ?inert=${c < 0 || r < 0}
429
- aria-rowindex=${r}
430
- aria-colindex=${c}
431
- data-dx-grid-action="cell"
432
- style="grid-column:${c0 + 1};grid-row:${r0 + 1}"
433
- >
434
- ${cell?.value}
435
- </div>`;
196
+ ${[...Array(visibleCols)].map((_, i) => {
197
+ return [...Array(visibleRows)].map((_, j) => {
198
+ const posAbs = `${i + colVisMin},${j + rowVisMin}`;
199
+ const cellId = this.cellByPosition[posAbs];
200
+ const cell = cellId ? this.values[cellId] : undefined;
201
+ if (cell?.end) {
202
+ // This is a merged cell
203
+ if (posAbs !== cell?.pos) {
204
+ // Don’t render subcells within the merge if not at the start (probably)
205
+ return null;
206
+ } else {
207
+ // Render the full merged cell
208
+ const { i: iEndAbs, j: jEndAbs } = posFromNumericNotation(cell.end);
209
+ return html`<div
210
+ role="gridcell"
211
+ style="grid-column:${i + 1} / ${iEndAbs - colVisMin + 2};grid-row:${j + 1} / ${jEndAbs -
212
+ rowVisMin +
213
+ 2}"
214
+ >
215
+ ${cell?.value}
216
+ </div>`;
217
+ }
218
+ } else {
219
+ return html`<div role="gridcell" style="grid-column:${i + 1};grid-row:${j + 1}">${cell?.value}</div>`;
220
+ }
436
221
  });
437
222
  })}
438
223
  </div>
@@ -450,18 +235,6 @@ export class DxGrid extends LitElement {
450
235
 
451
236
  override firstUpdated() {
452
237
  this.observer.observe(this.viewportRef.value!);
453
- this.colSizes = Object.entries(this.columns).reduce((acc: Record<string, number>, [colId, colMeta]) => {
454
- if (colMeta?.size) {
455
- acc[colId] = colMeta.size;
456
- }
457
- return acc;
458
- }, {});
459
- this.rowSizes = Object.entries(this.rows).reduce((acc: Record<string, number>, [rowId, rowMeta]) => {
460
- if (rowMeta?.size) {
461
- acc[rowId] = rowMeta.size;
462
- }
463
- return acc;
464
- }, {});
465
238
  }
466
239
 
467
240
  override disconnectedCallback() {
package/src/index.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export { DxGrid, type DxGridProps } from './dx-grid';
5
+ export { DxGrid } from './dx-grid';
@@ -0,0 +1,80 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export type CellPosition = { i: number; j: number };
6
+
7
+ export type CellRange = { from: CellPosition; to?: CellPosition };
8
+
9
+ export const posEquals = (a: CellPosition | undefined, b: CellPosition | undefined) => a?.i === b?.i && a?.j === b?.j;
10
+
11
+ export const colToA1Notation = (column: number): string => {
12
+ return (
13
+ (column >= 26 ? String.fromCharCode('A'.charCodeAt(0) + Math.floor(column / 26) - 1) : '') +
14
+ String.fromCharCode('A'.charCodeAt(0) + (column % 26))
15
+ );
16
+ };
17
+
18
+ export const rowToA1Notation = (row: number): string => {
19
+ return `${row + 1}`;
20
+ };
21
+
22
+ export const posToA1Notation = (column: number, row: number): string => {
23
+ const columnA1 = colToA1Notation(column);
24
+ const rowA1 = rowToA1Notation(row);
25
+ return `${columnA1}${rowA1}`;
26
+ };
27
+
28
+ export const posFromA1Notation = (notation: string): CellPosition => {
29
+ const match = notation.match(/([A-Z]+)(\d+)/);
30
+ if (!match) {
31
+ throw Error('[posFromA1Notation] No match.');
32
+ }
33
+ const column = match[1].split('').reduce((acc, c) => acc * 26 + c.charCodeAt(0) - 'A'.charCodeAt(0) + 1, 0) - 1;
34
+ const row = parseInt(match[2], 10) - 1;
35
+ return { i: column, j: row };
36
+ };
37
+
38
+ export const posFromNumericNotation = (notation: string): CellPosition => {
39
+ const [iStr, jStr] = notation.split(',');
40
+ if (!iStr || !jStr) {
41
+ throw Error('[posFromNumericNotation] Bad input');
42
+ }
43
+ return { i: parseInt(iStr), j: parseInt(jStr) };
44
+ };
45
+
46
+ export const rangeToA1Notation = (range: CellRange) =>
47
+ [range?.from && posToA1Notation(range?.from.i, range?.from.j), range?.to && posToA1Notation(range?.to.i, range.to.j)]
48
+ .filter(Boolean)
49
+ .join(':');
50
+
51
+ export const rangeFromA1Notation = (notation: string): CellRange => {
52
+ const [from, to] = notation.split(':').map(posFromA1Notation);
53
+ return { from, to };
54
+ };
55
+
56
+ export const inRange = (range: CellRange | undefined, pos: CellPosition): boolean => {
57
+ if (!range) {
58
+ return false;
59
+ }
60
+
61
+ const { from, to } = range;
62
+ if ((from && posEquals(from, pos)) || (to && posEquals(to, pos))) {
63
+ return true;
64
+ }
65
+
66
+ if (!from || !to) {
67
+ return false;
68
+ }
69
+
70
+ const { i, j } = pos;
71
+
72
+ const { i: c1, j: r1 } = from;
73
+ const { i: c2, j: r2 } = to;
74
+ const cMin = Math.min(c1, c2);
75
+ const cMax = Math.max(c1, c2);
76
+ const rMin = Math.min(r1, r2);
77
+ const rMax = Math.max(r1, r2);
78
+
79
+ return i >= cMin && i <= cMax && j >= rMin && j <= rMax;
80
+ };