@dxos/lit-grid 0.6.11 → 0.6.12-main.2d19bf1

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.
Files changed (56) hide show
  1. package/dist/src/dx-grid-axis-resize-handle.d.ts +16 -0
  2. package/dist/src/dx-grid-axis-resize-handle.d.ts.map +1 -0
  3. package/dist/src/dx-grid-axis-resize-handle.js +96 -0
  4. package/dist/src/dx-grid-axis-resize-handle.js.map +1 -0
  5. package/dist/src/dx-grid.d.ts +116 -0
  6. package/dist/src/dx-grid.d.ts.map +1 -0
  7. package/dist/src/dx-grid.js +1054 -0
  8. package/dist/src/dx-grid.js.map +1 -0
  9. package/dist/src/dx-grid.lit-stories.d.ts +42 -0
  10. package/dist/src/dx-grid.lit-stories.d.ts.map +1 -0
  11. package/dist/src/dx-grid.lit-stories.js +166 -0
  12. package/dist/src/dx-grid.lit-stories.js.map +1 -0
  13. package/dist/src/index.d.ts +3 -0
  14. package/dist/src/index.d.ts.map +1 -0
  15. package/dist/src/index.js +6 -0
  16. package/dist/src/index.js.map +1 -0
  17. package/dist/src/types.d.ts +106 -0
  18. package/dist/src/types.d.ts.map +1 -0
  19. package/dist/src/types.js +44 -0
  20. package/dist/src/types.js.map +1 -0
  21. package/dist/src/util.d.ts +9 -0
  22. package/dist/src/util.d.ts.map +1 -0
  23. package/dist/src/util.js +19 -0
  24. package/dist/src/util.js.map +1 -0
  25. package/dist/types/src/dx-grid-axis-resize-handle.d.ts +16 -0
  26. package/dist/types/src/dx-grid-axis-resize-handle.d.ts.map +1 -0
  27. package/dist/types/src/dx-grid-axis-resize-handle.js +96 -0
  28. package/dist/types/src/dx-grid-axis-resize-handle.js.map +1 -0
  29. package/dist/types/src/dx-grid.d.ts +94 -61
  30. package/dist/types/src/dx-grid.d.ts.map +1 -1
  31. package/dist/types/src/dx-grid.js +1054 -0
  32. package/dist/types/src/dx-grid.js.map +1 -0
  33. package/dist/types/src/dx-grid.lit-stories.d.ts +27 -2
  34. package/dist/types/src/dx-grid.lit-stories.d.ts.map +1 -1
  35. package/dist/types/src/dx-grid.lit-stories.js +166 -0
  36. package/dist/types/src/dx-grid.lit-stories.js.map +1 -0
  37. package/dist/types/src/index.js +6 -0
  38. package/dist/types/src/index.js.map +1 -0
  39. package/dist/types/src/types.d.ts +98 -1
  40. package/dist/types/src/types.d.ts.map +1 -1
  41. package/dist/types/src/types.js +44 -0
  42. package/dist/types/src/types.js.map +1 -0
  43. package/dist/types/src/util.d.ts +9 -0
  44. package/dist/types/src/util.d.ts.map +1 -0
  45. package/dist/types/src/util.js +19 -0
  46. package/dist/types/src/util.js.map +1 -0
  47. package/package.json +5 -6
  48. package/src/dx-grid-axis-resize-handle.ts +87 -0
  49. package/src/dx-grid.lit-stories.ts +148 -21
  50. package/src/dx-grid.pcss +68 -67
  51. package/src/dx-grid.ts +918 -329
  52. package/src/types.ts +135 -1
  53. package/src/util.ts +28 -0
  54. package/dist/lib/browser/index.mjs +0 -578
  55. package/dist/lib/browser/index.mjs.map +0 -7
  56. package/dist/lib/browser/meta.json +0 -1
package/src/dx-grid.ts CHANGED
@@ -2,11 +2,41 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { LitElement, html } from 'lit';
6
- import { customElement, state, property, eventOptions } from 'lit/decorators.js';
5
+ import { LitElement, html, nothing } from 'lit';
6
+ import { customElement, state, property } from 'lit/decorators.js';
7
7
  import { ref, createRef, type Ref } from 'lit/directives/ref.js';
8
-
9
- import { DxAxisResize, type DxAxisResizeProps, type DxGridAxis } from './types';
8
+ import { styleMap } from 'lit/directives/style-map.js';
9
+
10
+ // eslint-disable-next-line unused-imports/no-unused-imports
11
+ import './dx-grid-axis-resize-handle';
12
+ import {
13
+ type DxGridAxisMetaProps,
14
+ type DxGridAxisSizes,
15
+ type DxGridCellIndex,
16
+ type DxGridCellValue,
17
+ DxAxisResize,
18
+ type DxAxisResizeInternal,
19
+ DxEditRequest,
20
+ type DxGridAxisMeta,
21
+ type DxGridCells,
22
+ DxGridCellsSelect,
23
+ type DxGridFixedPlane,
24
+ type DxGridFrozenAxes,
25
+ type DxGridFrozenColsPlane,
26
+ type DxGridFrozenPlane,
27
+ type DxGridFrozenRowsPlane,
28
+ type DxGridMode,
29
+ type DxGridPlane,
30
+ type DxGridPlaneCells,
31
+ type DxGridPlaneRange,
32
+ type DxGridPlaneRecord,
33
+ type DxGridPointer,
34
+ type DxGridPosition,
35
+ type DxGridPositionNullable,
36
+ type DxGridAxis,
37
+ type DxGridSelectionProps,
38
+ } from './types';
39
+ import { separator, toCellIndex } from './util';
10
40
 
11
41
  /**
12
42
  * The size in pixels of the gap between cells
@@ -14,10 +44,14 @@ import { DxAxisResize, type DxAxisResizeProps, type DxGridAxis } from './types';
14
44
  const gap = 1;
15
45
 
16
46
  /**
17
- * This should be about the width of the `1` numeral so resize is triggered as the row header column’s intrinsic size
18
- * changes when scrolling vertically.
47
+ * ResizeObserver notices even subpixel changes, only respond to changes of at least 1px.
19
48
  */
20
- const resizeTolerance = 8;
49
+ const resizeTolerance = 1;
50
+
51
+ /**
52
+ * The amount of pixels the primary pointer has to move after PointerDown to engage in selection.
53
+ */
54
+ const selectTolerance = 4;
21
55
 
22
56
  //
23
57
  // `overscan` is the number of columns or rows to render outside of the viewport
@@ -33,116 +67,225 @@ const sizeColMax = 1024;
33
67
  const sizeRowMin = 16;
34
68
  const sizeRowMax = 1024;
35
69
 
36
- /**
37
- * Separator for serializing cell position vectors
38
- */
39
- const separator = ',';
70
+ const shouldSelect = (pointer: DxGridPointer, { pageX, pageY }: PointerEvent) => {
71
+ if (pointer?.state === 'maybeSelecting') {
72
+ return Math.hypot(Math.abs(pointer.pageX - pageX), Math.abs(pointer.pageY - pageY)) >= selectTolerance;
73
+ } else {
74
+ return false;
75
+ }
76
+ };
40
77
 
41
- //
42
- // A1 notation is the fallback for numbering columns and rows.
43
- //
78
+ const selectionProps = (selectionStart: DxGridPosition, selectionEnd: DxGridPosition): DxGridSelectionProps => {
79
+ const colMin = Math.min(selectionStart.col, selectionEnd.col);
80
+ const colMax = Math.max(selectionStart.col, selectionEnd.col);
81
+ const rowMin = Math.min(selectionStart.row, selectionEnd.row);
82
+ const rowMax = Math.max(selectionStart.row, selectionEnd.row);
83
+ const plane = selectionStart.plane;
84
+ const visible = colMin !== colMax || rowMin !== rowMax;
85
+ return { colMin, colMax, rowMin, rowMax, plane, visible };
86
+ };
44
87
 
