@alaarab/ogrid-js 2.1.9 → 2.1.11

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
@@ -561,20 +561,31 @@ function processClientSideData(data, columns, filters, sortBy, sortDirection) {
561
561
  if (Number.isNaN(bt)) return 1 * dir;
562
562
  return at === bt ? 0 : at > bt ? dir : -dir;
563
563
  });
564
- } else {
564
+ } else if (!compare) {
565
+ const keyCache = /* @__PURE__ */ new Map();
566
+ for (let i = 0; i < sortable.length; i++) {
567
+ const row = sortable[i];
568
+ const v = sortCol ? getCellValue(row, sortCol) : row[sortBy];
569
+ if (v == null) {
570
+ keyCache.set(row, void 0);
571
+ } else if (typeof v === "number") {
572
+ keyCache.set(row, v);
573
+ } else {
574
+ keyCache.set(row, String(v).toLowerCase());
575
+ }
576
+ }
565
577
  sortable.sort((a, b) => {
566
- if (compare) return compare(a, b) * dir;
567
- const av = sortCol ? getCellValue(a, sortCol) : a[sortBy];
568
- const bv = sortCol ? getCellValue(b, sortCol) : b[sortBy];
569
- if (av == null && bv == null) return 0;
570
- if (av == null) return -1 * dir;
571
- if (bv == null) return 1 * dir;
578
+ const av = keyCache.get(a);
579
+ const bv = keyCache.get(b);
580
+ if (av === void 0 && bv === void 0) return 0;
581
+ if (av === void 0) return -1 * dir;
582
+ if (bv === void 0) return 1 * dir;
572
583
  if (typeof av === "number" && typeof bv === "number")
573
584
  return av === bv ? 0 : av > bv ? dir : -dir;
574
- const as = String(av).toLowerCase();
575
- const bs = String(bv).toLowerCase();
576
- return as === bs ? 0 : as > bs ? dir : -dir;
585
+ return av === bv ? 0 : av > bv ? dir : -dir;
577
586
  });
587
+ } else {
588
+ sortable.sort((a, b) => compare(a, b) * dir);
578
589
  }
579
590
  return sortable;
580
591
  }
@@ -757,7 +768,77 @@ function getHeaderFilterConfig(col, input) {
757
768
  }
758
769
  return base;
759
770
  }
