@open-dcp/grid 0.0.1 → 0.2.17
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/.github/workflows/main.yml +42 -0
- package/CLA.md +22 -0
- package/LICENSE +190 -0
- package/NOTICE.md +31 -0
- package/README.md +39 -39
- package/SECURITY.md +27 -0
- package/dist/GridService.d.ts +83 -0
- package/dist/GridService.js +410 -0
- package/dist/__tests__/GridServices.test.d.ts +1 -0
- package/dist/__tests__/GridServices.test.js +288 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/services/FilteringService.d.ts +34 -0
- package/dist/services/FilteringService.js +278 -0
- package/dist/services/FormattingService.d.ts +17 -0
- package/dist/services/FormattingService.js +179 -0
- package/dist/services/RowHeightService.d.ts +15 -0
- package/dist/services/RowHeightService.js +63 -0
- package/dist/services/SelectionService.d.ts +14 -0
- package/dist/services/SelectionService.js +55 -0
- package/dist/services/SortingService.d.ts +15 -0
- package/dist/services/SortingService.js +82 -0
- package/dist/services/__tests__/FilteringService.test.d.ts +1 -0
- package/dist/services/__tests__/FilteringService.test.js +140 -0
- package/dist/services/__tests__/FormattingService.test.d.ts +1 -0
- package/dist/services/__tests__/FormattingService.test.js +122 -0
- package/dist/types/grid.d.ts +71 -0
- package/dist/types/grid.js +1 -0
- package/dist/types/sorting.d.ts +8 -0
- package/dist/types/sorting.js +1 -0
- package/jest.config.js +16 -0
- package/package.json +40 -9
- package/reports/junit.xml +3 -0
- package/src/GridService.ts +561 -0
- package/src/__tests__/GridServices.test.ts +348 -0
- package/src/index.ts +17 -0
- package/src/services/FilteringService.ts +347 -0
- package/src/services/FormattingService.ts +205 -0
- package/src/services/RowHeightService.ts +81 -0
- package/src/services/SelectionService.ts +71 -0
- package/src/services/SortingService.ts +105 -0
- package/src/services/__tests__/FilteringService.test.ts +173 -0
- package/src/services/__tests__/FormattingService.test.ts +157 -0
- package/src/types/grid.ts +84 -0
- package/src/types/sorting.ts +9 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { BehaviorSubject, Observable, skip, combineLatest, map } from 'rxjs';
|
|
2
|
+
import { isEqual } from 'lodash-es';
|
|
3
|
+
import { distinctUntilChanged } from 'rxjs/operators';
|
|
4
|
+
import {
|
|
5
|
+
GridState,
|
|
6
|
+
Column,
|
|
7
|
+
Row,
|
|
8
|
+
RenderRow,
|
|
9
|
+
DisplayFormat,
|
|
10
|
+
DisplayValue,
|
|
11
|
+
GridOptions,
|
|
12
|
+
FilterSortUpdatePayload,
|
|
13
|
+
GlobalSearchOptions,
|
|
14
|
+
GlobalSearchState,
|
|
15
|
+
} from './types/grid';
|
|
16
|
+
import { FilterNode } from '@open-dcp/filter';
|
|
17
|
+
import { logger } from '@open-dcp/utilities';
|
|
18
|
+
import { SortingCondition, ApiModeConfig } from './types/sorting';
|
|
19
|
+
|
|
20
|
+
import { RowHeightService } from './services/RowHeightService';
|
|
21
|
+
import { SortingService } from './services/SortingService';
|
|
22
|
+
import { FormattingService } from './services/FormattingService';
|
|
23
|
+
import { FilteringService } from './services/FilteringService';
|
|
24
|
+
import { SelectionService } from './services/SelectionService';
|
|
25
|
+
|
|
26
|
+
const DEFAULT_DISPLAY_FORMAT: Required<DisplayFormat> = {
|
|
27
|
+
timeZone: "UTC",
|
|
28
|
+
dateFormat: "yyyy-MM-dd",
|
|
29
|
+
timeFormat: "HH:mm:ss",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export class GridService {
|
|
33
|
+
public readonly elementHeight: number;
|
|
34
|
+
public readonly gridOptions: Partial<GridOptions>;
|
|
35
|
+
|
|
36
|
+
private readonly rowsSubject: BehaviorSubject<Row[]>;
|
|
37
|
+
private readonly columnsSubject: BehaviorSubject<Column[]>;
|
|
38
|
+
private readonly stateSubject: BehaviorSubject<GridState>;
|
|
39
|
+
|
|
40
|
+
public displayFormatSubject: BehaviorSubject<DisplayFormat>;
|
|
41
|
+
public displayFormat$: Observable<DisplayFormat>;
|
|
42
|
+
public rows$: Observable<Row[]>;
|
|
43
|
+
public rowCount$: Observable<number>;
|
|
44
|
+
public columns$: Observable<Column[]>;
|
|
45
|
+
private container: HTMLElement | null = null; // Reference to the scrolling container
|
|
46
|
+
public startIndex: number = 0;
|
|
47
|
+
private firstRowHeight: number = 0;
|
|
48
|
+
private lastRowHeight: number = 0;
|
|
49
|
+
|
|
50
|
+
private apiMode: boolean = false;
|
|
51
|
+
private clientSideSort: boolean = false;
|
|
52
|
+
private clientSideFilter: boolean = false;
|
|
53
|
+
|
|
54
|
+
private readonly rowHeightService: RowHeightService;
|
|
55
|
+
private readonly sortingService: SortingService;
|
|
56
|
+
private readonly formattingService: FormattingService;
|
|
57
|
+
private readonly filteringService: FilteringService;
|
|
58
|
+
private readonly selectionService: SelectionService;
|
|
59
|
+
|
|
60
|
+
private filterSortSubject: BehaviorSubject<FilterSortUpdatePayload>;
|
|
61
|
+
public filterSort$: Observable<FilterSortUpdatePayload>;
|
|
62
|
+
|
|
63
|
+
private indexMapping: Map<number, number> = new Map();
|
|
64
|
+
|
|
65
|
+
constructor(rows: Row[], columns: Column[], gridOptions: Partial<GridOptions> = {}, sorts: SortingCondition[] = [], filterTree: FilterNode | null = null, selections: Set<number> = new Set()) {
|
|
66
|
+
|
|
67
|
+
this.gridOptions = gridOptions;
|
|
68
|
+
|
|
69
|
+
this.filterSortSubject = new BehaviorSubject<FilterSortUpdatePayload>({ filterTree, sorts, globalSearch: null });
|
|
70
|
+
this.filterSort$ = this.filterSortSubject.asObservable();
|
|
71
|
+
|
|
72
|
+
this.displayFormatSubject = new BehaviorSubject<DisplayFormat>({
|
|
73
|
+
...DEFAULT_DISPLAY_FORMAT,
|
|
74
|
+
...gridOptions.displayFormat
|
|
75
|
+
});
|
|
76
|
+
this.displayFormat$ = this.displayFormatSubject.asObservable();
|
|
77
|
+
|
|
78
|
+
// Data independend services
|
|
79
|
+
this.formattingService = new FormattingService(this);
|
|
80
|
+
this.rowHeightService = new RowHeightService(this);
|
|
81
|
+
|
|
82
|
+
// Set Column
|
|
83
|
+
this.columnsSubject = new BehaviorSubject(columns);
|
|
84
|
+
this.columns$ = this.columnsSubject.asObservable();
|
|
85
|
+
|
|
86
|
+
// Format Rows
|
|
87
|
+
let initalRows = rows.map(row => this.formattingService.applyFormattingToRow(row));
|
|
88
|
+
this.rowsSubject = new BehaviorSubject(initalRows);
|
|
89
|
+
this.rows$ = this.rowsSubject.asObservable();
|
|
90
|
+
this.indexMapping = new Map([...Array(initalRows.length).keys()].map(i => [i, i]))
|
|
91
|
+
|
|
92
|
+
// Row Count Observable
|
|
93
|
+
this.rowCount$ = combineLatest([
|
|
94
|
+
this.rows$,
|
|
95
|
+
this.filterSort$,
|
|
96
|
+
this.columns$
|
|
97
|
+
]).pipe(
|
|
98
|
+
map(() => this.rowCount()),
|
|
99
|
+
distinctUntilChanged()
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Init Data Services
|
|
103
|
+
this.filteringService = new FilteringService(this, filterTree);
|
|
104
|
+
this.sortingService = new SortingService(this, sorts);
|
|
105
|
+
this.selectionService = new SelectionService(selections, gridOptions.multiSelect ?? true);
|
|
106
|
+
|
|
107
|
+
this.elementHeight = gridOptions.elementHeight ?? 42;
|
|
108
|
+
|
|
109
|
+
this.stateSubject = new BehaviorSubject<GridState>({
|
|
110
|
+
startIndex: 0,
|
|
111
|
+
step: 1,
|
|
112
|
+
firstRowHeight: 0,
|
|
113
|
+
lastRowHeight: 0,
|
|
114
|
+
renderRows: []
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.rows$.pipe(
|
|
118
|
+
skip(1),
|
|
119
|
+
distinctUntilChanged((prev, curr) => isEqual(curr, prev))
|
|
120
|
+
).subscribe(() => {
|
|
121
|
+
if (this.apiMode) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.updateRenderedRows()
|
|
126
|
+
this.rowHeightService.initRowHeights(this.rowCount())
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
this.columns$.pipe(
|
|
130
|
+
distinctUntilChanged((prev, curr) => isEqual(curr, prev))
|
|
131
|
+
).subscribe(() => {
|
|
132
|
+
if (this.getRows().length > 0) {
|
|
133
|
+
this.setRows(this.rowsSubject.getValue());
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
this.displayFormat$.subscribe(() => {
|
|
138
|
+
this.updateRenderedRows();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// API Support
|
|
143
|
+
private notifyFilterSortUpdate() {
|
|
144
|
+
const filterTree = this.filteringService.getFilterTree();
|
|
145
|
+
const sorts = this.sortingService.getSorts();
|
|
146
|
+
const globalSearch = this.filteringService.getGlobalSearch();
|
|
147
|
+
|
|
148
|
+
// Always emit to observable
|
|
149
|
+
this.filterSortSubject.next({
|
|
150
|
+
filterTree,
|
|
151
|
+
sorts,
|
|
152
|
+
globalSearch,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (this.apiMode && !this.clientSideSort && !this.clientSideFilter) {
|
|
156
|
+
// In API mode or with client-side operations, do not update locally
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.updateRenderedRows();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public enableApiMode(config: ApiModeConfig = {}): void {
|
|
164
|
+
logger.log('DcpGrid: api mode active');
|
|
165
|
+
this.apiMode = true;
|
|
166
|
+
this.clientSideSort = config.clientSideSort ?? false;
|
|
167
|
+
this.clientSideFilter = config.clientSideFilter ?? false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public disableApiMode(): void {
|
|
171
|
+
this.apiMode = false;
|
|
172
|
+
this.clientSideSort = false;
|
|
173
|
+
this.clientSideFilter = false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public shouldApplyClientSideFiltering(): boolean {
|
|
177
|
+
return !this.apiMode || this.clientSideFilter;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public setRowsFromApi(rows: Row[]) {
|
|
181
|
+
if (!this.apiMode) {
|
|
182
|
+
logger.warn('DcpGrid: setRowsFromApi should only be called in API mode');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Format the rows first!
|
|
187
|
+
const formattedRows = rows.map(row => this.formattingService.applyFormattingToRow(row));
|
|
188
|
+
|
|
189
|
+
// Set the formatted rows
|
|
190
|
+
this.rowsSubject.next(formattedRows);
|
|
191
|
+
|
|
192
|
+
// Reset index mapping - it will be recalculated based on client-side operations
|
|
193
|
+
this.indexMapping = new Map([...Array(formattedRows.length).keys()].map(i => [i, i]));
|
|
194
|
+
|
|
195
|
+
this.rowHeightService.initRowHeights(formattedRows.length);
|
|
196
|
+
this.updateRenderedRows();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Index Mapping
|
|
200
|
+
clearIndexMap(): void {
|
|
201
|
+
this.indexMapping.clear();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
setIndexMapping(newMap: Map<number, number>): void {
|
|
205
|
+
this.indexMapping = newMap
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
setIndexMappingValue(newIndex: number, originalIndex: number): void {
|
|
209
|
+
this.indexMapping.set(newIndex, originalIndex)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getOriginalIndex(index: number): number {
|
|
213
|
+
const originalIndex = this.indexMapping.get(index);
|
|
214
|
+
|
|
215
|
+
if (originalIndex === undefined) {
|
|
216
|
+
throw new Error(`Original index in rows not found for: ${index}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return originalIndex;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getIndexMapping(): Map<number, number> {
|
|
223
|
+
return this.indexMapping;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Others
|
|
227
|
+
updateDisplayFormat(newFormat: Partial<DisplayFormat>) {
|
|
228
|
+
this.displayFormatSubject.next({
|
|
229
|
+
...this.displayFormatSubject.getValue(),
|
|
230
|
+
...newFormat
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const globalSearch = this.filteringService.getGlobalSearch();
|
|
234
|
+
if (globalSearch?.options?.searchMode === 'display') {
|
|
235
|
+
this.filteringService.updateFilterIndexesForUpdatedData();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
attachContainer(container: HTMLElement) {
|
|
240
|
+
this.container = container;
|
|
241
|
+
this.rowHeightService.recalculateHeights();
|
|
242
|
+
this.updateRenderedRows();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getContainer(): HTMLElement | null {
|
|
246
|
+
return this.container;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
setStateValue(key: string, newValue: any): void {
|
|
250
|
+
this.stateSubject.next({
|
|
251
|
+
...this.stateSubject.value,
|
|
252
|
+
[key]: newValue
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
getState(): Observable<GridState> {
|
|
257
|
+
return this.stateSubject.asObservable();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
getFirstRowHeight(): number {
|
|
261
|
+
return this.firstRowHeight;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
getLastRowHeight(): number {
|
|
265
|
+
return this.lastRowHeight;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
getRenderRows() {
|
|
269
|
+
return this.stateSubject.getValue().renderRows;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
getRows() {
|
|
273
|
+
return this.rowsSubject.getValue();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
getCellDisplayValue(row: Row, columnKey: string | number): string {
|
|
277
|
+
const column = this.columnsSubject.getValue().find((col) => col.key === columnKey);
|
|
278
|
+
|
|
279
|
+
if (!column) {
|
|
280
|
+
return '';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const displayValue = this.formattingService.formatCellDisplay(row, column).display;
|
|
284
|
+
return displayValue === null || displayValue === undefined ? '' : String(displayValue);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
getFilteredRows() {
|
|
288
|
+
if (this.apiMode && !this.clientSideFilter) {
|
|
289
|
+
// In API mode without client-side filtering, return rows as-is (already filtered by backend)
|
|
290
|
+
return this.getRows();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Apply client-side filtering
|
|
294
|
+
return this.filteringService.applyFilter(this.getRows());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
getFilteredAndSortRows() {
|
|
298
|
+
const filteredRows = this.getFilteredRows();
|
|
299
|
+
|
|
300
|
+
if (this.apiMode && !this.clientSideSort) {
|
|
301
|
+
// In API mode without client-side sorting, return rows as-is (already sorted by backend)
|
|
302
|
+
return filteredRows;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Apply client-side sorting
|
|
306
|
+
return this.sortingService.applySorting(filteredRows);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
rowCount(): number {
|
|
310
|
+
let totalRows = this.rowsSubject.getValue().length;
|
|
311
|
+
|
|
312
|
+
if (this.apiMode && !this.clientSideFilter) {
|
|
313
|
+
// In API mode without client-side filtering, use total rows
|
|
314
|
+
return totalRows;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Use filtered count
|
|
318
|
+
let filterIndexes = this.filteringService.getFilterIndexes()
|
|
319
|
+
|
|
320
|
+
if (filterIndexes === null) {
|
|
321
|
+
return totalRows;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return filterIndexes.length;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
setRows(rows: Row[], shouldFormat: boolean = true): void {
|
|
328
|
+
const updatedRows = shouldFormat
|
|
329
|
+
? rows.map(row => this.formattingService.applyFormattingToRow(row))
|
|
330
|
+
: rows;
|
|
331
|
+
|
|
332
|
+
this.indexMapping = new Map([...Array(rows.length).keys()].map(i => [i, i]))
|
|
333
|
+
|
|
334
|
+
this.rowsSubject.next(updatedRows);
|
|
335
|
+
|
|
336
|
+
this.filteringService.updateFilterIndexesForUpdatedData();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
getColumns() {
|
|
340
|
+
return this.columnsSubject.getValue();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
setColumns(columns: Column[]): void {
|
|
344
|
+
this.columnsSubject.next(columns);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// SortingService
|
|
348
|
+
getSortCondition(columnKey: string | number): SortingCondition | null {
|
|
349
|
+
let matchingConditions = this.sortingService.getSortCondition(columnKey)
|
|
350
|
+
if (matchingConditions.length == 0) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return matchingConditions[0];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
toggleSort(columnKey: string | number) {
|
|
358
|
+
this.sortingService.toggleSort(columnKey);
|
|
359
|
+
this.notifyFilterSortUpdate();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// FilteringService
|
|
363
|
+
addFilter(filterNode: FilterNode): void {
|
|
364
|
+
this.filteringService.addFilter(filterNode);
|
|
365
|
+
this.sortingService.resetSorting();
|
|
366
|
+
|
|
367
|
+
this.notifyFilterSortUpdate();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
reset(): void {
|
|
371
|
+
this.filteringService.resetFilters();
|
|
372
|
+
this.sortingService.resetSorting();
|
|
373
|
+
this.notifyFilterSortUpdate();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
resetFilter(columnKey: string | number): void {
|
|
377
|
+
this.filteringService.resetFilter(columnKey);
|
|
378
|
+
this.sortingService.resetSorting();
|
|
379
|
+
this.notifyFilterSortUpdate();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
setGlobalSearch(query: string, options?: GlobalSearchOptions): void {
|
|
383
|
+
this.filteringService.setGlobalSearch(query, options);
|
|
384
|
+
this.notifyFilterSortUpdate();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
clearGlobalSearch(): void {
|
|
388
|
+
this.filteringService.clearGlobalSearch();
|
|
389
|
+
this.notifyFilterSortUpdate();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
getGlobalSearch(): GlobalSearchState | null {
|
|
393
|
+
return this.filteringService.getGlobalSearch();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
getFilterIndexes(): number[] | null {
|
|
397
|
+
return this.filteringService.getFilterIndexes();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
hasFilter(columnKey: string | number): boolean {
|
|
401
|
+
return this.filteringService.hasFilter(columnKey);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// SelectionService
|
|
405
|
+
getSelected() {
|
|
406
|
+
return this.selectionService.selected$;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
selectRow(index: number) {
|
|
410
|
+
this.selectionService.selectRow(index);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
deselectRow(index: number) {
|
|
414
|
+
this.selectionService.deselectRow(index);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
toggleRowSelection(index: number) {
|
|
418
|
+
this.selectionService.toggleRowSelection(index);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
selectAll() {
|
|
422
|
+
const rowsOfInterest = this.getRowsOfInterest();
|
|
423
|
+
const indices = rowsOfInterest.map((_, i) => i);
|
|
424
|
+
this.selectionService.selectAll(indices);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
shiftSelect(startIndex: number, endIndex: number) {
|
|
428
|
+
this.selectionService.shiftSelect(startIndex, endIndex);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
isSelected(index: number): boolean {
|
|
432
|
+
return this.selectionService.isSelected(index);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
deselectAll() {
|
|
436
|
+
this.selectionService.deselectAll();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
clearSelection() {
|
|
440
|
+
this.selectionService.clearSelection();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private getDisplayValue(row: Row, column: Column): DisplayValue {
|
|
444
|
+
return this.formattingService.formatCellDisplay(row, column);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private getRowsOfInterest(): Row[] {
|
|
448
|
+
if (this.apiMode) {
|
|
449
|
+
// API mode: apply only client-side operations
|
|
450
|
+
if (this.clientSideFilter && this.clientSideSort) {
|
|
451
|
+
return this.getFilteredAndSortRows();
|
|
452
|
+
} else if (this.clientSideFilter) {
|
|
453
|
+
return this.getFilteredRows();
|
|
454
|
+
} else if (this.clientSideSort) {
|
|
455
|
+
return this.sortingService.applySorting(this.getRows());
|
|
456
|
+
} else {
|
|
457
|
+
// Pure API mode - no client-side operations
|
|
458
|
+
return this.getRows();
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
// Normal mode - always apply both
|
|
462
|
+
return this.getFilteredAndSortRows();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private rebuildIndexMapping(processedRows: Row[]): void {
|
|
467
|
+
const originalRows = this.getRows();
|
|
468
|
+
const newMapping = new Map<number, number>();
|
|
469
|
+
|
|
470
|
+
processedRows.forEach((processedRow, newIdx) => {
|
|
471
|
+
const originalIdx = originalRows.indexOf(processedRow);
|
|
472
|
+
if (originalIdx !== -1) {
|
|
473
|
+
newMapping.set(newIdx, originalIdx);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
this.indexMapping = newMapping;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
updateRenderedRows() {
|
|
481
|
+
if (!this.container) return;
|
|
482
|
+
|
|
483
|
+
const scrollTop = this.getScrollHeight() ?? 0;
|
|
484
|
+
|
|
485
|
+
// Get the appropriate rows based on mode and settings
|
|
486
|
+
const rowsOfInterest = this.getRowsOfInterest();
|
|
487
|
+
|
|
488
|
+
// Rebuild index mapping ONLY if client-side operations were applied in API mode
|
|
489
|
+
if (this.apiMode && (this.clientSideFilter || this.clientSideSort)) {
|
|
490
|
+
this.rebuildIndexMapping(rowsOfInterest);
|
|
491
|
+
} else if (this.apiMode && !this.clientSideFilter && !this.clientSideSort) {
|
|
492
|
+
// Pure API mode - ensure 1:1 mapping
|
|
493
|
+
const rows = this.getRows();
|
|
494
|
+
this.indexMapping = new Map([...Array(rows.length).keys()].map(i => [i, i]));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
this.rowHeightService.recalculateHeights();
|
|
498
|
+
|
|
499
|
+
const scrollBottom = scrollTop + this.container.clientHeight;
|
|
500
|
+
this.startIndex = this.rowHeightService.findStartIndex(scrollTop);
|
|
501
|
+
const endIndex = this.rowHeightService.findEndIndex(scrollBottom);
|
|
502
|
+
|
|
503
|
+
// Correct firstRowHeight using cumulative heights
|
|
504
|
+
this.firstRowHeight = this.startIndex > 0 && rowsOfInterest.length > 0 ? this.rowHeightService.getHeightCache()[this.startIndex - 1] : 0;
|
|
505
|
+
|
|
506
|
+
// Calculate the actual visible height
|
|
507
|
+
const totalHeight = this.rowHeightService.getHeightCache()[this.rowHeightService.getHeightCache().length - 1] || 0;
|
|
508
|
+
const visibleHeight = this.rowHeightService.getHeightCache()[endIndex] - this.firstRowHeight;
|
|
509
|
+
|
|
510
|
+
// Ensure lastRowHeight is computed dynamically
|
|
511
|
+
this.lastRowHeight = rowsOfInterest.length > 0 ? Math.max(0, totalHeight - (this.firstRowHeight + visibleHeight)) : 0;
|
|
512
|
+
|
|
513
|
+
// Slice visible rows
|
|
514
|
+
const visibleRows = rowsOfInterest.slice(this.startIndex, endIndex + 1);
|
|
515
|
+
|
|
516
|
+
// Apply formatting
|
|
517
|
+
const formattedRenderRows: RenderRow[] = visibleRows.map((row: Row, index: number) => {
|
|
518
|
+
const currentIndex = this.startIndex + index;
|
|
519
|
+
|
|
520
|
+
// In pure API mode (no client-side operations), use currentIndex directly
|
|
521
|
+
const originalIndex = (this.apiMode && !this.clientSideFilter && !this.clientSideSort)
|
|
522
|
+
? currentIndex
|
|
523
|
+
: this.indexMapping.get(currentIndex) ?? currentIndex;
|
|
524
|
+
|
|
525
|
+
const formattedRow: RenderRow = {
|
|
526
|
+
globaIndex: originalIndex,
|
|
527
|
+
data: row
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
this.columnsSubject.getValue().forEach((column: Column) => {
|
|
531
|
+
const cellValue = formattedRow.data[column.key];
|
|
532
|
+
if (cellValue && typeof cellValue === 'object' && cellValue.raw !== undefined) {
|
|
533
|
+
const displayValue = this.getDisplayValue(row, column);
|
|
534
|
+
formattedRow.data[column.key].display = displayValue.display;
|
|
535
|
+
formattedRow.data[column.key].renderedElement = displayValue.renderedElement;
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
return formattedRow;
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Update state
|
|
542
|
+
this.stateSubject.next({
|
|
543
|
+
startIndex: this.startIndex,
|
|
544
|
+
step: endIndex - this.startIndex,
|
|
545
|
+
firstRowHeight: this.firstRowHeight,
|
|
546
|
+
lastRowHeight: this.lastRowHeight,
|
|
547
|
+
renderRows: formattedRenderRows
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
requestAnimationFrame(() => this.rowHeightService.updateRowHeights());
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
handleScroll(event: Event) {
|
|
554
|
+
if (!this.container) return;
|
|
555
|
+
this.updateRenderedRows();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
getScrollHeight(): number | undefined {
|
|
559
|
+
return this.container?.scrollTop;
|
|
560
|
+
}
|
|
561
|
+
}
|