@alaarab/ogrid-vue 2.1.15 → 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';
@@ -331,7 +331,11 @@ function useOGrid(props) {
331
331
  return new Set(visible.length > 0 ? visible : columns.value.map((c) => c.columnId));
332
332
  })());
333
333
  const columnWidthOverrides = ref({});
334
- 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);
335
339
  const page = computed(() => controlledState.value.page ?? internalPage.value);
336
340
  const pageSize = computed(() => controlledState.value.pageSize ?? internalPageSize.value);
337
341
  const sort = computed(() => controlledState.value.sort ?? internalSort.value);
@@ -389,8 +393,9 @@ function useOGrid(props) {
389
393
  if (hasServerFilterOptions.value) return serverFilterOptions.value;
390
394
  return deriveFilterOptionsFromData(displayData.value, columns.value);
391
395
  });
396
+ const workerSortEnabled = computed(() => !!props.value.workerSort);
392
397
  const clientItemsAndTotal = computed(() => {
393
- if (!isClientSide.value) return null;
398
+ if (!isClientSide.value || workerSortEnabled.value) return null;
394
399
  const rows = processClientSideData(
395
400
  displayData.value,
396
401
  columns.value,
@@ -403,6 +408,42 @@ function useOGrid(props) {
403
408
  const paged = rows.slice(start, start + pageSize.value);
404
409
  return { items: paged, totalCount: total };
405
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
+ });
406
447
  const serverItems = ref([]);
407
448
  const serverTotalCount = ref(0);
408
449
  const loading = ref(false);
@@ -448,10 +489,10 @@ function useOGrid(props) {
448
489
  isDestroyed = true;
449
490
  });
450
491
  const displayItems = computed(
451
- () => isClientSide.value && clientItemsAndTotal.value ? clientItemsAndTotal.value.items : serverItems.value
492
+ () => isClientSide.value && resolvedClientItems.value ? resolvedClientItems.value.items : serverItems.value
452
493
  );
453
494
  const displayTotalCount = computed(
454
- () => isClientSide.value && clientItemsAndTotal.value ? clientItemsAndTotal.value.totalCount : serverTotalCount.value
495
+ () => isClientSide.value && resolvedClientItems.value ? resolvedClientItems.value.totalCount : serverTotalCount.value
455
496
  );
456
497
  let firstDataRendered = false;
457
498
  let rowIdsValidated = false;
@@ -3104,13 +3145,24 @@ function useColumnReorder(params) {
3104
3145
  }
3105
3146
  var DEFAULT_PASSTHROUGH_THRESHOLD = 100;
3106
3147
  function useVirtualScroll(params) {
3107
- 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;
3108
3158
  onMounted(() => {
3109
3159
  validateVirtualScrollConfig({ enabled: enabled.value, rowHeight });
3110
3160
  });
3111
3161
  const containerRef = ref(null);
3112
3162
  const scrollTop = ref(0);
3163
+ const scrollLeft = ref(0);
3113
3164
  const containerHeight = ref(0);
3165
+ const containerWidth = ref(0);
3114
3166
  let rafId = 0;
3115
3167
  let resizeObserver;
3116
3168
  let prevObservedEl = null;
@@ -3131,6 +3183,17 @@ function useVirtualScroll(params) {
3131
3183
  if (!enabled.value) return 0;
3132
3184
  return computeTotalHeight(totalRows.value, rowHeight);
3133
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
+ });
3134
3197
  const onScroll = () => {
3135
3198
  if (!rafId) {
3136
3199
  rafId = requestAnimationFrame(() => {
@@ -3138,6 +3201,7 @@ function useVirtualScroll(params) {
3138
3201
  const el = containerRef.value;
3139
3202
  if (el) {
3140
3203
  scrollTop.value = el.scrollTop;
3204
+ scrollLeft.value = el.scrollLeft;
3141
3205
  }
3142
3206
  });
3143
3207
  }
@@ -3146,6 +3210,7 @@ function useVirtualScroll(params) {
3146
3210
  const el = containerRef.value;
3147
3211
  if (!el) return;
3148
3212
  containerHeight.value = el.clientHeight;
3213
+ containerWidth.value = el.clientWidth;
3149
3214
  };
3150
3215
  watch(containerRef, (el) => {
3151
3216
  if (el === prevObservedEl) return;
@@ -3165,6 +3230,7 @@ function useVirtualScroll(params) {
3165
3230
  }
3166
3231
  measure();
3167
3232
  scrollTop.value = el.scrollTop;
3233
+ scrollLeft.value = el.scrollLeft;
3168
3234
  }
3169
3235
  });
3170
3236
  onUnmounted(() => {
@@ -3183,7 +3249,7 @@ function useVirtualScroll(params) {
3183
3249
  if (!el) return;
3184
3250
  el.scrollTop = getScrollTopForRow(index, rowHeight, containerHeight.value, align);
3185
3251
  };
3186
- return { containerRef, visibleRange, totalHeight, scrollToRow };
3252
+ return { containerRef, visibleRange, totalHeight, scrollToRow, columnRange, scrollLeft };
3187
3253
  }
3188
3254
  function useDataGridTableSetup(params) {
3189
3255
  const { props: propsRef } = params;
@@ -3207,11 +3273,45 @@ function useDataGridTableSetup(params) {
3207
3273
  const totalRowsRef = computed(() => propsRef.value.items.length);
3208
3274
  const rowHeight = propsRef.value.virtualScroll?.rowHeight ?? 36;
3209
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
+ });
3210
3290
  const virtualScroll = useVirtualScroll({
3211
3291
  totalRows: totalRowsRef,
3212
3292
  rowHeight,
3213
3293
  enabled: virtualScrollEnabled,
3214
- 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;
3215
3315
  });
3216
3316
  const columnSizingOverridesRef = computed(() => state.layout.value.columnSizingOverrides);
3217
3317
  const columnResize = useColumnResize({
@@ -3227,7 +3327,9 @@ function useDataGridTableSetup(params) {
3227
3327
  columnReorder,
3228
3328
  virtualScroll,
3229
3329
  virtualScrollEnabled,
3230
- columnResize
3330
+ columnResize,
3331
+ columnPartition,
3332
+ globalColIndexMap
3231
3333
  };
3232
3334
  }
3233
3335
  function getCellInteractionProps(descriptor, columnId, handlers) {
@@ -3265,7 +3367,9 @@ function createDataGridTable(ui) {
3265
3367
  columnReorder: { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown: handleReorderMouseDown },
3266
3368
  virtualScroll: { containerRef: vsContainerRef, visibleRange, totalHeight: _totalHeight, scrollToRow: _scrollToRow },
3267
3369
  virtualScrollEnabled,
3268
- columnResize: { handleResizeStart, handleResizeDoubleClick, getColumnWidth }
3370
+ columnResize: { handleResizeStart, handleResizeDoubleClick, getColumnWidth },
3371
+ columnPartition,
3372
+ globalColIndexMap
3269
3373
  } = useDataGridTableSetup({ props: propsRef });
3270
3374
  const onWrapperMousedown = (e) => {
3271
3375
  lastMouseShift.value = e.shiftKey;
@@ -3457,11 +3561,25 @@ function createDataGridTable(ui) {
3457
3561
  ]);
3458
3562
  };
3459
3563
  const { cellStyles: colCellStyles, cellClasses: colCellClasses, hdrStyles: colHdrStyles, hdrClasses: colHdrClasses } = columnMetaCache.value;
3460
- const columnLayouts = visibleCols.map((col) => ({
3564
+ const allColumnLayouts = visibleCols.map((col) => ({
3461
3565
  col,
3462
3566
  tdClasses: colCellClasses[col.columnId] || "ogrid-data-cell",
3463
3567
  tdDynamicStyle: colCellStyles[col.columnId] || {}
3464
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;
3465
3583
  const getHeaderClassAndStyle = (col) => {
3466
3584
  const base = colHdrStyles[col.columnId] || {};
3467
3585
  return {
@@ -3522,7 +3640,8 @@ function createDataGridTable(ui) {
3522
3640
  },
3523
3641
  class: "ogrid-table",
3524
3642
  role: "grid",
3525
- style: { minWidth: `${minTableWidth}px` }
3643
+ style: { minWidth: `${minTableWidth}px` },
3644
+ ...virtualScrollEnabled.value ? { "data-virtual-scroll": "" } : {}
3526
3645
  }, [
3527
3646
  // Header
3528
3647
  h(
@@ -3705,15 +3824,23 @@ function createDataGridTable(ui) {
3705
3824
  }
3706
3825
  }, String(rowNumberOffset + rowIndex + 1))
3707
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
+ ] : [],
3708
3831
  // Data cells
3709
3832
  ...columnLayouts.map(
3710
- (cl, colIdx) => h("td", {
3833
+ (cl) => h("td", {
3711
3834
  key: cl.col.columnId,
3712
3835
  "data-column-id": cl.col.columnId,
3713
3836
  class: cl.tdClasses,
3714
3837
  style: cl.tdDynamicStyle
3715
- }, [renderCellContent(item, cl.col, rowIndex, colIdx)])
3716
- )
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
+ ] : []
3717
3844
  ]));
3718
3845
  }
3719
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.15",
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.15"
39
+ "@alaarab/ogrid-core": "2.2.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "vue": "^3.3.0"