@alaarab/ogrid-vue 2.0.22 → 2.1.0
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/createDataGridTable.js +5 -1
- package/dist/esm/components/createOGrid.js +10 -7
- package/dist/esm/composables/useCellSelection.js +101 -61
- package/dist/esm/composables/useClipboard.js +15 -55
- package/dist/esm/composables/useColumnChooserState.js +4 -7
- package/dist/esm/composables/useColumnHeaderFilterState.js +10 -7
- package/dist/esm/composables/useColumnHeaderMenuState.js +2 -4
- package/dist/esm/composables/useColumnPinning.js +2 -2
- package/dist/esm/composables/useColumnReorder.js +8 -1
- package/dist/esm/composables/useDataGridState.js +33 -30
- package/dist/esm/composables/useDateFilterState.js +1 -1
- package/dist/esm/composables/useFillHandle.js +67 -50
- package/dist/esm/composables/useKeyboardNavigation.js +25 -109
- package/dist/esm/composables/useLatestRef.js +2 -2
- package/dist/esm/composables/useMultiSelectFilterState.js +1 -1
- package/dist/esm/composables/useOGrid.js +29 -11
- package/dist/esm/composables/usePeopleFilterState.js +2 -2
- package/dist/esm/composables/useRowSelection.js +13 -16
- package/dist/esm/composables/useTableLayout.js +11 -11
- package/dist/esm/composables/useTextFilterState.js +1 -1
- package/dist/esm/composables/useVirtualScroll.js +20 -17
- package/dist/types/composables/index.d.ts +1 -0
- package/dist/types/composables/useCellSelection.d.ts +1 -1
- package/dist/types/composables/useClipboard.d.ts +1 -1
- package/dist/types/composables/useDateFilterState.d.ts +2 -2
- package/dist/types/composables/useFillHandle.d.ts +1 -1
- package/dist/types/composables/useKeyboardNavigation.d.ts +4 -6
- package/dist/types/composables/useLatestRef.d.ts +3 -1
- package/dist/types/composables/useMultiSelectFilterState.d.ts +1 -1
- package/dist/types/composables/usePeopleFilterState.d.ts +1 -1
- package/dist/types/composables/useTextFilterState.d.ts +2 -2
- package/dist/types/index.d.ts +1 -1
- package/package.json +10 -3
|
@@ -29,6 +29,8 @@ export function createDataGridTable(ui) {
|
|
|
29
29
|
const onWrapperMousedown = (e) => { lastMouseShift.value = e.shiftKey; };
|
|
30
30
|
const onContextmenu = (e) => e.preventDefault();
|
|
31
31
|
const stopPropagation = (e) => e.stopPropagation();
|
|
32
|
+
// Pre-compute header rows so buildHeaderRows is not called on every render
|
|
33
|
+
const headerRowsComputed = computed(() => buildHeaderRows(propsRef.value.columns, propsRef.value.visibleColumns));
|
|
32
34
|
// Pre-compute per-column layout metadata so it's only recalculated when
|
|
33
35
|
// column config, sizing, pinning, or measured widths change — not on every
|
|
34
36
|
// render (parity with React's columnMeta useMemo).
|
|
@@ -111,7 +113,7 @@ export function createDataGridTable(ui) {
|
|
|
111
113
|
const ariaLabelledBy = p['aria-labelledby'];
|
|
112
114
|
const fitToContent = layoutMode === 'content';
|
|
113
115
|
const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
|
|
114
|
-
const headerRows =
|
|
116
|
+
const headerRows = headerRowsComputed.value;
|
|
115
117
|
const editCallbacks = { commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit };
|
|
116
118
|
const interactionHandlers = { handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu };
|
|
117
119
|
const handleSingleRowClick = (e) => {
|
|
@@ -316,6 +318,8 @@ export function createDataGridTable(ui) {
|
|
|
316
318
|
class: 'ogrid-column-group-header',
|
|
317
319
|
}, cell.label);
|
|
318
320
|
}
|
|
321
|
+
if (!cell.columnDef)
|
|
322
|
+
return null;
|
|
319
323
|
const col = cell.columnDef;
|
|
320
324
|
const { classes: headerClasses, style: headerStyle } = getHeaderClassAndStyle(col);
|
|
321
325
|
return h('th', {
|
|
@@ -115,7 +115,8 @@ function renderSideBar(sb) {
|
|
|
115
115
|
h('div', { style: { fontWeight: '500', marginBottom: '4px', fontSize: '13px' } }, col.name),
|
|
116
116
|
];
|
|
117
117
|
if (col.filterType === 'text') {
|
|
118
|
-
const
|
|
118
|
+
const filterEntry = sb.filters[filterKey];
|
|
119
|
+
const currentVal = filterEntry?.type === 'text' ? filterEntry.value : '';
|
|
119
120
|
groupChildren.push(h('input', {
|
|
120
121
|
type: 'text',
|
|
121
122
|
value: currentVal,
|
|
@@ -131,13 +132,15 @@ function renderSideBar(sb) {
|
|
|
131
132
|
if (col.filterType === 'multiSelect') {
|
|
132
133
|
const options = sb.filterOptions[filterKey] ?? [];
|
|
133
134
|
const msChildren = options.map((opt) => {
|
|
134
|
-
const
|
|
135
|
+
const msFilter = sb.filters[filterKey];
|
|
136
|
+
const selected = msFilter?.type === 'multiSelect' ? msFilter.value.includes(opt) : false;
|
|
135
137
|
return h('label', { key: opt, style: { display: 'flex', alignItems: 'center', gap: '4px', padding: '1px 0', cursor: 'pointer', fontSize: '13px' } }, [
|
|
136
138
|
h('input', {
|
|
137
139
|
type: 'checkbox',
|
|
138
140
|
checked: selected,
|
|
139
141
|
onChange: (e) => {
|
|
140
|
-
const
|
|
142
|
+
const curFilter = sb.filters[filterKey];
|
|
143
|
+
const current = curFilter?.type === 'multiSelect' ? curFilter.value : [];
|
|
141
144
|
const next = e.target.checked
|
|
142
145
|
? [...current, opt]
|
|
143
146
|
: current.filter((v) => v !== opt);
|
|
@@ -150,7 +153,8 @@ function renderSideBar(sb) {
|
|
|
150
153
|
groupChildren.push(h('div', { style: { maxHeight: '120px', overflowY: 'auto' }, role: 'group', 'aria-label': `${col.name} options` }, msChildren));
|
|
151
154
|
}
|
|
152
155
|
if (col.filterType === 'date') {
|
|
153
|
-
const
|
|
156
|
+
const dateFilter = sb.filters[filterKey];
|
|
157
|
+
const existingValue = dateFilter?.type === 'date' ? dateFilter.value : { from: undefined, to: undefined };
|
|
154
158
|
groupChildren.push(h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } }, [
|
|
155
159
|
h('label', { style: { display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px' } }, [
|
|
156
160
|
'From:',
|
|
@@ -239,8 +243,8 @@ export function createOGrid(ui) {
|
|
|
239
243
|
setup(props, { expose }) {
|
|
240
244
|
const propsRef = computed(() => props.gridProps);
|
|
241
245
|
const { dataGridProps, pagination, columnChooser, layout, api } = useOGrid(propsRef);
|
|
242
|
-
// Expose the
|
|
243
|
-
expose({ api
|
|
246
|
+
// Expose the ref container so parent always gets the latest API value
|
|
247
|
+
expose({ api });
|
|
244
248
|
return () => {
|
|
245
249
|
const sideBar = layout.value.sideBarProps;
|
|
246
250
|
const hasSideBar = sideBar != null;
|
|
@@ -266,7 +270,6 @@ export function createOGrid(ui) {
|
|
|
266
270
|
onPageChange: pagination.value.setPage,
|
|
267
271
|
onPageSizeChange: (size) => {
|
|
268
272
|
pagination.value.setPageSize(size);
|
|
269
|
-
pagination.value.setPage(1);
|
|
270
273
|
},
|
|
271
274
|
pageSizeOptions: pagination.value.pageSizeOptions,
|
|
272
275
|
entityLabelPlural: pagination.value.entityLabelPlural,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { shallowRef, ref,
|
|
1
|
+
import { shallowRef, ref, isRef, onMounted, onUnmounted } from 'vue';
|
|
2
2
|
import { normalizeSelectionRange, rangesEqual, computeAutoScrollSpeed } from '@alaarab/ogrid-core';
|
|
3
3
|
import { useLatestRef } from './useLatestRef';
|
|
4
4
|
/** DOM attribute names used for drag-range highlighting (bypasses Vue). */
|
|
@@ -12,11 +12,13 @@ const AUTO_SCROLL_INTERVAL = 16;
|
|
|
12
12
|
*/
|
|
13
13
|
export function useCellSelection(params) {
|
|
14
14
|
// Store latest params in a ref for stable handler references
|
|
15
|
-
const paramsRef = useLatestRef(
|
|
16
|
-
const {
|
|
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;
|
|
17
18
|
const selectionRange = shallowRef(null);
|
|
18
19
|
const isDragging = ref(false); // boolean primitive, ref is fine
|
|
19
|
-
|
|
20
|
+
const isDraggingInternal = ref(false); // ref so event handlers always read current value
|
|
21
|
+
const isUnmounted = ref(false); // ref for clean unmount tracking
|
|
20
22
|
let dragMoved = false;
|
|
21
23
|
let dragStart = null;
|
|
22
24
|
let rafId = 0;
|
|
@@ -31,6 +33,7 @@ export function useCellSelection(params) {
|
|
|
31
33
|
const handleCellMouseDown = (e, rowIndex, globalColIndex) => {
|
|
32
34
|
if (e.button !== 0)
|
|
33
35
|
return;
|
|
36
|
+
const colOffset = getColOffset();
|
|
34
37
|
if (globalColIndex < colOffset)
|
|
35
38
|
return;
|
|
36
39
|
e.preventDefault();
|
|
@@ -57,7 +60,7 @@ export function useCellSelection(params) {
|
|
|
57
60
|
setSelectionRange(initial);
|
|
58
61
|
liveDragRange = initial;
|
|
59
62
|
setActiveCell({ rowIndex, columnIndex: globalColIndex });
|
|
60
|
-
isDraggingInternal = true;
|
|
63
|
+
isDraggingInternal.value = true;
|
|
61
64
|
// Apply drag attrs immediately for the initial cell so the anchor styling shows
|
|
62
65
|
// even before the first mousemove. This ensures instant visual feedback.
|
|
63
66
|
setTimeout(() => applyDragAttrs(initial), 0);
|
|
@@ -73,10 +76,60 @@ export function useCellSelection(params) {
|
|
|
73
76
|
endRow: rowCount.value - 1,
|
|
74
77
|
endCol: visibleColCount.value - 1,
|
|
75
78
|
});
|
|
76
|
-
setActiveCell({ rowIndex: 0, columnIndex:
|
|
79
|
+
setActiveCell({ rowIndex: 0, columnIndex: getColOffset() });
|
|
77
80
|
};
|
|
78
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
|
+
};
|
|
79
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.
|
|
80
133
|
* Also sets edge box-shadows for a green border around the selection range,
|
|
81
134
|
* and marks the anchor cell with DRAG_ANCHOR_ATTR (white background). */
|
|
82
135
|
const applyDragAttrs = (range) => {
|
|
@@ -88,58 +141,43 @@ export function useCellSelection(params) {
|
|
|
88
141
|
const minC = Math.min(range.startCol, range.endCol);
|
|
89
142
|
const maxC = Math.max(range.startCol, range.endCol);
|
|
90
143
|
const anchor = dragStart;
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const r = parseInt(el.getAttribute('data-row-index'), 10);
|
|
95
|
-
const c = parseInt(el.getAttribute('data-col-index'), 10) -
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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);
|
|
105
167
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
el.removeAttribute(DRAG_ANCHOR_ATTR);
|
|
168
|
+
if (el) {
|
|
169
|
+
styleCellInRange(el, r, c, minR, maxR, minC, maxC, anchor);
|
|
109
170
|
}
|
|
110
|
-
// Edge borders via inset box-shadow (no layout shift)
|
|
111
|
-
const shadows = [];
|
|
112
|
-
if (r === minR)
|
|
113
|
-
shadows.push('inset 0 2px 0 0 var(--ogrid-selection, #217346)');
|
|
114
|
-
if (r === maxR)
|
|
115
|
-
shadows.push('inset 0 -2px 0 0 var(--ogrid-selection, #217346)');
|
|
116
|
-
if (c === minC)
|
|
117
|
-
shadows.push('inset 2px 0 0 0 var(--ogrid-selection, #217346)');
|
|
118
|
-
if (c === maxC)
|
|
119
|
-
shadows.push('inset -2px 0 0 0 var(--ogrid-selection, #217346)');
|
|
120
|
-
el.style.boxShadow = shadows.length > 0 ? shadows.join(', ') : '';
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
if (el.hasAttribute(DRAG_ATTR))
|
|
124
|
-
el.removeAttribute(DRAG_ATTR);
|
|
125
|
-
if (el.hasAttribute(DRAG_ANCHOR_ATTR))
|
|
126
|
-
el.removeAttribute(DRAG_ANCHOR_ATTR);
|
|
127
|
-
if (el.style.boxShadow)
|
|
128
|
-
el.style.boxShadow = '';
|
|
129
171
|
}
|
|
130
172
|
}
|
|
131
173
|
};
|
|
174
|
+
/** Clear all drag styling using the tracked set — O(marked) not O(all cells). */
|
|
132
175
|
const clearDragAttrs = () => {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
return;
|
|
136
|
-
const marked = wrapper.querySelectorAll(`[${DRAG_ATTR}]`);
|
|
137
|
-
for (let i = 0; i < marked.length; i++) {
|
|
138
|
-
const el = marked[i];
|
|
139
|
-
el.removeAttribute(DRAG_ATTR);
|
|
140
|
-
el.removeAttribute(DRAG_ANCHOR_ATTR);
|
|
141
|
-
el.style.boxShadow = '';
|
|
176
|
+
for (const el of markedCells) {
|
|
177
|
+
unstyleCell(el);
|
|
142
178
|
}
|
|
179
|
+
markedCells.clear();
|
|
180
|
+
cellIndex = null;
|
|
143
181
|
};
|
|
144
182
|
const resolveRange = (cx, cy) => {
|
|
145
183
|
if (!dragStart)
|
|
@@ -150,6 +188,7 @@ export function useCellSelection(params) {
|
|
|
150
188
|
return null;
|
|
151
189
|
const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
|
|
152
190
|
const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
|
|
191
|
+
const colOffset = getColOffset();
|
|
153
192
|
if (Number.isNaN(r) || Number.isNaN(c) || c < colOffset)
|
|
154
193
|
return null;
|
|
155
194
|
const dataCol = c - colOffset;
|
|
@@ -168,7 +207,7 @@ export function useCellSelection(params) {
|
|
|
168
207
|
};
|
|
169
208
|
const updateAutoScroll = () => {
|
|
170
209
|
const wrapper = wrapperRef.value;
|
|
171
|
-
if (!wrapper || !lastMousePos || !isDraggingInternal) {
|
|
210
|
+
if (!wrapper || !lastMousePos || !isDraggingInternal.value) {
|
|
172
211
|
stopAutoScroll();
|
|
173
212
|
return;
|
|
174
213
|
}
|
|
@@ -195,7 +234,7 @@ export function useCellSelection(params) {
|
|
|
195
234
|
autoScrollInterval = setInterval(() => {
|
|
196
235
|
const w = wrapperRef.value;
|
|
197
236
|
const p = lastMousePos;
|
|
198
|
-
if (!w || !p || !isDraggingInternal) {
|
|
237
|
+
if (!w || !p || !isDraggingInternal.value) {
|
|
199
238
|
stopAutoScroll();
|
|
200
239
|
return;
|
|
201
240
|
}
|
|
@@ -225,11 +264,13 @@ export function useCellSelection(params) {
|
|
|
225
264
|
}
|
|
226
265
|
};
|
|
227
266
|
const onMove = (e) => {
|
|
228
|
-
if (!isDraggingInternal || !dragStart)
|
|
267
|
+
if (!isDraggingInternal.value || !dragStart)
|
|
229
268
|
return;
|
|
230
269
|
if (!dragMoved) {
|
|
231
270
|
dragMoved = true;
|
|
232
271
|
isDragging.value = true;
|
|
272
|
+
// Build cell index once at drag start for O(1) lookups during drag
|
|
273
|
+
buildCellIndex();
|
|
233
274
|
}
|
|
234
275
|
lastMousePos = { cx: e.clientX, cy: e.clientY };
|
|
235
276
|
updateAutoScroll();
|
|
@@ -255,14 +296,14 @@ export function useCellSelection(params) {
|
|
|
255
296
|
});
|
|
256
297
|
};
|
|
257
298
|
const onUp = () => {
|
|
258
|
-
if (!isDraggingInternal)
|
|
299
|
+
if (!isDraggingInternal.value)
|
|
259
300
|
return;
|
|
260
301
|
stopAutoScroll();
|
|
261
302
|
if (rafId) {
|
|
262
303
|
cancelAnimationFrame(rafId);
|
|
263
304
|
rafId = 0;
|
|
264
305
|
}
|
|
265
|
-
isDraggingInternal = false;
|
|
306
|
+
isDraggingInternal.value = false;
|
|
266
307
|
const wasDrag = dragMoved;
|
|
267
308
|
if (wasDrag) {
|
|
268
309
|
if (lastMousePos) {
|
|
@@ -275,7 +316,7 @@ export function useCellSelection(params) {
|
|
|
275
316
|
setSelectionRange(finalRange);
|
|
276
317
|
setActiveCell({
|
|
277
318
|
rowIndex: finalRange.endRow,
|
|
278
|
-
columnIndex: finalRange.endCol +
|
|
319
|
+
columnIndex: finalRange.endCol + getColOffset(),
|
|
279
320
|
});
|
|
280
321
|
}
|
|
281
322
|
}
|
|
@@ -286,14 +327,13 @@ export function useCellSelection(params) {
|
|
|
286
327
|
if (wasDrag)
|
|
287
328
|
isDragging.value = false;
|
|
288
329
|
};
|
|
289
|
-
let isUnmounted = false;
|
|
290
330
|
const onMoveSafe = (e) => {
|
|
291
|
-
if (isUnmounted)
|
|
331
|
+
if (isUnmounted.value)
|
|
292
332
|
return;
|
|
293
333
|
onMove(e);
|
|
294
334
|
};
|
|
295
335
|
const onUpSafe = () => {
|
|
296
|
-
if (isUnmounted)
|
|
336
|
+
if (isUnmounted.value)
|
|
297
337
|
return;
|
|
298
338
|
onUp();
|
|
299
339
|
};
|
|
@@ -302,7 +342,7 @@ export function useCellSelection(params) {
|
|
|
302
342
|
window.addEventListener('mouseup', onUpSafe, true);
|
|
303
343
|
});
|
|
304
344
|
onUnmounted(() => {
|
|
305
|
-
isUnmounted = true;
|
|
345
|
+
isUnmounted.value = true;
|
|
306
346
|
window.removeEventListener('mousemove', onMoveSafe, true);
|
|
307
347
|
window.removeEventListener('mouseup', onUpSafe, true);
|
|
308
348
|
if (rafId)
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { ref, shallowRef } from 'vue';
|
|
2
|
-
import {
|
|
1
|
+
import { ref, shallowRef, isRef } from 'vue';
|
|
2
|
+
import { normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear } from '@alaarab/ogrid-core';
|
|
3
3
|
/**
|
|
4
4
|
* Manages copy, cut, and paste operations for cell ranges with TSV clipboard format.
|
|
5
5
|
*/
|
|
6
6
|
export function useClipboard(params) {
|
|
7
|
-
const { items, visibleCols,
|
|
7
|
+
const { items, visibleCols, selectionRange, activeCell, editable, onCellValueChanged, beginBatch, endBatch, } = params;
|
|
8
|
+
const getColOffset = () => isRef(params.colOffset) ? params.colOffset.value : params.colOffset;
|
|
8
9
|
const cutRange = shallowRef(null);
|
|
9
10
|
const copyRange = shallowRef(null);
|
|
10
11
|
const internalClipboardRef = ref(null);
|
|
11
12
|
const getEffectiveRange = () => {
|
|
12
13
|
const sel = selectionRange.value;
|
|
13
14
|
const ac = activeCell.value;
|
|
15
|
+
const colOffset = getColOffset();
|
|
14
16
|
return sel ?? (ac != null
|
|
15
17
|
? { startRow: ac.rowIndex, startCol: ac.columnIndex - colOffset, endRow: ac.rowIndex, endCol: ac.columnIndex - colOffset }
|
|
16
18
|
: null);
|
|
@@ -23,7 +25,10 @@ export function useClipboard(params) {
|
|
|
23
25
|
const tsv = formatSelectionAsTsv(items.value, visibleCols.value, norm);
|
|
24
26
|
internalClipboardRef.value = tsv;
|
|
25
27
|
copyRange.value = norm;
|
|
26
|
-
void navigator.clipboard.writeText(tsv).catch(() => {
|
|
28
|
+
void navigator.clipboard.writeText(tsv).catch((err) => {
|
|
29
|
+
if (typeof console !== 'undefined')
|
|
30
|
+
console.warn('[OGrid] Clipboard write failed:', err);
|
|
31
|
+
});
|
|
27
32
|
};
|
|
28
33
|
const handleCut = () => {
|
|
29
34
|
if (editable.value === false)
|
|
@@ -62,58 +67,13 @@ export function useClipboard(params) {
|
|
|
62
67
|
const currentCols = visibleCols.value;
|
|
63
68
|
const parsedRows = parseTsvClipboard(text);
|
|
64
69
|
beginBatch?.();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const targetRow = anchorRow + r;
|
|
69
|
-
const targetCol = anchorCol + c;
|
|
70
|
-
if (targetRow >= currentItems.length || targetCol >= currentCols.length)
|
|
71
|
-
continue;
|
|
72
|
-
const item = currentItems[targetRow];
|
|
73
|
-
const col = currentCols[targetCol];
|
|
74
|
-
const colEditable = col.editable === true ||
|
|
75
|
-
(typeof col.editable === 'function' && col.editable(item));
|
|
76
|
-
if (!colEditable)
|
|
77
|
-
continue;
|
|
78
|
-
const rawValue = cells[c] ?? '';
|
|
79
|
-
const oldValue = getCellValue(item, col);
|
|
80
|
-
const result = parseValue(rawValue, oldValue, item, col);
|
|
81
|
-
if (!result.valid)
|
|
82
|
-
continue;
|
|
83
|
-
callback({
|
|
84
|
-
item,
|
|
85
|
-
columnId: col.columnId,
|
|
86
|
-
oldValue,
|
|
87
|
-
newValue: result.value,
|
|
88
|
-
rowIndex: targetRow,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
70
|
+
const pasteEvents = applyPastedValues(parsedRows, anchorRow, anchorCol, currentItems, currentCols);
|
|
71
|
+
for (const evt of pasteEvents)
|
|
72
|
+
callback(evt);
|
|
92
73
|
if (cutRange.value) {
|
|
93
|
-
const
|
|
94
|
-
for (
|
|
95
|
-
|
|
96
|
-
if (r >= currentItems.length || c >= currentCols.length)
|
|
97
|
-
continue;
|
|
98
|
-
const item = currentItems[r];
|
|
99
|
-
const col = currentCols[c];
|
|
100
|
-
const colEditable = col.editable === true ||
|
|
101
|
-
(typeof col.editable === 'function' && col.editable(item));
|
|
102
|
-
if (!colEditable)
|
|
103
|
-
continue;
|
|
104
|
-
const oldValue = getCellValue(item, col);
|
|
105
|
-
const result = parseValue('', oldValue, item, col);
|
|
106
|
-
if (!result.valid)
|
|
107
|
-
continue;
|
|
108
|
-
callback({
|
|
109
|
-
item,
|
|
110
|
-
columnId: col.columnId,
|
|
111
|
-
oldValue,
|
|
112
|
-
newValue: result.value,
|
|
113
|
-
rowIndex: r,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
74
|
+
const cutEvents = applyCutClear(cutRange.value, currentItems, currentCols);
|
|
75
|
+
for (const evt of cutEvents)
|
|
76
|
+
callback(evt);
|
|
117
77
|
cutRange.value = null;
|
|
118
78
|
}
|
|
119
79
|
endBatch?.();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref, watch, onUnmounted } from 'vue';
|
|
1
|
+
import { ref, computed, watch, onUnmounted } from 'vue';
|
|
2
2
|
/**
|
|
3
3
|
* Returns open/setOpen, handleToggle, handleClose, handleCheckboxChange, handleSelectAll, handleClearAll.
|
|
4
4
|
*/
|
|
@@ -51,18 +51,15 @@ export function useColumnChooserState(params) {
|
|
|
51
51
|
});
|
|
52
52
|
};
|
|
53
53
|
const handleClearAll = () => {
|
|
54
|
+
// Required columns are silently skipped — no feedback is provided to the user
|
|
54
55
|
columns.value.forEach((col) => {
|
|
55
56
|
if (!col.required && visibleColumns.value.has(col.columnId)) {
|
|
56
57
|
onVisibilityChange(col.columnId, false);
|
|
57
58
|
}
|
|
58
59
|
});
|
|
59
60
|
};
|
|
60
|
-
const visibleCount =
|
|
61
|
-
const totalCount =
|
|
62
|
-
watch([visibleColumns, columns], () => {
|
|
63
|
-
visibleCount.value = visibleColumns.value.size;
|
|
64
|
-
totalCount.value = columns.value.length;
|
|
65
|
-
}, { immediate: true });
|
|
61
|
+
const visibleCount = computed(() => visibleColumns.value.size);
|
|
62
|
+
const totalCount = computed(() => columns.value.length);
|
|
66
63
|
return {
|
|
67
64
|
open,
|
|
68
65
|
setOpen,
|
|
@@ -6,6 +6,8 @@ import { useDateFilterState } from './useDateFilterState';
|
|
|
6
6
|
const EMPTY_OPTIONS = [];
|
|
7
7
|
export function useColumnHeaderFilterState(params) {
|
|
8
8
|
const { filterType, onSort, } = params;
|
|
9
|
+
// Access params.selectedValues as a getter so hasActiveFilter tracks the reactive prop
|
|
10
|
+
// (when params is Vue's reactive props object, this is reactive; plain objects are snapshots)
|
|
9
11
|
const safeSelectedValues = () => params.selectedValues ?? EMPTY_OPTIONS;
|
|
10
12
|
// Shared state
|
|
11
13
|
const headerRef = ref(null);
|
|
@@ -15,30 +17,30 @@ export function useColumnHeaderFilterState(params) {
|
|
|
15
17
|
const setFilterOpen = (open) => {
|
|
16
18
|
isFilterOpen.value = open;
|
|
17
19
|
};
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
+
// Compose sub-hooks — pass the ref directly so Vue's reactivity system
|
|
21
|
+
// can properly track dependencies (instead of a getter function wrapper)
|
|
20
22
|
const textFilterState = useTextFilterState({
|
|
21
23
|
textValue: params.textValue,
|
|
22
24
|
onTextChange: params.onTextChange,
|
|
23
|
-
isFilterOpen
|
|
25
|
+
isFilterOpen,
|
|
24
26
|
});
|
|
25
27
|
const multiSelectFilterState = useMultiSelectFilterState({
|
|
26
28
|
selectedValues: params.selectedValues,
|
|
27
29
|
onFilterChange: params.onFilterChange,
|
|
28
30
|
options: params.options,
|
|
29
|
-
isFilterOpen
|
|
31
|
+
isFilterOpen,
|
|
30
32
|
});
|
|
31
33
|
const peopleFilterState = usePeopleFilterState({
|
|
32
34
|
selectedUser: params.selectedUser,
|
|
33
35
|
onUserChange: params.onUserChange,
|
|
34
36
|
peopleSearch: params.peopleSearch,
|
|
35
|
-
isFilterOpen
|
|
37
|
+
isFilterOpen,
|
|
36
38
|
filterType,
|
|
37
39
|
});
|
|
38
40
|
const dateFilterState = useDateFilterState({
|
|
39
41
|
dateValue: params.dateValue,
|
|
40
42
|
onDateChange: params.onDateChange,
|
|
41
|
-
isFilterOpen
|
|
43
|
+
isFilterOpen,
|
|
42
44
|
});
|
|
43
45
|
// Close popover resets position
|
|
44
46
|
watch(isFilterOpen, (open) => {
|
|
@@ -66,7 +68,8 @@ export function useColumnHeaderFilterState(params) {
|
|
|
66
68
|
isFilterOpen.value = false;
|
|
67
69
|
}
|
|
68
70
|
};
|
|
69
|
-
clickOutsideTimeout = setTimeout(() =>
|
|
71
|
+
clickOutsideTimeout = setTimeout(() => { if (clickOutsideHandler)
|
|
72
|
+
document.addEventListener('mousedown', clickOutsideHandler); }, 0);
|
|
70
73
|
document.addEventListener('keydown', keyDownHandler, true);
|
|
71
74
|
};
|
|
72
75
|
const cleanupListeners = () => {
|
|
@@ -34,10 +34,8 @@ export function useColumnHeaderMenuState(params) {
|
|
|
34
34
|
const col = currentColumn.value;
|
|
35
35
|
return col?.sortable !== false;
|
|
36
36
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return true;
|
|
40
|
-
});
|
|
37
|
+
// All columns are resizable by default (no per-column resizable flag in core)
|
|
38
|
+
const isResizable = ref(true);
|
|
41
39
|
const handlePinLeft = () => {
|
|
42
40
|
if (openForColumn.value && canPinLeft.value) {
|
|
43
41
|
onPinColumn(openForColumn.value, 'left');
|
|
@@ -22,8 +22,8 @@ export function useColumnPinning(params) {
|
|
|
22
22
|
onColumnPinned?.(columnId, side);
|
|
23
23
|
};
|
|
24
24
|
const unpinColumn = (columnId) => {
|
|
25
|
-
const next =
|
|
26
|
-
|
|
25
|
+
const { [columnId]: _removed, ...next } = pinnedColumns.value;
|
|
26
|
+
void _removed;
|
|
27
27
|
internalPinnedColumns.value = next;
|
|
28
28
|
onColumnPinned?.(columnId, null);
|
|
29
29
|
};
|
|
@@ -62,7 +62,14 @@ export function useColumnReorder(params) {
|
|
|
62
62
|
const tableEl = tableRef.value;
|
|
63
63
|
if (!tableEl || !draggedColumnId)
|
|
64
64
|
return;
|
|
65
|
-
const result = calculateDropTarget(
|
|
65
|
+
const result = calculateDropTarget({
|
|
66
|
+
mouseX: latestMouseX,
|
|
67
|
+
columnOrder: columnOrder.value,
|
|
68
|
+
draggedColumnId,
|
|
69
|
+
draggedPinState,
|
|
70
|
+
tableElement: tableEl,
|
|
71
|
+
pinnedColumns: pinnedColumns?.value,
|
|
72
|
+
});
|
|
66
73
|
if (result) {
|
|
67
74
|
targetIndex = result.targetIndex;
|
|
68
75
|
dropIndicatorX.value = result.indicatorX;
|