@ornery/ui-grid-vanilla 1.0.5 → 1.0.7
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/README.md +75 -11
- package/demo/main.ts +1 -2
- package/dist/browser/ui-grid-element.js +9586 -0
- package/dist/index.d.mts +43 -2
- package/dist/index.d.ts +43 -2
- package/dist/index.js +281 -100
- package/dist/index.mjs +283 -102
- package/package.json +14 -6
- package/src/browser.test.ts +9 -0
- package/src/browser.ts +8 -0
- package/src/cell-state-management.test.ts +419 -0
- package/src/events.ts +77 -26
- package/src/framework-slots.ts +18 -0
- package/src/grid-controller.ts +166 -98
- package/src/index.integration.test.ts +51 -8
- package/src/index.test.ts +0 -4
- package/src/index.ts +6 -9
- package/src/keyboard-navigation.test.ts +1 -7
- package/src/patch.ts +118 -1
- package/src/row-edit.test.ts +1 -1
- package/src/ui-grid-standalone.element.ts +79 -1
- package/tsup.config.ts +22 -7
- package/vitest.config.ts +30 -0
package/src/events.ts
CHANGED
|
@@ -28,6 +28,24 @@ import type { UiGridStandaloneElement } from './ui-grid-standalone.element';
|
|
|
28
28
|
|
|
29
29
|
type BoundShadowRoot = ShadowRoot & { __uiGridBound?: boolean };
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Find the `.body-cell[data-row][data-column]` ancestor of an event by walking
|
|
33
|
+
* the composed event path. Plain `closest()` from the click target stops at
|
|
34
|
+
* shadow boundaries — for clicks on framework-projected light-DOM cell
|
|
35
|
+
* content (Angular ng-template, React render props), the body cell sits in
|
|
36
|
+
* the shadow tree on the other side of the slot. composedPath includes both
|
|
37
|
+
* sides, so we just scan it.
|
|
38
|
+
*/
|
|
39
|
+
function bodyCellFromEvent(event: Event): HTMLElement | null {
|
|
40
|
+
const path = event.composedPath();
|
|
41
|
+
for (const node of path) {
|
|
42
|
+
if (node instanceof HTMLElement && node.classList.contains('body-cell')) {
|
|
43
|
+
if (node.dataset['row'] && node.dataset['column']) return node;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
/**
|
|
32
50
|
* Watch the element's light DOM for `<template slot="…">` changes so
|
|
33
51
|
* consumer template edits trigger a re-render. Ports the old grid's
|
|
@@ -37,7 +55,23 @@ type BoundShadowRoot = ShadowRoot & { __uiGridBound?: boolean };
|
|
|
37
55
|
export function observeTemplateSlots(el: UiGridStandaloneElement): void {
|
|
38
56
|
if (el.templateObserver) return;
|
|
39
57
|
|
|
40
|
-
el.templateObserver = new MutationObserver(() => {
|
|
58
|
+
el.templateObserver = new MutationObserver((mutations) => {
|
|
59
|
+
// We only care about consumer-provided `<template slot="…">` nodes —
|
|
60
|
+
// their innerHTML feeds the string-interpolation render path. Skip
|
|
61
|
+
// mutations that only touched non-template slotted nodes (e.g. the
|
|
62
|
+
// `<span slot="cell-…">` wrappers Angular / React wrappers append to
|
|
63
|
+
// project framework-rendered content). Otherwise those framework
|
|
64
|
+
// wrappers would re-trigger ensureController on every slot delta and
|
|
65
|
+
// loop forever.
|
|
66
|
+
const involvesTemplate = mutationsTouchTemplateSlot(mutations);
|
|
67
|
+
if (!involvesTemplate) return;
|
|
68
|
+
|
|
69
|
+
// Consumer light-DOM templates (`<template slot="cell-…">`) feed the
|
|
70
|
+
// string-interpolation render path. When they're added/removed/edited
|
|
71
|
+
// the per-row visual fingerprint cache becomes stale (the fingerprint
|
|
72
|
+
// doesn't track template content), so blow it away to force the next
|
|
73
|
+
// patch pass to re-render every cell shell.
|
|
74
|
+
el.lastRowStateFingerprints.clear();
|
|
41
75
|
if (el.activeOptions) {
|
|
42
76
|
el.ensureController(el.buildEffectiveOptions(el.activeOptions));
|
|
43
77
|
} else {
|
|
@@ -53,6 +87,23 @@ export function observeTemplateSlots(el: UiGridStandaloneElement): void {
|
|
|
53
87
|
});
|
|
54
88
|
}
|
|
55
89
|
|
|
90
|
+
function mutationsTouchTemplateSlot(mutations: MutationRecord[]): boolean {
|
|
91
|
+
for (const m of mutations) {
|
|
92
|
+
if (m.type === 'attributes' && m.target instanceof HTMLTemplateElement) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (m.type === 'childList') {
|
|
96
|
+
for (const node of m.addedNodes) {
|
|
97
|
+
if (node instanceof HTMLTemplateElement && node.hasAttribute('slot')) return true;
|
|
98
|
+
}
|
|
99
|
+
for (const node of m.removedNodes) {
|
|
100
|
+
if (node instanceof HTMLTemplateElement && node.hasAttribute('slot')) return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
56
107
|
/**
|
|
57
108
|
* Mirror horizontal scroll from the body viewport onto the header +
|
|
58
109
|
* filter strips. The strips are real scroll containers (necessary for
|
|
@@ -115,7 +166,7 @@ export function bindEvents(el: UiGridStandaloneElement): void {
|
|
|
115
166
|
// don't reliably focus a div with tabindex="0" on click, so we force it
|
|
116
167
|
// here. This is required for arrow-key nav to work — otherwise focus
|
|
117
168
|
// stays on the grid-table scroll container and arrows just scroll.
|
|
118
|
-
const clickedCell =
|
|
169
|
+
const clickedCell = bodyCellFromEvent(event);
|
|
119
170
|
if (clickedCell && !realTarget.closest('[data-role="editor"]')) {
|
|
120
171
|
const rowId = clickedCell.dataset['row'];
|
|
121
172
|
const columnName = clickedCell.dataset['column'];
|
|
@@ -140,11 +191,8 @@ export function bindEvents(el: UiGridStandaloneElement): void {
|
|
|
140
191
|
// Row-header checkbox column click — mirrors the old
|
|
141
192
|
// selectionRowHeaderButtons directive: even when enableFullRowSelection
|
|
142
193
|
// is off, clicking the header checkbox selects the row.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
);
|
|
146
|
-
if (checkboxNode) {
|
|
147
|
-
const rowId = checkboxNode.dataset['row'];
|
|
194
|
+
if (clickedCell?.dataset['column'] === 'selectionRowHeaderCol') {
|
|
195
|
+
const rowId = clickedCell.dataset['row'];
|
|
148
196
|
if (rowId) {
|
|
149
197
|
event.stopPropagation();
|
|
150
198
|
handleRowHeaderCheckboxClick(el, rowId, event);
|
|
@@ -255,9 +303,10 @@ export function bindEvents(el: UiGridStandaloneElement): void {
|
|
|
255
303
|
}
|
|
256
304
|
|
|
257
305
|
if (action === 'benchmark') {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
306
|
+
void el.controller.gridApi.core.benchmark().then((result) => {
|
|
307
|
+
el.benchmarkAverage = result.averageMs.toFixed(2);
|
|
308
|
+
el.render();
|
|
309
|
+
});
|
|
261
310
|
return;
|
|
262
311
|
}
|
|
263
312
|
|
|
@@ -270,13 +319,16 @@ export function bindEvents(el: UiGridStandaloneElement): void {
|
|
|
270
319
|
// focusin / focusout: keep `focusedCell` in sync with DOM focus
|
|
271
320
|
// ─────────────────────────────────────────────────────────────────
|
|
272
321
|
root.addEventListener('focusin', (event) => {
|
|
273
|
-
const
|
|
274
|
-
if (!target) return;
|
|
275
|
-
const cell = target.closest<HTMLElement>('.body-cell[data-row][data-column]');
|
|
322
|
+
const cell = bodyCellFromEvent(event);
|
|
276
323
|
const rowId = cell?.dataset['row'];
|
|
277
324
|
const columnName = cell?.dataset['column'];
|
|
278
325
|
if (rowId && columnName) {
|
|
279
|
-
el.focusedCell
|
|
326
|
+
const previous = el.focusedCell;
|
|
327
|
+
const next = { rowId, columnName };
|
|
328
|
+
if (!previous || previous.rowId !== rowId || previous.columnName !== columnName) {
|
|
329
|
+
el.focusedCell = next;
|
|
330
|
+
applyFocusedCellClass(el, previous, next);
|
|
331
|
+
}
|
|
280
332
|
}
|
|
281
333
|
});
|
|
282
334
|
|
|
@@ -325,7 +377,7 @@ export function bindEvents(el: UiGridStandaloneElement): void {
|
|
|
325
377
|
if (realTarget.closest('.filter-cell')) return;
|
|
326
378
|
const resolved = el.controller.getResolvedSelectionOptions();
|
|
327
379
|
if (!resolved.enableRowSelection || !resolved.multiSelect) return;
|
|
328
|
-
const startCell =
|
|
380
|
+
const startCell = bodyCellFromEvent(event);
|
|
329
381
|
if (!startCell) return;
|
|
330
382
|
const startRowId = startCell.dataset['row'];
|
|
331
383
|
if (!startRowId) return;
|
|
@@ -356,9 +408,7 @@ export function bindEvents(el: UiGridStandaloneElement): void {
|
|
|
356
408
|
};
|
|
357
409
|
|
|
358
410
|
const handleMove = (moveEvent: MouseEvent): void => {
|
|
359
|
-
const
|
|
360
|
-
const firstEl = path[0] as HTMLElement | undefined;
|
|
361
|
-
const cell = firstEl?.closest?.<HTMLElement>('.body-cell[data-row][data-column]');
|
|
411
|
+
const cell = bodyCellFromEvent(moveEvent);
|
|
362
412
|
if (!cell) return;
|
|
363
413
|
const rowId = cell.dataset['row'];
|
|
364
414
|
if (!rowId) return;
|
|
@@ -455,7 +505,7 @@ export function bindEvents(el: UiGridStandaloneElement): void {
|
|
|
455
505
|
}
|
|
456
506
|
}
|
|
457
507
|
|
|
458
|
-
const cell =
|
|
508
|
+
const cell = bodyCellFromEvent(event);
|
|
459
509
|
if (!cell) return;
|
|
460
510
|
|
|
461
511
|
const rowId = cell.dataset['row'];
|
|
@@ -473,11 +523,13 @@ export function bindEvents(el: UiGridStandaloneElement): void {
|
|
|
473
523
|
const target = event.target as HTMLElement | null;
|
|
474
524
|
if (!target || !el.controller) return;
|
|
475
525
|
|
|
526
|
+
const onBodyCell = bodyCellFromEvent(event);
|
|
527
|
+
|
|
476
528
|
// cellNav.keyDownOverrides: declared overrides skip cellnav's default
|
|
477
529
|
// handling and raise viewPortKeyDown so consumers can disable built-in
|
|
478
530
|
// key handling per-key.
|
|
479
531
|
const overrides = el.controller.getOptions().keyDownOverrides ?? [];
|
|
480
|
-
if (overrides.length &&
|
|
532
|
+
if (overrides.length && onBodyCell) {
|
|
481
533
|
for (const override of overrides) {
|
|
482
534
|
if (matchesKeyOverride(override, keyboardEvent)) {
|
|
483
535
|
el.controller.raiseCellNavKeyEvent('keydown', keyboardEvent);
|
|
@@ -490,7 +542,7 @@ export function bindEvents(el: UiGridStandaloneElement): void {
|
|
|
490
542
|
if (
|
|
491
543
|
(keyboardEvent.ctrlKey || keyboardEvent.metaKey) &&
|
|
492
544
|
(keyboardEvent.key === 'a' || keyboardEvent.key === 'A') &&
|
|
493
|
-
|
|
545
|
+
onBodyCell
|
|
494
546
|
) {
|
|
495
547
|
const resolved = el.controller.getResolvedSelectionOptions();
|
|
496
548
|
if (resolved.enableRowSelection && resolved.multiSelect) {
|
|
@@ -502,7 +554,7 @@ export function bindEvents(el: UiGridStandaloneElement): void {
|
|
|
502
554
|
|
|
503
555
|
// Space toggles the row when on the checkbox column or in full-row-selection mode.
|
|
504
556
|
if (keyboardEvent.key === ' ' || keyboardEvent.key === 'Spacebar') {
|
|
505
|
-
const cell =
|
|
557
|
+
const cell = onBodyCell;
|
|
506
558
|
const rowId = cell?.dataset['row'];
|
|
507
559
|
const columnName = cell?.dataset['column'];
|
|
508
560
|
if (cell && rowId && columnName) {
|
|
@@ -567,10 +619,9 @@ export function bindEvents(el: UiGridStandaloneElement): void {
|
|
|
567
619
|
// cell).
|
|
568
620
|
let rowId: string | undefined;
|
|
569
621
|
let columnName: string | undefined;
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
columnName = cell.dataset['column'];
|
|
622
|
+
if (onBodyCell) {
|
|
623
|
+
rowId = onBodyCell.dataset['row'];
|
|
624
|
+
columnName = onBodyCell.dataset['column'];
|
|
574
625
|
} else if (el.focusedCell) {
|
|
575
626
|
rowId = el.focusedCell.rowId;
|
|
576
627
|
columnName = el.focusedCell.columnName;
|
package/src/framework-slots.ts
CHANGED
|
@@ -261,6 +261,24 @@ export class FrameworkSlotBridge {
|
|
|
261
261
|
});
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Carry forward every previously-known cell slot for `rowId` into the
|
|
266
|
+
* current pending set. Used by the structural patch path when a row's
|
|
267
|
+
* fingerprint matches the previous render's: `stageCell` would normally
|
|
268
|
+
* re-record the slot during cell rendering, but the fingerprint
|
|
269
|
+
* short-circuit skips the render pass for unchanged rows. Without this,
|
|
270
|
+
* `flush()` would diff pending (missing the row's slots) against last
|
|
271
|
+
* (containing them) and emit a spurious `removed` for every cell of the
|
|
272
|
+
* unchanged row, destroying the framework wrappers' projected content.
|
|
273
|
+
*/
|
|
274
|
+
carryRowCells(rowId: string): void {
|
|
275
|
+
for (const [slotName, slot] of this.lastCellSlots) {
|
|
276
|
+
if (slot.rowId === rowId) {
|
|
277
|
+
this.pendingCellSlots.set(slotName, slot);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
264
282
|
stageExpandableRow(row: GridRow, rowIndex: number): void {
|
|
265
283
|
const slotName = expandableRowSlotName(row);
|
|
266
284
|
this.pendingExpandableRowSlots.set(slotName, {
|