@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/dist/lib/browser/index.mjs +134 -303
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/dx-grid.d.ts +10 -42
- package/dist/types/src/dx-grid.d.ts.map +1 -1
- package/dist/types/src/dx-grid.lit-stories.d.ts +4 -6
- package/dist/types/src/dx-grid.lit-stories.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/position.d.ts +18 -0
- package/dist/types/src/position.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/dx-grid.lit-stories.ts +7 -30
- package/src/dx-grid.pcss +10 -50
- package/src/dx-grid.ts +126 -353
- package/src/index.ts +1 -1
- package/src/position.ts +80 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
},
|
|
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
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
},
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
349
|
-
this.
|
|
350
|
-
this.updateVisBlock();
|
|
351
|
-
}
|
|
152
|
+
override render() {
|
|
153
|
+
const { colVisMin, colVisMax, rowVisMin, rowVisMax } = this.computeExtrema();
|
|
352
154
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
//
|
|
155
|
+
const visibleCols = colVisMax - colVisMin;
|
|
156
|
+
const visibleRows = rowVisMax - rowVisMin;
|
|
356
157
|
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
const
|
|
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
|
|
168
|
+
style="transform:translate3d(${offsetInline}px,0,0);grid-template-columns:repeat(${visibleCols},${colSize}px);"
|
|
377
169
|
>
|
|
378
|
-
${[...Array(visibleCols)].map((_,
|
|
379
|
-
const c = this.visColMin + c0;
|
|
170
|
+
${[...Array(visibleCols)].map((_, i) => {
|
|
380
171
|
return html`<div
|
|
381
|
-
role="
|
|
382
|
-
|
|
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
|
-
|
|
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((_,
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
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((_,
|
|
422
|
-
return [...Array(visibleRows)].map((_,
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
const cell = this.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
package/src/position.ts
ADDED
|
@@ -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
|
+
};
|