@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.
@@ -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
- // Pre-computed pinning offsets
141
- const leftOffsets = pinning.leftOffsets;
142
- const rightOffsets = pinning.rightOffsets;
143
- // Build column layouts
144
- const columnLayouts = visibleCols.map((col, colIdx) => {
145
- const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
146
- const isPinnedLeft = col.pinned === 'left';
147
- const isPinnedRight = col.pinned === 'right';
148
- const columnWidth = getColumnWidth(col);
149
- const tdClasses = ['ogrid-data-cell'];
150
- const tdDynamicStyle = {
151
- minWidth: `${col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH}px`,
152
- width: `${columnWidth}px`,
153
- maxWidth: `${columnWidth}px`,
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
- indeterminate: someSelected,
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 colIdx = visibleCols.indexOf(col);
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) => { e.stopPropagation(); handleResizeStart(e, col); },
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 '../types';
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 = -autoScrollSpeed(rect.top + AUTO_SCROLL_EDGE - lastMousePos.cy);
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 = autoScrollSpeed(lastMousePos.cy - (rect.bottom - AUTO_SCROLL_EDGE));
182
+ dy = computeAutoScrollSpeed(lastMousePos.cy - (rect.bottom - AUTO_SCROLL_EDGE));
198
183
  }
199
184
  if (lastMousePos.cx < rect.left + AUTO_SCROLL_EDGE) {
200
- dx = -autoScrollSpeed(rect.left + AUTO_SCROLL_EDGE - lastMousePos.cx);
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 = autoScrollSpeed(lastMousePos.cx - (rect.right - AUTO_SCROLL_EDGE));
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 = -autoScrollSpeed(r.top + AUTO_SCROLL_EDGE - p.cy);
206
+ sdy = -computeAutoScrollSpeed(r.top + AUTO_SCROLL_EDGE - p.cy);
222
207
  else if (p.cy > r.bottom - AUTO_SCROLL_EDGE)
223
- sdy = autoScrollSpeed(p.cy - (r.bottom - AUTO_SCROLL_EDGE));
208
+ sdy = computeAutoScrollSpeed(p.cy - (r.bottom - AUTO_SCROLL_EDGE));
224
209
  if (p.cx < r.left + AUTO_SCROLL_EDGE)
225
- sdx = -autoScrollSpeed(r.left + AUTO_SCROLL_EDGE - p.cx);
210
+ sdx = -computeAutoScrollSpeed(r.left + AUTO_SCROLL_EDGE - p.cx);
226
211
  else if (p.cx > r.right - AUTO_SCROLL_EDGE)
227
- sdx = autoScrollSpeed(p.cx - (r.right - AUTO_SCROLL_EDGE));
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 currentItems = items.value;
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 lines = text.split(/\r?\n/).filter((l) => l.length > 0);
63
+ const parsedRows = parseTsvClipboard(text);
80
64
  beginBatch?.();
81
- for (let r = 0; r < lines.length; r++) {
82
- const cells = lines[r].split('\t');
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
- ? findCtrlTarget(rowIndex, maxRowIndex, 1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
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
- ? findCtrlTarget(rowIndex, 0, -1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
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 = findCtrlTarget(dataColIndex, visibleCols.length - 1, 1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
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 = findCtrlTarget(dataColIndex, 0, -1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
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
- let newRowTab = rowIndex;
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
- let history = [];
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 = history.length > 0;
15
- canRedo.value = redoStack.length > 0;
13
+ canUndo.value = stack.canUndo;
14
+ canRedo.value = stack.canRedo;
16
15
  };
17
16
  const wrapped = onCellValueChanged
18
17
  ? (event) => {
19
- if (batch !== null) {
20
- batch.push(event);
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
- batch = [];
26
+ stack.beginBatch();
32
27
  };
33
28
  const endBatch = () => {
34
- const b = batch;
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 || history.length === 0)
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 || redoStack.length === 0)
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.15",
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.15"
38
+ "@alaarab/ogrid-core": "2.0.17"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "vue": "^3.3.0"