@ornery/ui-grid-vanilla 0.1.8 → 0.1.10

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/demo/index.html CHANGED
@@ -179,7 +179,7 @@
179
179
  <p>
180
180
  Familiar <code>gridOptions</code> and <code>onRegisterApi</code>, rebuilt as a
181
181
  framework-neutral custom element with the Rust-backed pipeline, grouping, sorting,
182
- filtering, and virtualization.
182
+ filtering, Excel-style column resizing, and virtualization.
183
183
  </p>
184
184
  </div>
185
185
  <div class="grid-demo-actions">
@@ -210,8 +210,8 @@
210
210
  <div class="grid-demo-toolbar">
211
211
  <div><strong id="visible-rows-inline">0</strong> of <span id="total-rows-value">0</span> rows</div>
212
212
  <p>
213
- <code>gridOptions</code> compatibility layer: sorting, filtering, grouping, templating,
214
- and virtualized rendering.
213
+ <code>gridOptions</code> compatibility layer: sorting, filtering, grouping, column resizing,
214
+ templating, and virtualized rendering.
215
215
  </p>
216
216
  </div>
217
217
 
package/demo/main.ts CHANGED
@@ -28,6 +28,7 @@ const options: GridOptions = {
28
28
  enableSorting: true,
29
29
  enableFiltering: true,
30
30
  enableGrouping: true,
31
+ enableColumnResizing: true,
31
32
  enableVirtualization: true,
32
33
  virtualizationThreshold: 25,
33
34
  grouping: { groupBy: ['status'] },
package/dist/index.d.mts CHANGED
@@ -41,6 +41,7 @@ declare class VanillaGridController {
41
41
  private expandedTreeRows;
42
42
  private pinnedColumns;
43
43
  private columnOrder;
44
+ private columnWidthOverrides;
44
45
  private currentPage;
45
46
  private pageSize;
46
47
  private editingCell;
@@ -84,6 +85,11 @@ declare class VanillaGridController {
84
85
  updateEditingValue(value: string): void;
85
86
  commitCellEdit(): void;
86
87
  cancelCellEdit(): void;
88
+ canResizeColumns(): boolean;
89
+ setColumnWidthOverride(columnName: string, widthPx: number): void;
90
+ /** Returns the grid-template-columns string as if `columnName` had the given width,
91
+ * without triggering a full refresh. Used for smooth drag-resize DOM updates. */
92
+ buildTemplateColumnsWithOverride(columnName: string, widthPx: number): string;
87
93
  isGroupingEnabled(): boolean;
88
94
  isSortingEnabled(): boolean;
89
95
  isFilteringEnabled(): boolean;
@@ -169,6 +175,7 @@ declare class UiGridStandaloneElement extends HTMLElement {
169
175
  private draggedColumnName;
170
176
  private dropTargetColumnName;
171
177
  private scrollPosition;
178
+ private horizontalScrollPosition;
172
179
  private scrollFrame;
173
180
  private suppressScrollEvent;
174
181
  private lastVirtualStartIndex;
@@ -178,6 +185,9 @@ declare class UiGridStandaloneElement extends HTMLElement {
178
185
  private benchmarkAverage;
179
186
  private skipNextRender;
180
187
  private dataFrame;
188
+ private pendingPatchedRowIds;
189
+ private pendingDataRefreshMode;
190
+ private lastScrollActivityAt;
181
191
  static get observedAttributes(): string[];
182
192
  get options(): GridOptions;
183
193
  set options(value: GridOptions);
@@ -232,6 +242,8 @@ declare class UiGridStandaloneElement extends HTMLElement {
232
242
  set enablePinning(value: boolean);
233
243
  get enableColumnMoving(): boolean;
234
244
  set enableColumnMoving(value: boolean);
245
+ get enableColumnResizing(): boolean;
246
+ set enableColumnResizing(value: boolean);
235
247
  get enableCellEdit(): boolean;
236
248
  set enableCellEdit(value: boolean);
237
249
  get enableCellEditOnFocus(): boolean;
@@ -273,6 +285,7 @@ declare class UiGridStandaloneElement extends HTMLElement {
273
285
  * (the fresh innerHTML already reflects the latest data).
274
286
  */
275
287
  setData(rows: GridRecord[]): void;
288
+ private flushPendingDataRefresh;
276
289
  private patchCells;
277
290
  connectedCallback(): void;
278
291
  disconnectedCallback(): void;
@@ -291,16 +304,20 @@ declare class UiGridStandaloneElement extends HTMLElement {
291
304
  private render;
292
305
  private renderHeaderCell;
293
306
  private renderFilterCell;
307
+ private measureAutoColumnWidth;
294
308
  private renderDisplayItem;
295
309
  private renderBodyCell;
296
310
  private renderPagination;
297
311
  private renderSlotRegistry;
298
312
  private renderCellTemplate;
313
+ private renderCellTemplateFromMarkup;
299
314
  private renderExpandableTemplate;
300
315
  private getTemplateMarkup;
301
316
  private interpolateTemplate;
302
317
  private resolveTemplateValue;
303
318
  private rowIndexFor;
319
+ private classifyDataRefresh;
320
+ private getRenderedDisplayWindow;
304
321
  private cellSlotName;
305
322
  private downloadCsv;
306
323
  private renderControlIcon;
package/dist/index.d.ts CHANGED
@@ -41,6 +41,7 @@ declare class VanillaGridController {
41
41
  private expandedTreeRows;
42
42
  private pinnedColumns;
43
43
  private columnOrder;
44
+ private columnWidthOverrides;
44
45
  private currentPage;
45
46
  private pageSize;
46
47
  private editingCell;
@@ -84,6 +85,11 @@ declare class VanillaGridController {
84
85
  updateEditingValue(value: string): void;
85
86
  commitCellEdit(): void;
86
87
  cancelCellEdit(): void;
88
+ canResizeColumns(): boolean;
89
+ setColumnWidthOverride(columnName: string, widthPx: number): void;
90
+ /** Returns the grid-template-columns string as if `columnName` had the given width,
91
+ * without triggering a full refresh. Used for smooth drag-resize DOM updates. */
92
+ buildTemplateColumnsWithOverride(columnName: string, widthPx: number): string;
87
93
  isGroupingEnabled(): boolean;
88
94
  isSortingEnabled(): boolean;
89
95
  isFilteringEnabled(): boolean;
@@ -169,6 +175,7 @@ declare class UiGridStandaloneElement extends HTMLElement {
169
175
  private draggedColumnName;
170
176
  private dropTargetColumnName;
171
177
  private scrollPosition;
178
+ private horizontalScrollPosition;
172
179
  private scrollFrame;
173
180
  private suppressScrollEvent;
174
181
  private lastVirtualStartIndex;
@@ -178,6 +185,9 @@ declare class UiGridStandaloneElement extends HTMLElement {
178
185
  private benchmarkAverage;
179
186
  private skipNextRender;
180
187
  private dataFrame;
188
+ private pendingPatchedRowIds;
189
+ private pendingDataRefreshMode;
190
+ private lastScrollActivityAt;
181
191
  static get observedAttributes(): string[];
182
192
  get options(): GridOptions;
183
193
  set options(value: GridOptions);
@@ -232,6 +242,8 @@ declare class UiGridStandaloneElement extends HTMLElement {
232
242
  set enablePinning(value: boolean);
233
243
  get enableColumnMoving(): boolean;
234
244
  set enableColumnMoving(value: boolean);
245
+ get enableColumnResizing(): boolean;
246
+ set enableColumnResizing(value: boolean);
235
247
  get enableCellEdit(): boolean;
236
248
  set enableCellEdit(value: boolean);
237
249
  get enableCellEditOnFocus(): boolean;
@@ -273,6 +285,7 @@ declare class UiGridStandaloneElement extends HTMLElement {
273
285
  * (the fresh innerHTML already reflects the latest data).
274
286
  */
275
287
  setData(rows: GridRecord[]): void;
288
+ private flushPendingDataRefresh;
276
289
  private patchCells;
277
290
  connectedCallback(): void;
278
291
  disconnectedCallback(): void;
@@ -291,16 +304,20 @@ declare class UiGridStandaloneElement extends HTMLElement {
291
304
  private render;
292
305
  private renderHeaderCell;
293
306
  private renderFilterCell;
307
+ private measureAutoColumnWidth;
294
308
  private renderDisplayItem;
295
309
  private renderBodyCell;
296
310
  private renderPagination;
297
311
  private renderSlotRegistry;
298
312
  private renderCellTemplate;
313
+ private renderCellTemplateFromMarkup;
299
314
  private renderExpandableTemplate;
300
315
  private getTemplateMarkup;
301
316
  private interpolateTemplate;
302
317
  private resolveTemplateValue;
303
318
  private rowIndexFor;
319
+ private classifyDataRefresh;
320
+ private getRenderedDisplayWindow;
304
321
  private cellSlotName;
305
322
  private downloadCsv;
306
323
  private renderControlIcon;
package/dist/index.js CHANGED
@@ -55,6 +55,7 @@ var VanillaGridController = class {
55
55
  expandedTreeRows = {};
56
56
  pinnedColumns = {};
57
57
  columnOrder = [];
58
+ columnWidthOverrides = {};
58
59
  currentPage = 1;
59
60
  pageSize = 0;
60
61
  editingCell = null;
@@ -463,6 +464,24 @@ var VanillaGridController = class {
463
464
  });
464
465
  this.refresh();
465
466
  }
467
+ canResizeColumns() {
468
+ return this.options.enableColumnResizing !== false;
469
+ }
470
+ setColumnWidthOverride(columnName, widthPx) {
471
+ const nextWidth = `${Math.max(88, Math.round(widthPx))}px`;
472
+ this.columnWidthOverrides = { ...this.columnWidthOverrides, [columnName]: nextWidth };
473
+ this.refresh();
474
+ }
475
+ /** Returns the grid-template-columns string as if `columnName` had the given width,
476
+ * without triggering a full refresh. Used for smooth drag-resize DOM updates. */
477
+ buildTemplateColumnsWithOverride(columnName, widthPx) {
478
+ const widthStr = `${Math.max(88, Math.round(widthPx))}px`;
479
+ return buildGridTemplateColumns(
480
+ this.visibleColumns.map(
481
+ (c) => c.name === columnName ? { ...c, width: widthStr } : c
482
+ )
483
+ );
484
+ }
466
485
  isGroupingEnabled() {
467
486
  return (0, import_ui_grid_core.isGridGroupingEnabled)(this.options);
468
487
  }
@@ -581,9 +600,13 @@ var VanillaGridController = class {
581
600
  }
582
601
  refresh() {
583
602
  const orderedColumns = orderVisibleColumns(this.options.columnDefs, this.columnOrder);
603
+ const applyWidthOverrides = (columns) => columns.map((col) => {
604
+ const override = this.columnWidthOverrides[col.name];
605
+ return override == null ? col : { ...col, width: override };
606
+ });
584
607
  const pinnedEntries = Object.entries(this.pinnedColumns);
585
608
  if (pinnedEntries.length === 0) {
586
- this.visibleColumns = orderedColumns;
609
+ this.visibleColumns = applyWidthOverrides(orderedColumns);
587
610
  } else {
588
611
  const byName = new Map(orderedColumns.map((column) => [column.name, column]));
589
612
  const pinnedLeft = pinnedEntries.filter((entry) => entry[1] === "left").map((entry) => byName.get(entry[0])).filter((column) => column !== void 0);
@@ -591,7 +614,7 @@ var VanillaGridController = class {
591
614
  const middleColumns = orderedColumns.filter(
592
615
  (column) => this.pinnedColumns[column.name] === void 0
593
616
  );
594
- this.visibleColumns = [...pinnedLeft, ...middleColumns, ...pinnedRight];
617
+ this.visibleColumns = applyWidthOverrides([...pinnedLeft, ...middleColumns, ...pinnedRight]);
595
618
  }
596
619
  this.pipeline = import_ui_grid_core.defaultGridEngine.buildPipeline({
597
620
  options: this.options,
@@ -779,6 +802,7 @@ var UiGridStandaloneElement = class extends HTMLElement {
779
802
  draggedColumnName = null;
780
803
  dropTargetColumnName = null;
781
804
  scrollPosition = 0;
805
+ horizontalScrollPosition = 0;
782
806
  scrollFrame = null;
783
807
  suppressScrollEvent = false;
784
808
  lastVirtualStartIndex = -1;
@@ -788,6 +812,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
788
812
  benchmarkAverage = "\u2014";
789
813
  skipNextRender = false;
790
814
  dataFrame = null;
815
+ pendingPatchedRowIds = null;
816
+ pendingDataRefreshMode = null;
817
+ lastScrollActivityAt = 0;
791
818
  static get observedAttributes() {
792
819
  return [
793
820
  // Scalar attributes
@@ -833,10 +860,14 @@ var UiGridStandaloneElement = class extends HTMLElement {
833
860
  ];
834
861
  }
835
862
  get options() {
836
- return this.activeOptions ?? {
863
+ if (this.activeOptions !== null) {
864
+ return this.buildEffectiveOptions(this.activeOptions);
865
+ }
866
+ return {
837
867
  id: "__ui-grid-pending__",
838
868
  data: [],
839
- columnDefs: []
869
+ columnDefs: [],
870
+ ...this.attributeOptions
840
871
  };
841
872
  }
842
873
  set options(value) {
@@ -907,6 +938,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
907
938
  const enableColumnMoving = this.parseBooleanAttribute("enable-column-moving");
908
939
  if (enableColumnMoving !== void 0)
909
940
  this.attributeOptions.enableColumnMoving = enableColumnMoving;
941
+ const enableColumnResizing = this.parseBooleanAttribute("enable-column-resizing");
942
+ if (enableColumnResizing !== void 0)
943
+ this.attributeOptions.enableColumnResizing = enableColumnResizing;
910
944
  const enableCellEdit = this.parseBooleanAttribute("enable-cell-edit");
911
945
  if (enableCellEdit !== void 0) this.attributeOptions.enableCellEdit = enableCellEdit;
912
946
  const enableCellEditOnFocus = this.parseBooleanAttribute("enable-cell-edit-on-focus");
@@ -959,8 +993,15 @@ var UiGridStandaloneElement = class extends HTMLElement {
959
993
  );
960
994
  if (paginationPageSizes !== void 0)
961
995
  this.attributeOptions.paginationPageSizes = paginationPageSizes;
962
- if (this.activeOptions) {
996
+ if (this.activeOptions !== null) {
963
997
  this.ensureController(this.buildEffectiveOptions(this.activeOptions));
998
+ } else {
999
+ this.ensureController({
1000
+ id: "__ui-grid-pending__",
1001
+ data: [],
1002
+ columnDefs: [],
1003
+ ...this.attributeOptions
1004
+ });
964
1005
  }
965
1006
  }
966
1007
  parseBooleanAttribute(name) {
@@ -1148,6 +1189,13 @@ var UiGridStandaloneElement = class extends HTMLElement {
1148
1189
  this.activeOptions = { ...this.activeOptions, enableColumnMoving: value };
1149
1190
  this.ensureController(this.buildEffectiveOptions(this.activeOptions));
1150
1191
  }
1192
+ get enableColumnResizing() {
1193
+ return this.options.enableColumnResizing ?? true;
1194
+ }
1195
+ set enableColumnResizing(value) {
1196
+ this.activeOptions = { ...this.activeOptions, enableColumnResizing: value };
1197
+ this.ensureController(this.buildEffectiveOptions(this.activeOptions));
1198
+ }
1151
1199
  get enableCellEdit() {
1152
1200
  return this.options.enableCellEdit ?? false;
1153
1201
  }
@@ -1272,18 +1320,49 @@ var UiGridStandaloneElement = class extends HTMLElement {
1272
1320
  if (!controller) {
1273
1321
  return;
1274
1322
  }
1323
+ const previousSnapshot = this.snapshot;
1275
1324
  this.skipNextRender = true;
1276
1325
  controller.refreshData(rows);
1277
1326
  this.skipNextRender = false;
1327
+ const { mode, changedRowIds } = this.classifyDataRefresh(previousSnapshot, this.snapshot);
1328
+ if (mode === null) {
1329
+ return;
1330
+ }
1331
+ this.pendingDataRefreshMode = mode;
1332
+ this.pendingPatchedRowIds = changedRowIds;
1278
1333
  if (this.dataFrame !== null) {
1279
1334
  cancelAnimationFrame(this.dataFrame);
1280
1335
  }
1281
1336
  this.dataFrame = requestAnimationFrame(() => {
1282
- this.dataFrame = null;
1283
- this.patchCells();
1337
+ this.flushPendingDataRefresh();
1284
1338
  });
1285
1339
  }
1286
- patchCells() {
1340
+ flushPendingDataRefresh() {
1341
+ this.dataFrame = null;
1342
+ const mode = this.pendingDataRefreshMode;
1343
+ if (mode === null) {
1344
+ return;
1345
+ }
1346
+ if (Date.now() - this.lastScrollActivityAt < 80) {
1347
+ this.dataFrame = requestAnimationFrame(() => {
1348
+ this.flushPendingDataRefresh();
1349
+ });
1350
+ return;
1351
+ }
1352
+ const changedRowIds = this.pendingPatchedRowIds;
1353
+ this.pendingDataRefreshMode = null;
1354
+ this.pendingPatchedRowIds = null;
1355
+ if (mode === "patch") {
1356
+ this.patchCells(changedRowIds ?? void 0);
1357
+ return;
1358
+ }
1359
+ if (mode === "virtual") {
1360
+ this.renderVirtualBody();
1361
+ return;
1362
+ }
1363
+ this.render();
1364
+ }
1365
+ patchCells(changedRowIds) {
1287
1366
  const snapshot = this.snapshot;
1288
1367
  if (!snapshot) {
1289
1368
  return;
@@ -1293,30 +1372,37 @@ var UiGridStandaloneElement = class extends HTMLElement {
1293
1372
  return;
1294
1373
  }
1295
1374
  const rowMap = /* @__PURE__ */ new Map();
1375
+ const rowIndexMap = /* @__PURE__ */ new Map();
1296
1376
  for (const row of snapshot.pipeline.visibleRows) {
1297
1377
  rowMap.set(row.id, row);
1378
+ rowIndexMap.set(row.id, rowIndexMap.size);
1298
1379
  }
1299
1380
  const colMap = /* @__PURE__ */ new Map();
1381
+ const templateMarkupMap = /* @__PURE__ */ new Map();
1300
1382
  for (const col of snapshot.visibleColumns) {
1301
1383
  colMap.set(col.name, col);
1384
+ templateMarkupMap.set(col.name, this.getTemplateMarkup(this.cellSlotName(col)));
1302
1385
  }
1303
1386
  const cells = root.querySelectorAll("[data-row][data-column]");
1304
1387
  for (const cell of cells) {
1305
1388
  const rowId = cell.dataset["row"];
1306
1389
  const colName = cell.dataset["column"];
1307
1390
  if (!rowId || !colName) continue;
1391
+ if (changedRowIds && !changedRowIds.has(rowId)) continue;
1308
1392
  const row = rowMap.get(rowId);
1309
1393
  const column = colMap.get(colName);
1310
1394
  if (!row || !column) continue;
1311
- const cellShell = cell.querySelector(".cell-shell");
1312
- if (!cellShell) continue;
1313
- const newContent = this.renderCellTemplate(row, column, 0);
1314
- const toggles = cellShell.querySelectorAll(".row-toggle");
1315
- if (toggles.length === 0) {
1316
- cellShell.innerHTML = newContent;
1317
- } else {
1318
- const toggleHtml = Array.from(toggles).map((t) => t.outerHTML).join("");
1319
- cellShell.innerHTML = toggleHtml + newContent;
1395
+ if (cell.classList.contains("cell-editing")) continue;
1396
+ const cellContent = cell.querySelector(".cell-content");
1397
+ if (!cellContent) continue;
1398
+ const newContent = this.renderCellTemplateFromMarkup(
1399
+ row,
1400
+ column,
1401
+ rowIndexMap.get(rowId) ?? 0,
1402
+ templateMarkupMap.get(colName) ?? null
1403
+ );
1404
+ if (cellContent.innerHTML !== newContent) {
1405
+ cellContent.innerHTML = newContent;
1320
1406
  }
1321
1407
  }
1322
1408
  }
@@ -1530,11 +1616,51 @@ var UiGridStandaloneElement = class extends HTMLElement {
1530
1616
  this.controller.setPageSize(value);
1531
1617
  }
1532
1618
  });
1619
+ root.addEventListener("mousedown", (event) => {
1620
+ const target = event.target;
1621
+ if (!target || !this.controller) return;
1622
+ const resizer = target.closest(".column-resizer[data-column]");
1623
+ if (!resizer || !this.controller.canResizeColumns()) return;
1624
+ const columnName = resizer.dataset["column"];
1625
+ if (!columnName) return;
1626
+ const headerCell = resizer.closest(".header-cell");
1627
+ if (!headerCell) return;
1628
+ event.preventDefault();
1629
+ event.stopPropagation();
1630
+ const startX = event.clientX;
1631
+ const startWidth = headerCell.getBoundingClientRect().width;
1632
+ let lastWidth = startWidth;
1633
+ const handleMove = (moveEvent) => {
1634
+ lastWidth = Math.max(88, startWidth + (moveEvent.clientX - startX));
1635
+ const newTemplate = this.controller.buildTemplateColumnsWithOverride(columnName, lastWidth);
1636
+ const root2 = this.shadowRoot ?? this;
1637
+ root2.querySelectorAll(".header-grid, .filter-grid, .body-grid").forEach((el) => {
1638
+ el.style.gridTemplateColumns = newTemplate;
1639
+ });
1640
+ };
1641
+ const handleUp = () => {
1642
+ window.removeEventListener("mousemove", handleMove);
1643
+ window.removeEventListener("mouseup", handleUp);
1644
+ this.controller.setColumnWidthOverride(columnName, lastWidth);
1645
+ };
1646
+ window.addEventListener("mousemove", handleMove);
1647
+ window.addEventListener("mouseup", handleUp);
1648
+ });
1533
1649
  root.addEventListener("dblclick", (event) => {
1534
1650
  const target = event.target;
1535
1651
  if (!target || !this.controller) {
1536
1652
  return;
1537
1653
  }
1654
+ const resizer = target.closest(".column-resizer[data-column]");
1655
+ if (resizer && this.controller.canResizeColumns()) {
1656
+ const columnName2 = resizer.dataset["column"];
1657
+ if (columnName2) {
1658
+ event.preventDefault();
1659
+ event.stopPropagation();
1660
+ this.controller.setColumnWidthOverride(columnName2, this.measureAutoColumnWidth(columnName2));
1661
+ return;
1662
+ }
1663
+ }
1538
1664
  const cell = target.closest(".body-cell");
1539
1665
  if (!cell) {
1540
1666
  return;
@@ -1668,7 +1794,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
1668
1794
  if (!target?.classList.contains("grid-table") || this.suppressScrollEvent) {
1669
1795
  return;
1670
1796
  }
1797
+ this.lastScrollActivityAt = Date.now();
1671
1798
  this.scrollPosition = target.scrollTop;
1799
+ this.horizontalScrollPosition = target.scrollLeft;
1672
1800
  if (!this.snapshot?.pipeline.virtualizationEnabled) {
1673
1801
  return;
1674
1802
  }
@@ -1749,6 +1877,7 @@ var UiGridStandaloneElement = class extends HTMLElement {
1749
1877
  const previousGridTable = root.querySelector(".grid-table");
1750
1878
  if (previousGridTable && !this.suppressScrollEvent) {
1751
1879
  this.scrollPosition = previousGridTable.scrollTop;
1880
+ this.horizontalScrollPosition = previousGridTable.scrollLeft;
1752
1881
  }
1753
1882
  if (!snapshot) {
1754
1883
  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 +1924,18 @@ var UiGridStandaloneElement = class extends HTMLElement {
1795
1924
  const bodyContent = itemsToRender.map((item, index) => this.renderDisplayItem(item, startIndex + index)).join("");
1796
1925
  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
1926
  const pagination = paginationEnabled && showPagination ? this.renderPagination(snapshot) : "";
1798
- const gridTableStyle = `${virtualizationEnabled ? `height:${viewportHeight}px;overflow-y:auto;` : ""}--ui-grid-header-sticky-top:${headerStickyTop}px;`;
1927
+ const hasViewportScroll = virtualizationEnabled || options.viewportHeight !== void 0;
1928
+ const gridTableStyle = `${hasViewportScroll ? `height:${viewportHeight}px;overflow-y:auto;` : ""}--ui-grid-header-sticky-top:${headerStickyTop}px;`;
1799
1929
  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
1930
  const gridTable = root.querySelector(".grid-table");
1801
- if (gridTable && this.scrollPosition > 0 && virtualizationEnabled) {
1931
+ if (gridTable && (this.scrollPosition > 0 || this.horizontalScrollPosition > 0)) {
1802
1932
  this.suppressScrollEvent = true;
1803
- gridTable.scrollTop = this.scrollPosition;
1933
+ if (this.scrollPosition > 0) {
1934
+ gridTable.scrollTop = this.scrollPosition;
1935
+ }
1936
+ if (this.horizontalScrollPosition > 0) {
1937
+ gridTable.scrollLeft = this.horizontalScrollPosition;
1938
+ }
1804
1939
  requestAnimationFrame(() => {
1805
1940
  this.suppressScrollEvent = false;
1806
1941
  });
@@ -1851,8 +1986,10 @@ var UiGridStandaloneElement = class extends HTMLElement {
1851
1986
  const sortIconKey = sortDirection === import_ui_grid_core2.SORT_DIRECTIONS.asc ? "sortAsc" : sortDirection === import_ui_grid_core2.SORT_DIRECTIONS.desc ? "sortDesc" : "sortNone";
1852
1987
  const draggable = (0, import_ui_grid_core2.canGridMoveColumns)(options) ? 'draggable="true"' : "";
1853
1988
  const pinLabel = isPinned ? this.snapshot?.labels.unpin ?? "Unpin" : this.snapshot?.labels.pinColumn ?? "Pin";
1989
+ const canResize = controller.canResizeColumns();
1854
1990
  const headerValue = escapeHtml((0, import_ui_grid_core2.formatGridHeaderDisplayValue)((0, import_ui_grid_core2.buildGridHeaderContext)(column)));
1855
- return `<div class="${classes}" data-column="${escapeHtml(column.name)}" ${draggable} style="${stickyStyle}"><span class="header-label">${headerValue}</span><span class="header-actions">${sortEnabled ? `<button type="button" class="header-action" data-action="sort" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(sortLabel)}" ${canSort ? "" : "disabled"}>${this.renderControlIcon(sortIconKey)}<span class="sr-only">${escapeHtml(sortLabel)}</span></button>` : ""}${groupingEnabled ? `<button type="button" class="chip-action${controller.isColumnGrouped(column) ? " chip-action-active" : ""}" data-action="group" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(groupingLabel)}" ${canGroup ? "" : "disabled"}>${this.renderControlIcon("group")}<span class="sr-only">${escapeHtml(groupingLabel)}</span></button>` : ""}${canPin ? `<div class="pin-control${this.openPinMenuColumn === column.name ? " pin-control-open" : ""}"><button type="button" class="chip-action pin-trigger${isPinned ? " chip-action-active" : ""}" data-action="pin-trigger" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(pinLabel)}">${this.renderControlIcon("pin")}<span class="sr-only">${escapeHtml(pinLabel)}</span></button><div class="pin-menu" role="menu" aria-label="${escapeHtml(pinLabel)}"><button type="button" class="pin-menu-action" data-action="pin-left" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(this.snapshot?.labels.pinLeft ?? "Pin left")}">${this.renderIconWithClass("control-icon", "pinLeft")}<span class="sr-only">${escapeHtml(this.snapshot?.labels.pinLeft ?? "Pin left")}</span></button><button type="button" class="pin-menu-action" data-action="pin-right" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(this.snapshot?.labels.pinRight ?? "Pin right")}">${this.renderIconWithClass("control-icon", "pinRight")}<span class="sr-only">${escapeHtml(this.snapshot?.labels.pinRight ?? "Pin right")}</span></button></div></div>` : ""}</span></div>`;
1991
+ const resizerHtml = canResize ? `<button type="button" class="column-resizer" data-action="resize" data-column="${escapeHtml(column.name)}" aria-label="Resize ${escapeHtml(headerValue)} column" title="Drag to resize, double-click to auto fit"></button>` : "";
1992
+ return `<div class="${classes}" data-column="${escapeHtml(column.name)}" ${draggable} style="${stickyStyle}"><span class="header-label">${headerValue}</span><span class="header-actions">${sortEnabled ? `<button type="button" class="header-action" data-action="sort" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(sortLabel)}" ${canSort ? "" : "disabled"}>${this.renderControlIcon(sortIconKey)}<span class="sr-only">${escapeHtml(sortLabel)}</span></button>` : ""}${groupingEnabled ? `<button type="button" class="chip-action${controller.isColumnGrouped(column) ? " chip-action-active" : ""}" data-action="group" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(groupingLabel)}" ${canGroup ? "" : "disabled"}>${this.renderControlIcon("group")}<span class="sr-only">${escapeHtml(groupingLabel)}</span></button>` : ""}${canPin ? `<div class="pin-control${this.openPinMenuColumn === column.name ? " pin-control-open" : ""}"><button type="button" class="chip-action pin-trigger${isPinned ? " chip-action-active" : ""}" data-action="pin-trigger" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(pinLabel)}">${this.renderControlIcon("pin")}<span class="sr-only">${escapeHtml(pinLabel)}</span></button><div class="pin-menu" role="menu" aria-label="${escapeHtml(pinLabel)}"><button type="button" class="pin-menu-action" data-action="pin-left" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(this.snapshot?.labels.pinLeft ?? "Pin left")}">${this.renderIconWithClass("control-icon", "pinLeft")}<span class="sr-only">${escapeHtml(this.snapshot?.labels.pinLeft ?? "Pin left")}</span></button><button type="button" class="pin-menu-action" data-action="pin-right" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(this.snapshot?.labels.pinRight ?? "Pin right")}">${this.renderIconWithClass("control-icon", "pinRight")}<span class="sr-only">${escapeHtml(this.snapshot?.labels.pinRight ?? "Pin right")}</span></button></div></div>` : ""}</span>${resizerHtml}</div>`;
1856
1993
  }
1857
1994
  renderFilterCell(column) {
1858
1995
  const value = this.snapshot?.activeFilters[column.name] ?? "";
@@ -1868,6 +2005,23 @@ var UiGridStandaloneElement = class extends HTMLElement {
1868
2005
  const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : "";
1869
2006
  return `<label class="${classes}" style="${stickyStyle}"><input class="ui-grid-filter-input" data-role="filter" data-column="${escapeHtml(column.name)}" placeholder="${escapeHtml(controller.filterPlaceholder(column))}" value="${escapeHtml(value)}" ${canFilter ? "" : "disabled"}></label>`;
1870
2007
  }
2008
+ measureAutoColumnWidth(columnName) {
2009
+ const root = this.shadowRoot;
2010
+ if (root == null) return 176;
2011
+ const escaped = CSS.escape ? CSS.escape(columnName) : columnName.replace(/([\\".#:[\](){}+~> ])/g, "\\$1");
2012
+ const selectors = [
2013
+ `.header-cell[data-column="${escaped}"]`,
2014
+ `.filter-cell[data-column="${escaped}"]`,
2015
+ `.body-cell[data-column="${escaped}"] .cell-shell`
2016
+ ];
2017
+ let maxWidth = 0;
2018
+ for (const selector of selectors) {
2019
+ for (const el of root.querySelectorAll(selector)) {
2020
+ maxWidth = Math.max(maxWidth, el.scrollWidth);
2021
+ }
2022
+ }
2023
+ return maxWidth + 12;
2024
+ }
1871
2025
  renderDisplayItem(item, displayIndex) {
1872
2026
  if (!this.snapshot || !this.controller) {
1873
2027
  return "";
@@ -1912,7 +2066,7 @@ var UiGridStandaloneElement = class extends HTMLElement {
1912
2066
  editing ? "cell-editing" : ""
1913
2067
  ].filter(Boolean).join(" ");
1914
2068
  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}${content}</div></div>`;
2069
+ 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
2070
  }
1917
2071
  renderPagination(snapshot) {
1918
2072
  const pageSizes = snapshot.options.paginationPageSizes ?? [10, 25, 50, 100];
@@ -1926,6 +2080,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
1926
2080
  }
1927
2081
  renderCellTemplate(row, column, displayIndex) {
1928
2082
  const templateMarkup = this.getTemplateMarkup(this.cellSlotName(column));
2083
+ return this.renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup);
2084
+ }
2085
+ renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup) {
1929
2086
  if (!templateMarkup) {
1930
2087
  return `<span class="cell-value">${escapeHtml(this.controller?.displayValue(row, column) ?? "")}</span>`;
1931
2088
  }
@@ -1978,6 +2135,79 @@ var UiGridStandaloneElement = class extends HTMLElement {
1978
2135
  rowIndexFor(row, fallback = 0) {
1979
2136
  return this.snapshot?.pipeline.visibleRows.findIndex((candidate) => candidate.id === row.id) ?? fallback;
1980
2137
  }
2138
+ classifyDataRefresh(previousSnapshot, nextSnapshot) {
2139
+ if (!nextSnapshot) {
2140
+ return { mode: "full", changedRowIds: null };
2141
+ }
2142
+ if (!previousSnapshot) {
2143
+ return {
2144
+ mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
2145
+ changedRowIds: null
2146
+ };
2147
+ }
2148
+ if (previousSnapshot.visibleColumns.length !== nextSnapshot.visibleColumns.length) {
2149
+ return { mode: "full", changedRowIds: null };
2150
+ }
2151
+ for (let index = 0; index < previousSnapshot.visibleColumns.length; index += 1) {
2152
+ if (previousSnapshot.visibleColumns[index]?.name !== nextSnapshot.visibleColumns[index]?.name) {
2153
+ return { mode: "full", changedRowIds: null };
2154
+ }
2155
+ }
2156
+ const previousWindow = this.getRenderedDisplayWindow(previousSnapshot);
2157
+ const nextWindow = this.getRenderedDisplayWindow(nextSnapshot);
2158
+ if (previousWindow.startIndex !== nextWindow.startIndex || previousWindow.items.length !== nextWindow.items.length) {
2159
+ return {
2160
+ mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
2161
+ changedRowIds: null
2162
+ };
2163
+ }
2164
+ const changedRowIds = /* @__PURE__ */ new Set();
2165
+ for (let index = 0; index < previousWindow.items.length; index += 1) {
2166
+ const previousItem = previousWindow.items[index];
2167
+ const nextItem = nextWindow.items[index];
2168
+ if (!previousItem || !nextItem || previousItem.kind !== "row" || nextItem.kind !== "row") {
2169
+ return {
2170
+ mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
2171
+ changedRowIds: null
2172
+ };
2173
+ }
2174
+ if (previousItem.row.id !== nextItem.row.id) {
2175
+ return {
2176
+ mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
2177
+ changedRowIds: null
2178
+ };
2179
+ }
2180
+ if (previousItem.row.entity !== nextItem.row.entity) {
2181
+ changedRowIds.add(nextItem.row.id);
2182
+ }
2183
+ }
2184
+ return {
2185
+ mode: changedRowIds.size > 0 ? "patch" : null,
2186
+ changedRowIds: changedRowIds.size > 0 ? changedRowIds : null
2187
+ };
2188
+ }
2189
+ getRenderedDisplayWindow(snapshot) {
2190
+ if (!snapshot.pipeline.virtualizationEnabled) {
2191
+ return {
2192
+ startIndex: 0,
2193
+ items: snapshot.pipeline.displayItems
2194
+ };
2195
+ }
2196
+ const stickyChromeHeight = this.measuredHeaderStickyHeight + this.measuredFilterStickyHeight;
2197
+ const viewportHeight = snapshot.options.viewportHeight ?? 560;
2198
+ const bodyViewportHeight = Math.max(snapshot.rowSize, viewportHeight - stickyChromeHeight);
2199
+ const bodyScrollTop = Math.max(0, this.scrollPosition - stickyChromeHeight);
2200
+ const overscan = 4;
2201
+ const startIndex = Math.max(0, Math.floor(bodyScrollTop / snapshot.rowSize) - overscan);
2202
+ const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
2203
+ return {
2204
+ startIndex,
2205
+ items: snapshot.pipeline.displayItems.slice(
2206
+ startIndex,
2207
+ Math.min(snapshot.pipeline.displayItems.length, startIndex + visibleCount)
2208
+ )
2209
+ };
2210
+ }
1981
2211
  cellSlotName(column) {
1982
2212
  return `cell-${column.name}`;
1983
2213
  }