@dxos/lit-grid 0.6.13 → 0.6.14-main.2b6a0f3

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