@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/dist/index.mjs CHANGED
@@ -97,6 +97,7 @@ var VanillaGridController = class {
97
97
  expandedTreeRows = {};
98
98
  pinnedColumns = {};
99
99
  columnOrder = [];
100
+ columnWidthOverrides = {};
100
101
  currentPage = 1;
101
102
  pageSize = 0;
102
103
  editingCell = null;
@@ -505,6 +506,24 @@ var VanillaGridController = class {
505
506
  });
506
507
  this.refresh();
507
508
  }
509
+ canResizeColumns() {
510
+ return this.options.enableColumnResizing !== false;
511
+ }
512
+ setColumnWidthOverride(columnName, widthPx) {
513
+ const nextWidth = `${Math.max(88, Math.round(widthPx))}px`;
514
+ this.columnWidthOverrides = { ...this.columnWidthOverrides, [columnName]: nextWidth };
515
+ this.refresh();
516
+ }
517
+ /** Returns the grid-template-columns string as if `columnName` had the given width,
518
+ * without triggering a full refresh. Used for smooth drag-resize DOM updates. */
519
+ buildTemplateColumnsWithOverride(columnName, widthPx) {
520
+ const widthStr = `${Math.max(88, Math.round(widthPx))}px`;
521
+ return buildGridTemplateColumns(
522
+ this.visibleColumns.map(
523
+ (c) => c.name === columnName ? { ...c, width: widthStr } : c
524
+ )
525
+ );
526
+ }
508
527
  isGroupingEnabled() {
509
528
  return isGridGroupingEnabled(this.options);
510
529
  }
@@ -623,9 +642,13 @@ var VanillaGridController = class {
623
642
  }
