@alaarab/ogrid-vue 2.0.2

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 (65) hide show
  1. package/dist/esm/components/SideBar.js +1 -0
  2. package/dist/esm/composables/index.js +27 -0
  3. package/dist/esm/composables/useActiveCell.js +58 -0
  4. package/dist/esm/composables/useCellEditing.js +20 -0
  5. package/dist/esm/composables/useCellSelection.js +281 -0
  6. package/dist/esm/composables/useClipboard.js +147 -0
  7. package/dist/esm/composables/useColumnChooserState.js +77 -0
  8. package/dist/esm/composables/useColumnHeaderFilterState.js +186 -0
  9. package/dist/esm/composables/useColumnResize.js +73 -0
  10. package/dist/esm/composables/useContextMenu.js +23 -0
  11. package/dist/esm/composables/useDataGridState.js +308 -0
  12. package/dist/esm/composables/useDateFilterState.js +36 -0
  13. package/dist/esm/composables/useDebounce.js +42 -0
  14. package/dist/esm/composables/useFillHandle.js +175 -0
  15. package/dist/esm/composables/useFilterOptions.js +39 -0
  16. package/dist/esm/composables/useInlineCellEditorState.js +42 -0
  17. package/dist/esm/composables/useKeyboardNavigation.js +353 -0
  18. package/dist/esm/composables/useMultiSelectFilterState.js +59 -0
  19. package/dist/esm/composables/useOGrid.js +406 -0
  20. package/dist/esm/composables/usePeopleFilterState.js +66 -0
  21. package/dist/esm/composables/useRichSelectState.js +59 -0
  22. package/dist/esm/composables/useRowSelection.js +75 -0
  23. package/dist/esm/composables/useSideBarState.js +41 -0
  24. package/dist/esm/composables/useTableLayout.js +85 -0
  25. package/dist/esm/composables/useTextFilterState.js +26 -0
  26. package/dist/esm/composables/useUndoRedo.js +75 -0
  27. package/dist/esm/index.js +7 -0
  28. package/dist/esm/types/columnTypes.js +1 -0
  29. package/dist/esm/types/dataGridTypes.js +1 -0
  30. package/dist/esm/types/index.js +1 -0
  31. package/dist/esm/utils/dataGridViewModel.js +195 -0
  32. package/dist/esm/utils/index.js +1 -0
  33. package/dist/types/components/SideBar.d.ts +26 -0
  34. package/dist/types/composables/index.d.ts +47 -0
  35. package/dist/types/composables/useActiveCell.d.ts +14 -0
  36. package/dist/types/composables/useCellEditing.d.ts +16 -0
  37. package/dist/types/composables/useCellSelection.d.ts +20 -0
  38. package/dist/types/composables/useClipboard.d.ts +25 -0
  39. package/dist/types/composables/useColumnChooserState.d.ts +22 -0
  40. package/dist/types/composables/useColumnHeaderFilterState.d.ts +68 -0
  41. package/dist/types/composables/useColumnResize.d.ts +21 -0
  42. package/dist/types/composables/useContextMenu.d.ts +19 -0
  43. package/dist/types/composables/useDataGridState.d.ts +129 -0
  44. package/dist/types/composables/useDateFilterState.d.ts +16 -0
  45. package/dist/types/composables/useDebounce.d.ts +10 -0
  46. package/dist/types/composables/useFillHandle.d.ts +30 -0
  47. package/dist/types/composables/useFilterOptions.d.ts +15 -0
  48. package/dist/types/composables/useInlineCellEditorState.d.ts +20 -0
  49. package/dist/types/composables/useKeyboardNavigation.d.ts +46 -0
  50. package/dist/types/composables/useMultiSelectFilterState.d.ts +20 -0
  51. package/dist/types/composables/useOGrid.d.ts +52 -0
  52. package/dist/types/composables/usePeopleFilterState.d.ts +20 -0
  53. package/dist/types/composables/useRichSelectState.d.ts +21 -0
  54. package/dist/types/composables/useRowSelection.d.ts +21 -0
  55. package/dist/types/composables/useSideBarState.d.ts +19 -0
  56. package/dist/types/composables/useTableLayout.d.ts +27 -0
  57. package/dist/types/composables/useTextFilterState.d.ts +13 -0
  58. package/dist/types/composables/useUndoRedo.d.ts +21 -0
  59. package/dist/types/index.d.ts +9 -0
  60. package/dist/types/types/columnTypes.d.ts +25 -0
  61. package/dist/types/types/dataGridTypes.d.ts +151 -0
  62. package/dist/types/types/index.d.ts +3 -0
  63. package/dist/types/utils/dataGridViewModel.d.ts +137 -0
  64. package/dist/types/utils/index.d.ts +2 -0
  65. package/package.json +38 -0
