@dxos/lit-grid 0.6.13 → 0.6.14-main.1366248

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