@alaarab/ogrid-vue 2.1.14 → 2.2.0

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
@@ -1,4 +1,4 @@
1
- import { injectGlobalStyles, Z_INDEX, getStatusBarParts, measureRange, flattenColumns, getMultiSelectFilterFields, deriveFilterOptionsFromData, processClientSideData, validateColumns, validateRowIds, computeRowSelectionState, buildCellIndex, UndoRedoStack, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, computeAggregations, getDataGridStatusBarConfig, validateVirtualScrollConfig, computeVisibleRange, computeTotalHeight, buildHeaderRows, ROW_NUMBER_COLUMN_WIDTH, getHeaderFilterConfig, getCellRenderDescriptor, buildInlineEditorProps, buildPopoverEditorProps, resolveCellDisplayContent, resolveCellStyle, rangesEqual, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, measureColumnContentWidth, getPinStateForColumn, parseValue, applyFillValues, applyCellDeletion, computeTabNavigation, computeArrowNavigation, computeNextSortState, mergeFilter, applyRangeRowSelection, getScrollTopForRow, getCellValue, calculateDropTarget, reorderColumnArray, computeAutoScrollSpeed } from '@alaarab/ogrid-core';
1
+ import { injectGlobalStyles, Z_INDEX, getStatusBarParts, measureRange, flattenColumns, getMultiSelectFilterFields, deriveFilterOptionsFromData, processClientSideData, processClientSideDataAsync, validateColumns, validateRowIds, computeRowSelectionState, buildCellIndex, UndoRedoStack, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, computeAggregations, getDataGridStatusBarConfig, validateVirtualScrollConfig, computeVisibleRange, computeTotalHeight, computeVisibleColumnRange, partitionColumnsForVirtualization, buildHeaderRows, ROW_NUMBER_COLUMN_WIDTH, getHeaderFilterConfig, getCellRenderDescriptor, buildInlineEditorProps, buildPopoverEditorProps, resolveCellDisplayContent, resolveCellStyle, rangesEqual, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, measureColumnContentWidth, getPinStateForColumn, parseValue, applyFillValues, applyCellDeletion, computeTabNavigation, computeArrowNavigation, computeNextSortState, mergeFilter, applyRangeRowSelection, getScrollTopForRow, getCellValue, calculateDropTarget, reorderColumnArray, computeAutoScrollSpeed } from '@alaarab/ogrid-core';
2
2
  export * from '@alaarab/ogrid-core';
3
3
  export { buildInlineEditorProps, buildPopoverEditorProps, getCellRenderDescriptor, getHeaderFilterConfig, isInSelectionRange, normalizeSelectionRange, resolveCellDisplayContent, resolveCellStyle, toUserLike } from '@alaarab/ogrid-core';
4
4
  import { defineComponent, ref, computed, onMounted, watch, toValue, onUnmounted, h, shallowRef, triggerRef, nextTick, Teleport, isRef, isReadonly, unref, customRef } from 'vue';
