@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.
- package/dist/esm/composables/index.js +3 -0
- package/dist/esm/composables/useActiveCell.js +26 -7
- package/dist/esm/composables/useCellEditing.js +10 -3
- package/dist/esm/composables/useCellSelection.js +8 -4
- package/dist/esm/composables/useClipboard.js +14 -14
- package/dist/esm/composables/useColumnReorder.js +103 -0
- package/dist/esm/composables/useContextMenu.js +2 -2
- package/dist/esm/composables/useDebounce.js +20 -2
- package/dist/esm/composables/useFillHandle.js +18 -5
- package/dist/esm/composables/useKeyboardNavigation.js +8 -2
- package/dist/esm/composables/useLatestRef.js +27 -0
- package/dist/esm/composables/useOGrid.js +19 -1
- package/dist/esm/composables/useRowSelection.js +4 -1
- package/dist/esm/composables/useVirtualScroll.js +84 -0
- package/dist/esm/index.js +1 -1
- package/dist/types/components/SideBar.d.ts +6 -0
- package/dist/types/composables/index.d.ts +7 -1
- package/dist/types/composables/useActiveCell.d.ts +1 -1
- package/dist/types/composables/useCellEditing.d.ts +8 -3
- package/dist/types/composables/useCellSelection.d.ts +1 -1
- package/dist/types/composables/useClipboard.d.ts +6 -5
- package/dist/types/composables/useColumnReorder.d.ts +20 -0
- package/dist/types/composables/useContextMenu.d.ts +2 -2
- package/dist/types/composables/useDebounce.d.ts +9 -2
- package/dist/types/composables/useFillHandle.d.ts +4 -2
- package/dist/types/composables/useKeyboardNavigation.d.ts +7 -3
- package/dist/types/composables/useLatestRef.d.ts +11 -0
- package/dist/types/composables/useVirtualScroll.d.ts +19 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/types/dataGridTypes.d.ts +6 -2
- package/dist/types/types/index.d.ts +1 -1
- 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 {
|
|
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 =
|
|
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
|
-
//
|
|
24
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
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
|
-
|
|
9
|
-
const cutRange =
|
|
10
|
-
const copyRange =
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() &&
|
|
72
|
-
text =
|
|
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 (
|
|
111
|
-
const cut =
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
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 } =
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
19
|
-
copyRange:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
16
|
-
selectionRange:
|
|
17
|
-
editingCell:
|
|
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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
25
|
+
"@alaarab/ogrid-core": "2.0.3"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"vue": "^3.3.0"
|