45
- const colToA1Notation = (col: number): string => {
88
+ const cellSelected = (col: number, row: number, plane: DxGridPlane, selection: DxGridSelectionProps): boolean => {
46
89
  return (
47
- (col >= 26 ? String.fromCharCode('A'.charCodeAt(0) + Math.floor(col / 26) - 1) : '') +
48
- String.fromCharCode('A'.charCodeAt(0) + (col % 26))
90
+ plane === selection.plane &&
91
+ col >= selection.colMin &&
92
+ col <= selection.colMax &&
93
+ row >= selection.rowMin &&
94
+ row <= selection.rowMax
49
95
  );
50
96
  };
51
97
 
52
- const rowToA1Notation = (row: number): string => {
53
- return `${row + 1}`;
98
+ const closestAction = (target: EventTarget | null): { action: string | null; actionEl: HTMLElement | null } => {
99
+ const actionEl: HTMLElement | null = (target as HTMLElement | null)?.closest('[data-dx-grid-action]') ?? null;
100
+ return { actionEl, action: actionEl?.getAttribute('data-dx-grid-action') ?? null };
54
101
  };
55
102
 
56
- export type CellValue = {
57
- /**
58
- * The content value
59
- */
60
- value: string;
61
- /**
62
- * If this is a merged cell, the bottomright-most of the range in numeric notation, otherwise undefined.
63
- */
64
- end?: string;
65
- /**
66
- * CSS inline styles to apply to the gridcell element
67
- */
68
- style?: string;
103
+ export const closestCell = (target: EventTarget | null, actionEl?: HTMLElement | null): DxGridPositionNullable => {
104
+ let cellElement = actionEl;
105
+ if (!cellElement) {
106
+ const { action, actionEl } = closestAction(target);
107
+ if (action === 'cell') {
108
+ cellElement = actionEl as HTMLElement;
109
+ }
110
+ }
111
+ if (cellElement) {
112
+ const col = parseInt(cellElement.getAttribute('aria-colindex') ?? 'never');
113
+ const row = parseInt(cellElement.getAttribute('aria-rowindex') ?? 'never');
114
+ const plane = (cellElement.closest('[data-dx-grid-plane]')?.getAttribute('data-dx-grid-plane') ??
115
+ 'grid') as DxGridPlane;
116
+ return { plane, col, row };
117
+ } else {
118
+ return null;
119
+ }
69
120
  };
70
121
 
71
- type AxisMeta = {
72
- size: number;
73
- description?: string;
74
- resizeable?: boolean;
122
+ const resolveRowPlane = (plane: DxGridPlane): 'grid' | DxGridFrozenRowsPlane => {
123
+ switch (plane) {
124
+ case 'fixedStartStart':
125
+ case 'fixedStartEnd':
126
+ case 'frozenRowsStart':
127
+ return 'frozenRowsStart';
128
+ case 'fixedEndStart':
129
+ case 'fixedEndEnd':
130
+ case 'frozenRowsEnd':
131
+ return 'frozenRowsEnd';
132
+ default:
133
+ return 'grid';
134
+ }
75
135
  };
76
136
 
77
- export type DxGridProps = Partial<Pick<DxGrid, 'cells' | 'rows' | 'columns' | 'rowDefault' | 'columnDefault'>>;
137
+ const resolveColPlane = (plane: DxGridPlane): 'grid' | DxGridFrozenColsPlane => {
138
+ switch (plane) {
139
+ case 'fixedStartStart':
140
+ case 'fixedEndStart':
141
+ case 'frozenColsStart':
142
+ return 'frozenColsStart';
143
+ case 'fixedStartEnd':
144
+ case 'fixedEndEnd':
145
+ case 'frozenColsEnd':
146
+ return 'frozenColsEnd';
147
+ default:
148
+ return 'grid';
149
+ }
150
+ };
151
+
152
+ const resolveResizePlane = (resizeAxis: DxGridAxis, cellPlane: DxGridPlane): 'grid' | DxGridFrozenPlane => {
153
+ switch (cellPlane) {
154
+ case 'fixedStartStart':
155
+ return resizeAxis === 'col' ? 'frozenColsStart' : 'frozenRowsStart';
156
+ case 'fixedStartEnd':
157
+ return resizeAxis === 'col' ? 'frozenColsEnd' : 'frozenRowsStart';
158
+ case 'fixedEndStart':
159
+ return resizeAxis === 'col' ? 'frozenColsStart' : 'frozenRowsEnd';
160
+ case 'fixedEndEnd':
161
+ return resizeAxis === 'col' ? 'frozenColsEnd' : 'frozenRowsEnd';
162
+ case 'frozenColsStart':
163
+ case 'frozenColsEnd':
164
+ return resizeAxis === 'col' ? cellPlane : 'grid';
165
+ case 'frozenRowsStart':
166
+ case 'frozenRowsEnd':
167
+ return resizeAxis === 'row' ? cellPlane : 'grid';
168
+ default:
169
+ return cellPlane;
170
+ }
171
+ };
78
172
 
79
- const localChId = (c0: number) => `ch--${c0}`;
80
- const localRhId = (r0: number) => `rh--${r0}`;
173
+ const isSameCell = (a: DxGridPositionNullable, b: DxGridPositionNullable) =>
174
+ a &&
175
+ b &&
176
+ a.plane === b.plane &&
177
+ Number.isFinite(a.col) &&
178
+ Number.isFinite(a.row) &&
179
+ a.col === b.col &&
180
+ a.row === b.row;
81
181
 
82
- const getPage = (axis: string, event: PointerEvent) => (axis === 'col' ? event.pageX : event.pageY);
182
+ const defaultRowSize = 32;
183
+
184
+ const defaultColSize = 180;
83
185
 
84
186
  @customElement('dx-grid')
85
187
  export class DxGrid extends LitElement {
188
+ constructor() {
189
+ super();
190
+ this.addEventListener('dx-axis-resize-internal', this.handleAxisResizeInternal as EventListener);
191
+ this.addEventListener('wheel', this.handleWheel, { passive: true });
192
+ this.addEventListener('pointerdown', this.handlePointerDown);
193
+ this.addEventListener('pointermove', this.handlePointerMove);
194
+ this.addEventListener('pointerup', this.handlePointerUp);
195
+ this.addEventListener('pointerleave', this.handlePointerUp);
196
+ this.addEventListener('focus', this.handleFocus, { capture: true });
197
+ this.addEventListener('blur', this.handleBlur, { capture: true });
198
+ this.addEventListener('keydown', this.handleKeydown);
199
+ }
200
+
201
+ @property({ type: String })
202
+ gridId: string = 'default-grid-id';
203
+
86
204
  @property({ type: Object })
87
- rowDefault: AxisMeta = { size: 32 };
205
+ rowDefault: DxGridPlaneRecord<DxGridFrozenRowsPlane, DxGridAxisMetaProps> = {
206
+ grid: { size: defaultRowSize },
207
+ };
208
+
209
+ @property({ type: Object })
210
+ columnDefault: DxGridPlaneRecord<DxGridFrozenColsPlane, DxGridAxisMetaProps> = {
211
+ grid: { size: defaultColSize },
212
+ };
88
213
 
89
214
  @property({ type: Object })
90
- columnDefault: AxisMeta = { size: 180 };
215
+ rows: DxGridAxisMeta = { grid: {} };
91
216
 
92
217
  @property({ type: Object })
93
- rows: Record<string, AxisMeta> = {};
218
+ columns: DxGridAxisMeta = { grid: {} };
94
219
 
95
220
  @property({ type: Object })
96
- columns: Record<string, AxisMeta> = {};
221
+ initialCells: DxGridCells = { grid: {} };
222
+
223
+ @property({ type: String })
224
+ mode: DxGridMode = 'browse';
225
+
226
+ @property({ type: Number })
227
+ limitColumns: number = Infinity;
228
+
229
+ @property({ type: Number })
230
+ limitRows: number = Infinity;
97
231
 
98
232
  @property({ type: Object })
99
- cells: Record<string, CellValue> = {};
233
+ frozen: DxGridFrozenAxes = {};
234
+
235
+ /**
236
+ * When this function is defined, it is used first to try to get a value for a cell, and otherwise will fall back
237
+ * to `cells`.
238
+ */
239
+ getCells: ((nextRange: DxGridPlaneRange, plane: DxGridPlane) => DxGridPlaneCells) | null = null;
240
+
241
+ @state()
242
+ private cells: DxGridCells = { grid: {} };
100
243
 
101
244
  //
102
245
  // `pos`, short for ‘position’, is the position in pixels of the viewport from the origin.
103
246
  //
104
247
 
105
248
  @state()
106
- posInline = 0;
249
+ private posInline = 0;
107
250
 
108
251
  @state()
109
- posBlock = 0;
252
+ private posBlock = 0;
110
253
 
111
254
  //
112
255
  // `size` (when not suffixed with ‘row’ or ‘col’, see above) is the size in pixels of the viewport.
113
256
  //
114
257
 
115
258
  @state()
116
- sizeInline = 0;
259
+ private sizeInline = 0;
117
260
 
118
261
  @state()
119
- sizeBlock = 0;
262
+ private sizeBlock = 0;
120
263
 
121
264
  //
122
265
  // `overscan` is the amount in pixels to offset the grid content due to the number of overscanned columns or rows.
123
266
  //
124
267
 
125
268
  @state()
126
- overscanInline = 0;
269
+ private overscanInline = 0;
127
270
 
128
271
  @state()
129
- overscanBlock = 0;
272
+ private overscanBlock = 0;
130
273
 
131
274
  //
132
275
  // `bin`, not short for anything, is the range in pixels within which virtualization does not need to reassess.
133
276
  //
134
277
 
135
278
  @state()
136
- binInlineMin = 0;
279
+ private binInlineMin = 0;
137
280
 
138
281
  @state()
139
- binInlineMax = this.colSize(0);
282
+ private binInlineMax = defaultColSize;
140
283
 
141
284
  @state()
142
- binBlockMin = 0;
285
+ private binBlockMin = 0;
143
286
 
144
287
  @state()
145
- binBlockMax = this.rowSize(0);
288
+ private binBlockMax = defaultRowSize;
146
289
 
147
290
  //
148
291
  // `vis`, short for ‘visible’, is the range in numeric index of the columns or rows which should be rendered within
@@ -150,97 +293,229 @@ export class DxGrid extends LitElement {
150
293
  //
151
294
 
152
295
  @state()
153
- visColMin = 0;
296
+ private visColMin = 0;
154
297
 
155
298
  @state()
156
- visColMax = 1;
299
+ private visColMax = 1;
157
300
 
158
301
  @state()
159
- visRowMin = 0;
302
+ private visRowMin = 0;
160
303
 
161
304
  @state()
162
- visRowMax = 1;
305
+ private visRowMax = 1;
163
306
 
164
307
  //
165
308
  // `template` is the rendered value of `grid-{axis}-template`.
166
309
  //
167
310
  @state()
168
- templateColumns = `${this.colSize(0)}px`;
311
+ private templateGridColumns = '0';
312
+
313
+ @state()
314
+ private templatefrozenColsStart = '';
315
+
316
+ @state()
317
+ private templatefrozenColsEnd = '';
318
+
319
+ @state()
320
+ private templateGridRows = '0';
169
321
 
170
322
  @state()
171
- templateRows = `${this.rowSize(0)}px`;
323
+ private templatefrozenRowsStart = '';
324
+
325
+ @state()
326
+ private templatefrozenRowsEnd = '';
172
327
 
173
328
  //
174
- // Resize state and handlers
329
+ // Focus, selection, and resize states
175
330
  //
176
331
 
177
332
  @state()
178
- colSizes: Record<string, number> = {};
333
+ private pointer: DxGridPointer = null;
179
334
 
180
335
  @state()
181
- rowSizes: Record<string, number> = {};
336
+ private colSizes: DxGridAxisSizes = { grid: {} };
182
337
 
183
338
  @state()
184
- resizing: null | (DxAxisResizeProps & { page: number }) = null;
185
-
186
- handlePointerDown = (event: PointerEvent) => {
187
- const actionEl = (event.target as HTMLElement)?.closest('[data-dx-grid-action]');
188
- const action = actionEl?.getAttribute('data-dx-grid-action');
189
- if (action) {
190
- if (action.startsWith('resize')) {
191
- const [resize, index] = action.split(',');
192
- const [_, axis] = resize.split('-');
193
- this.resizing = {
194
- axis: axis as DxGridAxis,
195
- size: axis === 'col' ? this.colSize(index) : this.rowSize(index),
196
- page: getPage(axis, event),
197
- index,
198
- };
339
+ private rowSizes: DxGridAxisSizes = { grid: {} };
340
+
341
+ @state()
342
+ private focusActive: boolean = false;
343
+
344
+ @state()
345
+ private focusedCell: DxGridPosition = { plane: 'grid', col: 0, row: 0 };
346
+
347
+ @state()
348
+ private selectionStart: DxGridPosition = { plane: 'grid', col: 0, row: 0 };
349
+
350
+ @state()
351
+ private selectionEnd: DxGridPosition = { plane: 'grid', col: 0, row: 0 };
352
+
353
+ //
354
+ // Limits
355
+ //
356
+
357
+ @state()
358
+ private intrinsicInlineSize: number = Infinity;
359
+
360
+ @state()
361
+ private intrinsicBlockSize: number = Infinity;
362
+
363
+ //
364
+ // Primary pointer and keyboard handlers
365
+ //
366
+
367
+ private dispatchEditRequest(initialContent?: string) {
368
+ this.snapPosToFocusedCell();
369
+ // Without deferring, the event dispatches before `focusedCellBox` can get updated bounds of the cell, hence:
370
+ queueMicrotask(() =>
371
+ this.dispatchEvent(
372
+ new DxEditRequest({
373
+ cellIndex: toCellIndex(this.focusedCell),
374
+ cellBox: this.focusedCellBox(),
375
+ initialContent,
376
+ }),
377
+ ),
378
+ );
379
+ }
380
+
381
+ private handlePointerDown = (event: PointerEvent) => {
382
+ if (event.isPrimary) {
383
+ const { action, actionEl } = closestAction(event.target);
384
+ if (action) {
385
+ if (action === 'cell') {
386
+ const cellCoords = closestCell(event.target, actionEl);
387
+ if (cellCoords) {
388
+ this.pointer = { state: 'maybeSelecting', pageX: event.pageX, pageY: event.pageY };
389
+ this.selectionStart = cellCoords;
390
+ this.selectionEnd = cellCoords;
391
+ }
392
+ if (this.mode === 'edit') {
393
+ event.preventDefault();
394
+ } else {
395
+ if (this.focusActive && isSameCell(this.focusedCell, cellCoords)) {
396
+ this.dispatchEditRequest();
397
+ }
398
+ }
399
+ }
199
400
  }
200
401
  }
201
402
  };
202
403
 
203
- handlePointerUp = (_event: PointerEvent) => {
204
- if (this.resizing) {
205
- const resizeEvent = new DxAxisResize({
206
- axis: this.resizing.axis,
207
- index: this.resizing.index,
208
- size: this[this.resizing.axis === 'col' ? 'colSize' : 'rowSize'](this.resizing.index),
209
- });
210
- this.dispatchEvent(resizeEvent);
211
- this.resizing = null;
404
+ private handlePointerUp = (event: PointerEvent) => {
405
+ const cell = closestCell(event.target);
406
+ if (cell) {
407
+ this.selectionEnd = cell;
408
+ this.dispatchEvent(
409
+ new DxGridCellsSelect({
410
+ start: this.selectionStart,
411
+ end: this.selectionEnd,
412
+ }),
413
+ );
212
414
  }
415
+ this.pointer = null;
213
416
  };
214
417
 
215
- handlePointerMove = (event: PointerEvent) => {
216
- if (this.resizing) {
217
- const delta = getPage(this.resizing.axis, event) - this.resizing.page;
218
- if (this.resizing.axis === 'col') {
219
- const nextSize = Math.max(sizeColMin, Math.min(sizeColMax, this.resizing.size + delta));
220
- this.colSizes = { ...this.colSizes, [this.resizing.index]: nextSize };
221
- this.updateVisInline();
222
- } else {
223
- const nextSize = Math.max(sizeRowMin, Math.min(sizeRowMax, this.resizing.size + delta));
224
- this.rowSizes = { ...this.rowSizes, [this.resizing.index]: nextSize };
225
- this.updateVisBlock();
418
+ private handlePointerMove = (event: PointerEvent) => {
419
+ if (shouldSelect(this.pointer, event)) {
420
+ this.pointer = { state: 'selecting' };
421
+ } else if (this.pointer?.state === 'selecting') {
422
+ const cell = closestCell(event.target);
423
+ if (
424
+ cell &&
425
+ cell.plane === this.selectionStart.plane &&
426
+ (cell.col !== this.selectionEnd.col || cell.row !== this.selectionEnd.row)
427
+ ) {
428
+ this.selectionEnd = cell;
226
429
  }
227
430
  }
228
431
  };
229
432
 
433
+ private handleKeydown(event: KeyboardEvent) {
434
+ if (this.focusActive && this.mode === 'browse') {
435
+ // Adjust state
436
+ switch (event.key) {
437
+ case 'ArrowDown':
438
+ this.focusedCell = { ...this.focusedCell, row: Math.min(this.limitRows - 1, this.focusedCell.row + 1) };
439
+ break;
440
+ case 'ArrowUp':
441
+ this.focusedCell = { ...this.focusedCell, row: Math.max(0, this.focusedCell.row - 1) };
442
+ break;
443
+ case 'ArrowRight':
444
+ this.focusedCell = { ...this.focusedCell, col: Math.min(this.limitColumns - 1, this.focusedCell.col + 1) };
445
+ break;
446
+ case 'ArrowLeft':
447
+ this.focusedCell = { ...this.focusedCell, col: Math.max(0, this.focusedCell.col - 1) };
448
+ break;
449
+ }
450
+ // Emit edit request if relevant
451
+ switch (event.key) {
452
+ case 'Enter':
453
+ this.dispatchEditRequest();
454
+ break;
455
+ default:
456
+ if (event.key.length === 1 && event.key.match(/\P{Cc}/u)) {
457
+ this.dispatchEditRequest(event.key);
458
+ }
459
+ break;
460
+ }
461
+ // Handle virtualization & focus consequences
462
+ switch (event.key) {
463
+ case 'ArrowDown':
464
+ case 'ArrowUp':
465
+ case 'ArrowRight':
466
+ case 'ArrowLeft':
467
+ event.preventDefault();
468
+ this.snapPosToFocusedCell();
469
+ break;
470
+ }
471
+ }
472
+ }
473
+
230
474
  //
231
475
  // Accessors
232
476
  //
233
477
 
234
- private colSize(c: number | string) {
235
- return this.colSizes?.[c] ?? this.columnDefault.size;
478
+ private colSize(c: number | string, plane: DxGridPlane) {
479
+ const resolvedPlane = resolveColPlane(plane);
480
+ return this.colSizes?.[resolvedPlane]?.[c] ?? this.columnDefault[resolvedPlane]?.size ?? defaultColSize;
236
481
  }
237
482
 
238
- private rowSize(r: number | string) {
239
- return this.rowSizes?.[r] ?? this.rowDefault.size;
483
+ private rowSize(r: number | string, plane: DxGridPlane) {
484
+ const resolvedPlane = resolveRowPlane(plane);
485
+ return this.rowSizes?.[resolvedPlane]?.[r] ?? this.rowDefault[resolvedPlane]?.size ?? defaultRowSize;
240
486
  }
241
487
 
242
- private getCell(c: number | string, r: number | string) {
243
- return this.cells[`${c}${separator}${r}`];
488
+ private cell(c: number | string, r: number | string, plane: DxGridPlane): DxGridCellValue | undefined {
489
+ const index: DxGridCellIndex = `${c}${separator}${r}`;
490
+ return this.cells?.[plane]?.[index] ?? this.initialCells?.[plane]?.[index];
491
+ }
492
+
493
+ private cellActive(c: number | string, r: number | string, plane: DxGridPlane): boolean {
494
+ return (
495
+ this.focusActive && this.focusedCell.plane === plane && this.focusedCell.col === c && this.focusedCell.row === r
496
+ );
497
+ }
498
+
499
+ private focusedCellBox(): DxEditRequest['cellBox'] {
500
+ const cellElement = this.focusedCellElement();
501
+ const cellSize = {
502
+ inlineSize: this.colSize(this.focusedCell.col, this.focusedCell.plane),
503
+ blockSize: this.rowSize(this.focusedCell.row, this.focusedCell.plane),
504
+ };
505
+ if (!cellElement) {
506
+ return { insetInlineStart: NaN, insetBlockStart: NaN, ...cellSize };
507
+ }
508
+ const contentElement = cellElement.offsetParent as HTMLElement;
509
+ // Note that storing `offset` in state causes performance issues, so instead the transform is parsed here.
510
+ const [_translate3d, inlineStr, blockStr] = contentElement.style.transform.split(/[()]|px,?\s?/);
511
+ const contentOffsetInline = parseFloat(inlineStr);
512
+ const contentOffsetBlock = parseFloat(blockStr);
513
+ const offsetParent = contentElement.offsetParent as HTMLElement;
514
+ return {
515
+ insetInlineStart: cellElement.offsetLeft + contentOffsetInline + offsetParent.offsetLeft,
516
+ insetBlockStart: cellElement.offsetTop + contentOffsetBlock + offsetParent.offsetTop,
517
+ ...cellSize,
518
+ };
244
519
  }
245
520
 
246
521
  //
@@ -248,7 +523,7 @@ export class DxGrid extends LitElement {
248
523
  //
249
524
 
250
525
  @state()
251
- observer = new ResizeObserver((entries) => {
526
+ private observer = new ResizeObserver((entries) => {
252
527
  const { inlineSize, blockSize } = entries?.[0]?.contentBoxSize?.[0] ?? {
253
528
  inlineSize: 0,
254
529
  blockSize: 0,
@@ -261,97 +536,126 @@ export class DxGrid extends LitElement {
261
536
  this.sizeInline = inlineSize;
262
537
  this.sizeBlock = blockSize;
263
538
  this.updateVis();
539
+ queueMicrotask(() => this.updatePos());
264
540
  }
265
541
  });
266
542
 
267
- viewportRef: Ref<HTMLDivElement> = createRef();
543
+ private viewportRef: Ref<HTMLDivElement> = createRef();
268
544
 
269
- handleWheel = ({ deltaX, deltaY }: WheelEvent) => {
270
- this.posInline = Math.max(0, this.posInline + deltaX);
271
- this.posBlock = Math.max(0, this.posBlock + deltaY);
272
- if (
273
- this.posInline >= this.binInlineMin &&
274
- this.posInline < this.binInlineMax &&
275
- this.posBlock >= this.binBlockMin &&
276
- this.posBlock < this.binBlockMax
277
- ) {
278
- // do nothing
279
- } else {
280
- // console.info(
281
- // '[updating bounds]',
282
- // 'wheel',
283
- // [this.binInlineMin, this.posInline, this.binInlineMax],
284
- // [this.binBlockMin, this.posBlock, this.binBlockMax],
285
- // );
286
- this.updateVis();
545
+ private maybeUpdateVisInline = () => {
546
+ if (this.posInline < this.binInlineMin || this.posInline >= this.binInlineMax) {
547
+ this.updateVisInline();
548
+ }
549
+ };
550
+
551
+ private maybeUpdateVisBlock = () => {
552
+ if (this.posBlock < this.binBlockMin || this.posBlock >= this.binBlockMax) {
553
+ this.updateVisBlock();
554
+ }
555
+ };
556
+
557
+ private updatePosInline(inline?: number) {
558
+ this.posInline = Math.max(0, Math.min(this.intrinsicInlineSize - this.sizeInline, inline ?? this.posInline));
559
+ this.maybeUpdateVisInline();
560
+ }
561
+
562
+ private updatePosBlock(block?: number) {
563
+ this.posBlock = Math.max(0, Math.min(this.intrinsicBlockSize - this.sizeBlock, block ?? this.posBlock));
564
+ this.maybeUpdateVisBlock();
565
+ }
566
+
567
+ private updatePos(inline?: number, block?: number) {
568
+ this.updatePosInline(inline);
569
+ this.updatePosBlock(block);
570
+ }
571
+
572
+ private handleWheel = ({ deltaX, deltaY }: Pick<WheelEvent, 'deltaX' | 'deltaY'>) => {
573
+ if (this.mode === 'browse') {
574
+ this.updatePos(this.posInline + deltaX, this.posBlock + deltaY);
287
575
  }
288
576
  };
289
577
 
290
578
  private updateVisInline() {
291
579
  // todo: avoid starting from zero
292
580
  let colIndex = 0;
293
- let pxInline = this.colSize(colIndex);
581
+ let pxInline = this.colSize(colIndex, 'grid');
294
582
 
295
583
  while (pxInline < this.posInline) {
296
584
  colIndex += 1;
297
- pxInline += this.colSize(colIndex) + gap;
585
+ pxInline += this.colSize(colIndex, 'grid') + gap;
298
586
  }
299
587
 
300
588
  this.visColMin = colIndex - overscanCol;
301
589
 
302
- this.binInlineMin = pxInline - this.colSize(colIndex) - gap;
590
+ this.binInlineMin = pxInline - this.colSize(colIndex, 'grid') - gap;
303
591
  this.binInlineMax = pxInline + gap;
304
592
 
305
593
  this.overscanInline =
306
594
  [...Array(overscanCol)].reduce((acc, _, c0) => {
307
- acc += this.colSize(this.visColMin + c0);
595
+ acc += this.colSize(this.visColMin + c0, 'grid');
308
596
  return acc;
309
597
  }, 0) +
310
598
  gap * (overscanCol - 1);
311
599
 
312
- while (pxInline < this.binInlineMax + this.sizeInline + gap) {
600
+ while (pxInline < this.binInlineMax + this.sizeInline - gap * 2) {
313
601
  colIndex += 1;
314
- pxInline += this.colSize(colIndex) + gap;
602
+ pxInline += this.colSize(colIndex, 'grid') + gap;
315
603
  }
316
604
 
317
- this.visColMax = colIndex + overscanCol;
605
+ this.visColMax = Math.min(this.limitColumns, colIndex + overscanCol);
606
+
607
+ this.templateGridColumns = [...Array(this.visColMax - this.visColMin)]
608
+ .map((_, c0) => `${this.colSize(this.visColMin + c0, 'grid')}px`)
609
+ .join(' ');
610
+
611
+ this.templatefrozenColsStart = [...Array(this.frozen.frozenColsStart ?? 0)]
612
+ .map((_, c0) => `${this.colSize(c0, 'frozenColsStart')}px`)
613
+ .join(' ');
318
614
 
319
- this.templateColumns = [...Array(this.visColMax - this.visColMin)]
320
- .map((_, c0) => `${this.colSize(this.visColMin + c0)}px`)
615
+ this.templatefrozenColsEnd = [...Array(this.frozen.frozenColsEnd ?? 0)]
616
+ .map((_, c0) => `${this.colSize(c0, 'frozenColsEnd')}px`)
321
617
  .join(' ');
322
618
  }
323
619
 
324
620
  private updateVisBlock() {
325
621
  // todo: avoid starting from zero
326
622
  let rowIndex = 0;
327
- let pxBlock = this.rowSize(rowIndex);
623
+ let pxBlock = this.rowSize(rowIndex, 'grid');
328
624
 
329
625
  while (pxBlock < this.posBlock) {
330
626
  rowIndex += 1;
331
- pxBlock += this.rowSize(rowIndex) + gap;
627
+ pxBlock += this.rowSize(rowIndex, 'grid') + gap;
332
628
  }
333
629
 
334
630
  this.visRowMin = rowIndex - overscanRow;
335
631
 
336
- this.binBlockMin = pxBlock - this.rowSize(rowIndex) - gap;
632
+ this.binBlockMin = pxBlock - this.rowSize(rowIndex, 'grid') - gap;
337
633
  this.binBlockMax = pxBlock + gap;
338
634
 
339
635
  this.overscanBlock =
340
636
  [...Array(overscanRow)].reduce((acc, _, r0) => {
341
- acc += this.rowSize(this.visRowMin + r0);
637
+ acc += this.rowSize(this.visRowMin + r0, 'grid');
342
638
  return acc;
343
639
  }, 0) +
344
640
  gap * (overscanRow - 1);
345
641
 
346
- while (pxBlock < this.binBlockMax + this.sizeBlock) {
642
+ while (pxBlock < this.binBlockMax + this.sizeBlock - gap * 2) {
347
643
  rowIndex += 1;
348
- pxBlock += this.rowSize(rowIndex) + gap;
644
+ pxBlock += this.rowSize(rowIndex, 'grid') + gap;
349
645
  }
350
646
 
351
- this.visRowMax = rowIndex + overscanRow;
647
+ this.visRowMax = Math.min(this.limitRows, rowIndex + overscanRow);
352
648
 
353
- this.templateRows = [...Array(this.visRowMax - this.visRowMin)]
354
- .map((_, r0) => `${this.rowSize(this.visRowMin + r0)}px`)
649
+ this.templateGridRows = [...Array(this.visRowMax - this.visRowMin)]
650
+ .map((_, r0) => `${this.rowSize(this.visRowMin + r0, 'grid')}px`)
651
+ .join(' ');
652
+
653
+ this.templatefrozenRowsStart = [...Array(this.frozen.frozenRowsStart ?? 0)]
654
+ .map((_, r0) => `${this.rowSize(r0, 'frozenRowsStart')}px`)
655
+ .join(' ');
656
+
657
+ this.templatefrozenRowsEnd = [...Array(this.frozen.frozenRowsEnd ?? 0)]
658
+ .map((_, r0) => `${this.rowSize(r0, 'frozenRowsEnd')}px`)
355
659
  .join(' ');
356
660
  }
357
661
 
@@ -360,130 +664,264 @@ export class DxGrid extends LitElement {
360
664
  this.updateVisBlock();
361
665
  }
362
666
 
363
- // Focus handlers
667
+ private updateCells(includeFixed?: boolean) {
668
+ this.cells.grid = this.getCells!(
669
+ {
670
+ start: { col: this.visColMin, row: this.visRowMin },
671
+ end: { col: this.visColMax, row: this.visRowMax },
672
+ },
673
+ 'grid',
674
+ );
675
+ Object.entries(this.frozen)
676
+ .filter(([_, limit]) => limit && limit > 0)
677
+ .forEach(([plane, limit]) => {
678
+ this.cells[plane as DxGridFrozenPlane] = this.getCells!(
679
+ plane.startsWith('frozenRows')
680
+ ? {
681
+ start: { col: this.visColMin, row: 0 },
682
+ end: { col: this.visColMax, row: limit },
683
+ }
684
+ : {
685
+ start: { col: 0, row: this.visRowMin },
686
+ end: { col: limit, row: this.visRowMax },
687
+ },
688
+ plane as DxGridFrozenPlane,
689
+ );
690
+ });
691
+ if (includeFixed) {
692
+ if ((this.frozen.frozenColsStart ?? 0) > 0 && (this.frozen.frozenRowsStart ?? 0) > 0) {
693
+ this.cells.fixedStartStart = this.getCells!(
694
+ {
695
+ start: { col: 0, row: 0 },
696
+ end: { col: this.frozen.frozenColsStart!, row: this.frozen.frozenRowsStart! },
697
+ },
698
+ 'fixedStartStart',
699
+ );
700
+ }
701
+ if ((this.frozen.frozenColsEnd ?? 0) > 0 && (this.frozen.frozenRowsStart ?? 0) > 0) {
702
+ this.cells.fixedStartEnd = this.getCells!(
703
+ {
704
+ start: { col: 0, row: 0 },
705
+ end: { col: this.frozen.frozenColsEnd!, row: this.frozen.frozenRowsStart! },
706
+ },
707
+ 'fixedStartEnd',
708
+ );
709
+ }
710
+ if ((this.frozen.frozenColsStart ?? 0) > 0 && (this.frozen.frozenRowsEnd ?? 0) > 0) {
711
+ this.cells.fixedEndStart = this.getCells!(
712
+ {
713
+ start: { col: 0, row: 0 },
714
+ end: { col: this.frozen.frozenColsStart!, row: this.frozen.frozenRowsEnd! },
715
+ },
716
+ 'fixedEndStart',
717
+ );
718
+ }
719
+ if ((this.frozen.frozenColsEnd ?? 0) > 0 && (this.frozen.frozenRowsEnd ?? 0) > 0) {
720
+ this.cells.fixedEndEnd = this.getCells!(
721
+ {
722
+ start: { col: 0, row: 0 },
723
+ end: { col: this.frozen.frozenColsEnd!, row: this.frozen.frozenRowsEnd! },
724
+ },
725
+ 'fixedEndEnd',
726
+ );
727
+ }
728
+ }
729
+ }
364
730
 
365
- @state()
366
- focusedCell: Record<DxGridAxis, number> = { col: 0, row: 0 };
731
+ // Focus handlers
367
732
 
368
- @state()
369
- focusActive: boolean = false;
733
+ setFocus(coords: DxGridPosition, snap = true) {
734
+ this.focusedCell = coords;
735
+ this.focusActive = true;
736
+ if (snap) {
737
+ this.snapPosToFocusedCell();
738
+ }
739
+ }
370
740
 
371
- @eventOptions({ capture: true })
372
- handleFocus(event: FocusEvent) {
373
- const target = event.target as HTMLElement;
374
- const action = target.getAttribute('data-dx-grid-action');
375
- if (action === 'cell') {
376
- const c = parseInt(target.getAttribute('aria-colindex') ?? 'never');
377
- const r = parseInt(target.getAttribute('aria-rowindex') ?? 'never');
378
- this.focusedCell = { col: c, row: r };
741
+ private handleFocus(event: FocusEvent) {
742
+ const cellCoords = closestCell(event.target);
743
+ if (cellCoords) {
744
+ this.focusedCell = cellCoords;
379
745
  this.focusActive = true;
380
746
  }
381
747
  }
382
748
 
383
- @eventOptions({ capture: true })
384
- handleBlur(event: FocusEvent) {
749
+ private handleBlur(event: FocusEvent) {
385
750
  // Only unset `focusActive` if focus is not moving to an element within the grid.
386
- if (
387
- !event.relatedTarget ||
388
- (event.relatedTarget as HTMLElement).closest('.dx-grid__viewport') !== this.viewportRef.value
389
- ) {
751
+ if (!event.relatedTarget || !(event.relatedTarget as HTMLElement).closest(`[data-grid="${this.gridId}"]`)) {
390
752
  this.focusActive = false;
391
753
  }
392
754
  }
393
755
 
756
+ private focusedCellElement() {
757
+ return this.viewportRef.value?.querySelector(
758
+ `[data-dx-grid-plane=${this.focusedCell.plane}] > [aria-colindex="${this.focusedCell.col}"][aria-rowindex="${this.focusedCell.row}"]`,
759
+ ) as HTMLElement | null;
760
+ }
761
+
762
+ //
763
+ // `outOfVis` returns by how many rows/cols the focused cell is outside of the `vis` range for an axis, inset by a
764
+ // `delta`, otherwise zero if it is within that range.
765
+ //
766
+
767
+ private focusedCellRowOutOfVis(minDelta = 0, maxDelta = minDelta) {
768
+ return this.focusedCell.row <= this.visRowMin + minDelta
769
+ ? this.focusedCell.row - (this.visRowMin + minDelta)
770
+ : this.focusedCell.row >= this.visRowMax - maxDelta
771
+ ? -(this.focusedCell.row - this.visRowMax - maxDelta)
772
+ : 0;
773
+ }
774
+
775
+ private focusedCellColOutOfVis(minDelta = 0, maxDelta = minDelta) {
776
+ return this.focusedCell.col <= this.visColMin + minDelta
777
+ ? this.focusedCell.col - (this.visColMin + minDelta)
778
+ : this.focusedCell.col >= this.visColMax - maxDelta
779
+ ? -(this.focusedCell.col - this.visColMax - maxDelta)
780
+ : 0;
781
+ }
782
+
783
+ private focusedCellOutOfVis(colDelta = 0, rowDelta = colDelta): { col: number; row: number } {
784
+ switch (this.focusedCell.plane) {
785
+ case 'grid':
786
+ return { row: this.focusedCellRowOutOfVis(rowDelta), col: this.focusedCellColOutOfVis(colDelta) };
787
+ case 'frozenRowsStart':
788
+ case 'frozenRowsEnd':
789
+ return { col: this.focusedCellColOutOfVis(colDelta), row: 0 };
790
+ case 'frozenColsStart':
791
+ case 'frozenColsEnd':
792
+ return { col: 0, row: this.focusedCellRowOutOfVis(rowDelta) };
793
+ default:
794
+ return { col: 0, row: 0 };
795
+ }
796
+ }
797
+
394
798
  /**
395
799
  * Moves focus to the cell with actual focus, otherwise moves focus to the viewport.
396
800
  */
397
- refocus() {
398
- (this.focusedCell.row < this.visRowMin ||
399
- this.focusedCell.row > this.visRowMax ||
400
- this.focusedCell.col < this.visColMin ||
401
- this.focusedCell.col > this.visColMax
402
- ? this.viewportRef.value
403
- : (this.viewportRef.value?.querySelector(
404
- `[aria-colindex="${this.focusedCell.col}"][aria-rowindex="${this.focusedCell.row}"]`,
405
- ) as HTMLElement | null)
406
- )?.focus({ preventScroll: true });
801
+ refocus(increment?: 'col' | 'row', delta: 1 | -1 = 1) {
802
+ switch (increment) {
803
+ case 'row':
804
+ this.focusedCell = { ...this.focusedCell, row: this.focusedCell.row + delta };
805
+ break;
806
+ case 'col':
807
+ this.focusedCell = { ...this.focusedCell, col: this.focusedCell.col + delta };
808
+ }
809
+ if (increment) {
810
+ this.snapPosToFocusedCell();
811
+ }
812
+ queueMicrotask(() => {
813
+ const outOfVis = this.focusedCellOutOfVis(overscanCol, overscanRow);
814
+ (outOfVis.col !== 0 || outOfVis.row !== 0 ? this.viewportRef.value : this.focusedCellElement())?.focus({
815
+ preventScroll: true,
816
+ });
817
+ });
818
+ }
819
+
820
+ private findPosInlineFromVisColMin(deltaCols: number) {
821
+ return [...Array(deltaCols)].reduce(
822
+ (acc, _, c0) => acc - this.colSize(this.visColMin - c0, 'grid') - gap,
823
+ this.binInlineMin + gap,
824
+ );
825
+ }
826
+
827
+ private findPosBlockFromVisRowMin(deltaRows: number) {
828
+ return [...Array(deltaRows)].reduce(
829
+ (acc, _, r0) => acc - this.rowSize(this.visRowMin - r0, 'grid') - gap,
830
+ this.binBlockMin + gap,
831
+ );
407
832
  }
408
833
 
409
834
  /**
410
835
  * Updates `pos` so that a cell in focus is fully within the viewport
411
836
  */
412
837
  snapPosToFocusedCell() {
413
- if (
414
- this.focusedCell.col < this.visColMin ||
415
- this.focusedCell.col > this.visColMax ||
416
- this.focusedCell.row < this.visRowMin ||
417
- this.focusedCell.row > this.visRowMax
418
- ) {
419
- // console.warn('Snapping position to a focused cell that is not already mounted is unsupported.');
420
- } else if (
421
- this.focusedCell.col > this.visColMin + overscanCol &&
422
- this.focusedCell.col < this.visColMax - overscanCol - 1 &&
423
- this.focusedCell.row > this.visRowMin + overscanRow &&
424
- this.focusedCell.row < this.visRowMax - overscanRow - 1
425
- ) {
426
- // console.log(
427
- // '[within bounds]',
428
- // this.focusedCell,
429
- // [this.visColMin, this.visColMax, overscanCol],
430
- // [this.visRowMin, this.visRowMax, overscanRow],
431
- // );
432
- } else {
433
- if (this.focusedCell.col <= this.visColMin + overscanCol) {
434
- this.posInline = this.binInlineMin;
435
- this.updateVisInline();
436
- } else if (this.focusedCell.col >= this.visColMax - overscanCol - 1) {
437
- const sizeSumCol = [...Array(this.focusedCell.col - this.visColMin)].reduce((acc, _, c0) => {
438
- acc += this.colSize(this.visColMin + overscanCol + c0) + gap;
439
- return acc;
440
- }, 0);
441
- this.posInline = this.binInlineMin + sizeSumCol + gap * 2 - this.sizeInline;
442
- this.updateVisInline();
443
- }
838
+ const outOfVis = this.focusedCellOutOfVis(overscanCol, overscanRow);
839
+ if (outOfVis.col < 0) {
840
+ this.posInline = this.findPosInlineFromVisColMin(-outOfVis.col);
841
+ this.updateVisInline();
842
+ } else if (outOfVis.col > 0) {
843
+ const sizeSumCol = [...Array(this.focusedCell.col - this.visColMin)].reduce((acc, _, c0) => {
844
+ acc += this.colSize(this.visColMin + overscanCol + c0, 'grid') + gap;
845
+ return acc;
846
+ }, 0);
847
+ this.posInline = Math.max(
848
+ 0,
849
+ Math.min(this.intrinsicInlineSize - this.sizeInline, this.binInlineMin + sizeSumCol - this.sizeInline),
850
+ );
851
+ this.updateVisInline();
852
+ }
444
853
 
445
- if (this.focusedCell.row <= this.visRowMin + overscanRow) {
446
- this.posBlock = this.binBlockMin;
447
- this.updateVisBlock();
448
- } else if (this.focusedCell.row >= this.visRowMax - overscanRow - 1) {
449
- const sizeSumRow = [...Array(this.focusedCell.row - this.visRowMin)].reduce((acc, _, r0) => {
450
- acc += this.rowSize(this.visRowMin + overscanRow + r0) + gap;
451
- return acc;
452
- }, 0);
453
- this.posBlock = this.binBlockMin + sizeSumRow + gap * 2 - this.sizeBlock;
454
- this.updateVisBlock();
455
- }
854
+ if (outOfVis.row < 0) {
855
+ this.posBlock = this.findPosBlockFromVisRowMin(-outOfVis.row);
856
+ this.updateVisBlock();
857
+ } else if (outOfVis.row > 0) {
858
+ const sizeSumRow = [...Array(this.focusedCell.row - this.visRowMin)].reduce((acc, _, r0) => {
859
+ acc += this.rowSize(this.visRowMin + overscanRow + r0, 'grid') + gap;
860
+ return acc;
861
+ }, 0);
862
+ this.posBlock = Math.max(
863
+ 0,
864
+ Math.min(this.intrinsicBlockSize - this.sizeBlock, this.binBlockMin + sizeSumRow - this.sizeBlock),
865
+ );
866
+ this.updateVisBlock();
456
867
  }
457
868
  }
458
869
 
459
- // Keyboard interactions
460
- handleKeydown(event: KeyboardEvent) {
461
- if (this.focusActive) {
462
- // Adjust state
463
- switch (event.key) {
464
- case 'ArrowDown':
465
- this.focusedCell = { ...this.focusedCell, row: this.focusedCell.row + 1 };
466
- break;
467
- case 'ArrowUp':
468
- this.focusedCell = { ...this.focusedCell, row: Math.max(0, this.focusedCell.row - 1) };
469
- break;
470
- case 'ArrowRight':
471
- this.focusedCell = { ...this.focusedCell, col: this.focusedCell.col + 1 };
472
- break;
473
- case 'ArrowLeft':
474
- this.focusedCell = { ...this.focusedCell, col: Math.max(0, this.focusedCell.col - 1) };
475
- break;
476
- }
477
- // Handle virtualization & focus consequences
478
- switch (event.key) {
479
- case 'ArrowDown':
480
- case 'ArrowUp':
481
- case 'ArrowRight':
482
- case 'ArrowLeft':
483
- event.preventDefault();
484
- this.snapPosToFocusedCell();
485
- break;
486
- }
870
+ //
871
+ // Map scroll DOM methods to virtualized value.
872
+ //
873
+
874
+ override get scrollLeft() {
875
+ return this.posInline;
876
+ }
877
+
878
+ override set scrollLeft(nextValue: number) {
879
+ this.posInline = nextValue;
880
+ this.maybeUpdateVisInline();
881
+ }
882
+
883
+ override get scrollTop() {
884
+ return this.posBlock;
885
+ }
886
+
887
+ override set scrollTop(nextValue: number) {
888
+ this.posBlock = nextValue;
889
+ this.maybeUpdateVisBlock();
890
+ }
891
+
892
+ //
893
+ // Resize handlers
894
+ //
895
+
896
+ private axisResizeable(plane: 'grid' | DxGridFrozenPlane, axis: DxGridAxis, index: number | string) {
897
+ return axis === 'col'
898
+ ? !!(this.columns[plane]?.[index]?.resizeable ?? this.columnDefault[plane as DxGridFrozenColsPlane]?.resizeable)
899
+ : !!(this.rows[plane]?.[index]?.resizeable ?? this.rowDefault[plane as DxGridFrozenRowsPlane]?.resizeable);
900
+ }
901
+
902
+ private handleAxisResizeInternal(event: DxAxisResizeInternal) {
903
+ event.stopPropagation();
904
+ const { plane, axis, delta, size, index, state } = event;
905
+ if (axis === 'col') {
906
+ const nextSize = Math.max(sizeColMin, Math.min(sizeColMax, size + delta));
907
+ this.colSizes = { ...this.colSizes, [plane]: { ...this.colSizes[plane], [index]: nextSize } };
908
+ this.updateVisInline();
909
+ this.updateIntrinsicInlineSize();
910
+ } else {
911
+ const nextSize = Math.max(sizeRowMin, Math.min(sizeRowMax, size + delta));
912
+ this.rowSizes = { ...this.colSizes, [plane]: { ...this.rowSizes[plane], [index]: nextSize } };
913
+ this.updateVisBlock();
914
+ this.updateIntrinsicBlockSize();
915
+ }
916
+ if (state === 'dropped') {
917
+ this.dispatchEvent(
918
+ new DxAxisResize({
919
+ plane,
920
+ axis,
921
+ index,
922
+ size: this[`${axis}Size`](index, plane),
923
+ }),
924
+ );
487
925
  }
488
926
  }
489
927
 
@@ -491,116 +929,259 @@ export class DxGrid extends LitElement {
491
929
  // Render and other lifecycle methods
492
930
  //
493
931
 
932
+ private renderFixed(plane: DxGridFixedPlane, selection: DxGridSelectionProps) {
933
+ const colPlane = resolveColPlane(plane) as DxGridFrozenPlane;
934
+ const rowPlane = resolveRowPlane(plane) as DxGridFrozenPlane;
935
+ const cols = this.frozen[colPlane];
936
+ const rows = this.frozen[rowPlane];
937
+ return (cols ?? 0) > 0 && (rows ?? 0) > 0
938
+ ? html`<div
939
+ role="none"
940
+ data-dx-grid-plane=${plane}
941
+ class="dx-grid__plane--fixed"
942
+ style=${styleMap({
943
+ 'grid-template-columns': this[`template${colPlane}`],
944
+ 'grid-template-rows': this[`template${rowPlane}`],
945
+ })}
946
+ >
947
+ ${[...Array(cols)].map((_, c) => {
948
+ return [...Array(rows)].map((_, r) => {
949
+ return this.renderCell(c, r, plane, cellSelected(c, r, plane, selection));
950
+ });
951
+ })}
952
+ </div>`
953
+ : null;
954
+ }
955
+
956
+ private renderFrozenRows(
957
+ plane: DxGridFrozenRowsPlane,
958
+ visibleCols: number,
959
+ offsetInline: number,
960
+ selection: DxGridSelectionProps,
961
+ ) {
962
+ const rowPlane = resolveRowPlane(plane) as DxGridFrozenPlane;
963
+ const rows = this.frozen[rowPlane];
964
+ return (rows ?? 0) > 0
965
+ ? html`<div role="none" class="dx-grid__plane--frozen-row">
966
+ <div
967
+ role="none"
968
+ data-dx-grid-plane=${plane}
969
+ class="dx-grid__plane--frozen-row__content"
970
+ style="transform:translate3d(${offsetInline}px,0,0);grid-template-columns:${this
971
+ .templateGridColumns};grid-template-rows:${this[`template${rowPlane}`]}"
972
+ >
973
+ ${[...Array(visibleCols)].map((_, c0) => {
974
+ return [...Array(rows)].map((_, r) => {
975
+ const c = this.visColMin + c0;
976
+ return this.renderCell(c, r, plane, cellSelected(c, r, plane, selection), c0, r);
977
+ });
978
+ })}
979
+ </div>
980
+ </div>`
981
+ : null;
982
+ }
983
+
984
+ private renderFrozenColumns(
985
+ plane: DxGridFrozenColsPlane,
986
+ visibleRows: number,
987
+ offsetBlock: number,
988
+ selection: DxGridSelectionProps,
989
+ ) {
990
+ const colPlane = resolveColPlane(plane) as DxGridFrozenPlane;
991
+ const cols = this.frozen[colPlane];
992
+ return (cols ?? 0) > 0
993
+ ? html`<div role="none" class="dx-grid__plane--frozen-col">
994
+ <div
995
+ role="none"
996
+ data-dx-grid-plane=${plane}
997
+ class="dx-grid__plane--frozen-col__content"
998
+ style="transform:translate3d(0,${offsetBlock}px,0);grid-template-rows:${this
999
+ .templateGridRows};grid-template-columns:${this[`template${colPlane}`]}"
1000
+ >
1001
+ ${[...Array(cols)].map((_, c) => {
1002
+ return [...Array(visibleRows)].map((_, r0) => {
1003
+ const r = this.visRowMin + r0;
1004
+ return this.renderCell(c, r, plane, cellSelected(c, r, plane, selection), c, r0);
1005
+ });
1006
+ })}
1007
+ </div>
1008
+ </div>`
1009
+ : null;
1010
+ }
1011
+
1012
+ private renderCell(col: number, row: number, plane: DxGridPlane, selected?: boolean, visCol = col, visRow = row) {
1013
+ const cell = this.cell(col, row, plane);
1014
+ const active = this.cellActive(col, row, plane);
1015
+ const resizeIndex = cell?.resizeHandle ? (cell.resizeHandle === 'col' ? col : row) : undefined;
1016
+ const resizePlane = cell?.resizeHandle ? resolveResizePlane(cell.resizeHandle, plane) : undefined;
1017
+ return html`<div
1018
+ role="gridcell"
1019
+ tabindex="0"
1020
+ ?inert=${col < 0 || row < 0}
1021
+ ?aria-selected=${selected}
1022
+ class=${cell || active
1023
+ ? (cell?.className ? cell.className + ' ' : '') + (active ? 'dx-grid__cell--active' : '')
1024
+ : nothing}
1025
+ aria-colindex=${col}
1026
+ aria-rowindex=${row}
1027
+ data-dx-grid-action="cell"
1028
+ style="grid-column:${visCol + 1};grid-row:${visRow + 1}"
1029
+ >
1030
+ ${cell?.value}${cell?.resizeHandle && this.axisResizeable(resizePlane!, cell.resizeHandle, resizeIndex!)
1031
+ ? html`<dx-grid-axis-resize-handle
1032
+ axis=${cell.resizeHandle}
1033
+ plane=${resizePlane}
1034
+ index=${resizeIndex}
1035
+ size=${this[`${cell.resizeHandle}Size`](resizeIndex!, plane)}
1036
+ ></dx-grid-axis-resize-handle>`
1037
+ : null}
1038
+ </div>`;
1039
+ }
1040
+
494
1041
  override render() {
495
1042
  const visibleCols = this.visColMax - this.visColMin;
496
1043
  const visibleRows = this.visRowMax - this.visRowMin;
497
- const offsetInline = gap + this.binInlineMin - this.posInline - this.overscanInline;
498
- const offsetBlock = gap + this.binBlockMin - this.posBlock - this.overscanBlock;
1044
+ const offsetInline = this.binInlineMin - this.posInline - this.overscanInline;
1045
+ const offsetBlock = this.binBlockMin - this.posBlock - this.overscanBlock;
1046
+ const selection = selectionProps(this.selectionStart, this.selectionEnd);
499
1047
 
500
1048
  return html`<div
501
1049
  role="none"
502
1050
  class="dx-grid"
503
- @pointerdown=${this.handlePointerDown}
504
- @pointerup=${this.handlePointerUp}
505
- @pointermove=${this.handlePointerMove}
506
- @focus=${this.handleFocus}
507
- @blur=${this.handleBlur}
508
- @keydown=${this.handleKeydown}
1051
+ style=${styleMap({
1052
+ 'grid-template-columns': `${this.templatefrozenColsStart ? 'min-content ' : ''}minmax(0, ${
1053
+ Number.isFinite(this.limitColumns) ? `${this.intrinsicInlineSize}px` : '1fr'
1054
+ })${this.templatefrozenColsEnd ? ' min-content' : ''}`,
1055
+ 'grid-template-rows': `${this.templatefrozenRowsStart ? 'min-content ' : ''}minmax(0, ${
1056
+ Number.isFinite(this.limitRows) ? `${this.intrinsicBlockSize}px` : '1fr'
1057
+ })${this.templatefrozenRowsEnd ? ' min-content' : ''}`,
1058
+ })}
1059
+ data-grid=${this.gridId}
1060
+ data-grid-mode=${this.mode}
1061
+ ?data-grid-select=${selection.visible}
509
1062
  >
510
- <div role="none" class="dx-grid__corner"></div>
511
- <div role="none" class="dx-grid__columnheader">
512
- <div
513
- role="none"
514
- class="dx-grid__columnheader__content"
515
- style="transform:translate3d(${offsetInline}px,0,0);grid-template-columns:${this.templateColumns};"
516
- >
517
- ${[...Array(visibleCols)].map((_, c0) => {
518
- const c = this.visColMin + c0;
519
- return html`<div
520
- role="columnheader"
521
- ?inert=${c < 0}
522
- style="block-size:${this.rowDefault.size}px;grid-column:${c0 + 1}/${c0 + 2};"
523
- >
524
- <span id=${localChId(c0)}>${colToA1Notation(c)}</span>
525
- ${(this.columns[c]?.resizeable ?? this.columnDefault.resizeable) &&
526
- html`<button class="dx-grid__resize-handle" data-dx-grid-action=${`resize-col,${c}`}>
527
- <span class="sr-only">Resize</span>
528
- </button>`}
529
- </div>`;
530
- })}
531
- </div>
532
- </div>
533
- <div role="none" class="dx-grid__corner"></div>
534
- <div role="none" class="dx-grid__rowheader">
1063
+ ${this.renderFixed('fixedStartStart', selection)}${this.renderFrozenRows(
1064
+ 'frozenRowsStart',
1065
+ visibleCols,
1066
+ offsetInline,
1067
+ selection,
1068
+ )}${this.renderFixed('fixedStartEnd', selection)}${this.renderFrozenColumns(
1069
+ 'frozenColsStart',
1070
+ visibleRows,
1071
+ offsetBlock,
1072
+ selection,
1073
+ )}
1074
+ <div role="grid" class="dx-grid__plane--grid" tabindex="0" ${ref(this.viewportRef)}>
535
1075
  <div
536
1076
  role="none"
537
- class="dx-grid__rowheader__content"
538
- style="transform:translate3d(0,${offsetBlock}px,0);grid-template-rows:${this.templateRows};"
539
- >
540
- ${[...Array(visibleRows)].map((_, r0) => {
541
- const r = this.visRowMin + r0;
542
- return html`<div role="rowheader" ?inert=${r < 0} style="grid-row:${r0 + 1}/${r0 + 2}">
543
- <span id=${localRhId(r0)}>${rowToA1Notation(r)}</span>
544
- ${(this.rows[r]?.resizeable ?? this.rowDefault.resizeable) &&
545
- html`<button class="dx-grid__resize-handle" data-dx-grid-action=${`resize-row,${r}`}>
546
- <span class="sr-only">Resize</span>
547
- </button>`}
548
- </div>`;
549
- })}
550
- </div>
551
- </div>
552
- <div role="grid" class="dx-grid__viewport" tabindex="0" @wheel=${this.handleWheel} ${ref(this.viewportRef)}>
553
- <div
554
- role="none"
555
- class="dx-grid__content"
1077
+ class="dx-grid__plane--grid__content"
1078
+ data-dx-grid-plane="grid"
556
1079
  style="transform:translate3d(${offsetInline}px,${offsetBlock}px,0);grid-template-columns:${this
557
- .templateColumns};grid-template-rows:${this.templateRows};"
1080
+ .templateGridColumns};grid-template-rows:${this.templateGridRows};"
558
1081
  >
559
1082
  ${[...Array(visibleCols)].map((_, c0) => {
560
1083
  return [...Array(visibleRows)].map((_, r0) => {
561
1084
  const c = c0 + this.visColMin;
562
1085
  const r = r0 + this.visRowMin;
563
- const cell = this.getCell(c, r);
564
- return html`<div
565
- role="gridcell"
566
- tabindex="0"
567
- ?inert=${c < 0 || r < 0}
568
- aria-rowindex=${r}
569
- aria-colindex=${c}
570
- data-dx-grid-action="cell"
571
- style="grid-column:${c0 + 1};grid-row:${r0 + 1}"
572
- >
573
- ${cell?.value}
574
- </div>`;
1086
+ return this.renderCell(c, r, 'grid', cellSelected(c, r, 'grid', selection), c0, r0);
575
1087
  });
576
1088
  })}
577
1089
  </div>
578
1090
  </div>
579
- <div role="none" class="dx-grid__scrollbar" aria-orientation="vertical">
580
- <div role="none" class="dx-grid__scrollbar__thumb"></div>
581
- </div>
582
- <div role="none" class="dx-grid__corner"></div>
583
- <div role="none" class="dx-grid__scrollbar" aria-orientation="horizontal">
584
- <div role="none" class="dx-grid__scrollbar__thumb"></div>
585
- </div>
586
- <div role="none" class="dx-grid__corner"></div>
1091
+ ${this.renderFrozenColumns('frozenColsEnd', visibleRows, offsetBlock, selection)}${this.renderFixed(
1092
+ 'fixedEndStart',
1093
+ selection,
1094
+ )}${this.renderFrozenRows('frozenRowsEnd', visibleCols, offsetInline, selection)}${this.renderFixed(
1095
+ 'fixedEndEnd',
1096
+ selection,
1097
+ )}
587
1098
  </div>`;
588
1099
  }
589
1100
 
1101
+ private updateIntrinsicInlineSize() {
1102
+ this.intrinsicInlineSize = Number.isFinite(this.limitColumns)
1103
+ ? [...Array(this.limitColumns)].reduce((acc, _, c0) => acc + this.colSize(c0, 'grid'), 0) +
1104
+ gap * (this.limitColumns - 1)
1105
+ : Infinity;
1106
+ }
1107
+
1108
+ private updateIntrinsicBlockSize() {
1109
+ this.intrinsicBlockSize = Number.isFinite(this.limitRows)
1110
+ ? [...Array(this.limitRows)].reduce((acc, _, r0) => acc + this.rowSize(r0, 'grid'), 0) +
1111
+ gap * (this.limitRows - 1)
1112
+ : Infinity;
1113
+ }
1114
+
1115
+ private updateIntrinsicSizes() {
1116
+ this.updateIntrinsicInlineSize();
1117
+ this.updateIntrinsicBlockSize();
1118
+ }
1119
+
590
1120
  override firstUpdated() {
1121
+ if (this.getCells) {
1122
+ this.updateCells(true);
1123
+ }
591
1124
  this.observer.observe(this.viewportRef.value!);
592
- this.colSizes = Object.entries(this.columns).reduce((acc: Record<string, number>, [colId, colMeta]) => {
593
- if (colMeta?.size) {
594
- acc[colId] = colMeta.size;
595
- }
596
- return acc;
597
- }, {});
598
- this.rowSizes = Object.entries(this.rows).reduce((acc: Record<string, number>, [rowId, rowMeta]) => {
599
- if (rowMeta?.size) {
600
- acc[rowId] = rowMeta.size;
601
- }
602
- return acc;
603
- }, {});
1125
+ this.colSizes = Object.entries(this.columns).reduce(
1126
+ (acc: DxGridAxisSizes, [plane, planeColMeta]) => {
1127
+ acc[plane as 'grid' | DxGridFrozenPlane] = Object.entries(planeColMeta).reduce(
1128
+ (planeAcc: Record<string, number>, [col, colMeta]) => {
1129
+ if (colMeta?.size) {
1130
+ planeAcc[col] = colMeta.size;
1131
+ }
1132
+ return planeAcc;
1133
+ },
1134
+ {},
1135
+ );
1136
+ return acc;
1137
+ },
1138
+ { grid: {} },
1139
+ );
1140
+ this.rowSizes = Object.entries(this.rows).reduce(
1141
+ (acc: DxGridAxisSizes, [plane, planeRowMeta]) => {
1142
+ acc[plane as 'grid' | DxGridFrozenPlane] = Object.entries(planeRowMeta).reduce(
1143
+ (planeAcc: Record<string, number>, [row, rowMeta]) => {
1144
+ if (rowMeta?.size) {
1145
+ planeAcc[row] = rowMeta.size;
1146
+ }
1147
+ return planeAcc;
1148
+ },
1149
+ {},
1150
+ );
1151
+ return acc;
1152
+ },
1153
+ { grid: {} },
1154
+ );
1155
+ this.updateIntrinsicSizes();
1156
+ }
1157
+
1158
+ override willUpdate(changedProperties: Map<string, any>) {
1159
+ if (
1160
+ this.getCells &&
1161
+ (changedProperties.has('initialCells') ||
1162
+ changedProperties.has('visColMin') ||
1163
+ changedProperties.has('visColMax') ||
1164
+ changedProperties.has('visRowMin') ||
1165
+ changedProperties.has('visRowMax'))
1166
+ ) {
1167
+ this.updateCells();
1168
+ }
1169
+
1170
+ if (changedProperties.has('rowDefault') || changedProperties.has('rows') || changedProperties.has('limitRows')) {
1171
+ this.updateIntrinsicBlockSize();
1172
+ this.updatePosBlock();
1173
+ this.updateVisBlock();
1174
+ }
1175
+
1176
+ if (
1177
+ changedProperties.has('colDefault') ||
1178
+ changedProperties.has('columns') ||
1179
+ changedProperties.has('limitColumns')
1180
+ ) {
1181
+ this.updateIntrinsicInlineSize();
1182
+ this.updatePosInline();
1183
+ this.updateVisInline();
1184
+ }
604
1185
  }
605
1186
 
606
1187
  override updated(changedProperties: Map<string, any>) {
@@ -613,10 +1194,16 @@ export class DxGrid extends LitElement {
613
1194
  }
614
1195
  }
615
1196
 
1197
+ public updateIfWithinBounds({ col, row }: { col: number; row: number }): boolean {
1198
+ if (col >= this.visColMin && col <= this.visColMax && row >= this.visRowMin && row <= this.visRowMax) {
1199
+ this.requestUpdate();
1200
+ return true;
1201
+ }
1202
+ return false;
1203
+ }
1204
+
616
1205
  override disconnectedCallback() {
617
1206
  super.disconnectedCallback();
618
- // console.log('[disconnected]', this.viewportRef.value);
619
- // TODO(thure): Will this even work?
620
1207
  if (this.viewportRef.value) {
621
1208
  this.observer.unobserve(this.viewportRef.value);
622
1209
  }
@@ -626,3 +1213,5 @@ export class DxGrid extends LitElement {
626
1213
  return this;
627
1214
  }
628
1215
  }
1216
+
1217
+ export { rowToA1Notation, colToA1Notation } from './util';