@@ -71,8 +71,8 @@ var MarchingAntsOverlay = defineComponent({
71
71
  return () => {
72
72
  if (!selRect.value && !clipRect.value) return null;
73
73
  return h("div", { style: { position: "relative" } }, [
74
- // Selection range: solid green border (hidden when clipboard range overlaps)
75
- selRect.value && !clipRangeMatchesSel.value ? h("svg", {
74
+ // Selection range: solid green border (hidden when clipboard range overlaps or single-cell — CSS outline handles that)
75
+ selRect.value && !clipRangeMatchesSel.value && !(props.selectionRange && props.selectionRange.startRow === props.selectionRange.endRow && props.selectionRange.startCol === props.selectionRange.endCol) ? h("svg", {
76
76
  style: {
77
77
  position: "absolute",
78
78
  top: `${selRect.value.top}px`,
@@ -151,7 +151,10 @@ var StatusBar = defineComponent({
151
151
  display: "flex",
152
152
  alignItems: "center",
153
153
  gap: "16px",
154
- fontSize: "0.875rem"
154
+ fontSize: "0.875rem",
155
+ lineHeight: "20px",
156
+ minHeight: "33px",
157
+ boxSizing: "border-box"
155
158
  }
156
159
  }, parts.map(
157
160
  (p, i) => h("span", {
@@ -328,7 +331,11 @@ function useOGrid(props) {
328
331
  return new Set(visible.length > 0 ? visible : columns.value.map((c) => c.columnId));
329
332
  })());
330
333
  const columnWidthOverrides = ref({});
331
- const pinnedOverrides = ref({});
334
+ const initialPinned = {};
335
+ for (const col of flattenColumns(props.value.columns)) {
336
+ if (col.pinned) initialPinned[col.columnId] = col.pinned;
337
+ }
338
+ const pinnedOverrides = ref(initialPinned);
332
339
  const page = computed(() => controlledState.value.page ?? internalPage.value);
333
340
  const pageSize = computed(() => controlledState.value.pageSize ?? internalPageSize.value);
334
341
  const sort = computed(() => controlledState.value.sort ?? internalSort.value);
@@ -386,8 +393,9 @@ function useOGrid(props) {
386
393
  if (hasServerFilterOptions.value) return serverFilterOptions.value;
387
394
  return deriveFilterOptionsFromData(displayData.value, columns.value);
388
395
  });
396
+ const workerSortEnabled = computed(() => !!props.value.workerSort);
389
397
  const clientItemsAndTotal = computed(() => {
390
- if (!isClientSide.value) return null;
398
+ if (!isClientSide.value || workerSortEnabled.value) return null;
391
399
  const rows = processClientSideData(
392
400
  displayData.value,
393
401
  columns.value,
@@ -400,6 +408,42 @@ function useOGrid(props) {
400
408
  const paged = rows.slice(start, start + pageSize.value);
401
409
  return { items: paged, totalCount: total };
402
410
  });
411
+ const asyncClientItems = ref(null);
412
+ let workerSortAbortId = 0;
413
+ watch(
414
+ [isClientSide, workerSortEnabled, displayData, columns, filters, () => sort.value.field, () => sort.value.direction, page, pageSize],
415
+ () => {
416
+ if (!isClientSide.value || !workerSortEnabled.value) return;
417
+ const data = displayData.value;
418
+ const cols = columns.value;
419
+ const f = filters.value;
420
+ const sf = sort.value.field;
421
+ const sd = sort.value.direction;
422
+ const p = page.value;
423
+ const ps = pageSize.value;
424
+ const abortId = ++workerSortAbortId;
425
+ processClientSideDataAsync(data, cols, f, sf, sd).then((rows) => {
426
+ if (abortId !== workerSortAbortId || isDestroyed) return;
427
+ const total = rows.length;
428
+ const start = (p - 1) * ps;
429
+ const paged = rows.slice(start, start + ps);
430
+ asyncClientItems.value = { items: paged, totalCount: total };
431
+ }).catch(() => {
432
+ if (abortId !== workerSortAbortId || isDestroyed) return;
433
+ const rows = processClientSideData(data, cols, f, sf, sd);
434
+ const total = rows.length;
435
+ const start = (p - 1) * ps;
436
+ const paged = rows.slice(start, start + ps);
437
+ asyncClientItems.value = { items: paged, totalCount: total };
438
+ });
439
+ },
440
+ { immediate: true }
441
+ );
442
+ const resolvedClientItems = computed(() => {
443
+ const syncResult = clientItemsAndTotal.value;
444
+ if (syncResult) return syncResult;
445
+ return asyncClientItems.value;
446
+ });
403
447
  const serverItems = ref([]);
404
448
  const serverTotalCount = ref(0);
405
449
  const loading = ref(false);
@@ -445,10 +489,10 @@ function useOGrid(props) {
445
489
  isDestroyed = true;
446
490
  });
447
491
  const displayItems = computed(
448
- () => isClientSide.value && clientItemsAndTotal.value ? clientItemsAndTotal.value.items : serverItems.value
492
+ () => isClientSide.value && resolvedClientItems.value ? resolvedClientItems.value.items : serverItems.value
449
493
  );
450
494
  const displayTotalCount = computed(
451
- () => isClientSide.value && clientItemsAndTotal.value ? clientItemsAndTotal.value.totalCount : serverTotalCount.value
495
+ () => isClientSide.value && resolvedClientItems.value ? resolvedClientItems.value.totalCount : serverTotalCount.value
452
496
  );
453
497
  let firstDataRendered = false;
454
498
  let rowIdsValidated = false;
@@ -961,15 +1005,17 @@ function useCellSelection(params) {
961
1005
  }
962
1006
  }
963
1007
  if (!cellIndex) cellIndex = buildCellIndex(wrapperRef.value);
1008
+ let rebuilt = false;
964
1009
  for (let r = minR; r <= maxR; r++) {
965
1010
  for (let c = minC; c <= maxC; c++) {
966
1011
  const key = `${r},${c + colOff}`;
967
1012
  let el = cellIndex?.get(key);
968
- if (el && !el.isConnected) {
1013
+ if (el && !el.isConnected && !rebuilt) {
1014
+ rebuilt = true;
969
1015
  cellIndex = buildCellIndex(wrapperRef.value);
970
1016
  el = cellIndex?.get(key);
971
1017
  }
972
- if (el) {
1018
+ if (el && el.isConnected) {
973
1019
  styleCellInRange(el, r, c, minR, maxR, minC, maxC, anchor);
974
1020
  }
975
1021
  }
@@ -1097,10 +1143,13 @@ function useCellSelection(params) {
1097
1143
  const finalRange = liveDragRange;
1098
1144
  if (finalRange) {
1099
1145
  setSelectionRange(finalRange);
1100
- setActiveCell({
1101
- rowIndex: finalRange.endRow,
1102
- columnIndex: finalRange.endCol + getColOffset()
1103
- });
1146
+ const anchor = dragStart;
1147
+ if (anchor) {
1148
+ setActiveCell({
1149
+ rowIndex: anchor.row,
1150
+ columnIndex: anchor.col + getColOffset()
1151
+ });
1152
+ }
1104
1153
  }
1105
1154
  }
1106
1155
  clearDragAttrs();
@@ -1607,7 +1656,7 @@ function useFillHandle(params) {
1607
1656
  norm.endRow = Math.min(norm.endRow, vr.endIndex);
1608
1657
  }
1609
1658
  setSelectionRange(norm);
1610
- setActiveCell({ rowIndex: end.endRow, columnIndex: end.endCol + getColOffset() });
1659
+ setActiveCell({ rowIndex: drag.startRow, columnIndex: drag.startCol + getColOffset() });
1611
1660
  const currentItems = items.value;
1612
1661
  const currentCols = visibleCols.value;
1613
1662
  const callback = onCellValueChanged.value;
@@ -3096,13 +3145,24 @@ function useColumnReorder(params) {
3096
3145
  }
3097
3146
  var DEFAULT_PASSTHROUGH_THRESHOLD = 100;
3098
3147
  function useVirtualScroll(params) {
3099
- const { totalRows, rowHeight, enabled, overscan = 5, threshold = DEFAULT_PASSTHROUGH_THRESHOLD } = params;
3148
+ const {
3149
+ totalRows,
3150
+ rowHeight,
3151
+ enabled,
3152
+ overscan = 5,
3153
+ threshold = DEFAULT_PASSTHROUGH_THRESHOLD,
3154
+ columnsEnabled,
3155
+ columnWidths,
3156
+ columnOverscan = 2
3157
+ } = params;
3100
3158
  onMounted(() => {
3101
3159
  validateVirtualScrollConfig({ enabled: enabled.value, rowHeight });
3102
3160
  });
3103
3161
  const containerRef = ref(null);
3104
3162
  const scrollTop = ref(0);
3163
+ const scrollLeft = ref(0);
3105
3164
  const containerHeight = ref(0);
3165
+ const containerWidth = ref(0);
3106
3166
  let rafId = 0;
3107
3167
  let resizeObserver;
3108
3168
  let prevObservedEl = null;
@@ -3123,6 +3183,17 @@ function useVirtualScroll(params) {
3123
3183
  if (!enabled.value) return 0;
3124
3184
  return computeTotalHeight(totalRows.value, rowHeight);
3125
3185
  });
3186
+ const columnRange = computed(() => {
3187
+ if (!columnsEnabled?.value) return null;
3188
+ const widths = columnWidths?.value;
3189
+ if (!widths || widths.length === 0) return null;
3190
+ return computeVisibleColumnRange(
3191
+ scrollLeft.value,
3192
+ widths,
3193
+ containerWidth.value,
3194
+ columnOverscan
3195
+ );
3196
+ });
3126
3197
  const onScroll = () => {
3127
3198
  if (!rafId) {
3128
3199
  rafId = requestAnimationFrame(() => {
@@ -3130,6 +3201,7 @@ function useVirtualScroll(params) {
3130
3201
  const el = containerRef.value;
3131
3202
  if (el) {
3132
3203
  scrollTop.value = el.scrollTop;
3204
+ scrollLeft.value = el.scrollLeft;
3133
3205
  }
3134
3206
  });
3135
3207
  }
@@ -3138,6 +3210,7 @@ function useVirtualScroll(params) {
3138
3210
  const el = containerRef.value;
3139
3211
  if (!el) return;
3140
3212
  containerHeight.value = el.clientHeight;
3213
+ containerWidth.value = el.clientWidth;
3141
3214
  };
3142
3215
  watch(containerRef, (el) => {
3143
3216
  if (el === prevObservedEl) return;
@@ -3157,6 +3230,7 @@ function useVirtualScroll(params) {
3157
3230
  }
3158
3231
  measure();
3159
3232
  scrollTop.value = el.scrollTop;
3233
+ scrollLeft.value = el.scrollLeft;
3160
3234
  }
3161
3235
  });
3162
3236
  onUnmounted(() => {
@@ -3175,7 +3249,7 @@ function useVirtualScroll(params) {
3175
3249
  if (!el) return;
3176
3250
  el.scrollTop = getScrollTopForRow(index, rowHeight, containerHeight.value, align);
3177
3251
  };
3178
- return { containerRef, visibleRange, totalHeight, scrollToRow };
3252
+ return { containerRef, visibleRange, totalHeight, scrollToRow, columnRange, scrollLeft };
3179
3253
  }
3180
3254
  function useDataGridTableSetup(params) {
3181
3255
  const { props: propsRef } = params;
@@ -3199,11 +3273,45 @@ function useDataGridTableSetup(params) {
3199
3273
  const totalRowsRef = computed(() => propsRef.value.items.length);
3200
3274
  const rowHeight = propsRef.value.virtualScroll?.rowHeight ?? 36;
3201
3275
  const overscan = propsRef.value.virtualScroll?.overscan ?? 5;
3276
+ const columnsVirtEnabled = computed(() => propsRef.value.virtualScroll?.columns === true);
3277
+ const columnOverscan = propsRef.value.virtualScroll?.columnOverscan ?? 2;
3278
+ const unpinnedColumnWidths = computed(() => {
3279
+ const layout = state.layout.value;
3280
+ const { visibleCols, columnSizingOverrides } = layout;
3281
+ const pinnedCols = propsRef.value.pinnedColumns ?? {};
3282
+ const widths = [];
3283
+ for (const col of visibleCols) {
3284
+ if (pinnedCols[col.columnId] || col.pinned) continue;
3285
+ const override = columnSizingOverrides[col.columnId];
3286
+ widths.push(override ? override.widthPx : col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH);
3287
+ }
3288
+ return widths;
3289
+ });
3202
3290
  const virtualScroll = useVirtualScroll({
3203
3291
  totalRows: totalRowsRef,
3204
3292
  rowHeight,
3205
3293
  enabled: virtualScrollEnabled,
3206
- overscan
3294
+ overscan,
3295
+ columnsEnabled: columnsVirtEnabled,
3296
+ columnWidths: unpinnedColumnWidths,
3297
+ columnOverscan
3298
+ });
3299
+ const columnPartition = computed(() => {
3300
+ if (!columnsVirtEnabled.value) return null;
3301
+ const layout = state.layout.value;
3302
+ const cols = layout.visibleCols;
3303
+ const range = virtualScroll.columnRange.value;
3304
+ const pinnedCols = propsRef.value.pinnedColumns;
3305
+ return partitionColumnsForVirtualization(cols, range, pinnedCols);
3306
+ });
3307
+ const globalColIndexMap = computed(() => {
3308
+ const layout = state.layout.value;
3309
+ const cols = layout.visibleCols;
3310
+ const map = /* @__PURE__ */ new Map();
3311
+ for (let i = 0; i < cols.length; i++) {
3312
+ map.set(cols[i].columnId, i);
3313
+ }
3314
+ return map;
3207
3315
  });
3208
3316
  const columnSizingOverridesRef = computed(() => state.layout.value.columnSizingOverrides);
3209
3317
  const columnResize = useColumnResize({
@@ -3219,7 +3327,9 @@ function useDataGridTableSetup(params) {
3219
3327
  columnReorder,
3220
3328
  virtualScroll,
3221
3329
  virtualScrollEnabled,
3222
- columnResize
3330
+ columnResize,
3331
+ columnPartition,
3332
+ globalColIndexMap
3223
3333
  };
3224
3334
  }
3225
3335
  function getCellInteractionProps(descriptor, columnId, handlers) {
@@ -3257,7 +3367,9 @@ function createDataGridTable(ui) {
3257
3367
  columnReorder: { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown: handleReorderMouseDown },
3258
3368
  virtualScroll: { containerRef: vsContainerRef, visibleRange, totalHeight: _totalHeight, scrollToRow: _scrollToRow },
3259
3369
  virtualScrollEnabled,
3260
- columnResize: { handleResizeStart, handleResizeDoubleClick, getColumnWidth }
3370
+ columnResize: { handleResizeStart, handleResizeDoubleClick, getColumnWidth },
3371
+ columnPartition,
3372
+ globalColIndexMap
3261
3373
  } = useDataGridTableSetup({ props: propsRef });
3262
3374
  const onWrapperMousedown = (e) => {
3263
3375
  lastMouseShift.value = e.shiftKey;
@@ -3430,8 +3542,8 @@ function createDataGridTable(ui) {
3430
3542
  if (col.type === "numeric") cellClasses.push("ogrid-cell-content--numeric");
3431
3543
  else if (col.type === "boolean") cellClasses.push("ogrid-cell-content--boolean");
3432
3544
  if (descriptor.canEditAny) cellClasses.push("ogrid-cell-content--editable");
3433
- if (descriptor.isActive && !descriptor.isInRange) cellClasses.push("ogrid-cell-content--active");
3434
- if (descriptor.isInRange) cellClasses.push("ogrid-cell-in-range");
3545
+ if (descriptor.isActive) cellClasses.push("ogrid-cell-content--active");
3546
+ if (descriptor.isInRange && !descriptor.isActive) cellClasses.push("ogrid-cell-in-range");
3435
3547
  if (descriptor.isInCutRange) cellClasses.push("ogrid-cell-cut");
3436
3548
  const styledContent = cellStyle ? h("span", { style: cellStyle }, content) : content;
3437
3549
  return h("div", {
@@ -3449,11 +3561,25 @@ function createDataGridTable(ui) {
3449
3561
  ]);
3450
3562
  };
3451
3563
  const { cellStyles: colCellStyles, cellClasses: colCellClasses, hdrStyles: colHdrStyles, hdrClasses: colHdrClasses } = columnMetaCache.value;
3452
- const columnLayouts = visibleCols.map((col) => ({
3564
+ const allColumnLayouts = visibleCols.map((col) => ({
3453
3565
  col,
3454
3566
  tdClasses: colCellClasses[col.columnId] || "ogrid-data-cell",
3455
3567
  tdDynamicStyle: colCellStyles[col.columnId] || {}
3456
3568
  }));
3569
+ const partition = columnPartition.value;
3570
+ let columnLayouts = allColumnLayouts;
3571
+ let leftSpacerWidth = 0;
3572
+ let rightSpacerWidth = 0;
3573
+ if (partition) {
3574
+ const visibleIds = /* @__PURE__ */ new Set();
3575
+ for (const col of partition.pinnedLeft) visibleIds.add(col.columnId);
3576
+ for (const col of partition.virtualizedUnpinned) visibleIds.add(col.columnId);
3577
+ for (const col of partition.pinnedRight) visibleIds.add(col.columnId);
3578
+ columnLayouts = allColumnLayouts.filter((cl) => visibleIds.has(cl.col.columnId));
3579
+ leftSpacerWidth = partition.leftSpacerWidth;
3580
+ rightSpacerWidth = partition.rightSpacerWidth;
3581
+ }
3582
+ const colIndexMap = globalColIndexMap.value;
3457
3583
  const getHeaderClassAndStyle = (col) => {
3458
3584
  const base = colHdrStyles[col.columnId] || {};
3459
3585
  return {
@@ -3514,7 +3640,8 @@ function createDataGridTable(ui) {
3514
3640
  },
3515
3641
  class: "ogrid-table",
3516
3642
  role: "grid",
3517
- style: { minWidth: `${minTableWidth}px` }
3643
+ style: { minWidth: `${minTableWidth}px` },
3644
+ ...virtualScrollEnabled.value ? { "data-virtual-scroll": "" } : {}
3518
3645
  }, [
3519
3646
  // Header
3520
3647
  h(
@@ -3697,15 +3824,23 @@ function createDataGridTable(ui) {
3697
3824
  }
3698
3825
  }, String(rowNumberOffset + rowIndex + 1))
3699
3826
  ] : [],
3827
+ // Left spacer for column virtualization
3828
+ ...leftSpacerWidth > 0 ? [
3829
+ h("td", { key: "__col-spacer-left", style: { width: `${leftSpacerWidth}px`, minWidth: `${leftSpacerWidth}px`, maxWidth: `${leftSpacerWidth}px`, padding: "0" } })
3830
+ ] : [],
3700
3831
  // Data cells
3701
3832
  ...columnLayouts.map(
3702
- (cl, colIdx) => h("td", {
3833
+ (cl) => h("td", {
3703
3834
  key: cl.col.columnId,
3704
3835
  "data-column-id": cl.col.columnId,
3705
3836
  class: cl.tdClasses,
3706
3837
  style: cl.tdDynamicStyle
3707
- }, [renderCellContent(item, cl.col, rowIndex, colIdx)])
3708
- )
3838
+ }, [renderCellContent(item, cl.col, rowIndex, colIndexMap.get(cl.col.columnId) ?? 0)])
3839
+ ),
3840
+ // Right spacer for column virtualization
3841
+ ...rightSpacerWidth > 0 ? [
3842
+ h("td", { key: "__col-spacer-right", style: { width: `${rightSpacerWidth}px`, minWidth: `${rightSpacerWidth}px`, maxWidth: `${rightSpacerWidth}px`, padding: "0" } })
3843
+ ] : []
3709
3844
  ]));
3710
3845
  }
3711
3846
  if (vsEnabled && vr.offsetBottom > 0) {
@@ -189,8 +189,16 @@
189
189
  position: relative;
190
190
  padding: 0;
191
191
  height: 1px;
192
+ contain: content;
192
193
  }
193
194
 
195
+ /* Pinned columns need contain: none because contain breaks position: sticky */
196
+ .ogrid-data-cell--pinned-left,
197
+ .ogrid-data-cell--pinned-right { contain: none; }
198
+
199
+ /* content-visibility: auto on rows for non-virtualized grids */
200
+ .ogrid-table:not([data-virtual-scroll]) tbody tr { content-visibility: auto; }
201
+
194
202
  .ogrid-data-cell--pinned-left {
195
203
  position: sticky;
196
204
  z-index: var(--ogrid-z-pinned, 6);
@@ -1,5 +1,5 @@
1
1
  import { type Ref } from 'vue';
2
- import type { IOGridDataGridProps } from '../types';
2
+ import type { IOGridDataGridProps, IColumnDef } from '../types';
3
3
  import { type UseDataGridStateResult } from './useDataGridState';
4
4
  import { type UseColumnResizeResult } from './useColumnResize';
5
5
  import { type UseColumnReorderResult } from './useColumnReorder';
@@ -27,6 +27,16 @@ export interface UseDataGridTableSetupResult<T> {
27
27
  virtualScrollEnabled: Ref<boolean>;
28
28
  /** Column resize handlers (handleResizeStart, getColumnWidth). */
29
29
  columnResize: UseColumnResizeResult<T>;
30
+ /** Column virtualization partition (or null when column virtualization is off). */
31
+ columnPartition: Ref<{
32
+ pinnedLeft: IColumnDef<T>[];
33
+ virtualizedUnpinned: IColumnDef<T>[];
34
+ pinnedRight: IColumnDef<T>[];
35
+ leftSpacerWidth: number;
36
+ rightSpacerWidth: number;
37
+ } | null>;
38
+ /** Map from columnId to its global index in visibleCols. */
39
+ globalColIndexMap: Ref<Map<string, number>>;
30
40
  }
31
41
  /**
32
42
  * Shared setup composable for Vue DataGridTable components.
@@ -1,5 +1,5 @@
1
1
  import { type Ref } from 'vue';
2
- import type { IVisibleRange } from '@alaarab/ogrid-core';
2
+ import type { IVisibleRange, IVisibleColumnRange } from '@alaarab/ogrid-core';
3
3
  export interface UseVirtualScrollParams {
4
4
  totalRows: Ref<number>;
5
5
  rowHeight: number;
@@ -10,12 +10,22 @@ export interface UseVirtualScrollParams {
10
10
  * When totalRows < threshold, all rows render without virtualization.
11
11
  */
12
12
  threshold?: number;
13
+ /** Enable column virtualization. */
14
+ columnsEnabled?: Ref<boolean>;
15
+ /** Column widths array for unpinned columns. */
16
+ columnWidths?: Ref<number[]>;
17
+ /** Number of extra columns to render outside the visible area. Default: 2. */
18
+ columnOverscan?: number;
13
19
  }
14
20
  export interface UseVirtualScrollResult {
15
21
  containerRef: Ref<HTMLElement | null>;
16
22
  visibleRange: Ref<IVisibleRange>;
17
23
  totalHeight: Ref<number>;
18
24
  scrollToRow: (index: number, align?: 'start' | 'center' | 'end') => void;
25
+ /** Visible column range for horizontal virtualization, or null when column virtualization is off. */
26
+ columnRange: Ref<IVisibleColumnRange | null>;
27
+ /** Reactive scrollLeft value. */
28
+ scrollLeft: Ref<number>;
19
29
  }
20
30
  /**
21
31
  * Manages virtual scrolling with RAF-throttled scroll handling and ResizeObserver
@@ -81,6 +81,8 @@ interface IOGridBaseProps<T> {
81
81
  columnReorder?: boolean;
82
82
  /** Virtual scrolling configuration. Set `enabled: true` with a fixed `rowHeight` to virtualize large datasets. */
83
83
  virtualScroll?: IVirtualScrollConfig;
84
+ /** Offload sort/filter to a Web Worker for large datasets. Falls back to sync when sort column has a custom compare. */
85
+ workerSort?: boolean;
84
86
  /** Fixed row height in pixels. Overrides default row height (36px). */
85
87
  rowHeight?: number;
86
88
  /** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-vue",
3
- "version": "2.1.14",
3
+ "version": "2.2.0",
4
4
  "description": "OGrid Vue – Vue 3 composables, headless components, and utilities for OGrid data grids.",
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.14"
39
+ "@alaarab/ogrid-core": "2.2.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "vue": "^3.3.0"