@alaarab/ogrid-vue 2.0.15 → 2.0.17
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 +84 -53
- package/dist/esm/composables/useCellSelection.js +9 -24
- package/dist/esm/composables/useClipboard.js +5 -21
- package/dist/esm/composables/useDataGridState.js +36 -1
- package/dist/esm/composables/useKeyboardNavigation.js +6 -49
- package/dist/esm/composables/useUndoRedo.js +16 -26
- package/dist/types/composables/useDataGridState.d.ts +4 -0
- package/package.json +2 -2
|
@@ -29,6 +29,60 @@ 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 per-column layout metadata so it's only recalculated when
|
|
33
|
+
// column config, sizing, pinning, or measured widths change — not on every
|
|
34
|
+
// render (parity with React's columnMeta useMemo).
|
|
35
|
+
const columnMetaCache = computed(() => {
|
|
36
|
+
const layout = state.layout.value;
|
|
37
|
+
const pinning = state.pinning.value;
|
|
38
|
+
const { visibleCols, columnSizingOverrides, measuredColumnWidths } = layout;
|
|
39
|
+
const { leftOffsets, rightOffsets } = pinning;
|
|
40
|
+
const freezeCols = propsRef.value.freezeCols;
|
|
41
|
+
const cellStyles = {};
|
|
42
|
+
const cellClasses = {};
|
|
43
|
+
const hdrStyles = {};
|
|
44
|
+
const hdrClasses = {};
|
|
45
|
+
for (let colIdx = 0; colIdx < visibleCols.length; colIdx++) {
|
|
46
|
+
const col = visibleCols[colIdx];
|
|
47
|
+
const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
|
|
48
|
+
const isPinnedLeft = col.pinned === 'left';
|
|
49
|
+
const isPinnedRight = col.pinned === 'right';
|
|
50
|
+
const columnWidth = getColumnWidth(col);
|
|
51
|
+
const hasResizeOverride = !!columnSizingOverrides[col.columnId];
|
|
52
|
+
const measuredW = measuredColumnWidths[col.columnId];
|
|
53
|
+
const baseMinWidth = col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
|
|
54
|
+
const effectiveMinWidth = hasResizeOverride ? columnWidth : Math.max(baseMinWidth, measuredW ?? 0);
|
|
55
|
+
const tdStyle = {
|
|
56
|
+
minWidth: `${effectiveMinWidth}px`,
|
|
57
|
+
width: `${columnWidth}px`,
|
|
58
|
+
maxWidth: `${columnWidth}px`,
|
|
59
|
+
};
|
|
60
|
+
const hdrStyle = {
|
|
61
|
+
minWidth: `${effectiveMinWidth}px`,
|
|
62
|
+
width: `${columnWidth}px`,
|
|
63
|
+
maxWidth: `${columnWidth}px`,
|
|
64
|
+
};
|
|
65
|
+
const tdClassParts = ['ogrid-data-cell'];
|
|
66
|
+
const hdrClassParts = ['ogrid-header-cell'];
|
|
67
|
+
if (isPinnedLeft || (isFreezeCol && colIdx === 0)) {
|
|
68
|
+
tdClassParts.push('ogrid-data-cell--pinned-left');
|
|
69
|
+
tdStyle.left = `${leftOffsets[col.columnId] ?? 0}px`;
|
|
70
|
+
hdrClassParts.push('ogrid-header-cell--pinned-left');
|
|
71
|
+
hdrStyle.left = `${leftOffsets[col.columnId] ?? 0}px`;
|
|
72
|
+
}
|
|
73
|
+
else if (isPinnedRight) {
|
|
74
|
+
tdClassParts.push('ogrid-data-cell--pinned-right');
|
|
75
|
+
tdStyle.right = `${rightOffsets[col.columnId] ?? 0}px`;
|
|
76
|
+
hdrClassParts.push('ogrid-header-cell--pinned-right');
|
|
77
|
+
hdrStyle.right = `${rightOffsets[col.columnId] ?? 0}px`;
|
|
78
|
+
}
|
|
79
|
+
cellStyles[col.columnId] = tdStyle;
|
|
80
|
+
cellClasses[col.columnId] = tdClassParts.join(' ');
|
|
81
|
+
hdrStyles[col.columnId] = hdrStyle;
|
|
82
|
+
hdrClasses[col.columnId] = hdrClassParts.join(' ');
|
|
83
|
+
}
|
|
84
|
+
return { cellStyles, cellClasses, hdrStyles, hdrClasses };
|
|
85
|
+
});
|
|
32
86
|
return () => {
|
|
33
87
|
const p = props.gridProps;
|
|
34
88
|
const layout = state.layout.value;
|
|
@@ -45,7 +99,7 @@ export function createDataGridTable(ui) {
|
|
|
45
99
|
const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * pageSize : 0;
|
|
46
100
|
const { selectedRowIds, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
|
|
47
101
|
const { editingCell: _editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
|
|
48
|
-
const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange: _cutRange, copyRange: _copyRange, canUndo, canRedo, onUndo, onRedo, isDragging: _isDragging, } = interaction;
|
|
102
|
+
const { setActiveCell, setSelectionRange, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange: _cutRange, copyRange: _copyRange, canUndo, canRedo, onUndo, onRedo, isDragging: _isDragging, } = interaction;
|
|
49
103
|
const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
|
|
50
104
|
const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError: _onCellError } = viewModels;
|
|
51
105
|
const items = p.items;
|
|
@@ -78,7 +132,7 @@ export function createDataGridTable(ui) {
|
|
|
78
132
|
const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInput);
|
|
79
133
|
if (descriptor.mode === 'editing-inline') {
|
|
80
134
|
const editorProps = buildInlineEditorProps(item, col, descriptor, editCallbacks);
|
|
81
|
-
return h(ui.InlineCellEditor, {
|
|
135
|
+
return h('div', { class: 'ogrid-editing-cell' }, h(ui.InlineCellEditor, {
|
|
82
136
|
value: editorProps.value,
|
|
83
137
|
item: editorProps.item,
|
|
84
138
|
column: editorProps.column,
|
|
@@ -86,7 +140,7 @@ export function createDataGridTable(ui) {
|
|
|
86
140
|
editorType: editorProps.editorType,
|
|
87
141
|
onCommit: editorProps.onCommit,
|
|
88
142
|
onCancel: editorProps.onCancel,
|
|
89
|
-
});
|
|
143
|
+
}));
|
|
90
144
|
}
|
|
91
145
|
if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
|
|
92
146
|
const editorProps = buildPopoverEditorProps(item, col, descriptor, pendingEditorValue, editCallbacks);
|
|
@@ -137,53 +191,22 @@ export function createDataGridTable(ui) {
|
|
|
137
191
|
] : []),
|
|
138
192
|
]);
|
|
139
193
|
};
|
|
140
|
-
//
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (isPinnedLeft || (isFreezeCol && colIdx === 0)) {
|
|
156
|
-
tdClasses.push('ogrid-data-cell--pinned-left');
|
|
157
|
-
tdDynamicStyle.left = `${leftOffsets[col.columnId] ?? 0}px`;
|
|
158
|
-
}
|
|
159
|
-
else if (isPinnedRight) {
|
|
160
|
-
tdClasses.push('ogrid-data-cell--pinned-right');
|
|
161
|
-
tdDynamicStyle.right = `${rightOffsets[col.columnId] ?? 0}px`;
|
|
162
|
-
}
|
|
163
|
-
return { col, tdClasses: tdClasses.join(' '), tdDynamicStyle };
|
|
164
|
-
});
|
|
165
|
-
// Build header cell classes + dynamic styles
|
|
166
|
-
const getHeaderClassAndStyle = (col, colIdx) => {
|
|
167
|
-
const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
|
|
168
|
-
const isPinnedLeft = col.pinned === 'left';
|
|
169
|
-
const isPinnedRight = col.pinned === 'right';
|
|
170
|
-
const columnWidth = getColumnWidth(col);
|
|
171
|
-
const classes = ['ogrid-header-cell'];
|
|
172
|
-
const style = {
|
|
173
|
-
cursor: isReorderDragging.value ? 'grabbing' : 'grab',
|
|
174
|
-
minWidth: `${col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH}px`,
|
|
175
|
-
width: `${columnWidth}px`,
|
|
176
|
-
maxWidth: `${columnWidth}px`,
|
|
194
|
+
// Use the pre-computed column metadata cache (computed in setup() for memoization)
|
|
195
|
+
const { cellStyles: colCellStyles, cellClasses: colCellClasses, hdrStyles: colHdrStyles, hdrClasses: colHdrClasses } = columnMetaCache.value;
|
|
196
|
+
// Build column layouts using cached metadata
|
|
197
|
+
const columnLayouts = visibleCols.map((col) => ({
|
|
198
|
+
col,
|
|
199
|
+
tdClasses: colCellClasses[col.columnId] || 'ogrid-data-cell',
|
|
200
|
+
tdDynamicStyle: colCellStyles[col.columnId] || {},
|
|
201
|
+
}));
|
|
202
|
+
// Header class+style lookup using cached metadata
|
|
203
|
+
const getHeaderClassAndStyle = (col) => {
|
|
204
|
+
const base = colHdrStyles[col.columnId] || {};
|
|
205
|
+
// cursor depends on drag state — add it at render time (not cached)
|
|
206
|
+
return {
|
|
207
|
+
classes: colHdrClasses[col.columnId] || 'ogrid-header-cell',
|
|
208
|
+
style: { ...base, cursor: isReorderDragging.value ? 'grabbing' : 'grab' },
|
|
177
209
|
};
|
|
178
|
-
if (isPinnedLeft || (isFreezeCol && colIdx === 0)) {
|
|
179
|
-
classes.push('ogrid-header-cell--pinned-left');
|
|
180
|
-
style.left = `${leftOffsets[col.columnId] ?? 0}px`;
|
|
181
|
-
}
|
|
182
|
-
else if (isPinnedRight) {
|
|
183
|
-
classes.push('ogrid-header-cell--pinned-right');
|
|
184
|
-
style.right = `${rightOffsets[col.columnId] ?? 0}px`;
|
|
185
|
-
}
|
|
186
|
-
return { classes: classes.join(' '), style };
|
|
187
210
|
};
|
|
188
211
|
// Dynamic wrapper style
|
|
189
212
|
const wrapperStyle = {
|
|
@@ -245,7 +268,8 @@ export function createDataGridTable(ui) {
|
|
|
245
268
|
},
|
|
246
269
|
}, ui.renderCheckbox({
|
|
247
270
|
modelValue: allSelected,
|
|
248
|
-
|
|
271
|
+
// Indeterminate only when some (but not all) rows are selected
|
|
272
|
+
indeterminate: someSelected && !allSelected,
|
|
249
273
|
ariaLabel: 'Select all rows',
|
|
250
274
|
onChange: (c) => handleSelectAll(!!c),
|
|
251
275
|
})),
|
|
@@ -296,8 +320,7 @@ export function createDataGridTable(ui) {
|
|
|
296
320
|
}, cell.label);
|
|
297
321
|
}
|
|
298
322
|
const col = cell.columnDef;
|
|
299
|
-
const
|
|
300
|
-
const { classes: headerClasses, style: headerStyle } = getHeaderClassAndStyle(col, colIdx);
|
|
323
|
+
const { classes: headerClasses, style: headerStyle } = getHeaderClassAndStyle(col);
|
|
301
324
|
return h('th', {
|
|
302
325
|
key: col.columnId,
|
|
303
326
|
scope: 'col',
|
|
@@ -320,7 +343,15 @@ export function createDataGridTable(ui) {
|
|
|
320
343
|
}, '\u22EE'),
|
|
321
344
|
]),
|
|
322
345
|
h('div', {
|
|
323
|
-
onMousedown: (e) => {
|
|
346
|
+
onMousedown: (e) => {
|
|
347
|
+
// Clear cell selection/focus before resize so outlines
|
|
348
|
+
// and focus rings don't persist during drag (parity with React).
|
|
349
|
+
setActiveCell(null);
|
|
350
|
+
setSelectionRange(null);
|
|
351
|
+
wrapperRef.value?.focus({ preventScroll: true });
|
|
352
|
+
e.stopPropagation();
|
|
353
|
+
handleResizeStart(e, col);
|
|
354
|
+
},
|
|
324
355
|
class: 'ogrid-resize-handle',
|
|
325
356
|
}),
|
|
326
357
|
]);
|
|
@@ -1,27 +1,12 @@
|
|
|
1
1
|
import { shallowRef, ref, computed, onMounted, onUnmounted } from 'vue';
|
|
2
|
-
import { normalizeSelectionRange } from '
|
|
2
|
+
import { normalizeSelectionRange, rangesEqual, computeAutoScrollSpeed } from '@alaarab/ogrid-core';
|
|
3
3
|
import { useLatestRef } from './useLatestRef';
|
|
4
|
-
/** Compares two selection ranges by value. */
|
|
5
|
-
function rangesEqual(a, b) {
|
|
6
|
-
if (a === b)
|
|
7
|
-
return true;
|
|
8
|
-
if (!a || !b)
|
|
9
|
-
return false;
|
|
10
|
-
return a.startRow === b.startRow && a.endRow === b.endRow &&
|
|
11
|
-
a.startCol === b.startCol && a.endCol === b.endCol;
|
|
12
|
-
}
|
|
13
4
|
/** DOM attribute names used for drag-range highlighting (bypasses Vue). */
|
|
14
5
|
const DRAG_ATTR = 'data-drag-range';
|
|
15
6
|
const DRAG_ANCHOR_ATTR = 'data-drag-anchor';
|
|
16
7
|
/** Auto-scroll config */
|
|
17
8
|
const AUTO_SCROLL_EDGE = 40;
|
|
18
|
-
const AUTO_SCROLL_MIN_SPEED = 2;
|
|
19
|
-
const AUTO_SCROLL_MAX_SPEED = 20;
|
|
20
9
|
const AUTO_SCROLL_INTERVAL = 16;
|
|
21
|
-
function autoScrollSpeed(distance) {
|
|
22
|
-
const t = Math.min(distance / AUTO_SCROLL_EDGE, 1);
|
|
23
|
-
return AUTO_SCROLL_MIN_SPEED + t * (AUTO_SCROLL_MAX_SPEED - AUTO_SCROLL_MIN_SPEED);
|
|
24
|
-
}
|
|
25
10
|
/**
|
|
26
11
|
* Manages cell selection range with drag-to-select and select-all support.
|
|
27
12
|
*/
|
|
@@ -191,16 +176,16 @@ export function useCellSelection(params) {
|
|
|
191
176
|
let dx = 0;
|
|
192
177
|
let dy = 0;
|
|
193
178
|
if (lastMousePos.cy < rect.top + AUTO_SCROLL_EDGE) {
|
|
194
|
-
dy = -
|
|
179
|
+
dy = -computeAutoScrollSpeed(rect.top + AUTO_SCROLL_EDGE - lastMousePos.cy);
|
|
195
180
|
}
|
|
196
181
|
else if (lastMousePos.cy > rect.bottom - AUTO_SCROLL_EDGE) {
|
|
197
|
-
dy =
|
|
182
|
+
dy = computeAutoScrollSpeed(lastMousePos.cy - (rect.bottom - AUTO_SCROLL_EDGE));
|
|
198
183
|
}
|
|
199
184
|
if (lastMousePos.cx < rect.left + AUTO_SCROLL_EDGE) {
|
|
200
|
-
dx = -
|
|
185
|
+
dx = -computeAutoScrollSpeed(rect.left + AUTO_SCROLL_EDGE - lastMousePos.cx);
|
|
201
186
|
}
|
|
202
187
|
else if (lastMousePos.cx > rect.right - AUTO_SCROLL_EDGE) {
|
|
203
|
-
dx =
|
|
188
|
+
dx = computeAutoScrollSpeed(lastMousePos.cx - (rect.right - AUTO_SCROLL_EDGE));
|
|
204
189
|
}
|
|
205
190
|
if (dx === 0 && dy === 0) {
|
|
206
191
|
stopAutoScroll();
|
|
@@ -218,13 +203,13 @@ export function useCellSelection(params) {
|
|
|
218
203
|
let sdx = 0;
|
|
219
204
|
let sdy = 0;
|
|
220
205
|
if (p.cy < r.top + AUTO_SCROLL_EDGE)
|
|
221
|
-
sdy = -
|
|
206
|
+
sdy = -computeAutoScrollSpeed(r.top + AUTO_SCROLL_EDGE - p.cy);
|
|
222
207
|
else if (p.cy > r.bottom - AUTO_SCROLL_EDGE)
|
|
223
|
-
sdy =
|
|
208
|
+
sdy = computeAutoScrollSpeed(p.cy - (r.bottom - AUTO_SCROLL_EDGE));
|
|
224
209
|
if (p.cx < r.left + AUTO_SCROLL_EDGE)
|
|
225
|
-
sdx = -
|
|
210
|
+
sdx = -computeAutoScrollSpeed(r.left + AUTO_SCROLL_EDGE - p.cx);
|
|
226
211
|
else if (p.cx > r.right - AUTO_SCROLL_EDGE)
|
|
227
|
-
sdx =
|
|
212
|
+
sdx = computeAutoScrollSpeed(p.cx - (r.right - AUTO_SCROLL_EDGE));
|
|
228
213
|
if (sdx === 0 && sdy === 0) {
|
|
229
214
|
stopAutoScroll();
|
|
230
215
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ref, shallowRef } from 'vue';
|
|
2
|
-
import { getCellValue, parseValue, normalizeSelectionRange } from '@alaarab/ogrid-core';
|
|
2
|
+
import { getCellValue, parseValue, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard } from '@alaarab/ogrid-core';
|
|
3
3
|
/**
|
|
4
4
|
* Manages copy, cut, and paste operations for cell ranges with TSV clipboard format.
|
|
5
5
|
*/
|
|
@@ -20,23 +20,7 @@ export function useClipboard(params) {
|
|
|
20
20
|
if (range == null)
|
|
21
21
|
return;
|
|
22
22
|
const norm = normalizeSelectionRange(range);
|
|
23
|
-
const
|
|
24
|
-
const currentCols = visibleCols.value;
|
|
25
|
-
const rows = [];
|
|
26
|
-
for (let r = norm.startRow; r <= norm.endRow; r++) {
|
|
27
|
-
const cells = [];
|
|
28
|
-
for (let c = norm.startCol; c <= norm.endCol; c++) {
|
|
29
|
-
if (r >= currentItems.length || c >= currentCols.length)
|
|
30
|
-
break;
|
|
31
|
-
const item = currentItems[r];
|
|
32
|
-
const col = currentCols[c];
|
|
33
|
-
const raw = getCellValue(item, col);
|
|
34
|
-
const val = col.valueFormatter ? col.valueFormatter(raw, item) : raw;
|
|
35
|
-
cells.push(val != null && val !== '' ? String(val).replace(/\t/g, ' ').replace(/\n/g, ' ') : '');
|
|
36
|
-
}
|
|
37
|
-
rows.push(cells.join('\t'));
|
|
38
|
-
}
|
|
39
|
-
const tsv = rows.join('\r\n');
|
|
23
|
+
const tsv = formatSelectionAsTsv(items.value, visibleCols.value, norm);
|
|
40
24
|
internalClipboardRef.value = tsv;
|
|
41
25
|
copyRange.value = norm;
|
|
42
26
|
void navigator.clipboard.writeText(tsv).catch(() => { });
|
|
@@ -76,10 +60,10 @@ export function useClipboard(params) {
|
|
|
76
60
|
const anchorCol = norm ? norm.startCol : 0;
|
|
77
61
|
const currentItems = items.value;
|
|
78
62
|
const currentCols = visibleCols.value;
|
|
79
|
-
const
|
|
63
|
+
const parsedRows = parseTsvClipboard(text);
|
|
80
64
|
beginBatch?.();
|
|
81
|
-
for (let r = 0; r <
|
|
82
|
-
const cells =
|
|
65
|
+
for (let r = 0; r < parsedRows.length; r++) {
|
|
66
|
+
const cells = parsedRows[r];
|
|
83
67
|
for (let c = 0; c < cells.length; c++) {
|
|
84
68
|
const targetRow = anchorRow + r;
|
|
85
69
|
const targetCol = anchorCol + c;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref, computed } from 'vue';
|
|
1
|
+
import { ref, computed, watch, nextTick } from 'vue';
|
|
2
2
|
import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregations, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH } from '@alaarab/ogrid-core';
|
|
3
3
|
import { useRowSelection } from './useRowSelection';
|
|
4
4
|
import { useCellEditing } from './useCellEditing';
|
|
@@ -191,6 +191,40 @@ export function useDataGridState(params) {
|
|
|
191
191
|
sortBy: computed(() => props.value.sortBy),
|
|
192
192
|
sortDirection: computed(() => props.value.sortDirection),
|
|
193
193
|
});
|
|
194
|
+
// Measure actual column widths from the DOM after layout changes.
|
|
195
|
+
// Used as a minWidth floor to prevent columns from shrinking when new data
|
|
196
|
+
// loads (e.g. during server-side pagination transitions).
|
|
197
|
+
const measuredColumnWidths = ref({});
|
|
198
|
+
watch([visibleCols, containerWidth, columnSizingOverrides], () => {
|
|
199
|
+
void nextTick(() => {
|
|
200
|
+
const wrapper = wrapperRef.value;
|
|
201
|
+
if (!wrapper)
|
|
202
|
+
return;
|
|
203
|
+
const headerCells = wrapper.querySelectorAll('th[data-column-id]');
|
|
204
|
+
if (headerCells.length === 0)
|
|
205
|
+
return;
|
|
206
|
+
const measured = {};
|
|
207
|
+
headerCells.forEach((cell) => {
|
|
208
|
+
const colId = cell.getAttribute('data-column-id');
|
|
209
|
+
if (colId)
|
|
210
|
+
measured[colId] = cell.offsetWidth;
|
|
211
|
+
});
|
|
212
|
+
// Only update if widths actually changed to avoid reactive loops
|
|
213
|
+
const prev = measuredColumnWidths.value;
|
|
214
|
+
const keys = Object.keys(measured);
|
|
215
|
+
let changed = keys.length !== Object.keys(prev).length;
|
|
216
|
+
if (!changed) {
|
|
217
|
+
for (const key of keys) {
|
|
218
|
+
if (prev[key] !== measured[key]) {
|
|
219
|
+
changed = true;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (changed)
|
|
225
|
+
measuredColumnWidths.value = measured;
|
|
226
|
+
});
|
|
227
|
+
}, { flush: 'post' });
|
|
194
228
|
// Build column width map for pinning offset computation
|
|
195
229
|
const columnWidthMap = computed(() => {
|
|
196
230
|
const map = {};
|
|
@@ -289,6 +323,7 @@ export function useDataGridState(params) {
|
|
|
289
323
|
columnSizingOverrides: columnSizingOverrides.value,
|
|
290
324
|
setColumnSizingOverrides,
|
|
291
325
|
onColumnResized: onColumnResizedProp.value,
|
|
326
|
+
measuredColumnWidths: measuredColumnWidths.value,
|
|
292
327
|
}));
|
|
293
328
|
const rowSelectionState = computed(() => ({
|
|
294
329
|
selectedRowIds: rowSelectionResult.selectedRowIds.value,
|
|
@@ -1,30 +1,6 @@
|
|
|
1
1
|
import { computed } from 'vue';
|
|
2
|
-
import { normalizeSelectionRange, getCellValue, parseValue } from '@alaarab/ogrid-core';
|
|
2
|
+
import { normalizeSelectionRange, getCellValue, parseValue, findCtrlArrowTarget, computeTabNavigation } from '@alaarab/ogrid-core';
|
|
3
3
|
import { useLatestRef } from './useLatestRef';
|
|
4
|
-
/**
|
|
5
|
-
* Excel-style Ctrl+Arrow: find the target position along a 1D axis.
|
|
6
|
-
*/
|
|
7
|
-
function findCtrlTarget(pos, edge, step, isEmpty) {
|
|
8
|
-
if (pos === edge)
|
|
9
|
-
return pos;
|
|
10
|
-
const next = pos + step;
|
|
11
|
-
if (!isEmpty(pos) && !isEmpty(next)) {
|
|
12
|
-
let p = next;
|
|
13
|
-
while (p !== edge) {
|
|
14
|
-
if (isEmpty(p + step))
|
|
15
|
-
return p;
|
|
16
|
-
p += step;
|
|
17
|
-
}
|
|
18
|
-
return edge;
|
|
19
|
-
}
|
|
20
|
-
let p = next;
|
|
21
|
-
while (p !== edge) {
|
|
22
|
-
if (!isEmpty(p))
|
|
23
|
-
return p;
|
|
24
|
-
p += step;
|
|
25
|
-
}
|
|
26
|
-
return edge;
|
|
27
|
-
}
|
|
28
4
|
/**
|
|
29
5
|
* Handles all keyboard navigation, shortcuts, and cell editing triggers for the grid.
|
|
30
6
|
*/
|
|
@@ -97,7 +73,7 @@ export function useKeyboardNavigation(params) {
|
|
|
97
73
|
e.preventDefault();
|
|
98
74
|
const ctrl = e.ctrlKey || e.metaKey;
|
|
99
75
|
const newRow = ctrl
|
|
100
|
-
?
|
|
76
|
+
? findCtrlArrowTarget(rowIndex, maxRowIndex, 1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
|
|
101
77
|
: Math.min(rowIndex + 1, maxRowIndex);
|
|
102
78
|
if (shift) {
|
|
103
79
|
setSelectionRange(normalizeSelectionRange({
|
|
@@ -118,7 +94,7 @@ export function useKeyboardNavigation(params) {
|
|
|
118
94
|
e.preventDefault();
|
|
119
95
|
const ctrl = e.ctrlKey || e.metaKey;
|
|
120
96
|
const newRowUp = ctrl
|
|
121
|
-
?
|
|
97
|
+
? findCtrlArrowTarget(rowIndex, 0, -1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
|
|
122
98
|
: Math.max(rowIndex - 1, 0);
|
|
123
99
|
if (shift) {
|
|
124
100
|
setSelectionRange(normalizeSelectionRange({
|
|
@@ -140,7 +116,7 @@ export function useKeyboardNavigation(params) {
|
|
|
140
116
|
const ctrl = e.ctrlKey || e.metaKey;
|
|
141
117
|
let newCol;
|
|
142
118
|
if (ctrl && dataColIndex >= 0) {
|
|
143
|
-
newCol =
|
|
119
|
+
newCol = findCtrlArrowTarget(dataColIndex, visibleCols.length - 1, 1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
|
|
144
120
|
}
|
|
145
121
|
else {
|
|
146
122
|
newCol = Math.min(columnIndex + 1, maxColIndex);
|
|
@@ -165,7 +141,7 @@ export function useKeyboardNavigation(params) {
|
|
|
165
141
|
const ctrl = e.ctrlKey || e.metaKey;
|
|
166
142
|
let newColLeft;
|
|
167
143
|
if (ctrl && dataColIndex >= 0) {
|
|
168
|
-
newColLeft =
|
|
144
|
+
newColLeft = findCtrlArrowTarget(dataColIndex, 0, -1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
|
|
169
145
|
}
|
|
170
146
|
else {
|
|
171
147
|
newColLeft = Math.max(columnIndex - 1, colOffset);
|
|
@@ -187,26 +163,7 @@ export function useKeyboardNavigation(params) {
|
|
|
187
163
|
}
|
|
188
164
|
case 'Tab': {
|
|
189
165
|
e.preventDefault();
|
|
190
|
-
|
|
191
|
-
let newColTab = columnIndex;
|
|
192
|
-
if (e.shiftKey) {
|
|
193
|
-
if (columnIndex > colOffset) {
|
|
194
|
-
newColTab = columnIndex - 1;
|
|
195
|
-
}
|
|
196
|
-
else if (rowIndex > 0) {
|
|
197
|
-
newRowTab = rowIndex - 1;
|
|
198
|
-
newColTab = maxColIndex;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
if (columnIndex < maxColIndex) {
|
|
203
|
-
newColTab = columnIndex + 1;
|
|
204
|
-
}
|
|
205
|
-
else if (rowIndex < maxRowIndex) {
|
|
206
|
-
newRowTab = rowIndex + 1;
|
|
207
|
-
newColTab = colOffset;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
166
|
+
const { rowIndex: newRowTab, columnIndex: newColTab } = computeTabNavigation(rowIndex, columnIndex, maxRowIndex, maxColIndex, colOffset, e.shiftKey);
|
|
210
167
|
const newDataColTab = newColTab - colOffset;
|
|
211
168
|
setSelectionRange({ startRow: newRowTab, startCol: newDataColTab, endRow: newRowTab, endCol: newDataColTab });
|
|
212
169
|
setActiveCell({ rowIndex: newRowTab, columnIndex: newColTab });
|
|
@@ -1,50 +1,40 @@
|
|
|
1
1
|
import { ref } from 'vue';
|
|
2
|
+
import { UndoRedoStack } from '@alaarab/ogrid-core';
|
|
2
3
|
/**
|
|
3
4
|
* Wraps onCellValueChanged with an undo/redo history stack.
|
|
4
5
|
* Supports batch operations: changes between beginBatch/endBatch are one undo step.
|
|
5
6
|
*/
|
|
6
7
|
export function useUndoRedo(params) {
|
|
7
8
|
const { onCellValueChanged, maxUndoDepth = 100 } = params;
|
|
8
|
-
|
|
9
|
-
let redoStack = [];
|
|
10
|
-
let batch = null;
|
|
9
|
+
const stack = new UndoRedoStack(maxUndoDepth);
|
|
11
10
|
const canUndo = ref(false);
|
|
12
11
|
const canRedo = ref(false);
|
|
13
12
|
const updateFlags = () => {
|
|
14
|
-
canUndo.value =
|
|
15
|
-
canRedo.value =
|
|
13
|
+
canUndo.value = stack.canUndo;
|
|
14
|
+
canRedo.value = stack.canRedo;
|
|
16
15
|
};
|
|
17
16
|
const wrapped = onCellValueChanged
|
|
18
17
|
? (event) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
history = [...history, [event]].slice(-maxUndoDepth);
|
|
24
|
-
redoStack = [];
|
|
18
|
+
stack.record(event);
|
|
19
|
+
if (!stack.isBatching) {
|
|
25
20
|
updateFlags();
|
|
26
21
|
}
|
|
27
22
|
onCellValueChanged(event);
|
|
28
23
|
}
|
|
29
24
|
: undefined;
|
|
30
25
|
const beginBatch = () => {
|
|
31
|
-
|
|
26
|
+
stack.beginBatch();
|
|
32
27
|
};
|
|
33
28
|
const endBatch = () => {
|
|
34
|
-
|
|
35
|
-
batch = null;
|
|
36
|
-
if (!b || b.length === 0)
|
|
37
|
-
return;
|
|
38
|
-
history = [...history, b].slice(-maxUndoDepth);
|
|
39
|
-
redoStack = [];
|
|
29
|
+
stack.endBatch();
|
|
40
30
|
updateFlags();
|
|
41
31
|
};
|
|
42
32
|
const undo = () => {
|
|
43
|
-
if (!onCellValueChanged
|
|
33
|
+
if (!onCellValueChanged)
|
|
34
|
+
return;
|
|
35
|
+
const lastBatch = stack.undo();
|
|
36
|
+
if (!lastBatch)
|
|
44
37
|
return;
|
|
45
|
-
const lastBatch = history[history.length - 1];
|
|
46
|
-
history = history.slice(0, -1);
|
|
47
|
-
redoStack = [...redoStack, lastBatch];
|
|
48
38
|
updateFlags();
|
|
49
39
|
for (let i = lastBatch.length - 1; i >= 0; i--) {
|
|
50
40
|
const ev = lastBatch[i];
|
|
@@ -52,11 +42,11 @@ export function useUndoRedo(params) {
|
|
|
52
42
|
}
|
|
53
43
|
};
|
|
54
44
|
const redo = () => {
|
|
55
|
-
if (!onCellValueChanged
|
|
45
|
+
if (!onCellValueChanged)
|
|
46
|
+
return;
|
|
47
|
+
const nextBatch = stack.redo();
|
|
48
|
+
if (!nextBatch)
|
|
56
49
|
return;
|
|
57
|
-
const nextBatch = redoStack[redoStack.length - 1];
|
|
58
|
-
redoStack = redoStack.slice(0, -1);
|
|
59
|
-
history = [...history, nextBatch];
|
|
60
50
|
updateFlags();
|
|
61
51
|
for (const ev of nextBatch) {
|
|
62
52
|
onCellValueChanged(ev);
|
|
@@ -24,6 +24,10 @@ export interface DataGridLayoutState<T> {
|
|
|
24
24
|
widthPx: number;
|
|
25
25
|
}>) => void;
|
|
26
26
|
onColumnResized?: (columnId: string, width: number) => void;
|
|
27
|
+
/** DOM-measured column widths from the previous layout pass.
|
|
28
|
+
* UI packages use these as a minWidth floor to prevent columns from
|
|
29
|
+
* shrinking when new data loads (e.g. during server-side pagination). */
|
|
30
|
+
measuredColumnWidths: Record<string, number>;
|
|
27
31
|
}
|
|
28
32
|
export interface DataGridRowSelectionState {
|
|
29
33
|
selectedRowIds: Set<RowId>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-vue",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.17",
|
|
4
4
|
"description": "OGrid Vue – Vue 3 composables, headless components, and utilities for OGrid data grids.",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"node": ">=18"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@alaarab/ogrid-core": "2.0.
|
|
38
|
+
"@alaarab/ogrid-core": "2.0.17"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"vue": "^3.3.0"
|