@ornery/ui-grid-vanilla 0.1.10 → 1.0.1
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/AGENTS.md +70 -0
- package/README.md +196 -0
- package/demo/main.ts +0 -1
- package/dist/index.d.mts +582 -55
- package/dist/index.d.ts +582 -55
- package/dist/index.js +4959 -1429
- package/dist/index.mjs +5137 -1560
- package/package.json +11 -8
- package/scss-plugin.ts +65 -0
- package/src/adopt-styles.ts +39 -0
- package/src/attribute-bridge.ts +291 -0
- package/src/auto-height.test.ts +142 -0
- package/src/auto-resize.test.ts +207 -0
- package/src/cell-nav.test.ts +262 -0
- package/src/components/grid-body-cell/grid-body-cell.ts +23 -0
- package/src/components/grid-body-cell/index.ts +1 -0
- package/src/components/grid-cell-editor/grid-cell-editor.ts +104 -0
- package/src/components/grid-cell-editor/index.ts +1 -0
- package/src/components/grid-filter-cell/grid-filter-cell.scss +14 -0
- package/src/components/grid-filter-cell/grid-filter-cell.template.ts +20 -0
- package/src/components/grid-filter-cell/grid-filter-cell.ts +121 -0
- package/src/components/grid-filter-cell/index.ts +1 -0
- package/src/components/grid-group-row/grid-group-row.scss +24 -0
- package/src/components/grid-group-row/grid-group-row.template.ts +23 -0
- package/src/components/grid-group-row/grid-group-row.ts +94 -0
- package/src/components/grid-group-row/index.ts +1 -0
- package/src/components/grid-header-cell/grid-header-cell.ts +17 -0
- package/src/components/grid-header-cell/index.ts +1 -0
- package/src/components/grid-pagination/grid-pagination.scss +39 -0
- package/src/components/grid-pagination/grid-pagination.template.ts +40 -0
- package/src/components/grid-pagination/grid-pagination.ts +204 -0
- package/src/components/grid-pagination/index.ts +1 -0
- package/src/components/grid-template/grid-template.ts +166 -0
- package/src/components/grid-template/index.ts +1 -0
- package/src/events.ts +824 -0
- package/src/exporter.test.ts +186 -0
- package/src/focus.ts +379 -0
- package/src/framework-slots.ts +440 -0
- package/src/grid-controller.ts +1532 -21
- package/src/icons.ts +88 -0
- package/src/importer.test.ts +147 -0
- package/src/index.integration.test.ts +2 -2
- package/src/index.ts +9 -0
- package/src/infinite-scroll.test.ts +220 -0
- package/src/keyboard-navigation.test.ts +579 -0
- package/src/patch.ts +722 -0
- package/src/render.ts +465 -0
- package/src/row-edit.test.ts +191 -0
- package/src/row-selection.test.ts +550 -0
- package/src/save-state.test.ts +235 -0
- package/src/scss.d.ts +4 -0
- package/src/templates/index.ts +62 -0
- package/src/test-setup.ts +23 -0
- package/src/ui-grid-empty.template.ts +23 -0
- package/src/ui-grid-shell.scss +25 -0
- package/src/ui-grid-shell.template.ts +45 -0
- package/src/ui-grid-standalone.element.ts +751 -1112
- package/src/utils/cell-class.ts +70 -0
- package/src/utils/display-items.ts +15 -0
- package/src/utils/dom.ts +64 -0
- package/src/validate.test.ts +170 -0
- package/tsup.config.ts +10 -0
- package/vite.config.ts +10 -1
package/src/events.ts
ADDED
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM event wiring for the grid element.
|
|
3
|
+
*
|
|
4
|
+
* One entry point — `bindEvents(el)` — attaches every shadow-root event
|
|
5
|
+
* listener the grid needs: click, focusin/out, input, mousedown (selection
|
|
6
|
+
* drag + column resize), dblclick, keydown, blur, dragstart/over/leave/
|
|
7
|
+
* drop/end, wheel, scroll. Each handler is a standalone function declared
|
|
8
|
+
* inside `bindEvents` so it closes over the element and keeps the coupling
|
|
9
|
+
* local.
|
|
10
|
+
*
|
|
11
|
+
* Supporting helpers (`observeTemplateSlots`, `syncHeaderHorizontalScroll`,
|
|
12
|
+
* `maybeTriggerInfiniteScroll`) also live here because they are only used
|
|
13
|
+
* by the event path.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { canGridMoveColumns } from '@ornery/ui-grid-core';
|
|
17
|
+
import {
|
|
18
|
+
applyFocusedCellClass,
|
|
19
|
+
commitAndMove,
|
|
20
|
+
focusCellElement,
|
|
21
|
+
handleRowHeaderCheckboxClick,
|
|
22
|
+
handleRowSelectionClick,
|
|
23
|
+
matchesKeyOverride,
|
|
24
|
+
measureAutoColumnWidth,
|
|
25
|
+
moveGridFocus,
|
|
26
|
+
} from './focus';
|
|
27
|
+
import type { UiGridStandaloneElement } from './ui-grid-standalone.element';
|
|
28
|
+
|
|
29
|
+
type BoundShadowRoot = ShadowRoot & { __uiGridBound?: boolean };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Watch the element's light DOM for `<template slot="…">` changes so
|
|
33
|
+
* consumer template edits trigger a re-render. Ports the old grid's
|
|
34
|
+
* observer that let template updates flow through without an explicit
|
|
35
|
+
* options re-assign.
|
|
36
|
+
*/
|
|
37
|
+
export function observeTemplateSlots(el: UiGridStandaloneElement): void {
|
|
38
|
+
if (el.templateObserver) return;
|
|
39
|
+
|
|
40
|
+
el.templateObserver = new MutationObserver(() => {
|
|
41
|
+
if (el.activeOptions) {
|
|
42
|
+
el.ensureController(el.buildEffectiveOptions(el.activeOptions));
|
|
43
|
+
} else {
|
|
44
|
+
el.render();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
el.templateObserver.observe(el, {
|
|
49
|
+
childList: true,
|
|
50
|
+
subtree: true,
|
|
51
|
+
attributes: true,
|
|
52
|
+
attributeFilter: ['slot'],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Mirror horizontal scroll from the body viewport onto the header +
|
|
58
|
+
* filter strips. The strips are real scroll containers (necessary for
|
|
59
|
+
* `position: sticky` on pinned cells), so they need scrollLeft set
|
|
60
|
+
* imperatively to stay aligned with the body.
|
|
61
|
+
*/
|
|
62
|
+
export function syncHeaderHorizontalScroll(
|
|
63
|
+
el: UiGridStandaloneElement,
|
|
64
|
+
scrollLeft: number,
|
|
65
|
+
): void {
|
|
66
|
+
const root = el.shadowRoot;
|
|
67
|
+
if (!root) return;
|
|
68
|
+
const headerStrip = root.querySelector<HTMLElement>('.grid-header-strip');
|
|
69
|
+
const filterStrip = root.querySelector<HTMLElement>('.grid-filter-strip');
|
|
70
|
+
if (headerStrip && headerStrip.scrollLeft !== scrollLeft) {
|
|
71
|
+
headerStrip.scrollLeft = scrollLeft;
|
|
72
|
+
}
|
|
73
|
+
if (filterStrip && filterStrip.scrollLeft !== scrollLeft) {
|
|
74
|
+
filterStrip.scrollLeft = scrollLeft;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Ask the controller to evaluate whether the current scroll position
|
|
80
|
+
* should request more data at the top or bottom — ports the old grid's
|
|
81
|
+
* `handleScroll → loadData` check. The controller raises
|
|
82
|
+
* `needLoadMoreData` / `needLoadMoreDataTop` when thresholds are crossed.
|
|
83
|
+
*/
|
|
84
|
+
export function maybeTriggerInfiniteScroll(el: UiGridStandaloneElement): void {
|
|
85
|
+
const snapshot = el.snapshot;
|
|
86
|
+
if (!snapshot || !el.controller) return;
|
|
87
|
+
if (snapshot.options.enableInfiniteScroll === false) return;
|
|
88
|
+
const bodyViewport = el.shadowRoot?.querySelector<HTMLElement>('.grid-body-viewport');
|
|
89
|
+
if (!bodyViewport) return;
|
|
90
|
+
const rowSize = snapshot.rowSize || 1;
|
|
91
|
+
const startIndex = Math.floor(el.scrollPosition / rowSize);
|
|
92
|
+
const viewportRows = Math.max(1, Math.floor(bodyViewport.clientHeight / rowSize));
|
|
93
|
+
const visibleRows = snapshot.pipeline.visibleRows.length;
|
|
94
|
+
el.controller.evaluateInfiniteScroll(startIndex, visibleRows, viewportRows);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Attach every shadow-root listener the grid needs. Idempotent: a
|
|
99
|
+
* `__uiGridBound` marker on the shadow root prevents re-binding on
|
|
100
|
+
* reconnect.
|
|
101
|
+
*/
|
|
102
|
+
export function bindEvents(el: UiGridStandaloneElement): void {
|
|
103
|
+
const root = el.ensureShadowRoot() as BoundShadowRoot;
|
|
104
|
+
if (root.__uiGridBound) return;
|
|
105
|
+
|
|
106
|
+
// ─────────────────────────────────────────────────────────────────
|
|
107
|
+
// click: body-cell focus seed, row-header checkbox, [data-action] dispatch
|
|
108
|
+
// ─────────────────────────────────────────────────────────────────
|
|
109
|
+
root.addEventListener('click', (event) => {
|
|
110
|
+
// Use composedPath to reach into sub-component shadow DOMs (pagination buttons, etc.).
|
|
111
|
+
const realTarget = (event.composedPath()[0] ?? event.target) as HTMLElement | null;
|
|
112
|
+
if (!realTarget || !el.controller || !el.snapshot) return;
|
|
113
|
+
|
|
114
|
+
// Clicking inside a body cell seeds keyboard navigation. Some browsers
|
|
115
|
+
// don't reliably focus a div with tabindex="0" on click, so we force it
|
|
116
|
+
// here. This is required for arrow-key nav to work — otherwise focus
|
|
117
|
+
// stays on the grid-table scroll container and arrows just scroll.
|
|
118
|
+
const clickedCell = realTarget.closest?.<HTMLElement>('.body-cell[data-row][data-column]') ?? null;
|
|
119
|
+
if (clickedCell && !realTarget.closest('[data-role="editor"]')) {
|
|
120
|
+
const rowId = clickedCell.dataset['row'];
|
|
121
|
+
const columnName = clickedCell.dataset['column'];
|
|
122
|
+
if (rowId && columnName) {
|
|
123
|
+
const previous = el.focusedCell;
|
|
124
|
+
const next = { rowId, columnName };
|
|
125
|
+
el.focusedCell = next;
|
|
126
|
+
applyFocusedCellClass(el, previous, next);
|
|
127
|
+
el.controller.setCellNavFocus(rowId, columnName);
|
|
128
|
+
const activeInShadow = (el.shadowRoot?.activeElement ?? null) as HTMLElement | null;
|
|
129
|
+
if (activeInShadow !== clickedCell && !activeInShadow?.closest?.('.body-cell')) {
|
|
130
|
+
try {
|
|
131
|
+
clickedCell.focus({ preventScroll: true });
|
|
132
|
+
} catch {
|
|
133
|
+
clickedCell.focus();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
handleRowSelectionClick(el, rowId, columnName, event);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Row-header checkbox column click — mirrors the old
|
|
141
|
+
// selectionRowHeaderButtons directive: even when enableFullRowSelection
|
|
142
|
+
// is off, clicking the header checkbox selects the row.
|
|
143
|
+
const checkboxNode = realTarget.closest?.<HTMLElement>(
|
|
144
|
+
'.body-cell[data-row][data-column="selectionRowHeaderCol"]',
|
|
145
|
+
);
|
|
146
|
+
if (checkboxNode) {
|
|
147
|
+
const rowId = checkboxNode.dataset['row'];
|
|
148
|
+
if (rowId) {
|
|
149
|
+
event.stopPropagation();
|
|
150
|
+
handleRowHeaderCheckboxClick(el, rowId, event);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Walk the composed path to find [data-action] across shadow boundaries.
|
|
155
|
+
let actionNode: HTMLElement | null = null;
|
|
156
|
+
for (const node of event.composedPath()) {
|
|
157
|
+
if (node instanceof HTMLElement && node.dataset['action']) {
|
|
158
|
+
actionNode = node;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!actionNode) return;
|
|
163
|
+
|
|
164
|
+
const action = actionNode.dataset['action'];
|
|
165
|
+
if (!action) return;
|
|
166
|
+
|
|
167
|
+
if (el.openPinMenuColumn && !realTarget.closest('.pin-control')) {
|
|
168
|
+
el.openPinMenuColumn = null;
|
|
169
|
+
el.render();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (action === 'sort') {
|
|
174
|
+
const columnName = actionNode.dataset['column'];
|
|
175
|
+
if (columnName) el.controller.toggleSort(columnName);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (action === 'select-all') {
|
|
180
|
+
const resolvedSel = el.controller.getResolvedSelectionOptions();
|
|
181
|
+
if (el.snapshot?.selectAll) {
|
|
182
|
+
el.controller.clearSelectedRows(event);
|
|
183
|
+
if (resolvedSel.noUnselect) {
|
|
184
|
+
el.controller.selectRowByVisibleIndex(0, event);
|
|
185
|
+
}
|
|
186
|
+
} else if (resolvedSel.multiSelect) {
|
|
187
|
+
el.controller.selectAllVisibleRows(event);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (action === 'group') {
|
|
193
|
+
const columnName = actionNode.dataset['column'];
|
|
194
|
+
if (columnName) el.controller.toggleGrouping(columnName);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (action === 'toggle-group') {
|
|
199
|
+
const groupId = actionNode.dataset['group'];
|
|
200
|
+
if (groupId) {
|
|
201
|
+
const collapsed = actionNode.dataset['collapsed'] !== 'true';
|
|
202
|
+
el.controller.setCollapsedGroup(groupId, collapsed);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (action === 'pin-trigger') {
|
|
208
|
+
const columnName = actionNode.dataset['column'];
|
|
209
|
+
const column = columnName
|
|
210
|
+
? el.snapshot.visibleColumns.find((candidate) => candidate.name === columnName)
|
|
211
|
+
: undefined;
|
|
212
|
+
|
|
213
|
+
if (columnName && column) {
|
|
214
|
+
if (el.controller.isPinned(column)) {
|
|
215
|
+
el.openPinMenuColumn = null;
|
|
216
|
+
el.controller.pinColumn(columnName, 'none');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
el.openPinMenuColumn = el.openPinMenuColumn === columnName ? null : columnName;
|
|
221
|
+
el.render();
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (action === 'pin-left' || action === 'pin-right') {
|
|
227
|
+
const columnName = actionNode.dataset['column'];
|
|
228
|
+
if (columnName) {
|
|
229
|
+
el.openPinMenuColumn = null;
|
|
230
|
+
el.controller.pinColumn(columnName, action === 'pin-left' ? 'left' : 'right');
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (action === 'toggle-tree') {
|
|
236
|
+
const rowId = actionNode.dataset['row'];
|
|
237
|
+
if (rowId) el.controller.toggleTreeRow(rowId);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (action === 'toggle-expand') {
|
|
242
|
+
const rowId = actionNode.dataset['row'];
|
|
243
|
+
if (rowId) el.controller.toggleRowExpansion(rowId);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (action === 'page-prev') {
|
|
248
|
+
el.controller.seekPage(el.snapshot.currentPage - 1);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (action === 'page-next') {
|
|
253
|
+
el.controller.seekPage(el.snapshot.currentPage + 1);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (action === 'benchmark') {
|
|
258
|
+
const result = el.controller.gridApi.core.benchmark();
|
|
259
|
+
el.benchmarkAverage = result.averageMs.toFixed(2);
|
|
260
|
+
el.render();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (action === 'export-csv') {
|
|
265
|
+
el.downloadCsv();
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ─────────────────────────────────────────────────────────────────
|
|
270
|
+
// focusin / focusout: keep `focusedCell` in sync with DOM focus
|
|
271
|
+
// ─────────────────────────────────────────────────────────────────
|
|
272
|
+
root.addEventListener('focusin', (event) => {
|
|
273
|
+
const target = event.target as HTMLElement | null;
|
|
274
|
+
if (!target) return;
|
|
275
|
+
const cell = target.closest<HTMLElement>('.body-cell[data-row][data-column]');
|
|
276
|
+
const rowId = cell?.dataset['row'];
|
|
277
|
+
const columnName = cell?.dataset['column'];
|
|
278
|
+
if (rowId && columnName) {
|
|
279
|
+
el.focusedCell = { rowId, columnName };
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
root.addEventListener('focusout', (event) => {
|
|
284
|
+
const relatedTarget = (event as FocusEvent).relatedTarget as HTMLElement | null;
|
|
285
|
+
if (!relatedTarget?.closest('.body-cell[data-row][data-column]')) {
|
|
286
|
+
el.focusedCell = null;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// ─────────────────────────────────────────────────────────────────
|
|
291
|
+
// input: filter inputs + cell editors
|
|
292
|
+
// ─────────────────────────────────────────────────────────────────
|
|
293
|
+
root.addEventListener('input', (event) => {
|
|
294
|
+
const target = (event.composedPath()[0] ?? event.target) as HTMLElement | null;
|
|
295
|
+
if (!target || !el.controller) return;
|
|
296
|
+
|
|
297
|
+
if (target instanceof HTMLInputElement && target.dataset['role'] === 'filter') {
|
|
298
|
+
const columnName = target.dataset['column'];
|
|
299
|
+
if (columnName) el.controller.setFilter(columnName, target.value);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (target instanceof HTMLInputElement && target.dataset['role'] === 'editor') {
|
|
303
|
+
el.controller.updateEditingValue(target.value);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Listen for the composed custom event from the pagination component's shadow DOM.
|
|
308
|
+
root.addEventListener('grid-page-size', ((event: CustomEvent<{ pageSize: number }>) => {
|
|
309
|
+
if (el.controller) el.controller.setPageSize(event.detail.pageSize);
|
|
310
|
+
}) as EventListener);
|
|
311
|
+
|
|
312
|
+
// ─────────────────────────────────────────────────────────────────
|
|
313
|
+
// mousedown #1: drag-paint row selection
|
|
314
|
+
// ─────────────────────────────────────────────────────────────────
|
|
315
|
+
root.addEventListener('mousedown', (event) => {
|
|
316
|
+
const mouseEvent = event as MouseEvent;
|
|
317
|
+
if (mouseEvent.button !== 0) return;
|
|
318
|
+
const realTarget = (event.composedPath()[0] ?? event.target) as HTMLElement | null;
|
|
319
|
+
if (!realTarget || !el.controller || !el.snapshot) return;
|
|
320
|
+
// Don't interfere with column resize / actions / editor / anything in
|
|
321
|
+
// the header or filter row.
|
|
322
|
+
if (realTarget.closest('.column-resizer')) return;
|
|
323
|
+
if (realTarget.closest('[data-role="editor"]')) return;
|
|
324
|
+
if (realTarget.closest('.header-cell')) return;
|
|
325
|
+
if (realTarget.closest('.filter-cell')) return;
|
|
326
|
+
const resolved = el.controller.getResolvedSelectionOptions();
|
|
327
|
+
if (!resolved.enableRowSelection || !resolved.multiSelect) return;
|
|
328
|
+
const startCell = realTarget.closest<HTMLElement>('.body-cell[data-row][data-column]');
|
|
329
|
+
if (!startCell) return;
|
|
330
|
+
const startRowId = startCell.dataset['row'];
|
|
331
|
+
if (!startRowId) return;
|
|
332
|
+
const isCheckboxCol = startCell.dataset['column'] === 'selectionRowHeaderCol';
|
|
333
|
+
if (!isCheckboxCol && !resolved.enableFullRowSelection) return;
|
|
334
|
+
// Clicks with modifier keys go through the single-click handler
|
|
335
|
+
// instead of drag-paint, so shift/ctrl don't accidentally commit a
|
|
336
|
+
// one-row drag on mouseup.
|
|
337
|
+
if (mouseEvent.shiftKey || mouseEvent.ctrlKey || mouseEvent.metaKey) return;
|
|
338
|
+
|
|
339
|
+
// Drag-paint tracking. We don't touch the start row on mousedown — the
|
|
340
|
+
// click handler owns single-click semantics. The starting row only
|
|
341
|
+
// gets painted once we observe a mousemove onto a DIFFERENT row.
|
|
342
|
+
const startSelected = el.snapshot.selectedRowIds.has(startRowId);
|
|
343
|
+
const targetSelected = !startSelected;
|
|
344
|
+
const touched = new Set<string>();
|
|
345
|
+
let dragStarted = false;
|
|
346
|
+
|
|
347
|
+
const paintRow = (rowId: string): void => {
|
|
348
|
+
if (touched.has(rowId)) return;
|
|
349
|
+
touched.add(rowId);
|
|
350
|
+
const currentlySelected = el.snapshot?.selectedRowIds.has(rowId) ?? false;
|
|
351
|
+
if (currentlySelected === targetSelected) return;
|
|
352
|
+
const row = el.controller!.findRowByIdPublic(rowId);
|
|
353
|
+
if (!row) return;
|
|
354
|
+
if (targetSelected) el.controller!.selectRow(row.entity, event);
|
|
355
|
+
else el.controller!.unSelectRow(row.entity, event);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const handleMove = (moveEvent: MouseEvent): void => {
|
|
359
|
+
const path = moveEvent.composedPath();
|
|
360
|
+
const firstEl = path[0] as HTMLElement | undefined;
|
|
361
|
+
const cell = firstEl?.closest?.<HTMLElement>('.body-cell[data-row][data-column]');
|
|
362
|
+
if (!cell) return;
|
|
363
|
+
const rowId = cell.dataset['row'];
|
|
364
|
+
if (!rowId) return;
|
|
365
|
+
if (!dragStarted) {
|
|
366
|
+
// First move onto a different row promotes this gesture to a drag —
|
|
367
|
+
// paint the starting row and suppress the trailing click.
|
|
368
|
+
if (rowId === startRowId) return;
|
|
369
|
+
dragStarted = true;
|
|
370
|
+
paintRow(startRowId);
|
|
371
|
+
}
|
|
372
|
+
paintRow(rowId);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const suppressNextClick = (e: Event): void => {
|
|
376
|
+
e.stopPropagation();
|
|
377
|
+
e.preventDefault();
|
|
378
|
+
root.removeEventListener('click', suppressNextClick, true);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const handleUp = (): void => {
|
|
382
|
+
window.removeEventListener('mousemove', handleMove, true);
|
|
383
|
+
window.removeEventListener('mouseup', handleUp, true);
|
|
384
|
+
if (dragStarted) {
|
|
385
|
+
root.addEventListener('click', suppressNextClick, true);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
window.addEventListener('mousemove', handleMove, true);
|
|
390
|
+
window.addEventListener('mouseup', handleUp, true);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// ─────────────────────────────────────────────────────────────────
|
|
394
|
+
// mousedown #2: column resize
|
|
395
|
+
// ─────────────────────────────────────────────────────────────────
|
|
396
|
+
root.addEventListener('mousedown', (event) => {
|
|
397
|
+
const target = event.target as HTMLElement | null;
|
|
398
|
+
if (!target || !el.controller) return;
|
|
399
|
+
|
|
400
|
+
const resizer = target.closest<HTMLElement>('.column-resizer[data-column]');
|
|
401
|
+
if (!resizer || !el.controller.canResizeColumns()) return;
|
|
402
|
+
|
|
403
|
+
const columnName = resizer.dataset['column'];
|
|
404
|
+
if (!columnName) return;
|
|
405
|
+
|
|
406
|
+
const headerCell = resizer.closest<HTMLElement>('.header-cell');
|
|
407
|
+
if (!headerCell) return;
|
|
408
|
+
|
|
409
|
+
event.preventDefault();
|
|
410
|
+
event.stopPropagation();
|
|
411
|
+
|
|
412
|
+
const startX = (event as MouseEvent).clientX;
|
|
413
|
+
const startWidth = headerCell.getBoundingClientRect().width;
|
|
414
|
+
let lastWidth = startWidth;
|
|
415
|
+
|
|
416
|
+
const handleMove = (moveEvent: MouseEvent): void => {
|
|
417
|
+
lastWidth = Math.max(88, startWidth + (moveEvent.clientX - startX));
|
|
418
|
+
|
|
419
|
+
// Write directly to DOM — skip the full refresh while dragging.
|
|
420
|
+
const nextTemplate = el.controller!.buildTemplateColumnsWithOverride(columnName, lastWidth);
|
|
421
|
+
const writeRoot = el.shadowRoot ?? el;
|
|
422
|
+
(writeRoot as ShadowRoot | HTMLElement)
|
|
423
|
+
.querySelectorAll<HTMLElement>('.header-grid, .filter-grid, .body-grid')
|
|
424
|
+
.forEach((targetEl) => {
|
|
425
|
+
targetEl.style.gridTemplateColumns = nextTemplate;
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const handleUp = (): void => {
|
|
430
|
+
window.removeEventListener('mousemove', handleMove);
|
|
431
|
+
window.removeEventListener('mouseup', handleUp);
|
|
432
|
+
// Commit final width once — triggers one full refresh.
|
|
433
|
+
el.controller!.setColumnWidthOverride(columnName, lastWidth);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
window.addEventListener('mousemove', handleMove);
|
|
437
|
+
window.addEventListener('mouseup', handleUp);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ─────────────────────────────────────────────────────────────────
|
|
441
|
+
// dblclick: auto-size column, begin cell edit
|
|
442
|
+
// ─────────────────────────────────────────────────────────────────
|
|
443
|
+
root.addEventListener('dblclick', (event) => {
|
|
444
|
+
const target = event.target as HTMLElement | null;
|
|
445
|
+
if (!target || !el.controller) return;
|
|
446
|
+
|
|
447
|
+
const resizer = target.closest<HTMLElement>('.column-resizer[data-column]');
|
|
448
|
+
if (resizer && el.controller.canResizeColumns()) {
|
|
449
|
+
const columnName = resizer.dataset['column'];
|
|
450
|
+
if (columnName) {
|
|
451
|
+
event.preventDefault();
|
|
452
|
+
event.stopPropagation();
|
|
453
|
+
el.controller.setColumnWidthOverride(columnName, measureAutoColumnWidth(el, columnName));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const cell = target.closest<HTMLElement>('.body-cell');
|
|
459
|
+
if (!cell) return;
|
|
460
|
+
|
|
461
|
+
const rowId = cell.dataset['row'];
|
|
462
|
+
const columnName = cell.dataset['column'];
|
|
463
|
+
if (rowId && columnName) {
|
|
464
|
+
el.controller.beginCellEdit(rowId, columnName, event);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// ─────────────────────────────────────────────────────────────────
|
|
469
|
+
// keydown: cellNav + editor shortcuts
|
|
470
|
+
// ─────────────────────────────────────────────────────────────────
|
|
471
|
+
root.addEventListener('keydown', (event) => {
|
|
472
|
+
const keyboardEvent = event as KeyboardEvent;
|
|
473
|
+
const target = event.target as HTMLElement | null;
|
|
474
|
+
if (!target || !el.controller) return;
|
|
475
|
+
|
|
476
|
+
// cellNav.keyDownOverrides: declared overrides skip cellnav's default
|
|
477
|
+
// handling and raise viewPortKeyDown so consumers can disable built-in
|
|
478
|
+
// key handling per-key.
|
|
479
|
+
const overrides = el.controller.getOptions().keyDownOverrides ?? [];
|
|
480
|
+
if (overrides.length && target.closest('.body-cell')) {
|
|
481
|
+
for (const override of overrides) {
|
|
482
|
+
if (matchesKeyOverride(override, keyboardEvent)) {
|
|
483
|
+
el.controller.raiseCellNavKeyEvent('keydown', keyboardEvent);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Ctrl/Cmd+A on a body cell — select all. Gated by enableRowSelection + multiSelect.
|
|
490
|
+
if (
|
|
491
|
+
(keyboardEvent.ctrlKey || keyboardEvent.metaKey) &&
|
|
492
|
+
(keyboardEvent.key === 'a' || keyboardEvent.key === 'A') &&
|
|
493
|
+
target.closest('.body-cell')
|
|
494
|
+
) {
|
|
495
|
+
const resolved = el.controller.getResolvedSelectionOptions();
|
|
496
|
+
if (resolved.enableRowSelection && resolved.multiSelect) {
|
|
497
|
+
event.preventDefault();
|
|
498
|
+
el.controller.selectAllRows(event);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Space toggles the row when on the checkbox column or in full-row-selection mode.
|
|
504
|
+
if (keyboardEvent.key === ' ' || keyboardEvent.key === 'Spacebar') {
|
|
505
|
+
const cell = target.closest<HTMLElement>('.body-cell[data-row][data-column]');
|
|
506
|
+
const rowId = cell?.dataset['row'];
|
|
507
|
+
const columnName = cell?.dataset['column'];
|
|
508
|
+
if (cell && rowId && columnName) {
|
|
509
|
+
const resolved = el.controller.getResolvedSelectionOptions();
|
|
510
|
+
if (resolved.enableRowSelection) {
|
|
511
|
+
const onCheckboxCol = columnName === 'selectionRowHeaderCol';
|
|
512
|
+
if (onCheckboxCol || resolved.enableFullRowSelection) {
|
|
513
|
+
event.preventDefault();
|
|
514
|
+
const row = el.controller.findRowByIdPublic(rowId);
|
|
515
|
+
if (row) {
|
|
516
|
+
if (resolved.multiSelect && !resolved.modifierKeysToMultiSelect) {
|
|
517
|
+
el.controller.toggleRowSelectionByEntity(row.entity, event);
|
|
518
|
+
} else {
|
|
519
|
+
el.controller.setMultiSelect(false);
|
|
520
|
+
el.controller.toggleRowSelectionByEntity(row.entity, event);
|
|
521
|
+
el.controller.setMultiSelect(resolved.multiSelect);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (target instanceof HTMLInputElement && target.dataset['role'] === 'editor') {
|
|
531
|
+
if (keyboardEvent.key === 'Enter') {
|
|
532
|
+
event.preventDefault();
|
|
533
|
+
event.stopPropagation();
|
|
534
|
+
const fromRow = target.dataset['row'] ?? null;
|
|
535
|
+
const fromCol = target.dataset['column'] ?? null;
|
|
536
|
+
const direction = keyboardEvent.shiftKey ? 'up' : 'down';
|
|
537
|
+
commitAndMove(el, fromRow, fromCol, direction);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (keyboardEvent.key === 'Escape') {
|
|
542
|
+
event.preventDefault();
|
|
543
|
+
event.stopPropagation();
|
|
544
|
+
const fromRow = target.dataset['row'] ?? null;
|
|
545
|
+
const fromCol = target.dataset['column'] ?? null;
|
|
546
|
+
el.controller.cancelCellEdit();
|
|
547
|
+
focusCellElement(el, fromRow, fromCol);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (keyboardEvent.key === 'Tab') {
|
|
552
|
+
event.preventDefault();
|
|
553
|
+
event.stopPropagation();
|
|
554
|
+
const fromRow = target.dataset['row'] ?? null;
|
|
555
|
+
const fromCol = target.dataset['column'] ?? null;
|
|
556
|
+
const direction = keyboardEvent.shiftKey ? 'left' : 'right';
|
|
557
|
+
commitAndMove(el, fromRow, fromCol, direction);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Derive the logical cell either from the event target (clicked cell) or
|
|
565
|
+
// from the element's tracked focusedCell state (covers the case where
|
|
566
|
+
// the browser routed the event to the scroll container instead of the
|
|
567
|
+
// cell).
|
|
568
|
+
let rowId: string | undefined;
|
|
569
|
+
let columnName: string | undefined;
|
|
570
|
+
const cell = target.closest<HTMLElement>('.body-cell');
|
|
571
|
+
if (cell) {
|
|
572
|
+
rowId = cell.dataset['row'];
|
|
573
|
+
columnName = cell.dataset['column'];
|
|
574
|
+
} else if (el.focusedCell) {
|
|
575
|
+
rowId = el.focusedCell.rowId;
|
|
576
|
+
columnName = el.focusedCell.columnName;
|
|
577
|
+
}
|
|
578
|
+
if (!rowId || !columnName) return;
|
|
579
|
+
|
|
580
|
+
switch (keyboardEvent.key) {
|
|
581
|
+
case 'ArrowLeft':
|
|
582
|
+
event.preventDefault();
|
|
583
|
+
moveGridFocus(el, 'left', rowId, columnName);
|
|
584
|
+
return;
|
|
585
|
+
case 'ArrowRight':
|
|
586
|
+
event.preventDefault();
|
|
587
|
+
moveGridFocus(el, 'right', rowId, columnName);
|
|
588
|
+
return;
|
|
589
|
+
case 'ArrowUp':
|
|
590
|
+
event.preventDefault();
|
|
591
|
+
moveGridFocus(el, 'up', rowId, columnName);
|
|
592
|
+
return;
|
|
593
|
+
case 'ArrowDown':
|
|
594
|
+
event.preventDefault();
|
|
595
|
+
moveGridFocus(el, 'down', rowId, columnName);
|
|
596
|
+
return;
|
|
597
|
+
case 'Tab':
|
|
598
|
+
event.preventDefault();
|
|
599
|
+
moveGridFocus(el, keyboardEvent.shiftKey ? 'left' : 'right', rowId, columnName);
|
|
600
|
+
return;
|
|
601
|
+
case 'Home':
|
|
602
|
+
event.preventDefault();
|
|
603
|
+
moveGridFocus(el, keyboardEvent.ctrlKey ? 'top' : 'rowStart', rowId, columnName);
|
|
604
|
+
return;
|
|
605
|
+
case 'End':
|
|
606
|
+
event.preventDefault();
|
|
607
|
+
moveGridFocus(el, keyboardEvent.ctrlKey ? 'bottom' : 'rowEnd', rowId, columnName);
|
|
608
|
+
return;
|
|
609
|
+
case 'Enter':
|
|
610
|
+
case 'F2':
|
|
611
|
+
event.preventDefault();
|
|
612
|
+
el.controller.beginCellEdit(rowId, columnName, event);
|
|
613
|
+
return;
|
|
614
|
+
default:
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (
|
|
619
|
+
keyboardEvent.key.length === 1 &&
|
|
620
|
+
!keyboardEvent.ctrlKey &&
|
|
621
|
+
!keyboardEvent.metaKey &&
|
|
622
|
+
!keyboardEvent.altKey
|
|
623
|
+
) {
|
|
624
|
+
event.preventDefault();
|
|
625
|
+
// Pass the typed character as the initial value — beginCellEdit seeds
|
|
626
|
+
// the editor with this instead of the cell's current value, so the
|
|
627
|
+
// first keystroke isn't lost to the "input is focused, skip value
|
|
628
|
+
// overwrite" guard in ui-grid-cell-editor.
|
|
629
|
+
el.controller.beginCellEdit(rowId, columnName, event, keyboardEvent.key);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// ─────────────────────────────────────────────────────────────────
|
|
634
|
+
// blur: commit edit on capture
|
|
635
|
+
// ─────────────────────────────────────────────────────────────────
|
|
636
|
+
root.addEventListener(
|
|
637
|
+
'blur',
|
|
638
|
+
(event) => {
|
|
639
|
+
const target = event.target as HTMLElement | null;
|
|
640
|
+
if (!target || !el.controller) return;
|
|
641
|
+
if (target instanceof HTMLInputElement && target.dataset['role'] === 'editor') {
|
|
642
|
+
el.controller.commitCellEdit();
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
true,
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
// ─────────────────────────────────────────────────────────────────
|
|
649
|
+
// drag & drop: column reorder
|
|
650
|
+
// ─────────────────────────────────────────────────────────────────
|
|
651
|
+
root.addEventListener('dragstart', (event) => {
|
|
652
|
+
const dragEvent = event as DragEvent;
|
|
653
|
+
const target = event.target as HTMLElement | null;
|
|
654
|
+
const headerCell = target?.closest<HTMLElement>('.header-cell[data-column]');
|
|
655
|
+
|
|
656
|
+
if (!headerCell || !el.snapshot || !canGridMoveColumns(el.snapshot.options)) {
|
|
657
|
+
event.preventDefault();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const columnName = headerCell.dataset['column'];
|
|
662
|
+
if (!columnName || !dragEvent.dataTransfer) {
|
|
663
|
+
event.preventDefault();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const colDef = el.snapshot.visibleColumns.find((c) => c.name === columnName);
|
|
668
|
+
if (colDef?.enableColumnMoving === false) {
|
|
669
|
+
event.preventDefault();
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
el.draggedColumnName = columnName;
|
|
674
|
+
el.dropTargetColumnName = null;
|
|
675
|
+
dragEvent.dataTransfer.effectAllowed = 'move';
|
|
676
|
+
dragEvent.dataTransfer.setData('text/plain', columnName);
|
|
677
|
+
headerCell.classList.add('is-dragging');
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
root.addEventListener('dragover', (event) => {
|
|
681
|
+
const dragEvent = event as DragEvent;
|
|
682
|
+
const target = event.target as HTMLElement | null;
|
|
683
|
+
const headerCell = target?.closest<HTMLElement>('.header-cell[data-column]');
|
|
684
|
+
|
|
685
|
+
if (!headerCell || !el.draggedColumnName || !dragEvent.dataTransfer) return;
|
|
686
|
+
|
|
687
|
+
const columnName = headerCell.dataset['column'];
|
|
688
|
+
if (!columnName || columnName === el.draggedColumnName) return;
|
|
689
|
+
|
|
690
|
+
const colDef = el.snapshot?.visibleColumns.find((c) => c.name === columnName);
|
|
691
|
+
if (colDef?.enableColumnMoving === false) return;
|
|
692
|
+
|
|
693
|
+
event.preventDefault();
|
|
694
|
+
dragEvent.dataTransfer.dropEffect = 'move';
|
|
695
|
+
|
|
696
|
+
if (el.dropTargetColumnName !== columnName) {
|
|
697
|
+
root.querySelectorAll('.header-cell.is-drag-target').forEach((element) => {
|
|
698
|
+
element.classList.remove('is-drag-target');
|
|
699
|
+
});
|
|
700
|
+
el.dropTargetColumnName = columnName;
|
|
701
|
+
headerCell.classList.add('is-drag-target');
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
root.addEventListener('dragleave', (event) => {
|
|
706
|
+
// `dragleave` bubbles from every descendant as the cursor moves over
|
|
707
|
+
// child nodes (the header-label span, the sort/pin buttons, etc.), so
|
|
708
|
+
// we must only clear the drop indicator when the pointer has actually
|
|
709
|
+
// left the header cell — i.e. when `relatedTarget` is outside it.
|
|
710
|
+
const dragEvent = event as DragEvent;
|
|
711
|
+
const target = event.target as HTMLElement | null;
|
|
712
|
+
const headerCell = target?.closest<HTMLElement>('.header-cell[data-column]');
|
|
713
|
+
if (!headerCell) return;
|
|
714
|
+
const related = dragEvent.relatedTarget as Node | null;
|
|
715
|
+
if (related && headerCell.contains(related)) return; // still inside the same header cell
|
|
716
|
+
if (headerCell.dataset['column'] === el.dropTargetColumnName) {
|
|
717
|
+
headerCell.classList.remove('is-drag-target');
|
|
718
|
+
el.dropTargetColumnName = null;
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
root.addEventListener('drop', (event) => {
|
|
723
|
+
event.preventDefault();
|
|
724
|
+
const dragEvent = event as DragEvent;
|
|
725
|
+
const target = event.target as HTMLElement | null;
|
|
726
|
+
const headerCell = target?.closest<HTMLElement>('.header-cell[data-column]');
|
|
727
|
+
const targetColumn = headerCell?.dataset['column'];
|
|
728
|
+
const sourceColumn =
|
|
729
|
+
el.draggedColumnName ?? dragEvent.dataTransfer?.getData('text/plain') ?? null;
|
|
730
|
+
|
|
731
|
+
el.draggedColumnName = null;
|
|
732
|
+
el.dropTargetColumnName = null;
|
|
733
|
+
|
|
734
|
+
root
|
|
735
|
+
.querySelectorAll('.header-cell.is-dragging, .header-cell.is-drag-target')
|
|
736
|
+
.forEach((element) => {
|
|
737
|
+
element.classList.remove('is-dragging', 'is-drag-target');
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
if (!sourceColumn || !targetColumn || sourceColumn === targetColumn || !el.controller) return;
|
|
741
|
+
|
|
742
|
+
el.controller.moveVisibleColumn(sourceColumn, targetColumn);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
root.addEventListener('dragend', () => {
|
|
746
|
+
el.draggedColumnName = null;
|
|
747
|
+
el.dropTargetColumnName = null;
|
|
748
|
+
root
|
|
749
|
+
.querySelectorAll('.header-cell.is-dragging, .header-cell.is-drag-target')
|
|
750
|
+
.forEach((element) => {
|
|
751
|
+
element.classList.remove('is-dragging', 'is-drag-target');
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// ─────────────────────────────────────────────────────────────────
|
|
756
|
+
// wheel on strips → forward to body viewport
|
|
757
|
+
// ─────────────────────────────────────────────────────────────────
|
|
758
|
+
// Forward wheel gestures originating inside the header / filter strips
|
|
759
|
+
// to the body viewport. The strips are technically scroll containers
|
|
760
|
+
// (necessary for `position: sticky` on pinned cells to anchor), but we
|
|
761
|
+
// don't want the user scrolling them independently — that would desync
|
|
762
|
+
// them from the body.
|
|
763
|
+
const onStripWheel = (event: WheelEvent): void => {
|
|
764
|
+
const target = event.target as HTMLElement | null;
|
|
765
|
+
const strip = target?.closest('.grid-header-strip, .grid-filter-strip');
|
|
766
|
+
if (!strip) return;
|
|
767
|
+
const bodyViewport = el.shadowRoot?.querySelector<HTMLElement>('.grid-body-viewport');
|
|
768
|
+
if (!bodyViewport) return;
|
|
769
|
+
event.preventDefault();
|
|
770
|
+
bodyViewport.scrollLeft += event.deltaX;
|
|
771
|
+
bodyViewport.scrollTop += event.deltaY;
|
|
772
|
+
};
|
|
773
|
+
root.addEventListener('wheel', onStripWheel as EventListener, { passive: false });
|
|
774
|
+
|
|
775
|
+
// ─────────────────────────────────────────────────────────────────
|
|
776
|
+
// scroll: virtualization window shift + infinite-scroll threshold
|
|
777
|
+
// ─────────────────────────────────────────────────────────────────
|
|
778
|
+
root.addEventListener(
|
|
779
|
+
'scroll',
|
|
780
|
+
(event) => {
|
|
781
|
+
const target = event.target as HTMLElement | null;
|
|
782
|
+
if (!target || el.suppressScrollEvent) return;
|
|
783
|
+
|
|
784
|
+
if (!target.classList.contains('grid-body-viewport')) return;
|
|
785
|
+
|
|
786
|
+
el.lastScrollActivityAt = Date.now();
|
|
787
|
+
el.scrollPosition = target.scrollTop;
|
|
788
|
+
el.horizontalScrollPosition = target.scrollLeft;
|
|
789
|
+
|
|
790
|
+
syncHeaderHorizontalScroll(el, target.scrollLeft);
|
|
791
|
+
|
|
792
|
+
const snapshot = el.snapshot;
|
|
793
|
+
if (!snapshot) return;
|
|
794
|
+
|
|
795
|
+
if (snapshot.pipeline.virtualizationEnabled) {
|
|
796
|
+
const overscan = 4;
|
|
797
|
+
const nextStartIndex = Math.max(
|
|
798
|
+
0,
|
|
799
|
+
Math.floor(el.scrollPosition / snapshot.rowSize) - overscan,
|
|
800
|
+
);
|
|
801
|
+
const startChanged = nextStartIndex !== el.lastVirtualStartIndex;
|
|
802
|
+
|
|
803
|
+
if (el.scrollFrame !== null) cancelAnimationFrame(el.scrollFrame);
|
|
804
|
+
el.scrollFrame = requestAnimationFrame(() => {
|
|
805
|
+
el.scrollFrame = null;
|
|
806
|
+
if (startChanged) el.renderVirtualBody();
|
|
807
|
+
maybeTriggerInfiniteScroll(el);
|
|
808
|
+
});
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Non-virtualized path: still evaluate infinite-scroll thresholds so
|
|
813
|
+
// large static datasets can page in via needLoadMoreData.
|
|
814
|
+
if (el.scrollFrame !== null) cancelAnimationFrame(el.scrollFrame);
|
|
815
|
+
el.scrollFrame = requestAnimationFrame(() => {
|
|
816
|
+
el.scrollFrame = null;
|
|
817
|
+
maybeTriggerInfiniteScroll(el);
|
|
818
|
+
});
|
|
819
|
+
},
|
|
820
|
+
true,
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
root.__uiGridBound = true;
|
|
824
|
+
}
|