@alaarab/ogrid-js 2.1.13 → 2.1.15

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/esm/index.js CHANGED
@@ -502,23 +502,39 @@ function processClientSideData(data, columns, filters, sortBy, sortDirection) {
502
502
  const trimmed = val.value.trim();
503
503
  if (trimmed) {
504
504
  const lower = trimmed.toLowerCase();
505
- predicates.push((r) => String(getCellValue(r, col) ?? "").toLowerCase().includes(lower));
505
+ const textCache = /* @__PURE__ */ new Map();
506
+ for (let j = 0; j < data.length; j++) {
507
+ textCache.set(data[j], String(getCellValue(data[j], col) ?? "").toLowerCase());
508
+ }
509
+ predicates.push((r) => (textCache.get(r) ?? "").includes(lower));
506
510
  }
507
511
  break;
508
512
  }
509
513
  case "people": {
510
514
  const email = val.value.email.toLowerCase();
511
- predicates.push((r) => String(getCellValue(r, col) ?? "").toLowerCase() === email);
515
+ const peopleCache = /* @__PURE__ */ new Map();
516
+ for (let j = 0; j < data.length; j++) {
517
+ peopleCache.set(data[j], String(getCellValue(data[j], col) ?? "").toLowerCase());
518
+ }
519
+ predicates.push((r) => (peopleCache.get(r) ?? "") === email);
512
520
  break;
513
521
  }
