@alaarab/ogrid-vue 2.0.2 → 2.0.3

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.
Files changed (32) hide show
  1. package/dist/esm/composables/index.js +3 -0
  2. package/dist/esm/composables/useActiveCell.js +26 -7
  3. package/dist/esm/composables/useCellEditing.js +10 -3
  4. package/dist/esm/composables/useCellSelection.js +8 -4
  5. package/dist/esm/composables/useClipboard.js +14 -14
  6. package/dist/esm/composables/useColumnReorder.js +103 -0
  7. package/dist/esm/composables/useContextMenu.js +2 -2
  8. package/dist/esm/composables/useDebounce.js +20 -2
  9. package/dist/esm/composables/useFillHandle.js +18 -5
  10. package/dist/esm/composables/useKeyboardNavigation.js +8 -2
  11. package/dist/esm/composables/useLatestRef.js +27 -0
  12. package/dist/esm/composables/useOGrid.js +19 -1
  13. package/dist/esm/composables/useRowSelection.js +4 -1
  14. package/dist/esm/composables/useVirtualScroll.js +84 -0
  15. package/dist/esm/index.js +1 -1
  16. package/dist/types/components/SideBar.d.ts +6 -0
  17. package/dist/types/composables/index.d.ts +7 -1
  18. package/dist/types/composables/useActiveCell.d.ts +1 -1
  19. package/dist/types/composables/useCellEditing.d.ts +8 -3
  20. package/dist/types/composables/useCellSelection.d.ts +1 -1
  21. package/dist/types/composables/useClipboard.d.ts +6 -5
  22. package/dist/types/composables/useColumnReorder.d.ts +20 -0
  23. package/dist/types/composables/useContextMenu.d.ts +2 -2
  24. package/dist/types/composables/useDebounce.d.ts +9 -2
  25. package/dist/types/composables/useFillHandle.d.ts +4 -2
  26. package/dist/types/composables/useKeyboardNavigation.d.ts +7 -3
  27. package/dist/types/composables/useLatestRef.d.ts +11 -0
  28. package/dist/types/composables/useVirtualScroll.d.ts +19 -0
  29. package/dist/types/index.d.ts +3 -3
  30. package/dist/types/types/dataGridTypes.d.ts +6 -2
  31. package/dist/types/types/index.d.ts +1 -1
  32. package/package.json +2 -2
@@ -14,6 +14,7 @@ export { useContextMenu } from './useContextMenu';
14
14
  export { useColumnResize } from './useColumnResize';
15
15
  export { useFilterOptions } from './useFilterOptions';
16
16
  export { useDebounce, useDebouncedCallback } from './useDebounce';
17
+ export { useLatestRef } from './useLatestRef';
17
18
  export { useTableLayout } from './useTableLayout';
18
19
  // Headless state composables
19
20
  export { useColumnHeaderFilterState } from './useColumnHeaderFilterState';
@@ -25,3 +26,5 @@ export { useColumnChooserState } from './useColumnChooserState';
25
26
  export { useInlineCellEditorState } from './useInlineCellEditorState';
26
27
  export { useRichSelectState } from './useRichSelectState';
27
28
  export { useSideBarState } from './useSideBarState';
29
+ export { useColumnReorder } from './useColumnReorder';
30
+ export { useVirtualScroll } from './useVirtualScroll';
@@ -1,10 +1,11 @@
1
- import { ref, watch, nextTick } from 'vue';
1
+ import { shallowRef, watch, onUnmounted } from 'vue';
2
2
  /**
3
3
  * Tracks the active cell for keyboard navigation.
4
4
  * When wrapperRef and editingCell are provided, scrolls the active cell into view when it changes (and not editing).
5
5
  */
