@alaarab/ogrid-angular 2.0.2 → 2.0.3
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/index.js +3 -0
- package/dist/esm/services/column-reorder.service.js +180 -0
- package/dist/esm/services/datagrid-state.service.js +13 -10
- package/dist/esm/services/ogrid.service.js +30 -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/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 +0 -1
- package/dist/types/services/ogrid.service.d.ts +3 -6
- package/dist/types/services/virtual-scroll.service.d.ts +54 -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
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 ---
|
|
@@ -199,13 +197,16 @@ let DataGridStateService = class DataGridStateService {
|
|
|
199
197
|
return p.items.length === 0 && !!p.emptyState && !p.isLoading;
|
|
200
198
|
});
|
|
201
199
|
// Setup window event listeners for cell selection drag
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
window.
|
|
208
|
-
|
|
200
|
+
// Using effect with cleanup return to ensure proper removal on destroy
|
|
201
|
+
effect((onCleanup) => {
|
|
202
|
+
const onMove = (e) => this.onWindowMouseMove(e);
|
|
203
|
+
const onUp = () => this.onWindowMouseUp();
|
|
204
|
+
window.addEventListener('mousemove', onMove, true);
|
|
205
|
+
window.addEventListener('mouseup', onUp, true);
|
|
206
|
+
onCleanup(() => {
|
|
207
|
+
window.removeEventListener('mousemove', onMove, true);
|
|
208
|
+
window.removeEventListener('mouseup', onUp, true);
|
|
209
|
+
});
|
|
209
210
|
});
|
|
210
211
|
// Initialize column sizing overrides from initial widths
|
|
211
212
|
effect(() => {
|
|
@@ -240,7 +241,6 @@ let DataGridStateService = class DataGridStateService {
|
|
|
240
241
|
});
|
|
241
242
|
// Cleanup on destroy
|
|
242
243
|
this.destroyRef.onDestroy(() => {
|
|
243
|
-
this.windowCleanups.forEach((fn) => fn());
|
|
244
244
|
if (this.rafId)
|
|
245
245
|
cancelAnimationFrame(this.rafId);
|
|
246
246
|
if (this.autoScrollInterval)
|
|
@@ -1257,6 +1257,9 @@ let DataGridStateService = class DataGridStateService {
|
|
|
1257
1257
|
this.endBatch();
|
|
1258
1258
|
}
|
|
1259
1259
|
this.fillDragStart = null;
|
|
1260
|
+
// Remove event listeners after mouseup completes
|
|
1261
|
+
window.removeEventListener('mousemove', onMove, true);
|
|
1262
|
+
window.removeEventListener('mouseup', onUp, true);
|
|
1260
1263
|
};
|
|
1261
1264
|
window.addEventListener('mousemove', onMove, true);
|
|
1262
1265
|
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) {
|
|
@@ -610,6 +595,14 @@ let OGridService = class OGridService {
|
|
|
610
595
|
this.refreshCounter.update((c) => c + 1);
|
|
611
596
|
}
|
|
612
597
|
},
|
|
598
|
+
getColumnOrder: () => this.columnOrder() ?? this.columns().map((c) => c.columnId),
|
|
599
|
+
setColumnOrder: (order) => {
|
|
600
|
+
this.onColumnOrderChange()?.(order);
|
|
601
|
+
},
|
|
602
|
+
scrollToRow: (_index, _options) => {
|
|
603
|
+
// Scrolling is handled by VirtualScrollService at the UI layer.
|
|
604
|
+
// The UI component should wire this to VirtualScrollService.scrollToRow().
|
|
605
|
+
},
|
|
613
606
|
};
|
|
614
607
|
}
|
|
615
608
|
};
|
|
@@ -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 };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View model helpers for Angular DataGridTable. Core owns the logic; UI packages only render.
|
|
3
|
+
* Ported from React's dataGridViewModel.ts to eliminate duplication in Angular Material and PrimeNG packages.
|
|
4
|
+
*/
|
|
5
|
+
import { getCellValue, isInSelectionRange } from '@alaarab/ogrid-core';
|
|
6
|
+
/**
|
|
7
|
+
* Returns ColumnHeaderFilter props from column def and grid filter/sort state.
|
|
8
|
+
* Use in Angular Material and PrimeNG DataGridTableComponent instead of inline logic.
|
|
9
|
+
*/
|
|
10
|
+
export function getHeaderFilterConfig(col, input) {
|
|
11
|
+
const filterable = col.filterable && typeof col.filterable === 'object' ? col.filterable : null;
|
|
12
|
+
const filterType = (filterable?.type ?? 'none');
|
|
13
|
+
const filterField = filterable?.filterField ?? col.columnId;
|
|
14
|
+
const sortable = col.sortable !== false;
|
|
15
|
+
const filterValue = input.filters[filterField];
|
|
16
|
+
const base = {
|
|
17
|
+
columnKey: col.columnId,
|
|
18
|
+
columnName: col.name,
|
|
19
|
+
filterType,
|
|
20
|
+
isSorted: input.sortBy === col.columnId,
|
|
21
|
+
isSortedDescending: input.sortBy === col.columnId && input.sortDirection === 'desc',
|
|
22
|
+
onSort: sortable ? () => input.onColumnSort(col.columnId) : undefined,
|
|
23
|
+
};
|
|
24
|
+
if (filterType === 'text') {
|
|
25
|
+
return {
|
|
26
|
+
...base,
|
|
27
|
+
textValue: filterValue?.type === 'text' ? filterValue.value : '',
|
|
28
|
+
onTextChange: (v) => input.onFilterChange(filterField, v.trim() ? { type: 'text', value: v } : undefined),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (filterType === 'people') {
|
|
32
|
+
return {
|
|
33
|
+
...base,
|
|
34
|
+
selectedUser: filterValue?.type === 'people' ? filterValue.value : undefined,
|
|
35
|
+
onUserChange: (u) => input.onFilterChange(filterField, u ? { type: 'people', value: u } : undefined),
|
|
36
|
+
peopleSearch: input.peopleSearch,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (filterType === 'multiSelect') {
|
|
40
|
+
return {
|
|
41
|
+
...base,
|
|
42
|
+
options: input.filterOptions[filterField] ?? [],
|
|
43
|
+
isLoadingOptions: input.loadingFilterOptions[filterField] ?? false,
|
|
44
|
+
selectedValues: filterValue?.type === 'multiSelect' ? filterValue.value : [],
|
|
45
|
+
onFilterChange: (values) => input.onFilterChange(filterField, values.length ? { type: 'multiSelect', value: values } : undefined),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (filterType === 'date') {
|
|
49
|
+
return {
|
|
50
|
+
...base,
|
|
51
|
+
dateValue: filterValue?.type === 'date' ? filterValue.value : undefined,
|
|
52
|
+
onDateChange: (v) => input.onFilterChange(filterField, v ? { type: 'date', value: v } : undefined),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return base;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Returns a descriptor for rendering a cell. UI uses this to decide editing-inline vs editing-popover vs display
|
|
59
|
+
* and to apply isActive, isInRange, etc. without duplicating the boolean logic.
|
|
60
|
+
*/
|
|
61
|
+
export function getCellRenderDescriptor(item, col, rowIndex, colIdx, input) {
|
|
62
|
+
const rowId = input.getRowId(item);
|
|
63
|
+
const globalColIndex = colIdx + input.colOffset;
|
|
64
|
+
const colEditable = col.editable === true ||
|
|
65
|
+
(typeof col.editable === 'function' && col.editable(item));
|
|
66
|
+
const canEditInline = input.editable !== false &&
|
|
67
|
+
!!colEditable &&
|
|
68
|
+
!!input.onCellValueChanged &&
|
|
69
|
+
typeof col.cellEditor !== 'function';
|
|
70
|
+
const canEditPopup = input.editable !== false &&
|
|
71
|
+
!!colEditable &&
|
|
72
|
+
!!input.onCellValueChanged &&
|
|
73
|
+
typeof col.cellEditor === 'function';
|
|
74
|
+
const canEditAny = canEditInline || canEditPopup;
|
|
75
|
+
const isEditing = input.editingCell?.rowId === rowId &&
|
|
76
|
+
input.editingCell?.columnId === col.columnId;
|
|
77
|
+
const isActive = input.activeCell?.rowIndex === rowIndex &&
|
|
78
|
+
input.activeCell?.columnIndex === globalColIndex;
|
|
79
|
+
const isInRange = input.selectionRange != null &&
|
|
80
|
+
isInSelectionRange(input.selectionRange, rowIndex, colIdx);
|
|
81
|
+
const isInCutRange = input.cutRange != null &&
|
|
82
|
+
isInSelectionRange(input.cutRange, rowIndex, colIdx);
|
|
83
|
+
const isInCopyRange = input.copyRange != null &&
|
|
84
|
+
isInSelectionRange(input.copyRange, rowIndex, colIdx);
|
|
85
|
+
const isSelectionEndCell = !input.isDragging &&
|
|
86
|
+
input.copyRange == null &&
|
|
87
|
+
input.cutRange == null &&
|
|
88
|
+
input.selectionRange != null &&
|
|
89
|
+
rowIndex === input.selectionRange.endRow &&
|
|
90
|
+
colIdx === input.selectionRange.endCol;
|
|
91
|
+
let mode = 'display';
|
|
92
|
+
let editorType;
|
|
93
|
+
const value = getCellValue(item, col);
|
|
94
|
+
if (isEditing && canEditInline) {
|
|
95
|
+
mode = 'editing-inline';
|
|
96
|
+
if (col.cellEditor === 'text' ||
|
|
97
|
+
col.cellEditor === 'select' ||
|
|
98
|
+
col.cellEditor === 'checkbox' ||
|
|
99
|
+
col.cellEditor === 'richSelect' ||
|
|
100
|
+
col.cellEditor === 'date') {
|
|
101
|
+
editorType = col.cellEditor;
|
|
102
|
+
}
|
|
103
|
+
else if (col.type === 'date') {
|
|
104
|
+
editorType = 'date';
|
|
105
|
+
}
|
|
106
|
+
else if (col.type === 'boolean') {
|
|
107
|
+
editorType = 'checkbox';
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
editorType = 'text';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else if (isEditing && canEditPopup) {
|
|
114
|
+
mode = 'editing-popover';
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
mode,
|
|
118
|
+
editorType,
|
|
119
|
+
value,
|
|
120
|
+
isActive,
|
|
121
|
+
isInRange,
|
|
122
|
+
isInCutRange,
|
|
123
|
+
isInCopyRange,
|
|
124
|
+
isSelectionEndCell,
|
|
125
|
+
canEditAny,
|
|
126
|
+
globalColIndex,
|
|
127
|
+
rowId,
|
|
128
|
+
rowIndex,
|
|
129
|
+
displayValue: value,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// --- Cell rendering helpers (reduce DataGridTable view-layer duplication) ---
|
|
133
|
+
/**
|
|
134
|
+
* Resolves display content for a cell in display mode.
|
|
135
|
+
* Handles the renderCell → valueFormatter → String() fallback chain.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveCellDisplayContent(col, item, displayValue) {
|
|
138
|
+
if (col.renderCell && typeof col.renderCell === 'function') {
|
|
139
|
+
const result = col.renderCell(item);
|
|
140
|
+
return result != null ? String(result) : '';
|
|
141
|
+
}
|
|
142
|
+
if (col.valueFormatter)
|
|
143
|
+
return String(col.valueFormatter(displayValue, item) ?? '');
|
|
144
|
+
if (displayValue == null)
|
|
145
|
+
return '';
|
|
146
|
+
if (col.type === 'date') {
|
|
147
|
+
const d = new Date(String(displayValue));
|
|
148
|
+
if (!Number.isNaN(d.getTime()))
|
|
149
|
+
return d.toLocaleDateString();
|
|
150
|
+
}
|
|
151
|
+
if (col.type === 'boolean') {
|
|
152
|
+
return displayValue ? 'True' : 'False';
|
|
153
|
+
}
|
|
154
|
+
return String(displayValue);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolves the cellStyle from a column def, handling both function and static values.
|
|
158
|
+
*/
|
|
159
|
+
export function resolveCellStyle(col, item) {
|
|
160
|
+
if (!col.cellStyle)
|
|
161
|
+
return undefined;
|
|
162
|
+
return typeof col.cellStyle === 'function' ? col.cellStyle(item) : col.cellStyle;
|
|
163
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debounce utilities for Angular using signals.
|
|
3
|
+
* Provides functional parity with React's useDebounce and useDebouncedCallback.
|
|
4
|
+
*/
|
|
5
|
+
import { signal, effect } from '@angular/core';
|
|
6
|
+
/**
|
|
7
|
+
* Creates a debounced signal that updates after the specified delay when the source value changes.
|
|
8
|
+
*
|
|
9
|
+
* @param source - The signal to debounce
|
|
10
|
+
* @param delayMs - Delay in milliseconds
|
|
11
|
+
* @returns A signal containing the debounced value
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const searchQuery = signal('');
|
|
16
|
+
* const debouncedQuery = createDebouncedSignal(searchQuery, 300);
|
|
17
|
+
*
|
|
18
|
+
* effect(() => {
|
|
19
|
+
* console.log('Debounced search:', debouncedQuery());
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function createDebouncedSignal(source, delayMs) {
|
|
24
|
+
const debouncedValue = signal(source());
|
|
25
|
+
effect((onCleanup) => {
|
|
26
|
+
const currentValue = source();
|
|
27
|
+
const timeoutId = setTimeout(() => {
|
|
28
|
+
debouncedValue.set(currentValue);
|
|
29
|
+
}, delayMs);
|
|
30
|
+
onCleanup(() => clearTimeout(timeoutId));
|
|
31
|
+
});
|
|
32
|
+
return debouncedValue;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates a debounced function that delays invoking the provided function
|
|
36
|
+
* until after `delayMs` milliseconds have elapsed since the last time it was invoked.
|
|
37
|
+
*
|
|
38
|
+
* @param fn - The function to debounce
|
|
39
|
+
* @param delayMs - Delay in milliseconds
|
|
40
|
+
* @returns A debounced version of the function
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const saveData = (value: string) => {
|
|
45
|
+
* console.log('Saving:', value);
|
|
46
|
+
* };
|
|
47
|
+
*
|
|
48
|
+
* const debouncedSave = createDebouncedCallback(saveData, 500);
|
|
49
|
+
*
|
|
50
|
+
* // Multiple rapid calls will only trigger once after 500ms
|
|
51
|
+
* debouncedSave('hello');
|
|
52
|
+
* debouncedSave('world'); // Only this will execute after 500ms
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function createDebouncedCallback(fn, delayMs) {
|
|
56
|
+
let timeoutId = null;
|
|
57
|
+
return ((...args) => {
|
|
58
|
+
if (timeoutId !== null) {
|
|
59
|
+
clearTimeout(timeoutId);
|
|
60
|
+
}
|
|
61
|
+
timeoutId = setTimeout(() => {
|
|
62
|
+
fn(...args);
|
|
63
|
+
timeoutId = null;
|
|
64
|
+
}, delayMs);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Simple debounce function (non-Angular-specific, can be used anywhere).
|
|
69
|
+
* Returns a debounced version of the provided function.
|
|
70
|
+
*
|
|
71
|
+
* @param fn - The function to debounce
|
|
72
|
+
* @param delayMs - Delay in milliseconds
|
|
73
|
+
* @returns A debounced version of the function with a `cancel()` method
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const handleResize = debounce(() => {
|
|
78
|
+
* console.log('Window resized');
|
|
79
|
+
* }, 200);
|
|
80
|
+
*
|
|
81
|
+
* window.addEventListener('resize', handleResize);
|
|
82
|
+
*
|
|
83
|
+
* // Later, cancel pending execution
|
|
84
|
+
* handleResize.cancel();
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function debounce(fn, delayMs) {
|
|
88
|
+
let timeoutId = null;
|
|
89
|
+
const debounced = ((...args) => {
|
|
90
|
+
if (timeoutId !== null) {
|
|
91
|
+
clearTimeout(timeoutId);
|
|
92
|
+
}
|
|
93
|
+
timeoutId = setTimeout(() => {
|
|
94
|
+
fn(...args);
|
|
95
|
+
timeoutId = null;
|
|
96
|
+
}, delayMs);
|
|
97
|
+
});
|
|
98
|
+
debounced.cancel = () => {
|
|
99
|
+
if (timeoutId !== null) {
|
|
100
|
+
clearTimeout(timeoutId);
|
|
101
|
+
timeoutId = null;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
return debounced;
|
|
105
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for Angular DataGridTable view layer.
|
|
3
|
+
*/
|
|
4
|
+
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, } from './dataGridViewModel';
|
|
5
|
+
// Debounce utilities
|
|
6
|
+
export { createDebouncedSignal, createDebouncedCallback, debounce, } from './debounce';
|
|
7
|
+
// Latest ref utilities
|
|
8
|
+
export { createLatestRef, createLatestCallback, } from './latestRef';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Latest ref utility for Angular using signals.
|
|
3
|
+
* Provides functional parity with React's useLatestRef.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Creates a stable wrapper function that always calls the latest version of the provided function.
|
|
7
|
+
* Useful for event handlers and callbacks where you want a stable reference but need to call
|
|
8
|
+
* the latest implementation.
|
|
9
|
+
*
|
|
10
|
+
* @param fn - Signal containing the function to wrap
|
|
11
|
+
* @returns A stable function that always invokes the latest version
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* class MyService {
|
|
16
|
+
* readonly onSave = signal<(value: string) => void>((val) => console.log('Default:', val));
|
|
17
|
+
* readonly stableOnSave = createLatestCallback(this.onSave);
|
|
18
|
+
*
|
|
19
|
+
* constructor() {
|
|
20
|
+
* // Setup event listener with stable reference
|
|
21
|
+
* effect((onCleanup) => {
|
|
22
|
+
* // stableOnSave never changes, so this effect only runs once
|
|
23
|
+
* const callback = () => this.stableOnSave('data');
|
|
24
|
+
* window.addEventListener('click', callback);
|
|
25
|
+
* onCleanup(() => window.removeEventListener('click', callback));
|
|
26
|
+
* });
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* updateHandler(newFn: (value: string) => void) {
|
|
30
|
+
* // Even though we change the function, the callback reference stays stable
|
|
31
|
+
* this.onSave.set(newFn);
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function createLatestCallback(fn) {
|
|
37
|
+
// Return a stable function that always calls the current value of the signal
|
|
38
|
+
return ((...args) => {
|
|
39
|
+
return fn()(...args);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Alias for createLatestCallback for consistency with React/Vue naming.
|
|
44
|
+
* @deprecated Use createLatestCallback instead
|
|
45
|
+
*/
|
|
46
|
+
export const createLatestRef = createLatestCallback;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export { OGridService } from './services/ogrid.service';
|
|
|
7
7
|
export type { ColumnChooserPlacement, OGridPagination, OGridColumnChooser, OGridFilters, OGridSideBarState, } from './services/ogrid.service';
|
|
8
8
|
export { DataGridStateService } from './services/datagrid-state.service';
|
|
9
9
|
export type { DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, DataGridStateResult, } from './services/datagrid-state.service';
|
|
10
|
+
export { ColumnReorderService } from './services/column-reorder.service';
|
|
11
|
+
export { VirtualScrollService } from './services/virtual-scroll.service';
|
|
12
|
+
export type { IVirtualScrollConfig } from './services/virtual-scroll.service';
|
|
10
13
|
export { OGridLayoutComponent } from './components/ogrid-layout.component';
|
|
11
14
|
export { StatusBarComponent } from './components/status-bar.component';
|
|
12
15
|
export { GridContextMenuComponent } from './components/grid-context-menu.component';
|
|
@@ -14,3 +17,5 @@ export { SideBarComponent } from './components/sidebar.component';
|
|
|
14
17
|
export type { SideBarProps, SideBarFilterColumn } from './components/sidebar.component';
|
|
15
18
|
export { MarchingAntsOverlayComponent } from './components/marching-ants-overlay.component';
|
|
16
19
|
export { EmptyStateComponent } from './components/empty-state.component';
|
|
20
|
+
export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, } from './utils';
|
|
21
|
+
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, createDebouncedSignal, createDebouncedCallback, debounce, createLatestRef, createLatestCallback, } from './utils';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IColumnDef } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Manages column reorder drag interactions with RAF-throttled updates.
|
|
4
|
+
* Angular signals-based port of React's useColumnReorder hook.
|
|
5
|
+
*/
|
|
6
|
+
export declare class ColumnReorderService<T> {
|
|
7
|
+
private destroyRef;
|
|
8
|
+
readonly columns: import("@angular/core").WritableSignal<IColumnDef<T>[]>;
|
|
9
|
+
readonly columnOrder: import("@angular/core").WritableSignal<string[] | undefined>;
|
|
10
|
+
readonly onColumnOrderChange: import("@angular/core").WritableSignal<((order: string[]) => void) | undefined>;
|
|
11
|
+
readonly enabled: import("@angular/core").WritableSignal<boolean>;
|
|
12
|
+
readonly wrapperEl: import("@angular/core").WritableSignal<HTMLElement | null>;
|
|
13
|
+
readonly isDragging: import("@angular/core").WritableSignal<boolean>;
|
|
14
|
+
readonly dropIndicatorX: import("@angular/core").WritableSignal<number | null>;
|
|
15
|
+
private rafId;
|
|
16
|
+
private cleanupFn;
|
|
17
|
+
private latestDropTargetIndex;
|
|
18
|
+
constructor();
|
|
19
|
+
/**
|
|
20
|
+
* Call this from the header cell's mousedown handler.
|
|
21
|
+
* @param columnId - The column being dragged
|
|
22
|
+
* @param event - The native MouseEvent
|
|
23
|
+
*/
|
|
24
|
+
handleHeaderMouseDown(columnId: string, event: MouseEvent): void;
|
|
25
|
+
/**
|
|
26
|
+
* Calculate drop target from mouse position and header cell rects.
|
|
27
|
+
* Same logic as React's useColumnReorder inline calculation.
|
|
28
|
+
*/
|
|
29
|
+
private calculateDrop;
|
|
30
|
+
}
|
|
@@ -159,7 +159,6 @@ export declare class DataGridStateService<T> {
|
|
|
159
159
|
private rafId;
|
|
160
160
|
private lastMousePos;
|
|
161
161
|
private autoScrollInterval;
|
|
162
|
-
private windowCleanups;
|
|
163
162
|
private resizeObserver;
|
|
164
163
|
private readonly propsResolved;
|
|
165
164
|
readonly cellSelection: import("@angular/core").Signal<boolean>;
|
|
@@ -105,10 +105,10 @@ export declare class OGridService<T> {
|
|
|
105
105
|
private readonly internalData;
|
|
106
106
|
private readonly internalLoading;
|
|
107
107
|
private readonly internalPage;
|
|
108
|
-
private readonly
|
|
109
|
-
private readonly
|
|
108
|
+
private readonly internalPageSizeOverride;
|
|
109
|
+
private readonly internalSortOverride;
|
|
110
110
|
private readonly internalFilters;
|
|
111
|
-
private readonly
|
|
111
|
+
private readonly internalVisibleColumnsOverride;
|
|
112
112
|
private readonly internalSelectedRows;
|
|
113
113
|
private readonly columnWidthOverrides;
|
|
114
114
|
private readonly pinnedOverrides;
|
|
@@ -121,9 +121,6 @@ export declare class OGridService<T> {
|
|
|
121
121
|
private readonly sideBarActivePanel;
|
|
122
122
|
private readonly serverFilterOptions;
|
|
123
123
|
private readonly loadingFilterOptions;
|
|
124
|
-
private sortInitialized;
|
|
125
|
-
private visibleColumnsInitialized;
|
|
126
|
-
private pageSizeInitialized;
|
|
127
124
|
readonly columns: import("@angular/core").Signal<IColumnDef<T>[]>;
|
|
128
125
|
readonly isServerSide: import("@angular/core").Signal<boolean>;
|
|
129
126
|
readonly isClientSide: import("@angular/core").Signal<boolean>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { IVisibleRange } from '@alaarab/ogrid-core';
|
|
2
|
+
export interface IVirtualScrollConfig {
|
|
3
|
+
/** Enable virtual scrolling. Default: true when provided. */
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
/** Row height in pixels (required for virtualization). */
|
|
6
|
+
rowHeight: number;
|
|
7
|
+
/** Number of rows to render outside the visible area. Default: 5. */
|
|
8
|
+
overscan?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Manages virtual scrolling state using Angular signals.
|
|
12
|
+
* Port of React's useVirtualScroll hook.
|
|
13
|
+
*
|
|
14
|
+
* Uses core's pure-TS `computeVisibleRange` and `getScrollTopForRow` utilities.
|
|
15
|
+
* The UI layer (Angular Material / PrimeNG) provides the scrollable container
|
|
16
|
+
* and calls `onScroll()` / sets `containerHeight`.
|
|
17
|
+
*/
|
|
18
|
+
export declare class VirtualScrollService {
|
|
19
|
+
private destroyRef;
|
|
20
|
+
readonly totalRows: import("@angular/core").WritableSignal<number>;
|
|
21
|
+
readonly config: import("@angular/core").WritableSignal<IVirtualScrollConfig>;
|
|
22
|
+
readonly containerHeight: import("@angular/core").WritableSignal<number>;
|
|
23
|
+
readonly scrollTop: import("@angular/core").WritableSignal<number>;
|
|
24
|
+
private containerEl;
|
|
25
|
+
readonly rowHeight: import("@angular/core").Signal<number>;
|
|
26
|
+
readonly overscan: import("@angular/core").Signal<number>;
|
|
27
|
+
readonly enabled: import("@angular/core").Signal<boolean>;
|
|
28
|
+
/** Whether virtual scrolling is actually active (enabled + enough rows). */
|
|
29
|
+
readonly isActive: import("@angular/core").Signal<boolean>;
|
|
30
|
+
/** The visible range of rows with spacer offsets. */
|
|
31
|
+
readonly visibleRange: import("@angular/core").Signal<IVisibleRange>;
|
|
32
|
+
/** Total scrollable height in pixels. */
|
|
33
|
+
readonly totalHeight: import("@angular/core").Signal<number>;
|
|
34
|
+
constructor();
|
|
35
|
+
/**
|
|
36
|
+
* Set the scrollable container element.
|
|
37
|
+
* Used for programmatic scrolling (scrollToRow).
|
|
38
|
+
*/
|
|
39
|
+
setContainer(el: HTMLElement | null): void;
|
|
40
|
+
/**
|
|
41
|
+
* Call this from the container's scroll event handler.
|
|
42
|
+
*/
|
|
43
|
+
onScroll(event: Event): void;
|
|
44
|
+
/**
|
|
45
|
+
* Scroll to a specific row index.
|
|
46
|
+
* @param index - The row index to scroll to.
|
|
47
|
+
* @param align - Where to position the row: 'start' (top), 'center', or 'end' (bottom). Default: 'start'.
|
|
48
|
+
*/
|
|
49
|
+
scrollToRow(index: number, align?: 'start' | 'center' | 'end'): void;
|
|
50
|
+
/**
|
|
51
|
+
* Update the virtual scroll configuration.
|
|
52
|
+
*/
|
|
53
|
+
updateConfig(updates: Partial<IVirtualScrollConfig>): void;
|
|
54
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View model helpers for Angular DataGridTable. Core owns the logic; UI packages only render.
|
|
3
|
+
* Ported from React's dataGridViewModel.ts to eliminate duplication in Angular Material and PrimeNG packages.
|
|
4
|
+
*/
|
|
5
|
+
import type { ColumnFilterType, IDateFilterValue } from '../types/columnTypes';
|
|
6
|
+
import type { IColumnDef } from '../types/columnTypes';
|
|
7
|
+
import type { RowId, UserLike, IFilters, FilterValue } from '../types/dataGridTypes';
|
|
8
|
+
export interface HeaderFilterConfigInput {
|
|
9
|
+
sortBy?: string;
|
|
10
|
+
sortDirection: 'asc' | 'desc';
|
|
11
|
+
onColumnSort: (columnKey: string) => void;
|
|
12
|
+
filters: IFilters;
|
|
13
|
+
onFilterChange: (key: string, value: FilterValue | undefined) => void;
|
|
14
|
+
filterOptions: Record<string, string[]>;
|
|
15
|
+
loadingFilterOptions: Record<string, boolean>;
|
|
16
|
+
peopleSearch?: (query: string) => Promise<UserLike[]>;
|
|
17
|
+
}
|
|
18
|
+
/** Props to pass to ColumnHeaderFilter. */
|
|
19
|
+
export interface HeaderFilterConfig {
|
|
20
|
+
columnKey: string;
|
|
21
|
+
columnName: string;
|
|
22
|
+
filterType: ColumnFilterType;
|
|
23
|
+
isSorted?: boolean;
|
|
24
|
+
isSortedDescending?: boolean;
|
|
25
|
+
onSort?: () => void;
|
|
26
|
+
selectedValues?: string[];
|
|
27
|
+
onFilterChange?: (values: string[]) => void;
|
|
28
|
+
options?: string[];
|
|
29
|
+
isLoadingOptions?: boolean;
|
|
30
|
+
textValue?: string;
|
|
31
|
+
onTextChange?: (value: string) => void;
|
|
32
|
+
selectedUser?: UserLike;
|
|
33
|
+
onUserChange?: (user: UserLike | undefined) => void;
|
|
34
|
+
peopleSearch?: (query: string) => Promise<UserLike[]>;
|
|
35
|
+
dateValue?: IDateFilterValue;
|
|
36
|
+
onDateChange?: (value: IDateFilterValue | undefined) => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returns ColumnHeaderFilter props from column def and grid filter/sort state.
|
|
40
|
+
* Use in Angular Material and PrimeNG DataGridTableComponent instead of inline logic.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getHeaderFilterConfig<T>(col: IColumnDef<T>, input: HeaderFilterConfigInput): HeaderFilterConfig;
|
|
43
|
+
export type CellRenderMode = 'editing-inline' | 'editing-popover' | 'display';
|
|
44
|
+
export interface CellRenderDescriptorInput<T> {
|
|
45
|
+
editingCell: {
|
|
46
|
+
rowId: RowId;
|
|
47
|
+
columnId: string;
|
|
48
|
+
} | null;
|
|
49
|
+
activeCell: {
|
|
50
|
+
rowIndex: number;
|
|
51
|
+
columnIndex: number;
|
|
52
|
+
} | null;
|
|
53
|
+
selectionRange: {
|
|
54
|
+
startRow: number;
|
|
55
|
+
startCol: number;
|
|
56
|
+
endRow: number;
|
|
57
|
+
endCol: number;
|
|
58
|
+
} | null;
|
|
59
|
+
cutRange: {
|
|
60
|
+
startRow: number;
|
|
61
|
+
startCol: number;
|
|
62
|
+
endRow: number;
|
|
63
|
+
endCol: number;
|
|
64
|
+
} | null;
|
|
65
|
+
copyRange: {
|
|
66
|
+
startRow: number;
|
|
67
|
+
startCol: number;
|
|
68
|
+
endRow: number;
|
|
69
|
+
endCol: number;
|
|
70
|
+
} | null;
|
|
71
|
+
colOffset: number;
|
|
72
|
+
getRowId: (item: T) => RowId;
|
|
73
|
+
editable?: boolean;
|
|
74
|
+
onCellValueChanged?: unknown;
|
|
75
|
+
/** True while user is drag-selecting cells — hides fill handle during drag. */
|
|
76
|
+
isDragging?: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface CellRenderDescriptor {
|
|
79
|
+
mode: CellRenderMode;
|
|
80
|
+
editorType?: 'text' | 'select' | 'checkbox' | 'richSelect' | 'date';
|
|
81
|
+
value?: unknown;
|
|
82
|
+
isActive: boolean;
|
|
83
|
+
isInRange: boolean;
|
|
84
|
+
isInCutRange: boolean;
|
|
85
|
+
isInCopyRange: boolean;
|
|
86
|
+
isSelectionEndCell: boolean;
|
|
87
|
+
canEditAny: boolean;
|
|
88
|
+
globalColIndex: number;
|
|
89
|
+
rowId: RowId;
|
|
90
|
+
rowIndex: number;
|
|
91
|
+
/** Raw value for display (when mode === 'display'). */
|
|
92
|
+
displayValue?: unknown;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Returns a descriptor for rendering a cell. UI uses this to decide editing-inline vs editing-popover vs display
|
|
96
|
+
* and to apply isActive, isInRange, etc. without duplicating the boolean logic.
|
|
97
|
+
*/
|
|
98
|
+
export declare function getCellRenderDescriptor<T>(item: T, col: IColumnDef<T>, rowIndex: number, colIdx: number, input: CellRenderDescriptorInput<T>): CellRenderDescriptor;
|
|
99
|
+
/**
|
|
100
|
+
* Resolves display content for a cell in display mode.
|
|
101
|
+
* Handles the renderCell → valueFormatter → String() fallback chain.
|
|
102
|
+
*/
|
|
103
|
+
export declare function resolveCellDisplayContent<T>(col: IColumnDef<T>, item: T, displayValue: unknown): string;
|
|
104
|
+
/**
|
|
105
|
+
* Resolves the cellStyle from a column def, handling both function and static values.
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolveCellStyle<T>(col: IColumnDef<T>, item: T): Record<string, string> | undefined;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debounce utilities for Angular using signals.
|
|
3
|
+
* Provides functional parity with React's useDebounce and useDebouncedCallback.
|
|
4
|
+
*/
|
|
5
|
+
import { type Signal } from '@angular/core';
|
|
6
|
+
/**
|
|
7
|
+
* Creates a debounced signal that updates after the specified delay when the source value changes.
|
|
8
|
+
*
|
|
9
|
+
* @param source - The signal to debounce
|
|
10
|
+
* @param delayMs - Delay in milliseconds
|
|
11
|
+
* @returns A signal containing the debounced value
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const searchQuery = signal('');
|
|
16
|
+
* const debouncedQuery = createDebouncedSignal(searchQuery, 300);
|
|
17
|
+
*
|
|
18
|
+
* effect(() => {
|
|
19
|
+
* console.log('Debounced search:', debouncedQuery());
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function createDebouncedSignal<T>(source: Signal<T>, delayMs: number): Signal<T>;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a debounced function that delays invoking the provided function
|
|
26
|
+
* until after `delayMs` milliseconds have elapsed since the last time it was invoked.
|
|
27
|
+
*
|
|
28
|
+
* @param fn - The function to debounce
|
|
29
|
+
* @param delayMs - Delay in milliseconds
|
|
30
|
+
* @returns A debounced version of the function
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const saveData = (value: string) => {
|
|
35
|
+
* console.log('Saving:', value);
|
|
36
|
+
* };
|
|
37
|
+
*
|
|
38
|
+
* const debouncedSave = createDebouncedCallback(saveData, 500);
|
|
39
|
+
*
|
|
40
|
+
* // Multiple rapid calls will only trigger once after 500ms
|
|
41
|
+
* debouncedSave('hello');
|
|
42
|
+
* debouncedSave('world'); // Only this will execute after 500ms
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function createDebouncedCallback<T extends (...args: unknown[]) => void>(fn: T, delayMs: number): T;
|
|
46
|
+
/**
|
|
47
|
+
* Simple debounce function (non-Angular-specific, can be used anywhere).
|
|
48
|
+
* Returns a debounced version of the provided function.
|
|
49
|
+
*
|
|
50
|
+
* @param fn - The function to debounce
|
|
51
|
+
* @param delayMs - Delay in milliseconds
|
|
52
|
+
* @returns A debounced version of the function with a `cancel()` method
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* const handleResize = debounce(() => {
|
|
57
|
+
* console.log('Window resized');
|
|
58
|
+
* }, 200);
|
|
59
|
+
*
|
|
60
|
+
* window.addEventListener('resize', handleResize);
|
|
61
|
+
*
|
|
62
|
+
* // Later, cancel pending execution
|
|
63
|
+
* handleResize.cancel();
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function debounce<T extends (...args: unknown[]) => void>(fn: T, delayMs: number): T & {
|
|
67
|
+
cancel: () => void;
|
|
68
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for Angular DataGridTable view layer.
|
|
3
|
+
*/
|
|
4
|
+
export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, } from './dataGridViewModel';
|
|
5
|
+
export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, } from './dataGridViewModel';
|
|
6
|
+
export { createDebouncedSignal, createDebouncedCallback, debounce, } from './debounce';
|
|
7
|
+
export { createLatestRef, createLatestCallback, } from './latestRef';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Latest ref utility for Angular using signals.
|
|
3
|
+
* Provides functional parity with React's useLatestRef.
|
|
4
|
+
*/
|
|
5
|
+
import { type Signal } from '@angular/core';
|
|
6
|
+
/**
|
|
7
|
+
* Creates a stable wrapper function that always calls the latest version of the provided function.
|
|
8
|
+
* Useful for event handlers and callbacks where you want a stable reference but need to call
|
|
9
|
+
* the latest implementation.
|
|
10
|
+
*
|
|
11
|
+
* @param fn - Signal containing the function to wrap
|
|
12
|
+
* @returns A stable function that always invokes the latest version
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* class MyService {
|
|
17
|
+
* readonly onSave = signal<(value: string) => void>((val) => console.log('Default:', val));
|
|
18
|
+
* readonly stableOnSave = createLatestCallback(this.onSave);
|
|
19
|
+
*
|
|
20
|
+
* constructor() {
|
|
21
|
+
* // Setup event listener with stable reference
|
|
22
|
+
* effect((onCleanup) => {
|
|
23
|
+
* // stableOnSave never changes, so this effect only runs once
|
|
24
|
+
* const callback = () => this.stableOnSave('data');
|
|
25
|
+
* window.addEventListener('click', callback);
|
|
26
|
+
* onCleanup(() => window.removeEventListener('click', callback));
|
|
27
|
+
* });
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* updateHandler(newFn: (value: string) => void) {
|
|
31
|
+
* // Even though we change the function, the callback reference stays stable
|
|
32
|
+
* this.onSave.set(newFn);
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function createLatestCallback<T extends (...args: unknown[]) => unknown>(fn: Signal<T>): T;
|
|
38
|
+
/**
|
|
39
|
+
* Alias for createLatestCallback for consistency with React/Vue naming.
|
|
40
|
+
* @deprecated Use createLatestCallback instead
|
|
41
|
+
*/
|
|
42
|
+
export declare const createLatestRef: typeof createLatestCallback;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-angular",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "OGrid Angular – Angular services, signals, and headless components for OGrid data grids.",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"files": ["dist", "README.md", "LICENSE"],
|
|
23
23
|
"engines": { "node": ">=18" },
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@alaarab/ogrid-core": "2.0.
|
|
25
|
+
"@alaarab/ogrid-core": "2.0.3"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"@angular/core": "^21.0.0",
|