514
522
  case "date": {
515
523
  const dv = val.value;
516
524
  const fromTs = dv.from ? (/* @__PURE__ */ new Date(dv.from + "T00:00:00")).getTime() : NaN;
517
525
  const toTs = dv.to ? (/* @__PURE__ */ new Date(dv.to + "T23:59:59.999")).getTime() : NaN;
526
+ const dateCache = /* @__PURE__ */ new Map();
527
+ for (let j = 0; j < data.length; j++) {
528
+ const cellVal = getCellValue(data[j], col);
529
+ if (cellVal == null) {
530
+ dateCache.set(data[j], NaN);
531
+ } else {
532
+ const t = new Date(String(cellVal)).getTime();
533
+ dateCache.set(data[j], Number.isNaN(t) ? NaN : t);
534
+ }
535
+ }
518
536
  predicates.push((r) => {
519
- const cellVal = getCellValue(r, col);
520
- if (cellVal == null) return false;
521
- const cellTs = new Date(String(cellVal)).getTime();
537
+ const cellTs = dateCache.get(r) ?? NaN;
522
538
  if (Number.isNaN(cellTs)) return false;
523
539
  if (!Number.isNaN(fromTs) && cellTs < fromTs) return false;
524
540
  if (!Number.isNaN(toTs) && cellTs > toTs) return false;
@@ -1012,23 +1028,94 @@ var ROW_NUMBER_COLUMN_WIDTH = 50;
1012
1028
  var DEFAULT_MIN_COLUMN_WIDTH = 80;
1013
1029
  var CELL_PADDING = 16;
1014
1030
  var GRID_BORDER_RADIUS = 6;
1015
- var AUTOSIZE_EXTRA_PX = 28;
1031
+ var AUTOSIZE_EXTRA_PX = 16;
1016
1032
  var AUTOSIZE_MAX_PX = 520;
1033
+ function measureHeaderWidth(th) {
1034
+ const cs = getComputedStyle(th);
1035
+ const thPadding = (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0);
1036
+ let resizeHandleWidth = 0;
1037
+ for (let i = 0; i < th.children.length; i++) {
1038
+ const child = th.children[i];
1039
+ const cls = child.className || "";
1040
+ if (cls.includes("resizeHandle") || cls.includes("resize-handle") || cls.includes("ogrid-resize-handle")) {
1041
+ resizeHandleWidth = child.offsetWidth;
1042
+ break;
1043
+ }
1044
+ }
1045
+ const contentContainer = th.firstElementChild;
1046
+ if (!contentContainer) return th.offsetWidth;
1047
+ const modified = [];
1048
+ const expandDescendants = (parent) => {
1049
+ for (let i = 0; i < parent.children.length; i++) {
1050
+ const child = parent.children[i];
1051
+ const style = getComputedStyle(child);
1052
+ if (style.overflow === "hidden" || style.flexShrink !== "0") {
1053
+ modified.push({
1054
+ el: child,
1055
+ overflow: child.style.overflow,
1056
+ flexShrink: child.style.flexShrink,
1057
+ width: child.style.width,
1058
+ minWidth: child.style.minWidth
1059
+ });
1060
+ child.style.overflow = "visible";
1061
+ child.style.flexShrink = "0";
1062
+ child.style.width = "max-content";
1063
+ child.style.minWidth = "max-content";
1064
+ }
1065
+ expandDescendants(child);
1066
+ }
1067
+ };
1068
+ const origPos = contentContainer.style.position;
1069
+ const origWidth = contentContainer.style.width;
1070
+ contentContainer.style.position = "absolute";
1071
+ contentContainer.style.width = "max-content";
1072
+ expandDescendants(contentContainer);
1073
+ const expandedWidth = contentContainer.offsetWidth;
1074
+ contentContainer.style.position = origPos;
1075
+ contentContainer.style.width = origWidth;
1076
+ for (const m of modified) {
1077
+ m.el.style.overflow = m.overflow;
1078
+ m.el.style.flexShrink = m.flexShrink;
1079
+ m.el.style.width = m.width;
1080
+ m.el.style.minWidth = m.minWidth;
1081
+ }
1082
+ return expandedWidth + resizeHandleWidth + thPadding;
1083
+ }
1017
1084
  function measureColumnContentWidth(columnId, minWidth, container) {
1018
1085
  const minW = minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
1019
1086
  const root = container ?? document;
1020
1087
  const cells = root.querySelectorAll(`[data-column-id="${columnId}"]`);
1021
1088
  if (cells.length === 0) return minW;
1022
1089
  let maxWidth = minW;
1090
+ const contentEls = [];
1091
+ const origPositions = [];
1092
+ const origWidths = [];
1023
1093
  cells.forEach((cell) => {
1024
1094
  const el = cell;
1025
- const label = el.querySelector?.("[data-header-label]");
1026
- if (label) {
1027
- maxWidth = Math.max(maxWidth, label.scrollWidth + AUTOSIZE_EXTRA_PX);
1095
+ const isHeader = !!el.querySelector?.("[data-header-label]");
1096
+ if (isHeader) {
1097
+ maxWidth = Math.max(maxWidth, measureHeaderWidth(el));
1028
1098
  } else {
1029
- maxWidth = Math.max(maxWidth, el.scrollWidth);
1099
+ const content = el.firstElementChild ?? el;
1100
+ contentEls.push(content);
1030
1101
  }
1031
1102
  });
1103
+ if (contentEls.length > 0) {
1104
+ for (let i = 0; i < contentEls.length; i++) {
1105
+ const el = contentEls[i];
1106
+ origPositions.push(el.style.position);
1107
+ origWidths.push(el.style.width);
1108
+ el.style.position = "absolute";
1109
+ el.style.width = "max-content";
1110
+ }
1111
+ for (let i = 0; i < contentEls.length; i++) {
1112
+ maxWidth = Math.max(maxWidth, contentEls[i].offsetWidth + AUTOSIZE_EXTRA_PX);
1113
+ }
1114
+ for (let i = 0; i < contentEls.length; i++) {
1115
+ contentEls[i].style.position = origPositions[i];
1116
+ contentEls[i].style.width = origWidths[i];
1117
+ }
1118
+ }
1032
1119
  return Math.min(AUTOSIZE_MAX_PX, Math.max(minW, Math.ceil(maxWidth)));
1033
1120
  }
1034
1121
  function findCtrlArrowTarget(pos, edge, step, isEmpty) {
@@ -1190,7 +1277,7 @@ function computeRowSelectionState(selectedIds, items, getRowId) {
1190
1277
  if (selectedIds.size === 0 || items.length === 0) {
1191
1278
  return { allSelected: false, someSelected: false };
1192
1279
  }
1193
- const allSelected = items.every((item) => selectedIds.has(getRowId(item)));
1280
+ const allSelected = selectedIds.size >= items.length && items.every((item) => selectedIds.has(getRowId(item)));
1194
1281
  const someSelected = !allSelected && selectedIds.size > 0;
1195
1282
  return { allSelected, someSelected };
1196
1283
  }
@@ -1887,6 +1974,17 @@ var GridState = class {
1887
1974
  };
1888
1975
 
1889
1976
  // src/renderer/TableRenderer.ts
1977
+ function rangeBounds(r) {
1978
+ return {
1979
+ minR: Math.min(r.startRow, r.endRow),
1980
+ maxR: Math.max(r.startRow, r.endRow),
1981
+ minC: Math.min(r.startCol, r.endCol),
1982
+ maxC: Math.max(r.startCol, r.endCol)
1983
+ };
1984
+ }
1985
+ function inBounds(b, row, col) {
1986
+ return row >= b.minR && row <= b.maxR && col >= b.minC && col <= b.maxC;
1987
+ }
1890
1988
  var TableRenderer = class {
1891
1989
  constructor(container, state) {
1892
1990
  this.table = null;
@@ -1907,6 +2005,7 @@ var TableRenderer = class {
1907
2005
  // Delegated event handlers bound to thead (avoids per-<th> inline listeners)
1908
2006
  this._theadClickHandler = null;
1909
2007
  this._theadMousedownHandler = null;
2008
+ this._theadDblclickHandler = null;
1910
2009
  // State tracking for incremental DOM patching
1911
2010
  this.lastActiveCell = null;
1912
2011
  this.lastSelectionRange = null;
@@ -2015,15 +2114,30 @@ var TableRenderer = class {
2015
2114
  }
2016
2115
  }
2017
2116
  };
2117
+ this._theadDblclickHandler = (e) => {
2118
+ const target = e.target;
2119
+ if (target.classList.contains("ogrid-resize-handle")) {
2120
+ e.stopPropagation();
2121
+ const th = target.closest("th[data-column-id]");
2122
+ if (!th) return;
2123
+ const columnId = th.getAttribute("data-column-id");
2124
+ if (columnId) {
2125
+ this.interactionState?.onResizeDoubleClick?.(columnId);
2126
+ }
2127
+ }
2128
+ };
2018
2129
  if (this._theadClickHandler) this.thead.addEventListener("click", this._theadClickHandler);
2019
2130
  this.thead.addEventListener("mousedown", this._theadMousedownHandler);
2131
+ this.thead.addEventListener("dblclick", this._theadDblclickHandler);
2020
2132
  }
2021
2133
  detachHeaderDelegation() {
2022
2134
  if (!this.thead) return;
2023
2135
  if (this._theadClickHandler) this.thead.removeEventListener("click", this._theadClickHandler);
2024
2136
  if (this._theadMousedownHandler) this.thead.removeEventListener("mousedown", this._theadMousedownHandler);
2137
+ if (this._theadDblclickHandler) this.thead.removeEventListener("dblclick", this._theadDblclickHandler);
2025
2138
  this._theadClickHandler = null;
2026
2139
  this._theadMousedownHandler = null;
2140
+ this._theadDblclickHandler = null;
2027
2141
  }
2028
2142
  getWrapperElement() {
2029
2143
  return this.wrapperEl;
@@ -2140,6 +2254,13 @@ var TableRenderer = class {
2140
2254
  const lastSelection = this.lastSelectionRange;
2141
2255
  const lastCopy = this.lastCopyRange;
2142
2256
  const lastCut = this.lastCutRange;
2257
+ const selBounds = selectionRange ? rangeBounds(selectionRange) : null;
2258
+ const lastSelBounds = lastSelection ? rangeBounds(lastSelection) : null;
2259
+ const copyBounds = copyRange ? rangeBounds(copyRange) : null;
2260
+ const lastCopyBounds = lastCopy ? rangeBounds(lastCopy) : null;
2261
+ const cutBounds = cutRange ? rangeBounds(cutRange) : null;
2262
+ const lastCutBounds = lastCut ? rangeBounds(lastCut) : null;
2263
+ const colOffset = this.getColOffset();
2143
2264
  const cells = this.tbody.querySelectorAll("td[data-row-index][data-col-index]");
2144
2265
  for (let i = 0; i < cells.length; i++) {
2145
2266
  const el = cells[i];
@@ -2147,7 +2268,6 @@ var TableRenderer = class {
2147
2268
  if (!coords) continue;
2148
2269
  const rowIndex = coords.rowIndex;
2149
2270
  const globalColIndex = coords.colIndex;
2150
- const colOffset = this.getColOffset();
2151
2271
  const colIndex = globalColIndex - colOffset;
2152
2272
  const wasActive = lastActive && lastActive.rowIndex === rowIndex && lastActive.columnIndex === globalColIndex;
2153
2273
  const isActive = activeCell && activeCell.rowIndex === rowIndex && activeCell.columnIndex === globalColIndex;
@@ -2158,28 +2278,30 @@ var TableRenderer = class {
2158
2278
  el.setAttribute("data-active-cell", "true");
2159
2279
  el.style.outline = "2px solid var(--ogrid-accent, #0078d4)";
2160
2280
  }
2161
- const wasInRange = lastSelection && isInSelectionRange(lastSelection, rowIndex, colIndex);
2162
- const isInRange = selectionRange && isInSelectionRange(selectionRange, rowIndex, colIndex);
2163
- if (wasInRange && !isInRange) {
2281
+ const wasInRange = lastSelBounds && inBounds(lastSelBounds, rowIndex, colIndex);
2282
+ const isInRange = selBounds && inBounds(selBounds, rowIndex, colIndex);
2283
+ const showRange = isInRange && !isActive;
2284
+ const showedRange = wasInRange && !(lastActive && lastActive.rowIndex === rowIndex && lastActive.columnIndex === globalColIndex);
2285
+ if (showedRange && !showRange) {
2164
2286
  el.removeAttribute("data-in-range");
2165
2287
  el.style.backgroundColor = "";
2166
- } else if (isInRange && !wasInRange) {
2288
+ } else if (showRange && !showedRange) {
2167
2289
  el.setAttribute("data-in-range", "true");
2168
2290
  el.style.backgroundColor = "var(--ogrid-range-bg, rgba(33, 115, 70, 0.12))";
2169
2291
  }
2170
- const wasInCopy = lastCopy && isInSelectionRange(lastCopy, rowIndex, colIndex);
2171
- const isInCopy = copyRange && isInSelectionRange(copyRange, rowIndex, colIndex);
2292
+ const wasInCopy = lastCopyBounds && inBounds(lastCopyBounds, rowIndex, colIndex);
2293
+ const isInCopy = copyBounds && inBounds(copyBounds, rowIndex, colIndex);
2172
2294
  if (wasInCopy && !isInCopy) {
2173
- if (!isActive && !(cutRange && isInSelectionRange(cutRange, rowIndex, colIndex))) {
2295
+ if (!isActive && !(cutBounds && inBounds(cutBounds, rowIndex, colIndex))) {
2174
2296
  el.style.outline = "";
2175
2297
  }
2176
2298
  } else if (isInCopy && !wasInCopy) {
2177
2299
  el.style.outline = "1px dashed var(--ogrid-fg-muted, rgba(0, 0, 0, 0.5))";
2178
2300
  }
2179
- const wasInCut = lastCut && isInSelectionRange(lastCut, rowIndex, colIndex);
2180
- const isInCut = cutRange && isInSelectionRange(cutRange, rowIndex, colIndex);
2301
+ const wasInCut = lastCutBounds && inBounds(lastCutBounds, rowIndex, colIndex);
2302
+ const isInCut = cutBounds && inBounds(cutBounds, rowIndex, colIndex);
2181
2303
  if (wasInCut && !isInCut) {
2182
- if (!isActive && !(copyRange && isInSelectionRange(copyRange, rowIndex, colIndex))) {
2304
+ if (!isActive && !(copyBounds && inBounds(copyBounds, rowIndex, colIndex))) {
2183
2305
  el.style.outline = "";
2184
2306
  }
2185
2307
  } else if (isInCut && !wasInCut) {
@@ -3902,6 +4024,7 @@ var VirtualScrollState = class {
3902
4024
  this._totalRows = 0;
3903
4025
  this.rafId = 0;
3904
4026
  this._ro = null;
4027
+ this._resizeRafId = 0;
3905
4028
  this._cachedRange = { startIndex: 0, endIndex: -1, offsetTop: 0, offsetBottom: 0 };
3906
4029
  this._config = config ?? { enabled: false };
3907
4030
  validateVirtualScrollConfig(this._config);
@@ -3965,11 +4088,13 @@ var VirtualScrollState = class {
3965
4088
  this.disconnectObserver();
3966
4089
  if (typeof ResizeObserver !== "undefined") {
3967
4090
  this._ro = new ResizeObserver((entries) => {
3968
- for (const entry of entries) {
3969
- const rect = entry.contentRect;
3970
- this._containerHeight = rect.height;
4091
+ if (entries.length === 0) return;
4092
+ this._containerHeight = entries[0].contentRect.height;
4093
+ if (this._resizeRafId) cancelAnimationFrame(this._resizeRafId);
4094
+ this._resizeRafId = requestAnimationFrame(() => {
4095
+ this._resizeRafId = 0;
3971
4096
  this.recompute();
3972
- }
4097
+ });
3973
4098
  });
3974
4099
  this._ro.observe(el);
3975
4100
  }
@@ -4011,6 +4136,7 @@ var VirtualScrollState = class {
4011
4136
  }
4012
4137
  destroy() {
4013
4138
  if (this.rafId) cancelAnimationFrame(this.rafId);
4139
+ if (this._resizeRafId) cancelAnimationFrame(this._resizeRafId);
4014
4140
  this.disconnectObserver();
4015
4141
  this.emitter.removeAllListeners();
4016
4142
  }
@@ -4547,6 +4673,11 @@ var ColumnResizeState = class {
4547
4673
  this.isResizing = false;
4548
4674
  this.resizeColumnId = null;
4549
4675
  }
4676
+ /** Set a column width directly (used by double-click auto-fit). */
4677
+ setColumnWidth(columnId, widthPx) {
4678
+ this.columnWidths.set(columnId, widthPx);
4679
+ this.emitter.emit("columnWidthChange", { columnId, widthPx });
4680
+ }
4550
4681
  onColumnWidthChange(handler) {
4551
4682
  this.emitter.on("columnWidthChange", handler);
4552
4683
  return () => this.emitter.off("columnWidthChange", handler);
@@ -4652,7 +4783,7 @@ var FillHandleState = class {
4652
4783
  endCol: end.endCol
4653
4784
  });
4654
4785
  this.setSelectionRange(norm);
4655
- this.setActiveCell({ rowIndex: end.endRow, columnIndex: end.endCol + this.params.colOffset });
4786
+ this.setActiveCell({ rowIndex: start.startRow, columnIndex: start.startCol + this.params.colOffset });
4656
4787
  this.applyFillValuesFromCore(norm, start);
4657
4788
  this._isFillDragging = false;
4658
4789
  this.fillDragStart = null;
@@ -4904,7 +5035,8 @@ var MarchingAntsOverlay = class {
4904
5035
  const clipRange = this.copyRange ?? this.cutRange;
4905
5036
  const selRect = this.selectionRange ? measureRange2(this.container, this.selectionRange, this.colOffset) : null;
4906
5037
  const clipRangeMatchesSel = this.selectionRange != null && clipRange != null && rangesEqual(this.selectionRange, clipRange);
4907
- if (selRect && !clipRangeMatchesSel) {
5038
+ const isSingleCell = this.selectionRange != null && this.selectionRange.startRow === this.selectionRange.endRow && this.selectionRange.startCol === this.selectionRange.endCol;
5039
+ if (selRect && !clipRangeMatchesSel && !isSingleCell) {
4908
5040
  if (!this.selSvg) {
4909
5041
  this.selSvg = this.createSvg(4);
4910
5042
  this.container.appendChild(this.selSvg);
@@ -5766,6 +5898,13 @@ var OGridRendering = class {
5766
5898
  if (ce.event) this.ctx.handleCellContextMenu(ce.rowIndex, ce.colIndex, ce.event);
5767
5899
  },
5768
5900
  onResizeStart: renderer.getOnResizeStart(),
5901
+ onResizeDoubleClick: (columnId) => {
5902
+ const col = visibleCols.find((c) => c.columnId === columnId);
5903
+ const minW = col?.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
5904
+ const container = renderer.getTableElement()?.parentElement ?? void 0;
5905
+ const idealWidth = measureColumnContentWidth(columnId, minW, container);
5906
+ resizeState.setColumnWidth(columnId, idealWidth);
5907
+ },
5769
5908
  // Fill handle
5770
5909
  onFillHandleMouseDown: options.editable !== false ? (e) => fillHandleState?.startFillDrag(e) : void 0,
5771
5910
  // Row selection
@@ -199,6 +199,7 @@
199
199
  position: relative;
200
200
  min-width: fit-content;
201
201
  background: var(--ogrid-bg, #fff);
202
+ overflow-x: clip;
202
203
  }
203
204
 
204
205
  .ogrid-table {
@@ -493,10 +494,11 @@
493
494
  padding: 6px 12px;
494
495
  box-sizing: border-box;
495
496
  font-size: 12px;
497
+ line-height: 20px;
496
498
  color: var(--ogrid-muted, #616161);
497
499
  background: var(--ogrid-bg-subtle, #f3f2f1);
498
500
  border-top: 1px solid var(--ogrid-border, #e0e0e0);
499
- min-height: 28px;
501
+ min-height: 33px;
500
502
  }
501
503
 
502
504
  .ogrid-status-part {
@@ -18,6 +18,7 @@ export interface TableRendererInteractionState {
18
18
  onCellDoubleClick?: (cellEvent: CellEvent) => void;
19
19
  onCellContextMenu?: (cellEvent: CellEvent) => void;
20
20
  onResizeStart?: (columnId: string, clientX: number, currentWidth: number) => void;
21
+ onResizeDoubleClick?: (columnId: string) => void;
21
22
  onFillHandleMouseDown?: (e: MouseEvent) => void;
22
23
  rowSelectionMode?: 'single' | 'multiple' | 'none';
23
24
  selectedRowIds?: Set<RowId>;
@@ -50,6 +51,7 @@ export declare class TableRenderer<T> {
50
51
  private _tbodyContextmenuHandler;
51
52
  private _theadClickHandler;
52
53
  private _theadMousedownHandler;
54
+ private _theadDblclickHandler;
53
55
  private lastActiveCell;
54
56
  private lastSelectionRange;
55
57
  private lastCopyRange;
@@ -17,6 +17,8 @@ export declare class ColumnResizeState {
17
17
  startResize(columnId: string, clientX: number, currentWidth: number): void;
18
18
  updateResize(clientX: number): number | null;
19
19
  endResize(clientX: number): void;
20
+ /** Set a column width directly (used by double-click auto-fit). */
21
+ setColumnWidth(columnId: string, widthPx: number): void;
20
22
  onColumnWidthChange(handler: (data: ColumnResizeStateEvents['columnWidthChange']) => void): () => void;
21
23
  destroy(): void;
22
24
  }
@@ -19,6 +19,7 @@ export declare class VirtualScrollState {
19
19
  private _totalRows;
20
20
  private rafId;
21
21
  private _ro;
22
+ private _resizeRafId;
22
23
  private _cachedRange;
23
24
  constructor(config?: IVirtualScrollConfig);
24
25
  /** Whether virtual scrolling is active (enabled + meets the row threshold). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-js",
3
- "version": "2.1.13",
3
+ "version": "2.1.15",
4
4
  "description": "OGrid vanilla JS – framework-free data grid with sorting, filtering, pagination, and spreadsheet-style editing.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -36,7 +36,7 @@
36
36
  "node": ">=18"
37
37
  },
38
38
  "dependencies": {
39
- "@alaarab/ogrid-core": "2.1.13"
39
+ "@alaarab/ogrid-core": "2.1.15"
40
40
  },
41
41
  "sideEffects": [
42
42
  "**/*.css"