@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.
Files changed (33) hide show
  1. package/dist/esm/components/createDataGridTable.js +5 -1
  2. package/dist/esm/components/createOGrid.js +10 -7
  3. package/dist/esm/composables/useCellSelection.js +101 -61
  4. package/dist/esm/composables/useClipboard.js +15 -55
  5. package/dist/esm/composables/useColumnChooserState.js +4 -7
  6. package/dist/esm/composables/useColumnHeaderFilterState.js +10 -7
  7. package/dist/esm/composables/useColumnHeaderMenuState.js +2 -4
  8. package/dist/esm/composables/useColumnPinning.js +2 -2
  9. package/dist/esm/composables/useColumnReorder.js +8 -1
  10. package/dist/esm/composables/useDataGridState.js +33 -30
  11. package/dist/esm/composables/useDateFilterState.js +1 -1
  12. package/dist/esm/composables/useFillHandle.js +67 -50
  13. package/dist/esm/composables/useKeyboardNavigation.js +25 -109
  14. package/dist/esm/composables/useLatestRef.js +2 -2
  15. package/dist/esm/composables/useMultiSelectFilterState.js +1 -1
  16. package/dist/esm/composables/useOGrid.js +29 -11
  17. package/dist/esm/composables/usePeopleFilterState.js +2 -2
  18. package/dist/esm/composables/useRowSelection.js +13 -16
  19. package/dist/esm/composables/useTableLayout.js +11 -11
  20. package/dist/esm/composables/useTextFilterState.js +1 -1
  21. package/dist/esm/composables/useVirtualScroll.js +20 -17
  22. package/dist/types/composables/index.d.ts +1 -0
  23. package/dist/types/composables/useCellSelection.d.ts +1 -1
  24. package/dist/types/composables/useClipboard.d.ts +1 -1
  25. package/dist/types/composables/useDateFilterState.d.ts +2 -2
  26. package/dist/types/composables/useFillHandle.d.ts +1 -1
  27. package/dist/types/composables/useKeyboardNavigation.d.ts +4 -6
  28. package/dist/types/composables/useLatestRef.d.ts +3 -1
  29. package/dist/types/composables/useMultiSelectFilterState.d.ts +1 -1
  30. package/dist/types/composables/usePeopleFilterState.d.ts +1 -1
  31. package/dist/types/composables/useTextFilterState.d.ts +2 -2
  32. package/dist/types/index.d.ts +1 -1
  33. 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 = buildHeaderRows(p.columns, p.visibleColumns);
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 currentVal = sb.filters[filterKey]?.type === 'text' ? sb.filters[filterKey].value : '';
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 selected = sb.filters[filterKey]?.type === 'multiSelect' ? sb.filters[filterKey].value.includes(opt) : false;
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 current = sb.filters[filterKey]?.type === 'multiSelect' ? sb.filters[filterKey].value : [];
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 existingValue = sb.filters[filterKey]?.type === 'date' ? sb.filters[filterKey].value : { from: undefined, to: undefined };
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 API for parent refs
243
- expose({ api: api.value });
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, computed, onMounted, onUnmounted } from 'vue';
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(computed(() => params));
16
- const { colOffset, wrapperRef, setActiveCell } = params; // These are stable, safe to destructure
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
- let isDraggingInternal = false;
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: colOffset });
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 cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
92
- for (let i = 0; i < cells.length; i++) {
93
- const el = cells[i];
94
- const r = parseInt(el.getAttribute('data-row-index'), 10);
95
- const c = parseInt(el.getAttribute('data-col-index'), 10) - colOffset;
96
- const inRange = r >= minR && r <= maxR && c >= minC && c <= maxC;
97
- if (inRange) {
98
- if (!el.hasAttribute(DRAG_ATTR))
99
- el.setAttribute(DRAG_ATTR, '');
100
- // Anchor cell gets white background instead of green
101
- const isAnchor = anchor && r === anchor.row && c === anchor.col;
102
- if (isAnchor) {
103
- if (!el.hasAttribute(DRAG_ANCHOR_ATTR))
104
- el.setAttribute(DRAG_ANCHOR_ATTR, '');
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
- else {
107
- if (el.hasAttribute(DRAG_ANCHOR_ATTR))
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 wrapper = wrapperRef.value;
134
- if (!wrapper)
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 + colOffset,
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 { getCellValue, parseValue, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard } from '@alaarab/ogrid-core';
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, colOffset, selectionRange, activeCell, editable, onCellValueChanged, beginBatch, endBatch, } = params;
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
- for (let r = 0; r < parsedRows.length; r++) {
66
- const cells = parsedRows[r];
67
- for (let c = 0; c < cells.length; c++) {
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 cut = cutRange.value;
94
- for (let r = cut.startRow; r <= cut.endRow; r++) {
95
- for (let c = cut.startCol; c <= cut.endCol; c++) {
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 = ref(visibleColumns.value.size);
61
- const totalCount = ref(columns.value.length);
62
- watch([visibleColumns, columns], () => {
63
- visibleCount.value = visibleColumns.value.size;
64
- totalCount.value = columns.value.length;
65
- }, { immediate: true });
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
- const isFilterOpenGetter = () => isFilterOpen.value;
19
- // Compose sub-hooks
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: isFilterOpenGetter,
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: isFilterOpenGetter,
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: isFilterOpenGetter,
37
+ isFilterOpen,
36
38
  filterType,
37
39
  });
38
40
  const dateFilterState = useDateFilterState({
39
41
  dateValue: params.dateValue,
40
42
  onDateChange: params.onDateChange,
41
- isFilterOpen: isFilterOpenGetter,
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(() => document.addEventListener('mousedown', clickOutsideHandler), 0);
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
- const isResizable = computed(() => {
38
- // All columns are resizable by default (no per-column resizable flag in core)
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 = { ...pinnedColumns.value };
26
- delete next[columnId];
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(latestMouseX, columnOrder.value, draggedColumnId, draggedPinState, tableEl, pinnedColumns?.value);
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;