@alaarab/ogrid-vue 2.1.3 → 2.1.4
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,77 +0,0 @@
|
|
|
1
|
-
import { shallowRef, watch, onUnmounted } 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 = shallowRef(null);
|
|
8
|
-
let pendingRaf = 0;
|
|
9
|
-
// Deduplicating setter — skips update when the cell coordinates haven't actually changed.
|
|
10
|
-
const setActiveCell = (cell) => {
|
|
11
|
-
const prev = activeCell.value;
|
|
12
|
-
if (prev === cell)
|
|
13
|
-
return;
|
|
14
|
-
if (prev && cell && prev.rowIndex === cell.rowIndex && prev.columnIndex === cell.columnIndex)
|
|
15
|
-
return;
|
|
16
|
-
activeCell.value = cell;
|
|
17
|
-
};
|
|
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.
|
|
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
|
-
}
|
|
27
|
-
if (activeCell.value == null ||
|
|
28
|
-
!wrapperRef?.value ||
|
|
29
|
-
editingCell?.value != null)
|
|
30
|
-
return;
|
|
31
|
-
// Capture the target coordinates before the async boundary
|
|
32
|
-
const { rowIndex, columnIndex } = activeCell.value;
|
|
33
|
-
pendingRaf = requestAnimationFrame(() => {
|
|
34
|
-
pendingRaf = 0;
|
|
35
|
-
const wrapper = wrapperRef.value;
|
|
36
|
-
if (!wrapper)
|
|
37
|
-
return;
|
|
38
|
-
// Verify the active cell hasn't changed since we scheduled
|
|
39
|
-
const current = activeCell.value;
|
|
40
|
-
if (!current || current.rowIndex !== rowIndex || current.columnIndex !== columnIndex)
|
|
41
|
-
return;
|
|
42
|
-
const selector = `[data-row-index="${rowIndex}"][data-col-index="${columnIndex}"]`;
|
|
43
|
-
const cell = wrapper.querySelector(selector);
|
|
44
|
-
if (cell) {
|
|
45
|
-
const thead = wrapper.querySelector('thead');
|
|
46
|
-
const headerHeight = thead ? thead.getBoundingClientRect().height : 0;
|
|
47
|
-
const wrapperRect = wrapper.getBoundingClientRect();
|
|
48
|
-
const cellRect = cell.getBoundingClientRect();
|
|
49
|
-
// Vertical scroll (account for sticky thead)
|
|
50
|
-
const visibleTop = wrapperRect.top + headerHeight;
|
|
51
|
-
if (cellRect.top < visibleTop) {
|
|
52
|
-
wrapper.scrollTop -= visibleTop - cellRect.top;
|
|
53
|
-
}
|
|
54
|
-
else if (cellRect.bottom > wrapperRect.bottom) {
|
|
55
|
-
wrapper.scrollTop += cellRect.bottom - wrapperRect.bottom;
|
|
56
|
-
}
|
|
57
|
-
// Horizontal scroll
|
|
58
|
-
if (cellRect.left < wrapperRect.left) {
|
|
59
|
-
wrapper.scrollLeft -= wrapperRect.left - cellRect.left;
|
|
60
|
-
}
|
|
61
|
-
else if (cellRect.right > wrapperRect.right) {
|
|
62
|
-
wrapper.scrollLeft += cellRect.right - wrapperRect.right;
|
|
63
|
-
}
|
|
64
|
-
if (document.activeElement !== cell && typeof cell.focus === 'function') {
|
|
65
|
-
cell.focus({ preventScroll: true });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
}, { flush: 'post' });
|
|
70
|
-
onUnmounted(() => {
|
|
71
|
-
if (pendingRaf) {
|
|
72
|
-
cancelAnimationFrame(pendingRaf);
|
|
73
|
-
pendingRaf = 0;
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
return { activeCell, setActiveCell };
|
|
77
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { shallowRef, ref } from 'vue';
|
|
2
|
-
/**
|
|
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.
|
|
5
|
-
*/
|
|
6
|
-
export function useCellEditing(params) {
|
|
7
|
-
const editingCell = shallowRef(null);
|
|
8
|
-
const pendingEditorValue = ref(undefined);
|
|
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
|
-
}
|
|
16
|
-
editingCell.value = cell;
|
|
17
|
-
};
|
|
18
|
-
const setPendingEditorValue = (value) => {
|
|
19
|
-
pendingEditorValue.value = value;
|
|
20
|
-
};
|
|
21
|
-
return {
|
|
22
|
-
editingCell,
|
|
23
|
-
setEditingCell,
|
|
24
|
-
pendingEditorValue,
|
|
25
|
-
setPendingEditorValue,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
import { shallowRef, ref, isRef, onMounted, onUnmounted } from 'vue';
|
|
2
|
-
import { normalizeSelectionRange, rangesEqual, computeAutoScrollSpeed } from '@alaarab/ogrid-core';
|
|
3
|
-
import { useLatestRef } from './useLatestRef';
|
|
4
|
-
/** DOM attribute names used for drag-range highlighting (bypasses Vue). */
|
|
5
|
-
const DRAG_ATTR = 'data-drag-range';
|
|
6
|
-
const DRAG_ANCHOR_ATTR = 'data-drag-anchor';
|
|
7
|
-
/** Auto-scroll config */
|
|
8
|
-
const AUTO_SCROLL_EDGE = 40;
|
|
9
|
-
const AUTO_SCROLL_INTERVAL = 16;
|
|
10
|
-
/**
|
|
11
|
-
* Manages cell selection range with drag-to-select and select-all support.
|
|
12
|
-
*/
|
|
13
|
-
export function useCellSelection(params) {
|
|
14
|
-
// Store latest params in a ref for stable handler references
|
|
15
|
-
const paramsRef = useLatestRef(params);
|
|
16
|
-
const { wrapperRef, setActiveCell } = params; // These are stable, safe to destructure
|
|
17
|
-
const getColOffset = () => isRef(params.colOffset) ? params.colOffset.value : params.colOffset;
|
|
18
|
-
const selectionRange = shallowRef(null);
|
|
19
|
-
const isDragging = ref(false); // boolean primitive, ref is fine
|
|
20
|
-
const isDraggingInternal = ref(false); // ref so event handlers always read current value
|
|
21
|
-
const isUnmounted = ref(false); // ref for clean unmount tracking
|
|
22
|
-
let dragMoved = false;
|
|
23
|
-
let dragStart = null;
|
|
24
|
-
let rafId = 0;
|
|
25
|
-
let liveDragRange = null;
|
|
26
|
-
let autoScrollInterval = null;
|
|
27
|
-
let lastMousePos = null;
|
|
28
|
-
const setSelectionRange = (next) => {
|
|
29
|
-
if (rangesEqual(selectionRange.value, next))
|
|
30
|
-
return;
|
|
31
|
-
selectionRange.value = next;
|
|
32
|
-
};
|
|
33
|
-
const handleCellMouseDown = (e, rowIndex, globalColIndex) => {
|
|
34
|
-
if (e.button !== 0)
|
|
35
|
-
return;
|
|
36
|
-
const colOffset = getColOffset();
|
|
37
|
-
if (globalColIndex < colOffset)
|
|
38
|
-
return;
|
|
39
|
-
e.preventDefault();
|
|
40
|
-
const dataColIndex = globalColIndex - colOffset;
|
|
41
|
-
const currentRange = selectionRange.value;
|
|
42
|
-
if (e.shiftKey && currentRange != null) {
|
|
43
|
-
setSelectionRange(normalizeSelectionRange({
|
|
44
|
-
startRow: currentRange.startRow,
|
|
45
|
-
startCol: currentRange.startCol,
|
|
46
|
-
endRow: rowIndex,
|
|
47
|
-
endCol: dataColIndex,
|
|
48
|
-
}));
|
|
49
|
-
setActiveCell({ rowIndex, columnIndex: globalColIndex });
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
dragStart = { row: rowIndex, col: dataColIndex };
|
|
53
|
-
dragMoved = false;
|
|
54
|
-
const initial = {
|
|
55
|
-
startRow: rowIndex,
|
|
56
|
-
startCol: dataColIndex,
|
|
57
|
-
endRow: rowIndex,
|
|
58
|
-
endCol: dataColIndex,
|
|
59
|
-
};
|
|
60
|
-
setSelectionRange(initial);
|
|
61
|
-
liveDragRange = initial;
|
|
62
|
-
setActiveCell({ rowIndex, columnIndex: globalColIndex });
|
|
63
|
-
isDraggingInternal.value = true;
|
|
64
|
-
// Apply drag attrs immediately for the initial cell so the anchor styling shows
|
|
65
|
-
// even before the first mousemove. This ensures instant visual feedback.
|
|
66
|
-
setTimeout(() => applyDragAttrs(initial), 0);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
const handleSelectAllCells = () => {
|
|
70
|
-
const { rowCount, visibleColCount } = paramsRef.value;
|
|
71
|
-
if (rowCount.value === 0 || visibleColCount.value === 0)
|
|
72
|
-
return;
|
|
73
|
-
setSelectionRange({
|
|
74
|
-
startRow: 0,
|
|
75
|
-
startCol: 0,
|
|
76
|
-
endRow: rowCount.value - 1,
|
|
77
|
-
endCol: visibleColCount.value - 1,
|
|
78
|
-
});
|
|
79
|
-
setActiveCell({ rowIndex: 0, columnIndex: getColOffset() });
|
|
80
|
-
};
|
|
81
|
-
// --- Window mouse move/up for drag selection ---
|
|
82
|
-
/** Set of currently drag-marked HTMLElements — avoids O(n) full DOM scan on each frame. */
|
|
83
|
-
const markedCells = new Set();
|
|
84
|
-
/** Cell lookup index built on drag start — O(1) lookups per frame instead of querySelectorAll. */
|
|
85
|
-
let cellIndex = null;
|
|
86
|
-
/** Build cell lookup index from a single querySelectorAll scan. */
|
|
87
|
-
const buildCellIndex = () => {
|
|
88
|
-
const wrapper = wrapperRef.value;
|
|
89
|
-
if (!wrapper)
|
|
90
|
-
return;
|
|
91
|
-
cellIndex = new Map();
|
|
92
|
-
const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
|
|
93
|
-
for (let i = 0; i < cells.length; i++) {
|
|
94
|
-
const el = cells[i];
|
|
95
|
-
const r = el.getAttribute('data-row-index') ?? '';
|
|
96
|
-
const c = el.getAttribute('data-col-index') ?? '';
|
|
97
|
-
cellIndex.set(`${r},${c}`, el);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
/** Apply styling to a single in-range cell (attrs + box-shadow). */
|
|
101
|
-
const styleCellInRange = (el, r, c, minR, maxR, minC, maxC, anchor) => {
|
|
102
|
-
if (!el.hasAttribute(DRAG_ATTR))
|
|
103
|
-
el.setAttribute(DRAG_ATTR, '');
|
|
104
|
-
const isAnchor = anchor && r === anchor.row && c === anchor.col;
|
|
105
|
-
if (isAnchor) {
|
|
106
|
-
if (!el.hasAttribute(DRAG_ANCHOR_ATTR))
|
|
107
|
-
el.setAttribute(DRAG_ANCHOR_ATTR, '');
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
if (el.hasAttribute(DRAG_ANCHOR_ATTR))
|
|
111
|
-
el.removeAttribute(DRAG_ANCHOR_ATTR);
|
|
112
|
-
}
|
|
113
|
-
const shadows = [];
|
|
114
|
-
if (r === minR)
|
|
115
|
-
shadows.push('inset 0 2px 0 0 var(--ogrid-selection, #217346)');
|
|
116
|
-
if (r === maxR)
|
|
117
|
-
shadows.push('inset 0 -2px 0 0 var(--ogrid-selection, #217346)');
|
|
118
|
-
if (c === minC)
|
|
119
|
-
shadows.push('inset 2px 0 0 0 var(--ogrid-selection, #217346)');
|
|
120
|
-
if (c === maxC)
|
|
121
|
-
shadows.push('inset -2px 0 0 0 var(--ogrid-selection, #217346)');
|
|
122
|
-
el.style.boxShadow = shadows.length > 0 ? shadows.join(', ') : '';
|
|
123
|
-
markedCells.add(el);
|
|
124
|
-
};
|
|
125
|
-
/** Remove drag styling from a single cell. */
|
|
126
|
-
const unstyleCell = (el) => {
|
|
127
|
-
el.removeAttribute(DRAG_ATTR);
|
|
128
|
-
el.removeAttribute(DRAG_ANCHOR_ATTR);
|
|
129
|
-
el.style.boxShadow = '';
|
|
130
|
-
};
|
|
131
|
-
/** Toggle DRAG_ATTR on cells to show the range highlight via CSS.
|
|
132
|
-
* Uses a cell index Map for O(1) lookups per cell in the range instead of scanning all cells.
|
|
133
|
-
* Also sets edge box-shadows for a green border around the selection range,
|
|
134
|
-
* and marks the anchor cell with DRAG_ANCHOR_ATTR (white background). */
|
|
135
|
-
const applyDragAttrs = (range) => {
|
|
136
|
-
const wrapper = wrapperRef.value;
|
|
137
|
-
if (!wrapper)
|
|
138
|
-
return;
|
|
139
|
-
const minR = Math.min(range.startRow, range.endRow);
|
|
140
|
-
const maxR = Math.max(range.startRow, range.endRow);
|
|
141
|
-
const minC = Math.min(range.startCol, range.endCol);
|
|
142
|
-
const maxC = Math.max(range.startCol, range.endCol);
|
|
143
|
-
const anchor = dragStart;
|
|
144
|
-
const colOff = getColOffset();
|
|
145
|
-
// 1. Un-mark cells that are no longer in the new range (iterate the small set, not all DOM)
|
|
146
|
-
for (const el of markedCells) {
|
|
147
|
-
const r = parseInt(el.getAttribute('data-row-index') ?? '', 10);
|
|
148
|
-
const c = parseInt(el.getAttribute('data-col-index') ?? '', 10) - colOff;
|
|
149
|
-
const stillInRange = r >= minR && r <= maxR && c >= minC && c <= maxC;
|
|
150
|
-
if (!stillInRange) {
|
|
151
|
-
unstyleCell(el);
|
|
152
|
-
markedCells.delete(el);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// Build index on first call if not yet initialized
|
|
156
|
-
if (!cellIndex)
|
|
157
|
-
buildCellIndex();
|
|
158
|
-
// 2. Look up only the cells in the new range — O(range size) via Map lookup.
|
|
159
|
-
for (let r = minR; r <= maxR; r++) {
|
|
160
|
-
for (let c = minC; c <= maxC; c++) {
|
|
161
|
-
const key = `${r},${c + colOff}`;
|
|
162
|
-
let el = cellIndex?.get(key);
|
|
163
|
-
// Handle virtual scroll recycling — if element is stale, rebuild index once
|
|
164
|
-
if (el && !el.isConnected) {
|
|
165
|
-
buildCellIndex();
|
|
166
|
-
el = cellIndex?.get(key);
|
|
167
|
-
}
|
|
168
|
-
if (el) {
|
|
169
|
-
styleCellInRange(el, r, c, minR, maxR, minC, maxC, anchor);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
/** Clear all drag styling using the tracked set — O(marked) not O(all cells). */
|
|
175
|
-
const clearDragAttrs = () => {
|
|
176
|
-
for (const el of markedCells) {
|
|
177
|
-
unstyleCell(el);
|
|
178
|
-
}
|
|
179
|
-
markedCells.clear();
|
|
180
|
-
cellIndex = null;
|
|
181
|
-
};
|
|
182
|
-
const resolveRange = (cx, cy) => {
|
|
183
|
-
if (!dragStart)
|
|
184
|
-
return null;
|
|
185
|
-
const target = document.elementFromPoint(cx, cy);
|
|
186
|
-
const cell = target?.closest?.('[data-row-index][data-col-index]');
|
|
187
|
-
if (!cell)
|
|
188
|
-
return null;
|
|
189
|
-
const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
|
|
190
|
-
const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
|
|
191
|
-
const colOffset = getColOffset();
|
|
192
|
-
if (Number.isNaN(r) || Number.isNaN(c) || c < colOffset)
|
|
193
|
-
return null;
|
|
194
|
-
const dataCol = c - colOffset;
|
|
195
|
-
return normalizeSelectionRange({
|
|
196
|
-
startRow: dragStart.row,
|
|
197
|
-
startCol: dragStart.col,
|
|
198
|
-
endRow: r,
|
|
199
|
-
endCol: dataCol,
|
|
200
|
-
});
|
|
201
|
-
};
|
|
202
|
-
const stopAutoScroll = () => {
|
|
203
|
-
if (autoScrollInterval) {
|
|
204
|
-
clearInterval(autoScrollInterval);
|
|
205
|
-
autoScrollInterval = null;
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
const updateAutoScroll = () => {
|
|
209
|
-
const wrapper = wrapperRef.value;
|
|
210
|
-
if (!wrapper || !lastMousePos || !isDraggingInternal.value) {
|
|
211
|
-
stopAutoScroll();
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
const rect = wrapper.getBoundingClientRect();
|
|
215
|
-
let dx = 0;
|
|
216
|
-
let dy = 0;
|
|
217
|
-
if (lastMousePos.cy < rect.top + AUTO_SCROLL_EDGE) {
|
|
218
|
-
dy = -computeAutoScrollSpeed(rect.top + AUTO_SCROLL_EDGE - lastMousePos.cy);
|
|
219
|
-
}
|
|
220
|
-
else if (lastMousePos.cy > rect.bottom - AUTO_SCROLL_EDGE) {
|
|
221
|
-
dy = computeAutoScrollSpeed(lastMousePos.cy - (rect.bottom - AUTO_SCROLL_EDGE));
|
|
222
|
-
}
|
|
223
|
-
if (lastMousePos.cx < rect.left + AUTO_SCROLL_EDGE) {
|
|
224
|
-
dx = -computeAutoScrollSpeed(rect.left + AUTO_SCROLL_EDGE - lastMousePos.cx);
|
|
225
|
-
}
|
|
226
|
-
else if (lastMousePos.cx > rect.right - AUTO_SCROLL_EDGE) {
|
|
227
|
-
dx = computeAutoScrollSpeed(lastMousePos.cx - (rect.right - AUTO_SCROLL_EDGE));
|
|
228
|
-
}
|
|
229
|
-
if (dx === 0 && dy === 0) {
|
|
230
|
-
stopAutoScroll();
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
if (!autoScrollInterval) {
|
|
234
|
-
autoScrollInterval = setInterval(() => {
|
|
235
|
-
const w = wrapperRef.value;
|
|
236
|
-
const p = lastMousePos;
|
|
237
|
-
if (!w || !p || !isDraggingInternal.value) {
|
|
238
|
-
stopAutoScroll();
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const r = w.getBoundingClientRect();
|
|
242
|
-
let sdx = 0;
|
|
243
|
-
let sdy = 0;
|
|
244
|
-
if (p.cy < r.top + AUTO_SCROLL_EDGE)
|
|
245
|
-
sdy = -computeAutoScrollSpeed(r.top + AUTO_SCROLL_EDGE - p.cy);
|
|
246
|
-
else if (p.cy > r.bottom - AUTO_SCROLL_EDGE)
|
|
247
|
-
sdy = computeAutoScrollSpeed(p.cy - (r.bottom - AUTO_SCROLL_EDGE));
|
|
248
|
-
if (p.cx < r.left + AUTO_SCROLL_EDGE)
|
|
249
|
-
sdx = -computeAutoScrollSpeed(r.left + AUTO_SCROLL_EDGE - p.cx);
|
|
250
|
-
else if (p.cx > r.right - AUTO_SCROLL_EDGE)
|
|
251
|
-
sdx = computeAutoScrollSpeed(p.cx - (r.right - AUTO_SCROLL_EDGE));
|
|
252
|
-
if (sdx === 0 && sdy === 0) {
|
|
253
|
-
stopAutoScroll();
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
w.scrollTop += sdy;
|
|
257
|
-
w.scrollLeft += sdx;
|
|
258
|
-
const newRange = resolveRange(p.cx, p.cy);
|
|
259
|
-
if (newRange) {
|
|
260
|
-
liveDragRange = newRange;
|
|
261
|
-
applyDragAttrs(newRange);
|
|
262
|
-
}
|
|
263
|
-
}, AUTO_SCROLL_INTERVAL);
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
const onMove = (e) => {
|
|
267
|
-
if (!isDraggingInternal.value || !dragStart)
|
|
268
|
-
return;
|
|
269
|
-
if (!dragMoved) {
|
|
270
|
-
dragMoved = true;
|
|
271
|
-
isDragging.value = true;
|
|
272
|
-
// Build cell index once at drag start for O(1) lookups during drag
|
|
273
|
-
buildCellIndex();
|
|
274
|
-
}
|
|
275
|
-
lastMousePos = { cx: e.clientX, cy: e.clientY };
|
|
276
|
-
updateAutoScroll();
|
|
277
|
-
if (rafId)
|
|
278
|
-
cancelAnimationFrame(rafId);
|
|
279
|
-
rafId = requestAnimationFrame(() => {
|
|
280
|
-
rafId = 0;
|
|
281
|
-
if (!lastMousePos)
|
|
282
|
-
return;
|
|
283
|
-
const newRange = resolveRange(lastMousePos.cx, lastMousePos.cy);
|
|
284
|
-
if (!newRange)
|
|
285
|
-
return;
|
|
286
|
-
const prev = liveDragRange;
|
|
287
|
-
if (prev &&
|
|
288
|
-
prev.startRow === newRange.startRow &&
|
|
289
|
-
prev.startCol === newRange.startCol &&
|
|
290
|
-
prev.endRow === newRange.endRow &&
|
|
291
|
-
prev.endCol === newRange.endCol) {
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
liveDragRange = newRange;
|
|
295
|
-
applyDragAttrs(newRange);
|
|
296
|
-
});
|
|
297
|
-
};
|
|
298
|
-
const onUp = () => {
|
|
299
|
-
if (!isDraggingInternal.value)
|
|
300
|
-
return;
|
|
301
|
-
stopAutoScroll();
|
|
302
|
-
if (rafId) {
|
|
303
|
-
cancelAnimationFrame(rafId);
|
|
304
|
-
rafId = 0;
|
|
305
|
-
}
|
|
306
|
-
isDraggingInternal.value = false;
|
|
307
|
-
const wasDrag = dragMoved;
|
|
308
|
-
if (wasDrag) {
|
|
309
|
-
if (lastMousePos) {
|
|
310
|
-
const flushed = resolveRange(lastMousePos.cx, lastMousePos.cy);
|
|
311
|
-
if (flushed)
|
|
312
|
-
liveDragRange = flushed;
|
|
313
|
-
}
|
|
314
|
-
const finalRange = liveDragRange;
|
|
315
|
-
if (finalRange) {
|
|
316
|
-
setSelectionRange(finalRange);
|
|
317
|
-
setActiveCell({
|
|
318
|
-
rowIndex: finalRange.endRow,
|
|
319
|
-
columnIndex: finalRange.endCol + getColOffset(),
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
clearDragAttrs();
|
|
324
|
-
liveDragRange = null;
|
|
325
|
-
lastMousePos = null;
|
|
326
|
-
dragStart = null;
|
|
327
|
-
if (wasDrag)
|
|
328
|
-
isDragging.value = false;
|
|
329
|
-
};
|
|
330
|
-
const onMoveSafe = (e) => {
|
|
331
|
-
if (isUnmounted.value)
|
|
332
|
-
return;
|
|
333
|
-
onMove(e);
|
|
334
|
-
};
|
|
335
|
-
const onUpSafe = () => {
|
|
336
|
-
if (isUnmounted.value)
|
|
337
|
-
return;
|
|
338
|
-
onUp();
|
|
339
|
-
};
|
|
340
|
-
onMounted(() => {
|
|
341
|
-
window.addEventListener('mousemove', onMoveSafe, true);
|
|
342
|
-
window.addEventListener('mouseup', onUpSafe, true);
|
|
343
|
-
});
|
|
344
|
-
onUnmounted(() => {
|
|
345
|
-
isUnmounted.value = true;
|
|
346
|
-
window.removeEventListener('mousemove', onMoveSafe, true);
|
|
347
|
-
window.removeEventListener('mouseup', onUpSafe, true);
|
|
348
|
-
if (rafId)
|
|
349
|
-
cancelAnimationFrame(rafId);
|
|
350
|
-
stopAutoScroll();
|
|
351
|
-
});
|
|
352
|
-
return {
|
|
353
|
-
selectionRange,
|
|
354
|
-
setSelectionRange,
|
|
355
|
-
handleCellMouseDown,
|
|
356
|
-
handleSelectAllCells,
|
|
357
|
-
isDragging,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { ref, shallowRef, isRef } from 'vue';
|
|
2
|
-
import { normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear } 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, selectionRange, activeCell, editable, onCellValueChanged, beginBatch, endBatch, } = params;
|
|
8
|
-
const getColOffset = () => isRef(params.colOffset) ? params.colOffset.value : params.colOffset;
|
|
9
|
-
const cutRange = shallowRef(null);
|
|
10
|
-
const copyRange = shallowRef(null);
|
|
11
|
-
const internalClipboardRef = ref(null);
|
|
12
|
-
const getEffectiveRange = () => {
|
|
13
|
-
const sel = selectionRange.value;
|
|
14
|
-
const ac = activeCell.value;
|
|
15
|
-
const colOffset = getColOffset();
|
|
16
|
-
return sel ?? (ac != null
|
|
17
|
-
? { startRow: ac.rowIndex, startCol: ac.columnIndex - colOffset, endRow: ac.rowIndex, endCol: ac.columnIndex - colOffset }
|
|
18
|
-
: null);
|
|
19
|
-
};
|
|
20
|
-
const handleCopy = () => {
|
|
21
|
-
const range = getEffectiveRange();
|
|
22
|
-
if (range == null)
|
|
23
|
-
return;
|
|
24
|
-
const norm = normalizeSelectionRange(range);
|
|
25
|
-
const tsv = formatSelectionAsTsv(items.value, visibleCols.value, norm);
|
|
26
|
-
internalClipboardRef.value = tsv;
|
|
27
|
-
copyRange.value = norm;
|
|
28
|
-
void navigator.clipboard.writeText(tsv).catch((err) => {
|
|
29
|
-
if (typeof console !== 'undefined')
|
|
30
|
-
console.warn('[OGrid] Clipboard write failed:', err);
|
|
31
|
-
});
|
|
32
|
-
};
|
|
33
|
-
const handleCut = () => {
|
|
34
|
-
if (editable.value === false)
|
|
35
|
-
return;
|
|
36
|
-
const range = getEffectiveRange();
|
|
37
|
-
if (range == null || onCellValueChanged.value == null)
|
|
38
|
-
return;
|
|
39
|
-
const norm = normalizeSelectionRange(range);
|
|
40
|
-
cutRange.value = norm;
|
|
41
|
-
copyRange.value = null;
|
|
42
|
-
handleCopy();
|
|
43
|
-
copyRange.value = null;
|
|
44
|
-
};
|
|
45
|
-
const handlePaste = async () => {
|
|
46
|
-
if (editable.value === false)
|
|
47
|
-
return;
|
|
48
|
-
const callback = onCellValueChanged.value;
|
|
49
|
-
if (callback == null)
|
|
50
|
-
return;
|
|
51
|
-
let text;
|
|
52
|
-
try {
|
|
53
|
-
text = await navigator.clipboard.readText();
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
text = '';
|
|
57
|
-
}
|
|
58
|
-
if (!text.trim() && internalClipboardRef.value != null) {
|
|
59
|
-
text = internalClipboardRef.value;
|
|
60
|
-
}
|
|
61
|
-
if (!text.trim())
|
|
62
|
-
return;
|
|
63
|
-
const norm = getEffectiveRange();
|
|
64
|
-
const anchorRow = norm ? norm.startRow : 0;
|
|
65
|
-
const anchorCol = norm ? norm.startCol : 0;
|
|
66
|
-
const currentItems = items.value;
|
|
67
|
-
const currentCols = visibleCols.value;
|
|
68
|
-
const parsedRows = parseTsvClipboard(text);
|
|
69
|
-
beginBatch?.();
|
|
70
|
-
const pasteEvents = applyPastedValues(parsedRows, anchorRow, anchorCol, currentItems, currentCols);
|
|
71
|
-
for (const evt of pasteEvents)
|
|
72
|
-
callback(evt);
|
|
73
|
-
if (cutRange.value) {
|
|
74
|
-
const cutEvents = applyCutClear(cutRange.value, currentItems, currentCols);
|
|
75
|
-
for (const evt of cutEvents)
|
|
76
|
-
callback(evt);
|
|
77
|
-
cutRange.value = null;
|
|
78
|
-
}
|
|
79
|
-
endBatch?.();
|
|
80
|
-
copyRange.value = null;
|
|
81
|
-
};
|
|
82
|
-
const clearClipboardRanges = () => {
|
|
83
|
-
copyRange.value = null;
|
|
84
|
-
cutRange.value = null;
|
|
85
|
-
};
|
|
86
|
-
return { handleCopy, handleCut, handlePaste, cutRange, copyRange, clearClipboardRanges };
|
|
87
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { ref, computed, 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
|
-
// Required columns are silently skipped — no feedback is provided to the user
|
|
55
|
-
columns.value.forEach((col) => {
|
|
56
|
-
if (!col.required && visibleColumns.value.has(col.columnId)) {
|
|
57
|
-
onVisibilityChange(col.columnId, false);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
};
|
|
61
|
-
const visibleCount = computed(() => visibleColumns.value.size);
|
|
62
|
-
const totalCount = computed(() => columns.value.length);
|
|
63
|
-
return {
|
|
64
|
-
open,
|
|
65
|
-
setOpen,
|
|
66
|
-
handleToggle,
|
|
67
|
-
handleClose,
|
|
68
|
-
handleCheckboxChange,
|
|
69
|
-
handleSelectAll,
|
|
70
|
-
handleClearAll,
|
|
71
|
-
visibleCount,
|
|
72
|
-
totalCount,
|
|
73
|
-
};
|
|
74
|
-
}
|