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.
- package/README.md +76 -0
- package/fesm2022/bways-grid.mjs +3440 -0
- package/fesm2022/bways-grid.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/bways-grid.module.d.ts +13 -0
- package/lib/components/cell/cell.component.d.ts +20 -0
- package/lib/components/choose-columns/choose-columns.component.d.ts +17 -0
- package/lib/components/column-tool-panel/column-tool-panel.component.d.ts +36 -0
- package/lib/components/filter-tool-panel/filter-tool-panel.component.d.ts +63 -0
- package/lib/components/header/header.component.d.ts +42 -0
- package/lib/components/header-filter/header-filter.component.d.ts +31 -0
- package/lib/components/header-menu/header-menu.component.d.ts +41 -0
- package/lib/components/pagination/pagination.component.d.ts +22 -0
- package/lib/components/row/row.component.d.ts +19 -0
- package/lib/components/side-bar/side-bar.component.d.ts +42 -0
- package/lib/components/ultra-grid/ultra-grid.component.d.ts +155 -0
- package/lib/core/grid-engine.service.d.ts +14 -0
- package/lib/core/grid-flattener.service.d.ts +8 -0
- package/lib/core/row-cache.d.ts +10 -0
- package/lib/core/viewport-manager.d.ts +21 -0
- package/lib/datasources/infinite-scroll.datasource.d.ts +17 -0
- package/lib/datasources/server-datasource.interface.d.ts +14 -0
- package/lib/directives/column-resize.directive.d.ts +18 -0
- package/lib/models/column.model.d.ts +21 -0
- package/lib/models/csv-export.model.d.ts +12 -0
- package/lib/models/filter.model.d.ts +18 -0
- package/lib/models/grid-config.model.d.ts +11 -0
- package/lib/models/row.model.d.ts +26 -0
- package/lib/services/csv-export.service.d.ts +11 -0
- package/lib/workers/generated/export.worker.code.d.ts +1 -0
- package/lib/workers/generated/sorting.worker.code.d.ts +1 -0
- package/package.json +23 -0
- package/public-api.d.ts +19 -0
- package/src/lib/workers/export.worker.ts +110 -0
- package/src/lib/workers/generated/export.worker.code.ts +7 -0
- package/src/lib/workers/generated/sorting.worker.code.ts +4 -0
- 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
|
+
}
|