760
- function getCellRenderDescriptor(item, col, rowIndex, colIdx, input) {
771
+ var _CellDescriptorCache = class _CellDescriptorCache2 {
772
+ constructor() {
773
+ this.cache = /* @__PURE__ */ new Map();
774
+ this.lastVersion = "";
775
+ }
776
+ /**
777
+ * Compute a version string from the volatile parts of CellRenderDescriptorInput.
778
+ * This string changes whenever any input that affects per-cell output changes.
779
+ * Cheap to compute (simple string concat) — O(1) regardless of grid size.
780
+ */
781
+ static computeVersion(input) {
782
+ const ec = input.editingCell;
783
+ const ac = input.activeCell;
784
+ const sr = input.selectionRange;
785
+ const cr = input.cutRange;
786
+ const cp = input.copyRange;
787
+ return (ec ? `${String(ec.rowId)}\0${ec.columnId}` : "") + "" + (ac ? `${ac.rowIndex}\0${ac.columnIndex}` : "") + "" + (sr ? `${sr.startRow}\0${sr.startCol}\0${sr.endRow}\0${sr.endCol}` : "") + "" + (cr ? `${cr.startRow}\0${cr.startCol}\0${cr.endRow}\0${cr.endCol}` : "") + "" + (cp ? `${cp.startRow}\0${cp.startCol}\0${cp.endRow}\0${cp.endCol}` : "") + "" + (input.isDragging ? "1" : "0") + "" + (input.editable !== false ? "1" : "0") + "" + (input.onCellValueChanged ? "1" : "0");
788
+ }
789
+ /**
790
+ * Get a cached descriptor or compute a new one.
791
+ *
792
+ * @param rowIndex - Row index in the dataset.
793
+ * @param colIdx - Column index within the visible columns.
794
+ * @param version - Volatile version string (from CellDescriptorCache.computeVersion).
795
+ * @param compute - Factory function called on cache miss.
796
+ * @returns The descriptor (cached or freshly computed).
797
+ */
798
+ get(rowIndex, colIdx, version, compute) {
799
+ const key = rowIndex * _CellDescriptorCache2.MAX_COL_STRIDE + colIdx;
800
+ const entry = this.cache.get(key);
801
+ if (entry !== void 0 && entry.version === version) {
802
+ return entry.descriptor;
803
+ }
804
+ const descriptor = compute();
805
+ this.cache.set(key, { version, descriptor });
806
+ return descriptor;
807
+ }
808
+ /**
809
+ * Update the last-seen version and return it.
810
+ * Call once per render pass to track whether any volatile state changed.
811
+ * If the version is unchanged from last render, the entire render is a no-op for all cells.
812
+ */
813
+ updateVersion(version) {
814
+ this.lastVersion = version;
815
+ }
816
+ /** The last version string set via updateVersion(). */
817
+ get currentVersion() {
818
+ return this.lastVersion;
819
+ }
820
+ /**
821
+ * Clear all cached entries. Call when the grid's data changes (new items array,
822
+ * different column count, etc.) to prevent stale cell values from being served.
823
+ */
824
+ clear() {
825
+ this.cache.clear();
826
+ }
827
+ };
828
+ _CellDescriptorCache.MAX_COL_STRIDE = 1024;
829
+ var CellDescriptorCache = _CellDescriptorCache;
830
+ function getCellRenderDescriptor(item, col, rowIndex, colIdx, input, cache) {
831
+ if (cache !== void 0) {
832
+ return cache.get(
833
+ rowIndex,
834
+ colIdx,
835
+ cache.currentVersion,
836
+ () => computeCellDescriptor(item, col, rowIndex, colIdx, input)
837
+ );
838
+ }
839
+ return computeCellDescriptor(item, col, rowIndex, colIdx, input);
840
+ }
841
+ function computeCellDescriptor(item, col, rowIndex, colIdx, input) {
761
842
  const rowId = input.getRowId(item);
762
843
  const globalColIndex = colIdx + input.colOffset;
763
844
  const colEditable = isColumnEditable(col, item);
@@ -1332,6 +1413,7 @@ function validateColumns(columns) {
1332
1413
  console.warn("[OGrid] columns prop is empty or not an array");
1333
1414
  return;
1334
1415
  }
1416
+ const isDev = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
1335
1417
  const ids = /* @__PURE__ */ new Set();
1336
1418
  for (const col of columns) {
1337
1419
  if (!col.columnId) {
@@ -1341,10 +1423,25 @@ function validateColumns(columns) {
1341
1423
  console.warn(`[OGrid] Duplicate columnId: "${col.columnId}"`);
1342
1424
  }
1343
1425
  ids.add(col.columnId);
1426
+ if (isDev && col.editable === true && col.cellEditor == null) {
1427
+ console.warn(
1428
+ `[OGrid] Column "${col.columnId}" has editable=true but no cellEditor defined. Cells will not open an editor on double-click. Set cellEditor to 'text', 'select', 'checkbox', 'date', or a custom component.`
1429
+ );
1430
+ }
1431
+ }
1432
+ }
1433
+ function validateVirtualScrollConfig(config) {
1434
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
1435
+ if (config.enabled !== true) return;
1436
+ if (!config.rowHeight || config.rowHeight <= 0) {
1437
+ console.warn(
1438
+ "[OGrid] virtualScroll.enabled is true but rowHeight is missing or <= 0. Set a positive rowHeight (e.g. virtualScroll: { enabled: true, rowHeight: 36 }) for correct virtual scrolling behavior."
1439
+ );
1344
1440
  }
1345
1441
  }
1346
1442
  function validateRowIds(items, getRowId) {
1347
1443
  if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
1444
+ if (!getRowId) return;
1348
1445
  const ids = /* @__PURE__ */ new Set();
1349
1446
  const limit = Math.min(items.length, 100);
1350
1447
  for (let i = 0; i < limit; i++) {
@@ -1366,16 +1463,44 @@ var DEFAULT_DEBOUNCE_MS = 300;
1366
1463
  var PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
1367
1464
  var SIDEBAR_TRANSITION_MS = 300;
1368
1465
  var Z_INDEX = {
1466
+ /** Column resize drag handle */
1467
+ RESIZE_HANDLE: 1,
1468
+ /** Active/editing cell outline */
1469
+ ACTIVE_CELL: 2,
1470
+ /** Fill handle dot */
1471
+ FILL_HANDLE: 3,
1369
1472
  /** Selection range overlay (marching ants) */
1370
1473
  SELECTION_OVERLAY: 4,
1474
+ /** Row number column */
1475
+ ROW_NUMBER: 5,
1371
1476
  /** Clipboard overlay (copy/cut animation) */
1372
1477
  CLIPBOARD_OVERLAY: 5,
1478
+ /** Sticky pinned body cells */
1479
+ PINNED: 6,
1480
+ /** Selection checkbox column in body */
1481
+ SELECTION_CELL: 7,
1482
+ /** Sticky thead row */
1483
+ THEAD: 8,
1484
+ /** Pinned header cells (sticky both axes) */
1485
+ PINNED_HEADER: 10,
1486
+ /** Focused header cell */
1487
+ HEADER_FOCUS: 11,
1488
+ /** Checkbox column in sticky header (sticky both axes) */
1489
+ SELECTION_HEADER_PINNED: 12,
1490
+ /** Loading overlay within table */
1491
+ LOADING: 2,
1492
+ /** Column reorder drop indicator */
1493
+ DROP_INDICATOR: 100,
1373
1494
  /** Dropdown menus (column chooser, pagination size select) */
1374
1495
  DROPDOWN: 1e3,
1496
+ /** Filter popovers */
1497
+ FILTER_POPOVER: 1e3,
1375
1498
  /** Modal dialogs */
1376
1499
  MODAL: 2e3,
1500
+ /** Fullscreen grid container */
1501
+ FULLSCREEN: 9999,
1377
1502
  /** Context menus (right-click grid menu) */
1378
- CONTEXT_MENU: 9999
1503
+ CONTEXT_MENU: 1e4
1379
1504
  };
1380
1505
 
1381
1506
  // src/utils/getCellCoordinates.ts
@@ -1908,18 +2033,18 @@ var TableRenderer = class {
1908
2033
  this.container.innerHTML = "";
1909
2034
  const wrapper = document.createElement("div");
1910
2035
  wrapper.className = "ogrid-wrapper";
1911
- wrapper.setAttribute("role", "grid");
2036
+ wrapper.setAttribute("role", "region");
1912
2037
  wrapper.setAttribute("tabindex", "0");
1913
2038
  wrapper.style.position = "relative";
1914
2039
  if (this.state.rowHeight) {
1915
2040
  wrapper.style.setProperty("--ogrid-row-height", `${this.state.rowHeight}px`);
1916
2041
  }
1917
- if (this.state.ariaLabel) {
1918
- wrapper.setAttribute("aria-label", this.state.ariaLabel);
1919
- }
2042
+ const label = this.state.ariaLabel ?? "Data grid";
2043
+ wrapper.setAttribute("aria-label", label);
1920
2044
  this.wrapperEl = wrapper;
1921
2045
  this.table = document.createElement("table");
1922
2046
  this.table.className = "ogrid-table";
2047
+ this.table.setAttribute("role", "grid");
1923
2048
  this.thead = document.createElement("thead");
1924
2049
  if (this.state.stickyHeader) {
1925
2050
  this.thead.classList.add("ogrid-sticky-header");
@@ -2174,6 +2299,11 @@ var TableRenderer = class {
2174
2299
  }
2175
2300
  if (!cell.isGroup && cell.columnDef) {
2176
2301
  th.setAttribute("data-column-id", cell.columnDef.columnId);
2302
+ th.setAttribute("scope", "col");
2303
+ const groupSort = this.state.sort;
2304
+ if (groupSort?.field === cell.columnDef.columnId) {
2305
+ th.setAttribute("aria-sort", groupSort.direction === "asc" ? "ascending" : "descending");
2306
+ }
2177
2307
  this.applyPinningStyles(th, cell.columnDef.columnId, true);
2178
2308
  }
2179
2309
  tr.appendChild(th);
@@ -2202,6 +2332,11 @@ var TableRenderer = class {
2202
2332
  const th = document.createElement("th");
2203
2333
  th.className = "ogrid-header-cell";
2204
2334
  th.setAttribute("data-column-id", col.columnId);
2335
+ th.setAttribute("scope", "col");
2336
+ const sort = this.state.sort;
2337
+ if (sort?.field === col.columnId) {
2338
+ th.setAttribute("aria-sort", sort.direction === "asc" ? "ascending" : "descending");
2339
+ }
2205
2340
  const textSpan = document.createElement("span");
2206
2341
  textSpan.textContent = col.name;
2207
2342
  th.appendChild(textSpan);
@@ -2232,6 +2367,8 @@ var TableRenderer = class {
2232
2367
  const filterBtn = document.createElement("button");
2233
2368
  filterBtn.className = "ogrid-filter-icon";
2234
2369
  filterBtn.setAttribute("aria-label", `Filter ${col.name}`);
2370
+ filterBtn.setAttribute("aria-expanded", "false");
2371
+ filterBtn.setAttribute("aria-haspopup", "dialog");
2235
2372
  filterBtn.style.border = "none";
2236
2373
  filterBtn.style.background = "transparent";
2237
2374
  filterBtn.style.cursor = "pointer";
@@ -2322,6 +2459,7 @@ var TableRenderer = class {
2322
2459
  const isRowSelected = this.interactionState?.selectedRowIds?.has(rowId) === true;
2323
2460
  if (isRowSelected) {
2324
2461
  tr.setAttribute("data-row-selected", "true");
2462
+ tr.setAttribute("aria-selected", "true");
2325
2463
  }
2326
2464
  if (hasCheckbox) {
2327
2465
  const td = document.createElement("td");
@@ -3755,6 +3893,7 @@ var ColumnPinningState = class {
3755
3893
  // src/state/VirtualScrollState.ts
3756
3894
  var DEFAULT_ROW_HEIGHT = 36;
3757
3895
  var DEFAULT_OVERSCAN = 5;
3896
+ var DEFAULT_PASSTHROUGH_THRESHOLD = 100;
3758
3897
  var VirtualScrollState = class {
3759
3898
  constructor(config) {
3760
3899
  this.emitter = new EventEmitter();
@@ -3765,10 +3904,12 @@ var VirtualScrollState = class {
3765
3904
  this._ro = null;
3766
3905
  this._cachedRange = { startIndex: 0, endIndex: -1, offsetTop: 0, offsetBottom: 0 };
3767
3906
  this._config = config ?? { enabled: false };
3907
+ validateVirtualScrollConfig(this._config);
3768
3908
  }
3769
- /** Whether virtual scrolling is active. */
3909
+ /** Whether virtual scrolling is active (enabled + meets the row threshold). */
3770
3910
  get enabled() {
3771
- return this._config.enabled === true && this._totalRows > 0;
3911
+ const threshold = this._config.threshold ?? DEFAULT_PASSTHROUGH_THRESHOLD;
3912
+ return this._config.enabled === true && this._totalRows >= threshold;
3772
3913
  }
3773
3914
  get config() {
3774
3915
  return this._config;
@@ -3809,6 +3950,7 @@ var VirtualScrollState = class {
3809
3950
  }
3810
3951
  /** Update the virtual scroll configuration. */
3811
3952
  updateConfig(config) {
3953
+ validateVirtualScrollConfig(config);
3812
3954
  this._config = config;
3813
3955
  this.recompute();
3814
3956
  this.emitter.emit("configChanged", { config });
@@ -3982,7 +4124,11 @@ var KeyboardNavState = class {
3982
4124
  constructor(params, getActiveCell, getSelectionRange, setActiveCell, setSelectionRange) {
3983
4125
  this.wrapperRef = null;
3984
4126
  this.handleKeyDown = (e) => {
3985
- const { items, visibleCols, colOffset, editable, onCellValueChanged, onCopy, onCut, onPaste, onUndo, onRedo, onContextMenu, onStartEdit, getRowId, clearClipboardRanges } = this.params;
4127
+ const { items, visibleCols, colOffset, editable, onCellValueChanged, onCopy, onCut, onPaste, onUndo, onRedo, onContextMenu, onStartEdit, getRowId, clearClipboardRanges, onKeyDown, onFillDown } = this.params;
4128
+ if (onKeyDown) {
4129
+ onKeyDown(e);
4130
+ if (e.defaultPrevented) return;
4131
+ }
3986
4132
  const activeCell = this.getActiveCell();
3987
4133
  const selectionRange = this.getSelectionRange();
3988
4134
  const maxRowIndex = items.length - 1;
@@ -4022,6 +4168,14 @@ var KeyboardNavState = class {
4022
4168
  void onPaste?.();
4023
4169
  }
4024
4170
  break;
4171
+ case "d":
4172
+ if (e.ctrlKey || e.metaKey) {
4173
+ if (editable !== false && onFillDown) {
4174
+ e.preventDefault();
4175
+ onFillDown();
4176
+ }
4177
+ }
4178
+ break;
4025
4179
  case "ArrowDown":
4026
4180
  case "ArrowUp":
4027
4181
  case "ArrowRight":
@@ -4433,6 +4587,13 @@ var FillHandleState = class {
4433
4587
  updateParams(params) {
4434
4588
  this.params = params;
4435
4589
  }
4590
+ /** Fill the current selection down from the top row (keyboard Ctrl+D). No-op if no selection or editable=false. */
4591
+ fillDown() {
4592
+ const range = this.getSelectionRange();
4593
+ if (!range || this.params.editable === false || !this.params.onCellValueChanged) return;
4594
+ const norm = normalizeSelectionRange(range);
4595
+ this.applyFillValuesFromCore(norm, { startRow: norm.startRow, startCol: norm.startCol });
4596
+ }
4436
4597
  /** Called when the fill handle square is mousedown'd. */
4437
4598
  startFillDrag(e) {
4438
4599
  e.preventDefault();
@@ -5706,7 +5867,9 @@ var OGridRendering = class {
5706
5867
  onRedo: () => undoRedoState?.redo(),
5707
5868
  onContextMenu: (x, y) => this.ctx.showContextMenu(x, y),
5708
5869
  onStartEdit: (rowId, columnId) => this.ctx.startCellEdit(rowId, columnId),
5709
- clearClipboardRanges: () => clipboardState?.clearClipboardRanges()
5870
+ clearClipboardRanges: () => clipboardState?.clearClipboardRanges(),
5871
+ onKeyDown: options.onKeyDown,
5872
+ onFillDown: fillHandleState ? () => fillHandleState.fillDown() : void 0
5710
5873
  });
5711
5874
  clipboardState.updateParams({
5712
5875
  items,
@@ -5738,6 +5901,11 @@ var OGridRendering = class {
5738
5901
  renderHeaderFilterPopover() {
5739
5902
  const { headerFilterState, headerFilterComponent, filterConfigs } = this.ctx;
5740
5903
  const openId = headerFilterState.openColumnId;
5904
+ const allBtns = this.ctx.tableContainer.querySelectorAll(".ogrid-filter-icon[aria-haspopup]");
5905
+ for (const btn of allBtns) {
5906
+ const colId = btn.closest("th[data-column-id]")?.getAttribute("data-column-id");
5907
+ btn.setAttribute("aria-expanded", colId === openId ? "true" : "false");
5908
+ }
5741
5909
  if (!openId) {
5742
5910
  headerFilterComponent.cleanup();
5743
5911
  return;
@@ -6372,4 +6540,4 @@ var OGrid = class {
6372
6540
  }
6373
6541
  };
6374
6542
 
6375
- export { AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, COLUMN_HEADER_MENU_ITEMS, ClipboardState, ColumnChooser, ColumnPinningState, ColumnReorderState, ColumnResizeState, ContextMenu, DEFAULT_DEBOUNCE_MS, DEFAULT_MIN_COLUMN_WIDTH, EventEmitter, FillHandleState, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GridState, HeaderFilter, HeaderFilterState, InlineCellEditor, KeyboardNavState, MAX_PAGE_BUTTONS, MarchingAntsOverlay, OGrid, OGridEventWiring, OGridRendering, PAGE_SIZE_OPTIONS, PEOPLE_SEARCH_DEBOUNCE_MS, PaginationControls, ROW_NUMBER_COLUMN_WIDTH, RowSelectionState, SIDEBAR_TRANSITION_MS, SelectionState, SideBar, SideBarState, StatusBar, TableLayoutState, TableRenderer, UndoRedoStack, UndoRedoState, VirtualScrollState, Z_INDEX, applyCellDeletion, applyCutClear, applyFillValues, applyPastedValues, applyRangeRowSelection, areGridRowPropsEqual, booleanParser, buildCellIndex, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps, buildPopoverEditorProps, calculateDropTarget, clampSelectionToBounds, computeAggregations, computeArrowNavigation, computeAutoScrollSpeed, computeNextSortState, computeRowSelectionState, computeTabNavigation, computeTotalHeight, computeVisibleRange, currencyParser, dateParser, debounce, deriveFilterOptionsFromData, emailParser, escapeCsvValue, exportToCsv, findCtrlArrowTarget, flattenColumns, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, getCellRenderDescriptor, getCellValue, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getPinStateForColumn, getScrollTopForRow, getStatusBarParts, injectGlobalStyles, isColumnEditable, isFilterConfig, isInSelectionRange, isRowInRange, measureColumnContentWidth, measureRange, mergeFilter, normalizeSelectionRange, numberParser, parseTsvClipboard, parseValue, processClientSideData, rangesEqual, reorderColumnArray, resolveCellDisplayContent, resolveCellStyle, toUserLike, triggerCsvDownload, validateColumns, validateRowIds };
6543
+ export { AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, COLUMN_HEADER_MENU_ITEMS, CellDescriptorCache, ClipboardState, ColumnChooser, ColumnPinningState, ColumnReorderState, ColumnResizeState, ContextMenu, DEFAULT_DEBOUNCE_MS, DEFAULT_MIN_COLUMN_WIDTH, EventEmitter, FillHandleState, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GridState, HeaderFilter, HeaderFilterState, InlineCellEditor, KeyboardNavState, MAX_PAGE_BUTTONS, MarchingAntsOverlay, OGrid, OGridEventWiring, OGridRendering, PAGE_SIZE_OPTIONS, PEOPLE_SEARCH_DEBOUNCE_MS, PaginationControls, ROW_NUMBER_COLUMN_WIDTH, RowSelectionState, SIDEBAR_TRANSITION_MS, SelectionState, SideBar, SideBarState, StatusBar, TableLayoutState, TableRenderer, UndoRedoStack, UndoRedoState, VirtualScrollState, Z_INDEX, applyCellDeletion, applyCutClear, applyFillValues, applyPastedValues, applyRangeRowSelection, areGridRowPropsEqual, booleanParser, buildCellIndex, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps, buildPopoverEditorProps, calculateDropTarget, clampSelectionToBounds, computeAggregations, computeArrowNavigation, computeAutoScrollSpeed, computeNextSortState, computeRowSelectionState, computeTabNavigation, computeTotalHeight, computeVisibleRange, currencyParser, dateParser, debounce, deriveFilterOptionsFromData, emailParser, escapeCsvValue, exportToCsv, findCtrlArrowTarget, flattenColumns, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, getCellRenderDescriptor, getCellValue, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getPinStateForColumn, getScrollTopForRow, getStatusBarParts, injectGlobalStyles, isColumnEditable, isFilterConfig, isInSelectionRange, isRowInRange, measureColumnContentWidth, measureRange, mergeFilter, normalizeSelectionRange, numberParser, parseTsvClipboard, parseValue, processClientSideData, rangesEqual, reorderColumnArray, resolveCellDisplayContent, resolveCellStyle, toUserLike, triggerCsvDownload, validateColumns, validateRowIds, validateVirtualScrollConfig };
@@ -6,6 +6,13 @@
6
6
 
7
7
  /* ── Light Theme (default) — :where() for zero specificity ── */
8
8
  :where(:root) {
9
+ /* Cell padding — override for row density:
10
+ --ogrid-cell-padding : shorthand (default 6px 10px)
11
+ --ogrid-cell-padding-vertical : vertical only (default 6px)
12
+ --ogrid-cell-padding-horizontal: horizontal only (default 10px) */
13
+ --ogrid-cell-padding: 6px 10px;
14
+ --ogrid-cell-padding-vertical: 6px;
15
+ --ogrid-cell-padding-horizontal: 10px;
9
16
  --ogrid-bg: #ffffff;
10
17
  --ogrid-fg: rgba(0, 0, 0, 0.87);
11
18
  --ogrid-fg-secondary: rgba(0, 0, 0, 0.6);
@@ -132,7 +139,7 @@
132
139
  .ogrid-container.ogrid-fullscreen {
133
140
  position: fixed;
134
141
  inset: 0;
135
- z-index: 9999;
142
+ z-index: var(--ogrid-z-fullscreen, 9999);
136
143
  border-radius: 0;
137
144
  border: none;
138
145
  }
@@ -224,7 +231,7 @@
224
231
 
225
232
  .ogrid-table thead {
226
233
  background: var(--ogrid-bg-subtle, #f3f2f1);
227
- z-index: 8;
234
+ z-index: var(--ogrid-z-thead, 8);
228
235
  }
229
236
 
230
237
  .ogrid-table thead.ogrid-sticky-header {
@@ -270,7 +277,7 @@
270
277
  width: 8px;
271
278
  cursor: col-resize;
272
279
  user-select: none;
273
- z-index: 1;
280
+ z-index: var(--ogrid-z-resize-handle, 1);
274
281
  }
275
282
 
276
283
  .ogrid-resize-handle::after {
@@ -316,7 +323,7 @@
316
323
  /* ── Body Rows ── */
317
324
 
318
325
  .ogrid-table tbody td {
319
- padding: 6px 10px;
326
+ padding: var(--ogrid-cell-padding, 6px 10px);
320
327
  background: var(--ogrid-bg, #ffffff);
321
328
  border-bottom: 1px solid var(--ogrid-border, #e8e8e8);
322
329
  position: relative;
@@ -345,12 +352,15 @@
345
352
  background: var(--ogrid-bg-selected-hover, #dae8f8);
346
353
  }
347
354
 
348
- .ogrid-checkbox-header,
349
- .ogrid-checkbox-cell {
355
+ /* Element-type selector (th.ogrid-checkbox-header, td.ogrid-checkbox-cell) has specificity
356
+ 0,1,1 which beats the generic .ogrid-table tbody td (0,1,2) — wait, 0,1,1 < 0,1,2.
357
+ Qualify with .ogrid-table to reach 0,2,1, beating the base td padding rule (0,1,2). */
358
+ .ogrid-table th.ogrid-checkbox-header,
359
+ .ogrid-table td.ogrid-checkbox-cell {
350
360
  width: 48px;
351
361
  min-width: 48px;
352
362
  max-width: 48px;
353
- padding: 4px !important;
363
+ padding: 4px;
354
364
  text-align: center;
355
365
  }
356
366
 
@@ -367,20 +377,22 @@
367
377
  .ogrid-cell[data-active-cell='true'] {
368
378
  outline: 2px solid var(--ogrid-selection, #217346);
369
379
  outline-offset: -1px;
370
- z-index: 2;
380
+ z-index: var(--ogrid-z-active-cell, 2);
371
381
  position: relative;
372
382
  }
373
383
 
374
- .ogrid-cell[data-in-range='true'] {
375
- background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12)) !important;
384
+ /* Cell range/drag highlights: qualify with .ogrid-container to reach specificity 0,3,0,
385
+ beating row-hover rules (.ogrid-table tbody tr:hover td = 0,2,3). */
386
+ .ogrid-container .ogrid-cell[data-in-range='true'] {
387
+ background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12));
376
388
  }
377
389
 
378
- [data-drag-range] {
379
- background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12)) !important;
390
+ .ogrid-container [data-drag-range] {
391
+ background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12));
380
392
  }
381
393
 
382
- [data-drag-anchor] {
383
- background: var(--ogrid-bg, #fff) !important;
394
+ .ogrid-container [data-drag-anchor] {
395
+ background: var(--ogrid-bg, #fff);
384
396
  }
385
397
 
386
398
  /* ── Fill Handle ── */
@@ -396,7 +408,7 @@
396
408
  border-radius: 1px;
397
409
  cursor: crosshair;
398
410
  pointer-events: auto;
399
- z-index: 3;
411
+ z-index: var(--ogrid-z-fill-handle, 3);
400
412
  }
401
413
 
402
414
  /* ── Pinned Columns ── */
@@ -404,7 +416,7 @@
404
416
  .ogrid-table th[data-pinned='left'],
405
417
  .ogrid-table td[data-pinned='left'] {
406
418
  position: sticky;
407
- z-index: 6;
419
+ z-index: var(--ogrid-z-pinned, 6);
408
420
  background: var(--ogrid-bg, #fff);
409
421
  will-change: transform;
410
422
  border-right: 1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12));
@@ -413,13 +425,13 @@
413
425
 
414
426
  .ogrid-table thead th[data-pinned='left'] {
415
427
  background: var(--ogrid-bg-subtle, #f3f2f1);
416
- z-index: 9;
428
+ z-index: var(--ogrid-z-pinned-header, 10);
417
429
  }
418
430
 
419
431
  .ogrid-table th[data-pinned='right'],
420
432
  .ogrid-table td[data-pinned='right'] {
421
433
  position: sticky;
422
- z-index: 6;
434
+ z-index: var(--ogrid-z-pinned, 6);
423
435
  background: var(--ogrid-bg, #fff);
424
436
  will-change: transform;
425
437
  border-left: 1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12));
@@ -428,7 +440,7 @@
428
440
 
429
441
  .ogrid-table thead th[data-pinned='right'] {
430
442
  background: var(--ogrid-bg-subtle, #f3f2f1);
431
- z-index: 9;
443
+ z-index: var(--ogrid-z-pinned-header, 10);
432
444
  }
433
445
 
434
446
  /* ── Empty State ── */
@@ -445,7 +457,7 @@
445
457
  .ogrid-loading-overlay {
446
458
  position: absolute;
447
459
  inset: 0;
448
- z-index: 100;
460
+ z-index: var(--ogrid-z-drop-indicator, 100);
449
461
  display: flex;
450
462
  align-items: center;
451
463
  justify-content: center;
@@ -589,10 +601,20 @@
589
601
  cursor: not-allowed;
590
602
  }
591
603
 
592
- .ogrid-pagination-active {
593
- background: var(--ogrid-primary, #217346) !important;
594
- border-color: var(--ogrid-primary, #217346) !important;
595
- color: var(--ogrid-primary-fg, #fff) !important;
604
+ /* Active pagination button: compound selector (0,2,0) beats base (0,1,0).
605
+ The :hover rule (.ogrid-pagination-btn:hover:not(:disabled), 0,3,0) still wins on hover,
606
+ but that is the correct UX — hovered active page shows hover style. */
607
+ .ogrid-pagination-btn.ogrid-pagination-active {
608
+ background: var(--ogrid-primary, #217346);
609
+ border-color: var(--ogrid-primary, #217346);
610
+ color: var(--ogrid-primary-fg, #fff);
611
+ }
612
+
613
+ /* Keep active state even when hovered */
614
+ .ogrid-pagination-btn.ogrid-pagination-active:hover:not(:disabled) {
615
+ background: var(--ogrid-primary, #217346);
616
+ border-color: var(--ogrid-primary, #217346);
617
+ color: var(--ogrid-primary-fg, #fff);
596
618
  }
597
619
 
598
620
  .ogrid-pagination-ellipsis {
@@ -797,7 +819,7 @@
797
819
 
798
820
  .ogrid-header-filter-popover {
799
821
  position: fixed;
800
- z-index: 1000;
822
+ z-index: var(--ogrid-z-filter-popover, 1000);
801
823
  min-width: 280px;
802
824
  max-width: 320px;
803
825
  background: var(--ogrid-bg, #fff);
@@ -909,7 +931,7 @@
909
931
 
910
932
  .ogrid-context-menu {
911
933
  position: fixed;
912
- z-index: 10000;
934
+ z-index: var(--ogrid-z-context-menu, 10000);
913
935
  min-width: 160px;
914
936
  padding: 4px 0;
915
937
  background: var(--ogrid-bg, #fff);
@@ -952,7 +974,7 @@
952
974
 
953
975
  .ogrid-cell-editor {
954
976
  position: absolute;
955
- z-index: 10;
977
+ z-index: var(--ogrid-z-filter-popover, 1000);
956
978
  background: var(--ogrid-bg, #fff);
957
979
  border: 2px solid var(--ogrid-selection, #217346);
958
980
  border-radius: 0;
@@ -982,7 +1004,7 @@
982
1004
  width: 3px;
983
1005
  background: var(--ogrid-primary, #217346);
984
1006
  pointer-events: none;
985
- z-index: 100;
1007
+ z-index: var(--ogrid-z-drop-indicator, 100);
986
1008
  transition: left 0.05s;
987
1009
  }
988
1010
 
@@ -996,9 +1018,11 @@
996
1018
 
997
1019
  /* ── Virtual Scrolling ── */
998
1020
 
999
- .ogrid-virtual-spacer td {
1000
- padding: 0 !important;
1001
- border: none !important;
1021
+ /* Virtual spacer: element+class selector (0,1,1) beats plain td rules (0,0,1).
1022
+ But .ogrid-table tbody td (0,1,2) has higher specificity so we qualify with the table. */
1023
+ .ogrid-table .ogrid-virtual-spacer td {
1024
+ padding: 0;
1025
+ border: none;
1002
1026
  line-height: 0;
1003
1027
  }
1004
1028
 
@@ -38,6 +38,8 @@ export declare class FillHandleState<T> {
38
38
  get fillRange(): ISelectionRange | null;
39
39
  setWrapperRef(ref: HTMLElement | null): void;
40
40
  updateParams(params: FillHandleParams<T>): void;
41
+ /** Fill the current selection down from the top row (keyboard Ctrl+D). No-op if no selection or editable=false. */
42
+ fillDown(): void;
41
43
  /** Called when the fill handle square is mousedown'd. */
42
44
  startFillDrag(e: MouseEvent): void;
43
45
  private onMouseMove;
@@ -14,6 +14,10 @@ export interface KeyboardNavParams<T> {
14
14
  onContextMenu?: (x: number, y: number) => void;
15
15
  onStartEdit?: (rowId: RowId, columnId: string) => void;
16
16
  clearClipboardRanges?: () => void;
17
+ /** Custom keydown handler. Called before grid default. preventDefault() suppresses grid handling. */
18
+ onKeyDown?: (event: KeyboardEvent) => void;
19
+ /** Fill-down callback (Ctrl+D). Provided by FillHandleState. */
20
+ onFillDown?: () => void;
17
21
  }
18
22
  export declare class KeyboardNavState<T> {
19
23
  private params;
@@ -21,7 +21,7 @@ export declare class VirtualScrollState {
21
21
  private _ro;
22
22
  private _cachedRange;
23
23
  constructor(config?: IVirtualScrollConfig);
24
- /** Whether virtual scrolling is active. */
24
+ /** Whether virtual scrolling is active (enabled + meets the row threshold). */
25
25
  get enabled(): boolean;
26
26
  get config(): IVirtualScrollConfig;
27
27
  get containerHeight(): number;
@@ -122,6 +122,8 @@ export interface OGridOptions<T> {
122
122
  columnChooser?: boolean | 'toolbar' | 'sidebar';
123
123
  /** Secondary toolbar row rendered below the primary toolbar. */
124
124
  toolbarBelow?: HTMLElement | null;
125
+ /** Custom keydown handler. Called before grid's built-in handling. Call event.preventDefault() to suppress grid default. */
126
+ onKeyDown?: (event: KeyboardEvent) => void;
125
127
  }
126
128
  /** Events emitted by the OGrid instance. */
127
129
  export interface OGridEvents<T> extends Record<string, unknown> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-js",
3
- "version": "2.1.9",
3
+ "version": "2.1.11",
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.9"
39
+ "@alaarab/ogrid-core": "2.1.11"
40
40
  },
41
41
  "sideEffects": [
42
42
  "**/*.css"