bways-grid 0.0.5

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.
Files changed (37) hide show
  1. package/README.md +76 -0
  2. package/fesm2022/bways-grid.mjs +3440 -0
  3. package/fesm2022/bways-grid.mjs.map +1 -0
  4. package/index.d.ts +5 -0
  5. package/lib/bways-grid.module.d.ts +13 -0
  6. package/lib/components/cell/cell.component.d.ts +20 -0
  7. package/lib/components/choose-columns/choose-columns.component.d.ts +17 -0
  8. package/lib/components/column-tool-panel/column-tool-panel.component.d.ts +36 -0
  9. package/lib/components/filter-tool-panel/filter-tool-panel.component.d.ts +63 -0
  10. package/lib/components/header/header.component.d.ts +42 -0
  11. package/lib/components/header-filter/header-filter.component.d.ts +31 -0
  12. package/lib/components/header-menu/header-menu.component.d.ts +41 -0
  13. package/lib/components/pagination/pagination.component.d.ts +22 -0
  14. package/lib/components/row/row.component.d.ts +19 -0
  15. package/lib/components/side-bar/side-bar.component.d.ts +42 -0
  16. package/lib/components/ultra-grid/ultra-grid.component.d.ts +155 -0
  17. package/lib/core/grid-engine.service.d.ts +14 -0
  18. package/lib/core/grid-flattener.service.d.ts +8 -0
  19. package/lib/core/row-cache.d.ts +10 -0
  20. package/lib/core/viewport-manager.d.ts +21 -0
  21. package/lib/datasources/infinite-scroll.datasource.d.ts +17 -0
  22. package/lib/datasources/server-datasource.interface.d.ts +14 -0
  23. package/lib/directives/column-resize.directive.d.ts +18 -0
  24. package/lib/models/column.model.d.ts +21 -0
  25. package/lib/models/csv-export.model.d.ts +12 -0
  26. package/lib/models/filter.model.d.ts +18 -0
  27. package/lib/models/grid-config.model.d.ts +11 -0
  28. package/lib/models/row.model.d.ts +26 -0
  29. package/lib/services/csv-export.service.d.ts +11 -0
  30. package/lib/workers/generated/export.worker.code.d.ts +1 -0
  31. package/lib/workers/generated/sorting.worker.code.d.ts +1 -0
  32. package/package.json +23 -0
  33. package/public-api.d.ts +19 -0
  34. package/src/lib/workers/export.worker.ts +110 -0
  35. package/src/lib/workers/generated/export.worker.code.ts +7 -0
  36. package/src/lib/workers/generated/sorting.worker.code.ts +4 -0
  37. package/src/lib/workers/sorting.worker.ts +423 -0
