@alaarab/ogrid-vue 2.0.9 → 2.0.11

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.
@@ -7,37 +7,7 @@
7
7
  * Uses SVG rects positioned via cell data-attribute measurements.
8
8
  */
9
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
- }
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);
@@ -70,10 +48,10 @@ export const MarchingAntsOverlay = defineComponent({
70
48
  };
71
49
  // Inject keyframes on mount
72
50
  onMounted(() => {
73
- ensureKeyframes();
51
+ injectGlobalStyles('ogrid-marching-ants-keyframes', '@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}');
74
52
  });
75
- // Measure when any range changes; re-measure on resize
76
- watch([() => props.selectionRange, clipRange, () => props.containerRef.value], () => {
53
+ // Measure when any range changes; re-measure on resize, column changes, data changes
54
+ watch([() => props.selectionRange, clipRange, () => props.containerRef.value, () => props.items, () => props.visibleColumns, () => props.columnSizingOverrides, () => props.columnOrder], () => {
77
55
  if (!props.selectionRange && !clipRange.value) {
78
56
  selRect.value = null;
79
57
  clipRect.value = null;
@@ -1,10 +1,10 @@
1
1
  import { ref, computed } from 'vue';
2
2
  /**
3
- * Manages state for the column header menu (pin left/right/unpin actions).
3
+ * Manages state for the column header menu (pin/unpin, sort, autosize actions).
4
4
  * Tracks which column's menu is open, anchor element, and action handlers.
5
5
  */
6
6
  export function useColumnHeaderMenuState(params) {
7
- const { pinnedColumns, onPinColumn, onUnpinColumn } = params;
7
+ const { columns, pinnedColumns, onPinColumn, onUnpinColumn, onSort, onAutosizeColumn, onAutosizeAllColumns, sortBy, sortDirection, } = params;
8
8
  const isOpen = ref(false);
9
9
  const openForColumn = ref(null);
10
10
  const anchorElement = ref(null);
@@ -18,10 +18,25 @@ export function useColumnHeaderMenuState(params) {
18
18
  openForColumn.value = null;
19
19
  anchorElement.value = null;
20
20
  };
21
+ const currentColumn = computed(() => openForColumn.value ? columns.value.find((c) => c.columnId === openForColumn.value) : undefined);
21
22
  const currentPinState = computed(() => openForColumn.value ? pinnedColumns.value[openForColumn.value] : undefined);
22
23
  const canPinLeft = computed(() => currentPinState.value !== 'left');
23
24
  const canPinRight = computed(() => currentPinState.value !== 'right');
24
25
  const canUnpin = computed(() => !!currentPinState.value);
26
+ const currentSort = computed(() => {
27
+ if (!openForColumn.value || !sortBy?.value || sortBy.value !== openForColumn.value) {
28
+ return null;
29
+ }
30
+ return sortDirection?.value ?? null;
31
+ });
32
+ const isSortable = computed(() => {
33
+ const col = currentColumn.value;
34
+ return col?.sortable !== false;
35
+ });
36
+ const isResizable = computed(() => {
37
+ // All columns are resizable by default (no per-column resizable flag in core)
38
+ return true;
39
+ });
25
40
  const handlePinLeft = () => {
26
41
  if (openForColumn.value && canPinLeft.value) {
27
42
  onPinColumn(openForColumn.value, 'left');
@@ -40,6 +55,36 @@ export function useColumnHeaderMenuState(params) {
40
55
  close();
41
56
  }
42
57
  };
58
+ const handleSortAsc = () => {
59
+ if (openForColumn.value && onSort) {
60
+ onSort(openForColumn.value, 'asc');
61
+ close();
62
+ }
63
+ };
64
+ const handleSortDesc = () => {
65
+ if (openForColumn.value && onSort) {
66
+ onSort(openForColumn.value, 'desc');
67
+ close();
68
+ }
69
+ };
70
+ const handleClearSort = () => {
71
+ if (openForColumn.value && onSort) {
72
+ onSort(openForColumn.value, null);
73
+ close();
74
+ }
75
+ };
76
+ const handleAutosizeThis = () => {
77
+ if (openForColumn.value && onAutosizeColumn) {
78
+ onAutosizeColumn(openForColumn.value);
79
+ close();
80
+ }
81
+ };
82
+ const handleAutosizeAll = () => {
83
+ if (onAutosizeAllColumns) {
84
+ onAutosizeAllColumns();
85
+ close();
86
+ }
87
+ };
43
88
  return {
44
89
  isOpen,
45
90
  openForColumn,
@@ -49,8 +94,16 @@ export function useColumnHeaderMenuState(params) {
49
94
  handlePinLeft,
50
95
  handlePinRight,
51
96
  handleUnpin,
97
+ handleSortAsc,
98
+ handleSortDesc,
99
+ handleClearSort,
100
+ handleAutosizeThis,
101
+ handleAutosizeAll,
52
102
  canPinLeft,
53
103
  canPinRight,
54
104
  canUnpin,
105
+ currentSort,
106
+ isSortable,
107
+ isResizable,
55
108
  };
56
109
  }
@@ -176,9 +176,15 @@ export function useDataGridState(params) {
176
176
  onColumnPinned: onColumnPinnedProp.value,
177
177
  });
178
178
  const headerMenuResult = useColumnHeaderMenuState({
179
+ columns: flatColumns,
179
180
  pinnedColumns: pinningResult.pinnedColumns,
180
181
  onPinColumn: pinningResult.pinColumn,
181
182
  onUnpinColumn: pinningResult.unpinColumn,
183
+ onSort: props.value.onColumnSort,
184
+ onAutosizeColumn: props.value.onAutosizeColumn,
185
+ onAutosizeAllColumns: props.value.onAutosizeAllColumns,
186
+ sortBy: computed(() => props.value.sortBy),
187
+ sortDirection: computed(() => props.value.sortDirection),
182
188
  });
183
189
  const aggregation = computed(() => computeAggregations(items.value, visibleCols.value, cellSelection.value ? selectionRange.value : null));
184
190
  const statusBarConfig = computed(() => {
@@ -335,9 +341,17 @@ export function useDataGridState(params) {
335
341
  handlePinLeft: headerMenuResult.handlePinLeft,
336
342
  handlePinRight: headerMenuResult.handlePinRight,
337
343
  handleUnpin: headerMenuResult.handleUnpin,
344
+ handleSortAsc: headerMenuResult.handleSortAsc,
345
+ handleSortDesc: headerMenuResult.handleSortDesc,
346
+ handleClearSort: headerMenuResult.handleClearSort,
347
+ handleAutosizeThis: headerMenuResult.handleAutosizeThis,
348
+ handleAutosizeAll: headerMenuResult.handleAutosizeAll,
338
349
  canPinLeft: headerMenuResult.canPinLeft.value,
339
350
  canPinRight: headerMenuResult.canPinRight.value,
340
351
  canUnpin: headerMenuResult.canUnpin.value,
352
+ currentSort: headerMenuResult.currentSort.value,
353
+ isSortable: headerMenuResult.isSortable.value,
354
+ isResizable: headerMenuResult.isResizable.value,
341
355
  },
342
356
  }));
343
357
  return {
@@ -127,10 +127,21 @@ export function useOGrid(props) {
127
127
  callbacks.value.onVisibleColumnsChange?.(cols);
128
128
  };
129
129
  const handleSort = (columnKey) => {
130
- setSort({
131
- field: columnKey,
132
- direction: sort.value.field === columnKey && sort.value.direction === 'asc' ? 'desc' : 'asc',
133
- });
130
+ const currentSort = sort.value;
131
+ if (currentSort.field === columnKey) {
132
+ // Cycle: asc desc clear
133
+ if (currentSort.direction === 'asc') {
134
+ setSort({ field: columnKey, direction: 'desc' });
135
+ }
136
+ else {
137
+ // Clear sort (empty field means no column is sorted)
138
+ setSort({ field: '', direction: 'asc' });
139
+ }
140
+ }
141
+ else {
142
+ // Start new sort
143
+ setSort({ field: columnKey, direction: 'asc' });
144
+ }
134
145
  };
135
146
  const handleFilterChange = (key, value) => {
136
147
  setFilters(mergeFilter(filters.value, key, value));
@@ -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>;
@@ -1,8 +1,15 @@
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
+ onAutosizeColumn?: (columnId: string) => void;
10
+ onAutosizeAllColumns?: () => void;
11
+ sortBy?: Ref<string | undefined>;
12
+ sortDirection?: Ref<'asc' | 'desc' | undefined>;
6
13
  }
7
14
  export interface UseColumnHeaderMenuStateResult {
8
15
  isOpen: Ref<boolean>;
@@ -13,12 +20,20 @@ export interface UseColumnHeaderMenuStateResult {
13
20
  handlePinLeft: () => void;
14
21
  handlePinRight: () => void;
15
22
  handleUnpin: () => void;
23
+ handleSortAsc: () => void;
24
+ handleSortDesc: () => void;
25
+ handleClearSort: () => void;
26
+ handleAutosizeThis: () => void;
27
+ handleAutosizeAll: () => void;
16
28
  canPinLeft: Ref<boolean>;
17
29
  canPinRight: Ref<boolean>;
18
30
  canUnpin: Ref<boolean>;
31
+ currentSort: Ref<'asc' | 'desc' | null>;
32
+ isSortable: Ref<boolean>;
33
+ isResizable: Ref<boolean>;
19
34
  }
20
35
  /**
21
- * Manages state for the column header menu (pin left/right/unpin actions).
36
+ * Manages state for the column header menu (pin/unpin, sort, autosize actions).
22
37
  * Tracks which column's menu is open, anchor element, and action handlers.
23
38
  */
24
- export declare function useColumnHeaderMenuState(params: UseColumnHeaderMenuStateParams): UseColumnHeaderMenuStateResult;
39
+ export declare function useColumnHeaderMenuState<T = unknown>(params: UseColumnHeaderMenuStateParams<T>): UseColumnHeaderMenuStateResult;
@@ -136,9 +136,17 @@ export interface DataGridPinningState {
136
136
  handlePinLeft: () => void;
137
137
  handlePinRight: () => void;
138
138
  handleUnpin: () => void;
139
+ handleSortAsc: () => void;
140
+ handleSortDesc: () => void;
141
+ handleClearSort: () => void;
142
+ handleAutosizeThis: () => void;
143
+ handleAutosizeAll: () => void;
139
144
  canPinLeft: boolean;
140
145
  canPinRight: boolean;
141
146
  canUnpin: boolean;
147
+ currentSort: 'asc' | 'desc' | null;
148
+ isSortable: boolean;
149
+ isResizable: boolean;
142
150
  };
143
151
  }
144
152
  export interface UseDataGridStateResult<T> {
@@ -109,6 +109,10 @@ export interface IOGridDataGridProps<T> {
109
109
  onColumnOrderChange?: (order: string[]) => void;
110
110
  /** Called when a column is resized by the user. */
111
111
  onColumnResized?: (columnId: string, width: number) => void;
112
+ /** Called when user requests autosize for a single column. */
113
+ onAutosizeColumn?: (columnId: string) => void;
114
+ /** Called when user requests autosize for all columns. */
115
+ onAutosizeAllColumns?: () => 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.11",
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.11"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "vue": "^3.3.0"