@svgrid/grid 1.2.3 → 1.2.5
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/SvGrid.controller.svelte.js +84 -18
- package/dist/a11y.d.ts +1 -1
- package/dist/a11y.js +1 -1
- package/dist/cdn/svgrid.js +538 -504
- package/dist/cdn/svgrid.svelte-external.js +1944 -1910
- package/dist/menus.js +9 -1
- package/dist/scroll-sync.js +1 -1
- package/dist/sv-grid-scrollbar.js +16 -1
- package/package.json +1 -1
- package/src/SvGrid.controller.svelte.ts +82 -17
- package/src/a11y.ts +1 -1
- package/src/menus.ts +9 -1
- package/src/scroll-sync.ts +1 -1
- package/src/sv-grid-scrollbar.ts +14 -1
package/dist/menus.js
CHANGED
|
@@ -221,12 +221,14 @@ export function createMenus(ctx) {
|
|
|
221
221
|
...prev,
|
|
222
222
|
pageIndex: Math.max((prev?.pageIndex ?? 0) + delta, 0),
|
|
223
223
|
}));
|
|
224
|
+
ctx.scrollContainer?.scrollTo({ top: 0 });
|
|
224
225
|
}
|
|
225
226
|
function goToPage(pageIndex) {
|
|
226
227
|
ctx.grid.setPagination((prev) => ({
|
|
227
228
|
...prev,
|
|
228
229
|
pageIndex: Math.max(0, pageIndex),
|
|
229
230
|
}));
|
|
231
|
+
ctx.scrollContainer?.scrollTo({ top: 0 });
|
|
230
232
|
}
|
|
231
233
|
function setPageSize(pageSize) {
|
|
232
234
|
ctx.grid.setPagination((prev) => {
|
|
@@ -281,9 +283,15 @@ export function createMenus(ctx) {
|
|
|
281
283
|
const rowModel = ctx.allRows[rowIndex];
|
|
282
284
|
const row = rowModel?.original ?? null;
|
|
283
285
|
const rowId = rowModel?.id ?? String(rowIndex);
|
|
286
|
+
// Reserve ~280px for the menu body. If the click is in the bottom third,
|
|
287
|
+
// flip the menu above the cursor so it stays fully in the viewport.
|
|
288
|
+
const menuHeight = 280;
|
|
289
|
+
const y = event.clientY + menuHeight > window.innerHeight
|
|
290
|
+
? Math.max(8, event.clientY - menuHeight)
|
|
291
|
+
: event.clientY;
|
|
284
292
|
ctx.contextMenuPos = {
|
|
285
293
|
x: clampMenuX(event.clientX, 220),
|
|
286
|
-
y
|
|
294
|
+
y,
|
|
287
295
|
};
|
|
288
296
|
ctx.contextMenuFor = { rowIndex, colIndex, columnId, rowId, row };
|
|
289
297
|
}
|
package/dist/scroll-sync.js
CHANGED
|
@@ -61,7 +61,7 @@ export function createScrollSync(ctx) {
|
|
|
61
61
|
const container = event.currentTarget;
|
|
62
62
|
if (!container)
|
|
63
63
|
return;
|
|
64
|
-
if (ctx.columnMenuFor || ctx.operatorMenuFor)
|
|
64
|
+
if (ctx.columnMenuFor || ctx.operatorMenuFor || ctx.contextMenuFor)
|
|
65
65
|
ctx.closeMenus();
|
|
66
66
|
scheduleScrollSync(container.scrollTop, container.scrollLeft);
|
|
67
67
|
// Mirror horizontal scroll to any aligned grids in the same group.
|
|
@@ -148,6 +148,7 @@ class SvGridScrollbarElement extends ScrollbarBase {
|
|
|
148
148
|
this.valueStart = 0;
|
|
149
149
|
this.holdTimer = null;
|
|
150
150
|
this.resizeObserver = null;
|
|
151
|
+
this.resizeFrame = 0;
|
|
151
152
|
this.onArrowStartPointerDown = (event) => {
|
|
152
153
|
event.preventDefault();
|
|
153
154
|
this.startHold(-1);
|
|
@@ -225,12 +226,26 @@ class SvGridScrollbarElement extends ScrollbarBase {
|
|
|
225
226
|
btn.addEventListener('pointerleave', this.stopHold);
|
|
226
227
|
btn.addEventListener('pointercancel', this.stopHold);
|
|
227
228
|
}
|
|
228
|
-
|
|
229
|
+
// Coalesce resize notifications into a single next-frame render so the
|
|
230
|
+
// observer callback never re-lays-out the observed element within the same
|
|
231
|
+
// delivery cycle (which is what triggers the benign but noisy
|
|
232
|
+
// "ResizeObserver loop completed with undelivered notifications" warning).
|
|
233
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
234
|
+
if (this.resizeFrame)
|
|
235
|
+
return;
|
|
236
|
+
this.resizeFrame = requestAnimationFrame(() => {
|
|
237
|
+
this.resizeFrame = 0;
|
|
238
|
+
this.render();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
229
241
|
this.resizeObserver.observe(this);
|
|
230
242
|
this.render();
|
|
231
243
|
}
|
|
232
244
|
disconnectedCallback() {
|
|
233
245
|
this.stopHold();
|
|
246
|
+
if (this.resizeFrame)
|
|
247
|
+
cancelAnimationFrame(this.resizeFrame);
|
|
248
|
+
this.resizeFrame = 0;
|
|
234
249
|
this.resizeObserver?.disconnect();
|
|
235
250
|
this.resizeObserver = null;
|
|
236
251
|
document.removeEventListener('pointermove', this.onPointerMove);
|
package/package.json
CHANGED
|
@@ -216,6 +216,35 @@ function getMaxDomScrollHeight(): number {
|
|
|
216
216
|
return detectedMaxDomHeight;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Observe an element's size, but run the callback on the next animation frame
|
|
221
|
+
* and coalesce bursts into a single call. This is what keeps the benign but
|
|
222
|
+
* noisy "ResizeObserver loop completed with undelivered notifications" warning
|
|
223
|
+
* out of the console: the browser emits it when an observer callback
|
|
224
|
+
* synchronously mutates layout in a way that would require another notification
|
|
225
|
+
* within the same delivery cycle - which our callbacks do (they bump reactive
|
|
226
|
+
* versions / remeasure, driving a re-layout of the observed element). Deferring
|
|
227
|
+
* the work to the next frame lets the current delivery finish cleanly, so the
|
|
228
|
+
* loop never spans a single cycle. This is especially visible when swapping the
|
|
229
|
+
* whole grid (e.g. switching demos), which remounts everything at once.
|
|
230
|
+
* Returns a disconnect function suitable for an $effect cleanup.
|
|
231
|
+
*/
|
|
232
|
+
function observeSizeRaf(el: Element, cb: () => void): () => void {
|
|
233
|
+
let frame = 0;
|
|
234
|
+
const observer = new ResizeObserver(() => {
|
|
235
|
+
if (frame) return;
|
|
236
|
+
frame = requestAnimationFrame(() => {
|
|
237
|
+
frame = 0;
|
|
238
|
+
cb();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
observer.observe(el);
|
|
242
|
+
return () => {
|
|
243
|
+
if (frame) cancelAnimationFrame(frame);
|
|
244
|
+
observer.disconnect();
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
219
248
|
/**
|
|
220
249
|
* SvGrid controller. The component's entire reactive core - every $state,
|
|
221
250
|
* $derived, $effect and handler - lives here so SvGrid.svelte can stay a thin
|
|
@@ -1057,6 +1086,17 @@ export function createSvGridController<
|
|
|
1057
1086
|
const start = pageIndex * pageSize;
|
|
1058
1087
|
return rows.slice(start, start + pageSize);
|
|
1059
1088
|
});
|
|
1089
|
+
|
|
1090
|
+
// When a filter reduces the dataset, the stored pageIndex can point beyond
|
|
1091
|
+
// the last valid page. Reset to page 0 so the grid never shows a blank body.
|
|
1092
|
+
$effect(() => {
|
|
1093
|
+
if (!paginationEnabled) return;
|
|
1094
|
+
const { pageIndex, pageSize } = paginationState;
|
|
1095
|
+
const pageCount = Math.ceil(allRowsBeforePagination.length / pageSize);
|
|
1096
|
+
if (pageCount > 0 && pageIndex >= pageCount) {
|
|
1097
|
+
grid.setPagination({ pageIndex: 0, pageSize });
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1060
1100
|
const rowSelectionState = $derived.by(() => {
|
|
1061
1101
|
gridStateVersion;
|
|
1062
1102
|
return grid.getState().rowSelection ?? {};
|
|
@@ -1356,14 +1396,45 @@ export function createSvGridController<
|
|
|
1356
1396
|
return columnVirtualizer.getTotalSize();
|
|
1357
1397
|
});
|
|
1358
1398
|
const renderedColumnItems = $derived.by(() => {
|
|
1359
|
-
if (columnVirtualizationEnabled)
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1399
|
+
if (!columnVirtualizationEnabled) {
|
|
1400
|
+
let start = 0;
|
|
1401
|
+
return allColumns.map((column, index) => {
|
|
1402
|
+
const size = getColumnWidth(column.id);
|
|
1403
|
+
const item = { index, key: index, size, start, end: start + size };
|
|
1404
|
+
start += size;
|
|
1405
|
+
return item;
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
// Pinned columns are position:sticky, so they only stay pinned while their
|
|
1409
|
+
// cell is in the DOM. Plain column virtualization drops them once they leave
|
|
1410
|
+
// the scroll window, and the pinned column vanishes. Because allColumns is
|
|
1411
|
+
// ordered [pinnedLeft, unpinned, pinnedRight], we keep the rendered window
|
|
1412
|
+
// CONTIGUOUS from the pinned-left prefix (index 0) through the pinned-right
|
|
1413
|
+
// suffix (last index) whenever those exist. The pinned cells are then always
|
|
1414
|
+
// rendered - the existing single-spacer layout positions everything, so no
|
|
1415
|
+
// markup changes are needed. (Cost: with a pinned side, the columns between
|
|
1416
|
+
// that edge and the window are also rendered; negligible for typical grids,
|
|
1417
|
+
// and correctness beats shaving a few off-screen cells.)
|
|
1418
|
+
const window = virtualColumns;
|
|
1419
|
+
const hasLeft = columnPinning.left.length > 0;
|
|
1420
|
+
const hasRight = columnPinning.right.length > 0;
|
|
1421
|
+
if ((!hasLeft && !hasRight) || window.length === 0) return window;
|
|
1422
|
+
|
|
1423
|
+
const firstIdx = window[0]!.index;
|
|
1424
|
+
const lastIdx = window[window.length - 1]!.index;
|
|
1425
|
+
const startIndex = hasLeft ? 0 : firstIdx;
|
|
1426
|
+
const endIndex = hasRight ? allColumns.length - 1 : lastIdx;
|
|
1427
|
+
if (startIndex === firstIdx && endIndex === lastIdx) return window;
|
|
1428
|
+
|
|
1429
|
+
const items: Array<{ index: number; key: number; size: number; start: number; end: number }> = [];
|
|
1430
|
+
let offset = 0;
|
|
1431
|
+
for (let i = 0; i < startIndex; i += 1) offset += getColumnWidth(allColumns[i]!.id);
|
|
1432
|
+
for (let i = startIndex; i <= endIndex; i += 1) {
|
|
1433
|
+
const size = getColumnWidth(allColumns[i]!.id);
|
|
1434
|
+
items.push({ index: i, key: i, size, start: offset, end: offset + size });
|
|
1435
|
+
offset += size;
|
|
1436
|
+
}
|
|
1437
|
+
return items;
|
|
1367
1438
|
});
|
|
1368
1439
|
const renderedColumns = $derived.by(() =>
|
|
1369
1440
|
renderedColumnItems
|
|
@@ -1461,11 +1532,9 @@ export function createSvGridController<
|
|
|
1461
1532
|
$effect(() => {
|
|
1462
1533
|
if (!theadEl) return;
|
|
1463
1534
|
headerHeight = theadEl.offsetHeight;
|
|
1464
|
-
|
|
1535
|
+
return observeSizeRaf(theadEl, () => {
|
|
1465
1536
|
headerHeight = theadEl?.offsetHeight ?? 0;
|
|
1466
1537
|
});
|
|
1467
|
-
observer.observe(theadEl);
|
|
1468
|
-
return () => observer.disconnect();
|
|
1469
1538
|
});
|
|
1470
1539
|
|
|
1471
1540
|
// Bump scrollVersion when the table's layout size changes so scrollbar
|
|
@@ -1473,11 +1542,9 @@ export function createSvGridController<
|
|
|
1473
1542
|
// after column resize / show-hide / add-remove.
|
|
1474
1543
|
$effect(() => {
|
|
1475
1544
|
if (!gridRootEl) return;
|
|
1476
|
-
|
|
1545
|
+
return observeSizeRaf(gridRootEl, () => {
|
|
1477
1546
|
scrollVersion += 1;
|
|
1478
1547
|
});
|
|
1479
|
-
observer.observe(gridRootEl);
|
|
1480
|
-
return () => observer.disconnect();
|
|
1481
1548
|
});
|
|
1482
1549
|
|
|
1483
1550
|
$effect(() => {
|
|
@@ -1596,12 +1663,10 @@ export function createSvGridController<
|
|
|
1596
1663
|
|
|
1597
1664
|
$effect(() => {
|
|
1598
1665
|
if (!scrollContainer) return;
|
|
1599
|
-
|
|
1666
|
+
return observeSizeRaf(scrollContainer, () => {
|
|
1600
1667
|
viewportVersion += 1;
|
|
1601
1668
|
if (!hasMeasured) hasMeasured = true;
|
|
1602
1669
|
});
|
|
1603
|
-
observer.observe(scrollContainer);
|
|
1604
|
-
return () => observer.disconnect();
|
|
1605
1670
|
});
|
|
1606
1671
|
|
|
1607
1672
|
$effect(() => {
|
package/src/a11y.ts
CHANGED
|
@@ -50,7 +50,7 @@ export function getGridCellA11yProps(input: GridCellA11yInput = {}) {
|
|
|
50
50
|
...(id ? { id } : {}),
|
|
51
51
|
...(colIndex !== undefined ? { 'aria-colindex': colIndex } : {}),
|
|
52
52
|
...(rowIndex !== undefined ? { 'aria-rowindex': rowIndex } : {}),
|
|
53
|
-
|
|
53
|
+
'aria-selected': selected,
|
|
54
54
|
} as const
|
|
55
55
|
}
|
|
56
56
|
|
package/src/menus.ts
CHANGED
|
@@ -373,6 +373,7 @@ export function createMenus<
|
|
|
373
373
|
...prev,
|
|
374
374
|
pageIndex: Math.max((prev?.pageIndex ?? 0) + delta, 0),
|
|
375
375
|
}));
|
|
376
|
+
ctx.scrollContainer?.scrollTo({ top: 0 });
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
function goToPage(pageIndex: number) {
|
|
@@ -380,6 +381,7 @@ export function createMenus<
|
|
|
380
381
|
...prev,
|
|
381
382
|
pageIndex: Math.max(0, pageIndex),
|
|
382
383
|
}));
|
|
384
|
+
ctx.scrollContainer?.scrollTo({ top: 0 });
|
|
383
385
|
}
|
|
384
386
|
|
|
385
387
|
function setPageSize(pageSize: number) {
|
|
@@ -447,9 +449,15 @@ export function createMenus<
|
|
|
447
449
|
const rowModel = ctx.allRows[rowIndex];
|
|
448
450
|
const row = rowModel?.original ?? null;
|
|
449
451
|
const rowId = rowModel?.id ?? String(rowIndex);
|
|
452
|
+
// Reserve ~280px for the menu body. If the click is in the bottom third,
|
|
453
|
+
// flip the menu above the cursor so it stays fully in the viewport.
|
|
454
|
+
const menuHeight = 280;
|
|
455
|
+
const y = event.clientY + menuHeight > window.innerHeight
|
|
456
|
+
? Math.max(8, event.clientY - menuHeight)
|
|
457
|
+
: event.clientY;
|
|
450
458
|
ctx.contextMenuPos = {
|
|
451
459
|
x: clampMenuX(event.clientX, 220),
|
|
452
|
-
y
|
|
460
|
+
y,
|
|
453
461
|
};
|
|
454
462
|
ctx.contextMenuFor = { rowIndex, colIndex, columnId, rowId, row };
|
|
455
463
|
}
|
package/src/scroll-sync.ts
CHANGED
|
@@ -191,7 +191,7 @@ export function createScrollSync<
|
|
|
191
191
|
function onBodyScroll(event: Event) {
|
|
192
192
|
const container = event.currentTarget as HTMLDivElement | null;
|
|
193
193
|
if (!container) return;
|
|
194
|
-
if (ctx.columnMenuFor || ctx.operatorMenuFor) ctx.closeMenus();
|
|
194
|
+
if (ctx.columnMenuFor || ctx.operatorMenuFor || ctx.contextMenuFor) ctx.closeMenus();
|
|
195
195
|
scheduleScrollSync(container.scrollTop, container.scrollLeft);
|
|
196
196
|
// Mirror horizontal scroll to any aligned grids in the same group.
|
|
197
197
|
if (ctx.props.alignedGridGroup != null) ctx.broadcastAlignedScroll(container.scrollLeft);
|
package/src/sv-grid-scrollbar.ts
CHANGED
|
@@ -160,6 +160,7 @@ class SvGridScrollbarElement extends ScrollbarBase {
|
|
|
160
160
|
private valueStart = 0
|
|
161
161
|
private holdTimer: ReturnType<typeof setTimeout> | null = null
|
|
162
162
|
private resizeObserver: ResizeObserver | null = null
|
|
163
|
+
private resizeFrame = 0
|
|
163
164
|
|
|
164
165
|
connectedCallback() {
|
|
165
166
|
if (this.shadowRoot) return
|
|
@@ -187,7 +188,17 @@ class SvGridScrollbarElement extends ScrollbarBase {
|
|
|
187
188
|
btn.addEventListener('pointercancel', this.stopHold)
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
|
|
191
|
+
// Coalesce resize notifications into a single next-frame render so the
|
|
192
|
+
// observer callback never re-lays-out the observed element within the same
|
|
193
|
+
// delivery cycle (which is what triggers the benign but noisy
|
|
194
|
+
// "ResizeObserver loop completed with undelivered notifications" warning).
|
|
195
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
196
|
+
if (this.resizeFrame) return
|
|
197
|
+
this.resizeFrame = requestAnimationFrame(() => {
|
|
198
|
+
this.resizeFrame = 0
|
|
199
|
+
this.render()
|
|
200
|
+
})
|
|
201
|
+
})
|
|
191
202
|
this.resizeObserver.observe(this)
|
|
192
203
|
|
|
193
204
|
this.render()
|
|
@@ -195,6 +206,8 @@ class SvGridScrollbarElement extends ScrollbarBase {
|
|
|
195
206
|
|
|
196
207
|
disconnectedCallback() {
|
|
197
208
|
this.stopHold()
|
|
209
|
+
if (this.resizeFrame) cancelAnimationFrame(this.resizeFrame)
|
|
210
|
+
this.resizeFrame = 0
|
|
198
211
|
this.resizeObserver?.disconnect()
|
|
199
212
|
this.resizeObserver = null
|
|
200
213
|
document.removeEventListener('pointermove', this.onPointerMove)
|