@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.
- package/dist/esm/components/SideBar.js +1 -0
- package/dist/esm/composables/index.js +27 -0
- package/dist/esm/composables/useActiveCell.js +58 -0
- package/dist/esm/composables/useCellEditing.js +20 -0
- package/dist/esm/composables/useCellSelection.js +281 -0
- package/dist/esm/composables/useClipboard.js +147 -0
- package/dist/esm/composables/useColumnChooserState.js +77 -0
- package/dist/esm/composables/useColumnHeaderFilterState.js +186 -0
- package/dist/esm/composables/useColumnResize.js +73 -0
- package/dist/esm/composables/useContextMenu.js +23 -0
- package/dist/esm/composables/useDataGridState.js +308 -0
- package/dist/esm/composables/useDateFilterState.js +36 -0
- package/dist/esm/composables/useDebounce.js +42 -0
- package/dist/esm/composables/useFillHandle.js +175 -0
- package/dist/esm/composables/useFilterOptions.js +39 -0
- package/dist/esm/composables/useInlineCellEditorState.js +42 -0
- package/dist/esm/composables/useKeyboardNavigation.js +353 -0
- package/dist/esm/composables/useMultiSelectFilterState.js +59 -0
- package/dist/esm/composables/useOGrid.js +406 -0
- package/dist/esm/composables/usePeopleFilterState.js +66 -0
- package/dist/esm/composables/useRichSelectState.js +59 -0
- package/dist/esm/composables/useRowSelection.js +75 -0
- package/dist/esm/composables/useSideBarState.js +41 -0
- package/dist/esm/composables/useTableLayout.js +85 -0
- package/dist/esm/composables/useTextFilterState.js +26 -0
- package/dist/esm/composables/useUndoRedo.js +75 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/types/columnTypes.js +1 -0
- package/dist/esm/types/dataGridTypes.js +1 -0
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/utils/dataGridViewModel.js +195 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/types/components/SideBar.d.ts +26 -0
- package/dist/types/composables/index.d.ts +47 -0
- package/dist/types/composables/useActiveCell.d.ts +14 -0
- package/dist/types/composables/useCellEditing.d.ts +16 -0
- package/dist/types/composables/useCellSelection.d.ts +20 -0
- package/dist/types/composables/useClipboard.d.ts +25 -0
- package/dist/types/composables/useColumnChooserState.d.ts +22 -0
- package/dist/types/composables/useColumnHeaderFilterState.d.ts +68 -0
- package/dist/types/composables/useColumnResize.d.ts +21 -0
- package/dist/types/composables/useContextMenu.d.ts +19 -0
- package/dist/types/composables/useDataGridState.d.ts +129 -0
- package/dist/types/composables/useDateFilterState.d.ts +16 -0
- package/dist/types/composables/useDebounce.d.ts +10 -0
- package/dist/types/composables/useFillHandle.d.ts +30 -0
- package/dist/types/composables/useFilterOptions.d.ts +15 -0
- package/dist/types/composables/useInlineCellEditorState.d.ts +20 -0
- package/dist/types/composables/useKeyboardNavigation.d.ts +46 -0
- package/dist/types/composables/useMultiSelectFilterState.d.ts +20 -0
- package/dist/types/composables/useOGrid.d.ts +52 -0
- package/dist/types/composables/usePeopleFilterState.d.ts +20 -0
- package/dist/types/composables/useRichSelectState.d.ts +21 -0
- package/dist/types/composables/useRowSelection.d.ts +21 -0
- package/dist/types/composables/useSideBarState.d.ts +19 -0
- package/dist/types/composables/useTableLayout.d.ts +27 -0
- package/dist/types/composables/useTextFilterState.d.ts +13 -0
- package/dist/types/composables/useUndoRedo.d.ts +21 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/types/columnTypes.d.ts +25 -0
- package/dist/types/types/dataGridTypes.d.ts +151 -0
- package/dist/types/types/index.d.ts +3 -0
- package/dist/types/utils/dataGridViewModel.d.ts +137 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/package.json +38 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Main orchestration composables
|
|
2
|
+
export { useOGrid } from './useOGrid';
|
|
3
|
+
export { useDataGridState } from './useDataGridState';
|
|
4
|
+
// Feature composables
|
|
5
|
+
export { useActiveCell } from './useActiveCell';
|
|
6
|
+
export { useCellEditing } from './useCellEditing';
|
|
7
|
+
export { useCellSelection } from './useCellSelection';
|
|
8
|
+
export { useClipboard } from './useClipboard';
|
|
9
|
+
export { useRowSelection } from './useRowSelection';
|
|
10
|
+
export { useKeyboardNavigation } from './useKeyboardNavigation';
|
|
11
|
+
export { useFillHandle } from './useFillHandle';
|
|
12
|
+
export { useUndoRedo } from './useUndoRedo';
|
|
13
|
+
export { useContextMenu } from './useContextMenu';
|
|
14
|
+
export { useColumnResize } from './useColumnResize';
|
|
15
|
+
export { useFilterOptions } from './useFilterOptions';
|
|
16
|
+
export { useDebounce, useDebouncedCallback } from './useDebounce';
|
|
17
|
+
export { useTableLayout } from './useTableLayout';
|
|
18
|
+
// Headless state composables
|
|
19
|
+
export { useColumnHeaderFilterState } from './useColumnHeaderFilterState';
|
|
20
|
+
export { useTextFilterState } from './useTextFilterState';
|
|
21
|
+
export { useMultiSelectFilterState } from './useMultiSelectFilterState';
|
|
22
|
+
export { usePeopleFilterState } from './usePeopleFilterState';
|
|
23
|
+
export { useDateFilterState } from './useDateFilterState';
|
|
24
|
+
export { useColumnChooserState } from './useColumnChooserState';
|
|
25
|
+
export { useInlineCellEditorState } from './useInlineCellEditorState';
|
|
26
|
+
export { useRichSelectState } from './useRichSelectState';
|
|
27
|
+
export { useSideBarState } from './useSideBarState';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ref, watch, nextTick } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Tracks the active cell for keyboard navigation.
|
|
4
|
+
* When wrapperRef and editingCell are provided, scrolls the active cell into view when it changes (and not editing).
|
|
5
|
+
*/
|
|
6
|
+
export function useActiveCell(wrapperRef, editingCell) {
|
|
7
|
+
const activeCell = ref(null);
|
|
8
|
+
// Deduplicating setter — skips update when the cell coordinates haven't actually changed.
|
|
9
|
+
const setActiveCell = (cell) => {
|
|
10
|
+
const prev = activeCell.value;
|
|
11
|
+
if (prev === cell)
|
|
12
|
+
return;
|
|
13
|
+
if (prev && cell && prev.rowIndex === cell.rowIndex && prev.columnIndex === cell.columnIndex)
|
|
14
|
+
return;
|
|
15
|
+
activeCell.value = cell;
|
|
16
|
+
};
|
|
17
|
+
// Scroll active cell into view when it changes (equivalent to useLayoutEffect)
|
|
18
|
+
watch([activeCell, () => editingCell?.value], () => {
|
|
19
|
+
if (activeCell.value == null ||
|
|
20
|
+
!wrapperRef?.value ||
|
|
21
|
+
editingCell?.value != null)
|
|
22
|
+
return;
|
|
23
|
+
// Use nextTick to ensure DOM is updated before scrolling
|
|
24
|
+
void nextTick(() => {
|
|
25
|
+
const wrapper = wrapperRef.value;
|
|
26
|
+
if (!wrapper || !activeCell.value)
|
|
27
|
+
return;
|
|
28
|
+
const { rowIndex, columnIndex } = activeCell.value;
|
|
29
|
+
const selector = `[data-row-index="${rowIndex}"][data-col-index="${columnIndex}"]`;
|
|
30
|
+
const cell = wrapper.querySelector(selector);
|
|
31
|
+
if (cell) {
|
|
32
|
+
const thead = wrapper.querySelector('thead');
|
|
33
|
+
const headerHeight = thead ? thead.getBoundingClientRect().height : 0;
|
|
34
|
+
const wrapperRect = wrapper.getBoundingClientRect();
|
|
35
|
+
const cellRect = cell.getBoundingClientRect();
|
|
36
|
+
// Vertical scroll (account for sticky thead)
|
|
37
|
+
const visibleTop = wrapperRect.top + headerHeight;
|
|
38
|
+
if (cellRect.top < visibleTop) {
|
|
39
|
+
wrapper.scrollTop -= visibleTop - cellRect.top;
|
|
40
|
+
}
|
|
41
|
+
else if (cellRect.bottom > wrapperRect.bottom) {
|
|
42
|
+
wrapper.scrollTop += cellRect.bottom - wrapperRect.bottom;
|
|
43
|
+
}
|
|
44
|
+
// Horizontal scroll
|
|
45
|
+
if (cellRect.left < wrapperRect.left) {
|
|
46
|
+
wrapper.scrollLeft -= wrapperRect.left - cellRect.left;
|
|
47
|
+
}
|
|
48
|
+
else if (cellRect.right > wrapperRect.right) {
|
|
49
|
+
wrapper.scrollLeft += cellRect.right - wrapperRect.right;
|
|
50
|
+
}
|
|
51
|
+
if (document.activeElement !== cell && typeof cell.focus === 'function') {
|
|
52
|
+
cell.focus({ preventScroll: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}, { flush: 'post' });
|
|
57
|
+
return { activeCell, setActiveCell };
|
|
58
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Manages cell editing state: which cell is being edited and its pending value.
|
|
4
|
+
*/
|
|
5
|
+
export function useCellEditing() {
|
|
6
|
+
const editingCell = ref(null);
|
|
7
|
+
const pendingEditorValue = ref(undefined);
|
|
8
|
+
const setEditingCell = (cell) => {
|
|
9
|
+
editingCell.value = cell;
|
|
10
|
+
};
|
|
11
|
+
const setPendingEditorValue = (value) => {
|
|
12
|
+
pendingEditorValue.value = value;
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
editingCell,
|
|
16
|
+
setEditingCell,
|
|
17
|
+
pendingEditorValue,
|
|
18
|
+
setPendingEditorValue,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { ref, onMounted, onUnmounted } from 'vue';
|
|
2
|
+
import { normalizeSelectionRange } from '../types';
|
|
3
|
+
/** Compares two selection ranges by value. */
|
|
4
|
+
function rangesEqual(a, b) {
|
|
5
|
+
if (a === b)
|
|
6
|
+
return true;
|
|
7
|
+
if (!a || !b)
|
|
8
|
+
return false;
|
|
9
|
+
return a.startRow === b.startRow && a.endRow === b.endRow &&
|
|
10
|
+
a.startCol === b.startCol && a.endCol === b.endCol;
|
|
11
|
+
}
|
|
12
|
+
/** DOM attribute name used for drag-range highlighting (bypasses Vue). */
|
|
13
|
+
const DRAG_ATTR = 'data-drag-range';
|
|
14
|
+
/** Auto-scroll config */
|
|
15
|
+
const AUTO_SCROLL_EDGE = 40;
|
|
16
|
+
const AUTO_SCROLL_MIN_SPEED = 2;
|
|
17
|
+
const AUTO_SCROLL_MAX_SPEED = 20;
|
|
18
|
+
const AUTO_SCROLL_INTERVAL = 16;
|
|
19
|
+
function autoScrollSpeed(distance) {
|
|
20
|
+
const t = Math.min(distance / AUTO_SCROLL_EDGE, 1);
|
|
21
|
+
return AUTO_SCROLL_MIN_SPEED + t * (AUTO_SCROLL_MAX_SPEED - AUTO_SCROLL_MIN_SPEED);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Manages cell selection range with drag-to-select and select-all support.
|
|
25
|
+
*/
|
|
26
|
+
export function useCellSelection(params) {
|
|
27
|
+
const { colOffset, rowCount, visibleColCount, setActiveCell, wrapperRef } = params;
|
|
28
|
+
const selectionRange = ref(null);
|
|
29
|
+
const isDragging = ref(false);
|
|
30
|
+
let isDraggingInternal = false;
|
|
31
|
+
let dragMoved = false;
|
|
32
|
+
let dragStart = null;
|
|
33
|
+
let rafId = 0;
|
|
34
|
+
let liveDragRange = null;
|
|
35
|
+
let autoScrollInterval = null;
|
|
36
|
+
let lastMousePos = null;
|
|
37
|
+
const setSelectionRange = (next) => {
|
|
38
|
+
if (rangesEqual(selectionRange.value, next))
|
|
39
|
+
return;
|
|
40
|
+
selectionRange.value = next;
|
|
41
|
+
};
|
|
42
|
+
const handleCellMouseDown = (e, rowIndex, globalColIndex) => {
|
|
43
|
+
if (e.button !== 0)
|
|
44
|
+
return;
|
|
45
|
+
if (globalColIndex < colOffset)
|
|
46
|
+
return;
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
const dataColIndex = globalColIndex - colOffset;
|
|
49
|
+
const currentRange = selectionRange.value;
|
|
50
|
+
if (e.shiftKey && currentRange != null) {
|
|
51
|
+
setSelectionRange(normalizeSelectionRange({
|
|
52
|
+
startRow: currentRange.startRow,
|
|
53
|
+
startCol: currentRange.startCol,
|
|
54
|
+
endRow: rowIndex,
|
|
55
|
+
endCol: dataColIndex,
|
|
56
|
+
}));
|
|
57
|
+
setActiveCell({ rowIndex, columnIndex: globalColIndex });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
dragStart = { row: rowIndex, col: dataColIndex };
|
|
61
|
+
dragMoved = false;
|
|
62
|
+
const initial = {
|
|
63
|
+
startRow: rowIndex,
|
|
64
|
+
startCol: dataColIndex,
|
|
65
|
+
endRow: rowIndex,
|
|
66
|
+
endCol: dataColIndex,
|
|
67
|
+
};
|
|
68
|
+
setSelectionRange(initial);
|
|
69
|
+
liveDragRange = initial;
|
|
70
|
+
setActiveCell({ rowIndex, columnIndex: globalColIndex });
|
|
71
|
+
isDraggingInternal = true;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const handleSelectAllCells = () => {
|
|
75
|
+
if (rowCount.value === 0 || visibleColCount.value === 0)
|
|
76
|
+
return;
|
|
77
|
+
setSelectionRange({
|
|
78
|
+
startRow: 0,
|
|
79
|
+
startCol: 0,
|
|
80
|
+
endRow: rowCount.value - 1,
|
|
81
|
+
endCol: visibleColCount.value - 1,
|
|
82
|
+
});
|
|
83
|
+
setActiveCell({ rowIndex: 0, columnIndex: colOffset });
|
|
84
|
+
};
|
|
85
|
+
// --- Window mouse move/up for drag selection ---
|
|
86
|
+
const applyDragAttrs = (range) => {
|
|
87
|
+
const wrapper = wrapperRef.value;
|
|
88
|
+
if (!wrapper)
|
|
89
|
+
return;
|
|
90
|
+
const minR = Math.min(range.startRow, range.endRow);
|
|
91
|
+
const maxR = Math.max(range.startRow, range.endRow);
|
|
92
|
+
const minC = Math.min(range.startCol, range.endCol);
|
|
93
|
+
const maxC = Math.max(range.startCol, range.endCol);
|
|
94
|
+
const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
|
|
95
|
+
for (let i = 0; i < cells.length; i++) {
|
|
96
|
+
const el = cells[i];
|
|
97
|
+
const r = parseInt(el.getAttribute('data-row-index'), 10);
|
|
98
|
+
const c = parseInt(el.getAttribute('data-col-index'), 10) - colOffset;
|
|
99
|
+
const inRange = r >= minR && r <= maxR && c >= minC && c <= maxC;
|
|
100
|
+
if (inRange) {
|
|
101
|
+
if (!el.hasAttribute(DRAG_ATTR))
|
|
102
|
+
el.setAttribute(DRAG_ATTR, '');
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
if (el.hasAttribute(DRAG_ATTR))
|
|
106
|
+
el.removeAttribute(DRAG_ATTR);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const clearDragAttrs = () => {
|
|
111
|
+
const wrapper = wrapperRef.value;
|
|
112
|
+
if (!wrapper)
|
|
113
|
+
return;
|
|
114
|
+
const marked = wrapper.querySelectorAll(`[${DRAG_ATTR}]`);
|
|
115
|
+
for (let i = 0; i < marked.length; i++)
|
|
116
|
+
marked[i].removeAttribute(DRAG_ATTR);
|
|
117
|
+
};
|
|
118
|
+
const resolveRange = (cx, cy) => {
|
|
119
|
+
if (!dragStart)
|
|
120
|
+
return null;
|
|
121
|
+
const target = document.elementFromPoint(cx, cy);
|
|
122
|
+
const cell = target?.closest?.('[data-row-index][data-col-index]');
|
|
123
|
+
if (!cell)
|
|
124
|
+
return null;
|
|
125
|
+
const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
|
|
126
|
+
const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
|
|
127
|
+
if (Number.isNaN(r) || Number.isNaN(c) || c < colOffset)
|
|
128
|
+
return null;
|
|
129
|
+
const dataCol = c - colOffset;
|
|
130
|
+
return normalizeSelectionRange({
|
|
131
|
+
startRow: dragStart.row,
|
|
132
|
+
startCol: dragStart.col,
|
|
133
|
+
endRow: r,
|
|
134
|
+
endCol: dataCol,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
const stopAutoScroll = () => {
|
|
138
|
+
if (autoScrollInterval) {
|
|
139
|
+
clearInterval(autoScrollInterval);
|
|
140
|
+
autoScrollInterval = null;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const updateAutoScroll = () => {
|
|
144
|
+
const wrapper = wrapperRef.value;
|
|
145
|
+
if (!wrapper || !lastMousePos || !isDraggingInternal) {
|
|
146
|
+
stopAutoScroll();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const rect = wrapper.getBoundingClientRect();
|
|
150
|
+
let dx = 0;
|
|
151
|
+
let dy = 0;
|
|
152
|
+
if (lastMousePos.cy < rect.top + AUTO_SCROLL_EDGE) {
|
|
153
|
+
dy = -autoScrollSpeed(rect.top + AUTO_SCROLL_EDGE - lastMousePos.cy);
|
|
154
|
+
}
|
|
155
|
+
else if (lastMousePos.cy > rect.bottom - AUTO_SCROLL_EDGE) {
|
|
156
|
+
dy = autoScrollSpeed(lastMousePos.cy - (rect.bottom - AUTO_SCROLL_EDGE));
|
|
157
|
+
}
|
|
158
|
+
if (lastMousePos.cx < rect.left + AUTO_SCROLL_EDGE) {
|
|
159
|
+
dx = -autoScrollSpeed(rect.left + AUTO_SCROLL_EDGE - lastMousePos.cx);
|
|
160
|
+
}
|
|
161
|
+
else if (lastMousePos.cx > rect.right - AUTO_SCROLL_EDGE) {
|
|
162
|
+
dx = autoScrollSpeed(lastMousePos.cx - (rect.right - AUTO_SCROLL_EDGE));
|
|
163
|
+
}
|
|
164
|
+
if (dx === 0 && dy === 0) {
|
|
165
|
+
stopAutoScroll();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!autoScrollInterval) {
|
|
169
|
+
autoScrollInterval = setInterval(() => {
|
|
170
|
+
const w = wrapperRef.value;
|
|
171
|
+
const p = lastMousePos;
|
|
172
|
+
if (!w || !p || !isDraggingInternal) {
|
|
173
|
+
stopAutoScroll();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const r = w.getBoundingClientRect();
|
|
177
|
+
let sdx = 0;
|
|
178
|
+
let sdy = 0;
|
|
179
|
+
if (p.cy < r.top + AUTO_SCROLL_EDGE)
|
|
180
|
+
sdy = -autoScrollSpeed(r.top + AUTO_SCROLL_EDGE - p.cy);
|
|
181
|
+
else if (p.cy > r.bottom - AUTO_SCROLL_EDGE)
|
|
182
|
+
sdy = autoScrollSpeed(p.cy - (r.bottom - AUTO_SCROLL_EDGE));
|
|
183
|
+
if (p.cx < r.left + AUTO_SCROLL_EDGE)
|
|
184
|
+
sdx = -autoScrollSpeed(r.left + AUTO_SCROLL_EDGE - p.cx);
|
|
185
|
+
else if (p.cx > r.right - AUTO_SCROLL_EDGE)
|
|
186
|
+
sdx = autoScrollSpeed(p.cx - (r.right - AUTO_SCROLL_EDGE));
|
|
187
|
+
if (sdx === 0 && sdy === 0) {
|
|
188
|
+
stopAutoScroll();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
w.scrollTop += sdy;
|
|
192
|
+
w.scrollLeft += sdx;
|
|
193
|
+
const newRange = resolveRange(p.cx, p.cy);
|
|
194
|
+
if (newRange) {
|
|
195
|
+
liveDragRange = newRange;
|
|
196
|
+
applyDragAttrs(newRange);
|
|
197
|
+
}
|
|
198
|
+
}, AUTO_SCROLL_INTERVAL);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
const onMove = (e) => {
|
|
202
|
+
if (!isDraggingInternal || !dragStart)
|
|
203
|
+
return;
|
|
204
|
+
if (!dragMoved) {
|
|
205
|
+
dragMoved = true;
|
|
206
|
+
isDragging.value = true;
|
|
207
|
+
}
|
|
208
|
+
lastMousePos = { cx: e.clientX, cy: e.clientY };
|
|
209
|
+
updateAutoScroll();
|
|
210
|
+
if (rafId)
|
|
211
|
+
cancelAnimationFrame(rafId);
|
|
212
|
+
rafId = requestAnimationFrame(() => {
|
|
213
|
+
rafId = 0;
|
|
214
|
+
if (!lastMousePos)
|
|
215
|
+
return;
|
|
216
|
+
const newRange = resolveRange(lastMousePos.cx, lastMousePos.cy);
|
|
217
|
+
if (!newRange)
|
|
218
|
+
return;
|
|
219
|
+
const prev = liveDragRange;
|
|
220
|
+
if (prev &&
|
|
221
|
+
prev.startRow === newRange.startRow &&
|
|
222
|
+
prev.startCol === newRange.startCol &&
|
|
223
|
+
prev.endRow === newRange.endRow &&
|
|
224
|
+
prev.endCol === newRange.endCol) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
liveDragRange = newRange;
|
|
228
|
+
applyDragAttrs(newRange);
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
const onUp = () => {
|
|
232
|
+
if (!isDraggingInternal)
|
|
233
|
+
return;
|
|
234
|
+
stopAutoScroll();
|
|
235
|
+
if (rafId) {
|
|
236
|
+
cancelAnimationFrame(rafId);
|
|
237
|
+
rafId = 0;
|
|
238
|
+
}
|
|
239
|
+
isDraggingInternal = false;
|
|
240
|
+
const wasDrag = dragMoved;
|
|
241
|
+
if (wasDrag) {
|
|
242
|
+
if (lastMousePos) {
|
|
243
|
+
const flushed = resolveRange(lastMousePos.cx, lastMousePos.cy);
|
|
244
|
+
if (flushed)
|
|
245
|
+
liveDragRange = flushed;
|
|
246
|
+
}
|
|
247
|
+
const finalRange = liveDragRange;
|
|
248
|
+
if (finalRange) {
|
|
249
|
+
setSelectionRange(finalRange);
|
|
250
|
+
setActiveCell({
|
|
251
|
+
rowIndex: finalRange.endRow,
|
|
252
|
+
columnIndex: finalRange.endCol + colOffset,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
clearDragAttrs();
|
|
257
|
+
liveDragRange = null;
|
|
258
|
+
lastMousePos = null;
|
|
259
|
+
dragStart = null;
|
|
260
|
+
if (wasDrag)
|
|
261
|
+
isDragging.value = false;
|
|
262
|
+
};
|
|
263
|
+
onMounted(() => {
|
|
264
|
+
window.addEventListener('mousemove', onMove, true);
|
|
265
|
+
window.addEventListener('mouseup', onUp, true);
|
|
266
|
+
});
|
|
267
|
+
onUnmounted(() => {
|
|
268
|
+
window.removeEventListener('mousemove', onMove, true);
|
|
269
|
+
window.removeEventListener('mouseup', onUp, true);
|
|
270
|
+
if (rafId)
|
|
271
|
+
cancelAnimationFrame(rafId);
|
|
272
|
+
stopAutoScroll();
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
selectionRange,
|
|
276
|
+
setSelectionRange,
|
|
277
|
+
handleCellMouseDown,
|
|
278
|
+
handleSelectAllCells,
|
|
279
|
+
isDragging,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import { getCellValue, parseValue, normalizeSelectionRange } from '@alaarab/ogrid-core';
|
|
3
|
+
/**
|
|
4
|
+
* Manages copy, cut, and paste operations for cell ranges with TSV clipboard format.
|
|
5
|
+
*/
|
|
6
|
+
export function useClipboard(params) {
|
|
7
|
+
const { items, visibleCols, colOffset, selectionRange, activeCell, editable, onCellValueChanged, beginBatch, endBatch, } = params;
|
|
8
|
+
let cutRangeInternal = null;
|
|
9
|
+
const cutRange = ref(null);
|
|
10
|
+
const copyRange = ref(null);
|
|
11
|
+
let internalClipboard = null;
|
|
12
|
+
const getEffectiveRange = () => {
|
|
13
|
+
const sel = selectionRange.value;
|
|
14
|
+
const ac = activeCell.value;
|
|
15
|
+
return sel ?? (ac != null
|
|
16
|
+
? { startRow: ac.rowIndex, startCol: ac.columnIndex - colOffset, endRow: ac.rowIndex, endCol: ac.columnIndex - colOffset }
|
|
17
|
+
: null);
|
|
18
|
+
};
|
|
19
|
+
const handleCopy = () => {
|
|
20
|
+
const range = getEffectiveRange();
|
|
21
|
+
if (range == null)
|
|
22
|
+
return;
|
|
23
|
+
const norm = normalizeSelectionRange(range);
|
|
24
|
+
const currentItems = items.value;
|
|
25
|
+
const currentCols = visibleCols.value;
|
|
26
|
+
const rows = [];
|
|
27
|
+
for (let r = norm.startRow; r <= norm.endRow; r++) {
|
|
28
|
+
const cells = [];
|
|
29
|
+
for (let c = norm.startCol; c <= norm.endCol; c++) {
|
|
30
|
+
if (r >= currentItems.length || c >= currentCols.length)
|
|
31
|
+
break;
|
|
32
|
+
const item = currentItems[r];
|
|
33
|
+
const col = currentCols[c];
|
|
34
|
+
const raw = getCellValue(item, col);
|
|
35
|
+
const val = col.valueFormatter ? col.valueFormatter(raw, item) : raw;
|
|
36
|
+
cells.push(val != null && val !== '' ? String(val).replace(/\t/g, ' ').replace(/\n/g, ' ') : '');
|
|
37
|
+
}
|
|
38
|
+
rows.push(cells.join('\t'));
|
|
39
|
+
}
|
|
40
|
+
const tsv = rows.join('\r\n');
|
|
41
|
+
internalClipboard = tsv;
|
|
42
|
+
copyRange.value = norm;
|
|
43
|
+
void navigator.clipboard.writeText(tsv).catch(() => { });
|
|
44
|
+
};
|
|
45
|
+
const handleCut = () => {
|
|
46
|
+
if (editable.value === false)
|
|
47
|
+
return;
|
|
48
|
+
const range = getEffectiveRange();
|
|
49
|
+
if (range == null || onCellValueChanged.value == null)
|
|
50
|
+
return;
|
|
51
|
+
const norm = normalizeSelectionRange(range);
|
|
52
|
+
cutRangeInternal = norm;
|
|
53
|
+
cutRange.value = norm;
|
|
54
|
+
copyRange.value = null;
|
|
55
|
+
handleCopy();
|
|
56
|
+
copyRange.value = null;
|
|
57
|
+
};
|
|
58
|
+
const handlePaste = async () => {
|
|
59
|
+
if (editable.value === false)
|
|
60
|
+
return;
|
|
61
|
+
const callback = onCellValueChanged.value;
|
|
62
|
+
if (callback == null)
|
|
63
|
+
return;
|
|
64
|
+
let text;
|
|
65
|
+
try {
|
|
66
|
+
text = await navigator.clipboard.readText();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
text = '';
|
|
70
|
+
}
|
|
71
|
+
if (!text.trim() && internalClipboard != null) {
|
|
72
|
+
text = internalClipboard;
|
|
73
|
+
}
|
|
74
|
+
if (!text.trim())
|
|
75
|
+
return;
|
|
76
|
+
const norm = getEffectiveRange();
|
|
77
|
+
const anchorRow = norm ? norm.startRow : 0;
|
|
78
|
+
const anchorCol = norm ? norm.startCol : 0;
|
|
79
|
+
const currentItems = items.value;
|
|
80
|
+
const currentCols = visibleCols.value;
|
|
81
|
+
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
|
|
82
|
+
beginBatch?.();
|
|
83
|
+
for (let r = 0; r < lines.length; r++) {
|
|
84
|
+
const cells = lines[r].split('\t');
|
|
85
|
+
for (let c = 0; c < cells.length; c++) {
|
|
86
|
+
const targetRow = anchorRow + r;
|
|
87
|
+
const targetCol = anchorCol + c;
|
|
88
|
+
if (targetRow >= currentItems.length || targetCol >= currentCols.length)
|
|
89
|
+
continue;
|
|
90
|
+
const item = currentItems[targetRow];
|
|
91
|
+
const col = currentCols[targetCol];
|
|
92
|
+
const colEditable = col.editable === true ||
|
|
93
|
+
(typeof col.editable === 'function' && col.editable(item));
|
|
94
|
+
if (!colEditable)
|
|
95
|
+
continue;
|
|
96
|
+
const rawValue = cells[c] ?? '';
|
|
97
|
+
const oldValue = getCellValue(item, col);
|
|
98
|
+
const result = parseValue(rawValue, oldValue, item, col);
|
|
99
|
+
if (!result.valid)
|
|
100
|
+
continue;
|
|
101
|
+
callback({
|
|
102
|
+
item,
|
|
103
|
+
columnId: col.columnId,
|
|
104
|
+
oldValue,
|
|
105
|
+
newValue: result.value,
|
|
106
|
+
rowIndex: targetRow,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (cutRangeInternal) {
|
|
111
|
+
const cut = cutRangeInternal;
|
|
112
|
+
for (let r = cut.startRow; r <= cut.endRow; r++) {
|
|
113
|
+
for (let c = cut.startCol; c <= cut.endCol; c++) {
|
|
114
|
+
if (r >= currentItems.length || c >= currentCols.length)
|
|
115
|
+
continue;
|
|
116
|
+
const item = currentItems[r];
|
|
117
|
+
const col = currentCols[c];
|
|
118
|
+
const colEditable = col.editable === true ||
|
|
119
|
+
(typeof col.editable === 'function' && col.editable(item));
|
|
120
|
+
if (!colEditable)
|
|
121
|
+
continue;
|
|
122
|
+
const oldValue = getCellValue(item, col);
|
|
123
|
+
const result = parseValue('', oldValue, item, col);
|
|
124
|
+
if (!result.valid)
|
|
125
|
+
continue;
|
|
126
|
+
callback({
|
|
127
|
+
item,
|
|
128
|
+
columnId: col.columnId,
|
|
129
|
+
oldValue,
|
|
130
|
+
newValue: result.value,
|
|
131
|
+
rowIndex: r,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
cutRangeInternal = null;
|
|
136
|
+
cutRange.value = null;
|
|
137
|
+
}
|
|
138
|
+
endBatch?.();
|
|
139
|
+
copyRange.value = null;
|
|
140
|
+
};
|
|
141
|
+
const clearClipboardRanges = () => {
|
|
142
|
+
copyRange.value = null;
|
|
143
|
+
cutRange.value = null;
|
|
144
|
+
cutRangeInternal = null;
|
|
145
|
+
};
|
|
146
|
+
return { handleCopy, handleCut, handlePaste, cutRange, copyRange, clearClipboardRanges };
|
|
147
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ref, watch, onUnmounted } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Returns open/setOpen, handleToggle, handleClose, handleCheckboxChange, handleSelectAll, handleClearAll.
|
|
4
|
+
*/
|
|
5
|
+
export function useColumnChooserState(params) {
|
|
6
|
+
const { columns, visibleColumns, onVisibilityChange } = params;
|
|
7
|
+
const open = ref(false);
|
|
8
|
+
let keyDownHandler = null;
|
|
9
|
+
const setupEscapeHandler = () => {
|
|
10
|
+
cleanupEscapeHandler();
|
|
11
|
+
keyDownHandler = (event) => {
|
|
12
|
+
if (event.key === 'Escape') {
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
open.value = false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
document.addEventListener('keydown', keyDownHandler, true);
|
|
18
|
+
};
|
|
19
|
+
const cleanupEscapeHandler = () => {
|
|
20
|
+
if (keyDownHandler) {
|
|
21
|
+
document.removeEventListener('keydown', keyDownHandler, true);
|
|
22
|
+
keyDownHandler = null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
watch(open, (isOpen) => {
|
|
26
|
+
if (isOpen)
|
|
27
|
+
setupEscapeHandler();
|
|
28
|
+
else
|
|
29
|
+
cleanupEscapeHandler();
|
|
30
|
+
});
|
|
31
|
+
onUnmounted(() => cleanupEscapeHandler());
|
|
32
|
+
const setOpen = (value) => {
|
|
33
|
+
open.value = value;
|
|
34
|
+
};
|
|
35
|
+
const handleToggle = () => {
|
|
36
|
+
open.value = !open.value;
|
|
37
|
+
};
|
|
38
|
+
const handleClose = () => {
|
|
39
|
+
open.value = false;
|
|
40
|
+
};
|
|
41
|
+
const handleCheckboxChange = (columnKey) => {
|
|
42
|
+
return (visible) => {
|
|
43
|
+
onVisibilityChange(columnKey, visible);
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
const handleSelectAll = () => {
|
|
47
|
+
columns.value.forEach((col) => {
|
|
48
|
+
if (!visibleColumns.value.has(col.columnId)) {
|
|
49
|
+
onVisibilityChange(col.columnId, true);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const handleClearAll = () => {
|
|
54
|
+
columns.value.forEach((col) => {
|
|
55
|
+
if (!col.required && visibleColumns.value.has(col.columnId)) {
|
|
56
|
+
onVisibilityChange(col.columnId, false);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const visibleCount = ref(visibleColumns.value.size);
|
|
61
|
+
const totalCount = ref(columns.value.length);
|
|
62
|
+
watch([visibleColumns, columns], () => {
|
|
63
|
+
visibleCount.value = visibleColumns.value.size;
|
|
64
|
+
totalCount.value = columns.value.length;
|
|
65
|
+
}, { immediate: true });
|
|
66
|
+
return {
|
|
67
|
+
open,
|
|
68
|
+
setOpen,
|
|
69
|
+
handleToggle,
|
|
70
|
+
handleClose,
|
|
71
|
+
handleCheckboxChange,
|
|
72
|
+
handleSelectAll,
|
|
73
|
+
handleClearAll,
|
|
74
|
+
visibleCount,
|
|
75
|
+
totalCount,
|
|
76
|
+
};
|
|
77
|
+
}
|