@alaarab/ogrid-angular 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/marching-ants-overlay.component.js +2 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/services/column-reorder.service.js +180 -0
- package/dist/esm/services/datagrid-state.service.js +18 -12
- package/dist/esm/services/ogrid.service.js +88 -37
- package/dist/esm/services/virtual-scroll.service.js +91 -0
- package/dist/esm/utils/dataGridViewModel.js +163 -0
- package/dist/esm/utils/debounce.js +105 -0
- package/dist/esm/utils/index.js +8 -0
- package/dist/esm/utils/latestRef.js +46 -0
- package/dist/types/components/marching-ants-overlay.component.d.ts +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/services/column-reorder.service.d.ts +30 -0
- package/dist/types/services/datagrid-state.service.d.ts +4 -2
- package/dist/types/services/ogrid.service.d.ts +29 -6
- package/dist/types/services/virtual-scroll.service.d.ts +54 -0
- package/dist/types/types/dataGridTypes.d.ts +3 -0
- package/dist/types/utils/dataGridViewModel.d.ts +107 -0
- package/dist/types/utils/debounce.d.ts +68 -0
- package/dist/types/utils/index.d.ts +7 -0
- package/dist/types/utils/latestRef.d.ts +42 -0
- package/package.json +2 -2
|
@@ -41,6 +41,7 @@ let MarchingAntsOverlayComponent = class MarchingAntsOverlayComponent {
|
|
|
41
41
|
this.copyRange = input(null);
|
|
42
42
|
this.cutRange = input(null);
|
|
43
43
|
this.colOffset = input(0);
|
|
44
|
+
this.columnSizingVersion = input(0);
|
|
44
45
|
this.selRect = signal(null);
|
|
45
46
|
this.clipRect = signal(null);
|
|
46
47
|
this.rafId = 0;
|
|
@@ -51,6 +52,7 @@ let MarchingAntsOverlayComponent = class MarchingAntsOverlayComponent {
|
|
|
51
52
|
const selRange = this.selectionRange();
|
|
52
53
|
const clipRange = this.copyRange() ?? this.cutRange();
|
|
53
54
|
const colOff = this.colOffset();
|
|
55
|
+
const _version = this.columnSizingVersion(); // Track column resize changes
|
|
54
56
|
if (this.resizeObserver) {
|
|
55
57
|
this.resizeObserver.disconnect();
|
|
56
58
|
this.resizeObserver = null;
|
package/dist/esm/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types
|
|
|
4
4
|
// Services
|
|
5
5
|
export { OGridService } from './services/ogrid.service';
|
|
6
6
|
export { DataGridStateService } from './services/datagrid-state.service';
|
|
7
|
+
export { ColumnReorderService } from './services/column-reorder.service';
|
|
8
|
+
export { VirtualScrollService } from './services/virtual-scroll.service';
|
|
7
9
|
// Components
|
|
8
10
|
export { OGridLayoutComponent } from './components/ogrid-layout.component';
|
|
9
11
|
export { StatusBarComponent } from './components/status-bar.component';
|
|
@@ -11,3 +13,4 @@ export { GridContextMenuComponent } from './components/grid-context-menu.compone
|
|
|
11
13
|
export { SideBarComponent } from './components/sidebar.component';
|
|
12
14
|
export { MarchingAntsOverlayComponent } from './components/marching-ants-overlay.component';
|
|
13
15
|
export { EmptyStateComponent } from './components/empty-state.component';
|
|
16
|
+
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, createDebouncedSignal, createDebouncedCallback, debounce, createLatestRef, createLatestCallback, } from './utils';
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Injectable, signal, DestroyRef, inject } from '@angular/core';
|
|
8
|
+
import { reorderColumnArray } from '@alaarab/ogrid-core';
|
|
9
|
+
/** Width of the resize handle zone on the right edge of each header cell. */
|
|
10
|
+
const RESIZE_HANDLE_ZONE = 8;
|
|
11
|
+
/**
|
|
12
|
+
* Manages column reorder drag interactions with RAF-throttled updates.
|
|
13
|
+
* Angular signals-based port of React's useColumnReorder hook.
|
|
14
|
+
*/
|
|
15
|
+
let ColumnReorderService = class ColumnReorderService {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.destroyRef = inject(DestroyRef);
|
|
18
|
+
// --- Input signals (set by consuming component) ---
|
|
19
|
+
this.columns = signal([]);
|
|
20
|
+
this.columnOrder = signal(undefined);
|
|
21
|
+
this.onColumnOrderChange = signal(undefined);
|
|
22
|
+
this.enabled = signal(true);
|
|
23
|
+
this.wrapperEl = signal(null);
|
|
24
|
+
// --- Internal state ---
|
|
25
|
+
this.isDragging = signal(false);
|
|
26
|
+
this.dropIndicatorX = signal(null);
|
|
27
|
+
// Imperative drag tracking (not reactive)
|
|
28
|
+
this.rafId = 0;
|
|
29
|
+
this.cleanupFn = null;
|
|
30
|
+
// Refs for latest values (captured in closure)
|
|
31
|
+
this.latestDropTargetIndex = null;
|
|
32
|
+
this.destroyRef.onDestroy(() => {
|
|
33
|
+
if (this.cleanupFn) {
|
|
34
|
+
this.cleanupFn();
|
|
35
|
+
this.cleanupFn = null;
|
|
36
|
+
}
|
|
37
|
+
if (this.rafId) {
|
|
38
|
+
cancelAnimationFrame(this.rafId);
|
|
39
|
+
this.rafId = 0;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Call this from the header cell's mousedown handler.
|
|
45
|
+
* @param columnId - The column being dragged
|
|
46
|
+
* @param event - The native MouseEvent
|
|
47
|
+
*/
|
|
48
|
+
handleHeaderMouseDown(columnId, event) {
|
|
49
|
+
if (!this.enabled())
|
|
50
|
+
return;
|
|
51
|
+
if (!this.onColumnOrderChange())
|
|
52
|
+
return;
|
|
53
|
+
// Gate on left-click only
|
|
54
|
+
if (event.button !== 0)
|
|
55
|
+
return;
|
|
56
|
+
// Skip if in resize handle zone (right 8px of the header cell)
|
|
57
|
+
const target = event.currentTarget;
|
|
58
|
+
const rect = target.getBoundingClientRect();
|
|
59
|
+
if (event.clientX > rect.right - RESIZE_HANDLE_ZONE)
|
|
60
|
+
return;
|
|
61
|
+
// Skip column groups — only reorder leaf columns
|
|
62
|
+
const cols = this.columns();
|
|
63
|
+
const colIndex = cols.findIndex((c) => c.columnId === columnId);
|
|
64
|
+
if (colIndex === -1)
|
|
65
|
+
return;
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
const startX = event.clientX;
|
|
68
|
+
let hasMoved = false;
|
|
69
|
+
this.latestDropTargetIndex = null;
|
|
70
|
+
// Lock text selection during drag
|
|
71
|
+
const prevUserSelect = document.body.style.userSelect;
|
|
72
|
+
document.body.style.userSelect = 'none';
|
|
73
|
+
const onMove = (moveEvent) => {
|
|
74
|
+
// Require a small minimum drag distance before activating
|
|
75
|
+
if (!hasMoved && Math.abs(moveEvent.clientX - startX) < 5)
|
|
76
|
+
return;
|
|
77
|
+
if (!hasMoved) {
|
|
78
|
+
hasMoved = true;
|
|
79
|
+
this.isDragging.set(true);
|
|
80
|
+
}
|
|
81
|
+
if (this.rafId)
|
|
82
|
+
cancelAnimationFrame(this.rafId);
|
|
83
|
+
this.rafId = requestAnimationFrame(() => {
|
|
84
|
+
this.rafId = 0;
|
|
85
|
+
const wrapper = this.wrapperEl();
|
|
86
|
+
if (!wrapper)
|
|
87
|
+
return;
|
|
88
|
+
const headerCells = wrapper.querySelectorAll('th[data-column-id]');
|
|
89
|
+
const rects = [];
|
|
90
|
+
for (let i = 0; i < headerCells.length; i++) {
|
|
91
|
+
const th = headerCells[i];
|
|
92
|
+
const id = th.getAttribute('data-column-id');
|
|
93
|
+
if (!id)
|
|
94
|
+
continue;
|
|
95
|
+
const thRect = th.getBoundingClientRect();
|
|
96
|
+
rects.push({
|
|
97
|
+
columnId: id,
|
|
98
|
+
left: thRect.left,
|
|
99
|
+
right: thRect.right,
|
|
100
|
+
centerX: thRect.left + thRect.width / 2,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const result = this.calculateDrop(columnId, moveEvent.clientX, rects);
|
|
104
|
+
this.latestDropTargetIndex = result.dropIndex;
|
|
105
|
+
this.dropIndicatorX.set(result.indicatorX);
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
const cleanup = () => {
|
|
109
|
+
window.removeEventListener('mousemove', onMove, true);
|
|
110
|
+
window.removeEventListener('mouseup', onUp, true);
|
|
111
|
+
this.cleanupFn = null;
|
|
112
|
+
// Restore user-select
|
|
113
|
+
document.body.style.userSelect = prevUserSelect;
|
|
114
|
+
// Cancel pending RAF
|
|
115
|
+
if (this.rafId) {
|
|
116
|
+
cancelAnimationFrame(this.rafId);
|
|
117
|
+
this.rafId = 0;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const onUp = () => {
|
|
121
|
+
cleanup();
|
|
122
|
+
if (hasMoved && this.latestDropTargetIndex != null) {
|
|
123
|
+
const currentOrder = this.columnOrder() ?? this.columns().map((c) => c.columnId);
|
|
124
|
+
const newOrder = reorderColumnArray(currentOrder, columnId, this.latestDropTargetIndex);
|
|
125
|
+
this.onColumnOrderChange()?.(newOrder);
|
|
126
|
+
}
|
|
127
|
+
this.isDragging.set(false);
|
|
128
|
+
this.dropIndicatorX.set(null);
|
|
129
|
+
};
|
|
130
|
+
window.addEventListener('mousemove', onMove, true);
|
|
131
|
+
window.addEventListener('mouseup', onUp, true);
|
|
132
|
+
this.cleanupFn = cleanup;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Calculate drop target from mouse position and header cell rects.
|
|
136
|
+
* Same logic as React's useColumnReorder inline calculation.
|
|
137
|
+
*/
|
|
138
|
+
calculateDrop(draggedColumnId, mouseX, rects) {
|
|
139
|
+
if (rects.length === 0) {
|
|
140
|
+
return { dropIndex: null, indicatorX: null };
|
|
141
|
+
}
|
|
142
|
+
const order = this.columnOrder() ?? this.columns().map((c) => c.columnId);
|
|
143
|
+
const currentIndex = order.indexOf(draggedColumnId);
|
|
144
|
+
// Find which column the mouse is closest to
|
|
145
|
+
let bestIndex = 0;
|
|
146
|
+
let indicatorX = null;
|
|
147
|
+
if (mouseX <= rects[0].centerX) {
|
|
148
|
+
// Before the first column
|
|
149
|
+
bestIndex = 0;
|
|
150
|
+
indicatorX = rects[0].left;
|
|
151
|
+
}
|
|
152
|
+
else if (mouseX >= rects[rects.length - 1].centerX) {
|
|
153
|
+
// After the last column
|
|
154
|
+
bestIndex = rects.length;
|
|
155
|
+
indicatorX = rects[rects.length - 1].right;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
for (let i = 0; i < rects.length - 1; i++) {
|
|
159
|
+
if (mouseX >= rects[i].centerX && mouseX < rects[i + 1].centerX) {
|
|
160
|
+
bestIndex = i + 1;
|
|
161
|
+
indicatorX = rects[i].right;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Map visual index back to order array index
|
|
167
|
+
const targetOrderIndex = bestIndex < rects.length
|
|
168
|
+
? order.indexOf(rects[bestIndex]?.columnId ?? '')
|
|
169
|
+
: order.length;
|
|
170
|
+
// Check if this is a no-op (dropping at same position)
|
|
171
|
+
if (currentIndex === targetOrderIndex || currentIndex + 1 === targetOrderIndex) {
|
|
172
|
+
return { dropIndex: targetOrderIndex, indicatorX: null };
|
|
173
|
+
}
|
|
174
|
+
return { dropIndex: targetOrderIndex, indicatorX };
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
ColumnReorderService = __decorate([
|
|
178
|
+
Injectable()
|
|
179
|
+
], ColumnReorderService);
|
|
180
|
+
export { ColumnReorderService };
|
|
@@ -51,8 +51,6 @@ let DataGridStateService = class DataGridStateService {
|
|
|
51
51
|
this.rafId = 0;
|
|
52
52
|
this.lastMousePos = null;
|
|
53
53
|
this.autoScrollInterval = null;
|
|
54
|
-
// Window event listeners cleanup
|
|
55
|
-
this.windowCleanups = [];
|
|
56
54
|
// ResizeObserver
|
|
57
55
|
this.resizeObserver = null;
|
|
58
56
|
// --- Derived computed ---
|
|
@@ -124,8 +122,10 @@ let DataGridStateService = class DataGridStateService {
|
|
|
124
122
|
});
|
|
125
123
|
this.visibleColumnCount = computed(() => this.visibleCols().length);
|
|
126
124
|
this.hasCheckboxCol = computed(() => (this.props()?.rowSelection ?? 'none') === 'multiple');
|
|
127
|
-
this.
|
|
128
|
-
this.
|
|
125
|
+
this.hasRowNumbersCol = computed(() => !!this.props()?.showRowNumbers);
|
|
126
|
+
this.specialColsCount = computed(() => (this.hasCheckboxCol() ? 1 : 0) + (this.hasRowNumbersCol() ? 1 : 0));
|
|
127
|
+
this.totalColCount = computed(() => this.visibleColumnCount() + this.specialColsCount());
|
|
128
|
+
this.colOffset = computed(() => this.specialColsCount());
|
|
129
129
|
this.rowIndexByRowId = computed(() => {
|
|
130
130
|
const p = this.props();
|
|
131
131
|
if (!p)
|
|
@@ -199,13 +199,16 @@ let DataGridStateService = class DataGridStateService {
|
|
|
199
199
|
return p.items.length === 0 && !!p.emptyState && !p.isLoading;
|
|
200
200
|
});
|
|
201
201
|
// Setup window event listeners for cell selection drag
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
window.
|
|
208
|
-
|
|
202
|
+
// Using effect with cleanup return to ensure proper removal on destroy
|
|
203
|
+
effect((onCleanup) => {
|
|
204
|
+
const onMove = (e) => this.onWindowMouseMove(e);
|
|
205
|
+
const onUp = () => this.onWindowMouseUp();
|
|
206
|
+
window.addEventListener('mousemove', onMove, true);
|
|
207
|
+
window.addEventListener('mouseup', onUp, true);
|
|
208
|
+
onCleanup(() => {
|
|
209
|
+
window.removeEventListener('mousemove', onMove, true);
|
|
210
|
+
window.removeEventListener('mouseup', onUp, true);
|
|
211
|
+
});
|
|
209
212
|
});
|
|
210
213
|
// Initialize column sizing overrides from initial widths
|
|
211
214
|
effect(() => {
|
|
@@ -240,7 +243,6 @@ let DataGridStateService = class DataGridStateService {
|
|
|
240
243
|
});
|
|
241
244
|
// Cleanup on destroy
|
|
242
245
|
this.destroyRef.onDestroy(() => {
|
|
243
|
-
this.windowCleanups.forEach((fn) => fn());
|
|
244
246
|
if (this.rafId)
|
|
245
247
|
cancelAnimationFrame(this.rafId);
|
|
246
248
|
if (this.autoScrollInterval)
|
|
@@ -947,6 +949,7 @@ let DataGridStateService = class DataGridStateService {
|
|
|
947
949
|
totalColCount: this.totalColCount(),
|
|
948
950
|
colOffset: this.colOffset(),
|
|
949
951
|
hasCheckboxCol: this.hasCheckboxCol(),
|
|
952
|
+
hasRowNumbersCol: this.hasRowNumbersCol(),
|
|
950
953
|
rowIndexByRowId: this.rowIndexByRowId(),
|
|
951
954
|
containerWidth: this.containerWidthSig(),
|
|
952
955
|
minTableWidth: this.minTableWidth(),
|
|
@@ -1257,6 +1260,9 @@ let DataGridStateService = class DataGridStateService {
|
|
|
1257
1260
|
this.endBatch();
|
|
1258
1261
|
}
|
|
1259
1262
|
this.fillDragStart = null;
|
|
1263
|
+
// Remove event listeners after mouseup completes
|
|
1264
|
+
window.removeEventListener('mousemove', onMove, true);
|
|
1265
|
+
window.removeEventListener('mouseup', onUp, true);
|
|
1260
1266
|
};
|
|
1261
1267
|
window.addEventListener('mousemove', onMove, true);
|
|
1262
1268
|
window.addEventListener('mouseup', onUp, true);
|
|
@@ -72,10 +72,10 @@ let OGridService = class OGridService {
|
|
|
72
72
|
this.internalData = signal([]);
|
|
73
73
|
this.internalLoading = signal(false);
|
|
74
74
|
this.internalPage = signal(1);
|
|
75
|
-
this.
|
|
76
|
-
this.
|
|
75
|
+
this.internalPageSizeOverride = signal(null);
|
|
76
|
+
this.internalSortOverride = signal(null);
|
|
77
77
|
this.internalFilters = signal({});
|
|
78
|
-
this.
|
|
78
|
+
this.internalVisibleColumnsOverride = signal(null);
|
|
79
79
|
this.internalSelectedRows = signal(new Set());
|
|
80
80
|
this.columnWidthOverrides = signal({});
|
|
81
81
|
this.pinnedOverrides = signal({});
|
|
@@ -91,10 +91,6 @@ let OGridService = class OGridService {
|
|
|
91
91
|
// Filter options state
|
|
92
92
|
this.serverFilterOptions = signal({});
|
|
93
93
|
this.loadingFilterOptions = signal({});
|
|
94
|
-
// --- Initialization ---
|
|
95
|
-
this.sortInitialized = false;
|
|
96
|
-
this.visibleColumnsInitialized = false;
|
|
97
|
-
this.pageSizeInitialized = false;
|
|
98
94
|
// --- Derived computed signals ---
|
|
99
95
|
this.columns = computed(() => flattenColumns(this.columnsProp()));
|
|
100
96
|
this.isServerSide = computed(() => this.dataSource() != null);
|
|
@@ -103,10 +99,23 @@ let OGridService = class OGridService {
|
|
|
103
99
|
this.displayLoading = computed(() => this.controlledLoading() ?? this.internalLoading());
|
|
104
100
|
this.defaultSortField = computed(() => this.defaultSortBy() ?? this.columns()[0]?.columnId ?? '');
|
|
105
101
|
this.page = computed(() => this.controlledPage() ?? this.internalPage());
|
|
106
|
-
this.pageSize = computed(() => this.controlledPageSize() ?? this.
|
|
107
|
-
this.sort = computed(() => this.controlledSort() ?? this.
|
|
102
|
+
this.pageSize = computed(() => this.controlledPageSize() ?? this.internalPageSizeOverride() ?? this.defaultPageSize());
|
|
103
|
+
this.sort = computed(() => this.controlledSort() ?? this.internalSortOverride() ?? {
|
|
104
|
+
field: this.defaultSortField(),
|
|
105
|
+
direction: this.defaultSortDirection(),
|
|
106
|
+
});
|
|
108
107
|
this.filters = computed(() => this.controlledFilters() ?? this.internalFilters());
|
|
109
|
-
this.visibleColumns = computed(() =>
|
|
108
|
+
this.visibleColumns = computed(() => {
|
|
109
|
+
if (this.controlledVisibleColumns())
|
|
110
|
+
return this.controlledVisibleColumns();
|
|
111
|
+
if (this.internalVisibleColumnsOverride())
|
|
112
|
+
return this.internalVisibleColumnsOverride();
|
|
113
|
+
const cols = this.columns();
|
|
114
|
+
if (cols.length === 0)
|
|
115
|
+
return new Set();
|
|
116
|
+
const visible = cols.filter((c) => c.defaultVisible !== false).map((c) => c.columnId);
|
|
117
|
+
return new Set(visible.length > 0 ? visible : cols.map((c) => c.columnId));
|
|
118
|
+
});
|
|
110
119
|
this.effectiveSelectedRows = computed(() => this.selectedRows() ?? this.internalSelectedRows());
|
|
111
120
|
this.columnChooserPlacement = computed(() => {
|
|
112
121
|
const prop = this.columnChooserProp();
|
|
@@ -281,30 +290,6 @@ let OGridService = class OGridService {
|
|
|
281
290
|
filterOptions: this.clientFilterOptions(),
|
|
282
291
|
};
|
|
283
292
|
});
|
|
284
|
-
// Initialize internal default values based on config
|
|
285
|
-
effect(() => {
|
|
286
|
-
if (!this.sortInitialized) {
|
|
287
|
-
this.sortInitialized = true;
|
|
288
|
-
this.internalSort.set({
|
|
289
|
-
field: this.defaultSortField(),
|
|
290
|
-
direction: this.defaultSortDirection(),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
effect(() => {
|
|
295
|
-
if (!this.pageSizeInitialized) {
|
|
296
|
-
this.pageSizeInitialized = true;
|
|
297
|
-
this.internalPageSize.set(this.defaultPageSize());
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
effect(() => {
|
|
301
|
-
if (!this.visibleColumnsInitialized && this.columns().length > 0) {
|
|
302
|
-
this.visibleColumnsInitialized = true;
|
|
303
|
-
const cols = this.columns();
|
|
304
|
-
const visible = cols.filter((c) => c.defaultVisible !== false).map((c) => c.columnId);
|
|
305
|
-
this.internalVisibleColumns.set(new Set(visible.length > 0 ? visible : cols.map((c) => c.columnId)));
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
293
|
// Server-side data fetching effect
|
|
309
294
|
effect(() => {
|
|
310
295
|
const ds = this.dataSource();
|
|
@@ -391,13 +376,13 @@ let OGridService = class OGridService {
|
|
|
391
376
|
}
|
|
392
377
|
setPageSize(size) {
|
|
393
378
|
if (this.controlledPageSize() === undefined)
|
|
394
|
-
this.
|
|
379
|
+
this.internalPageSizeOverride.set(size);
|
|
395
380
|
this.onPageSizeChange()?.(size);
|
|
396
381
|
this.setPage(1);
|
|
397
382
|
}
|
|
398
383
|
setSort(s) {
|
|
399
384
|
if (this.controlledSort() === undefined)
|
|
400
|
-
this.
|
|
385
|
+
this.internalSortOverride.set(s);
|
|
401
386
|
this.onSortChange()?.(s);
|
|
402
387
|
this.setPage(1);
|
|
403
388
|
}
|
|
@@ -409,7 +394,7 @@ let OGridService = class OGridService {
|
|
|
409
394
|
}
|
|
410
395
|
setVisibleColumns(cols) {
|
|
411
396
|
if (this.controlledVisibleColumns() === undefined)
|
|
412
|
-
this.
|
|
397
|
+
this.internalVisibleColumnsOverride.set(cols);
|
|
413
398
|
this.onVisibleColumnsChange()?.(cols);
|
|
414
399
|
}
|
|
415
400
|
handleSort(columnKey) {
|
|
@@ -544,6 +529,64 @@ let OGridService = class OGridService {
|
|
|
544
529
|
this.ariaLabelledBy.set(props['aria-labelledby']);
|
|
545
530
|
}
|
|
546
531
|
// --- API ---
|
|
532
|
+
// --- Column Pinning Methods ---
|
|
533
|
+
/**
|
|
534
|
+
* Pin a column to the left or right edge.
|
|
535
|
+
*/
|
|
536
|
+
pinColumn(columnId, side) {
|
|
537
|
+
this.pinnedOverrides.update((prev) => ({ ...prev, [columnId]: side }));
|
|
538
|
+
this.onColumnPinned()?.(columnId, side);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Unpin a column (remove sticky positioning).
|
|
542
|
+
*/
|
|
543
|
+
unpinColumn(columnId) {
|
|
544
|
+
this.pinnedOverrides.update((prev) => {
|
|
545
|
+
const next = { ...prev };
|
|
546
|
+
delete next[columnId];
|
|
547
|
+
return next;
|
|
548
|
+
});
|
|
549
|
+
this.onColumnPinned()?.(columnId, null);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Check if a column is pinned and which side.
|
|
553
|
+
*/
|
|
554
|
+
isPinned(columnId) {
|
|
555
|
+
return this.pinnedOverrides()[columnId];
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Compute sticky left offsets for left-pinned columns.
|
|
559
|
+
* Returns a map of columnId -> left offset in pixels.
|
|
560
|
+
*/
|
|
561
|
+
computeLeftOffsets(visibleCols, columnWidths, defaultWidth, hasCheckboxColumn, checkboxColumnWidth) {
|
|
562
|
+
const offsets = {};
|
|
563
|
+
const pinned = this.pinnedOverrides();
|
|
564
|
+
let left = hasCheckboxColumn ? checkboxColumnWidth : 0;
|
|
565
|
+
for (const col of visibleCols) {
|
|
566
|
+
if (pinned[col.columnId] === 'left') {
|
|
567
|
+
offsets[col.columnId] = left;
|
|
568
|
+
left += columnWidths[col.columnId] ?? defaultWidth;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return offsets;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Compute sticky right offsets for right-pinned columns.
|
|
575
|
+
* Returns a map of columnId -> right offset in pixels.
|
|
576
|
+
*/
|
|
577
|
+
computeRightOffsets(visibleCols, columnWidths, defaultWidth) {
|
|
578
|
+
const offsets = {};
|
|
579
|
+
const pinned = this.pinnedOverrides();
|
|
580
|
+
let right = 0;
|
|
581
|
+
for (let i = visibleCols.length - 1; i >= 0; i--) {
|
|
582
|
+
const col = visibleCols[i];
|
|
583
|
+
if (pinned[col.columnId] === 'right') {
|
|
584
|
+
offsets[col.columnId] = right;
|
|
585
|
+
right += columnWidths[col.columnId] ?? defaultWidth;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return offsets;
|
|
589
|
+
}
|
|
547
590
|
getApi() {
|
|
548
591
|
return {
|
|
549
592
|
setRowData: (d) => {
|
|
@@ -610,6 +653,14 @@ let OGridService = class OGridService {
|
|
|
610
653
|
this.refreshCounter.update((c) => c + 1);
|
|
611
654
|
}
|
|
612
655
|
},
|
|
656
|
+
getColumnOrder: () => this.columnOrder() ?? this.columns().map((c) => c.columnId),
|
|
657
|
+
setColumnOrder: (order) => {
|
|
658
|
+
this.onColumnOrderChange()?.(order);
|
|
659
|
+
},
|
|
660
|
+
scrollToRow: (_index, _options) => {
|
|
661
|
+
// Scrolling is handled by VirtualScrollService at the UI layer.
|
|
662
|
+
// The UI component should wire this to VirtualScrollService.scrollToRow().
|
|
663
|
+
},
|
|
613
664
|
};
|
|
614
665
|
}
|
|
615
666
|
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Injectable, signal, computed, DestroyRef, inject } from '@angular/core';
|
|
8
|
+
import { computeVisibleRange, computeTotalHeight, getScrollTopForRow, } from '@alaarab/ogrid-core';
|
|
9
|
+
/** Threshold below which virtual scrolling is a no-op (all rows rendered). */
|
|
10
|
+
const PASSTHROUGH_THRESHOLD = 100;
|
|
11
|
+
/**
|
|
12
|
+
* Manages virtual scrolling state using Angular signals.
|
|
13
|
+
* Port of React's useVirtualScroll hook.
|
|
14
|
+
*
|
|
15
|
+
* Uses core's pure-TS `computeVisibleRange` and `getScrollTopForRow` utilities.
|
|
16
|
+
* The UI layer (Angular Material / PrimeNG) provides the scrollable container
|
|
17
|
+
* and calls `onScroll()` / sets `containerHeight`.
|
|
18
|
+
*/
|
|
19
|
+
let VirtualScrollService = class VirtualScrollService {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.destroyRef = inject(DestroyRef);
|
|
22
|
+
// --- Input signals (set by consuming component) ---
|
|
23
|
+
this.totalRows = signal(0);
|
|
24
|
+
this.config = signal({ rowHeight: 36 });
|
|
25
|
+
this.containerHeight = signal(0);
|
|
26
|
+
// --- Internal state ---
|
|
27
|
+
this.scrollTop = signal(0);
|
|
28
|
+
// Scrollable container reference for programmatic scrolling
|
|
29
|
+
this.containerEl = null;
|
|
30
|
+
// --- Derived computed signals ---
|
|
31
|
+
this.rowHeight = computed(() => this.config().rowHeight);
|
|
32
|
+
this.overscan = computed(() => this.config().overscan ?? 5);
|
|
33
|
+
this.enabled = computed(() => this.config().enabled !== false);
|
|
34
|
+
/** Whether virtual scrolling is actually active (enabled + enough rows). */
|
|
35
|
+
this.isActive = computed(() => this.enabled() && this.totalRows() >= PASSTHROUGH_THRESHOLD);
|
|
36
|
+
/** The visible range of rows with spacer offsets. */
|
|
37
|
+
this.visibleRange = computed(() => {
|
|
38
|
+
if (!this.isActive()) {
|
|
39
|
+
// Passthrough: render all rows
|
|
40
|
+
return {
|
|
41
|
+
startIndex: 0,
|
|
42
|
+
endIndex: Math.max(0, this.totalRows() - 1),
|
|
43
|
+
offsetTop: 0,
|
|
44
|
+
offsetBottom: 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return computeVisibleRange(this.scrollTop(), this.rowHeight(), this.containerHeight(), this.totalRows(), this.overscan());
|
|
48
|
+
});
|
|
49
|
+
/** Total scrollable height in pixels. */
|
|
50
|
+
this.totalHeight = computed(() => computeTotalHeight(this.totalRows(), this.rowHeight()));
|
|
51
|
+
this.destroyRef.onDestroy(() => {
|
|
52
|
+
this.containerEl = null;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Set the scrollable container element.
|
|
57
|
+
* Used for programmatic scrolling (scrollToRow).
|
|
58
|
+
*/
|
|
59
|
+
setContainer(el) {
|
|
60
|
+
this.containerEl = el;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Call this from the container's scroll event handler.
|
|
64
|
+
*/
|
|
65
|
+
onScroll(event) {
|
|
66
|
+
const target = event.target;
|
|
67
|
+
this.scrollTop.set(target.scrollTop);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Scroll to a specific row index.
|
|
71
|
+
* @param index - The row index to scroll to.
|
|
72
|
+
* @param align - Where to position the row: 'start' (top), 'center', or 'end' (bottom). Default: 'start'.
|
|
73
|
+
*/
|
|
74
|
+
scrollToRow(index, align = 'start') {
|
|
75
|
+
const container = this.containerEl;
|
|
76
|
+
if (!container)
|
|
77
|
+
return;
|
|
78
|
+
const targetScrollTop = getScrollTopForRow(index, this.rowHeight(), this.containerHeight(), align);
|
|
79
|
+
container.scrollTo({ top: targetScrollTop, behavior: 'auto' });
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Update the virtual scroll configuration.
|
|
83
|
+
*/
|
|
84
|
+
updateConfig(updates) {
|
|
85
|
+
this.config.update((prev) => ({ ...prev, ...updates }));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
VirtualScrollService = __decorate([
|
|
89
|
+
Injectable()
|
|
90
|
+
], VirtualScrollService);
|
|
91
|
+
export { VirtualScrollService };
|