@ornery/ui-grid-vanilla 0.1.8 → 0.1.9
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/index.d.mts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +157 -19
- package/dist/index.mjs +157 -19
- package/package.json +2 -2
- package/src/index.integration.test.ts +31 -0
- package/src/index.test.ts +1 -1
- package/src/ui-grid-standalone.element.ts +200 -26
package/dist/index.d.mts
CHANGED
|
@@ -169,6 +169,7 @@ declare class UiGridStandaloneElement extends HTMLElement {
|
|
|
169
169
|
private draggedColumnName;
|
|
170
170
|
private dropTargetColumnName;
|
|
171
171
|
private scrollPosition;
|
|
172
|
+
private horizontalScrollPosition;
|
|
172
173
|
private scrollFrame;
|
|
173
174
|
private suppressScrollEvent;
|
|
174
175
|
private lastVirtualStartIndex;
|
|
@@ -178,6 +179,9 @@ declare class UiGridStandaloneElement extends HTMLElement {
|
|
|
178
179
|
private benchmarkAverage;
|
|
179
180
|
private skipNextRender;
|
|
180
181
|
private dataFrame;
|
|
182
|
+
private pendingPatchedRowIds;
|
|
183
|
+
private pendingDataRefreshMode;
|
|
184
|
+
private lastScrollActivityAt;
|
|
181
185
|
static get observedAttributes(): string[];
|
|
182
186
|
get options(): GridOptions;
|
|
183
187
|
set options(value: GridOptions);
|
|
@@ -273,6 +277,7 @@ declare class UiGridStandaloneElement extends HTMLElement {
|
|
|
273
277
|
* (the fresh innerHTML already reflects the latest data).
|
|
274
278
|
*/
|
|
275
279
|
setData(rows: GridRecord[]): void;
|
|
280
|
+
private flushPendingDataRefresh;
|
|
276
281
|
private patchCells;
|
|
277
282
|
connectedCallback(): void;
|
|
278
283
|
disconnectedCallback(): void;
|
|
@@ -296,11 +301,14 @@ declare class UiGridStandaloneElement extends HTMLElement {
|
|
|
296
301
|
private renderPagination;
|
|
297
302
|
private renderSlotRegistry;
|
|
298
303
|
private renderCellTemplate;
|
|
304
|
+
private renderCellTemplateFromMarkup;
|
|
299
305
|
private renderExpandableTemplate;
|
|
300
306
|
private getTemplateMarkup;
|
|
301
307
|
private interpolateTemplate;
|
|
302
308
|
private resolveTemplateValue;
|
|
303
309
|
private rowIndexFor;
|
|
310
|
+
private classifyDataRefresh;
|
|
311
|
+
private getRenderedDisplayWindow;
|
|
304
312
|
private cellSlotName;
|
|
305
313
|
private downloadCsv;
|
|
306
314
|
private renderControlIcon;
|
package/dist/index.d.ts
CHANGED
|
@@ -169,6 +169,7 @@ declare class UiGridStandaloneElement extends HTMLElement {
|
|
|
169
169
|
private draggedColumnName;
|
|
170
170
|
private dropTargetColumnName;
|
|
171
171
|
private scrollPosition;
|
|
172
|
+
private horizontalScrollPosition;
|
|
172
173
|
private scrollFrame;
|
|
173
174
|
private suppressScrollEvent;
|
|
174
175
|
private lastVirtualStartIndex;
|
|
@@ -178,6 +179,9 @@ declare class UiGridStandaloneElement extends HTMLElement {
|
|
|
178
179
|
private benchmarkAverage;
|
|
179
180
|
private skipNextRender;
|
|
180
181
|
private dataFrame;
|
|
182
|
+
private pendingPatchedRowIds;
|
|
183
|
+
private pendingDataRefreshMode;
|
|
184
|
+
private lastScrollActivityAt;
|
|
181
185
|
static get observedAttributes(): string[];
|
|
182
186
|
get options(): GridOptions;
|
|
183
187
|
set options(value: GridOptions);
|
|
@@ -273,6 +277,7 @@ declare class UiGridStandaloneElement extends HTMLElement {
|
|
|
273
277
|
* (the fresh innerHTML already reflects the latest data).
|
|
274
278
|
*/
|
|
275
279
|
setData(rows: GridRecord[]): void;
|
|
280
|
+
private flushPendingDataRefresh;
|
|
276
281
|
private patchCells;
|
|
277
282
|
connectedCallback(): void;
|
|
278
283
|
disconnectedCallback(): void;
|
|
@@ -296,11 +301,14 @@ declare class UiGridStandaloneElement extends HTMLElement {
|
|
|
296
301
|
private renderPagination;
|
|
297
302
|
private renderSlotRegistry;
|
|
298
303
|
private renderCellTemplate;
|
|
304
|
+
private renderCellTemplateFromMarkup;
|
|
299
305
|
private renderExpandableTemplate;
|
|
300
306
|
private getTemplateMarkup;
|
|
301
307
|
private interpolateTemplate;
|
|
302
308
|
private resolveTemplateValue;
|
|
303
309
|
private rowIndexFor;
|
|
310
|
+
private classifyDataRefresh;
|
|
311
|
+
private getRenderedDisplayWindow;
|
|
304
312
|
private cellSlotName;
|
|
305
313
|
private downloadCsv;
|
|
306
314
|
private renderControlIcon;
|
package/dist/index.js
CHANGED
|
@@ -779,6 +779,7 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
779
779
|
draggedColumnName = null;
|
|
780
780
|
dropTargetColumnName = null;
|
|
781
781
|
scrollPosition = 0;
|
|
782
|
+
horizontalScrollPosition = 0;
|
|
782
783
|
scrollFrame = null;
|
|
783
784
|
suppressScrollEvent = false;
|
|
784
785
|
lastVirtualStartIndex = -1;
|
|
@@ -788,6 +789,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
788
789
|
benchmarkAverage = "\u2014";
|
|
789
790
|
skipNextRender = false;
|
|
790
791
|
dataFrame = null;
|
|
792
|
+
pendingPatchedRowIds = null;
|
|
793
|
+
pendingDataRefreshMode = null;
|
|
794
|
+
lastScrollActivityAt = 0;
|
|
791
795
|
static get observedAttributes() {
|
|
792
796
|
return [
|
|
793
797
|
// Scalar attributes
|
|
@@ -833,10 +837,14 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
833
837
|
];
|
|
834
838
|
}
|
|
835
839
|
get options() {
|
|
836
|
-
|
|
840
|
+
if (this.activeOptions !== null) {
|
|
841
|
+
return this.buildEffectiveOptions(this.activeOptions);
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
837
844
|
id: "__ui-grid-pending__",
|
|
838
845
|
data: [],
|
|
839
|
-
columnDefs: []
|
|
846
|
+
columnDefs: [],
|
|
847
|
+
...this.attributeOptions
|
|
840
848
|
};
|
|
841
849
|
}
|
|
842
850
|
set options(value) {
|
|
@@ -959,8 +967,15 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
959
967
|
);
|
|
960
968
|
if (paginationPageSizes !== void 0)
|
|
961
969
|
this.attributeOptions.paginationPageSizes = paginationPageSizes;
|
|
962
|
-
if (this.activeOptions) {
|
|
970
|
+
if (this.activeOptions !== null) {
|
|
963
971
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
972
|
+
} else {
|
|
973
|
+
this.ensureController({
|
|
974
|
+
id: "__ui-grid-pending__",
|
|
975
|
+
data: [],
|
|
976
|
+
columnDefs: [],
|
|
977
|
+
...this.attributeOptions
|
|
978
|
+
});
|
|
964
979
|
}
|
|
965
980
|
}
|
|
966
981
|
parseBooleanAttribute(name) {
|
|
@@ -1272,18 +1287,49 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1272
1287
|
if (!controller) {
|
|
1273
1288
|
return;
|
|
1274
1289
|
}
|
|
1290
|
+
const previousSnapshot = this.snapshot;
|
|
1275
1291
|
this.skipNextRender = true;
|
|
1276
1292
|
controller.refreshData(rows);
|
|
1277
1293
|
this.skipNextRender = false;
|
|
1294
|
+
const { mode, changedRowIds } = this.classifyDataRefresh(previousSnapshot, this.snapshot);
|
|
1295
|
+
if (mode === null) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
this.pendingDataRefreshMode = mode;
|
|
1299
|
+
this.pendingPatchedRowIds = changedRowIds;
|
|
1278
1300
|
if (this.dataFrame !== null) {
|
|
1279
1301
|
cancelAnimationFrame(this.dataFrame);
|
|
1280
1302
|
}
|
|
1281
1303
|
this.dataFrame = requestAnimationFrame(() => {
|
|
1282
|
-
this.
|
|
1283
|
-
this.patchCells();
|
|
1304
|
+
this.flushPendingDataRefresh();
|
|
1284
1305
|
});
|
|
1285
1306
|
}
|
|
1286
|
-
|
|
1307
|
+
flushPendingDataRefresh() {
|
|
1308
|
+
this.dataFrame = null;
|
|
1309
|
+
const mode = this.pendingDataRefreshMode;
|
|
1310
|
+
if (mode === null) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (Date.now() - this.lastScrollActivityAt < 80) {
|
|
1314
|
+
this.dataFrame = requestAnimationFrame(() => {
|
|
1315
|
+
this.flushPendingDataRefresh();
|
|
1316
|
+
});
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const changedRowIds = this.pendingPatchedRowIds;
|
|
1320
|
+
this.pendingDataRefreshMode = null;
|
|
1321
|
+
this.pendingPatchedRowIds = null;
|
|
1322
|
+
if (mode === "patch") {
|
|
1323
|
+
this.patchCells(changedRowIds ?? void 0);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
if (mode === "virtual") {
|
|
1327
|
+
this.renderVirtualBody();
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
this.render();
|
|
1331
|
+
}
|
|
1332
|
+
patchCells(changedRowIds) {
|
|
1287
1333
|
const snapshot = this.snapshot;
|
|
1288
1334
|
if (!snapshot) {
|
|
1289
1335
|
return;
|
|
@@ -1293,30 +1339,37 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1293
1339
|
return;
|
|
1294
1340
|
}
|
|
1295
1341
|
const rowMap = /* @__PURE__ */ new Map();
|
|
1342
|
+
const rowIndexMap = /* @__PURE__ */ new Map();
|
|
1296
1343
|
for (const row of snapshot.pipeline.visibleRows) {
|
|
1297
1344
|
rowMap.set(row.id, row);
|
|
1345
|
+
rowIndexMap.set(row.id, rowIndexMap.size);
|
|
1298
1346
|
}
|
|
1299
1347
|
const colMap = /* @__PURE__ */ new Map();
|
|
1348
|
+
const templateMarkupMap = /* @__PURE__ */ new Map();
|
|
1300
1349
|
for (const col of snapshot.visibleColumns) {
|
|
1301
1350
|
colMap.set(col.name, col);
|
|
1351
|
+
templateMarkupMap.set(col.name, this.getTemplateMarkup(this.cellSlotName(col)));
|
|
1302
1352
|
}
|
|
1303
1353
|
const cells = root.querySelectorAll("[data-row][data-column]");
|
|
1304
1354
|
for (const cell of cells) {
|
|
1305
1355
|
const rowId = cell.dataset["row"];
|
|
1306
1356
|
const colName = cell.dataset["column"];
|
|
1307
1357
|
if (!rowId || !colName) continue;
|
|
1358
|
+
if (changedRowIds && !changedRowIds.has(rowId)) continue;
|
|
1308
1359
|
const row = rowMap.get(rowId);
|
|
1309
1360
|
const column = colMap.get(colName);
|
|
1310
1361
|
if (!row || !column) continue;
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1362
|
+
if (cell.classList.contains("cell-editing")) continue;
|
|
1363
|
+
const cellContent = cell.querySelector(".cell-content");
|
|
1364
|
+
if (!cellContent) continue;
|
|
1365
|
+
const newContent = this.renderCellTemplateFromMarkup(
|
|
1366
|
+
row,
|
|
1367
|
+
column,
|
|
1368
|
+
rowIndexMap.get(rowId) ?? 0,
|
|
1369
|
+
templateMarkupMap.get(colName) ?? null
|
|
1370
|
+
);
|
|
1371
|
+
if (cellContent.innerHTML !== newContent) {
|
|
1372
|
+
cellContent.innerHTML = newContent;
|
|
1320
1373
|
}
|
|
1321
1374
|
}
|
|
1322
1375
|
}
|
|
@@ -1668,7 +1721,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1668
1721
|
if (!target?.classList.contains("grid-table") || this.suppressScrollEvent) {
|
|
1669
1722
|
return;
|
|
1670
1723
|
}
|
|
1724
|
+
this.lastScrollActivityAt = Date.now();
|
|
1671
1725
|
this.scrollPosition = target.scrollTop;
|
|
1726
|
+
this.horizontalScrollPosition = target.scrollLeft;
|
|
1672
1727
|
if (!this.snapshot?.pipeline.virtualizationEnabled) {
|
|
1673
1728
|
return;
|
|
1674
1729
|
}
|
|
@@ -1749,6 +1804,7 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1749
1804
|
const previousGridTable = root.querySelector(".grid-table");
|
|
1750
1805
|
if (previousGridTable && !this.suppressScrollEvent) {
|
|
1751
1806
|
this.scrollPosition = previousGridTable.scrollTop;
|
|
1807
|
+
this.horizontalScrollPosition = previousGridTable.scrollLeft;
|
|
1752
1808
|
}
|
|
1753
1809
|
if (!snapshot) {
|
|
1754
1810
|
root.innerHTML = `<style>${import_ui_grid_core2.GRID_CORE_CSS}</style><section class="grid-shell ui-grid-shell"><div class="empty-state ui-grid-no-row-overlay"><strong>No grid options provided.</strong></div></section>`;
|
|
@@ -1795,12 +1851,18 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1795
1851
|
const bodyContent = itemsToRender.map((item, index) => this.renderDisplayItem(item, startIndex + index)).join("");
|
|
1796
1852
|
const body = snapshot.pipeline.displayItems.length > 0 ? virtualizationEnabled ? `<div class="grid-virtual-spacer" style="height:${totalVirtualHeight}px"><div class="body-grid ui-grid-canvas grid-virtual-body" style="grid-template-columns:${templateColumns};top:${virtualOffset}px">${bodyContent}</div></div>` : `<div class="body-grid ui-grid-canvas" style="grid-template-columns:${templateColumns}">${bodyContent}</div>` : `<div class="empty-state ui-grid-no-row-overlay"><strong>${escapeHtml(options.emptyMessage ?? labels.emptyHeading)}</strong><p>${escapeHtml(labels.emptyDescription)}</p></div>`;
|
|
1797
1853
|
const pagination = paginationEnabled && showPagination ? this.renderPagination(snapshot) : "";
|
|
1798
|
-
const
|
|
1854
|
+
const hasViewportScroll = virtualizationEnabled || options.viewportHeight !== void 0;
|
|
1855
|
+
const gridTableStyle = `${hasViewportScroll ? `height:${viewportHeight}px;overflow-y:auto;` : ""}--ui-grid-header-sticky-top:${headerStickyTop}px;`;
|
|
1799
1856
|
root.innerHTML = `<style>${import_ui_grid_core2.GRID_CORE_CSS}</style>${slotRegistry}<section class="grid-frame ui-grid" role="grid" aria-label="${escapeHtml(options.title ?? "Data grid")}"><div class="grid-table ui-grid-contents-wrapper" style="${gridTableStyle}"><div class="header-grid ui-grid-header ui-grid-header-canvas" style="grid-template-columns:${templateColumns}">${header}</div>${filterRow}${body}</div>${pagination}</section>`;
|
|
1800
1857
|
const gridTable = root.querySelector(".grid-table");
|
|
1801
|
-
if (gridTable && this.scrollPosition > 0
|
|
1858
|
+
if (gridTable && (this.scrollPosition > 0 || this.horizontalScrollPosition > 0)) {
|
|
1802
1859
|
this.suppressScrollEvent = true;
|
|
1803
|
-
|
|
1860
|
+
if (this.scrollPosition > 0) {
|
|
1861
|
+
gridTable.scrollTop = this.scrollPosition;
|
|
1862
|
+
}
|
|
1863
|
+
if (this.horizontalScrollPosition > 0) {
|
|
1864
|
+
gridTable.scrollLeft = this.horizontalScrollPosition;
|
|
1865
|
+
}
|
|
1804
1866
|
requestAnimationFrame(() => {
|
|
1805
1867
|
this.suppressScrollEvent = false;
|
|
1806
1868
|
});
|
|
@@ -1912,7 +1974,7 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1912
1974
|
editing ? "cell-editing" : ""
|
|
1913
1975
|
].filter(Boolean).join(" ");
|
|
1914
1976
|
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : "";
|
|
1915
|
-
return `<div class="${classes}" tabindex="0" data-row="${escapeHtml(rowId)}" data-column="${escapeHtml(columnName)}" style="${stickyStyle}"><div class="cell-shell" style="padding-inline-start:${escapeHtml(controller.cellIndent(row, column))}">${treeToggle}${expandToggle}
|
|
1977
|
+
return `<div class="${classes}" tabindex="0" data-row="${escapeHtml(rowId)}" data-column="${escapeHtml(columnName)}" style="${stickyStyle}"><div class="cell-shell" style="padding-inline-start:${escapeHtml(controller.cellIndent(row, column))}">${treeToggle}${expandToggle}<div class="cell-content">${content}</div></div></div>`;
|
|
1916
1978
|
}
|
|
1917
1979
|
renderPagination(snapshot) {
|
|
1918
1980
|
const pageSizes = snapshot.options.paginationPageSizes ?? [10, 25, 50, 100];
|
|
@@ -1926,6 +1988,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1926
1988
|
}
|
|
1927
1989
|
renderCellTemplate(row, column, displayIndex) {
|
|
1928
1990
|
const templateMarkup = this.getTemplateMarkup(this.cellSlotName(column));
|
|
1991
|
+
return this.renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup);
|
|
1992
|
+
}
|
|
1993
|
+
renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup) {
|
|
1929
1994
|
if (!templateMarkup) {
|
|
1930
1995
|
return `<span class="cell-value">${escapeHtml(this.controller?.displayValue(row, column) ?? "")}</span>`;
|
|
1931
1996
|
}
|
|
@@ -1978,6 +2043,79 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1978
2043
|
rowIndexFor(row, fallback = 0) {
|
|
1979
2044
|
return this.snapshot?.pipeline.visibleRows.findIndex((candidate) => candidate.id === row.id) ?? fallback;
|
|
1980
2045
|
}
|
|
2046
|
+
classifyDataRefresh(previousSnapshot, nextSnapshot) {
|
|
2047
|
+
if (!nextSnapshot) {
|
|
2048
|
+
return { mode: "full", changedRowIds: null };
|
|
2049
|
+
}
|
|
2050
|
+
if (!previousSnapshot) {
|
|
2051
|
+
return {
|
|
2052
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
|
|
2053
|
+
changedRowIds: null
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
if (previousSnapshot.visibleColumns.length !== nextSnapshot.visibleColumns.length) {
|
|
2057
|
+
return { mode: "full", changedRowIds: null };
|
|
2058
|
+
}
|
|
2059
|
+
for (let index = 0; index < previousSnapshot.visibleColumns.length; index += 1) {
|
|
2060
|
+
if (previousSnapshot.visibleColumns[index]?.name !== nextSnapshot.visibleColumns[index]?.name) {
|
|
2061
|
+
return { mode: "full", changedRowIds: null };
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
const previousWindow = this.getRenderedDisplayWindow(previousSnapshot);
|
|
2065
|
+
const nextWindow = this.getRenderedDisplayWindow(nextSnapshot);
|
|
2066
|
+
if (previousWindow.startIndex !== nextWindow.startIndex || previousWindow.items.length !== nextWindow.items.length) {
|
|
2067
|
+
return {
|
|
2068
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
|
|
2069
|
+
changedRowIds: null
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
const changedRowIds = /* @__PURE__ */ new Set();
|
|
2073
|
+
for (let index = 0; index < previousWindow.items.length; index += 1) {
|
|
2074
|
+
const previousItem = previousWindow.items[index];
|
|
2075
|
+
const nextItem = nextWindow.items[index];
|
|
2076
|
+
if (!previousItem || !nextItem || previousItem.kind !== "row" || nextItem.kind !== "row") {
|
|
2077
|
+
return {
|
|
2078
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
|
|
2079
|
+
changedRowIds: null
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
if (previousItem.row.id !== nextItem.row.id) {
|
|
2083
|
+
return {
|
|
2084
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
|
|
2085
|
+
changedRowIds: null
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
if (previousItem.row.entity !== nextItem.row.entity) {
|
|
2089
|
+
changedRowIds.add(nextItem.row.id);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return {
|
|
2093
|
+
mode: changedRowIds.size > 0 ? "patch" : null,
|
|
2094
|
+
changedRowIds: changedRowIds.size > 0 ? changedRowIds : null
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
getRenderedDisplayWindow(snapshot) {
|
|
2098
|
+
if (!snapshot.pipeline.virtualizationEnabled) {
|
|
2099
|
+
return {
|
|
2100
|
+
startIndex: 0,
|
|
2101
|
+
items: snapshot.pipeline.displayItems
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
const stickyChromeHeight = this.measuredHeaderStickyHeight + this.measuredFilterStickyHeight;
|
|
2105
|
+
const viewportHeight = snapshot.options.viewportHeight ?? 560;
|
|
2106
|
+
const bodyViewportHeight = Math.max(snapshot.rowSize, viewportHeight - stickyChromeHeight);
|
|
2107
|
+
const bodyScrollTop = Math.max(0, this.scrollPosition - stickyChromeHeight);
|
|
2108
|
+
const overscan = 4;
|
|
2109
|
+
const startIndex = Math.max(0, Math.floor(bodyScrollTop / snapshot.rowSize) - overscan);
|
|
2110
|
+
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
2111
|
+
return {
|
|
2112
|
+
startIndex,
|
|
2113
|
+
items: snapshot.pipeline.displayItems.slice(
|
|
2114
|
+
startIndex,
|
|
2115
|
+
Math.min(snapshot.pipeline.displayItems.length, startIndex + visibleCount)
|
|
2116
|
+
)
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
1981
2119
|
cellSlotName(column) {
|
|
1982
2120
|
return `cell-${column.name}`;
|
|
1983
2121
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -821,6 +821,7 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
821
821
|
draggedColumnName = null;
|
|
822
822
|
dropTargetColumnName = null;
|
|
823
823
|
scrollPosition = 0;
|
|
824
|
+
horizontalScrollPosition = 0;
|
|
824
825
|
scrollFrame = null;
|
|
825
826
|
suppressScrollEvent = false;
|
|
826
827
|
lastVirtualStartIndex = -1;
|
|
@@ -830,6 +831,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
830
831
|
benchmarkAverage = "\u2014";
|
|
831
832
|
skipNextRender = false;
|
|
832
833
|
dataFrame = null;
|
|
834
|
+
pendingPatchedRowIds = null;
|
|
835
|
+
pendingDataRefreshMode = null;
|
|
836
|
+
lastScrollActivityAt = 0;
|
|
833
837
|
static get observedAttributes() {
|
|
834
838
|
return [
|
|
835
839
|
// Scalar attributes
|
|
@@ -875,10 +879,14 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
875
879
|
];
|
|
876
880
|
}
|
|
877
881
|
get options() {
|
|
878
|
-
|
|
882
|
+
if (this.activeOptions !== null) {
|
|
883
|
+
return this.buildEffectiveOptions(this.activeOptions);
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
879
886
|
id: "__ui-grid-pending__",
|
|
880
887
|
data: [],
|
|
881
|
-
columnDefs: []
|
|
888
|
+
columnDefs: [],
|
|
889
|
+
...this.attributeOptions
|
|
882
890
|
};
|
|
883
891
|
}
|
|
884
892
|
set options(value) {
|
|
@@ -1001,8 +1009,15 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1001
1009
|
);
|
|
1002
1010
|
if (paginationPageSizes !== void 0)
|
|
1003
1011
|
this.attributeOptions.paginationPageSizes = paginationPageSizes;
|
|
1004
|
-
if (this.activeOptions) {
|
|
1012
|
+
if (this.activeOptions !== null) {
|
|
1005
1013
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
1014
|
+
} else {
|
|
1015
|
+
this.ensureController({
|
|
1016
|
+
id: "__ui-grid-pending__",
|
|
1017
|
+
data: [],
|
|
1018
|
+
columnDefs: [],
|
|
1019
|
+
...this.attributeOptions
|
|
1020
|
+
});
|
|
1006
1021
|
}
|
|
1007
1022
|
}
|
|
1008
1023
|
parseBooleanAttribute(name) {
|
|
@@ -1314,18 +1329,49 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1314
1329
|
if (!controller) {
|
|
1315
1330
|
return;
|
|
1316
1331
|
}
|
|
1332
|
+
const previousSnapshot = this.snapshot;
|
|
1317
1333
|
this.skipNextRender = true;
|
|
1318
1334
|
controller.refreshData(rows);
|
|
1319
1335
|
this.skipNextRender = false;
|
|
1336
|
+
const { mode, changedRowIds } = this.classifyDataRefresh(previousSnapshot, this.snapshot);
|
|
1337
|
+
if (mode === null) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
this.pendingDataRefreshMode = mode;
|
|
1341
|
+
this.pendingPatchedRowIds = changedRowIds;
|
|
1320
1342
|
if (this.dataFrame !== null) {
|
|
1321
1343
|
cancelAnimationFrame(this.dataFrame);
|
|
1322
1344
|
}
|
|
1323
1345
|
this.dataFrame = requestAnimationFrame(() => {
|
|
1324
|
-
this.
|
|
1325
|
-
this.patchCells();
|
|
1346
|
+
this.flushPendingDataRefresh();
|
|
1326
1347
|
});
|
|
1327
1348
|
}
|
|
1328
|
-
|
|
1349
|
+
flushPendingDataRefresh() {
|
|
1350
|
+
this.dataFrame = null;
|
|
1351
|
+
const mode = this.pendingDataRefreshMode;
|
|
1352
|
+
if (mode === null) {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
if (Date.now() - this.lastScrollActivityAt < 80) {
|
|
1356
|
+
this.dataFrame = requestAnimationFrame(() => {
|
|
1357
|
+
this.flushPendingDataRefresh();
|
|
1358
|
+
});
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const changedRowIds = this.pendingPatchedRowIds;
|
|
1362
|
+
this.pendingDataRefreshMode = null;
|
|
1363
|
+
this.pendingPatchedRowIds = null;
|
|
1364
|
+
if (mode === "patch") {
|
|
1365
|
+
this.patchCells(changedRowIds ?? void 0);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (mode === "virtual") {
|
|
1369
|
+
this.renderVirtualBody();
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
this.render();
|
|
1373
|
+
}
|
|
1374
|
+
patchCells(changedRowIds) {
|
|
1329
1375
|
const snapshot = this.snapshot;
|
|
1330
1376
|
if (!snapshot) {
|
|
1331
1377
|
return;
|
|
@@ -1335,30 +1381,37 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1335
1381
|
return;
|
|
1336
1382
|
}
|
|
1337
1383
|
const rowMap = /* @__PURE__ */ new Map();
|
|
1384
|
+
const rowIndexMap = /* @__PURE__ */ new Map();
|
|
1338
1385
|
for (const row of snapshot.pipeline.visibleRows) {
|
|
1339
1386
|
rowMap.set(row.id, row);
|
|
1387
|
+
rowIndexMap.set(row.id, rowIndexMap.size);
|
|
1340
1388
|
}
|
|
1341
1389
|
const colMap = /* @__PURE__ */ new Map();
|
|
1390
|
+
const templateMarkupMap = /* @__PURE__ */ new Map();
|
|
1342
1391
|
for (const col of snapshot.visibleColumns) {
|
|
1343
1392
|
colMap.set(col.name, col);
|
|
1393
|
+
templateMarkupMap.set(col.name, this.getTemplateMarkup(this.cellSlotName(col)));
|
|
1344
1394
|
}
|
|
1345
1395
|
const cells = root.querySelectorAll("[data-row][data-column]");
|
|
1346
1396
|
for (const cell of cells) {
|
|
1347
1397
|
const rowId = cell.dataset["row"];
|
|
1348
1398
|
const colName = cell.dataset["column"];
|
|
1349
1399
|
if (!rowId || !colName) continue;
|
|
1400
|
+
if (changedRowIds && !changedRowIds.has(rowId)) continue;
|
|
1350
1401
|
const row = rowMap.get(rowId);
|
|
1351
1402
|
const column = colMap.get(colName);
|
|
1352
1403
|
if (!row || !column) continue;
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1404
|
+
if (cell.classList.contains("cell-editing")) continue;
|
|
1405
|
+
const cellContent = cell.querySelector(".cell-content");
|
|
1406
|
+
if (!cellContent) continue;
|
|
1407
|
+
const newContent = this.renderCellTemplateFromMarkup(
|
|
1408
|
+
row,
|
|
1409
|
+
column,
|
|
1410
|
+
rowIndexMap.get(rowId) ?? 0,
|
|
1411
|
+
templateMarkupMap.get(colName) ?? null
|
|
1412
|
+
);
|
|
1413
|
+
if (cellContent.innerHTML !== newContent) {
|
|
1414
|
+
cellContent.innerHTML = newContent;
|
|
1362
1415
|
}
|
|
1363
1416
|
}
|
|
1364
1417
|
}
|
|
@@ -1710,7 +1763,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1710
1763
|
if (!target?.classList.contains("grid-table") || this.suppressScrollEvent) {
|
|
1711
1764
|
return;
|
|
1712
1765
|
}
|
|
1766
|
+
this.lastScrollActivityAt = Date.now();
|
|
1713
1767
|
this.scrollPosition = target.scrollTop;
|
|
1768
|
+
this.horizontalScrollPosition = target.scrollLeft;
|
|
1714
1769
|
if (!this.snapshot?.pipeline.virtualizationEnabled) {
|
|
1715
1770
|
return;
|
|
1716
1771
|
}
|
|
@@ -1791,6 +1846,7 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1791
1846
|
const previousGridTable = root.querySelector(".grid-table");
|
|
1792
1847
|
if (previousGridTable && !this.suppressScrollEvent) {
|
|
1793
1848
|
this.scrollPosition = previousGridTable.scrollTop;
|
|
1849
|
+
this.horizontalScrollPosition = previousGridTable.scrollLeft;
|
|
1794
1850
|
}
|
|
1795
1851
|
if (!snapshot) {
|
|
1796
1852
|
root.innerHTML = `<style>${GRID_CORE_CSS}</style><section class="grid-shell ui-grid-shell"><div class="empty-state ui-grid-no-row-overlay"><strong>No grid options provided.</strong></div></section>`;
|
|
@@ -1837,12 +1893,18 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1837
1893
|
const bodyContent = itemsToRender.map((item, index) => this.renderDisplayItem(item, startIndex + index)).join("");
|
|
1838
1894
|
const body = snapshot.pipeline.displayItems.length > 0 ? virtualizationEnabled ? `<div class="grid-virtual-spacer" style="height:${totalVirtualHeight}px"><div class="body-grid ui-grid-canvas grid-virtual-body" style="grid-template-columns:${templateColumns};top:${virtualOffset}px">${bodyContent}</div></div>` : `<div class="body-grid ui-grid-canvas" style="grid-template-columns:${templateColumns}">${bodyContent}</div>` : `<div class="empty-state ui-grid-no-row-overlay"><strong>${escapeHtml(options.emptyMessage ?? labels.emptyHeading)}</strong><p>${escapeHtml(labels.emptyDescription)}</p></div>`;
|
|
1839
1895
|
const pagination = paginationEnabled && showPagination ? this.renderPagination(snapshot) : "";
|
|
1840
|
-
const
|
|
1896
|
+
const hasViewportScroll = virtualizationEnabled || options.viewportHeight !== void 0;
|
|
1897
|
+
const gridTableStyle = `${hasViewportScroll ? `height:${viewportHeight}px;overflow-y:auto;` : ""}--ui-grid-header-sticky-top:${headerStickyTop}px;`;
|
|
1841
1898
|
root.innerHTML = `<style>${GRID_CORE_CSS}</style>${slotRegistry}<section class="grid-frame ui-grid" role="grid" aria-label="${escapeHtml(options.title ?? "Data grid")}"><div class="grid-table ui-grid-contents-wrapper" style="${gridTableStyle}"><div class="header-grid ui-grid-header ui-grid-header-canvas" style="grid-template-columns:${templateColumns}">${header}</div>${filterRow}${body}</div>${pagination}</section>`;
|
|
1842
1899
|
const gridTable = root.querySelector(".grid-table");
|
|
1843
|
-
if (gridTable && this.scrollPosition > 0
|
|
1900
|
+
if (gridTable && (this.scrollPosition > 0 || this.horizontalScrollPosition > 0)) {
|
|
1844
1901
|
this.suppressScrollEvent = true;
|
|
1845
|
-
|
|
1902
|
+
if (this.scrollPosition > 0) {
|
|
1903
|
+
gridTable.scrollTop = this.scrollPosition;
|
|
1904
|
+
}
|
|
1905
|
+
if (this.horizontalScrollPosition > 0) {
|
|
1906
|
+
gridTable.scrollLeft = this.horizontalScrollPosition;
|
|
1907
|
+
}
|
|
1846
1908
|
requestAnimationFrame(() => {
|
|
1847
1909
|
this.suppressScrollEvent = false;
|
|
1848
1910
|
});
|
|
@@ -1954,7 +2016,7 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1954
2016
|
editing ? "cell-editing" : ""
|
|
1955
2017
|
].filter(Boolean).join(" ");
|
|
1956
2018
|
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : "";
|
|
1957
|
-
return `<div class="${classes}" tabindex="0" data-row="${escapeHtml(rowId)}" data-column="${escapeHtml(columnName)}" style="${stickyStyle}"><div class="cell-shell" style="padding-inline-start:${escapeHtml(controller.cellIndent(row, column))}">${treeToggle}${expandToggle}
|
|
2019
|
+
return `<div class="${classes}" tabindex="0" data-row="${escapeHtml(rowId)}" data-column="${escapeHtml(columnName)}" style="${stickyStyle}"><div class="cell-shell" style="padding-inline-start:${escapeHtml(controller.cellIndent(row, column))}">${treeToggle}${expandToggle}<div class="cell-content">${content}</div></div></div>`;
|
|
1958
2020
|
}
|
|
1959
2021
|
renderPagination(snapshot) {
|
|
1960
2022
|
const pageSizes = snapshot.options.paginationPageSizes ?? [10, 25, 50, 100];
|
|
@@ -1968,6 +2030,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
1968
2030
|
}
|
|
1969
2031
|
renderCellTemplate(row, column, displayIndex) {
|
|
1970
2032
|
const templateMarkup = this.getTemplateMarkup(this.cellSlotName(column));
|
|
2033
|
+
return this.renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup);
|
|
2034
|
+
}
|
|
2035
|
+
renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup) {
|
|
1971
2036
|
if (!templateMarkup) {
|
|
1972
2037
|
return `<span class="cell-value">${escapeHtml(this.controller?.displayValue(row, column) ?? "")}</span>`;
|
|
1973
2038
|
}
|
|
@@ -2020,6 +2085,79 @@ var UiGridStandaloneElement = class extends HTMLElement {
|
|
|
2020
2085
|
rowIndexFor(row, fallback = 0) {
|
|
2021
2086
|
return this.snapshot?.pipeline.visibleRows.findIndex((candidate) => candidate.id === row.id) ?? fallback;
|
|
2022
2087
|
}
|
|
2088
|
+
classifyDataRefresh(previousSnapshot, nextSnapshot) {
|
|
2089
|
+
if (!nextSnapshot) {
|
|
2090
|
+
return { mode: "full", changedRowIds: null };
|
|
2091
|
+
}
|
|
2092
|
+
if (!previousSnapshot) {
|
|
2093
|
+
return {
|
|
2094
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
|
|
2095
|
+
changedRowIds: null
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
if (previousSnapshot.visibleColumns.length !== nextSnapshot.visibleColumns.length) {
|
|
2099
|
+
return { mode: "full", changedRowIds: null };
|
|
2100
|
+
}
|
|
2101
|
+
for (let index = 0; index < previousSnapshot.visibleColumns.length; index += 1) {
|
|
2102
|
+
if (previousSnapshot.visibleColumns[index]?.name !== nextSnapshot.visibleColumns[index]?.name) {
|
|
2103
|
+
return { mode: "full", changedRowIds: null };
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
const previousWindow = this.getRenderedDisplayWindow(previousSnapshot);
|
|
2107
|
+
const nextWindow = this.getRenderedDisplayWindow(nextSnapshot);
|
|
2108
|
+
if (previousWindow.startIndex !== nextWindow.startIndex || previousWindow.items.length !== nextWindow.items.length) {
|
|
2109
|
+
return {
|
|
2110
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
|
|
2111
|
+
changedRowIds: null
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
const changedRowIds = /* @__PURE__ */ new Set();
|
|
2115
|
+
for (let index = 0; index < previousWindow.items.length; index += 1) {
|
|
2116
|
+
const previousItem = previousWindow.items[index];
|
|
2117
|
+
const nextItem = nextWindow.items[index];
|
|
2118
|
+
if (!previousItem || !nextItem || previousItem.kind !== "row" || nextItem.kind !== "row") {
|
|
2119
|
+
return {
|
|
2120
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
|
|
2121
|
+
changedRowIds: null
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
if (previousItem.row.id !== nextItem.row.id) {
|
|
2125
|
+
return {
|
|
2126
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
|
|
2127
|
+
changedRowIds: null
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
if (previousItem.row.entity !== nextItem.row.entity) {
|
|
2131
|
+
changedRowIds.add(nextItem.row.id);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
return {
|
|
2135
|
+
mode: changedRowIds.size > 0 ? "patch" : null,
|
|
2136
|
+
changedRowIds: changedRowIds.size > 0 ? changedRowIds : null
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
getRenderedDisplayWindow(snapshot) {
|
|
2140
|
+
if (!snapshot.pipeline.virtualizationEnabled) {
|
|
2141
|
+
return {
|
|
2142
|
+
startIndex: 0,
|
|
2143
|
+
items: snapshot.pipeline.displayItems
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
const stickyChromeHeight = this.measuredHeaderStickyHeight + this.measuredFilterStickyHeight;
|
|
2147
|
+
const viewportHeight = snapshot.options.viewportHeight ?? 560;
|
|
2148
|
+
const bodyViewportHeight = Math.max(snapshot.rowSize, viewportHeight - stickyChromeHeight);
|
|
2149
|
+
const bodyScrollTop = Math.max(0, this.scrollPosition - stickyChromeHeight);
|
|
2150
|
+
const overscan = 4;
|
|
2151
|
+
const startIndex = Math.max(0, Math.floor(bodyScrollTop / snapshot.rowSize) - overscan);
|
|
2152
|
+
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
2153
|
+
return {
|
|
2154
|
+
startIndex,
|
|
2155
|
+
items: snapshot.pipeline.displayItems.slice(
|
|
2156
|
+
startIndex,
|
|
2157
|
+
Math.min(snapshot.pipeline.displayItems.length, startIndex + visibleCount)
|
|
2158
|
+
)
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2023
2161
|
cellSlotName(column) {
|
|
2024
2162
|
return `cell-${column.name}`;
|
|
2025
2163
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ornery/ui-grid-vanilla",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Vanilla DOM wrapper for the Rust/WASM-backed @ornery/ui-grid-core custom element",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@ornery/ui-grid-core": "
|
|
19
|
+
"@ornery/ui-grid-core": "0.1.9"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"typescript": "~5.8.0",
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
clearRustWasmGridEngine,
|
|
7
7
|
} from '@ornery/ui-grid-core';
|
|
8
8
|
import {
|
|
9
|
+
defineStandaloneUiGridElement,
|
|
9
10
|
mountVanillaUiGrid,
|
|
10
11
|
registerVanillaUiGridRustModule,
|
|
11
12
|
type GridOptions,
|
|
@@ -274,4 +275,34 @@ describe('mountVanillaUiGrid integration', () => {
|
|
|
274
275
|
|
|
275
276
|
expect(detailCard.textContent).toContain('Owner Mina Patel');
|
|
276
277
|
});
|
|
278
|
+
|
|
279
|
+
it('preserves declarative attribute data when augmenting options imperatively', async () => {
|
|
280
|
+
// Use the already-registered 'ui-grid-element-vanilla-test' tag (from mountVanillaUiGrid
|
|
281
|
+
// in prior tests) to avoid the jsdom constraint of not registering the same class twice.
|
|
282
|
+
const grid = document.createElement('ui-grid-element-vanilla-test') as HTMLElement & {
|
|
283
|
+
options: GridOptions;
|
|
284
|
+
};
|
|
285
|
+
document.getElementById('app')!.appendChild(grid);
|
|
286
|
+
|
|
287
|
+
grid.setAttribute('grid-id', 'declarative-test');
|
|
288
|
+
grid.setAttribute('column-defs', JSON.stringify([{ name: 'name', displayName: 'Customer' }]));
|
|
289
|
+
grid.setAttribute('data', JSON.stringify([{ id: 'row-1', name: 'Gamma' }]));
|
|
290
|
+
|
|
291
|
+
// Flush the attributeChangedCallback microtask so attributeOptions are populated.
|
|
292
|
+
await new Promise<void>((r) => setTimeout(r, 0));
|
|
293
|
+
|
|
294
|
+
// Before any imperative assignment, the getter must surface attribute-derived data.
|
|
295
|
+
expect((grid.options.data as Array<{ id: string; name: string }>)[0].name).toBe('Gamma');
|
|
296
|
+
expect(grid.options.columnDefs[0].name).toBe('name');
|
|
297
|
+
|
|
298
|
+
// Simulate the bridge pattern: augment without losing attribute-derived data.
|
|
299
|
+
grid.options = {
|
|
300
|
+
...grid.options,
|
|
301
|
+
viewportHeight: 999,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
expect((grid.options.data as Array<{ id: string; name: string }>)[0].name).toBe('Gamma');
|
|
305
|
+
expect(grid.options.viewportHeight).toBe(999);
|
|
306
|
+
});
|
|
307
|
+
|
|
277
308
|
});
|
package/src/index.test.ts
CHANGED
|
@@ -40,7 +40,7 @@ describe('mountVanillaUiGrid', () => {
|
|
|
40
40
|
expect(registerRustWasmGridEngine).toHaveBeenCalledTimes(1);
|
|
41
41
|
expect(customElements.get('ui-grid-element')).toBeDefined();
|
|
42
42
|
expect(grid.tagName.toLowerCase()).toBe('ui-grid-element');
|
|
43
|
-
expect(grid.options).
|
|
43
|
+
expect(grid.options).toStrictEqual(options);
|
|
44
44
|
expect(target.firstElementChild).toBe(grid);
|
|
45
45
|
});
|
|
46
46
|
|
|
@@ -112,6 +112,7 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
112
112
|
private draggedColumnName: string | null = null;
|
|
113
113
|
private dropTargetColumnName: string | null = null;
|
|
114
114
|
private scrollPosition = 0;
|
|
115
|
+
private horizontalScrollPosition = 0;
|
|
115
116
|
private scrollFrame: number | null = null;
|
|
116
117
|
private suppressScrollEvent = false;
|
|
117
118
|
private lastVirtualStartIndex = -1;
|
|
@@ -121,6 +122,9 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
121
122
|
private benchmarkAverage = '—';
|
|
122
123
|
private skipNextRender = false;
|
|
123
124
|
private dataFrame: number | null = null;
|
|
125
|
+
private pendingPatchedRowIds: Set<string> | null = null;
|
|
126
|
+
private pendingDataRefreshMode: 'patch' | 'virtual' | 'full' | null = null;
|
|
127
|
+
private lastScrollActivityAt = 0;
|
|
124
128
|
|
|
125
129
|
static get observedAttributes(): string[] {
|
|
126
130
|
return [
|
|
@@ -168,13 +172,16 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
get options(): GridOptions {
|
|
171
|
-
|
|
172
|
-
this.activeOptions
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
if (this.activeOptions !== null) {
|
|
176
|
+
return this.buildEffectiveOptions(this.activeOptions);
|
|
177
|
+
}
|
|
178
|
+
// No imperative options set yet: safe defaults with attributes taking priority.
|
|
179
|
+
return {
|
|
180
|
+
id: '__ui-grid-pending__',
|
|
181
|
+
data: [],
|
|
182
|
+
columnDefs: [],
|
|
183
|
+
...this.attributeOptions,
|
|
184
|
+
} as GridOptions;
|
|
178
185
|
}
|
|
179
186
|
|
|
180
187
|
set options(value: GridOptions) {
|
|
@@ -347,8 +354,16 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
347
354
|
this.attributeOptions.paginationPageSizes = paginationPageSizes;
|
|
348
355
|
|
|
349
356
|
// Re-render with the merged options.
|
|
350
|
-
if (this.activeOptions) {
|
|
357
|
+
if (this.activeOptions !== null) {
|
|
351
358
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
359
|
+
} else {
|
|
360
|
+
// Purely declarative path: attributes drive the grid without an imperative bridge.
|
|
361
|
+
this.ensureController({
|
|
362
|
+
id: '__ui-grid-pending__',
|
|
363
|
+
data: [],
|
|
364
|
+
columnDefs: [],
|
|
365
|
+
...this.attributeOptions,
|
|
366
|
+
} as VanillaGridOptions);
|
|
352
367
|
}
|
|
353
368
|
}
|
|
354
369
|
|
|
@@ -707,22 +722,63 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
707
722
|
return;
|
|
708
723
|
}
|
|
709
724
|
|
|
725
|
+
const previousSnapshot = this.snapshot;
|
|
726
|
+
|
|
710
727
|
// Update the pipeline synchronously (cheap, no DOM work).
|
|
711
728
|
this.skipNextRender = true;
|
|
712
729
|
controller.refreshData(rows);
|
|
713
730
|
this.skipNextRender = false;
|
|
714
731
|
|
|
732
|
+
const { mode, changedRowIds } = this.classifyDataRefresh(previousSnapshot, this.snapshot);
|
|
733
|
+
if (mode === null) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
this.pendingDataRefreshMode = mode;
|
|
738
|
+
this.pendingPatchedRowIds = changedRowIds;
|
|
739
|
+
|
|
715
740
|
// Schedule DOM patching on the next paint — latest call wins.
|
|
716
741
|
if (this.dataFrame !== null) {
|
|
717
742
|
cancelAnimationFrame(this.dataFrame);
|
|
718
743
|
}
|
|
719
744
|
this.dataFrame = requestAnimationFrame(() => {
|
|
720
|
-
this.
|
|
721
|
-
this.patchCells();
|
|
745
|
+
this.flushPendingDataRefresh();
|
|
722
746
|
});
|
|
723
747
|
}
|
|
724
748
|
|
|
725
|
-
private
|
|
749
|
+
private flushPendingDataRefresh(): void {
|
|
750
|
+
this.dataFrame = null;
|
|
751
|
+
|
|
752
|
+
const mode = this.pendingDataRefreshMode;
|
|
753
|
+
if (mode === null) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (Date.now() - this.lastScrollActivityAt < 80) {
|
|
758
|
+
this.dataFrame = requestAnimationFrame(() => {
|
|
759
|
+
this.flushPendingDataRefresh();
|
|
760
|
+
});
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const changedRowIds = this.pendingPatchedRowIds;
|
|
765
|
+
this.pendingDataRefreshMode = null;
|
|
766
|
+
this.pendingPatchedRowIds = null;
|
|
767
|
+
|
|
768
|
+
if (mode === 'patch') {
|
|
769
|
+
this.patchCells(changedRowIds ?? undefined);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (mode === 'virtual') {
|
|
774
|
+
this.renderVirtualBody();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
this.render();
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private patchCells(changedRowIds?: ReadonlySet<string>): void {
|
|
726
782
|
const snapshot = this.snapshot;
|
|
727
783
|
if (!snapshot) {
|
|
728
784
|
return;
|
|
@@ -735,12 +791,16 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
735
791
|
|
|
736
792
|
// Build O(1) lookup maps from the current pipeline snapshot.
|
|
737
793
|
const rowMap = new Map<string, GridRow>();
|
|
794
|
+
const rowIndexMap = new Map<string, number>();
|
|
738
795
|
for (const row of snapshot.pipeline.visibleRows) {
|
|
739
796
|
rowMap.set(row.id, row);
|
|
797
|
+
rowIndexMap.set(row.id, rowIndexMap.size);
|
|
740
798
|
}
|
|
741
799
|
const colMap = new Map<string, GridColumnDef>();
|
|
800
|
+
const templateMarkupMap = new Map<string, string | null>();
|
|
742
801
|
for (const col of snapshot.visibleColumns) {
|
|
743
802
|
colMap.set(col.name, col);
|
|
803
|
+
templateMarkupMap.set(col.name, this.getTemplateMarkup(this.cellSlotName(col)));
|
|
744
804
|
}
|
|
745
805
|
|
|
746
806
|
// Patch every rendered body cell in-place.
|
|
@@ -749,23 +809,24 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
749
809
|
const rowId = cell.dataset['row'];
|
|
750
810
|
const colName = cell.dataset['column'];
|
|
751
811
|
if (!rowId || !colName) continue;
|
|
812
|
+
if (changedRowIds && !changedRowIds.has(rowId)) continue;
|
|
752
813
|
|
|
753
814
|
const row = rowMap.get(rowId);
|
|
754
815
|
const column = colMap.get(colName);
|
|
755
816
|
if (!row || !column) continue;
|
|
817
|
+
if (cell.classList.contains('cell-editing')) continue;
|
|
756
818
|
|
|
757
|
-
const
|
|
758
|
-
if (!
|
|
819
|
+
const cellContent = cell.querySelector<HTMLElement>('.cell-content');
|
|
820
|
+
if (!cellContent) continue;
|
|
759
821
|
|
|
760
|
-
const newContent = this.
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
cellShell.innerHTML = toggleHtml + newContent;
|
|
822
|
+
const newContent = this.renderCellTemplateFromMarkup(
|
|
823
|
+
row,
|
|
824
|
+
column,
|
|
825
|
+
rowIndexMap.get(rowId) ?? 0,
|
|
826
|
+
templateMarkupMap.get(colName) ?? null,
|
|
827
|
+
);
|
|
828
|
+
if (cellContent.innerHTML !== newContent) {
|
|
829
|
+
cellContent.innerHTML = newContent;
|
|
769
830
|
}
|
|
770
831
|
}
|
|
771
832
|
}
|
|
@@ -1203,7 +1264,9 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1203
1264
|
return;
|
|
1204
1265
|
}
|
|
1205
1266
|
|
|
1267
|
+
this.lastScrollActivityAt = Date.now();
|
|
1206
1268
|
this.scrollPosition = target.scrollTop;
|
|
1269
|
+
this.horizontalScrollPosition = target.scrollLeft;
|
|
1207
1270
|
if (!this.snapshot?.pipeline.virtualizationEnabled) {
|
|
1208
1271
|
return;
|
|
1209
1272
|
}
|
|
@@ -1301,6 +1364,7 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1301
1364
|
const previousGridTable = root.querySelector<HTMLElement>('.grid-table');
|
|
1302
1365
|
if (previousGridTable && !this.suppressScrollEvent) {
|
|
1303
1366
|
this.scrollPosition = previousGridTable.scrollTop;
|
|
1367
|
+
this.horizontalScrollPosition = previousGridTable.scrollLeft;
|
|
1304
1368
|
}
|
|
1305
1369
|
|
|
1306
1370
|
if (!snapshot) {
|
|
@@ -1368,14 +1432,20 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1368
1432
|
: `<div class="empty-state ui-grid-no-row-overlay"><strong>${escapeHtml(options.emptyMessage ?? labels.emptyHeading)}</strong><p>${escapeHtml(labels.emptyDescription)}</p></div>`;
|
|
1369
1433
|
|
|
1370
1434
|
const pagination = paginationEnabled && showPagination ? this.renderPagination(snapshot) : '';
|
|
1371
|
-
const
|
|
1435
|
+
const hasViewportScroll = virtualizationEnabled || options.viewportHeight !== undefined;
|
|
1436
|
+
const gridTableStyle = `${hasViewportScroll ? `height:${viewportHeight}px;overflow-y:auto;` : ''}--ui-grid-header-sticky-top:${headerStickyTop}px;`;
|
|
1372
1437
|
|
|
1373
1438
|
root.innerHTML = `<style>${GRID_CORE_CSS}</style>${slotRegistry}<section class="grid-frame ui-grid" role="grid" aria-label="${escapeHtml(options.title ?? 'Data grid')}"><div class="grid-table ui-grid-contents-wrapper" style="${gridTableStyle}"><div class="header-grid ui-grid-header ui-grid-header-canvas" style="grid-template-columns:${templateColumns}">${header}</div>${filterRow}${body}</div>${pagination}</section>`;
|
|
1374
1439
|
|
|
1375
1440
|
const gridTable = root.querySelector<HTMLElement>('.grid-table');
|
|
1376
|
-
if (gridTable && this.scrollPosition > 0
|
|
1441
|
+
if (gridTable && (this.scrollPosition > 0 || this.horizontalScrollPosition > 0)) {
|
|
1377
1442
|
this.suppressScrollEvent = true;
|
|
1378
|
-
|
|
1443
|
+
if (this.scrollPosition > 0) {
|
|
1444
|
+
gridTable.scrollTop = this.scrollPosition;
|
|
1445
|
+
}
|
|
1446
|
+
if (this.horizontalScrollPosition > 0) {
|
|
1447
|
+
gridTable.scrollLeft = this.horizontalScrollPosition;
|
|
1448
|
+
}
|
|
1379
1449
|
requestAnimationFrame(() => {
|
|
1380
1450
|
this.suppressScrollEvent = false;
|
|
1381
1451
|
});
|
|
@@ -1537,7 +1607,7 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1537
1607
|
.join(' ');
|
|
1538
1608
|
|
|
1539
1609
|
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : '';
|
|
1540
|
-
return `<div class="${classes}" tabindex="0" data-row="${escapeHtml(rowId)}" data-column="${escapeHtml(columnName)}" style="${stickyStyle}"><div class="cell-shell" style="padding-inline-start:${escapeHtml(controller.cellIndent(row, column))}">${treeToggle}${expandToggle}
|
|
1610
|
+
return `<div class="${classes}" tabindex="0" data-row="${escapeHtml(rowId)}" data-column="${escapeHtml(columnName)}" style="${stickyStyle}"><div class="cell-shell" style="padding-inline-start:${escapeHtml(controller.cellIndent(row, column))}">${treeToggle}${expandToggle}<div class="cell-content">${content}</div></div></div>`;
|
|
1541
1611
|
}
|
|
1542
1612
|
|
|
1543
1613
|
private renderPagination(snapshot: GridControllerSnapshot): string {
|
|
@@ -1559,6 +1629,15 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1559
1629
|
|
|
1560
1630
|
private renderCellTemplate(row: GridRow, column: GridColumnDef, displayIndex: number): string {
|
|
1561
1631
|
const templateMarkup = this.getTemplateMarkup(this.cellSlotName(column));
|
|
1632
|
+
return this.renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
private renderCellTemplateFromMarkup(
|
|
1636
|
+
row: GridRow,
|
|
1637
|
+
column: GridColumnDef,
|
|
1638
|
+
displayIndex: number,
|
|
1639
|
+
templateMarkup: string | null,
|
|
1640
|
+
): string {
|
|
1562
1641
|
if (!templateMarkup) {
|
|
1563
1642
|
return `<span class="cell-value">${escapeHtml(this.controller?.displayValue(row, column) ?? '')}</span>`;
|
|
1564
1643
|
}
|
|
@@ -1624,6 +1703,101 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1624
1703
|
);
|
|
1625
1704
|
}
|
|
1626
1705
|
|
|
1706
|
+
private classifyDataRefresh(
|
|
1707
|
+
previousSnapshot: GridControllerSnapshot | null,
|
|
1708
|
+
nextSnapshot: GridControllerSnapshot | null,
|
|
1709
|
+
): { mode: 'patch' | 'virtual' | 'full' | null; changedRowIds: Set<string> | null } {
|
|
1710
|
+
if (!nextSnapshot) {
|
|
1711
|
+
return { mode: 'full', changedRowIds: null };
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
if (!previousSnapshot) {
|
|
1715
|
+
return {
|
|
1716
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? 'virtual' : 'full',
|
|
1717
|
+
changedRowIds: null,
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
if (previousSnapshot.visibleColumns.length !== nextSnapshot.visibleColumns.length) {
|
|
1722
|
+
return { mode: 'full', changedRowIds: null };
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
for (let index = 0; index < previousSnapshot.visibleColumns.length; index += 1) {
|
|
1726
|
+
if (previousSnapshot.visibleColumns[index]?.name !== nextSnapshot.visibleColumns[index]?.name) {
|
|
1727
|
+
return { mode: 'full', changedRowIds: null };
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const previousWindow = this.getRenderedDisplayWindow(previousSnapshot);
|
|
1732
|
+
const nextWindow = this.getRenderedDisplayWindow(nextSnapshot);
|
|
1733
|
+
if (
|
|
1734
|
+
previousWindow.startIndex !== nextWindow.startIndex ||
|
|
1735
|
+
previousWindow.items.length !== nextWindow.items.length
|
|
1736
|
+
) {
|
|
1737
|
+
return {
|
|
1738
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? 'virtual' : 'full',
|
|
1739
|
+
changedRowIds: null,
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const changedRowIds = new Set<string>();
|
|
1744
|
+
for (let index = 0; index < previousWindow.items.length; index += 1) {
|
|
1745
|
+
const previousItem = previousWindow.items[index];
|
|
1746
|
+
const nextItem = nextWindow.items[index];
|
|
1747
|
+
|
|
1748
|
+
if (!previousItem || !nextItem || previousItem.kind !== 'row' || nextItem.kind !== 'row') {
|
|
1749
|
+
return {
|
|
1750
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? 'virtual' : 'full',
|
|
1751
|
+
changedRowIds: null,
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (previousItem.row.id !== nextItem.row.id) {
|
|
1756
|
+
return {
|
|
1757
|
+
mode: nextSnapshot.pipeline.virtualizationEnabled ? 'virtual' : 'full',
|
|
1758
|
+
changedRowIds: null,
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
if (previousItem.row.entity !== nextItem.row.entity) {
|
|
1763
|
+
changedRowIds.add(nextItem.row.id);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
return {
|
|
1768
|
+
mode: changedRowIds.size > 0 ? 'patch' : null,
|
|
1769
|
+
changedRowIds: changedRowIds.size > 0 ? changedRowIds : null,
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
private getRenderedDisplayWindow(snapshot: GridControllerSnapshot): {
|
|
1774
|
+
startIndex: number;
|
|
1775
|
+
items: readonly DisplayItem[];
|
|
1776
|
+
} {
|
|
1777
|
+
if (!snapshot.pipeline.virtualizationEnabled) {
|
|
1778
|
+
return {
|
|
1779
|
+
startIndex: 0,
|
|
1780
|
+
items: snapshot.pipeline.displayItems,
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
const stickyChromeHeight = this.measuredHeaderStickyHeight + this.measuredFilterStickyHeight;
|
|
1785
|
+
const viewportHeight = snapshot.options.viewportHeight ?? 560;
|
|
1786
|
+
const bodyViewportHeight = Math.max(snapshot.rowSize, viewportHeight - stickyChromeHeight);
|
|
1787
|
+
const bodyScrollTop = Math.max(0, this.scrollPosition - stickyChromeHeight);
|
|
1788
|
+
const overscan = 4;
|
|
1789
|
+
const startIndex = Math.max(0, Math.floor(bodyScrollTop / snapshot.rowSize) - overscan);
|
|
1790
|
+
const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
|
|
1791
|
+
|
|
1792
|
+
return {
|
|
1793
|
+
startIndex,
|
|
1794
|
+
items: snapshot.pipeline.displayItems.slice(
|
|
1795
|
+
startIndex,
|
|
1796
|
+
Math.min(snapshot.pipeline.displayItems.length, startIndex + visibleCount),
|
|
1797
|
+
),
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1627
1801
|
private cellSlotName(column: GridColumnDef): string {
|
|
1628
1802
|
return `cell-${column.name}`;
|
|
1629
1803
|
}
|