624
643
  refresh() {
625
644
  const orderedColumns = orderVisibleColumns(this.options.columnDefs, this.columnOrder);
645
+ const applyWidthOverrides = (columns) => columns.map((col) => {
646
+ const override = this.columnWidthOverrides[col.name];
647
+ return override == null ? col : { ...col, width: override };
648
+ });
626
649
  const pinnedEntries = Object.entries(this.pinnedColumns);
627
650
  if (pinnedEntries.length === 0) {
628
- this.visibleColumns = orderedColumns;
651
+ this.visibleColumns = applyWidthOverrides(orderedColumns);
629
652
  } else {
630
653
  const byName = new Map(orderedColumns.map((column) => [column.name, column]));
631
654
  const pinnedLeft = pinnedEntries.filter((entry) => entry[1] === "left").map((entry) => byName.get(entry[0])).filter((column) => column !== void 0);
@@ -633,7 +656,7 @@ var VanillaGridController = class {
633
656
  const middleColumns = orderedColumns.filter(
634
657
  (column) => this.pinnedColumns[column.name] === void 0
635
658
  );
636
- this.visibleColumns = [...pinnedLeft, ...middleColumns, ...pinnedRight];
659
+ this.visibleColumns = applyWidthOverrides([...pinnedLeft, ...middleColumns, ...pinnedRight]);
637
660
  }
638
661
  this.pipeline = defaultGridEngine.buildPipeline({
639
662
  options: this.options,
@@ -821,6 +844,7 @@ var UiGridStandaloneElement = class extends HTMLElement {
821
844
  draggedColumnName = null;
822
845
  dropTargetColumnName = null;
823
846
  scrollPosition = 0;
847
+ horizontalScrollPosition = 0;
824
848
  scrollFrame = null;
825
849
  suppressScrollEvent = false;
826
850
  lastVirtualStartIndex = -1;
@@ -830,6 +854,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
830
854
  benchmarkAverage = "\u2014";
831
855
  skipNextRender = false;
832
856
  dataFrame = null;
857
+ pendingPatchedRowIds = null;
858
+ pendingDataRefreshMode = null;
859
+ lastScrollActivityAt = 0;
833
860
  static get observedAttributes() {
834
861
  return [
835
862
  // Scalar attributes
@@ -875,10 +902,14 @@ var UiGridStandaloneElement = class extends HTMLElement {
875
902
  ];
876
903
  }
877
904
  get options() {
878
- return this.activeOptions ?? {
905
+ if (this.activeOptions !== null) {
906
+ return this.buildEffectiveOptions(this.activeOptions);
907
+ }
908
+ return {
879
909
  id: "__ui-grid-pending__",
880
910
  data: [],
881
- columnDefs: []
911
+ columnDefs: [],
912
+ ...this.attributeOptions
882
913
  };
883
914
  }
884
915
  set options(value) {
@@ -949,6 +980,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
949
980
  const enableColumnMoving = this.parseBooleanAttribute("enable-column-moving");
950
981
  if (enableColumnMoving !== void 0)
951
982
  this.attributeOptions.enableColumnMoving = enableColumnMoving;
983
+ const enableColumnResizing = this.parseBooleanAttribute("enable-column-resizing");
984
+ if (enableColumnResizing !== void 0)
985
+ this.attributeOptions.enableColumnResizing = enableColumnResizing;
952
986
  const enableCellEdit = this.parseBooleanAttribute("enable-cell-edit");
953
987
  if (enableCellEdit !== void 0) this.attributeOptions.enableCellEdit = enableCellEdit;
954
988
  const enableCellEditOnFocus = this.parseBooleanAttribute("enable-cell-edit-on-focus");
@@ -1001,8 +1035,15 @@ var UiGridStandaloneElement = class extends HTMLElement {
1001
1035
  );
1002
1036
  if (paginationPageSizes !== void 0)
1003
1037
  this.attributeOptions.paginationPageSizes = paginationPageSizes;
1004
- if (this.activeOptions) {
1038
+ if (this.activeOptions !== null) {
1005
1039
  this.ensureController(this.buildEffectiveOptions(this.activeOptions));
1040
+ } else {
1041
+ this.ensureController({
1042
+ id: "__ui-grid-pending__",
1043
+ data: [],
1044
+ columnDefs: [],
1045
+ ...this.attributeOptions
1046
+ });
1006
1047
  }
1007
1048
  }
1008
1049
  parseBooleanAttribute(name) {
@@ -1190,6 +1231,13 @@ var UiGridStandaloneElement = class extends HTMLElement {
1190
1231
  this.activeOptions = { ...this.activeOptions, enableColumnMoving: value };
1191
1232
  this.ensureController(this.buildEffectiveOptions(this.activeOptions));
1192
1233
  }
1234
+ get enableColumnResizing() {
1235
+ return this.options.enableColumnResizing ?? true;
1236
+ }
1237
+ set enableColumnResizing(value) {
1238
+ this.activeOptions = { ...this.activeOptions, enableColumnResizing: value };
1239
+ this.ensureController(this.buildEffectiveOptions(this.activeOptions));
1240
+ }
1193
1241
  get enableCellEdit() {
1194
1242
  return this.options.enableCellEdit ?? false;
1195
1243
  }
@@ -1314,18 +1362,49 @@ var UiGridStandaloneElement = class extends HTMLElement {
1314
1362
  if (!controller) {
1315
1363
  return;
1316
1364
  }
1365
+ const previousSnapshot = this.snapshot;
1317
1366
  this.skipNextRender = true;
1318
1367
  controller.refreshData(rows);
1319
1368
  this.skipNextRender = false;
1369
+ const { mode, changedRowIds } = this.classifyDataRefresh(previousSnapshot, this.snapshot);
1370
+ if (mode === null) {
1371
+ return;
1372
+ }
1373
+ this.pendingDataRefreshMode = mode;
1374
+ this.pendingPatchedRowIds = changedRowIds;
1320
1375
  if (this.dataFrame !== null) {
1321
1376
  cancelAnimationFrame(this.dataFrame);
1322
1377
  }
1323
1378
  this.dataFrame = requestAnimationFrame(() => {
1324
- this.dataFrame = null;
1325
- this.patchCells();
1379
+ this.flushPendingDataRefresh();
1326
1380
  });
1327
1381
  }
1328
- patchCells() {
1382
+ flushPendingDataRefresh() {
1383
+ this.dataFrame = null;
1384
+ const mode = this.pendingDataRefreshMode;
1385
+ if (mode === null) {
1386
+ return;
1387
+ }
1388
+ if (Date.now() - this.lastScrollActivityAt < 80) {
1389
+ this.dataFrame = requestAnimationFrame(() => {
1390
+ this.flushPendingDataRefresh();
1391
+ });
1392
+ return;
1393
+ }
1394
+ const changedRowIds = this.pendingPatchedRowIds;
1395
+ this.pendingDataRefreshMode = null;
1396
+ this.pendingPatchedRowIds = null;
1397
+ if (mode === "patch") {
1398
+ this.patchCells(changedRowIds ?? void 0);
1399
+ return;
1400
+ }
1401
+ if (mode === "virtual") {
1402
+ this.renderVirtualBody();
1403
+ return;
1404
+ }
1405
+ this.render();
1406
+ }
1407
+ patchCells(changedRowIds) {
1329
1408
  const snapshot = this.snapshot;
1330
1409
  if (!snapshot) {
1331
1410
  return;
@@ -1335,30 +1414,37 @@ var UiGridStandaloneElement = class extends HTMLElement {
1335
1414
  return;
1336
1415
  }
1337
1416
  const rowMap = /* @__PURE__ */ new Map();
1417
+ const rowIndexMap = /* @__PURE__ */ new Map();
1338
1418
  for (const row of snapshot.pipeline.visibleRows) {
1339
1419
  rowMap.set(row.id, row);
1420
+ rowIndexMap.set(row.id, rowIndexMap.size);
1340
1421
  }
1341
1422
  const colMap = /* @__PURE__ */ new Map();
1423
+ const templateMarkupMap = /* @__PURE__ */ new Map();
1342
1424
  for (const col of snapshot.visibleColumns) {
1343
1425
  colMap.set(col.name, col);
1426
+ templateMarkupMap.set(col.name, this.getTemplateMarkup(this.cellSlotName(col)));
1344
1427
  }
1345
1428
  const cells = root.querySelectorAll("[data-row][data-column]");
1346
1429
  for (const cell of cells) {
1347
1430
  const rowId = cell.dataset["row"];
1348
1431
  const colName = cell.dataset["column"];
1349
1432
  if (!rowId || !colName) continue;
1433
+ if (changedRowIds && !changedRowIds.has(rowId)) continue;
1350
1434
  const row = rowMap.get(rowId);
1351
1435
  const column = colMap.get(colName);
1352
1436
  if (!row || !column) continue;
1353
- const cellShell = cell.querySelector(".cell-shell");
1354
- if (!cellShell) continue;
1355
- const newContent = this.renderCellTemplate(row, column, 0);
1356
- const toggles = cellShell.querySelectorAll(".row-toggle");
1357
- if (toggles.length === 0) {
1358
- cellShell.innerHTML = newContent;
1359
- } else {
1360
- const toggleHtml = Array.from(toggles).map((t) => t.outerHTML).join("");
1361
- cellShell.innerHTML = toggleHtml + newContent;
1437
+ if (cell.classList.contains("cell-editing")) continue;
1438
+ const cellContent = cell.querySelector(".cell-content");
1439
+ if (!cellContent) continue;
1440
+ const newContent = this.renderCellTemplateFromMarkup(
1441
+ row,
1442
+ column,
1443
+ rowIndexMap.get(rowId) ?? 0,
1444
+ templateMarkupMap.get(colName) ?? null
1445
+ );
1446
+ if (cellContent.innerHTML !== newContent) {
1447
+ cellContent.innerHTML = newContent;
1362
1448
  }
1363
1449
  }
1364
1450
  }
@@ -1572,11 +1658,51 @@ var UiGridStandaloneElement = class extends HTMLElement {
1572
1658
  this.controller.setPageSize(value);
1573
1659
  }
1574
1660
  });
1661
+ root.addEventListener("mousedown", (event) => {
1662
+ const target = event.target;
1663
+ if (!target || !this.controller) return;
1664
+ const resizer = target.closest(".column-resizer[data-column]");
1665
+ if (!resizer || !this.controller.canResizeColumns()) return;
1666
+ const columnName = resizer.dataset["column"];
1667
+ if (!columnName) return;
1668
+ const headerCell = resizer.closest(".header-cell");
1669
+ if (!headerCell) return;
1670
+ event.preventDefault();
1671
+ event.stopPropagation();
1672
+ const startX = event.clientX;
1673
+ const startWidth = headerCell.getBoundingClientRect().width;
1674
+ let lastWidth = startWidth;
1675
+ const handleMove = (moveEvent) => {
1676
+ lastWidth = Math.max(88, startWidth + (moveEvent.clientX - startX));
1677
+ const newTemplate = this.controller.buildTemplateColumnsWithOverride(columnName, lastWidth);
1678
+ const root2 = this.shadowRoot ?? this;
1679
+ root2.querySelectorAll(".header-grid, .filter-grid, .body-grid").forEach((el) => {
1680
+ el.style.gridTemplateColumns = newTemplate;
1681
+ });
1682
+ };
1683
+ const handleUp = () => {
1684
+ window.removeEventListener("mousemove", handleMove);
1685
+ window.removeEventListener("mouseup", handleUp);
1686
+ this.controller.setColumnWidthOverride(columnName, lastWidth);
1687
+ };
1688
+ window.addEventListener("mousemove", handleMove);
1689
+ window.addEventListener("mouseup", handleUp);
1690
+ });
1575
1691
  root.addEventListener("dblclick", (event) => {
1576
1692
  const target = event.target;
1577
1693
  if (!target || !this.controller) {
1578
1694
  return;
1579
1695
  }
1696
+ const resizer = target.closest(".column-resizer[data-column]");
1697
+ if (resizer && this.controller.canResizeColumns()) {
1698
+ const columnName2 = resizer.dataset["column"];
1699
+ if (columnName2) {
1700
+ event.preventDefault();
1701
+ event.stopPropagation();
1702
+ this.controller.setColumnWidthOverride(columnName2, this.measureAutoColumnWidth(columnName2));
1703
+ return;
1704
+ }
1705
+ }
1580
1706
  const cell = target.closest(".body-cell");
1581
1707
  if (!cell) {
1582
1708
  return;
@@ -1710,7 +1836,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
1710
1836
  if (!target?.classList.contains("grid-table") || this.suppressScrollEvent) {
1711
1837
  return;
1712
1838
  }
1839
+ this.lastScrollActivityAt = Date.now();
1713
1840
  this.scrollPosition = target.scrollTop;
1841
+ this.horizontalScrollPosition = target.scrollLeft;
1714
1842
  if (!this.snapshot?.pipeline.virtualizationEnabled) {
1715
1843
  return;
1716
1844
  }
@@ -1791,6 +1919,7 @@ var UiGridStandaloneElement = class extends HTMLElement {
1791
1919
  const previousGridTable = root.querySelector(".grid-table");
1792
1920
  if (previousGridTable && !this.suppressScrollEvent) {
1793
1921
  this.scrollPosition = previousGridTable.scrollTop;
1922
+ this.horizontalScrollPosition = previousGridTable.scrollLeft;
1794
1923
  }
1795
1924
  if (!snapshot) {
1796
1925
  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 +1966,18 @@ var UiGridStandaloneElement = class extends HTMLElement {
1837
1966
  const bodyContent = itemsToRender.map((item, index) => this.renderDisplayItem(item, startIndex + index)).join("");
1838
1967
  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
1968
  const pagination = paginationEnabled && showPagination ? this.renderPagination(snapshot) : "";
1840
- const gridTableStyle = `${virtualizationEnabled ? `height:${viewportHeight}px;overflow-y:auto;` : ""}--ui-grid-header-sticky-top:${headerStickyTop}px;`;
1969
+ const hasViewportScroll = virtualizationEnabled || options.viewportHeight !== void 0;
1970
+ const gridTableStyle = `${hasViewportScroll ? `height:${viewportHeight}px;overflow-y:auto;` : ""}--ui-grid-header-sticky-top:${headerStickyTop}px;`;
1841
1971
  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
1972
  const gridTable = root.querySelector(".grid-table");
1843
- if (gridTable && this.scrollPosition > 0 && virtualizationEnabled) {
1973
+ if (gridTable && (this.scrollPosition > 0 || this.horizontalScrollPosition > 0)) {
1844
1974
  this.suppressScrollEvent = true;
1845
- gridTable.scrollTop = this.scrollPosition;
1975
+ if (this.scrollPosition > 0) {
1976
+ gridTable.scrollTop = this.scrollPosition;
1977
+ }
1978
+ if (this.horizontalScrollPosition > 0) {
1979
+ gridTable.scrollLeft = this.horizontalScrollPosition;
1980
+ }
1846
1981
  requestAnimationFrame(() => {
1847
1982
  this.suppressScrollEvent = false;
1848
1983
  });
@@ -1893,8 +2028,10 @@ var UiGridStandaloneElement = class extends HTMLElement {
1893
2028
  const sortIconKey = sortDirection === SORT_DIRECTIONS2.asc ? "sortAsc" : sortDirection === SORT_DIRECTIONS2.desc ? "sortDesc" : "sortNone";
1894
2029
  const draggable = canGridMoveColumns2(options) ? 'draggable="true"' : "";
1895
2030
  const pinLabel = isPinned ? this.snapshot?.labels.unpin ?? "Unpin" : this.snapshot?.labels.pinColumn ?? "Pin";
2031
+ const canResize = controller.canResizeColumns();
1896
2032
  const headerValue = escapeHtml(formatGridHeaderDisplayValue(buildGridHeaderContext(column)));
1897
- 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>`;
2033
+ 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>` : "";
2034
+ 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>`;
1898
2035
  }
1899
2036
  renderFilterCell(column) {
1900
2037
  const value = this.snapshot?.activeFilters[column.name] ?? "";
@@ -1910,6 +2047,23 @@ var UiGridStandaloneElement = class extends HTMLElement {
1910
2047
  const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : "";
1911
2048
  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>`;
1912
2049
  }
2050
+ measureAutoColumnWidth(columnName) {
2051
+ const root = this.shadowRoot;
2052
+ if (root == null) return 176;
2053
+ const escaped = CSS.escape ? CSS.escape(columnName) : columnName.replace(/([\\".#:[\](){}+~> ])/g, "\\$1");
2054
+ const selectors = [
2055
+ `.header-cell[data-column="${escaped}"]`,
2056
+ `.filter-cell[data-column="${escaped}"]`,
2057
+ `.body-cell[data-column="${escaped}"] .cell-shell`
2058
+ ];
2059
+ let maxWidth = 0;
2060
+ for (const selector of selectors) {
2061
+ for (const el of root.querySelectorAll(selector)) {
2062
+ maxWidth = Math.max(maxWidth, el.scrollWidth);
2063
+ }
2064
+ }
2065
+ return maxWidth + 12;
2066
+ }
1913
2067
  renderDisplayItem(item, displayIndex) {
1914
2068
  if (!this.snapshot || !this.controller) {
1915
2069
  return "";
@@ -1954,7 +2108,7 @@ var UiGridStandaloneElement = class extends HTMLElement {
1954
2108
  editing ? "cell-editing" : ""
1955
2109
  ].filter(Boolean).join(" ");
1956
2110
  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}${content}</div></div>`;
2111
+ 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
2112
  }
1959
2113
  renderPagination(snapshot) {
1960
2114
  const pageSizes = snapshot.options.paginationPageSizes ?? [10, 25, 50, 100];
@@ -1968,6 +2122,9 @@ var UiGridStandaloneElement = class extends HTMLElement {
1968
2122
  }
1969
2123
  renderCellTemplate(row, column, displayIndex) {
1970
2124
  const templateMarkup = this.getTemplateMarkup(this.cellSlotName(column));
2125
+ return this.renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup);
2126
+ }
2127
+ renderCellTemplateFromMarkup(row, column, displayIndex, templateMarkup) {
1971
2128
  if (!templateMarkup) {
1972
2129
  return `<span class="cell-value">${escapeHtml(this.controller?.displayValue(row, column) ?? "")}</span>`;
1973
2130
  }
@@ -2020,6 +2177,79 @@ var UiGridStandaloneElement = class extends HTMLElement {
2020
2177
  rowIndexFor(row, fallback = 0) {
2021
2178
  return this.snapshot?.pipeline.visibleRows.findIndex((candidate) => candidate.id === row.id) ?? fallback;
2022
2179
  }
2180
+ classifyDataRefresh(previousSnapshot, nextSnapshot) {
2181
+ if (!nextSnapshot) {
2182
+ return { mode: "full", changedRowIds: null };
2183
+ }
2184
+ if (!previousSnapshot) {
2185
+ return {
2186
+ mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
2187
+ changedRowIds: null
2188
+ };
2189
+ }
2190
+ if (previousSnapshot.visibleColumns.length !== nextSnapshot.visibleColumns.length) {
2191
+ return { mode: "full", changedRowIds: null };
2192
+ }
2193
+ for (let index = 0; index < previousSnapshot.visibleColumns.length; index += 1) {
2194
+ if (previousSnapshot.visibleColumns[index]?.name !== nextSnapshot.visibleColumns[index]?.name) {
2195
+ return { mode: "full", changedRowIds: null };
2196
+ }
2197
+ }
2198
+ const previousWindow = this.getRenderedDisplayWindow(previousSnapshot);
2199
+ const nextWindow = this.getRenderedDisplayWindow(nextSnapshot);
2200
+ if (previousWindow.startIndex !== nextWindow.startIndex || previousWindow.items.length !== nextWindow.items.length) {
2201
+ return {
2202
+ mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
2203
+ changedRowIds: null
2204
+ };
2205
+ }
2206
+ const changedRowIds = /* @__PURE__ */ new Set();
2207
+ for (let index = 0; index < previousWindow.items.length; index += 1) {
2208
+ const previousItem = previousWindow.items[index];
2209
+ const nextItem = nextWindow.items[index];
2210
+ if (!previousItem || !nextItem || previousItem.kind !== "row" || nextItem.kind !== "row") {
2211
+ return {
2212
+ mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
2213
+ changedRowIds: null
2214
+ };
2215
+ }
2216
+ if (previousItem.row.id !== nextItem.row.id) {
2217
+ return {
2218
+ mode: nextSnapshot.pipeline.virtualizationEnabled ? "virtual" : "full",
2219
+ changedRowIds: null
2220
+ };
2221
+ }
2222
+ if (previousItem.row.entity !== nextItem.row.entity) {
2223
+ changedRowIds.add(nextItem.row.id);
2224
+ }
2225
+ }
2226
+ return {
2227
+ mode: changedRowIds.size > 0 ? "patch" : null,
2228
+ changedRowIds: changedRowIds.size > 0 ? changedRowIds : null
2229
+ };
2230
+ }
2231
+ getRenderedDisplayWindow(snapshot) {
2232
+ if (!snapshot.pipeline.virtualizationEnabled) {
2233
+ return {
2234
+ startIndex: 0,
2235
+ items: snapshot.pipeline.displayItems
2236
+ };
2237
+ }
2238
+ const stickyChromeHeight = this.measuredHeaderStickyHeight + this.measuredFilterStickyHeight;
2239
+ const viewportHeight = snapshot.options.viewportHeight ?? 560;
2240
+ const bodyViewportHeight = Math.max(snapshot.rowSize, viewportHeight - stickyChromeHeight);
2241
+ const bodyScrollTop = Math.max(0, this.scrollPosition - stickyChromeHeight);
2242
+ const overscan = 4;
2243
+ const startIndex = Math.max(0, Math.floor(bodyScrollTop / snapshot.rowSize) - overscan);
2244
+ const visibleCount = Math.ceil(bodyViewportHeight / snapshot.rowSize) + overscan * 2;
2245
+ return {
2246
+ startIndex,
2247
+ items: snapshot.pipeline.displayItems.slice(
2248
+ startIndex,
2249
+ Math.min(snapshot.pipeline.displayItems.length, startIndex + visibleCount)
2250
+ )
2251
+ };
2252
+ }
2023
2253
  cellSlotName(column) {
2024
2254
  return `cell-${column.name}`;
2025
2255
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornery/ui-grid-vanilla",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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": "^0.1.7"
19
+ "@ornery/ui-grid-core": "0.1.10"
20
20
  },
21
21
  "devDependencies": {
22
22
  "typescript": "~5.8.0",
@@ -134,6 +134,7 @@ export class VanillaGridController {
134
134
  private expandedTreeRows: Record<string, boolean> = {};
135
135
  private pinnedColumns: PinnedColumnState = {};
136
136
  private columnOrder: string[] = [];
137
+ private columnWidthOverrides: Record<string, string> = {};
137
138
  private currentPage = 1;
138
139
  private pageSize = 0;
139
140
  private editingCell: GridCellPosition | null = null;
@@ -590,6 +591,27 @@ export class VanillaGridController {
590
591
  this.refresh();
591
592
  }
592
593
 
594
+ canResizeColumns(): boolean {
595
+ return this.options.enableColumnResizing !== false;
596
+ }
597
+
598
+ setColumnWidthOverride(columnName: string, widthPx: number): void {
599
+ const nextWidth = `${Math.max(88, Math.round(widthPx))}px`;
600
+ this.columnWidthOverrides = { ...this.columnWidthOverrides, [columnName]: nextWidth };
601
+ this.refresh();
602
+ }
603
+
604
+ /** Returns the grid-template-columns string as if `columnName` had the given width,
605
+ * without triggering a full refresh. Used for smooth drag-resize DOM updates. */
606
+ buildTemplateColumnsWithOverride(columnName: string, widthPx: number): string {
607
+ const widthStr = `${Math.max(88, Math.round(widthPx))}px`;
608
+ return buildGridTemplateColumns(
609
+ this.visibleColumns.map((c) =>
610
+ c.name === columnName ? { ...c, width: widthStr } : c,
611
+ ),
612
+ );
613
+ }
614
+
593
615
  isGroupingEnabled(): boolean {
594
616
  return isGridGroupingEnabled(this.options);
595
617
  }
@@ -743,10 +765,15 @@ export class VanillaGridController {
743
765
 
744
766
  private refresh(): void {
745
767
  const orderedColumns = orderVisibleColumns(this.options.columnDefs, this.columnOrder);
768
+ const applyWidthOverrides = (columns: GridColumnDef[]): GridColumnDef[] =>
769
+ columns.map((col) => {
770
+ const override = this.columnWidthOverrides[col.name];
771
+ return override == null ? col : { ...col, width: override };
772
+ });
746
773
  const pinnedEntries = Object.entries(this.pinnedColumns);
747
774
 
748
775
  if (pinnedEntries.length === 0) {
749
- this.visibleColumns = orderedColumns;
776
+ this.visibleColumns = applyWidthOverrides(orderedColumns);
750
777
  } else {
751
778
  const byName = new Map(orderedColumns.map((column) => [column.name, column]));
752
779
  const pinnedLeft = pinnedEntries
@@ -760,7 +787,7 @@ export class VanillaGridController {
760
787
  const middleColumns = orderedColumns.filter(
761
788
  (column) => this.pinnedColumns[column.name] === undefined,
762
789
  );
763
- this.visibleColumns = [...pinnedLeft, ...middleColumns, ...pinnedRight];
790
+ this.visibleColumns = applyWidthOverrides([...pinnedLeft, ...middleColumns, ...pinnedRight]);
764
791
  }
765
792
 
766
793
  this.pipeline = defaultGridEngine.buildPipeline({
@@ -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).toBe(options);
43
+ expect(grid.options).toStrictEqual(options);
44
44
  expect(target.firstElementChild).toBe(grid);
45
45
  });
46
46