@alaarab/ogrid-vue 2.0.9 → 2.0.12

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.
@@ -6,38 +6,8 @@
6
6
  *
7
7
  * Uses SVG rects positioned via cell data-attribute measurements.
8
8
  */
9
- import { defineComponent, ref, computed, watch, onMounted, onUnmounted, h } from 'vue';
10
- import { Z_INDEX } from '@alaarab/ogrid-core';
11
- // Inject the @keyframes rule once into <head> (deduplicates across multiple OGrid instances)
12
- function ensureKeyframes() {
13
- if (typeof document === 'undefined')
14
- return;
15
- if (document.getElementById('ogrid-marching-ants-keyframes'))
16
- return;
17
- const style = document.createElement('style');
18
- style.id = 'ogrid-marching-ants-keyframes';
19
- style.textContent =
20
- '@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}';
21
- document.head.appendChild(style);
22
- }
23
- /** Measure the bounding rect of a range within a container. */
24
- function measureRange(container, range, colOffset) {
25
- const startGlobalCol = range.startCol + colOffset;
26
- const endGlobalCol = range.endCol + colOffset;
27
- const topLeft = container.querySelector(`[data-row-index="${range.startRow}"][data-col-index="${startGlobalCol}"]`);
28
- const bottomRight = container.querySelector(`[data-row-index="${range.endRow}"][data-col-index="${endGlobalCol}"]`);
29
- if (!topLeft || !bottomRight)
30
- return null;
31
- const cRect = container.getBoundingClientRect();
32
- const tlRect = topLeft.getBoundingClientRect();
33
- const brRect = bottomRight.getBoundingClientRect();
34
- return {
35
- top: tlRect.top - cRect.top,
36
- left: tlRect.left - cRect.left,
37
- width: brRect.right - tlRect.left,
38
- height: brRect.bottom - tlRect.top,
39
- };
40
- }
9
+ import { defineComponent, ref, computed, watch, onMounted, onUnmounted, h, toValue } from 'vue';
10
+ import { Z_INDEX, measureRange, injectGlobalStyles } from '@alaarab/ogrid-core';
41
11
  export const MarchingAntsOverlay = defineComponent({
42
12
  name: 'MarchingAntsOverlay',
43
13
  props: {
@@ -51,6 +21,14 @@ export const MarchingAntsOverlay = defineComponent({
51
21
  cutRange: { type: Object, default: null },
52
22
  /** Column offset — 1 when checkbox column is present, else 0 */
53
23
  colOffset: { type: Number, required: true },
24
+ /** Items array — triggers re-measurement when data changes (e.g., sorting) */
25
+ items: { type: Array, required: true },
26
+ /** Visible columns — triggers re-measurement when columns are hidden/shown */
27
+ visibleColumns: { type: Array, default: undefined },
28
+ /** Column sizing overrides — triggers re-measurement when columns are resized */
29
+ columnSizingOverrides: { type: Object, required: true },
30
+ /** Column order — triggers re-measurement when columns are reordered */
31
+ columnOrder: { type: Array, default: undefined },
54
32
  },
55
33
  setup(props) {
56
34
  const selRect = ref(null);
@@ -59,7 +37,9 @@ export const MarchingAntsOverlay = defineComponent({
59
37
  let ro;
60
38
  const clipRange = computed(() => props.copyRange ?? props.cutRange);
61
39
  const measureAll = () => {
62
- const container = props.containerRef.value;
40
+ // Use toValue to handle both Ref<HTMLElement> and raw HTMLElement
41
+ // (Vue templates may auto-unwrap refs passed as props)
42
+ const container = toValue(props.containerRef);
63
43
  if (!container) {
64
44
  selRect.value = null;
65
45
  clipRect.value = null;
@@ -70,10 +50,10 @@ export const MarchingAntsOverlay = defineComponent({
70
50
  };
71
51
  // Inject keyframes on mount
72
52
  onMounted(() => {
73
- ensureKeyframes();
53
+ injectGlobalStyles('ogrid-marching-ants-keyframes', '@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}');
74
54
  });
75
- // Measure when any range changes; re-measure on resize
76
- watch([() => props.selectionRange, clipRange, () => props.containerRef.value], () => {
55
+ // Measure when any range changes; re-measure on resize, column changes, data changes
56
+ watch([() => props.selectionRange, clipRange, () => toValue(props.containerRef), () => props.items, () => props.visibleColumns, () => props.columnSizingOverrides, () => props.columnOrder], () => {
77
57
  if (!props.selectionRange && !clipRange.value) {
78
58
  selRect.value = null;
79
59
  clipRect.value = null;
@@ -81,7 +61,7 @@ export const MarchingAntsOverlay = defineComponent({
81
61
  }
82
62
  // Delay one frame so cells are rendered
83
63
  rafId = requestAnimationFrame(measureAll);
84
- const container = props.containerRef.value;
64
+ const container = toValue(props.containerRef);
85
65
  if (container) {
86
66
  ro?.disconnect();
87
67
  ro = new ResizeObserver(measureAll);
@@ -23,8 +23,8 @@ export const StatusBar = defineComponent({
23
23
  style: {
24
24
  marginTop: 'auto',
25
25
  padding: '6px 12px',
26
- borderTop: '1px solid rgba(0,0,0,0.12)',
27
- backgroundColor: 'rgba(0,0,0,0.04)',
26
+ borderTop: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
27
+ backgroundColor: 'var(--ogrid-header-bg, rgba(0,0,0,0.04))',
28
28
  display: 'flex',
29
29
  alignItems: 'center',
30
30
  gap: '16px',
@@ -37,11 +37,11 @@ export const StatusBar = defineComponent({
37
37
  alignItems: 'center',
38
38
  gap: '4px',
39
39
  ...(i < parts.length - 1
40
- ? { marginRight: '16px', borderRight: '1px solid rgba(0,0,0,0.12)', paddingRight: '16px' }
40
+ ? { marginRight: '16px', borderRight: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))', paddingRight: '16px' }
41
41
  : {}),
42
42
  },
43
43
  }, [
44
- h('span', { style: { color: 'rgba(0,0,0,0.6)' } }, p.label),
44
+ h('span', { style: { color: 'var(--ogrid-fg-secondary, rgba(0,0,0,0.6))' } }, p.label),
45
45
  h('span', { style: { fontWeight: '600' } }, p.value.toLocaleString()),
46
46
  ])));
47
47
  };
@@ -0,0 +1,481 @@
1
+ /**
2
+ * Shared DataGridTable factory for Vue UI packages.
3
+ *
4
+ * Both vue-vuetify and vue-primevue DataGridTable components are 97% identical —
5
+ * they only differ in which checkbox and spinner components they render.
6
+ * This factory extracts all shared logic into one place.
7
+ */
8
+ import { defineComponent, computed, h, Teleport } from 'vue';
9
+ import { useDataGridTableSetup, } from '../composables';
10
+ import { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from '../utils';
11
+ import { buildHeaderRows, CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH } from '@alaarab/ogrid-core';
12
+ import { StatusBar } from './StatusBar';
13
+ import { MarchingAntsOverlay } from './MarchingAntsOverlay';
14
+ const NOOP = () => { };
15
+ /**
16
+ * Creates a DataGridTable component with framework-specific UI bindings.
17
+ * All grid logic, layout, and interaction handling is shared.
18
+ */
19
+ export function createDataGridTable(ui) {
20
+ return defineComponent({
21
+ name: 'DataGridTable',
22
+ props: {
23
+ gridProps: { type: Object, required: true },
24
+ },
25
+ setup(props) {
26
+ const propsRef = computed(() => props.gridProps);
27
+ const { wrapperRef, tableContainerRef, tableRef, lastMouseShift, state, columnReorder: { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown: handleReorderMouseDown }, virtualScroll: { containerRef: vsContainerRef, visibleRange, totalHeight: _totalHeight, scrollToRow: _scrollToRow }, virtualScrollEnabled, columnResize: { handleResizeStart, getColumnWidth }, } = useDataGridTableSetup({ props: propsRef });
28
+ // Stable handlers — avoid creating new closures per render
29
+ const onWrapperMousedown = (e) => { lastMouseShift.value = e.shiftKey; };
30
+ const onContextmenu = (e) => e.preventDefault();
31
+ const stopPropagation = (e) => e.stopPropagation();
32
+ return () => {
33
+ const p = props.gridProps;
34
+ const layout = state.layout.value;
35
+ const rowSel = state.rowSelection.value;
36
+ const editing = state.editing.value;
37
+ const interaction = state.interaction.value;
38
+ const ctxMenu = state.contextMenu.value;
39
+ const viewModels = state.viewModels.value;
40
+ const pinning = state.pinning.value;
41
+ const { headerMenu } = pinning;
42
+ const { visibleCols, hasCheckboxCol, hasRowNumbersCol, colOffset: _colOffset, containerWidth, minTableWidth, desiredTableWidth, } = layout;
43
+ const currentPage = p.currentPage ?? 1;
44
+ const pageSize = p.pageSize ?? 25;
45
+ const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * pageSize : 0;
46
+ const { selectedRowIds, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
47
+ 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;
49
+ const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
50
+ const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError: _onCellError } = viewModels;
51
+ const items = p.items;
52
+ const getRowId = p.getRowId;
53
+ const layoutMode = p.layoutMode ?? 'fill';
54
+ const rowSelection = p.rowSelection ?? 'none';
55
+ const freezeRows = p.freezeRows;
56
+ const freezeCols = p.freezeCols;
57
+ const suppressHorizontalScroll = p.suppressHorizontalScroll;
58
+ const isLoading = p.isLoading ?? false;
59
+ const loadingMessage = p.loadingMessage ?? 'Loading\u2026';
60
+ const ariaLabel = p['aria-label'];
61
+ const ariaLabelledBy = p['aria-labelledby'];
62
+ const fitToContent = layoutMode === 'content';
63
+ const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
64
+ const headerRows = buildHeaderRows(p.columns, p.visibleColumns);
65
+ const editCallbacks = { commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit };
66
+ const interactionHandlers = { handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu };
67
+ const handleSingleRowClick = (e) => {
68
+ if (rowSelection !== 'single')
69
+ return;
70
+ const tr = e.currentTarget;
71
+ const rowId = tr.dataset.rowId;
72
+ if (!rowId)
73
+ return;
74
+ rowSel.updateSelection(selectedRowIds.has(rowId) ? new Set() : new Set([rowId]));
75
+ };
76
+ // Render a cell's content
77
+ const renderCellContent = (item, col, rowIndex, colIdx) => {
78
+ const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInput);
79
+ if (descriptor.mode === 'editing-inline') {
80
+ const editorProps = buildInlineEditorProps(item, col, descriptor, editCallbacks);
81
+ return h(ui.InlineCellEditor, {
82
+ value: editorProps.value,
83
+ item: editorProps.item,
84
+ column: editorProps.column,
85
+ rowIndex: editorProps.rowIndex,
86
+ editorType: editorProps.editorType,
87
+ onCommit: editorProps.onCommit,
88
+ onCancel: editorProps.onCancel,
89
+ });
90
+ }
91
+ if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
92
+ const editorProps = buildPopoverEditorProps(item, col, descriptor, pendingEditorValue, editCallbacks);
93
+ const CustomEditor = col.cellEditor;
94
+ return h('div', [
95
+ h('div', {
96
+ ref: (el) => { if (el)
97
+ setPopoverAnchorEl(el); },
98
+ class: 'ogrid-popover-anchor',
99
+ 'aria-hidden': 'true',
100
+ }),
101
+ popoverAnchorEl
102
+ ? h(CustomEditor, editorProps)
103
+ : null,
104
+ ]);
105
+ }
106
+ // Display mode
107
+ const content = resolveCellDisplayContent(col, item, descriptor.displayValue);
108
+ const cellStyle = resolveCellStyle(col, item);
109
+ const interactionProps2 = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
110
+ const cellClasses = ['ogrid-cell-content'];
111
+ if (col.type === 'numeric')
112
+ cellClasses.push('ogrid-cell-content--numeric');
113
+ else if (col.type === 'boolean')
114
+ cellClasses.push('ogrid-cell-content--boolean');
115
+ if (descriptor.canEditAny)
116
+ cellClasses.push('ogrid-cell-content--editable');
117
+ if (descriptor.isActive && !descriptor.isInRange)
118
+ cellClasses.push('ogrid-cell-content--active');
119
+ if (descriptor.isInRange)
120
+ cellClasses.push('ogrid-cell-in-range');
121
+ if (descriptor.isInCutRange)
122
+ cellClasses.push('ogrid-cell-cut');
123
+ const styledContent = cellStyle
124
+ ? h('span', { style: cellStyle }, content)
125
+ : content;
126
+ return h('div', {
127
+ ...interactionProps2,
128
+ class: cellClasses.join(' '),
129
+ }, [
130
+ styledContent,
131
+ ...(descriptor.canEditAny && descriptor.isSelectionEndCell ? [
132
+ h('div', {
133
+ onMousedown: handleFillHandleMouseDown,
134
+ 'aria-label': 'Fill handle',
135
+ class: 'ogrid-fill-handle',
136
+ }),
137
+ ] : []),
138
+ ]);
139
+ };
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`,
177
+ };
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
+ };
188
+ // Dynamic wrapper style
189
+ const wrapperStyle = {
190
+ position: 'relative',
191
+ flex: '1',
192
+ minHeight: '0',
193
+ width: fitToContent ? 'fit-content' : '100%',
194
+ maxWidth: '100%',
195
+ overflowX: suppressHorizontalScroll ? 'hidden' : allowOverflowX ? 'auto' : 'hidden',
196
+ overflowY: 'auto',
197
+ backgroundColor: '#fff',
198
+ willChange: 'scroll-position',
199
+ };
200
+ return h('div', { class: 'ogrid-outer-container' }, [
201
+ // Scrollable wrapper
202
+ h('div', {
203
+ ref: (el) => { wrapperRef.value = el; vsContainerRef.value = el; },
204
+ tabindex: 0,
205
+ role: 'region',
206
+ 'aria-label': ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'),
207
+ 'aria-labelledby': ariaLabelledBy,
208
+ onMousedown: onWrapperMousedown,
209
+ onKeydown: handleGridKeyDown,
210
+ onContextmenu,
211
+ 'data-overflow-x': allowOverflowX ? 'true' : 'false',
212
+ style: wrapperStyle,
213
+ }, [
214
+ h('div', { class: 'ogrid-scroll-wrapper' }, [
215
+ h('div', { style: { minWidth: allowOverflowX ? `${minTableWidth}px` : undefined } }, [
216
+ h('div', {
217
+ ref: (el) => { tableContainerRef.value = el; },
218
+ class: ['ogrid-table-container', isLoading && items.length > 0 ? 'ogrid-table-container--loading' : ''],
219
+ }, [
220
+ // Drop indicator for column reorder
221
+ ...(isReorderDragging.value && dropIndicatorX.value !== null ? [
222
+ h('div', {
223
+ class: 'ogrid-drop-indicator',
224
+ style: { left: `${dropIndicatorX.value}px` },
225
+ }),
226
+ ] : []),
227
+ // Table
228
+ h('table', {
229
+ ref: (el) => { tableRef.value = el; },
230
+ class: 'ogrid-table',
231
+ style: { minWidth: `${minTableWidth}px` },
232
+ 'data-freeze-rows': freezeRows != null && freezeRows >= 1 ? freezeRows : undefined,
233
+ 'data-freeze-cols': freezeCols != null && freezeCols >= 1 ? freezeCols : undefined,
234
+ }, [
235
+ // Header
236
+ h('thead', { class: 'ogrid-thead' }, headerRows.map((row, rowIdx) => h('tr', { key: rowIdx, class: 'ogrid-header-row' }, [
237
+ // Checkbox header cell
238
+ ...(rowIdx === headerRows.length - 1 && hasCheckboxCol ? [
239
+ h('th', {
240
+ class: 'ogrid-checkbox-header',
241
+ style: {
242
+ width: `${CHECKBOX_COLUMN_WIDTH}px`,
243
+ minWidth: `${CHECKBOX_COLUMN_WIDTH}px`,
244
+ maxWidth: `${CHECKBOX_COLUMN_WIDTH}px`,
245
+ },
246
+ }, ui.renderCheckbox({
247
+ modelValue: allSelected,
248
+ indeterminate: someSelected,
249
+ ariaLabel: 'Select all rows',
250
+ onChange: (c) => handleSelectAll(!!c),
251
+ })),
252
+ ] : []),
253
+ // Checkbox spacer in group header row
254
+ ...(rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol ? [
255
+ h('th', {
256
+ rowSpan: headerRows.length - 1,
257
+ class: 'ogrid-checkbox-spacer',
258
+ style: { width: `${CHECKBOX_COLUMN_WIDTH}px`, minWidth: `${CHECKBOX_COLUMN_WIDTH}px` },
259
+ }),
260
+ ] : []),
261
+ // Row numbers header
262
+ ...(rowIdx === headerRows.length - 1 && hasRowNumbersCol ? [
263
+ h('th', {
264
+ class: 'ogrid-row-number-header',
265
+ style: {
266
+ width: `${ROW_NUMBER_COLUMN_WIDTH}px`,
267
+ minWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`,
268
+ maxWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`,
269
+ position: 'sticky',
270
+ left: hasCheckboxCol ? `${CHECKBOX_COLUMN_WIDTH}px` : '0',
271
+ zIndex: 3,
272
+ },
273
+ }, '#'),
274
+ ] : []),
275
+ // Row numbers spacer
276
+ ...(rowIdx === 0 && rowIdx < headerRows.length - 1 && hasRowNumbersCol ? [
277
+ h('th', {
278
+ rowSpan: headerRows.length - 1,
279
+ class: 'ogrid-row-number-spacer',
280
+ style: {
281
+ width: `${ROW_NUMBER_COLUMN_WIDTH}px`,
282
+ position: 'sticky',
283
+ left: hasCheckboxCol ? `${CHECKBOX_COLUMN_WIDTH}px` : '0',
284
+ zIndex: 3,
285
+ },
286
+ }),
287
+ ] : []),
288
+ // Header cells
289
+ ...row.map((cell, cellIdx) => {
290
+ if (cell.isGroup) {
291
+ return h('th', {
292
+ key: cellIdx,
293
+ colSpan: cell.colSpan,
294
+ scope: 'colgroup',
295
+ class: 'ogrid-column-group-header',
296
+ }, cell.label);
297
+ }
298
+ const col = cell.columnDef;
299
+ const colIdx = visibleCols.indexOf(col);
300
+ const { classes: headerClasses, style: headerStyle } = getHeaderClassAndStyle(col, colIdx);
301
+ return h('th', {
302
+ key: col.columnId,
303
+ scope: 'col',
304
+ 'data-column-id': col.columnId,
305
+ rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined,
306
+ class: headerClasses,
307
+ style: headerStyle,
308
+ onMousedown: (e) => handleReorderMouseDown(col.columnId, e),
309
+ }, [
310
+ h('div', { class: 'ogrid-header-content' }, [
311
+ h(ui.ColumnHeaderFilter, getHeaderFilterConfig(col, headerFilterInput)),
312
+ h('button', {
313
+ onClick: (e) => {
314
+ e.stopPropagation();
315
+ headerMenu.open(col.columnId, e.currentTarget);
316
+ },
317
+ 'aria-label': 'Column options',
318
+ title: 'Column options',
319
+ class: 'ogrid-column-menu-btn',
320
+ }, '\u22EE'),
321
+ ]),
322
+ h('div', {
323
+ onMousedown: (e) => { e.stopPropagation(); handleResizeStart(e, col); },
324
+ class: 'ogrid-resize-handle',
325
+ }),
326
+ ]);
327
+ }),
328
+ ]))),
329
+ // Body
330
+ ...(!showEmptyInGrid ? [
331
+ h('tbody', {}, (() => {
332
+ const vsEnabled = virtualScrollEnabled.value;
333
+ const vr = visibleRange.value;
334
+ const startIdx = vsEnabled ? vr.startIndex : 0;
335
+ const endIdx = vsEnabled ? Math.min(vr.endIndex, items.length - 1) : items.length - 1;
336
+ const rows = [];
337
+ if (vsEnabled && vr.offsetTop > 0) {
338
+ rows.push(h('tr', { key: '__vs-top', style: { height: `${vr.offsetTop}px` } }));
339
+ }
340
+ for (let rowIndex = startIdx; rowIndex <= endIdx; rowIndex++) {
341
+ const item = items[rowIndex];
342
+ if (!item)
343
+ continue;
344
+ const rowIdStr = getRowId(item);
345
+ const isSelected = selectedRowIds.has(rowIdStr);
346
+ rows.push(h('tr', {
347
+ key: rowIdStr,
348
+ 'data-row-id': rowIdStr,
349
+ onClick: handleSingleRowClick,
350
+ style: { cursor: rowSelection === 'single' ? 'pointer' : undefined },
351
+ }, [
352
+ // Checkbox cell
353
+ ...(hasCheckboxCol ? [
354
+ h('td', {
355
+ class: 'ogrid-checkbox-cell',
356
+ style: {
357
+ width: `${CHECKBOX_COLUMN_WIDTH}px`,
358
+ minWidth: `${CHECKBOX_COLUMN_WIDTH}px`,
359
+ maxWidth: `${CHECKBOX_COLUMN_WIDTH}px`,
360
+ },
361
+ }, h('div', {
362
+ 'data-row-index': rowIndex,
363
+ 'data-col-index': 0,
364
+ onClick: stopPropagation,
365
+ class: 'ogrid-checkbox-wrapper',
366
+ }, ui.renderCheckbox({
367
+ modelValue: isSelected,
368
+ ariaLabel: `Select row ${rowIndex + 1}`,
369
+ onChange: (checked) => handleRowCheckboxChange(rowIdStr, checked, rowIndex, lastMouseShift.value),
370
+ }))),
371
+ ] : []),
372
+ // Row numbers cell
373
+ ...(hasRowNumbersCol ? [
374
+ h('td', {
375
+ class: 'ogrid-row-number-cell',
376
+ style: {
377
+ width: `${ROW_NUMBER_COLUMN_WIDTH}px`,
378
+ minWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`,
379
+ maxWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`,
380
+ padding: '6px',
381
+ position: 'sticky',
382
+ left: hasCheckboxCol ? `${CHECKBOX_COLUMN_WIDTH}px` : '0',
383
+ zIndex: 2,
384
+ },
385
+ }, String(rowNumberOffset + rowIndex + 1)),
386
+ ] : []),
387
+ // Data cells
388
+ ...columnLayouts.map((cl, colIdx) => h('td', {
389
+ key: cl.col.columnId,
390
+ 'data-column-id': cl.col.columnId,
391
+ class: cl.tdClasses,
392
+ style: cl.tdDynamicStyle,
393
+ }, [renderCellContent(item, cl.col, rowIndex, colIdx)])),
394
+ ]));
395
+ }
396
+ if (vsEnabled && vr.offsetBottom > 0) {
397
+ rows.push(h('tr', { key: '__vs-bottom', style: { height: `${vr.offsetBottom}px` } }));
398
+ }
399
+ return rows;
400
+ })()),
401
+ ] : []),
402
+ ]),
403
+ // Empty state
404
+ ...(showEmptyInGrid && p.emptyState ? [
405
+ ui.renderEmptyState(p.emptyState),
406
+ ] : []),
407
+ ]),
408
+ ]),
409
+ ]),
410
+ ]),
411
+ // Context menu (teleported to body)
412
+ ...(menuPosition ? [
413
+ h(Teleport, { to: 'body' }, h(ui.GridContextMenu, {
414
+ x: menuPosition.x,
415
+ y: menuPosition.y,
416
+ hasSelection: hasCellSelection,
417
+ canUndo,
418
+ canRedo,
419
+ onUndo: onUndo ?? NOOP,
420
+ onRedo: onRedo ?? NOOP,
421
+ onCopy: handleCopy,
422
+ onCut: handleCut,
423
+ onPaste: () => { void handlePaste(); },
424
+ onSelectAll: handleSelectAllCells,
425
+ onClose: closeContextMenu,
426
+ })),
427
+ ] : []),
428
+ // Marching ants overlay
429
+ h(MarchingAntsOverlay, {
430
+ containerRef: tableContainerRef,
431
+ selectionRange,
432
+ copyRange: _copyRange,
433
+ cutRange: _cutRange,
434
+ colOffset: _colOffset,
435
+ items,
436
+ visibleColumns: p.visibleColumns instanceof Set ? Array.from(p.visibleColumns) : p.visibleColumns,
437
+ columnSizingOverrides: layout.columnSizingOverrides,
438
+ columnOrder: p.columnOrder,
439
+ }),
440
+ // Column header menu
441
+ h(ui.ColumnHeaderMenu, {
442
+ isOpen: headerMenu.isOpen,
443
+ anchorElement: headerMenu.anchorElement,
444
+ onClose: headerMenu.close,
445
+ onPinLeft: headerMenu.handlePinLeft,
446
+ onPinRight: headerMenu.handlePinRight,
447
+ onUnpin: headerMenu.handleUnpin,
448
+ onSortAsc: headerMenu.handleSortAsc,
449
+ onSortDesc: headerMenu.handleSortDesc,
450
+ onClearSort: headerMenu.handleClearSort,
451
+ onAutosizeThis: headerMenu.handleAutosizeThis,
452
+ onAutosizeAll: headerMenu.handleAutosizeAll,
453
+ canPinLeft: headerMenu.canPinLeft,
454
+ canPinRight: headerMenu.canPinRight,
455
+ canUnpin: headerMenu.canUnpin,
456
+ currentSort: headerMenu.currentSort,
457
+ isSortable: headerMenu.isSortable,
458
+ isResizable: headerMenu.isResizable,
459
+ }),
460
+ // Status bar
461
+ ...(statusBarConfig ? [
462
+ h(StatusBar, {
463
+ totalCount: statusBarConfig.totalCount,
464
+ filteredCount: statusBarConfig.filteredCount,
465
+ selectedCount: statusBarConfig.selectedCount ?? selectedRowIds.size,
466
+ selectedCellCount: selectionRange
467
+ ? (Math.abs(selectionRange.endRow - selectionRange.startRow) + 1) * (Math.abs(selectionRange.endCol - selectionRange.startCol) + 1)
468
+ : undefined,
469
+ aggregation: statusBarConfig.aggregation,
470
+ suppressRowCount: statusBarConfig.suppressRowCount,
471
+ }),
472
+ ] : []),
473
+ // Loading overlay
474
+ ...(isLoading ? [
475
+ h('div', { class: 'ogrid-loading-overlay' }, ui.renderSpinner(loadingMessage)),
476
+ ] : []),
477
+ ]);
478
+ };
479
+ },
480
+ });
481
+ }
@@ -1,10 +1,11 @@
1
1
  import { ref, computed } from 'vue';
2
+ import { measureColumnContentWidth } from '@alaarab/ogrid-core';
2
3
  /**
3
- * Manages state for the column header menu (pin left/right/unpin actions).
4
+ * Manages state for the column header menu (pin/unpin, sort, autosize actions).
4
5
  * Tracks which column's menu is open, anchor element, and action handlers.
5
6
  */
6
7
  export function useColumnHeaderMenuState(params) {
7
- const { pinnedColumns, onPinColumn, onUnpinColumn } = params;
8
+ const { columns, pinnedColumns, onPinColumn, onUnpinColumn, onSort, onColumnResized, onAutosizeColumn, sortBy, sortDirection, } = params;
8
9
  const isOpen = ref(false);
9
10
  const openForColumn = ref(null);
10
11
  const anchorElement = ref(null);
@@ -18,10 +19,25 @@ export function useColumnHeaderMenuState(params) {
18
19
  openForColumn.value = null;
19
20
  anchorElement.value = null;
20
21
  };
22
+ const currentColumn = computed(() => openForColumn.value ? columns.value.find((c) => c.columnId === openForColumn.value) : undefined);
21
23
  const currentPinState = computed(() => openForColumn.value ? pinnedColumns.value[openForColumn.value] : undefined);
22
24
  const canPinLeft = computed(() => currentPinState.value !== 'left');
23
25
  const canPinRight = computed(() => currentPinState.value !== 'right');
24
26
  const canUnpin = computed(() => !!currentPinState.value);
27
+ const currentSort = computed(() => {
28
+ if (!openForColumn.value || !sortBy?.value || sortBy.value !== openForColumn.value) {
29
+ return null;
30
+ }
31
+ return sortDirection?.value ?? null;
32
+ });
33
+ const isSortable = computed(() => {
34
+ const col = currentColumn.value;
35
+ return col?.sortable !== false;
36
+ });
37
+ const isResizable = computed(() => {
38
+ // All columns are resizable by default (no per-column resizable flag in core)
39
+ return true;
40
+ });
25
41
  const handlePinLeft = () => {
26
42
  if (openForColumn.value && canPinLeft.value) {
27
43
  onPinColumn(openForColumn.value, 'left');
@@ -40,6 +56,41 @@ export function useColumnHeaderMenuState(params) {
40
56
  close();
41
57
  }
42
58
  };
59
+ const handleSortAsc = () => {
60
+ if (openForColumn.value && onSort) {
61
+ onSort(openForColumn.value, 'asc');
62
+ close();
63
+ }
64
+ };
65
+ const handleSortDesc = () => {
66
+ if (openForColumn.value && onSort) {
67
+ onSort(openForColumn.value, 'desc');
68
+ close();
69
+ }
70
+ };
71
+ const handleClearSort = () => {
72
+ if (openForColumn.value && onSort) {
73
+ onSort(openForColumn.value, null);
74
+ close();
75
+ }
76
+ };
77
+ const handleAutosizeThis = () => {
78
+ const resizer = onAutosizeColumn ?? onColumnResized;
79
+ if (!openForColumn.value || !resizer || !isResizable.value)
80
+ return;
81
+ const col = currentColumn.value;
82
+ resizer(openForColumn.value, measureColumnContentWidth(openForColumn.value, col?.minWidth));
83
+ close();
84
+ };
85
+ const handleAutosizeAll = () => {
86
+ const resizer = onAutosizeColumn ?? onColumnResized;
87
+ if (!resizer)
88
+ return;
89
+ columns.value.forEach((col) => {
90
+ resizer(col.columnId, measureColumnContentWidth(col.columnId, col.minWidth));
91
+ });
92
+ close();
93
+ };
43
94
  return {
44
95
  isOpen,
45
96
  openForColumn,
@@ -49,8 +100,16 @@ export function useColumnHeaderMenuState(params) {
49
100
  handlePinLeft,
50
101
  handlePinRight,
51
102
  handleUnpin,
103
+ handleSortAsc,
104
+ handleSortDesc,
105
+ handleClearSort,
106
+ handleAutosizeThis,
107
+ handleAutosizeAll,
52
108
  canPinLeft,
53
109
  canPinRight,
54
110
  canUnpin,
111
+ currentSort,
112
+ isSortable,
113
+ isResizable,
55
114
  };
56
115
  }
@@ -1,5 +1,5 @@
1
1
  import { ref, computed } from 'vue';
2
- import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregations } from '@alaarab/ogrid-core';
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';
5
5
  import { useActiveCell } from './useActiveCell';
@@ -175,11 +175,35 @@ export function useDataGridState(params) {
175
175
  pinnedColumns: pinnedColumnsProp,
176
176
  onColumnPinned: onColumnPinnedProp.value,
177
177
  });
178
+ // Autosize callback — updates internal column sizing state + notifies external listener
179
+ const handleAutosizeColumn = (columnId, width) => {
180
+ setColumnSizingOverrides({ ...columnSizingOverrides.value, [columnId]: { widthPx: width } });
181
+ onColumnResizedProp.value?.(columnId, width);
182
+ };
178
183
  const headerMenuResult = useColumnHeaderMenuState({
184
+ columns: flatColumns,
179
185
  pinnedColumns: pinningResult.pinnedColumns,
180
186
  onPinColumn: pinningResult.pinColumn,
181
187
  onUnpinColumn: pinningResult.unpinColumn,
188
+ onSort: props.value.onColumnSort,
189
+ onColumnResized: onColumnResizedProp.value,
190
+ onAutosizeColumn: handleAutosizeColumn,
191
+ sortBy: computed(() => props.value.sortBy),
192
+ sortDirection: computed(() => props.value.sortDirection),
193
+ });
194
+ // Build column width map for pinning offset computation
195
+ const columnWidthMap = computed(() => {
196
+ const map = {};
197
+ for (const col of visibleCols.value) {
198
+ const override = columnSizingOverrides.value[col.columnId];
199
+ map[col.columnId] = override
200
+ ? override.widthPx
201
+ : (col.idealWidth ?? col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH);
202
+ }
203
+ return map;
182
204
  });
205
+ const leftOffsets = computed(() => pinningResult.computeLeftOffsets(visibleCols.value, columnWidthMap.value, DEFAULT_MIN_COLUMN_WIDTH, hasCheckboxCol.value, CHECKBOX_COLUMN_WIDTH));
206
+ const rightOffsets = computed(() => pinningResult.computeRightOffsets(visibleCols.value, columnWidthMap.value, DEFAULT_MIN_COLUMN_WIDTH));
183
207
  const aggregation = computed(() => computeAggregations(items.value, visibleCols.value, cellSelection.value ? selectionRange.value : null));
184
208
  const statusBarConfig = computed(() => {
185
209
  const base = getDataGridStatusBarConfig(statusBarProp.value, items.value.length, rowSelectionResult.selectedRowIds.value.size);
@@ -324,8 +348,8 @@ export function useDataGridState(params) {
324
348
  pinColumn: pinningResult.pinColumn,
325
349
  unpinColumn: pinningResult.unpinColumn,
326
350
  isPinned: pinningResult.isPinned,
327
- computeLeftOffsets: pinningResult.computeLeftOffsets,
328
- computeRightOffsets: pinningResult.computeRightOffsets,
351
+ leftOffsets: leftOffsets.value,
352
+ rightOffsets: rightOffsets.value,
329
353
  headerMenu: {
330
354
  isOpen: headerMenuResult.isOpen.value,
331
355
  openForColumn: headerMenuResult.openForColumn.value,
@@ -335,9 +359,17 @@ export function useDataGridState(params) {
335
359
  handlePinLeft: headerMenuResult.handlePinLeft,
336
360
  handlePinRight: headerMenuResult.handlePinRight,
337
361
  handleUnpin: headerMenuResult.handleUnpin,
362
+ handleSortAsc: headerMenuResult.handleSortAsc,
363
+ handleSortDesc: headerMenuResult.handleSortDesc,
364
+ handleClearSort: headerMenuResult.handleClearSort,
365
+ handleAutosizeThis: headerMenuResult.handleAutosizeThis,
366
+ handleAutosizeAll: headerMenuResult.handleAutosizeAll,
338
367
  canPinLeft: headerMenuResult.canPinLeft.value,
339
368
  canPinRight: headerMenuResult.canPinRight.value,
340
369
  canUnpin: headerMenuResult.canUnpin.value,
370
+ currentSort: headerMenuResult.currentSort.value,
371
+ isSortable: headerMenuResult.isSortable.value,
372
+ isResizable: headerMenuResult.isResizable.value,
341
373
  },
342
374
  }));
343
375
  return {
@@ -1,5 +1,5 @@
1
1
  import { ref, computed, watch } from 'vue';
2
- import { mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, flattenColumns, processClientSideData, } from '@alaarab/ogrid-core';
2
+ import { mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, flattenColumns, processClientSideData, computeNextSortState, } from '@alaarab/ogrid-core';
3
3
  import { useFilterOptions } from './useFilterOptions';
4
4
  import { useSideBarState } from './useSideBarState';
5
5
  const DEFAULT_PAGE_SIZE = 25;
@@ -126,11 +126,8 @@ export function useOGrid(props) {
126
126
  internalVisibleColumns.value = cols;
127
127
  callbacks.value.onVisibleColumnsChange?.(cols);
128
128
  };
129
- const handleSort = (columnKey) => {
130
- setSort({
131
- field: columnKey,
132
- direction: sort.value.field === columnKey && sort.value.direction === 'asc' ? 'desc' : 'asc',
133
- });
129
+ const handleSort = (columnKey, direction) => {
130
+ setSort(computeNextSortState(sort.value, columnKey, direction));
134
131
  };
135
132
  const handleFilterChange = (key, value) => {
136
133
  setFilters(mergeFilter(filters.value, key, value));
@@ -329,6 +326,9 @@ export function useOGrid(props) {
329
326
  rowSelection: p.rowSelection ?? 'none',
330
327
  selectedRows: effectiveSelectedRows.value,
331
328
  onSelectionChange: handleSelectionChange,
329
+ showRowNumbers: p.showRowNumbers,
330
+ currentPage: page.value,
331
+ pageSize: pageSize.value,
332
332
  statusBar: statusBarConfig.value,
333
333
  isLoading: isLoadingResolved.value,
334
334
  filters: filters.value,
@@ -339,7 +339,9 @@ export function useOGrid(props) {
339
339
  getUserByEmail: ds?.getUserByEmail,
340
340
  layoutMode: p.layoutMode,
341
341
  suppressHorizontalScroll: p.suppressHorizontalScroll,
342
+ columnReorder: p.columnReorder,
342
343
  virtualScroll: p.virtualScroll,
344
+ density: p.density ?? 'normal',
343
345
  'aria-label': p['aria-label'],
344
346
  'aria-labelledby': p['aria-labelledby'],
345
347
  emptyState: {
package/dist/esm/index.js CHANGED
@@ -8,3 +8,5 @@ export { StatusBar } from './components/StatusBar';
8
8
  export { useOGrid, useDataGridState, useActiveCell, useCellEditing, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useFillHandle, useUndoRedo, useContextMenu, useColumnResize, useColumnReorder, useVirtualScroll, useFilterOptions, useDebounce, useDebouncedCallback, useTableLayout, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useRichSelectState, useSideBarState, useColumnPinning, useColumnHeaderMenuState, useDataGridTableSetup, } from './composables';
9
9
  // View model utilities (for UI packages)
10
10
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './utils';
11
+ // DataGridTable factory (for UI packages)
12
+ export { createDataGridTable } from './components/createDataGridTable';
@@ -34,6 +34,28 @@ export declare const MarchingAntsOverlay: import("vue").DefineComponent<import("
34
34
  type: NumberConstructor;
35
35
  required: true;
36
36
  };
37
+ /** Items array — triggers re-measurement when data changes (e.g., sorting) */
38
+ items: {
39
+ type: PropType<readonly unknown[]>;
40
+ required: true;
41
+ };
42
+ /** Visible columns — triggers re-measurement when columns are hidden/shown */
43
+ visibleColumns: {
44
+ type: PropType<readonly string[] | undefined>;
45
+ default: undefined;
46
+ };
47
+ /** Column sizing overrides — triggers re-measurement when columns are resized */
48
+ columnSizingOverrides: {
49
+ type: PropType<Record<string, {
50
+ widthPx: number;
51
+ }>>;
52
+ required: true;
53
+ };
54
+ /** Column order — triggers re-measurement when columns are reordered */
55
+ columnOrder: {
56
+ type: PropType<readonly string[] | undefined>;
57
+ default: undefined;
58
+ };
37
59
  }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
38
60
  [key: string]: any;
39
61
  }> | null, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -62,8 +84,32 @@ export declare const MarchingAntsOverlay: import("vue").DefineComponent<import("
62
84
  type: NumberConstructor;
63
85
  required: true;
64
86
  };
87
+ /** Items array — triggers re-measurement when data changes (e.g., sorting) */
88
+ items: {
89
+ type: PropType<readonly unknown[]>;
90
+ required: true;
91
+ };
92
+ /** Visible columns — triggers re-measurement when columns are hidden/shown */
93
+ visibleColumns: {
94
+ type: PropType<readonly string[] | undefined>;
95
+ default: undefined;
96
+ };
97
+ /** Column sizing overrides — triggers re-measurement when columns are resized */
98
+ columnSizingOverrides: {
99
+ type: PropType<Record<string, {
100
+ widthPx: number;
101
+ }>>;
102
+ required: true;
103
+ };
104
+ /** Column order — triggers re-measurement when columns are reordered */
105
+ columnOrder: {
106
+ type: PropType<readonly string[] | undefined>;
107
+ default: undefined;
108
+ };
65
109
  }>> & Readonly<{}>, {
66
110
  selectionRange: ISelectionRange | null;
67
111
  copyRange: ISelectionRange | null;
68
112
  cutRange: ISelectionRange | null;
113
+ visibleColumns: readonly string[] | undefined;
114
+ columnOrder: readonly string[] | undefined;
69
115
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared DataGridTable factory for Vue UI packages.
3
+ *
4
+ * Both vue-vuetify and vue-primevue DataGridTable components are 97% identical —
5
+ * they only differ in which checkbox and spinner components they render.
6
+ * This factory extracts all shared logic into one place.
7
+ */
8
+ import { type PropType, type VNode, type Component } from 'vue';
9
+ import type { IOGridDataGridProps } from '../types';
10
+ /** Framework-specific component bindings passed by each UI package */
11
+ export interface IDataGridTableUIBindings {
12
+ /** Render a checkbox (Vuetify VCheckbox vs PrimeVue Checkbox) */
13
+ renderCheckbox: (props: {
14
+ modelValue: boolean;
15
+ indeterminate?: boolean;
16
+ ariaLabel: string;
17
+ onChange: (checked: boolean) => void;
18
+ }) => VNode;
19
+ /** Render a loading spinner with message */
20
+ renderSpinner: (message: string) => VNode;
21
+ /** Package-local ColumnHeaderFilter component */
22
+ ColumnHeaderFilter: Component;
23
+ /** Package-local ColumnHeaderMenu component */
24
+ ColumnHeaderMenu: Component;
25
+ /** Package-local InlineCellEditor component */
26
+ InlineCellEditor: Component;
27
+ /** Package-local GridContextMenu component */
28
+ GridContextMenu: Component;
29
+ /** Package-local renderEmptyState function */
30
+ renderEmptyState: (emptyState: {
31
+ render?: () => unknown;
32
+ message?: string | null;
33
+ hasActiveFilters?: boolean;
34
+ onClearAll?: () => void;
35
+ }) => VNode;
36
+ }
37
+ /**
38
+ * Creates a DataGridTable component with framework-specific UI bindings.
39
+ * All grid logic, layout, and interaction handling is shared.
40
+ */
41
+ export declare function createDataGridTable(ui: IDataGridTableUIBindings): import("vue").DefineComponent<import("vue").ExtractPropTypes<{
42
+ gridProps: {
43
+ type: PropType<IOGridDataGridProps<unknown>>;
44
+ required: true;
45
+ };
46
+ }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
47
+ [key: string]: any;
48
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
49
+ gridProps: {
50
+ type: PropType<IOGridDataGridProps<unknown>>;
51
+ required: true;
52
+ };
53
+ }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -1,8 +1,17 @@
1
1
  import { type Ref } from 'vue';
2
- export interface UseColumnHeaderMenuStateParams {
2
+ import type { IColumnDef } from '../types';
3
+ export interface UseColumnHeaderMenuStateParams<T = unknown> {
4
+ columns: Ref<IColumnDef<T>[]>;
3
5
  pinnedColumns: Ref<Record<string, 'left' | 'right'>>;
4
6
  onPinColumn: (columnId: string, side: 'left' | 'right') => void;
5
7
  onUnpinColumn: (columnId: string) => void;
8
+ onSort?: (columnId: string, direction: 'asc' | 'desc' | null) => void;
9
+ onColumnResized?: (columnId: string, width: number) => void;
10
+ onAutosizeColumn?: (columnId: string, width: number) => void;
11
+ sortBy?: Ref<string | undefined>;
12
+ sortDirection?: Ref<'asc' | 'desc' | undefined>;
13
+ data?: Ref<unknown[]>;
14
+ getRowId?: (item: unknown) => string | number;
6
15
  }
7
16
  export interface UseColumnHeaderMenuStateResult {
8
17
  isOpen: Ref<boolean>;
@@ -13,12 +22,20 @@ export interface UseColumnHeaderMenuStateResult {
13
22
  handlePinLeft: () => void;
14
23
  handlePinRight: () => void;
15
24
  handleUnpin: () => void;
25
+ handleSortAsc: () => void;
26
+ handleSortDesc: () => void;
27
+ handleClearSort: () => void;
28
+ handleAutosizeThis: () => void;
29
+ handleAutosizeAll: () => void;
16
30
  canPinLeft: Ref<boolean>;
17
31
  canPinRight: Ref<boolean>;
18
32
  canUnpin: Ref<boolean>;
33
+ currentSort: Ref<'asc' | 'desc' | null>;
34
+ isSortable: Ref<boolean>;
35
+ isResizable: Ref<boolean>;
19
36
  }
20
37
  /**
21
- * Manages state for the column header menu (pin left/right/unpin actions).
38
+ * Manages state for the column header menu (pin/unpin, sort, autosize actions).
22
39
  * Tracks which column's menu is open, anchor element, and action handlers.
23
40
  */
24
- export declare function useColumnHeaderMenuState(params: UseColumnHeaderMenuStateParams): UseColumnHeaderMenuStateResult;
41
+ export declare function useColumnHeaderMenuState<T = unknown>(params: UseColumnHeaderMenuStateParams<T>): UseColumnHeaderMenuStateResult;
@@ -121,12 +121,8 @@ export interface DataGridPinningState {
121
121
  pinColumn: (columnId: string, side: 'left' | 'right') => void;
122
122
  unpinColumn: (columnId: string) => void;
123
123
  isPinned: (columnId: string) => 'left' | 'right' | undefined;
124
- computeLeftOffsets: (visibleCols: {
125
- columnId: string;
126
- }[], columnWidths: Record<string, number>, defaultWidth: number, hasCheckboxColumn: boolean, checkboxColumnWidth: number) => Record<string, number>;
127
- computeRightOffsets: (visibleCols: {
128
- columnId: string;
129
- }[], columnWidths: Record<string, number>, defaultWidth: number) => Record<string, number>;
124
+ leftOffsets: Record<string, number>;
125
+ rightOffsets: Record<string, number>;
130
126
  headerMenu: {
131
127
  isOpen: boolean;
132
128
  openForColumn: string | null;
@@ -136,9 +132,17 @@ export interface DataGridPinningState {
136
132
  handlePinLeft: () => void;
137
133
  handlePinRight: () => void;
138
134
  handleUnpin: () => void;
135
+ handleSortAsc: () => void;
136
+ handleSortDesc: () => void;
137
+ handleClearSort: () => void;
138
+ handleAutosizeThis: () => void;
139
+ handleAutosizeAll: () => void;
139
140
  canPinLeft: boolean;
140
141
  canPinRight: boolean;
141
142
  canUnpin: boolean;
143
+ currentSort: 'asc' | 'desc' | null;
144
+ isSortable: boolean;
145
+ isResizable: boolean;
142
146
  };
143
147
  }
144
148
  export interface UseDataGridStateResult<T> {
@@ -8,4 +8,5 @@ export { useOGrid, useDataGridState, useActiveCell, useCellEditing, useCellSelec
8
8
  export type { UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGridLayout, UseOGridFilters, ColumnChooserPlacement, UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, DataGridPinningState, UseActiveCellResult, EditingCell, UseCellEditingParams, UseCellEditingResult, UseCellSelectionParams, UseCellSelectionResult, UseClipboardParams, UseClipboardResult, UseRowSelectionParams, UseRowSelectionResult, UseKeyboardNavigationParams, UseKeyboardNavigationResult, UseFillHandleParams, UseFillHandleResult, UseUndoRedoParams, UseUndoRedoResult, ContextMenuPosition, UseContextMenuResult, UseColumnResizeParams, UseColumnResizeResult, UseColumnReorderParams, UseColumnReorderResult, UseVirtualScrollParams, UseVirtualScrollResult, UseFilterOptionsResult, UseTableLayoutParams, UseTableLayoutResult, UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, UseTextFilterStateParams, UseTextFilterStateResult, UseMultiSelectFilterStateParams, UseMultiSelectFilterStateResult, UsePeopleFilterStateParams, UsePeopleFilterStateResult, UseDateFilterStateParams, UseDateFilterStateResult, UseColumnChooserStateParams, UseColumnChooserStateResult, InlineCellEditorType, UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, UseRichSelectStateParams, UseRichSelectStateResult, UseSideBarStateParams, UseSideBarStateResult, DebouncedFn, UseColumnPinningParams, UseColumnPinningResult, UseColumnHeaderMenuStateParams, UseColumnHeaderMenuStateResult, UseDataGridTableSetupParams, UseDataGridTableSetupResult, } from './composables';
9
9
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './utils';
10
10
  export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, CellInteractionHandlers, CellInteractionProps, } from './utils';
11
+ export { createDataGridTable, type IDataGridTableUIBindings } from './components/createDataGridTable';
11
12
  export type { SideBarProps, SideBarFilterColumn } from './components/SideBar';
@@ -42,6 +42,8 @@ interface IOGridBaseProps<T> {
42
42
  rowSelection?: RowSelectionMode;
43
43
  selectedRows?: Set<RowId>;
44
44
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
45
+ /** Show Excel-style row numbers column at the start of the grid (1, 2, 3...). Default: false. */
46
+ showRowNumbers?: boolean;
45
47
  statusBar?: boolean | IStatusBarProps;
46
48
  defaultPageSize?: number;
47
49
  defaultSortBy?: string;
@@ -102,13 +104,15 @@ export interface IOGridDataGridProps<T> {
102
104
  getRowId: (item: T) => RowId;
103
105
  sortBy?: string;
104
106
  sortDirection: 'asc' | 'desc';
105
- onColumnSort: (columnKey: string) => void;
107
+ onColumnSort: (columnKey: string, direction?: 'asc' | 'desc' | null) => void;
106
108
  visibleColumns: Set<string>;
107
109
  /** Optional column display order (column ids). When set, visible columns are ordered by this array. */
108
110
  columnOrder?: string[];
109
111
  onColumnOrderChange?: (order: string[]) => void;
110
112
  /** Called when a column is resized by the user. */
111
113
  onColumnResized?: (columnId: string, width: number) => void;
114
+ /** Called when user requests autosize for a single column (with measured width). */
115
+ onAutosizeColumn?: (columnId: string, width: number) => void;
112
116
  /** Called when a column is pinned or unpinned. */
113
117
  onColumnPinned?: (columnId: string, pinned: 'left' | 'right' | null) => void;
114
118
  /** Runtime pin overrides (from restored state or programmatic changes). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-vue",
3
- "version": "2.0.9",
3
+ "version": "2.0.12",
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.9"
38
+ "@alaarab/ogrid-core": "2.0.12"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "vue": "^3.3.0"