@dxos/lit-grid 0.6.11 → 0.6.12-main.15a606f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/index.mjs +283 -103
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/dx-grid.d.ts +41 -58
- package/dist/types/src/dx-grid.d.ts.map +1 -1
- package/dist/types/src/dx-grid.lit-stories.d.ts +4 -1
- package/dist/types/src/dx-grid.lit-stories.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +51 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +7 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/dx-grid.lit-stories.ts +10 -7
- package/src/dx-grid.pcss +27 -3
- package/src/dx-grid.ts +280 -154
- package/src/types.ts +69 -0
- package/src/util.ts +12 -0
package/src/dx-grid.ts
CHANGED
|
@@ -2,11 +2,24 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { LitElement, html } from 'lit';
|
|
5
|
+
import { LitElement, html, nothing } from 'lit';
|
|
6
6
|
import { customElement, state, property, eventOptions } from 'lit/decorators.js';
|
|
7
7
|
import { ref, createRef, type Ref } from 'lit/directives/ref.js';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type AxisMeta,
|
|
11
|
+
type CellIndex,
|
|
12
|
+
type CellValue,
|
|
13
|
+
DxAxisResize,
|
|
14
|
+
DxEditRequest,
|
|
15
|
+
type DxGridAxis,
|
|
16
|
+
DxGridCellsSelect,
|
|
17
|
+
type DxGridMode,
|
|
18
|
+
type DxGridPointer,
|
|
19
|
+
type DxGridPosition,
|
|
20
|
+
type DxGridPositionNullable,
|
|
21
|
+
} from './types';
|
|
22
|
+
import { separator, toCellIndex } from './util';
|
|
10
23
|
|
|
11
24
|
/**
|
|
12
25
|
* The size in pixels of the gap between cells
|
|
@@ -33,11 +46,6 @@ const sizeColMax = 1024;
|
|
|
33
46
|
const sizeRowMin = 16;
|
|
34
47
|
const sizeRowMax = 1024;
|
|
35
48
|
|
|
36
|
-
/**
|
|
37
|
-
* Separator for serializing cell position vectors
|
|
38
|
-
*/
|
|
39
|
-
const separator = ',';
|
|
40
|
-
|
|
41
49
|
//
|
|
42
50
|
// A1 notation is the fallback for numbering columns and rows.
|
|
43
51
|
//
|
|
@@ -53,28 +61,30 @@ const rowToA1Notation = (row: number): string => {
|
|
|
53
61
|
return `${row + 1}`;
|
|
54
62
|
};
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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;
|
|
64
|
+
const closestAction = (target: EventTarget | null): { action: string | null; actionEl: HTMLElement | null } => {
|
|
65
|
+
const actionEl: HTMLElement | null = (target as HTMLElement | null)?.closest('[data-dx-grid-action]') ?? null;
|
|
66
|
+
return { actionEl, action: actionEl?.getAttribute('data-dx-grid-action') ?? null };
|
|
69
67
|
};
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
const closestCell = (target: EventTarget | null, actionEl?: HTMLElement | null): DxGridPositionNullable => {
|
|
70
|
+
let cellElement = actionEl;
|
|
71
|
+
if (!cellElement) {
|
|
72
|
+
const { action, actionEl } = closestAction(target);
|
|
73
|
+
if (action === 'cell') {
|
|
74
|
+
cellElement = actionEl as HTMLElement;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (cellElement) {
|
|
78
|
+
const col = parseInt(cellElement.getAttribute('aria-colindex') ?? 'never');
|
|
79
|
+
const row = parseInt(cellElement.getAttribute('aria-rowindex') ?? 'never');
|
|
80
|
+
return { col, row };
|
|
81
|
+
} else {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
75
84
|
};
|
|
76
85
|
|
|
77
|
-
|
|
86
|
+
const isSameCell = (a: DxGridPositionNullable, b: DxGridPositionNullable) =>
|
|
87
|
+
a && b && Number.isFinite(a.col) && Number.isFinite(a.row) && a.col === b.col && a.row === b.row;
|
|
78
88
|
|
|
79
89
|
const localChId = (c0: number) => `ch--${c0}`;
|
|
80
90
|
const localRhId = (r0: number) => `rh--${r0}`;
|
|
@@ -83,6 +93,9 @@ const getPage = (axis: string, event: PointerEvent) => (axis === 'col' ? event.p
|
|
|
83
93
|
|
|
84
94
|
@customElement('dx-grid')
|
|
85
95
|
export class DxGrid extends LitElement {
|
|
96
|
+
@property({ type: String })
|
|
97
|
+
gridId: string = 'default-grid-id';
|
|
98
|
+
|
|
86
99
|
@property({ type: Object })
|
|
87
100
|
rowDefault: AxisMeta = { size: 32 };
|
|
88
101
|
|
|
@@ -96,53 +109,56 @@ export class DxGrid extends LitElement {
|
|
|
96
109
|
columns: Record<string, AxisMeta> = {};
|
|
97
110
|
|
|
98
111
|
@property({ type: Object })
|
|
99
|
-
cells: Record<
|
|
112
|
+
cells: Record<CellIndex, CellValue> = {};
|
|
113
|
+
|
|
114
|
+
@property({ type: String })
|
|
115
|
+
mode: DxGridMode = 'browse';
|
|
100
116
|
|
|
101
117
|
//
|
|
102
118
|
// `pos`, short for ‘position’, is the position in pixels of the viewport from the origin.
|
|
103
119
|
//
|
|
104
120
|
|
|
105
121
|
@state()
|
|
106
|
-
posInline = 0;
|
|
122
|
+
private posInline = 0;
|
|
107
123
|
|
|
108
124
|
@state()
|
|
109
|
-
posBlock = 0;
|
|
125
|
+
private posBlock = 0;
|
|
110
126
|
|
|
111
127
|
//
|
|
112
128
|
// `size` (when not suffixed with ‘row’ or ‘col’, see above) is the size in pixels of the viewport.
|
|
113
129
|
//
|
|
114
130
|
|
|
115
131
|
@state()
|
|
116
|
-
sizeInline = 0;
|
|
132
|
+
private sizeInline = 0;
|
|
117
133
|
|
|
118
134
|
@state()
|
|
119
|
-
sizeBlock = 0;
|
|
135
|
+
private sizeBlock = 0;
|
|
120
136
|
|
|
121
137
|
//
|
|
122
138
|
// `overscan` is the amount in pixels to offset the grid content due to the number of overscanned columns or rows.
|
|
123
139
|
//
|
|
124
140
|
|
|
125
141
|
@state()
|
|
126
|
-
overscanInline = 0;
|
|
142
|
+
private overscanInline = 0;
|
|
127
143
|
|
|
128
144
|
@state()
|
|
129
|
-
overscanBlock = 0;
|
|
145
|
+
private overscanBlock = 0;
|
|
130
146
|
|
|
131
147
|
//
|
|
132
148
|
// `bin`, not short for anything, is the range in pixels within which virtualization does not need to reassess.
|
|
133
149
|
//
|
|
134
150
|
|
|
135
151
|
@state()
|
|
136
|
-
binInlineMin = 0;
|
|
152
|
+
private binInlineMin = 0;
|
|
137
153
|
|
|
138
154
|
@state()
|
|
139
|
-
binInlineMax = this.colSize(0);
|
|
155
|
+
private binInlineMax = this.colSize(0);
|
|
140
156
|
|
|
141
157
|
@state()
|
|
142
|
-
binBlockMin = 0;
|
|
158
|
+
private binBlockMin = 0;
|
|
143
159
|
|
|
144
160
|
@state()
|
|
145
|
-
binBlockMax = this.rowSize(0);
|
|
161
|
+
private binBlockMax = this.rowSize(0);
|
|
146
162
|
|
|
147
163
|
//
|
|
148
164
|
// `vis`, short for ‘visible’, is the range in numeric index of the columns or rows which should be rendered within
|
|
@@ -150,83 +166,185 @@ export class DxGrid extends LitElement {
|
|
|
150
166
|
//
|
|
151
167
|
|
|
152
168
|
@state()
|
|
153
|
-
visColMin = 0;
|
|
169
|
+
private visColMin = 0;
|
|
154
170
|
|
|
155
171
|
@state()
|
|
156
|
-
visColMax = 1;
|
|
172
|
+
private visColMax = 1;
|
|
157
173
|
|
|
158
174
|
@state()
|
|
159
|
-
visRowMin = 0;
|
|
175
|
+
private visRowMin = 0;
|
|
160
176
|
|
|
161
177
|
@state()
|
|
162
|
-
visRowMax = 1;
|
|
178
|
+
private visRowMax = 1;
|
|
163
179
|
|
|
164
180
|
//
|
|
165
181
|
// `template` is the rendered value of `grid-{axis}-template`.
|
|
166
182
|
//
|
|
167
183
|
@state()
|
|
168
|
-
templateColumns = `${this.colSize(0)}px`;
|
|
184
|
+
private templateColumns = `${this.colSize(0)}px`;
|
|
169
185
|
|
|
170
186
|
@state()
|
|
171
|
-
templateRows = `${this.rowSize(0)}px`;
|
|
187
|
+
private templateRows = `${this.rowSize(0)}px`;
|
|
172
188
|
|
|
173
189
|
//
|
|
174
|
-
//
|
|
190
|
+
// Focus, selection, and resize states
|
|
175
191
|
//
|
|
176
192
|
|
|
177
193
|
@state()
|
|
178
|
-
|
|
194
|
+
private pointer: DxGridPointer = null;
|
|
179
195
|
|
|
180
196
|
@state()
|
|
181
|
-
|
|
197
|
+
private colSizes: Record<string, number> = {};
|
|
182
198
|
|
|
183
199
|
@state()
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
200
|
+
private rowSizes: Record<string, number> = {};
|
|
201
|
+
|
|
202
|
+
@state()
|
|
203
|
+
private focusActive: boolean = false;
|
|
204
|
+
|
|
205
|
+
@state()
|
|
206
|
+
private focusedCell: DxGridPosition = { col: 0, row: 0 };
|
|
207
|
+
|
|
208
|
+
@state()
|
|
209
|
+
private selectionStart: DxGridPosition = { col: 0, row: 0 };
|
|
210
|
+
|
|
211
|
+
@state()
|
|
212
|
+
private selectionEnd: DxGridPosition = { col: 0, row: 0 };
|
|
213
|
+
|
|
214
|
+
//
|
|
215
|
+
// Primary pointer and keyboard handlers
|
|
216
|
+
//
|
|
217
|
+
|
|
218
|
+
private dispatchEditRequest(initialContent?: string) {
|
|
219
|
+
this.snapPosToFocusedCell();
|
|
220
|
+
// Without deferring, the event dispatches before `focusedCellBox` can get updated bounds of the cell, hence:
|
|
221
|
+
queueMicrotask(() =>
|
|
222
|
+
this.dispatchEvent(
|
|
223
|
+
new DxEditRequest({
|
|
224
|
+
cellIndex: toCellIndex(this.focusedCell),
|
|
225
|
+
cellBox: this.focusedCellBox(),
|
|
226
|
+
initialContent,
|
|
227
|
+
}),
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private handlePointerDown = (event: PointerEvent) => {
|
|
233
|
+
if (event.isPrimary) {
|
|
234
|
+
const { action, actionEl } = closestAction(event.target);
|
|
235
|
+
if (action) {
|
|
236
|
+
if (action.startsWith('resize') && this.mode === 'browse') {
|
|
237
|
+
const [resize, index] = action.split(',');
|
|
238
|
+
const [_, axis] = resize.split('-');
|
|
239
|
+
this.pointer = {
|
|
240
|
+
state: 'resizing',
|
|
241
|
+
axis: axis as DxGridAxis,
|
|
242
|
+
size: axis === 'col' ? this.colSize(index) : this.rowSize(index),
|
|
243
|
+
page: getPage(axis, event),
|
|
244
|
+
index,
|
|
245
|
+
};
|
|
246
|
+
} else if (action === 'cell') {
|
|
247
|
+
const cellCoords = closestCell(event.target, actionEl);
|
|
248
|
+
if (cellCoords) {
|
|
249
|
+
this.pointer = { state: 'selecting' };
|
|
250
|
+
this.selectionStart = cellCoords;
|
|
251
|
+
}
|
|
252
|
+
if (this.mode === 'edit') {
|
|
253
|
+
event.preventDefault();
|
|
254
|
+
} else {
|
|
255
|
+
if (this.focusActive && isSameCell(this.focusedCell, cellCoords)) {
|
|
256
|
+
this.dispatchEditRequest();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
199
260
|
}
|
|
200
261
|
}
|
|
201
262
|
};
|
|
202
263
|
|
|
203
|
-
handlePointerUp = (
|
|
204
|
-
if (this.resizing) {
|
|
264
|
+
private handlePointerUp = (event: PointerEvent) => {
|
|
265
|
+
if (this.pointer?.state === 'resizing') {
|
|
205
266
|
const resizeEvent = new DxAxisResize({
|
|
206
|
-
axis: this.
|
|
207
|
-
index: this.
|
|
208
|
-
size: this[this.
|
|
267
|
+
axis: this.pointer.axis,
|
|
268
|
+
index: this.pointer.index,
|
|
269
|
+
size: this[this.pointer.axis === 'col' ? 'colSize' : 'rowSize'](this.pointer.index),
|
|
209
270
|
});
|
|
210
271
|
this.dispatchEvent(resizeEvent);
|
|
211
|
-
|
|
272
|
+
} else {
|
|
273
|
+
const cell = closestCell(event.target);
|
|
274
|
+
if (cell) {
|
|
275
|
+
this.selectionEnd = cell;
|
|
276
|
+
this.dispatchEvent(
|
|
277
|
+
new DxGridCellsSelect({
|
|
278
|
+
start: this.selectionStart,
|
|
279
|
+
end: this.selectionEnd,
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
212
283
|
}
|
|
284
|
+
this.pointer = null;
|
|
213
285
|
};
|
|
214
286
|
|
|
215
|
-
handlePointerMove = (event: PointerEvent) => {
|
|
216
|
-
if (this.resizing) {
|
|
217
|
-
const delta = getPage(this.
|
|
218
|
-
if (this.
|
|
219
|
-
const nextSize = Math.max(sizeColMin, Math.min(sizeColMax, this.
|
|
220
|
-
this.colSizes = { ...this.colSizes, [this.
|
|
287
|
+
private handlePointerMove = (event: PointerEvent) => {
|
|
288
|
+
if (this.pointer?.state === 'resizing') {
|
|
289
|
+
const delta = getPage(this.pointer.axis, event) - this.pointer.page;
|
|
290
|
+
if (this.pointer.axis === 'col') {
|
|
291
|
+
const nextSize = Math.max(sizeColMin, Math.min(sizeColMax, this.pointer.size + delta));
|
|
292
|
+
this.colSizes = { ...this.colSizes, [this.pointer.index]: nextSize };
|
|
221
293
|
this.updateVisInline();
|
|
222
294
|
} else {
|
|
223
|
-
const nextSize = Math.max(sizeRowMin, Math.min(sizeRowMax, this.
|
|
224
|
-
this.rowSizes = { ...this.rowSizes, [this.
|
|
295
|
+
const nextSize = Math.max(sizeRowMin, Math.min(sizeRowMax, this.pointer.size + delta));
|
|
296
|
+
this.rowSizes = { ...this.rowSizes, [this.pointer.index]: nextSize };
|
|
225
297
|
this.updateVisBlock();
|
|
226
298
|
}
|
|
299
|
+
} else if (this.pointer?.state === 'selecting') {
|
|
300
|
+
const cell = closestCell(event.target);
|
|
301
|
+
if (cell && (cell.col !== this.selectionEnd.col || cell.row !== this.selectionEnd.row)) {
|
|
302
|
+
this.selectionEnd = cell;
|
|
303
|
+
}
|
|
227
304
|
}
|
|
228
305
|
};
|
|
229
306
|
|
|
307
|
+
private handleKeydown(event: KeyboardEvent) {
|
|
308
|
+
if (this.focusActive && this.mode === 'browse') {
|
|
309
|
+
// Adjust state
|
|
310
|
+
switch (event.key) {
|
|
311
|
+
case 'ArrowDown':
|
|
312
|
+
this.focusedCell = { ...this.focusedCell, row: this.focusedCell.row + 1 };
|
|
313
|
+
break;
|
|
314
|
+
case 'ArrowUp':
|
|
315
|
+
this.focusedCell = { ...this.focusedCell, row: Math.max(0, this.focusedCell.row - 1) };
|
|
316
|
+
break;
|
|
317
|
+
case 'ArrowRight':
|
|
318
|
+
this.focusedCell = { ...this.focusedCell, col: this.focusedCell.col + 1 };
|
|
319
|
+
break;
|
|
320
|
+
case 'ArrowLeft':
|
|
321
|
+
this.focusedCell = { ...this.focusedCell, col: Math.max(0, this.focusedCell.col - 1) };
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
// Emit edit request if relevant
|
|
325
|
+
switch (event.key) {
|
|
326
|
+
case 'Enter':
|
|
327
|
+
this.dispatchEditRequest();
|
|
328
|
+
break;
|
|
329
|
+
default:
|
|
330
|
+
if (event.key.length === 1 && event.key.match(/\P{Cc}/u)) {
|
|
331
|
+
this.dispatchEditRequest(event.key);
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
// Handle virtualization & focus consequences
|
|
336
|
+
switch (event.key) {
|
|
337
|
+
case 'ArrowDown':
|
|
338
|
+
case 'ArrowUp':
|
|
339
|
+
case 'ArrowRight':
|
|
340
|
+
case 'ArrowLeft':
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
this.snapPosToFocusedCell();
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
230
348
|
//
|
|
231
349
|
// Accessors
|
|
232
350
|
//
|
|
@@ -239,16 +357,35 @@ export class DxGrid extends LitElement {
|
|
|
239
357
|
return this.rowSizes?.[r] ?? this.rowDefault.size;
|
|
240
358
|
}
|
|
241
359
|
|
|
242
|
-
private
|
|
360
|
+
private cell(c: number | string, r: number | string) {
|
|
243
361
|
return this.cells[`${c}${separator}${r}`];
|
|
244
362
|
}
|
|
245
363
|
|
|
364
|
+
private focusedCellBox(): DxEditRequest['cellBox'] {
|
|
365
|
+
const cellElement = this.focusedCellElement();
|
|
366
|
+
const cellSize = { inlineSize: this.colSize(this.focusedCell.col), blockSize: this.rowSize(this.focusedCell.row) };
|
|
367
|
+
if (!cellElement) {
|
|
368
|
+
return { insetInlineStart: NaN, insetBlockStart: NaN, ...cellSize };
|
|
369
|
+
}
|
|
370
|
+
const contentElement = cellElement.offsetParent as HTMLElement;
|
|
371
|
+
// Note that storing `offset` in state causes performance issues, so instead the transform is parsed here.
|
|
372
|
+
const [_translate3d, inlineStr, blockStr] = contentElement.style.transform.split(/[()]|px,?\s?/);
|
|
373
|
+
const contentOffsetInline = parseFloat(inlineStr);
|
|
374
|
+
const contentOffsetBlock = parseFloat(blockStr);
|
|
375
|
+
const offsetParent = contentElement.offsetParent as HTMLElement;
|
|
376
|
+
return {
|
|
377
|
+
insetInlineStart: cellElement.offsetLeft + contentOffsetInline + offsetParent.offsetLeft,
|
|
378
|
+
insetBlockStart: cellElement.offsetTop + contentOffsetBlock + offsetParent.offsetTop,
|
|
379
|
+
...cellSize,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
246
383
|
//
|
|
247
384
|
// Resize & reposition handlers, observer, ref
|
|
248
385
|
//
|
|
249
386
|
|
|
250
387
|
@state()
|
|
251
|
-
observer = new ResizeObserver((entries) => {
|
|
388
|
+
private observer = new ResizeObserver((entries) => {
|
|
252
389
|
const { inlineSize, blockSize } = entries?.[0]?.contentBoxSize?.[0] ?? {
|
|
253
390
|
inlineSize: 0,
|
|
254
391
|
blockSize: 0,
|
|
@@ -264,26 +401,28 @@ export class DxGrid extends LitElement {
|
|
|
264
401
|
}
|
|
265
402
|
});
|
|
266
403
|
|
|
267
|
-
viewportRef: Ref<HTMLDivElement> = createRef();
|
|
268
|
-
|
|
269
|
-
handleWheel = ({ deltaX, deltaY }: WheelEvent) => {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
404
|
+
private viewportRef: Ref<HTMLDivElement> = createRef();
|
|
405
|
+
|
|
406
|
+
private handleWheel = ({ deltaX, deltaY }: WheelEvent) => {
|
|
407
|
+
if (this.mode === 'browse') {
|
|
408
|
+
this.posInline = Math.max(0, this.posInline + deltaX);
|
|
409
|
+
this.posBlock = Math.max(0, this.posBlock + deltaY);
|
|
410
|
+
if (
|
|
411
|
+
this.posInline >= this.binInlineMin &&
|
|
412
|
+
this.posInline < this.binInlineMax &&
|
|
413
|
+
this.posBlock >= this.binBlockMin &&
|
|
414
|
+
this.posBlock < this.binBlockMax
|
|
415
|
+
) {
|
|
416
|
+
// do nothing
|
|
417
|
+
} else {
|
|
418
|
+
// console.info(
|
|
419
|
+
// '[updating bounds]',
|
|
420
|
+
// 'wheel',
|
|
421
|
+
// [this.binInlineMin, this.posInline, this.binInlineMax],
|
|
422
|
+
// [this.binBlockMin, this.posBlock, this.binBlockMax],
|
|
423
|
+
// );
|
|
424
|
+
this.updateVis();
|
|
425
|
+
}
|
|
287
426
|
}
|
|
288
427
|
};
|
|
289
428
|
|
|
@@ -362,48 +501,50 @@ export class DxGrid extends LitElement {
|
|
|
362
501
|
|
|
363
502
|
// Focus handlers
|
|
364
503
|
|
|
365
|
-
@state()
|
|
366
|
-
focusedCell: Record<DxGridAxis, number> = { col: 0, row: 0 };
|
|
367
|
-
|
|
368
|
-
@state()
|
|
369
|
-
focusActive: boolean = false;
|
|
370
|
-
|
|
371
504
|
@eventOptions({ capture: true })
|
|
372
|
-
handleFocus(event: FocusEvent) {
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
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 };
|
|
505
|
+
private handleFocus(event: FocusEvent) {
|
|
506
|
+
const cellCoords = closestCell(event.target);
|
|
507
|
+
if (cellCoords) {
|
|
508
|
+
this.focusedCell = cellCoords;
|
|
379
509
|
this.focusActive = true;
|
|
380
510
|
}
|
|
381
511
|
}
|
|
382
512
|
|
|
383
513
|
@eventOptions({ capture: true })
|
|
384
|
-
handleBlur(event: FocusEvent) {
|
|
514
|
+
private handleBlur(event: FocusEvent) {
|
|
385
515
|
// 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
|
-
) {
|
|
516
|
+
if (!event.relatedTarget || !(event.relatedTarget as HTMLElement).closest(`[data-grid="${this.gridId}"]`)) {
|
|
390
517
|
this.focusActive = false;
|
|
391
518
|
}
|
|
392
519
|
}
|
|
393
520
|
|
|
521
|
+
private focusedCellElement() {
|
|
522
|
+
return this.viewportRef.value?.querySelector(
|
|
523
|
+
`[aria-colindex="${this.focusedCell.col}"][aria-rowindex="${this.focusedCell.row}"]`,
|
|
524
|
+
) as HTMLElement | null;
|
|
525
|
+
}
|
|
526
|
+
|
|
394
527
|
/**
|
|
395
528
|
* Moves focus to the cell with actual focus, otherwise moves focus to the viewport.
|
|
396
529
|
*/
|
|
397
|
-
refocus() {
|
|
530
|
+
refocus(increment?: 'col' | 'row', delta: 1 | -1 = 1) {
|
|
531
|
+
switch (increment) {
|
|
532
|
+
case 'row':
|
|
533
|
+
this.focusedCell = { ...this.focusedCell, row: this.focusedCell.row + delta };
|
|
534
|
+
break;
|
|
535
|
+
case 'col':
|
|
536
|
+
this.focusedCell = { ...this.focusedCell, col: this.focusedCell.col + delta };
|
|
537
|
+
}
|
|
398
538
|
(this.focusedCell.row < this.visRowMin ||
|
|
399
539
|
this.focusedCell.row > this.visRowMax ||
|
|
400
540
|
this.focusedCell.col < this.visColMin ||
|
|
401
541
|
this.focusedCell.col > this.visColMax
|
|
402
542
|
? this.viewportRef.value
|
|
403
|
-
:
|
|
404
|
-
`[aria-colindex="${this.focusedCell.col}"][aria-rowindex="${this.focusedCell.row}"]`,
|
|
405
|
-
) as HTMLElement | null)
|
|
543
|
+
: this.focusedCellElement()
|
|
406
544
|
)?.focus({ preventScroll: true });
|
|
545
|
+
if (increment) {
|
|
546
|
+
this.snapPosToFocusedCell();
|
|
547
|
+
}
|
|
407
548
|
}
|
|
408
549
|
|
|
409
550
|
/**
|
|
@@ -438,7 +579,7 @@ export class DxGrid extends LitElement {
|
|
|
438
579
|
acc += this.colSize(this.visColMin + overscanCol + c0) + gap;
|
|
439
580
|
return acc;
|
|
440
581
|
}, 0);
|
|
441
|
-
this.posInline = this.binInlineMin + sizeSumCol + gap * 2 - this.sizeInline;
|
|
582
|
+
this.posInline = Math.max(0, this.binInlineMin + sizeSumCol + gap * 2 - this.sizeInline);
|
|
442
583
|
this.updateVisInline();
|
|
443
584
|
}
|
|
444
585
|
|
|
@@ -450,43 +591,12 @@ export class DxGrid extends LitElement {
|
|
|
450
591
|
acc += this.rowSize(this.visRowMin + overscanRow + r0) + gap;
|
|
451
592
|
return acc;
|
|
452
593
|
}, 0);
|
|
453
|
-
this.posBlock = this.binBlockMin + sizeSumRow + gap * 2 - this.sizeBlock;
|
|
594
|
+
this.posBlock = Math.max(0, this.binBlockMin + sizeSumRow + gap * 2 - this.sizeBlock);
|
|
454
595
|
this.updateVisBlock();
|
|
455
596
|
}
|
|
456
597
|
}
|
|
457
598
|
}
|
|
458
599
|
|
|
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
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
600
|
//
|
|
491
601
|
// Render and other lifecycle methods
|
|
492
602
|
//
|
|
@@ -494,15 +604,25 @@ export class DxGrid extends LitElement {
|
|
|
494
604
|
override render() {
|
|
495
605
|
const visibleCols = this.visColMax - this.visColMin;
|
|
496
606
|
const visibleRows = this.visRowMax - this.visRowMin;
|
|
497
|
-
const offsetInline =
|
|
498
|
-
const offsetBlock =
|
|
607
|
+
const offsetInline = this.binInlineMin - this.posInline - this.overscanInline;
|
|
608
|
+
const offsetBlock = this.binBlockMin - this.posBlock - this.overscanBlock;
|
|
609
|
+
|
|
610
|
+
const selectColMin = Math.min(this.selectionStart.col, this.selectionEnd.col);
|
|
611
|
+
const selectColMax = Math.max(this.selectionStart.col, this.selectionEnd.col);
|
|
612
|
+
const selectRowMin = Math.min(this.selectionStart.row, this.selectionEnd.row);
|
|
613
|
+
const selectRowMax = Math.max(this.selectionStart.row, this.selectionEnd.row);
|
|
614
|
+
const selectVisible = selectColMin !== selectColMax || selectRowMin !== selectRowMax;
|
|
499
615
|
|
|
500
616
|
return html`<div
|
|
501
617
|
role="none"
|
|
502
618
|
class="dx-grid"
|
|
619
|
+
data-grid=${this.gridId}
|
|
620
|
+
data-grid-mode=${this.mode}
|
|
621
|
+
?data-grid-select=${selectVisible}
|
|
503
622
|
@pointerdown=${this.handlePointerDown}
|
|
504
623
|
@pointerup=${this.handlePointerUp}
|
|
505
624
|
@pointermove=${this.handlePointerMove}
|
|
625
|
+
@pointerleave=${this.handlePointerUp}
|
|
506
626
|
@focus=${this.handleFocus}
|
|
507
627
|
@blur=${this.handleBlur}
|
|
508
628
|
@keydown=${this.handleKeydown}
|
|
@@ -560,11 +680,17 @@ export class DxGrid extends LitElement {
|
|
|
560
680
|
return [...Array(visibleRows)].map((_, r0) => {
|
|
561
681
|
const c = c0 + this.visColMin;
|
|
562
682
|
const r = r0 + this.visRowMin;
|
|
563
|
-
const cell = this.
|
|
683
|
+
const cell = this.cell(c, r);
|
|
684
|
+
const active = this.focusActive && this.focusedCell.col === c && this.focusedCell.row === r;
|
|
685
|
+
const selected = c >= selectColMin && c <= selectColMax && r >= selectRowMin && r <= selectRowMax;
|
|
564
686
|
return html`<div
|
|
565
687
|
role="gridcell"
|
|
566
688
|
tabindex="0"
|
|
567
689
|
?inert=${c < 0 || r < 0}
|
|
690
|
+
?aria-selected=${selected}
|
|
691
|
+
class=${cell || active
|
|
692
|
+
? (cell?.className ? cell.className + ' ' : '') + (active ? 'dx-grid__cell--active' : '')
|
|
693
|
+
: nothing}
|
|
568
694
|
aria-rowindex=${r}
|
|
569
695
|
aria-colindex=${c}
|
|
570
696
|
data-dx-grid-action="cell"
|