6
6
  export function useActiveCell(wrapperRef, editingCell) {
7
- const activeCell = ref(null);
7
+ const activeCell = shallowRef(null);
8
+ let pendingRaf = 0;
8
9
  // Deduplicating setter — skips update when the cell coordinates haven't actually changed.
9
10
  const setActiveCell = (cell) => {
10
11
  const prev = activeCell.value;
@@ -14,18 +15,30 @@ export function useActiveCell(wrapperRef, editingCell) {
14
15
  return;
15
16
  activeCell.value = cell;
16
17
  };
17
- // Scroll active cell into view when it changes (equivalent to useLayoutEffect)
18
+ // Scroll active cell into view when it changes (equivalent to useLayoutEffect).
19
+ // Uses requestAnimationFrame to batch DOM reads (getBoundingClientRect) with the
20
+ // browser's layout cycle, avoiding forced reflows when rapidly clicking cells.
18
21
  watch([activeCell, () => editingCell?.value], () => {
22
+ // Cancel any pending scroll from a previous cell change
23
+ if (pendingRaf) {
24
+ cancelAnimationFrame(pendingRaf);
25
+ pendingRaf = 0;
26
+ }
19
27
  if (activeCell.value == null ||
20
28
  !wrapperRef?.value ||
21
29
  editingCell?.value != null)
22
30
  return;
23
- // Use nextTick to ensure DOM is updated before scrolling
24
- void nextTick(() => {
31
+ // Capture the target coordinates before the async boundary
32
+ const { rowIndex, columnIndex } = activeCell.value;
33
+ pendingRaf = requestAnimationFrame(() => {
34
+ pendingRaf = 0;
25
35
  const wrapper = wrapperRef.value;
26
- if (!wrapper || !activeCell.value)
36
+ if (!wrapper)
37
+ return;
38
+ // Verify the active cell hasn't changed since we scheduled
39
+ const current = activeCell.value;
40
+ if (!current || current.rowIndex !== rowIndex || current.columnIndex !== columnIndex)
27
41
  return;
28
- const { rowIndex, columnIndex } = activeCell.value;
29
42
  const selector = `[data-row-index="${rowIndex}"][data-col-index="${columnIndex}"]`;
30
43
  const cell = wrapper.querySelector(selector);
31
44
  if (cell) {
@@ -54,5 +67,11 @@ export function useActiveCell(wrapperRef, editingCell) {
54
67
  }
55
68
  });
56
69
  }, { flush: 'post' });
70
+ onUnmounted(() => {
71
+ if (pendingRaf) {
72
+ cancelAnimationFrame(pendingRaf);
73
+ pendingRaf = 0;
74
+ }
75
+ });
57
76
  return { activeCell, setActiveCell };
58
77
  }
@@ -1,11 +1,18 @@
1
- import { ref } from 'vue';
1
+ import { shallowRef, ref } from 'vue';
2
2
  /**
3
3
  * Manages cell editing state: which cell is being edited and its pending value.
4
+ * Optionally scrolls to the cell's row before opening the editor when virtual scrolling is active.
4
5
  */
5
- export function useCellEditing() {
6
- const editingCell = ref(null);
6
+ export function useCellEditing(params) {
7
+ const editingCell = shallowRef(null);
7
8
  const pendingEditorValue = ref(undefined);
8
9
  const setEditingCell = (cell) => {
10
+ if (cell && params?.scrollToRow && params?.getRowIndex) {
11
+ const rowIndex = params.getRowIndex(cell.rowId);
12
+ if (rowIndex >= 0) {
13
+ params.scrollToRow(rowIndex, 'center');
14
+ }
15
+ }
9
16
  editingCell.value = cell;
10
17
  };
11
18
  const setPendingEditorValue = (value) => {
@@ -1,5 +1,6 @@
1
- import { ref, onMounted, onUnmounted } from 'vue';
1
+ import { shallowRef, ref, computed, onMounted, onUnmounted } from 'vue';
2
2
  import { normalizeSelectionRange } from '../types';
3
+ import { useLatestRef } from './useLatestRef';
3
4
  /** Compares two selection ranges by value. */
4
5
  function rangesEqual(a, b) {
5
6
  if (a === b)
@@ -24,9 +25,11 @@ function autoScrollSpeed(distance) {
24
25
  * Manages cell selection range with drag-to-select and select-all support.
25
26
  */
26
27
  export function useCellSelection(params) {
27
- const { colOffset, rowCount, visibleColCount, setActiveCell, wrapperRef } = params;
28
- const selectionRange = ref(null);
29
- const isDragging = ref(false);
28
+ // Store latest params in a ref for stable handler references
29
+ const paramsRef = useLatestRef(computed(() => params));
30
+ const { colOffset, wrapperRef, setActiveCell } = params; // These are stable, safe to destructure
31
+ const selectionRange = shallowRef(null);
32
+ const isDragging = ref(false); // boolean primitive, ref is fine
30
33
  let isDraggingInternal = false;
31
34
  let dragMoved = false;
32
35
  let dragStart = null;
@@ -72,6 +75,7 @@ export function useCellSelection(params) {
72
75
  }
73
76
  };
74
77
  const handleSelectAllCells = () => {
78
+ const { rowCount, visibleColCount } = paramsRef.value;
75
79
  if (rowCount.value === 0 || visibleColCount.value === 0)
76
80
  return;
77
81
  setSelectionRange({
@@ -1,14 +1,14 @@
1
- import { ref } from 'vue';
1
+ import { ref, shallowRef } from 'vue';
2
2
  import { getCellValue, parseValue, normalizeSelectionRange } from '@alaarab/ogrid-core';
3
3
  /**
4
4
  * Manages copy, cut, and paste operations for cell ranges with TSV clipboard format.
5
5
  */
6
6
  export function useClipboard(params) {
7
7
  const { items, visibleCols, colOffset, selectionRange, activeCell, editable, onCellValueChanged, beginBatch, endBatch, } = params;
8
- let cutRangeInternal = null;
9
- const cutRange = ref(null);
10
- const copyRange = ref(null);
11
- let internalClipboard = null;
8
+ const cutRangeRef = ref(null);
9
+ const cutRange = shallowRef(null);
10
+ const copyRange = shallowRef(null);
11
+ const internalClipboardRef = ref(null);
12
12
  const getEffectiveRange = () => {
13
13
  const sel = selectionRange.value;
14
14
  const ac = activeCell.value;
@@ -38,7 +38,7 @@ export function useClipboard(params) {
38
38
  rows.push(cells.join('\t'));
39
39
  }
40
40
  const tsv = rows.join('\r\n');
41
- internalClipboard = tsv;
41
+ internalClipboardRef.value = tsv;
42
42
  copyRange.value = norm;
43
43
  void navigator.clipboard.writeText(tsv).catch(() => { });
44
44
  };
@@ -49,7 +49,7 @@ export function useClipboard(params) {
49
49
  if (range == null || onCellValueChanged.value == null)
50
50
  return;
51
51
  const norm = normalizeSelectionRange(range);
52
- cutRangeInternal = norm;
52
+ cutRangeRef.value = norm;
53
53
  cutRange.value = norm;
54
54
  copyRange.value = null;
55
55
  handleCopy();
@@ -68,8 +68,8 @@ export function useClipboard(params) {
68
68
  catch {
69
69
  text = '';
70
70
  }
71
- if (!text.trim() && internalClipboard != null) {
72
- text = internalClipboard;
71
+ if (!text.trim() && internalClipboardRef.value != null) {
72
+ text = internalClipboardRef.value;
73
73
  }
74
74
  if (!text.trim())
75
75
  return;
@@ -107,8 +107,8 @@ export function useClipboard(params) {
107
107
  });
108
108
  }
109
109
  }
110
- if (cutRangeInternal) {
111
- const cut = cutRangeInternal;
110
+ if (cutRangeRef.value) {
111
+ const cut = cutRangeRef.value;
112
112
  for (let r = cut.startRow; r <= cut.endRow; r++) {
113
113
  for (let c = cut.startCol; c <= cut.endCol; c++) {
114
114
  if (r >= currentItems.length || c >= currentCols.length)
@@ -132,7 +132,7 @@ export function useClipboard(params) {
132
132
  });
133
133
  }
134
134
  }
135
- cutRangeInternal = null;
135
+ cutRangeRef.value = null;
136
136
  cutRange.value = null;
137
137
  }
138
138
  endBatch?.();
@@ -141,7 +141,7 @@ export function useClipboard(params) {
141
141
  const clearClipboardRanges = () => {
142
142
  copyRange.value = null;
143
143
  cutRange.value = null;
144
- cutRangeInternal = null;
144
+ cutRangeRef.value = null;
145
145
  };
146
- return { handleCopy, handleCut, handlePaste, cutRange, copyRange, clearClipboardRanges };
146
+ return { handleCopy, handleCut, handlePaste, cutRange, copyRange, clearClipboardRanges, cutRangeRef };
147
147
  }
@@ -0,0 +1,103 @@
1
+ import { ref, onUnmounted } from 'vue';
2
+ import { calculateDropTarget, reorderColumnArray, getPinStateForColumn, } from '@alaarab/ogrid-core';
3
+ /** Width of the resize handle zone on the right edge of each header cell. */
4
+ const RESIZE_HANDLE_ZONE = 8;
5
+ /** Minimum drag distance (px) before activating reorder to prevent accidental drags on click. */
6
+ const MIN_DRAG_DISTANCE = 5;
7
+ /**
8
+ * Manages column reordering via drag-and-drop on header cells.
9
+ * Uses RAF-throttled mouse tracking and core's calculateDropTarget/reorderColumnArray.
10
+ */
11
+ export function useColumnReorder(params) {
12
+ const { columnOrder, onColumnOrderChange, tableRef, pinnedColumns } = params;
13
+ const isDragging = ref(false);
14
+ const dropIndicatorX = ref(null);
15
+ let draggedColumnId = null;
16
+ let draggedPinState = 'unpinned';
17
+ let rafId = 0;
18
+ let cleanupFn = null;
19
+ onUnmounted(() => {
20
+ cleanupFn?.();
21
+ cleanupFn = null;
22
+ });
23
+ const handleHeaderMouseDown = (columnId, event) => {
24
+ if (event.button !== 0)
25
+ return;
26
+ // Skip if in resize handle zone (right 8px of the header cell)
27
+ const th = event.target.closest('th');
28
+ if (th) {
29
+ const rect = th.getBoundingClientRect();
30
+ if (event.clientX > rect.right - RESIZE_HANDLE_ZONE)
31
+ return;
32
+ }
33
+ event.preventDefault();
34
+ const table = tableRef.value;
35
+ if (!table)
36
+ return;
37
+ if (!onColumnOrderChange.value)
38
+ return;
39
+ draggedColumnId = columnId;
40
+ draggedPinState = getPinStateForColumn(columnId, pinnedColumns?.value);
41
+ dropIndicatorX.value = null;
42
+ const startX = event.clientX;
43
+ let hasMoved = false;
44
+ let latestMouseX = event.clientX;
45
+ let targetIndex = -1;
46
+ const prevCursor = document.body.style.cursor;
47
+ const prevUserSelect = document.body.style.userSelect;
48
+ document.body.style.cursor = 'grabbing';
49
+ document.body.style.userSelect = 'none';
50
+ const onMove = (moveEvent) => {
51
+ // Require minimum drag distance before activating
52
+ if (!hasMoved && Math.abs(moveEvent.clientX - startX) < MIN_DRAG_DISTANCE)
53
+ return;
54
+ if (!hasMoved) {
55
+ hasMoved = true;
56
+ isDragging.value = true;
57
+ }
58
+ latestMouseX = moveEvent.clientX;
59
+ if (!rafId) {
60
+ rafId = requestAnimationFrame(() => {
61
+ rafId = 0;
62
+ const tableEl = tableRef.value;
63
+ if (!tableEl || !draggedColumnId)
64
+ return;
65
+ const result = calculateDropTarget(latestMouseX, columnOrder.value, draggedColumnId, draggedPinState, tableEl, pinnedColumns?.value);
66
+ if (result) {
67
+ targetIndex = result.targetIndex;
68
+ dropIndicatorX.value = result.indicatorX;
69
+ }
70
+ else {
71
+ dropIndicatorX.value = null;
72
+ }
73
+ });
74
+ }
75
+ };
76
+ const cleanup = () => {
77
+ window.removeEventListener('mousemove', onMove, true);
78
+ window.removeEventListener('mouseup', onUp, true);
79
+ cleanupFn = null;
80
+ document.body.style.cursor = prevCursor;
81
+ document.body.style.userSelect = prevUserSelect;
82
+ if (rafId) {
83
+ cancelAnimationFrame(rafId);
84
+ rafId = 0;
85
+ }
86
+ };
87
+ const onUp = () => {
88
+ cleanup();
89
+ if (hasMoved && draggedColumnId && targetIndex >= 0 && onColumnOrderChange.value) {
90
+ const newOrder = reorderColumnArray(columnOrder.value, draggedColumnId, targetIndex);
91
+ onColumnOrderChange.value(newOrder);
92
+ }
93
+ draggedColumnId = null;
94
+ isDragging.value = false;
95
+ dropIndicatorX.value = null;
96
+ targetIndex = -1;
97
+ };
98
+ window.addEventListener('mousemove', onMove, true);
99
+ window.addEventListener('mouseup', onUp, true);
100
+ cleanupFn = cleanup;
101
+ };
102
+ return { isDragging, dropIndicatorX, handleHeaderMouseDown };
103
+ }
@@ -1,9 +1,9 @@
1
- import { ref } from 'vue';
1
+ import { shallowRef } from 'vue';
2
2
  /**
3
3
  * Manages context menu position state for right-click menus.
4
4
  */
5
5
  export function useContextMenu() {
6
- const contextMenuPosition = ref(null);
6
+ const contextMenuPosition = shallowRef(null);
7
7
  const setContextMenuPosition = (pos) => {
8
8
  contextMenuPosition.value = pos;
9
9
  };
@@ -20,20 +20,38 @@ export function useDebounce(value, delayMs) {
20
20
  }
21
21
  /**
22
22
  * Returns a stable callback that invokes the given function after the specified delay.
23
- * Each new call resets the timer.
23
+ * Each new call resets the timer. Includes `.cancel()` and `.flush()` methods.
24
24
  */
25
25
  export function useDebouncedCallback(fn, delayMs) {
26
26
  let timeoutId;
27
- // Keep a reference to the latest fn
28
27
  let latestFn = fn;
28
+ let latestArgs;
29
29
  const debounced = ((...args) => {
30
30
  latestFn = fn;
31
+ latestArgs = args;
31
32
  if (timeoutId !== undefined)
32
33
  clearTimeout(timeoutId);
33
34
  timeoutId = setTimeout(() => {
34
35
  latestFn(...args);
36
+ latestArgs = undefined;
37
+ timeoutId = undefined;
35
38
  }, delayMs);
36
39
  });
40
+ debounced.cancel = () => {
41
+ if (timeoutId !== undefined)
42
+ clearTimeout(timeoutId);
43
+ timeoutId = undefined;
44
+ latestArgs = undefined;
45
+ };
46
+ debounced.flush = () => {
47
+ if (timeoutId !== undefined && latestArgs !== undefined) {
48
+ clearTimeout(timeoutId);
49
+ timeoutId = undefined;
50
+ const args = latestArgs;
51
+ latestArgs = undefined;
52
+ latestFn(...args);
53
+ }
54
+ };
37
55
  onUnmounted(() => {
38
56
  if (timeoutId !== undefined)
39
57
  clearTimeout(timeoutId);
@@ -1,12 +1,12 @@
1
- import { ref, watch, onUnmounted } from 'vue';
1
+ import { shallowRef, watch, onUnmounted } from 'vue';
2
2
  import { normalizeSelectionRange, getCellValue, parseValue } from '@alaarab/ogrid-core';
3
3
  const DRAG_ATTR = 'data-drag-range';
4
4
  /**
5
5
  * Manages Excel-style fill handle drag-to-fill for cell ranges.
6
6
  */
7
7
  export function useFillHandle(params) {
8
- const { items, visibleCols, editable, onCellValueChanged, selectionRange, setSelectionRange, setActiveCell, colOffset, wrapperRef, beginBatch, endBatch, } = params;
9
- const fillDrag = ref(null);
8
+ const { items, visibleCols, editable, onCellValueChanged, selectionRange, setSelectionRange, setActiveCell, colOffset, wrapperRef, beginBatch, endBatch, visibleRange, } = params;
9
+ const fillDrag = shallowRef(null);
10
10
  let fillDragEnd = { endRow: 0, endCol: 0 };
11
11
  let rafId = 0;
12
12
  let liveFillRange = null;
@@ -30,9 +30,12 @@ export function useFillHandle(params) {
30
30
  }
31
31
  };
32
32
  watch(fillDrag, (drag) => {
33
- cleanup();
34
- if (!drag || editable.value === false || !onCellValueChanged.value || !wrapperRef.value)
33
+ // Guard early before setting up any state
34
+ if (!drag || editable.value === false || !onCellValueChanged.value || !wrapperRef.value) {
35
+ // Still cleanup if transitioning from active to inactive
36
+ cleanup();
35
37
  return;
38
+ }
36
39
  fillDragEnd = { endRow: drag.startRow, endCol: drag.startCol };
37
40
  liveFillRange = null;
38
41
  const applyDragAttrs = (range) => {
@@ -124,6 +127,12 @@ export function useFillHandle(params) {
124
127
  endRow: end.endRow,
125
128
  endCol: end.endCol,
126
129
  });
130
+ // Clamp fill range to visible + overscan when virtual scrolling is active
131
+ const vr = visibleRange?.value;
132
+ if (vr) {
133
+ norm.startRow = Math.max(norm.startRow, vr.startIndex);
134
+ norm.endRow = Math.min(norm.endRow, vr.endIndex);
135
+ }
127
136
  setSelectionRange(norm);
128
137
  setActiveCell({ rowIndex: end.endRow, columnIndex: end.endCol + colOffset });
129
138
  const currentItems = items.value;
@@ -161,6 +170,10 @@ export function useFillHandle(params) {
161
170
  };
162
171
  window.addEventListener('mousemove', moveListener, true);
163
172
  window.addEventListener('mouseup', upListener, true);
173
+ // Return cleanup function - Vue will call this BEFORE next watch run
174
+ return () => {
175
+ cleanup();
176
+ };
164
177
  });
165
178
  onUnmounted(() => cleanup());
166
179
  const handleFillHandleMouseDown = (e) => {
@@ -1,4 +1,6 @@
1
+ import { computed } from 'vue';
1
2
  import { normalizeSelectionRange, getCellValue, parseValue } from '@alaarab/ogrid-core';
3
+ import { useLatestRef } from './useLatestRef';
2
4
  /**
3
5
  * Excel-style Ctrl+Arrow: find the target position along a 1D axis.
4
6
  */
@@ -27,9 +29,10 @@ function findCtrlTarget(pos, edge, step, isEmpty) {
27
29
  * Handles all keyboard navigation, shortcuts, and cell editing triggers for the grid.
28
30
  */
29
31
  export function useKeyboardNavigation(params) {
30
- // Read latest values from refs on each call no memoization needed in Vue
32
+ // Store latest params in a ref so handleGridKeyDown is a stable callback
33
+ const paramsRef = useLatestRef(computed(() => params));
31
34
  const handleGridKeyDown = (e) => {
32
- const { data, state, handlers, features } = params;
35
+ const { data, state, handlers, features } = paramsRef.value;
33
36
  const items = data.items.value;
34
37
  const visibleCols = data.visibleCols.value;
35
38
  const { colOffset, getRowId } = data;
@@ -44,6 +47,7 @@ export function useKeyboardNavigation(params) {
44
47
  const onCellValueChanged = features.onCellValueChanged.value;
45
48
  const rowSelection = features.rowSelection.value;
46
49
  const wrapperRef = features.wrapperRef;
50
+ const scrollToRow = features.scrollToRow;
47
51
  const maxRowIndex = items.length - 1;
48
52
  const maxColIndex = visibleColumnCount - 1 + colOffset;
49
53
  if (items.length === 0)
@@ -107,6 +111,7 @@ export function useKeyboardNavigation(params) {
107
111
  setSelectionRange({ startRow: newRow, startCol: dataColIndex, endRow: newRow, endCol: dataColIndex });
108
112
  }
109
113
  setActiveCell({ rowIndex: newRow, columnIndex });
114
+ scrollToRow?.(newRow, 'center');
110
115
  break;
111
116
  }
112
117
  case 'ArrowUp': {
@@ -127,6 +132,7 @@ export function useKeyboardNavigation(params) {
127
132
  setSelectionRange({ startRow: newRowUp, startCol: dataColIndex, endRow: newRowUp, endCol: dataColIndex });
128
133
  }
129
134
  setActiveCell({ rowIndex: newRowUp, columnIndex });
135
+ scrollToRow?.(newRowUp, 'center');
130
136
  break;
131
137
  }
132
138
  case 'ArrowRight': {
@@ -0,0 +1,27 @@
1
+ import { customRef, unref } from 'vue';
2
+ /**
3
+ * Returns a ref that always holds the latest value.
4
+ * Useful for capturing volatile state in stable callbacks
5
+ * without adding the value to reactive dependencies.
6
+ *
7
+ * Similar to React's useLatestRef, but uses Vue's customRef for synchronous updates.
8
+ * The returned ref does NOT trigger reactivity when read - it's a "silent" ref
9
+ * that always returns the current value without tracking dependencies.
10
+ */
11
+ export function useLatestRef(source) {
12
+ let value = unref(source);
13
+ return customRef((track, trigger) => ({
14
+ get() {
15
+ // Update value from source on every read (if source is a ref)
16
+ if (typeof source === 'object' && source !== null && 'value' in source) {
17
+ value = source.value;
18
+ }
19
+ // Don't call track() - we don't want to add this to reactive dependencies
20
+ return value;
21
+ },
22
+ set(newValue) {
23
+ value = newValue;
24
+ trigger(); // Still allow setting and triggering if needed
25
+ },
26
+ }));
27
+ }
@@ -243,11 +243,20 @@ export function useOGrid(props) {
243
243
  const sideBarProps = computed(() => {
244
244
  if (!sideBarState.isEnabled)
245
245
  return null;
246
+ // Re-read reactive deps so the computed tracks them, but use getters for
247
+ // activePanel/isOpen so that a stored reference stays current after toggle/close.
248
+ const _activePanel = sideBarState.activePanel.value;
249
+ const _isOpen = sideBarState.isOpen.value;
250
+ void _activePanel;
251
+ void _isOpen;
246
252
  return {
247
- activePanel: sideBarState.activePanel.value,
253
+ get activePanel() { return sideBarState.activePanel.value; },
248
254
  onPanelChange: sideBarState.setActivePanel,
249
255
  panels: sideBarState.panels,
250
256
  position: sideBarState.position,
257
+ get isOpen() { return sideBarState.isOpen.value; },
258
+ toggle: sideBarState.toggle,
259
+ close: sideBarState.close,
251
260
  columns: columnChooserColumns.value,
252
261
  visibleColumns: visibleColumns.value,
253
262
  onVisibilityChange: handleVisibilityChange,
@@ -297,6 +306,7 @@ export function useOGrid(props) {
297
306
  getUserByEmail: dataSource.value?.getUserByEmail,
298
307
  layoutMode: props.value.layoutMode,
299
308
  suppressHorizontalScroll: props.value.suppressHorizontalScroll,
309
+ virtualScroll: props.value.virtualScroll,
300
310
  'aria-label': props.value['aria-label'],
301
311
  'aria-labelledby': props.value['aria-labelledby'],
302
312
  emptyState: {
@@ -394,6 +404,14 @@ export function useOGrid(props) {
394
404
  if (isServerSide.value)
395
405
  refreshCounter.value++;
396
406
  },
407
+ scrollToRow: () => {
408
+ // No-op at orchestration level — DataGridTable components implement
409
+ // this via useVirtualScroll.scrollToRow when virtual scrolling is active.
410
+ },
411
+ getColumnOrder: () => columnOrder.value ?? columns.value.map((c) => c.columnId),
412
+ setColumnOrder: (order) => {
413
+ onColumnOrderChange.value?.(order);
414
+ },
397
415
  }));
398
416
  return {
399
417
  dataGridProps,
@@ -16,7 +16,10 @@ export function useRowSelection(params) {
16
16
  return internalSelectedRows.value;
17
17
  });
18
18
  const updateSelection = (newSelectedIds) => {
19
- if (controlledSelectedRows.value === undefined) {
19
+ if (controlledSelectedRows.value !== undefined) {
20
+ controlledSelectedRows.value = newSelectedIds;
21
+ }
22
+ else {
20
23
  internalSelectedRows.value = newSelectedIds;
21
24
  }
22
25
  onSelectionChange?.({
@@ -0,0 +1,84 @@
1
+ import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
2
+ import { computeVisibleRange, computeTotalHeight, getScrollTopForRow, } from '@alaarab/ogrid-core';
3
+ /**
4
+ * Manages virtual scrolling with RAF-throttled scroll handling and ResizeObserver
5
+ * for container height tracking. Uses core's computeVisibleRange for range calculation.
6
+ */
7
+ export function useVirtualScroll(params) {
8
+ const { totalRows, rowHeight, enabled, overscan = 5 } = params;
9
+ const containerRef = ref(null);
10
+ const scrollTop = ref(0);
11
+ const containerHeight = ref(0);
12
+ let rafId = 0;
13
+ let resizeObserver;
14
+ const visibleRange = computed(() => {
15
+ if (!enabled.value) {
16
+ return { startIndex: 0, endIndex: totalRows.value - 1, offsetTop: 0, offsetBottom: 0 };
17
+ }
18
+ return computeVisibleRange(scrollTop.value, rowHeight, containerHeight.value, totalRows.value, overscan);
19
+ });
20
+ const totalHeight = computed(() => {
21
+ if (!enabled.value)
22
+ return 0;
23
+ return computeTotalHeight(totalRows.value, rowHeight);
24
+ });
25
+ const onScroll = () => {
26
+ if (!rafId) {
27
+ rafId = requestAnimationFrame(() => {
28
+ rafId = 0;
29
+ const el = containerRef.value;
30
+ if (el) {
31
+ scrollTop.value = el.scrollTop;
32
+ }
33
+ });
34
+ }
35
+ };
36
+ const measure = () => {
37
+ const el = containerRef.value;
38
+ if (!el)
39
+ return;
40
+ containerHeight.value = el.clientHeight;
41
+ };
42
+ // Watch containerRef to attach/detach scroll listener and ResizeObserver
43
+ watch(containerRef, (el, prevEl) => {
44
+ if (prevEl) {
45
+ prevEl.removeEventListener('scroll', onScroll);
46
+ resizeObserver?.disconnect();
47
+ }
48
+ if (el) {
49
+ el.addEventListener('scroll', onScroll, { passive: true });
50
+ resizeObserver = new ResizeObserver(measure);
51
+ resizeObserver.observe(el);
52
+ measure();
53
+ scrollTop.value = el.scrollTop;
54
+ }
55
+ });
56
+ onMounted(() => {
57
+ const el = containerRef.value;
58
+ if (el) {
59
+ el.addEventListener('scroll', onScroll, { passive: true });
60
+ resizeObserver = new ResizeObserver(measure);
61
+ resizeObserver.observe(el);
62
+ measure();
63
+ scrollTop.value = el.scrollTop;
64
+ }
65
+ });
66
+ onUnmounted(() => {
67
+ const el = containerRef.value;
68
+ if (el) {
69
+ el.removeEventListener('scroll', onScroll);
70
+ }
71
+ resizeObserver?.disconnect();
72
+ if (rafId) {
73
+ cancelAnimationFrame(rafId);
74
+ rafId = 0;
75
+ }
76
+ });
77
+ const scrollToRow = (index, align = 'start') => {
78
+ const el = containerRef.value;
79
+ if (!el)
80
+ return;
81
+ el.scrollTop = getScrollTopForRow(index, rowHeight, containerHeight.value, align);
82
+ };
83
+ return { containerRef, visibleRange, totalHeight, scrollToRow };
84
+ }
package/dist/esm/index.js CHANGED
@@ -2,6 +2,6 @@
2
2
  export * from '@alaarab/ogrid-core';
3
3
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
4
4
  // Composables
5
- export { useOGrid, useDataGridState, useActiveCell, useCellEditing, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useFillHandle, useUndoRedo, useContextMenu, useColumnResize, useFilterOptions, useDebounce, useDebouncedCallback, useTableLayout, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useRichSelectState, useSideBarState, } from './composables';
5
+ export { useOGrid, useDataGridState, useActiveCell, useCellEditing, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useFillHandle, useUndoRedo, useContextMenu, useColumnResize, useColumnReorder, useVirtualScroll, useFilterOptions, useDebounce, useDebouncedCallback, useTableLayout, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useRichSelectState, useSideBarState, } from './composables';
6
6
  // View model utilities (for UI packages)
7
7
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './utils';
@@ -15,6 +15,12 @@ export interface SideBarProps {
15
15
  onPanelChange: (panel: SideBarPanelId | null) => void;
16
16
  panels: SideBarPanelId[];
17
17
  position: 'left' | 'right';
18
+ /** Whether a panel is currently open. */
19
+ isOpen: boolean;
20
+ /** Toggle a specific panel open/closed. */
21
+ toggle: (panel: SideBarPanelId) => void;
22
+ /** Close the sidebar (set activePanel to null). */
23
+ close: () => void;
18
24
  columns: IColumnDefinition[];
19
25
  visibleColumns: Set<string>;
20
26
  onVisibilityChange: (columnKey: string, visible: boolean) => void;
@@ -5,7 +5,7 @@ export type { UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutStat
5
5
  export { useActiveCell } from './useActiveCell';
6
6
  export type { UseActiveCellResult } from './useActiveCell';
7
7
  export { useCellEditing } from './useCellEditing';
8
- export type { EditingCell, UseCellEditingResult } from './useCellEditing';
8
+ export type { EditingCell, UseCellEditingParams, UseCellEditingResult } from './useCellEditing';
9
9
  export { useCellSelection } from './useCellSelection';
10
10
  export type { UseCellSelectionParams, UseCellSelectionResult } from './useCellSelection';
11
11
  export { useClipboard } from './useClipboard';
@@ -25,6 +25,8 @@ export type { UseColumnResizeParams, UseColumnResizeResult } from './useColumnRe
25
25
  export { useFilterOptions } from './useFilterOptions';
26
26
  export type { UseFilterOptionsResult } from './useFilterOptions';
27
27
  export { useDebounce, useDebouncedCallback } from './useDebounce';
28
+ export type { DebouncedFn } from './useDebounce';
29
+ export { useLatestRef } from './useLatestRef';
28
30
  export { useTableLayout } from './useTableLayout';
29
31
  export type { UseTableLayoutParams, UseTableLayoutResult } from './useTableLayout';
30
32
  export { useColumnHeaderFilterState } from './useColumnHeaderFilterState';
@@ -45,3 +47,7 @@ export { useRichSelectState } from './useRichSelectState';
45
47
  export type { UseRichSelectStateParams, UseRichSelectStateResult } from './useRichSelectState';
46
48
  export { useSideBarState } from './useSideBarState';
47
49
  export type { UseSideBarStateParams, UseSideBarStateResult } from './useSideBarState';
50
+ export { useColumnReorder } from './useColumnReorder';
51
+ export type { UseColumnReorderParams, UseColumnReorderResult } from './useColumnReorder';
52
+ export { useVirtualScroll } from './useVirtualScroll';
53
+ export type { UseVirtualScrollParams, UseVirtualScrollResult } from './useVirtualScroll';
@@ -1,7 +1,7 @@
1
1
  import { type Ref, type ShallowRef } from 'vue';
2
2
  import type { IActiveCell, RowId } from '../types';
3
3
  export interface UseActiveCellResult {
4
- activeCell: Ref<IActiveCell | null>;
4
+ activeCell: ShallowRef<IActiveCell | null>;
5
5
  setActiveCell: (cell: IActiveCell | null) => void;
6
6
  }
7
7
  /**
@@ -1,16 +1,21 @@
1
- import { type Ref } from 'vue';
1
+ import { type Ref, type ShallowRef } from 'vue';
2
2
  import type { RowId } from '../types';
3
3
  export interface EditingCell {
4
4
  rowId: RowId;
5
5
  columnId: string;
6
6
  }
7
+ export interface UseCellEditingParams {
8
+ scrollToRow?: (index: number, align?: 'start' | 'center' | 'end') => void;
9
+ getRowIndex?: (rowId: RowId) => number;
10
+ }
7
11
  export interface UseCellEditingResult {
8
- editingCell: Ref<EditingCell | null>;
12
+ editingCell: ShallowRef<EditingCell | null>;
9
13
  setEditingCell: (cell: EditingCell | null) => void;
10
14
  pendingEditorValue: Ref<unknown>;
11
15
  setPendingEditorValue: (value: unknown) => void;
12
16
  }
13
17
  /**
14
18
  * Manages cell editing state: which cell is being edited and its pending value.
19
+ * Optionally scrolls to the cell's row before opening the editor when virtual scrolling is active.
15
20
  */
16
- export declare function useCellEditing(): UseCellEditingResult;
21
+ export declare function useCellEditing(params?: UseCellEditingParams): UseCellEditingResult;
@@ -8,7 +8,7 @@ export interface UseCellSelectionParams {
8
8
  wrapperRef: Ref<HTMLElement | null> | ShallowRef<HTMLElement | null>;
9
9
  }
10
10
  export interface UseCellSelectionResult {
11
- selectionRange: Ref<ISelectionRange | null>;
11
+ selectionRange: ShallowRef<ISelectionRange | null>;
12
12
  setSelectionRange: (range: ISelectionRange | null) => void;
13
13
  handleCellMouseDown: (e: MouseEvent, rowIndex: number, globalColIndex: number) => void;
14
14
  handleSelectAllCells: () => void;
@@ -1,11 +1,11 @@
1
- import { type Ref } from 'vue';
1
+ import { type Ref, type ShallowRef } from 'vue';
2
2
  import type { ISelectionRange, IActiveCell, ICellValueChangedEvent, IColumnDef } from '../types';
3
3
  export interface UseClipboardParams<T> {
4
4
  items: Ref<T[]>;
5
5
  visibleCols: Ref<IColumnDef<T>[]>;
6
6
  colOffset: number;
7
- selectionRange: Ref<ISelectionRange | null>;
8
- activeCell: Ref<IActiveCell | null>;
7
+ selectionRange: Ref<ISelectionRange | null> | ShallowRef<ISelectionRange | null>;
8
+ activeCell: Ref<IActiveCell | null> | ShallowRef<IActiveCell | null>;
9
9
  editable: Ref<boolean | undefined>;
10
10
  onCellValueChanged: Ref<((event: ICellValueChangedEvent<T>) => void) | undefined>;
11
11
  beginBatch?: () => void;
@@ -15,9 +15,10 @@ export interface UseClipboardResult {
15
15
  handleCopy: () => void;
16
16
  handleCut: () => void;
17
17
  handlePaste: () => Promise<void>;
18
- cutRange: Ref<ISelectionRange | null>;
19
- copyRange: Ref<ISelectionRange | null>;
18
+ cutRange: ShallowRef<ISelectionRange | null>;
19
+ copyRange: ShallowRef<ISelectionRange | null>;
20
20
  clearClipboardRanges: () => void;
21
+ cutRangeRef: Ref<ISelectionRange | null>;
21
22
  }
22
23
  /**
23
24
  * Manages copy, cut, and paste operations for cell ranges with TSV clipboard format.
@@ -0,0 +1,20 @@
1
+ import { type Ref } from 'vue';
2
+ export interface UseColumnReorderParams {
3
+ columnOrder: Ref<string[]>;
4
+ onColumnOrderChange: Ref<((order: string[]) => void) | undefined>;
5
+ tableRef: Ref<HTMLElement | null>;
6
+ pinnedColumns?: Ref<{
7
+ left?: string[];
8
+ right?: string[];
9
+ } | undefined>;
10
+ }
11
+ export interface UseColumnReorderResult {
12
+ isDragging: Ref<boolean>;
13
+ dropIndicatorX: Ref<number | null>;
14
+ handleHeaderMouseDown: (columnId: string, event: MouseEvent) => void;
15
+ }
16
+ /**
17
+ * Manages column reordering via drag-and-drop on header cells.
18
+ * Uses RAF-throttled mouse tracking and core's calculateDropTarget/reorderColumnArray.
19
+ */
20
+ export declare function useColumnReorder(params: UseColumnReorderParams): UseColumnReorderResult;
@@ -1,10 +1,10 @@
1
- import { type Ref } from 'vue';
1
+ import { type ShallowRef } from 'vue';
2
2
  export interface ContextMenuPosition {
3
3
  x: number;
4
4
  y: number;
5
5
  }
6
6
  export interface UseContextMenuResult {
7
- contextMenuPosition: Ref<ContextMenuPosition | null>;
7
+ contextMenuPosition: ShallowRef<ContextMenuPosition | null>;
8
8
  setContextMenuPosition: (pos: ContextMenuPosition | null) => void;
9
9
  handleCellContextMenu: (e: {
10
10
  clientX: number;
@@ -3,8 +3,15 @@ import { type Ref } from 'vue';
3
3
  * Returns a debounced ref that updates after the specified delay when the source value changes.
4
4
  */
5
5
  export declare function useDebounce<T>(value: Ref<T>, delayMs: number): Ref<T>;
6
+ export interface DebouncedFn<T extends (...args: unknown[]) => void> {
7
+ (...args: Parameters<T>): void;
8
+ /** Cancel the pending invocation. */
9
+ cancel: () => void;
10
+ /** Execute the pending invocation immediately (no-op if nothing pending). */
11
+ flush: () => void;
12
+ }
6
13
  /**
7
14
  * Returns a stable callback that invokes the given function after the specified delay.
8
- * Each new call resets the timer.
15
+ * Each new call resets the timer. Includes `.cancel()` and `.flush()` methods.
9
16
  */
10
- export declare function useDebouncedCallback<T extends (...args: unknown[]) => void>(fn: T, delayMs: number): T;
17
+ export declare function useDebouncedCallback<T extends (...args: unknown[]) => void>(fn: T, delayMs: number): DebouncedFn<T>;
@@ -1,20 +1,22 @@
1
1
  import { type Ref, type ShallowRef } from 'vue';
2
2
  import type { ISelectionRange, IActiveCell, IColumnDef, ICellValueChangedEvent } from '../types';
3
+ import type { IVisibleRange } from '@alaarab/ogrid-core';
3
4
  export interface UseFillHandleParams<T> {
4
5
  items: Ref<T[]>;
5
6
  visibleCols: Ref<IColumnDef<T>[]>;
6
7
  editable: Ref<boolean | undefined>;
7
8
  onCellValueChanged: Ref<((event: ICellValueChangedEvent<T>) => void) | undefined>;
8
- selectionRange: Ref<ISelectionRange | null>;
9
+ selectionRange: Ref<ISelectionRange | null> | ShallowRef<ISelectionRange | null>;
9
10
  setSelectionRange: (range: ISelectionRange | null) => void;
10
11
  setActiveCell: (cell: IActiveCell | null) => void;
11
12
  colOffset: number;
12
13
  wrapperRef: Ref<HTMLElement | null> | ShallowRef<HTMLElement | null>;
13
14
  beginBatch?: () => void;
14
15
  endBatch?: () => void;
16
+ visibleRange?: Ref<IVisibleRange | null>;
15
17
  }
16
18
  export interface UseFillHandleResult {
17
- fillDrag: Ref<{
19
+ fillDrag: ShallowRef<{
18
20
  startRow: number;
19
21
  startCol: number;
20
22
  } | null>;
@@ -2,6 +2,8 @@ import { type Ref, type ShallowRef } from 'vue';
2
2
  import type { RowId, IActiveCell, ISelectionRange, IColumnDef, ICellValueChangedEvent, RowSelectionMode } from '../types';
3
3
  import type { EditingCell } from './useCellEditing';
4
4
  import type { ContextMenuPosition } from './useContextMenu';
5
+ /** Accept either Ref or ShallowRef for state fields */
6
+ type MaybeShallowRef<T> = Ref<T> | ShallowRef<T>;
5
7
  export interface UseKeyboardNavigationParams<T> {
6
8
  data: {
7
9
  items: Ref<T[]>;
@@ -12,9 +14,9 @@ export interface UseKeyboardNavigationParams<T> {
12
14
  getRowId: (item: T) => RowId;
13
15
  };
14
16
  state: {
15
- activeCell: Ref<IActiveCell | null>;
16
- selectionRange: Ref<ISelectionRange | null>;
17
- editingCell: Ref<EditingCell | null>;
17
+ activeCell: MaybeShallowRef<IActiveCell | null>;
18
+ selectionRange: MaybeShallowRef<ISelectionRange | null>;
19
+ editingCell: MaybeShallowRef<EditingCell | null>;
18
20
  selectedRowIds: Ref<Set<RowId>>;
19
21
  };
20
22
  handlers: {
@@ -35,6 +37,7 @@ export interface UseKeyboardNavigationParams<T> {
35
37
  onCellValueChanged: Ref<((event: ICellValueChangedEvent<T>) => void) | undefined>;
36
38
  rowSelection: Ref<RowSelectionMode>;
37
39
  wrapperRef: Ref<HTMLElement | null> | ShallowRef<HTMLElement | null>;
40
+ scrollToRow?: (index: number, align?: 'start' | 'center' | 'end') => void;
38
41
  };
39
42
  }
40
43
  export interface UseKeyboardNavigationResult {
@@ -44,3 +47,4 @@ export interface UseKeyboardNavigationResult {
44
47
  * Handles all keyboard navigation, shortcuts, and cell editing triggers for the grid.
45
48
  */
46
49
  export declare function useKeyboardNavigation<T>(params: UseKeyboardNavigationParams<T>): UseKeyboardNavigationResult;
50
+ export {};
@@ -0,0 +1,11 @@
1
+ import { type Ref } from 'vue';
2
+ /**
3
+ * Returns a ref that always holds the latest value.
4
+ * Useful for capturing volatile state in stable callbacks
5
+ * without adding the value to reactive dependencies.
6
+ *
7
+ * Similar to React's useLatestRef, but uses Vue's customRef for synchronous updates.
8
+ * The returned ref does NOT trigger reactivity when read - it's a "silent" ref
9
+ * that always returns the current value without tracking dependencies.
10
+ */
11
+ export declare function useLatestRef<T>(source: Ref<T> | T): Ref<T>;
@@ -0,0 +1,19 @@
1
+ import { type Ref } from 'vue';
2
+ import type { IVisibleRange } from '@alaarab/ogrid-core';
3
+ export interface UseVirtualScrollParams {
4
+ totalRows: Ref<number>;
5
+ rowHeight: number;
6
+ enabled: Ref<boolean>;
7
+ overscan?: number;
8
+ }
9
+ export interface UseVirtualScrollResult {
10
+ containerRef: Ref<HTMLElement | null>;
11
+ visibleRange: Ref<IVisibleRange>;
12
+ totalHeight: Ref<number>;
13
+ scrollToRow: (index: number, align?: 'start' | 'center' | 'end') => void;
14
+ }
15
+ /**
16
+ * Manages virtual scrolling with RAF-throttled scroll handling and ResizeObserver
17
+ * for container height tracking. Uses core's computeVisibleRange for range calculation.
18
+ */
19
+ export declare function useVirtualScroll(params: UseVirtualScrollParams): UseVirtualScrollResult;
@@ -1,9 +1,9 @@
1
1
  export * from '@alaarab/ogrid-core';
2
2
  export type { IColumnDef, ICellEditorProps, IOGridProps, IOGridClientProps, IOGridServerProps, IOGridDataGridProps, } from './types';
3
- export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, CellEditorParams, IValueParserParams, IDateFilterValue, HeaderCell, HeaderRow, RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, } from './types';
3
+ export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, CellEditorParams, IValueParserParams, IDateFilterValue, HeaderCell, HeaderRow, RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, } from './types';
4
4
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
5
- export { useOGrid, useDataGridState, useActiveCell, useCellEditing, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useFillHandle, useUndoRedo, useContextMenu, useColumnResize, useFilterOptions, useDebounce, useDebouncedCallback, useTableLayout, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useRichSelectState, useSideBarState, } from './composables';
6
- export type { UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGridLayout, UseOGridFilters, ColumnChooserPlacement, UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, UseActiveCellResult, EditingCell, UseCellEditingResult, UseCellSelectionParams, UseCellSelectionResult, UseClipboardParams, UseClipboardResult, UseRowSelectionParams, UseRowSelectionResult, UseKeyboardNavigationParams, UseKeyboardNavigationResult, UseFillHandleParams, UseFillHandleResult, UseUndoRedoParams, UseUndoRedoResult, ContextMenuPosition, UseContextMenuResult, UseColumnResizeParams, UseColumnResizeResult, UseFilterOptionsResult, UseTableLayoutParams, UseTableLayoutResult, UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, UseTextFilterStateParams, UseTextFilterStateResult, UseMultiSelectFilterStateParams, UseMultiSelectFilterStateResult, UsePeopleFilterStateParams, UsePeopleFilterStateResult, UseDateFilterStateParams, UseDateFilterStateResult, UseColumnChooserStateParams, UseColumnChooserStateResult, InlineCellEditorType, UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, UseRichSelectStateParams, UseRichSelectStateResult, UseSideBarStateParams, UseSideBarStateResult, } from './composables';
5
+ export { useOGrid, useDataGridState, useActiveCell, useCellEditing, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useFillHandle, useUndoRedo, useContextMenu, useColumnResize, useColumnReorder, useVirtualScroll, useFilterOptions, useDebounce, useDebouncedCallback, useTableLayout, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useRichSelectState, useSideBarState, } from './composables';
6
+ export type { UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGridLayout, UseOGridFilters, ColumnChooserPlacement, UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, UseActiveCellResult, EditingCell, UseCellEditingParams, UseCellEditingResult, UseCellSelectionParams, UseCellSelectionResult, UseClipboardParams, UseClipboardResult, UseRowSelectionParams, UseRowSelectionResult, UseKeyboardNavigationParams, UseKeyboardNavigationResult, UseFillHandleParams, UseFillHandleResult, UseUndoRedoParams, UseUndoRedoResult, ContextMenuPosition, UseContextMenuResult, UseColumnResizeParams, UseColumnResizeResult, UseColumnReorderParams, UseColumnReorderResult, UseVirtualScrollParams, UseVirtualScrollResult, UseFilterOptionsResult, UseTableLayoutParams, UseTableLayoutResult, UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, UseTextFilterStateParams, UseTextFilterStateResult, UseMultiSelectFilterStateParams, UseMultiSelectFilterStateResult, UsePeopleFilterStateParams, UsePeopleFilterStateResult, UseDateFilterStateParams, UseDateFilterStateResult, UseColumnChooserStateParams, UseColumnChooserStateResult, InlineCellEditorType, UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, UseRichSelectStateParams, UseRichSelectStateResult, UseSideBarStateParams, UseSideBarStateResult, DebouncedFn, } from './composables';
7
7
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './utils';
8
8
  export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, CellInteractionHandlers, CellInteractionProps, } from './utils';
9
9
  export type { SideBarProps, SideBarFilterColumn } from './components/SideBar';
@@ -1,7 +1,7 @@
1
1
  import type { IColumnDef, IColumnGroupDef, ICellValueChangedEvent } from './columnTypes';
2
- export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IOGridApi, } from '@alaarab/ogrid-core';
2
+ export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IOGridApi, IVirtualScrollConfig, } from '@alaarab/ogrid-core';
3
3
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from '@alaarab/ogrid-core';
4
- import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef } from '@alaarab/ogrid-core';
4
+ import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef, IVirtualScrollConfig } from '@alaarab/ogrid-core';
5
5
  /** Base props shared by both client-side and server-side OGrid modes. */
6
6
  interface IOGridBaseProps<T> {
7
7
  columns: (IColumnDef<T> | IColumnGroupDef<T>)[];
@@ -73,6 +73,8 @@ interface IOGridBaseProps<T> {
73
73
  onError?: (error: unknown) => void;
74
74
  /** Called when a cell renderer or custom editor throws an error. */
75
75
  onCellError?: (error: Error, info: unknown) => void;
76
+ /** Virtual scrolling configuration. Set `enabled: true` with a fixed `rowHeight` to virtualize large datasets. */
77
+ virtualScroll?: IVirtualScrollConfig;
76
78
  'aria-label'?: string;
77
79
  'aria-labelledby'?: string;
78
80
  }
@@ -146,6 +148,8 @@ export interface IOGridDataGridProps<T> {
146
148
  };
147
149
  /** Called when a cell renderer or custom editor throws an error. */
148
150
  onCellError?: (error: Error, info: unknown) => void;
151
+ /** Virtual scrolling configuration. */
152
+ virtualScroll?: IVirtualScrollConfig;
149
153
  'aria-label'?: string;
150
154
  'aria-labelledby'?: string;
151
155
  }
@@ -1,3 +1,3 @@
1
1
  export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, IDateFilterValue, HeaderCell, HeaderRow, } from './columnTypes';
2
- export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridClientProps, IOGridServerProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, } from './dataGridTypes';
2
+ export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridClientProps, IOGridServerProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, } from './dataGridTypes';
3
3
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './dataGridTypes';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-vue",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
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",
@@ -22,7 +22,7 @@
22
22
  "files": ["dist", "README.md", "LICENSE"],
23
23
  "engines": { "node": ">=18" },
24
24
  "dependencies": {
25
- "@alaarab/ogrid-core": "2.0.2"
25
+ "@alaarab/ogrid-core": "2.0.3"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "vue": "^3.3.0"