@@ -0,0 +1,423 @@
1
+ import { UltraGridNode, UltraGridLeafNode, UltraGridGroupNode, UltraGridRow, UltraGridGroupFooterNode } from '../models/row.model';
2
+
3
+ let cachedRows: UltraGridLeafNode[] = [];
4
+ let cachedResultTree: UltraGridNode[] = [];
5
+
6
+ addEventListener('message', (event: MessageEvent) => {
7
+ const { action, rows, sortModel, filterModel, conditionFilters, groupModel, aggregations, groupStateMap } = event.data;
8
+
9
+ if (action === 'INIT') {
10
+ const mappedRows = rows || [];
11
+ cachedRows = mappedRows.map((r: any) => ({
12
+ ...r,
13
+ type: 'leaf',
14
+ level: 0
15
+ }));
16
+ postMessage({ action: 'INIT_DONE' });
17
+ return;
18
+ }
19
+
20
+ if (action === 'FLATTEN_ONLY') {
21
+ const flatList = flattenTree(cachedResultTree, groupStateMap || {}, event.data.groupIncludeFooter);
22
+ postMessage({ flatList });
23
+ return;
24
+ }
25
+
26
+ if (action === 'EXECUTE') {
27
+ let processed = cachedRows;
28
+
29
+ // 1. Apply Filtering if present
30
+ if (filterModel && Object.keys(filterModel).length > 0) {
31
+ const filterSets: { [key: string]: Set<any> } = {};
32
+ for (const field of Object.keys(filterModel)) {
33
+ filterSets[field] = new Set(filterModel[field]);
34
+ }
35
+
36
+ processed = processed.filter((row: UltraGridLeafNode) => {
37
+ for (const field of Object.keys(filterSets)) {
38
+ const allowedValues = filterSets[field];
39
+ const cellValue = row.data ? row.data[field] : null;
40
+ if (!allowedValues.has(cellValue)) {
41
+ return false;
42
+ }
43
+ }
44
+ return true;
45
+ });
46
+ }
47
+
48
+ // 1b. Apply Condition Filters (Contains, Equals, Greater Than, etc.)
49
+ if (conditionFilters && Object.keys(conditionFilters).length > 0) {
50
+ processed = applyConditionFilters(processed, conditionFilters);
51
+ }
52
+
53
+ // 2. Apply Grouping and Aggregation if present
54
+ let resultTree: UltraGridNode[] = processed;
55
+ const aggs = aggregations || {};
56
+
57
+ if (groupModel && groupModel.length > 0) {
58
+ resultTree = buildTree(processed, groupModel, aggs);
59
+ } else {
60
+ processed.forEach(r => r.level = 0);
61
+ }
62
+
63
+ // 3. Apply Recursive Sorting
64
+ if (sortModel && sortModel.length > 0) {
65
+ sortTree(resultTree, sortModel);
66
+ }
67
+
68
+ // Cache the processed tree so toggle operations don't need to rebuild it
69
+ cachedResultTree = resultTree;
70
+
71
+ // 4. Compute Grand Total if grouping and footers are enabled
72
+ if (groupModel && groupModel.length > 0 && event.data.groupIncludeFooter && Object.keys(aggs).length > 0) {
73
+ const grandTotalData = computeAggregations(processed, aggs);
74
+ const grandTotalNode: UltraGridGroupFooterNode = {
75
+ id: 'grand-total-footer',
76
+ type: 'group-footer',
77
+ level: 0,
78
+ groupField: 'Grand Total',
79
+ groupKey: 'Grand Total',
80
+ data: grandTotalData
81
+ };
82
+ // Append grand total at the end of the root level resultTree
83
+ cachedResultTree.push(grandTotalNode as any); // Cast as any to bypass strict type check for now since GroupFooterNode is expected by flattener
84
+ }
85
+
86
+ // 5. Client-side Flattening
87
+ const flatList = flattenTree(cachedResultTree, groupStateMap || {}, event.data.groupIncludeFooter);
88
+
89
+ // Strip massive data payloads from leaf nodes before sending full tree back
90
+ const strippedTree = stripTreeData(cachedResultTree);
91
+
92
+ postMessage({ flatList, resultTree: strippedTree });
93
+ }
94
+ });
95
+
96
+ function stripTreeData(nodes: UltraGridNode[]): UltraGridNode[] {
97
+ return nodes.map(node => {
98
+ if (node.type === 'leaf') {
99
+ // Strip the heavy .data payload, the main thread will rehydrate it by ID
100
+ return {
101
+ id: node.id,
102
+ type: 'leaf',
103
+ level: node.level
104
+ // Notice: no .data
105
+ } as any;
106
+ } else if (node.type === 'group') {
107
+ const groupNode = node as UltraGridGroupNode;
108
+ return {
109
+ ...groupNode,
110
+ children: groupNode.children ? stripTreeData(groupNode.children) : undefined
111
+ } as any;
112
+ }
113
+ return node;
114
+ });
115
+ }
116
+
117
+ function flattenTree(nodes: UltraGridNode[], stateMap: { [key: string]: boolean }, groupIncludeFooter: boolean = false): any[] {
118
+ const result: any[] = [];
119
+
120
+ // Use an explicit stack to avoid recursion stack overflow with 200,000+ children
121
+ // Process in reverse order so items appear in correct order when popped
122
+ const stack: UltraGridNode[] = [];
123
+ for (let i = nodes.length - 1; i >= 0; i--) {
124
+ stack.push(nodes[i]);
125
+ }
126
+
127
+ while (stack.length > 0) {
128
+ const node = stack.pop()!;
129
+
130
+ if (node.type === 'group') {
131
+ const groupNode = node as UltraGridGroupNode;
132
+ // Send a lightweight group header — strip the heavy `.children` array
133
+ result.push({
134
+ id: groupNode.id,
135
+ type: 'group',
136
+ level: groupNode.level,
137
+ groupField: groupNode.groupField,
138
+ groupKey: groupNode.groupKey,
139
+ data: groupNode.data,
140
+ childCount: groupNode.children ? groupNode.children.length : 0
141
+ });
142
+
143
+ const isExpanded = !!stateMap[groupNode.id as string];
144
+ if (isExpanded) {
145
+ if (groupIncludeFooter) {
146
+ stack.push({
147
+ id: `${groupNode.id}-footer`,
148
+ type: 'group-footer',
149
+ level: groupNode.level,
150
+ groupField: groupNode.groupField,
151
+ groupKey: groupNode.groupKey,
152
+ data: { ...groupNode.data }
153
+ } as any);
154
+ }
155
+ if (groupNode.children) {
156
+ // Push children in reverse order so they come out in correct order
157
+ for (let i = groupNode.children.length - 1; i >= 0; i--) {
158
+ stack.push(groupNode.children[i]);
159
+ }
160
+ }
161
+ }
162
+ } else if (node.type === 'group-footer') {
163
+ result.push({
164
+ id: node.id,
165
+ type: 'group-footer',
166
+ level: node.level,
167
+ groupField: (node as any).groupField,
168
+ groupKey: (node as any).groupKey,
169
+ data: node.data
170
+ });
171
+ } else {
172
+ // CRITICAL PERFORMANCE: Send only a lightweight reference for leaf nodes.
173
+ // The main thread already has the full `.data` object cached in `_mappedRowData`.
174
+ // Sending 200,000 full data objects via postMessage would force V8 to deep-clone
175
+ // ~50MB of JSON, freezing the browser for 3-5 seconds.
176
+ result.push({
177
+ id: node.id,
178
+ type: 'leaf',
179
+ level: node.level
180
+ });
181
+ }
182
+ }
183
+
184
+ return result;
185
+ }
186
+
187
+ function buildTree(
188
+ leaves: UltraGridLeafNode[],
189
+ groupModel: string[],
190
+ aggregations: { [field: string]: string }
191
+ ): UltraGridGroupNode[] {
192
+ return groupLevel(leaves, groupModel, 0, aggregations, 'root');
193
+ }
194
+
195
+ function groupLevel(
196
+ nodes: (UltraGridLeafNode | UltraGridGroupNode)[],
197
+ groupModel: string[],
198
+ level: number,
199
+ aggregations: { [field: string]: string },
200
+ parentPath: string
201
+ ): UltraGridGroupNode[] {
202
+ const field = groupModel[level];
203
+ const isLastLevel = level === groupModel.length - 1;
204
+
205
+ const buckets = new Map<any, (UltraGridLeafNode | UltraGridGroupNode)[]>();
206
+
207
+ // 1. Bucket the nodes
208
+ for (const node of nodes) {
209
+ // Only bucket leaf nodes based on their data.
210
+ // If it's already a group node, we shouldn't be re-bucketing it unless we flatten first.
211
+ const key = node.data ? node.data[field] : null;
212
+
213
+ if (!buckets.has(key)) {
214
+ buckets.set(key, []);
215
+ }
216
+ buckets.get(key)!.push(node);
217
+ }
218
+
219
+ const result: UltraGridGroupNode[] = [];
220
+
221
+ // 2. Build group nodes from buckets
222
+ for (const [key, bucketNodes] of buckets.entries()) {
223
+ const groupId = `${parentPath}-${field}-${key}`;
224
+
225
+ const groupNode: UltraGridGroupNode = {
226
+ id: groupId,
227
+ type: 'group',
228
+ level: level,
229
+ groupField: field,
230
+ groupKey: key,
231
+ data: { [field]: key },
232
+ children: []
233
+ };
234
+
235
+ // Aggregation Step
236
+ const bucketAggData = computeAggregations(bucketNodes, aggregations);
237
+ Object.assign(groupNode.data, bucketAggData);
238
+
239
+ // Recursive children or direct assignment
240
+ if (!isLastLevel) {
241
+ groupNode.children = groupLevel(
242
+ bucketNodes,
243
+ groupModel,
244
+ level + 1,
245
+ aggregations,
246
+ groupId
247
+ );
248
+ } else {
249
+ // Assign leaf nodes, increment their level to sit below this group
250
+ bucketNodes.forEach(leaf => leaf.level = level + 1);
251
+ groupNode.children = bucketNodes;
252
+ }
253
+
254
+ result.push(groupNode);
255
+ }
256
+
257
+ return result;
258
+ }
259
+
260
+ function computeAggregations(
261
+ nodes: (UltraGridLeafNode | UltraGridGroupNode)[],
262
+ aggregations: { [field: string]: string }
263
+ ): any {
264
+ const resultData: any = {};
265
+
266
+ for (const aggField of Object.keys(aggregations)) {
267
+ const aggType = aggregations[aggField];
268
+
269
+ let aggValue = 0;
270
+ if (aggType === 'count') {
271
+ aggValue = nodes.length;
272
+ } else {
273
+ let first = true;
274
+ let sumForAvg = 0;
275
+ let countForAvg = 0;
276
+
277
+ for (const child of nodes) {
278
+ const val = child.data ? child.data[aggField] : 0;
279
+
280
+ // Robust numeric parsing: strip currency symbols, commas, spaces
281
+ let numVal = 0;
282
+ if (typeof val === 'number') {
283
+ numVal = val;
284
+ } else if (typeof val === 'string') {
285
+ numVal = Number(val.replace(/[^0-9.-]+/g, ""));
286
+ }
287
+
288
+ if (isNaN(numVal)) numVal = 0;
289
+
290
+ if (aggType === 'sum') {
291
+ aggValue += numVal;
292
+ } else if (aggType === 'min') {
293
+ if (first) aggValue = numVal;
294
+ else aggValue = Math.min(aggValue, numVal);
295
+ } else if (aggType === 'max') {
296
+ if (first) aggValue = numVal;
297
+ else aggValue = Math.max(aggValue, numVal);
298
+ } else if (aggType === 'avg') {
299
+ sumForAvg += numVal;
300
+ countForAvg++;
301
+ }
302
+ first = false;
303
+ }
304
+
305
+ if (aggType === 'avg') {
306
+ aggValue = countForAvg > 0 ? sumForAvg / countForAvg : 0;
307
+ }
308
+ }
309
+ resultData[aggField] = aggValue;
310
+ }
311
+ return resultData;
312
+ }
313
+
314
+ function sortTree(nodes: UltraGridNode[], sortModel: { field: string, direction: 'asc' | 'desc' }[]) {
315
+ nodes.sort((a, b) => {
316
+ for (const sort of sortModel) {
317
+ const { field, direction } = sort;
318
+ const valA = a.data ? a.data[field] : null;
319
+ const valB = b.data ? b.data[field] : null;
320
+
321
+ if (valA === valB) continue;
322
+
323
+ const comparison = valA > valB ? 1 : -1;
324
+ return direction === 'asc' ? comparison : -comparison;
325
+ }
326
+ return 0;
327
+ });
328
+
329
+ for (const node of nodes) {
330
+ if (node.type === 'group' && (node as UltraGridGroupNode).children) {
331
+ sortTree((node as UltraGridGroupNode).children, sortModel);
332
+ }
333
+ }
334
+ }
335
+
336
+ // --- Condition Filter Engine ---
337
+
338
+ function applyConditionFilters(
339
+ rows: UltraGridLeafNode[],
340
+ conditionFilters: { [field: string]: any }
341
+ ): UltraGridLeafNode[] {
342
+ const fields = Object.keys(conditionFilters);
343
+ if (fields.length === 0) return rows;
344
+
345
+ return rows.filter(row => {
346
+ for (const field of fields) {
347
+ const filter = conditionFilters[field];
348
+ if (!filter || !filter.condition1) continue;
349
+
350
+ const cellValue = row.data ? row.data[field] : null;
351
+ const result1 = evaluateCondition(cellValue, filter.condition1);
352
+
353
+ if (filter.condition2 && filter.condition2.type) {
354
+ const result2 = evaluateCondition(cellValue, filter.condition2);
355
+ const combined = filter.operator === 'OR'
356
+ ? (result1 || result2)
357
+ : (result1 && result2);
358
+ if (!combined) return false;
359
+ } else {
360
+ if (!result1) return false;
361
+ }
362
+ }
363
+ return true;
364
+ });
365
+ }
366
+
367
+ function evaluateCondition(cellValue: any, condition: { type: string; value?: string; valueTo?: string }): boolean {
368
+ const { type, value, valueTo } = condition;
369
+
370
+ // Blank / Not Blank work without a value
371
+ if (type === 'blank') {
372
+ return cellValue === null || cellValue === undefined || cellValue === '';
373
+ }
374
+ if (type === 'notBlank') {
375
+ return cellValue !== null && cellValue !== undefined && cellValue !== '';
376
+ }
377
+
378
+ // For all other types, if no filter value is entered, pass the row through
379
+ if (value === undefined || value === null || value === '') return true;
380
+
381
+ const cellStr = String(cellValue ?? '').toLowerCase();
382
+ const filterStr = String(value).toLowerCase();
383
+
384
+ switch (type) {
385
+ // --- Text conditions ---
386
+ case 'contains':
387
+ return cellStr.includes(filterStr);
388
+ case 'notContains':
389
+ return !cellStr.includes(filterStr);
390
+ case 'equals':
391
+ // Try numeric comparison first, fall back to string
392
+ if (!isNaN(Number(cellValue)) && !isNaN(Number(value))) {
393
+ return Number(cellValue) === Number(value);
394
+ }
395
+ return cellStr === filterStr;
396
+ case 'notEqual':
397
+ if (!isNaN(Number(cellValue)) && !isNaN(Number(value))) {
398
+ return Number(cellValue) !== Number(value);
399
+ }
400
+ return cellStr !== filterStr;
401
+ case 'startsWith':
402
+ return cellStr.startsWith(filterStr);
403
+ case 'endsWith':
404
+ return cellStr.endsWith(filterStr);
405
+
406
+ // --- Number conditions ---
407
+ case 'greaterThan':
408
+ return Number(cellValue) > Number(value);
409
+ case 'greaterThanOrEqual':
410
+ return Number(cellValue) >= Number(value);
411
+ case 'lessThan':
412
+ return Number(cellValue) < Number(value);
413
+ case 'lessThanOrEqual':
414
+ return Number(cellValue) <= Number(value);
415
+ case 'inRange':
416
+ if (valueTo === undefined || valueTo === null || valueTo === '') return true;
417
+ const num = Number(cellValue);
418
+ return num >= Number(value) && num <= Number(valueTo);
419
+
420
+ default:
421
+ return true;
422
+ }
423
+ }