@alaarab/ogrid-vue 2.1.3 → 2.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.js +4336 -15
- package/package.json +4 -4
- package/dist/esm/components/MarchingAntsOverlay.js +0 -144
- package/dist/esm/components/SideBar.js +0 -1
- package/dist/esm/components/StatusBar.js +0 -49
- package/dist/esm/components/createDataGridTable.js +0 -514
- package/dist/esm/components/createInlineCellEditor.js +0 -194
- package/dist/esm/components/createOGrid.js +0 -383
- package/dist/esm/composables/index.js +0 -33
- package/dist/esm/composables/useActiveCell.js +0 -77
- package/dist/esm/composables/useCellEditing.js +0 -27
- package/dist/esm/composables/useCellSelection.js +0 -359
- package/dist/esm/composables/useClipboard.js +0 -87
- package/dist/esm/composables/useColumnChooserState.js +0 -74
- package/dist/esm/composables/useColumnHeaderFilterState.js +0 -189
- package/dist/esm/composables/useColumnHeaderMenuState.js +0 -113
- package/dist/esm/composables/useColumnPinning.js +0 -64
- package/dist/esm/composables/useColumnReorder.js +0 -110
- package/dist/esm/composables/useColumnResize.js +0 -73
- package/dist/esm/composables/useContextMenu.js +0 -23
- package/dist/esm/composables/useDataGridState.js +0 -425
- package/dist/esm/composables/useDataGridTableSetup.js +0 -66
- package/dist/esm/composables/useDateFilterState.js +0 -36
- package/dist/esm/composables/useDebounce.js +0 -60
- package/dist/esm/composables/useFillHandle.js +0 -205
- package/dist/esm/composables/useFilterOptions.js +0 -39
- package/dist/esm/composables/useInlineCellEditorState.js +0 -42
- package/dist/esm/composables/useKeyboardNavigation.js +0 -232
- package/dist/esm/composables/useLatestRef.js +0 -27
- package/dist/esm/composables/useMultiSelectFilterState.js +0 -59
- package/dist/esm/composables/useOGrid.js +0 -491
- package/dist/esm/composables/usePeopleFilterState.js +0 -66
- package/dist/esm/composables/useRichSelectState.js +0 -59
- package/dist/esm/composables/useRowSelection.js +0 -75
- package/dist/esm/composables/useSideBarState.js +0 -41
- package/dist/esm/composables/useTableLayout.js +0 -85
- package/dist/esm/composables/useTextFilterState.js +0 -26
- package/dist/esm/composables/useUndoRedo.js +0 -65
- package/dist/esm/composables/useVirtualScroll.js +0 -87
- package/dist/esm/types/columnTypes.js +0 -1
- package/dist/esm/types/dataGridTypes.js +0 -1
- package/dist/esm/types/index.js +0 -1
- package/dist/esm/utils/dataGridViewModel.js +0 -23
- package/dist/esm/utils/index.js +0 -1
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { shallowRef, watch, isRef, onUnmounted } from 'vue';
|
|
2
|
-
import { normalizeSelectionRange, applyFillValues } from '@alaarab/ogrid-core';
|
|
3
|
-
const DRAG_ATTR = 'data-drag-range';
|
|
4
|
-
/**
|
|
5
|
-
* Manages Excel-style fill handle drag-to-fill for cell ranges.
|
|
6
|
-
*/
|
|
7
|
-
export function useFillHandle(params) {
|
|
8
|
-
const { items, visibleCols, editable, onCellValueChanged, selectionRange, setSelectionRange, setActiveCell, wrapperRef, beginBatch, endBatch, visibleRange, } = params;
|
|
9
|
-
const getColOffset = () => isRef(params.colOffset) ? params.colOffset.value : params.colOffset;
|
|
10
|
-
const fillDrag = shallowRef(null);
|
|
11
|
-
let fillDragEnd = { endRow: 0, endCol: 0 };
|
|
12
|
-
let rafId = 0;
|
|
13
|
-
let liveFillRange = null;
|
|
14
|
-
let moveListener = null;
|
|
15
|
-
let upListener = null;
|
|
16
|
-
const setFillDrag = (value) => {
|
|
17
|
-
fillDrag.value = value;
|
|
18
|
-
};
|
|
19
|
-
const cleanup = () => {
|
|
20
|
-
if (moveListener) {
|
|
21
|
-
window.removeEventListener('mousemove', moveListener, true);
|
|
22
|
-
moveListener = null;
|
|
23
|
-
}
|
|
24
|
-
if (upListener) {
|
|
25
|
-
window.removeEventListener('mouseup', upListener, true);
|
|
26
|
-
upListener = null;
|
|
27
|
-
}
|
|
28
|
-
if (rafId) {
|
|
29
|
-
cancelAnimationFrame(rafId);
|
|
30
|
-
rafId = 0;
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
watch(fillDrag, (drag, _oldDrag, onCleanup) => {
|
|
34
|
-
// Guard early before setting up any state
|
|
35
|
-
if (!drag || editable.value === false || !onCellValueChanged.value || !wrapperRef.value) {
|
|
36
|
-
// Still cleanup if transitioning from active to inactive
|
|
37
|
-
cleanup();
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
fillDragEnd = { endRow: drag.startRow, endCol: drag.startCol };
|
|
41
|
-
liveFillRange = null;
|
|
42
|
-
/** Set of currently drag-marked HTMLElements — avoids O(n) full DOM scan on clear. */
|
|
43
|
-
const markedCells = new Set();
|
|
44
|
-
/** Cell lookup index built on drag start — O(1) lookups per frame. */
|
|
45
|
-
let fillCellIndex = null;
|
|
46
|
-
const buildFillCellIndex = () => {
|
|
47
|
-
const wrapper = wrapperRef.value;
|
|
48
|
-
if (!wrapper)
|
|
49
|
-
return;
|
|
50
|
-
fillCellIndex = new Map();
|
|
51
|
-
const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
|
|
52
|
-
for (let i = 0; i < cells.length; i++) {
|
|
53
|
-
const el = cells[i];
|
|
54
|
-
const r = el.getAttribute('data-row-index') ?? '';
|
|
55
|
-
const c = el.getAttribute('data-col-index') ?? '';
|
|
56
|
-
fillCellIndex.set(`${r},${c}`, el);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
// Build the index once at fill drag start
|
|
60
|
-
buildFillCellIndex();
|
|
61
|
-
const applyDragAttrs = (range) => {
|
|
62
|
-
const wrapper = wrapperRef.value;
|
|
63
|
-
if (!wrapper)
|
|
64
|
-
return;
|
|
65
|
-
const minR = Math.min(range.startRow, range.endRow);
|
|
66
|
-
const maxR = Math.max(range.startRow, range.endRow);
|
|
67
|
-
const minC = Math.min(range.startCol, range.endCol);
|
|
68
|
-
const maxC = Math.max(range.startCol, range.endCol);
|
|
69
|
-
const colOff = getColOffset();
|
|
70
|
-
// Un-mark cells no longer in range
|
|
71
|
-
for (const el of markedCells) {
|
|
72
|
-
const r = parseInt(el.getAttribute('data-row-index') ?? '', 10);
|
|
73
|
-
const c = parseInt(el.getAttribute('data-col-index') ?? '', 10) - colOff;
|
|
74
|
-
if (!(r >= minR && r <= maxR && c >= minC && c <= maxC)) {
|
|
75
|
-
el.removeAttribute(DRAG_ATTR);
|
|
76
|
-
markedCells.delete(el);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// Look up only cells in the new range — O(range size) via Map lookup
|
|
80
|
-
for (let r = minR; r <= maxR; r++) {
|
|
81
|
-
for (let c = minC; c <= maxC; c++) {
|
|
82
|
-
const key = `${r},${c + colOff}`;
|
|
83
|
-
let el = fillCellIndex?.get(key);
|
|
84
|
-
// Handle virtual scroll recycling — if element is stale, rebuild index once
|
|
85
|
-
if (el && !el.isConnected) {
|
|
86
|
-
buildFillCellIndex();
|
|
87
|
-
el = fillCellIndex?.get(key);
|
|
88
|
-
}
|
|
89
|
-
if (el) {
|
|
90
|
-
if (!el.hasAttribute(DRAG_ATTR))
|
|
91
|
-
el.setAttribute(DRAG_ATTR, '');
|
|
92
|
-
markedCells.add(el);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
const clearDragAttrs = () => {
|
|
98
|
-
for (const el of markedCells) {
|
|
99
|
-
el.removeAttribute(DRAG_ATTR);
|
|
100
|
-
}
|
|
101
|
-
markedCells.clear();
|
|
102
|
-
fillCellIndex = null;
|
|
103
|
-
};
|
|
104
|
-
let lastFillMousePos = null;
|
|
105
|
-
const resolveRange = (cx, cy) => {
|
|
106
|
-
const target = document.elementFromPoint(cx, cy);
|
|
107
|
-
const cell = target?.closest?.('[data-row-index][data-col-index]');
|
|
108
|
-
if (!cell || !wrapperRef.value?.contains(cell))
|
|
109
|
-
return null;
|
|
110
|
-
const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
|
|
111
|
-
const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
|
|
112
|
-
const colOffset = getColOffset();
|
|
113
|
-
if (Number.isNaN(r) || Number.isNaN(c) || c < colOffset)
|
|
114
|
-
return null;
|
|
115
|
-
const dataCol = c - colOffset;
|
|
116
|
-
return normalizeSelectionRange({
|
|
117
|
-
startRow: drag.startRow,
|
|
118
|
-
startCol: drag.startCol,
|
|
119
|
-
endRow: r,
|
|
120
|
-
endCol: dataCol,
|
|
121
|
-
});
|
|
122
|
-
};
|
|
123
|
-
moveListener = (e) => {
|
|
124
|
-
lastFillMousePos = { cx: e.clientX, cy: e.clientY };
|
|
125
|
-
if (rafId)
|
|
126
|
-
cancelAnimationFrame(rafId);
|
|
127
|
-
rafId = requestAnimationFrame(() => {
|
|
128
|
-
rafId = 0;
|
|
129
|
-
if (!lastFillMousePos)
|
|
130
|
-
return;
|
|
131
|
-
const newRange = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
|
|
132
|
-
if (!newRange)
|
|
133
|
-
return;
|
|
134
|
-
const prev = liveFillRange;
|
|
135
|
-
if (prev && prev.startRow === newRange.startRow && prev.startCol === newRange.startCol && prev.endRow === newRange.endRow && prev.endCol === newRange.endCol)
|
|
136
|
-
return;
|
|
137
|
-
liveFillRange = newRange;
|
|
138
|
-
fillDragEnd = { endRow: newRange.endRow, endCol: newRange.endCol };
|
|
139
|
-
applyDragAttrs(newRange);
|
|
140
|
-
});
|
|
141
|
-
};
|
|
142
|
-
upListener = () => {
|
|
143
|
-
if (rafId) {
|
|
144
|
-
cancelAnimationFrame(rafId);
|
|
145
|
-
rafId = 0;
|
|
146
|
-
}
|
|
147
|
-
if (lastFillMousePos) {
|
|
148
|
-
const flushed = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
|
|
149
|
-
if (flushed) {
|
|
150
|
-
liveFillRange = flushed;
|
|
151
|
-
fillDragEnd = { endRow: flushed.endRow, endCol: flushed.endCol };
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
clearDragAttrs();
|
|
155
|
-
const end = fillDragEnd;
|
|
156
|
-
const norm = normalizeSelectionRange({
|
|
157
|
-
startRow: drag.startRow,
|
|
158
|
-
startCol: drag.startCol,
|
|
159
|
-
endRow: end.endRow,
|
|
160
|
-
endCol: end.endCol,
|
|
161
|
-
});
|
|
162
|
-
// Clamp fill range to visible + overscan when virtual scrolling is active
|
|
163
|
-
const vr = visibleRange?.value;
|
|
164
|
-
if (vr) {
|
|
165
|
-
norm.startRow = Math.max(norm.startRow, vr.startIndex);
|
|
166
|
-
norm.endRow = Math.min(norm.endRow, vr.endIndex);
|
|
167
|
-
}
|
|
168
|
-
setSelectionRange(norm);
|
|
169
|
-
setActiveCell({ rowIndex: end.endRow, columnIndex: end.endCol + getColOffset() });
|
|
170
|
-
const currentItems = items.value;
|
|
171
|
-
const currentCols = visibleCols.value;
|
|
172
|
-
const callback = onCellValueChanged.value;
|
|
173
|
-
if (callback) {
|
|
174
|
-
const fillEvents = applyFillValues(norm, drag.startRow, drag.startCol, currentItems, currentCols);
|
|
175
|
-
if (fillEvents.length > 0) {
|
|
176
|
-
beginBatch?.();
|
|
177
|
-
for (const evt of fillEvents)
|
|
178
|
-
callback(evt);
|
|
179
|
-
endBatch?.();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
fillDrag.value = null;
|
|
183
|
-
liveFillRange = null;
|
|
184
|
-
cleanup();
|
|
185
|
-
};
|
|
186
|
-
window.addEventListener('mousemove', moveListener, true);
|
|
187
|
-
window.addEventListener('mouseup', upListener, true);
|
|
188
|
-
// Register cleanup via onCleanup — Vue calls this BEFORE next watch run
|
|
189
|
-
// and on unmount. Compatible with Vue 3.3+ (unlike return-value cleanup
|
|
190
|
-
// which requires Vue 3.5+).
|
|
191
|
-
onCleanup(() => {
|
|
192
|
-
cleanup();
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
onUnmounted(() => cleanup());
|
|
196
|
-
const handleFillHandleMouseDown = (e) => {
|
|
197
|
-
e.preventDefault();
|
|
198
|
-
e.stopPropagation();
|
|
199
|
-
const range = selectionRange.value;
|
|
200
|
-
if (!range)
|
|
201
|
-
return;
|
|
202
|
-
fillDrag.value = { startRow: range.startRow, startCol: range.startCol };
|
|
203
|
-
};
|
|
204
|
-
return { fillDrag, setFillDrag, handleFillHandleMouseDown };
|
|
205
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { ref, watch, computed } from 'vue';
|
|
2
|
-
/**
|
|
3
|
-
* Load filter options for the given fields from a data source.
|
|
4
|
-
*/
|
|
5
|
-
export function useFilterOptions(dataSource, fields) {
|
|
6
|
-
const filterOptions = ref({});
|
|
7
|
-
const loadingOptions = ref({});
|
|
8
|
-
const fieldsKey = computed(() => [...fields.value].sort().join(','));
|
|
9
|
-
const load = async () => {
|
|
10
|
-
const ds = dataSource.value;
|
|
11
|
-
const currentFields = fields.value;
|
|
12
|
-
const fetcher = 'fetchFilterOptions' in ds && typeof ds.fetchFilterOptions === 'function'
|
|
13
|
-
? ds.fetchFilterOptions.bind(ds)
|
|
14
|
-
: undefined;
|
|
15
|
-
if (!fetcher) {
|
|
16
|
-
filterOptions.value = {};
|
|
17
|
-
loadingOptions.value = {};
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
const loading = {};
|
|
21
|
-
currentFields.forEach((f) => { loading[f] = true; });
|
|
22
|
-
loadingOptions.value = loading;
|
|
23
|
-
const results = {};
|
|
24
|
-
await Promise.all(currentFields.map(async (field) => {
|
|
25
|
-
try {
|
|
26
|
-
results[field] = await fetcher(field);
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
results[field] = [];
|
|
30
|
-
}
|
|
31
|
-
}));
|
|
32
|
-
filterOptions.value = results;
|
|
33
|
-
loadingOptions.value = {};
|
|
34
|
-
};
|
|
35
|
-
watch([dataSource, fieldsKey], () => {
|
|
36
|
-
load().catch(() => { });
|
|
37
|
-
}, { immediate: true });
|
|
38
|
-
return { filterOptions, loadingOptions };
|
|
39
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { ref } from 'vue';
|
|
2
|
-
/**
|
|
3
|
-
* Returns localValue/setLocalValue, handleKeyDown, handleBlur, commit, cancel.
|
|
4
|
-
*/
|
|
5
|
-
export function useInlineCellEditorState(params) {
|
|
6
|
-
const { value, editorType, onCommit, onCancel } = params;
|
|
7
|
-
const localValue = ref(value !== null && value !== undefined ? String(value) : '');
|
|
8
|
-
const setLocalValue = (v) => {
|
|
9
|
-
localValue.value = v;
|
|
10
|
-
};
|
|
11
|
-
const commit = (v) => {
|
|
12
|
-
onCommit(v);
|
|
13
|
-
};
|
|
14
|
-
const cancel = () => {
|
|
15
|
-
onCancel();
|
|
16
|
-
};
|
|
17
|
-
const handleKeyDown = (e) => {
|
|
18
|
-
if (e.key === 'Escape') {
|
|
19
|
-
e.preventDefault();
|
|
20
|
-
e.stopPropagation();
|
|
21
|
-
cancel();
|
|
22
|
-
}
|
|
23
|
-
if (e.key === 'Enter' && editorType === 'text') {
|
|
24
|
-
e.preventDefault();
|
|
25
|
-
e.stopPropagation();
|
|
26
|
-
commit(localValue.value);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
const handleBlur = () => {
|
|
30
|
-
if (editorType === 'text') {
|
|
31
|
-
commit(localValue.value);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
return {
|
|
35
|
-
localValue,
|
|
36
|
-
setLocalValue,
|
|
37
|
-
handleKeyDown,
|
|
38
|
-
handleBlur,
|
|
39
|
-
commit,
|
|
40
|
-
cancel,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { isRef } from 'vue';
|
|
2
|
-
import { getCellValue, computeTabNavigation, computeArrowNavigation, applyCellDeletion } from '@alaarab/ogrid-core';
|
|
3
|
-
import { useLatestRef } from './useLatestRef';
|
|
4
|
-
/**
|
|
5
|
-
* Handles all keyboard navigation, shortcuts, and cell editing triggers for the grid.
|
|
6
|
-
*/
|
|
7
|
-
export function useKeyboardNavigation(params) {
|
|
8
|
-
// Store latest params in a ref so handleGridKeyDown is a stable callback
|
|
9
|
-
const paramsRef = useLatestRef(params);
|
|
10
|
-
const handleGridKeyDown = (e) => {
|
|
11
|
-
const { data, state, handlers, features } = paramsRef.value;
|
|
12
|
-
const items = data.items.value;
|
|
13
|
-
const visibleCols = data.visibleCols.value;
|
|
14
|
-
const { getRowId } = data;
|
|
15
|
-
const colOffset = isRef(data.colOffset) ? data.colOffset.value : data.colOffset;
|
|
16
|
-
const hasCheckboxCol = data.hasCheckboxCol.value;
|
|
17
|
-
const visibleColumnCount = data.visibleColumnCount.value;
|
|
18
|
-
const activeCell = state.activeCell.value;
|
|
19
|
-
const selectionRange = state.selectionRange.value;
|
|
20
|
-
const editingCell = state.editingCell.value;
|
|
21
|
-
const selectedRowIds = state.selectedRowIds.value;
|
|
22
|
-
const { setActiveCell, setSelectionRange, setEditingCell, handleRowCheckboxChange, handleCopy, handleCut, handlePaste, setContextMenu, onUndo, onRedo, clearClipboardRanges } = handlers;
|
|
23
|
-
const editable = features.editable.value;
|
|
24
|
-
const onCellValueChanged = features.onCellValueChanged.value;
|
|
25
|
-
const rowSelection = features.rowSelection.value;
|
|
26
|
-
const wrapperRef = features.wrapperRef;
|
|
27
|
-
const scrollToRow = features.scrollToRow;
|
|
28
|
-
const maxRowIndex = items.length - 1;
|
|
29
|
-
const maxColIndex = visibleColumnCount - 1 + colOffset;
|
|
30
|
-
if (items.length === 0)
|
|
31
|
-
return;
|
|
32
|
-
if (activeCell === null) {
|
|
33
|
-
if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End'].includes(e.key)) {
|
|
34
|
-
setActiveCell({ rowIndex: 0, columnIndex: colOffset });
|
|
35
|
-
e.preventDefault();
|
|
36
|
-
}
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const { rowIndex, columnIndex } = activeCell;
|
|
40
|
-
const dataColIndex = columnIndex - colOffset;
|
|
41
|
-
const shift = e.shiftKey;
|
|
42
|
-
const isEmptyAt = (r, c) => {
|
|
43
|
-
if (r < 0 || r >= items.length || c < 0 || c >= visibleCols.length)
|
|
44
|
-
return true;
|
|
45
|
-
const v = getCellValue(items[r], visibleCols[c]);
|
|
46
|
-
return v == null || v === '';
|
|
47
|
-
};
|
|
48
|
-
switch (e.key) {
|
|
49
|
-
case 'c':
|
|
50
|
-
if (e.ctrlKey || e.metaKey) {
|
|
51
|
-
if (editingCell != null)
|
|
52
|
-
break;
|
|
53
|
-
e.preventDefault();
|
|
54
|
-
handleCopy();
|
|
55
|
-
}
|
|
56
|
-
break;
|
|
57
|
-
case 'x':
|
|
58
|
-
if (e.ctrlKey || e.metaKey) {
|
|
59
|
-
if (editingCell != null)
|
|
60
|
-
break;
|
|
61
|
-
e.preventDefault();
|
|
62
|
-
handleCut();
|
|
63
|
-
}
|
|
64
|
-
break;
|
|
65
|
-
case 'v':
|
|
66
|
-
if (e.ctrlKey || e.metaKey) {
|
|
67
|
-
if (editingCell != null)
|
|
68
|
-
break;
|
|
69
|
-
e.preventDefault();
|
|
70
|
-
void handlePaste();
|
|
71
|
-
}
|
|
72
|
-
break;
|
|
73
|
-
case 'ArrowDown':
|
|
74
|
-
case 'ArrowUp':
|
|
75
|
-
case 'ArrowRight':
|
|
76
|
-
case 'ArrowLeft': {
|
|
77
|
-
e.preventDefault();
|
|
78
|
-
const { newRowIndex, newColumnIndex, newRange } = computeArrowNavigation({
|
|
79
|
-
direction: e.key,
|
|
80
|
-
rowIndex, columnIndex, dataColIndex, colOffset,
|
|
81
|
-
maxRowIndex, maxColIndex,
|
|
82
|
-
visibleColCount: visibleCols.length,
|
|
83
|
-
isCtrl: e.ctrlKey || e.metaKey,
|
|
84
|
-
isShift: shift,
|
|
85
|
-
selectionRange,
|
|
86
|
-
isEmptyAt,
|
|
87
|
-
});
|
|
88
|
-
setSelectionRange(newRange);
|
|
89
|
-
setActiveCell({ rowIndex: newRowIndex, columnIndex: newColumnIndex });
|
|
90
|
-
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
91
|
-
scrollToRow?.(newRowIndex, 'center');
|
|
92
|
-
}
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
case 'Tab': {
|
|
96
|
-
e.preventDefault();
|
|
97
|
-
const { rowIndex: newRowTab, columnIndex: newColTab } = computeTabNavigation(rowIndex, columnIndex, maxRowIndex, maxColIndex, colOffset, e.shiftKey);
|
|
98
|
-
const newDataColTab = newColTab - colOffset;
|
|
99
|
-
setSelectionRange({ startRow: newRowTab, startCol: newDataColTab, endRow: newRowTab, endCol: newDataColTab });
|
|
100
|
-
setActiveCell({ rowIndex: newRowTab, columnIndex: newColTab });
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
case 'Home': {
|
|
104
|
-
e.preventDefault();
|
|
105
|
-
const newRowHome = e.ctrlKey ? 0 : rowIndex;
|
|
106
|
-
setSelectionRange({ startRow: newRowHome, startCol: 0, endRow: newRowHome, endCol: 0 });
|
|
107
|
-
setActiveCell({ rowIndex: newRowHome, columnIndex: colOffset });
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
case 'End': {
|
|
111
|
-
e.preventDefault();
|
|
112
|
-
const newRowEnd = e.ctrlKey ? maxRowIndex : rowIndex;
|
|
113
|
-
setSelectionRange({ startRow: newRowEnd, startCol: visibleColumnCount - 1, endRow: newRowEnd, endCol: visibleColumnCount - 1 });
|
|
114
|
-
setActiveCell({ rowIndex: newRowEnd, columnIndex: maxColIndex });
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
case 'Enter':
|
|
118
|
-
case 'F2': {
|
|
119
|
-
e.preventDefault();
|
|
120
|
-
if (dataColIndex >= 0 && dataColIndex < visibleCols.length) {
|
|
121
|
-
const col = visibleCols[dataColIndex];
|
|
122
|
-
const item = items[rowIndex];
|
|
123
|
-
if (item && col) {
|
|
124
|
-
const colEditable = col.editable === true ||
|
|
125
|
-
(typeof col.editable === 'function' && col.editable(item));
|
|
126
|
-
if (editable !== false && colEditable && onCellValueChanged != null) {
|
|
127
|
-
setEditingCell({ rowId: getRowId(item), columnId: col.columnId });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
case 'Escape':
|
|
134
|
-
e.preventDefault();
|
|
135
|
-
if (editingCell != null) {
|
|
136
|
-
setEditingCell(null);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
clearClipboardRanges?.();
|
|
140
|
-
setActiveCell(null);
|
|
141
|
-
setSelectionRange(null);
|
|
142
|
-
}
|
|
143
|
-
break;
|
|
144
|
-
case ' ':
|
|
145
|
-
if (rowSelection !== 'none' && columnIndex === 0 && hasCheckboxCol) {
|
|
146
|
-
e.preventDefault();
|
|
147
|
-
const item = items[rowIndex];
|
|
148
|
-
if (item) {
|
|
149
|
-
const id = getRowId(item);
|
|
150
|
-
const isSelected = selectedRowIds.has(id);
|
|
151
|
-
handleRowCheckboxChange(id, !isSelected, rowIndex, e.shiftKey);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
break;
|
|
155
|
-
case 'z':
|
|
156
|
-
if (e.ctrlKey || e.metaKey) {
|
|
157
|
-
if (editingCell == null) {
|
|
158
|
-
if (e.shiftKey && onRedo) {
|
|
159
|
-
e.preventDefault();
|
|
160
|
-
onRedo();
|
|
161
|
-
}
|
|
162
|
-
else if (!e.shiftKey && onUndo) {
|
|
163
|
-
e.preventDefault();
|
|
164
|
-
onUndo();
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
break;
|
|
169
|
-
case 'y':
|
|
170
|
-
if (e.ctrlKey || e.metaKey) {
|
|
171
|
-
if (editingCell == null && onRedo) {
|
|
172
|
-
e.preventDefault();
|
|
173
|
-
onRedo();
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
break;
|
|
177
|
-
case 'a':
|
|
178
|
-
if (e.ctrlKey || e.metaKey) {
|
|
179
|
-
if (editingCell != null)
|
|
180
|
-
break;
|
|
181
|
-
e.preventDefault();
|
|
182
|
-
if (items.length > 0 && visibleColumnCount > 0) {
|
|
183
|
-
setSelectionRange({ startRow: 0, startCol: 0, endRow: items.length - 1, endCol: visibleColumnCount - 1 });
|
|
184
|
-
setActiveCell({ rowIndex: 0, columnIndex: colOffset });
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
break;
|
|
188
|
-
case 'Delete':
|
|
189
|
-
case 'Backspace': {
|
|
190
|
-
if (editingCell != null)
|
|
191
|
-
break;
|
|
192
|
-
if (editable === false)
|
|
193
|
-
break;
|
|
194
|
-
if (onCellValueChanged == null)
|
|
195
|
-
break;
|
|
196
|
-
const range = selectionRange ??
|
|
197
|
-
(activeCell != null
|
|
198
|
-
? { startRow: activeCell.rowIndex, startCol: activeCell.columnIndex - colOffset, endRow: activeCell.rowIndex, endCol: activeCell.columnIndex - colOffset }
|
|
199
|
-
: null);
|
|
200
|
-
if (range == null)
|
|
201
|
-
break;
|
|
202
|
-
e.preventDefault();
|
|
203
|
-
const deleteEvents = applyCellDeletion(range, items, visibleCols);
|
|
204
|
-
for (const evt of deleteEvents)
|
|
205
|
-
onCellValueChanged(evt);
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
case 'F10':
|
|
209
|
-
if (e.shiftKey) {
|
|
210
|
-
e.preventDefault();
|
|
211
|
-
if (activeCell != null && wrapperRef.value) {
|
|
212
|
-
const sel = `[data-row-index="${activeCell.rowIndex}"][data-col-index="${activeCell.columnIndex}"]`;
|
|
213
|
-
const cell = wrapperRef.value.querySelector(sel);
|
|
214
|
-
if (cell) {
|
|
215
|
-
const rect = cell.getBoundingClientRect();
|
|
216
|
-
setContextMenu({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 });
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
setContextMenu({ x: 100, y: 100 });
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
setContextMenu({ x: 100, y: 100 });
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
break;
|
|
227
|
-
default:
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
return { handleGridKeyDown };
|
|
232
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { customRef, isRef, 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 (isRef(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
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { ref, computed, watch } from 'vue';
|
|
2
|
-
import { useDebounce } from './useDebounce';
|
|
3
|
-
const SEARCH_DEBOUNCE_MS = 150;
|
|
4
|
-
const EMPTY_OPTIONS = [];
|
|
5
|
-
export function useMultiSelectFilterState(params) {
|
|
6
|
-
const { onFilterChange } = params;
|
|
7
|
-
const tempSelected = ref(new Set(params.selectedValues ?? EMPTY_OPTIONS));
|
|
8
|
-
const searchText = ref('');
|
|
9
|
-
const debouncedSearchText = useDebounce(searchText, SEARCH_DEBOUNCE_MS);
|
|
10
|
-
// Sync temp state when popover opens
|
|
11
|
-
watch(params.isFilterOpen, (open) => {
|
|
12
|
-
if (open) {
|
|
13
|
-
tempSelected.value = new Set(params.selectedValues ?? EMPTY_OPTIONS);
|
|
14
|
-
searchText.value = '';
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
const filteredOptions = computed(() => {
|
|
18
|
-
const safeOptions = params.options ?? EMPTY_OPTIONS;
|
|
19
|
-
if (!debouncedSearchText.value.trim())
|
|
20
|
-
return safeOptions;
|
|
21
|
-
const searchLower = debouncedSearchText.value.toLowerCase().trim();
|
|
22
|
-
return safeOptions.filter((opt) => opt.toLowerCase().includes(searchLower));
|
|
23
|
-
});
|
|
24
|
-
const setTempSelected = (v) => {
|
|
25
|
-
tempSelected.value = v;
|
|
26
|
-
};
|
|
27
|
-
const setSearchText = (v) => {
|
|
28
|
-
searchText.value = v;
|
|
29
|
-
};
|
|
30
|
-
const handleCheckboxChange = (option, checked) => {
|
|
31
|
-
const next = new Set(tempSelected.value);
|
|
32
|
-
if (checked)
|
|
33
|
-
next.add(option);
|
|
34
|
-
else
|
|
35
|
-
next.delete(option);
|
|
36
|
-
tempSelected.value = next;
|
|
37
|
-
};
|
|
38
|
-
const handleSelectAll = () => {
|
|
39
|
-
tempSelected.value = new Set(filteredOptions.value);
|
|
40
|
-
};
|
|
41
|
-
const handleClearSelection = () => {
|
|
42
|
-
tempSelected.value = new Set();
|
|
43
|
-
};
|
|
44
|
-
const handleApplyMultiSelect = () => {
|
|
45
|
-
onFilterChange?.(Array.from(tempSelected.value));
|
|
46
|
-
};
|
|
47
|
-
return {
|
|
48
|
-
tempSelected,
|
|
49
|
-
setTempSelected,
|
|
50
|
-
searchText,
|
|
51
|
-
setSearchText,
|
|
52
|
-
debouncedSearchText,
|
|
53
|
-
filteredOptions,
|
|
54
|
-
handleCheckboxChange,
|
|
55
|
-
handleSelectAll,
|
|
56
|
-
handleClearSelection,
|
|
57
|
-
handleApplyMultiSelect,
|
|
58
|
-
};
|
|
59
|
-
}
|