@@ -0,0 +1,186 @@
1
+ import { ref, computed, watch, onUnmounted } from 'vue';
2
+ import { useTextFilterState } from './useTextFilterState';
3
+ import { useMultiSelectFilterState } from './useMultiSelectFilterState';
4
+ import { usePeopleFilterState } from './usePeopleFilterState';
5
+ import { useDateFilterState } from './useDateFilterState';
6
+ const EMPTY_OPTIONS = [];
7
+ export function useColumnHeaderFilterState(params) {
8
+ const { filterType, onSort, } = params;
9
+ const safeSelectedValues = () => params.selectedValues ?? EMPTY_OPTIONS;
10
+ // Shared state
11
+ const headerRef = ref(null);
12
+ const popoverRef = ref(null);
13
+ const isFilterOpen = ref(false);
14
+ const popoverPosition = ref(null);
15
+ const setFilterOpen = (open) => {
16
+ isFilterOpen.value = open;
17
+ };
18
+ const isFilterOpenGetter = () => isFilterOpen.value;
19
+ // Compose sub-hooks
20
+ const textFilterState = useTextFilterState({
21
+ textValue: params.textValue,
22
+ onTextChange: params.onTextChange,
23
+ isFilterOpen: isFilterOpenGetter,
24
+ });
25
+ const multiSelectFilterState = useMultiSelectFilterState({
26
+ selectedValues: params.selectedValues,
27
+ onFilterChange: params.onFilterChange,
28
+ options: params.options,
29
+ isFilterOpen: isFilterOpenGetter,
30
+ });
31
+ const peopleFilterState = usePeopleFilterState({
32
+ selectedUser: params.selectedUser,
33
+ onUserChange: params.onUserChange,
34
+ peopleSearch: params.peopleSearch,
35
+ isFilterOpen: isFilterOpenGetter,
36
+ filterType,
37
+ });
38
+ const dateFilterState = useDateFilterState({
39
+ dateValue: params.dateValue,
40
+ onDateChange: params.onDateChange,
41
+ isFilterOpen: isFilterOpenGetter,
42
+ });
43
+ // Close popover resets position
44
+ watch(isFilterOpen, (open) => {
45
+ if (!open) {
46
+ popoverPosition.value = null;
47
+ }
48
+ });
49
+ // Click outside and Escape to close
50
+ let clickOutsideHandler = null;
51
+ let keyDownHandler = null;
52
+ let clickOutsideTimeout;
53
+ const setupListeners = () => {
54
+ cleanupListeners();
55
+ clickOutsideHandler = (e) => {
56
+ const target = e.target;
57
+ if (popoverRef.value && !popoverRef.value.contains(target) &&
58
+ headerRef.value && !headerRef.value.contains(target)) {
59
+ isFilterOpen.value = false;
60
+ }
61
+ };
62
+ keyDownHandler = (e) => {
63
+ if (e.key === 'Escape' || e.key === 'Esc') {
64
+ e.preventDefault();
65
+ e.stopPropagation();
66
+ isFilterOpen.value = false;
67
+ }
68
+ };
69
+ clickOutsideTimeout = setTimeout(() => document.addEventListener('mousedown', clickOutsideHandler), 0);
70
+ document.addEventListener('keydown', keyDownHandler, true);
71
+ };
72
+ const cleanupListeners = () => {
73
+ if (clickOutsideTimeout)
74
+ clearTimeout(clickOutsideTimeout);
75
+ if (clickOutsideHandler)
76
+ document.removeEventListener('mousedown', clickOutsideHandler);
77
+ if (keyDownHandler)
78
+ document.removeEventListener('keydown', keyDownHandler, true);
79
+ clickOutsideHandler = null;
80
+ keyDownHandler = null;
81
+ };
82
+ watch(isFilterOpen, (open) => {
83
+ if (open)
84
+ setupListeners();
85
+ else
86
+ cleanupListeners();
87
+ });
88
+ onUnmounted(() => cleanupListeners());
89
+ // Shared handlers
90
+ const handleFilterIconClick = (e) => {
91
+ e.stopPropagation();
92
+ e.preventDefault();
93
+ if (!isFilterOpen.value && headerRef.value) {
94
+ const rect = headerRef.value.getBoundingClientRect();
95
+ popoverPosition.value = { top: rect.bottom + 4, left: rect.left };
96
+ }
97
+ isFilterOpen.value = !isFilterOpen.value;
98
+ };
99
+ const handleSortClick = (e) => {
100
+ e.stopPropagation();
101
+ onSort?.();
102
+ };
103
+ const handleApplyMultiSelect = () => {
104
+ multiSelectFilterState.handleApplyMultiSelect();
105
+ isFilterOpen.value = false;
106
+ };
107
+ const handleTextApply = () => {
108
+ textFilterState.handleTextApply();
109
+ isFilterOpen.value = false;
110
+ };
111
+ const handleUserSelect = (user) => {
112
+ peopleFilterState.handleUserSelect(user);
113
+ isFilterOpen.value = false;
114
+ };
115
+ const handleClearUser = () => {
116
+ peopleFilterState.handleClearUser();
117
+ isFilterOpen.value = false;
118
+ };
119
+ const handleDateApply = () => {
120
+ dateFilterState.handleDateApply();
121
+ isFilterOpen.value = false;
122
+ };
123
+ const handlePopoverClick = (e) => e.stopPropagation();
124
+ const handleInputFocus = (e) => e.stopPropagation();
125
+ const handleInputMouseDown = (e) => e.stopPropagation();
126
+ const handleInputClick = (e) => e.stopPropagation();
127
+ const handleInputKeyDown = (e) => {
128
+ if (e.key !== 'Escape' && e.key !== 'Esc')
129
+ e.stopPropagation();
130
+ };
131
+ const hasActiveFilter = computed(() => {
132
+ if (filterType === 'multiSelect')
133
+ return safeSelectedValues().length > 0;
134
+ if (filterType === 'text')
135
+ return !!(params.textValue ?? '').trim();
136
+ if (filterType === 'people')
137
+ return !!params.selectedUser;
138
+ if (filterType === 'date')
139
+ return !!(params.dateValue?.from || params.dateValue?.to);
140
+ return false;
141
+ });
142
+ return {
143
+ headerRef,
144
+ popoverRef,
145
+ peopleInputRef: peopleFilterState.peopleInputRef,
146
+ isFilterOpen,
147
+ setFilterOpen,
148
+ tempSelected: multiSelectFilterState.tempSelected,
149
+ setTempSelected: multiSelectFilterState.setTempSelected,
150
+ tempTextValue: textFilterState.tempTextValue,
151
+ setTempTextValue: textFilterState.setTempTextValue,
152
+ searchText: multiSelectFilterState.searchText,
153
+ setSearchText: multiSelectFilterState.setSearchText,
154
+ debouncedSearchText: multiSelectFilterState.debouncedSearchText,
155
+ filteredOptions: multiSelectFilterState.filteredOptions,
156
+ peopleSuggestions: peopleFilterState.peopleSuggestions,
157
+ isPeopleLoading: peopleFilterState.isPeopleLoading,
158
+ peopleSearchText: peopleFilterState.peopleSearchText,
159
+ setPeopleSearchText: peopleFilterState.setPeopleSearchText,
160
+ tempDateFrom: dateFilterState.tempDateFrom,
161
+ setTempDateFrom: dateFilterState.setTempDateFrom,
162
+ tempDateTo: dateFilterState.tempDateTo,
163
+ setTempDateTo: dateFilterState.setTempDateTo,
164
+ hasActiveFilter,
165
+ popoverPosition,
166
+ handlers: {
167
+ handleFilterIconClick,
168
+ handleApplyMultiSelect,
169
+ handleTextApply,
170
+ handleTextClear: textFilterState.handleTextClear,
171
+ handleUserSelect,
172
+ handleClearUser,
173
+ handleCheckboxChange: multiSelectFilterState.handleCheckboxChange,
174
+ handleSelectAll: multiSelectFilterState.handleSelectAll,
175
+ handleClearSelection: multiSelectFilterState.handleClearSelection,
176
+ handlePopoverClick,
177
+ handleInputFocus,
178
+ handleInputMouseDown,
179
+ handleInputClick,
180
+ handleInputKeyDown,
181
+ handleDateApply,
182
+ handleDateClear: dateFilterState.handleDateClear,
183
+ handleSortClick,
184
+ },
185
+ };
186
+ }
@@ -0,0 +1,73 @@
1
+ import { onUnmounted } from 'vue';
2
+ /**
3
+ * Manages column resize drag interactions with RAF-throttled state updates.
4
+ */
5
+ export function useColumnResize(params) {
6
+ const { columnSizingOverrides, setColumnSizingOverrides, minWidth = 80, defaultWidth = 120, onColumnResized, } = params;
7
+ let rafId = 0;
8
+ let cleanupFn = null;
9
+ onUnmounted(() => {
10
+ cleanupFn?.();
11
+ cleanupFn = null;
12
+ });
13
+ const handleResizeStart = (e, col) => {
14
+ e.preventDefault();
15
+ e.stopPropagation();
16
+ const startX = e.clientX;
17
+ const columnId = col.columnId;
18
+ const thEl = e.currentTarget.parentElement;
19
+ const startWidth = thEl
20
+ ? thEl.getBoundingClientRect().width
21
+ : columnSizingOverrides.value[columnId]?.widthPx
22
+ ?? col.idealWidth
23
+ ?? col.defaultWidth
24
+ ?? defaultWidth;
25
+ let latestWidth = startWidth;
26
+ const prevCursor = document.body.style.cursor;
27
+ const prevUserSelect = document.body.style.userSelect;
28
+ document.body.style.cursor = 'col-resize';
29
+ document.body.style.userSelect = 'none';
30
+ const flushWidth = () => {
31
+ setColumnSizingOverrides({
32
+ ...columnSizingOverrides.value,
33
+ [columnId]: { widthPx: latestWidth },
34
+ });
35
+ };
36
+ const onMove = (moveEvent) => {
37
+ const deltaX = moveEvent.clientX - startX;
38
+ latestWidth = Math.max(minWidth, startWidth + deltaX);
39
+ if (!rafId) {
40
+ rafId = requestAnimationFrame(() => {
41
+ rafId = 0;
42
+ flushWidth();
43
+ });
44
+ }
45
+ };
46
+ const cleanup = () => {
47
+ document.removeEventListener('mousemove', onMove);
48
+ document.removeEventListener('mouseup', onUp);
49
+ cleanupFn = null;
50
+ document.body.style.cursor = prevCursor;
51
+ document.body.style.userSelect = prevUserSelect;
52
+ if (rafId) {
53
+ cancelAnimationFrame(rafId);
54
+ rafId = 0;
55
+ }
56
+ };
57
+ const onUp = () => {
58
+ cleanup();
59
+ flushWidth();
60
+ onColumnResized?.(columnId, latestWidth);
61
+ };
62
+ document.addEventListener('mousemove', onMove);
63
+ document.addEventListener('mouseup', onUp);
64
+ cleanupFn = cleanup;
65
+ };
66
+ const getColumnWidth = (col) => {
67
+ return columnSizingOverrides.value[col.columnId]?.widthPx
68
+ ?? col.idealWidth
69
+ ?? col.defaultWidth
70
+ ?? defaultWidth;
71
+ };
72
+ return { handleResizeStart, getColumnWidth };
73
+ }
@@ -0,0 +1,23 @@
1
+ import { ref } from 'vue';
2
+ /**
3
+ * Manages context menu position state for right-click menus.
4
+ */
5
+ export function useContextMenu() {
6
+ const contextMenuPosition = ref(null);
7
+ const setContextMenuPosition = (pos) => {
8
+ contextMenuPosition.value = pos;
9
+ };
10
+ const handleCellContextMenu = (e) => {
11
+ e.preventDefault?.();
12
+ contextMenuPosition.value = { x: e.clientX, y: e.clientY };
13
+ };
14
+ const closeContextMenu = () => {
15
+ contextMenuPosition.value = null;
16
+ };
17
+ return {
18
+ contextMenuPosition,
19
+ setContextMenuPosition,
20
+ handleCellContextMenu,
21
+ closeContextMenu,
22
+ };
23
+ }
@@ -0,0 +1,308 @@
1
+ import { ref, computed } from 'vue';
2
+ import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregations } from '@alaarab/ogrid-core';
3
+ import { useRowSelection } from './useRowSelection';
4
+ import { useCellEditing } from './useCellEditing';
5
+ import { useActiveCell } from './useActiveCell';
6
+ import { useCellSelection } from './useCellSelection';
7
+ import { useContextMenu } from './useContextMenu';
8
+ import { useClipboard } from './useClipboard';
9
+ import { useKeyboardNavigation } from './useKeyboardNavigation';
10
+ import { useFillHandle } from './useFillHandle';
11
+ import { useUndoRedo } from './useUndoRedo';
12
+ import { useTableLayout } from './useTableLayout';
13
+ // Stable no-op handlers
14
+ const NOOP = () => { };
15
+ const NOOP_ASYNC = async () => { };
16
+ const NOOP_MOUSE = (_e, _r, _c) => { };
17
+ const NOOP_KEY = (_e) => { };
18
+ const NOOP_CTX = (_e) => { };
19
+ /**
20
+ * Single orchestration composable for DataGridTable. Takes grid props and wrapper ref,
21
+ * returns all derived state and handlers so UI packages can be thin view layers.
22
+ */
23
+ export function useDataGridState(params) {
24
+ const { props, wrapperRef } = params;
25
+ const items = computed(() => props.value.items);
26
+ const columnsProp = computed(() => props.value.columns);
27
+ const getRowId = computed(() => props.value.getRowId).value; // getRowId is stable
28
+ const visibleColumnsProp = computed(() => props.value.visibleColumns);
29
+ const columnOrderProp = computed(() => props.value.columnOrder);
30
+ const rowSelectionProp = computed(() => props.value.rowSelection ?? 'none');
31
+ const controlledSelectedRows = computed(() => props.value.selectedRows);
32
+ const onSelectionChangeProp = computed(() => props.value.onSelectionChange);
33
+ const statusBarProp = computed(() => props.value.statusBar);
34
+ const emptyStateProp = computed(() => props.value.emptyState);
35
+ const editableProp = computed(() => props.value.editable);
36
+ const cellSelectionPropRaw = computed(() => props.value.cellSelection);
37
+ const cellSelection = computed(() => cellSelectionPropRaw.value !== false);
38
+ const onCellValueChangedProp = computed(() => props.value.onCellValueChanged);
39
+ const initialColumnWidths = computed(() => props.value.initialColumnWidths);
40
+ const onColumnResizedProp = computed(() => props.value.onColumnResized);
41
+ const pinnedColumnsProp = computed(() => props.value.pinnedColumns);
42
+ const onCellErrorProp = computed(() => props.value.onCellError);
43
+ // Undo/redo wrapping
44
+ const undoRedo = useUndoRedo({ onCellValueChanged: onCellValueChangedProp.value });
45
+ const onCellValueChanged = computed(() => undoRedo.onCellValueChanged);
46
+ const flatColumnsRaw = computed(() => flattenColumns(columnsProp.value));
47
+ const flatColumns = computed(() => {
48
+ const pinned = pinnedColumnsProp.value;
49
+ if (!pinned || Object.keys(pinned).length === 0)
50
+ return flatColumnsRaw.value;
51
+ return flatColumnsRaw.value.map((col) => {
52
+ const override = pinned[col.columnId];
53
+ if (override && col.pinned !== override)
54
+ return { ...col, pinned: override };
55
+ return col;
56
+ });
57
+ });
58
+ const visibleCols = computed(() => {
59
+ const vis = visibleColumnsProp.value;
60
+ const order = columnOrderProp.value;
61
+ const filtered = vis ? flatColumns.value.filter((c) => vis.has(c.columnId)) : flatColumns.value;
62
+ if (!order?.length)
63
+ return filtered;
64
+ return [...filtered].sort((a, b) => {
65
+ const ia = order.indexOf(a.columnId);
66
+ const ib = order.indexOf(b.columnId);
67
+ if (ia === -1 && ib === -1)
68
+ return 0;
69
+ if (ia === -1)
70
+ return 1;
71
+ if (ib === -1)
72
+ return -1;
73
+ return ia - ib;
74
+ });
75
+ });
76
+ const visibleColumnCount = computed(() => visibleCols.value.length);
77
+ const hasCheckboxCol = computed(() => rowSelectionProp.value === 'multiple');
78
+ const totalColCount = computed(() => visibleColumnCount.value + (hasCheckboxCol.value ? 1 : 0));
79
+ const colOffset = computed(() => hasCheckboxCol.value ? 1 : 0).value; // stable once computed
80
+ const rowIndexByRowId = computed(() => {
81
+ const m = new Map();
82
+ items.value.forEach((item, idx) => m.set(getRowId(item), idx));
83
+ return m;
84
+ });
85
+ const rowSelectionResult = useRowSelection({
86
+ items,
87
+ getRowId,
88
+ rowSelection: rowSelectionProp,
89
+ controlledSelectedRows,
90
+ onSelectionChange: onSelectionChangeProp.value,
91
+ });
92
+ const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue } = useCellEditing();
93
+ const { activeCell, setActiveCell } = useActiveCell(wrapperRef, editingCell);
94
+ const rowCount = computed(() => items.value.length);
95
+ const visColCount = computed(() => visibleCols.value.length);
96
+ const { selectionRange, setSelectionRange, handleCellMouseDown: handleCellMouseDownBase, handleSelectAllCells, isDragging, } = useCellSelection({
97
+ colOffset,
98
+ rowCount,
99
+ visibleColCount: visColCount,
100
+ setActiveCell,
101
+ wrapperRef,
102
+ });
103
+ const { contextMenuPosition, setContextMenuPosition, handleCellContextMenu, closeContextMenu } = useContextMenu();
104
+ const { handleCopy, handleCut, handlePaste, cutRange, copyRange, clearClipboardRanges } = useClipboard({
105
+ items,
106
+ visibleCols,
107
+ colOffset,
108
+ selectionRange,
109
+ activeCell,
110
+ editable: editableProp,
111
+ onCellValueChanged,
112
+ beginBatch: undoRedo.beginBatch,
113
+ endBatch: undoRedo.endBatch,
114
+ });
115
+ const handleCellMouseDown = (e, rowIndex, globalColIndex) => {
116
+ if (e.button !== 0)
117
+ return;
118
+ wrapperRef.value?.focus({ preventScroll: true });
119
+ clearClipboardRanges();
120
+ handleCellMouseDownBase(e, rowIndex, globalColIndex);
121
+ };
122
+ const { handleGridKeyDown } = useKeyboardNavigation({
123
+ data: { items, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, getRowId },
124
+ state: { activeCell, selectionRange, editingCell, selectedRowIds: rowSelectionResult.selectedRowIds },
125
+ handlers: {
126
+ setActiveCell, setSelectionRange, setEditingCell,
127
+ handleRowCheckboxChange: rowSelectionResult.handleRowCheckboxChange,
128
+ handleCopy, handleCut, handlePaste,
129
+ setContextMenu: setContextMenuPosition,
130
+ onUndo: undoRedo.undo,
131
+ onRedo: undoRedo.redo,
132
+ clearClipboardRanges,
133
+ },
134
+ features: {
135
+ editable: editableProp,
136
+ onCellValueChanged,
137
+ rowSelection: rowSelectionProp,
138
+ wrapperRef,
139
+ },
140
+ });
141
+ const { handleFillHandleMouseDown } = useFillHandle({
142
+ items,
143
+ visibleCols,
144
+ editable: editableProp,
145
+ onCellValueChanged,
146
+ selectionRange,
147
+ setSelectionRange,
148
+ setActiveCell,
149
+ colOffset,
150
+ wrapperRef,
151
+ beginBatch: undoRedo.beginBatch,
152
+ endBatch: undoRedo.endBatch,
153
+ });
154
+ const { containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, } = useTableLayout({
155
+ wrapperRef,
156
+ visibleCols,
157
+ flatColumns,
158
+ hasCheckboxCol,
159
+ initialColumnWidths: initialColumnWidths.value,
160
+ onColumnResized: onColumnResizedProp.value,
161
+ });
162
+ const aggregation = computed(() => computeAggregations(items.value, visibleCols.value, cellSelection.value ? selectionRange.value : null));
163
+ const statusBarConfig = computed(() => {
164
+ const base = getDataGridStatusBarConfig(statusBarProp.value, items.value.length, rowSelectionResult.selectedRowIds.value.size);
165
+ if (!base)
166
+ return null;
167
+ return { ...base, aggregation: aggregation.value ?? undefined };
168
+ });
169
+ const showEmptyInGrid = computed(() => items.value.length === 0 && !!emptyStateProp.value && !props.value.isLoading);
170
+ const hasCellSelection = computed(() => selectionRange.value != null || activeCell.value != null);
171
+ // --- View-model inputs ---
172
+ const headerFilterInput = computed(() => ({
173
+ sortBy: props.value.sortBy,
174
+ sortDirection: props.value.sortDirection,
175
+ onColumnSort: props.value.onColumnSort,
176
+ filters: props.value.filters,
177
+ onFilterChange: props.value.onFilterChange,
178
+ filterOptions: props.value.filterOptions,
179
+ loadingFilterOptions: props.value.loadingFilterOptions,
180
+ peopleSearch: props.value.peopleSearch,
181
+ }));
182
+ const cellDescriptorInput = computed(() => ({
183
+ editingCell: editingCell.value,
184
+ activeCell: cellSelection.value ? activeCell.value : null,
185
+ selectionRange: cellSelection.value ? selectionRange.value : null,
186
+ cutRange: cellSelection.value ? cutRange.value : null,
187
+ copyRange: cellSelection.value ? copyRange.value : null,
188
+ colOffset,
189
+ itemsLength: items.value.length,
190
+ getRowId,
191
+ editable: editableProp.value,
192
+ onCellValueChanged: onCellValueChanged.value,
193
+ isDragging: cellSelection.value ? isDragging.value : false,
194
+ }));
195
+ // --- Cell edit helpers ---
196
+ const popoverAnchorEl = ref(null);
197
+ const setPopoverAnchorEl = (el) => {
198
+ popoverAnchorEl.value = el;
199
+ };
200
+ const commitCellEdit = (item, columnId, oldValue, newValue, rowIndex, globalColIndex) => {
201
+ const col = visibleCols.value.find((c) => c.columnId === columnId);
202
+ if (col) {
203
+ const result = parseValue(newValue, oldValue, item, col);
204
+ if (!result.valid) {
205
+ setEditingCell(null);
206
+ setPopoverAnchorEl(null);
207
+ setPendingEditorValue(undefined);
208
+ return;
209
+ }
210
+ newValue = result.value;
211
+ }
212
+ onCellValueChanged.value?.({
213
+ item,
214
+ columnId,
215
+ oldValue,
216
+ newValue,
217
+ rowIndex,
218
+ });
219
+ setEditingCell(null);
220
+ setPopoverAnchorEl(null);
221
+ setPendingEditorValue(undefined);
222
+ if (rowIndex < items.value.length - 1) {
223
+ setActiveCell({ rowIndex: rowIndex + 1, columnIndex: globalColIndex });
224
+ }
225
+ };
226
+ const cancelPopoverEdit = () => {
227
+ setEditingCell(null);
228
+ setPopoverAnchorEl(null);
229
+ setPendingEditorValue(undefined);
230
+ };
231
+ // --- Memoize each sub-object ---
232
+ const layoutState = computed(() => ({
233
+ flatColumns: flatColumns.value,
234
+ visibleCols: visibleCols.value,
235
+ visibleColumnCount: visibleColumnCount.value,
236
+ totalColCount: totalColCount.value,
237
+ colOffset,
238
+ hasCheckboxCol: hasCheckboxCol.value,
239
+ rowIndexByRowId: rowIndexByRowId.value,
240
+ containerWidth: containerWidth.value,
241
+ minTableWidth: minTableWidth.value,
242
+ desiredTableWidth: desiredTableWidth.value,
243
+ columnSizingOverrides: columnSizingOverrides.value,
244
+ setColumnSizingOverrides,
245
+ onColumnResized: onColumnResizedProp.value,
246
+ }));
247
+ const rowSelectionState = computed(() => ({
248
+ selectedRowIds: rowSelectionResult.selectedRowIds.value,
249
+ updateSelection: rowSelectionResult.updateSelection,
250
+ handleRowCheckboxChange: rowSelectionResult.handleRowCheckboxChange,
251
+ handleSelectAll: rowSelectionResult.handleSelectAll,
252
+ allSelected: rowSelectionResult.allSelected.value,
253
+ someSelected: rowSelectionResult.someSelected.value,
254
+ }));
255
+ const editingState = computed(() => ({
256
+ editingCell: editingCell.value,
257
+ setEditingCell,
258
+ pendingEditorValue: pendingEditorValue.value,
259
+ setPendingEditorValue,
260
+ commitCellEdit,
261
+ cancelPopoverEdit,
262
+ popoverAnchorEl: popoverAnchorEl.value,
263
+ setPopoverAnchorEl,
264
+ }));
265
+ const interactionState = computed(() => ({
266
+ activeCell: cellSelection.value ? activeCell.value : null,
267
+ setActiveCell: cellSelection.value ? setActiveCell : NOOP,
268
+ selectionRange: cellSelection.value ? selectionRange.value : null,
269
+ setSelectionRange: cellSelection.value ? setSelectionRange : NOOP,
270
+ handleCellMouseDown: cellSelection.value ? handleCellMouseDown : NOOP_MOUSE,
271
+ handleSelectAllCells: cellSelection.value ? handleSelectAllCells : NOOP,
272
+ hasCellSelection: cellSelection.value ? hasCellSelection.value : false,
273
+ handleGridKeyDown: cellSelection.value ? handleGridKeyDown : NOOP_KEY,
274
+ handleFillHandleMouseDown: cellSelection.value ? handleFillHandleMouseDown : NOOP,
275
+ handleCopy: cellSelection.value ? handleCopy : NOOP,
276
+ handleCut: cellSelection.value ? handleCut : NOOP,
277
+ handlePaste: cellSelection.value ? handlePaste : NOOP_ASYNC,
278
+ cutRange: cellSelection.value ? cutRange.value : null,
279
+ copyRange: cellSelection.value ? copyRange.value : null,
280
+ clearClipboardRanges: cellSelection.value ? clearClipboardRanges : NOOP,
281
+ canUndo: undoRedo.canUndo.value,
282
+ canRedo: undoRedo.canRedo.value,
283
+ onUndo: undoRedo.undo,
284
+ onRedo: undoRedo.redo,
285
+ isDragging: cellSelection.value ? isDragging.value : false,
286
+ }));
287
+ const contextMenuState = computed(() => ({
288
+ menuPosition: cellSelection.value ? contextMenuPosition.value : null,
289
+ setMenuPosition: cellSelection.value ? setContextMenuPosition : NOOP,
290
+ handleCellContextMenu: cellSelection.value ? handleCellContextMenu : NOOP_CTX,
291
+ closeContextMenu: cellSelection.value ? closeContextMenu : NOOP,
292
+ }));
293
+ const viewModelsState = computed(() => ({
294
+ headerFilterInput: headerFilterInput.value,
295
+ cellDescriptorInput: cellDescriptorInput.value,
296
+ statusBarConfig: statusBarConfig.value,
297
+ showEmptyInGrid: showEmptyInGrid.value,
298
+ onCellError: onCellErrorProp.value,
299
+ }));
300
+ return {
301
+ layout: layoutState,
302
+ rowSelection: rowSelectionState,
303
+ editing: editingState,
304
+ interaction: interactionState,
305
+ contextMenu: contextMenuState,
306
+ viewModels: viewModelsState,
307
+ };
308
+ }
@@ -0,0 +1,36 @@
1
+ import { ref, watch } from 'vue';
2
+ export function useDateFilterState(params) {
3
+ const { onDateChange } = params;
4
+ const tempDateFrom = ref(params.dateValue?.from ?? '');
5
+ const tempDateTo = ref(params.dateValue?.to ?? '');
6
+ // Sync temp state when popover opens
7
+ watch(() => params.isFilterOpen(), (open) => {
8
+ if (open) {
9
+ tempDateFrom.value = params.dateValue?.from ?? '';
10
+ tempDateTo.value = params.dateValue?.to ?? '';
11
+ }
12
+ });
13
+ const setTempDateFrom = (v) => {
14
+ tempDateFrom.value = v;
15
+ };
16
+ const setTempDateTo = (v) => {
17
+ tempDateTo.value = v;
18
+ };
19
+ const handleDateApply = () => {
20
+ const from = tempDateFrom.value || undefined;
21
+ const to = tempDateTo.value || undefined;
22
+ onDateChange?.(from || to ? { from, to } : undefined);
23
+ };
24
+ const handleDateClear = () => {
25
+ tempDateFrom.value = '';
26
+ tempDateTo.value = '';
27
+ };
28
+ return {
29
+ tempDateFrom,
30
+ setTempDateFrom,
31
+ tempDateTo,
32
+ setTempDateTo,
33
+ handleDateApply,
34
+ handleDateClear,
35
+ };
36
+ }
@@ -0,0 +1,42 @@
1
+ import { ref, watch, onUnmounted } from 'vue';
2
+ /**
3
+ * Returns a debounced ref that updates after the specified delay when the source value changes.
4
+ */
5
+ export function useDebounce(value, delayMs) {
6
+ const debouncedValue = ref(value.value);
7
+ let timeoutId;
8
+ watch(value, (newVal) => {
9
+ if (timeoutId !== undefined)
10
+ clearTimeout(timeoutId);
11
+ timeoutId = setTimeout(() => {
12
+ debouncedValue.value = newVal;
13
+ }, delayMs);
14
+ });
15
+ onUnmounted(() => {
16
+ if (timeoutId !== undefined)
17
+ clearTimeout(timeoutId);
18
+ });
19
+ return debouncedValue;
20
+ }
21
+ /**
22
+ * Returns a stable callback that invokes the given function after the specified delay.
23
+ * Each new call resets the timer.
24
+ */
25
+ export function useDebouncedCallback(fn, delayMs) {
26
+ let timeoutId;
27
+ // Keep a reference to the latest fn
28
+ let latestFn = fn;
29
+ const debounced = ((...args) => {
30
+ latestFn = fn;
31
+ if (timeoutId !== undefined)
32
+ clearTimeout(timeoutId);
33
+ timeoutId = setTimeout(() => {
34
+ latestFn(...args);
35
+ }, delayMs);
36
+ });
37
+ onUnmounted(() => {
38
+ if (timeoutId !== undefined)
39
+ clearTimeout(timeoutId);
40
+ });
41
+ return debounced;
42
+ }