ngx-column-filter-popup 1.0.0
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/DOCUMENTATION.md +690 -0
- package/LICENSE +21 -0
- package/README.md +451 -0
- package/USAGE_EXAMPLES.md +251 -0
- package/package.json +86 -0
- package/src/app/components/column-filter/column-filter.component.html +131 -0
- package/src/app/components/column-filter/column-filter.component.scss +426 -0
- package/src/app/components/column-filter/column-filter.component.ts +445 -0
- package/src/app/components/column-filter/column-filter.module.ts +31 -0
- package/src/app/lib/models/filter.models.ts +51 -0
- package/src/app/lib/public-api.ts +21 -0
- package/src/app/lib/services/column-filter.service.ts +35 -0
- package/src/app/lib/utils/column-filter.utils.ts +236 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { FilterRule, FilterConfig, GlobalMatchMode, FieldType } from '../models/filter.models';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apply column filter rules to a dataset
|
|
5
|
+
* @param data - Array of objects to filter
|
|
6
|
+
* @param columnKey - The property name to filter on
|
|
7
|
+
* @param filterConfig - The filter configuration containing rules
|
|
8
|
+
* @returns Filtered array
|
|
9
|
+
*/
|
|
10
|
+
export function applyColumnFilter<T extends Record<string, any>>(
|
|
11
|
+
data: T[],
|
|
12
|
+
columnKey: string,
|
|
13
|
+
filterConfig: FilterConfig | null | undefined
|
|
14
|
+
): T[] {
|
|
15
|
+
if (!filterConfig || !filterConfig.rules || filterConfig.rules.length === 0) {
|
|
16
|
+
return data;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return data.filter(item => itemMatchesFilter(item, columnKey, filterConfig));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a single item matches the filter rules
|
|
24
|
+
* @param item - The item to check
|
|
25
|
+
* @param columnKey - The property name to filter on
|
|
26
|
+
* @param filterConfig - The filter configuration containing rules
|
|
27
|
+
* @returns true if the item matches the filter
|
|
28
|
+
*/
|
|
29
|
+
export function itemMatchesFilter<T extends Record<string, any>>(
|
|
30
|
+
item: T,
|
|
31
|
+
columnKey: string,
|
|
32
|
+
filterConfig: FilterConfig
|
|
33
|
+
): boolean {
|
|
34
|
+
if (!filterConfig || !filterConfig.rules || filterConfig.rules.length === 0) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const value = item[columnKey];
|
|
39
|
+
if (value === null || value === undefined) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Filter out empty rules
|
|
44
|
+
const validRules = filterConfig.rules.filter(rule => {
|
|
45
|
+
if (filterConfig.fieldType === 'status') {
|
|
46
|
+
return rule.value !== '';
|
|
47
|
+
}
|
|
48
|
+
return rule.value.trim() !== '';
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (validRules.length === 0) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fieldType = filterConfig.fieldType || 'text';
|
|
56
|
+
|
|
57
|
+
// Check if rule matches based on field type
|
|
58
|
+
const checkRuleMatch = (rule: FilterRule): boolean => {
|
|
59
|
+
if (fieldType === 'currency') {
|
|
60
|
+
return checkCurrencyMatch(value, rule);
|
|
61
|
+
} else if (fieldType === 'age') {
|
|
62
|
+
return checkAgeMatch(value, rule);
|
|
63
|
+
} else if (fieldType === 'date') {
|
|
64
|
+
return checkDateMatch(value, rule);
|
|
65
|
+
} else if (fieldType === 'status') {
|
|
66
|
+
return checkStatusMatch(value, rule);
|
|
67
|
+
} else {
|
|
68
|
+
return checkTextMatch(value, rule);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Get global match mode (default to 'match-any-rule' for backward compatibility)
|
|
73
|
+
const globalMatchMode: GlobalMatchMode = filterConfig.globalMatchMode || 'match-any-rule';
|
|
74
|
+
|
|
75
|
+
// Apply global match mode
|
|
76
|
+
if (globalMatchMode === 'match-all-rules') {
|
|
77
|
+
// ALL rules must match (AND logic)
|
|
78
|
+
return validRules.every(rule => checkRuleMatch(rule));
|
|
79
|
+
} else {
|
|
80
|
+
// ANY rule can match (OR logic)
|
|
81
|
+
return validRules.some(rule => checkRuleMatch(rule));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check text match based on match type
|
|
87
|
+
*/
|
|
88
|
+
function checkTextMatch(value: any, rule: FilterRule): boolean {
|
|
89
|
+
const stringValue = String(value).toLowerCase();
|
|
90
|
+
const ruleValue = rule.value.trim().toLowerCase();
|
|
91
|
+
|
|
92
|
+
switch (rule.matchType) {
|
|
93
|
+
case 'match-all':
|
|
94
|
+
const words = ruleValue.split(/\s+/).filter(w => w.length > 0);
|
|
95
|
+
return words.length > 0 && words.every(word => stringValue.includes(word));
|
|
96
|
+
|
|
97
|
+
case 'match-any':
|
|
98
|
+
const anyWords = ruleValue.split(/\s+/).filter(w => w.length > 0);
|
|
99
|
+
return anyWords.length > 0 && anyWords.some(word => stringValue.includes(word));
|
|
100
|
+
|
|
101
|
+
case 'starts-with':
|
|
102
|
+
return stringValue.startsWith(ruleValue);
|
|
103
|
+
|
|
104
|
+
case 'ends-with':
|
|
105
|
+
return stringValue.endsWith(ruleValue);
|
|
106
|
+
|
|
107
|
+
case 'contains':
|
|
108
|
+
return stringValue.includes(ruleValue);
|
|
109
|
+
|
|
110
|
+
case 'equals':
|
|
111
|
+
return stringValue === ruleValue;
|
|
112
|
+
|
|
113
|
+
default:
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check currency/number match
|
|
120
|
+
*/
|
|
121
|
+
function checkCurrencyMatch(value: any, rule: FilterRule): boolean {
|
|
122
|
+
// Parse currency values (remove currency symbols and commas)
|
|
123
|
+
const numValue = parseFloat(String(value).replace(/[^0-9.-]/g, '')) || 0;
|
|
124
|
+
const ruleValue = parseFloat(rule.value.replace(/[^0-9.-]/g, '')) || 0;
|
|
125
|
+
|
|
126
|
+
if (isNaN(numValue) || isNaN(ruleValue)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
switch (rule.matchType) {
|
|
131
|
+
case 'equals':
|
|
132
|
+
return numValue === ruleValue;
|
|
133
|
+
|
|
134
|
+
case 'greater-than':
|
|
135
|
+
return numValue > ruleValue;
|
|
136
|
+
|
|
137
|
+
case 'less-than':
|
|
138
|
+
return numValue < ruleValue;
|
|
139
|
+
|
|
140
|
+
case 'greater-equal':
|
|
141
|
+
return numValue >= ruleValue;
|
|
142
|
+
|
|
143
|
+
case 'less-equal':
|
|
144
|
+
return numValue <= ruleValue;
|
|
145
|
+
|
|
146
|
+
default:
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check age/number match (same logic as currency but simpler parsing)
|
|
153
|
+
*/
|
|
154
|
+
function checkAgeMatch(value: any, rule: FilterRule): boolean {
|
|
155
|
+
// Parse number values
|
|
156
|
+
const numValue = parseFloat(String(value)) || 0;
|
|
157
|
+
const ruleValue = parseFloat(rule.value) || 0;
|
|
158
|
+
|
|
159
|
+
if (isNaN(numValue) || isNaN(ruleValue)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
switch (rule.matchType) {
|
|
164
|
+
case 'equals':
|
|
165
|
+
return numValue === ruleValue;
|
|
166
|
+
|
|
167
|
+
case 'greater-than':
|
|
168
|
+
return numValue > ruleValue;
|
|
169
|
+
|
|
170
|
+
case 'less-than':
|
|
171
|
+
return numValue < ruleValue;
|
|
172
|
+
|
|
173
|
+
case 'greater-equal':
|
|
174
|
+
return numValue >= ruleValue;
|
|
175
|
+
|
|
176
|
+
case 'less-equal':
|
|
177
|
+
return numValue <= ruleValue;
|
|
178
|
+
|
|
179
|
+
default:
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check date match
|
|
186
|
+
*/
|
|
187
|
+
function checkDateMatch(value: any, rule: FilterRule): boolean {
|
|
188
|
+
const dateValue = new Date(value);
|
|
189
|
+
const ruleDate = new Date(rule.value);
|
|
190
|
+
|
|
191
|
+
if (isNaN(dateValue.getTime()) || isNaN(ruleDate.getTime())) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Normalize dates to midnight for comparison
|
|
196
|
+
const normalizedValue = new Date(dateValue.getFullYear(), dateValue.getMonth(), dateValue.getDate());
|
|
197
|
+
const normalizedRule = new Date(ruleDate.getFullYear(), ruleDate.getMonth(), ruleDate.getDate());
|
|
198
|
+
|
|
199
|
+
switch (rule.matchType) {
|
|
200
|
+
case 'is':
|
|
201
|
+
case 'is-on':
|
|
202
|
+
return normalizedValue.getTime() === normalizedRule.getTime();
|
|
203
|
+
|
|
204
|
+
case 'is-not':
|
|
205
|
+
return normalizedValue.getTime() !== normalizedRule.getTime();
|
|
206
|
+
|
|
207
|
+
case 'is-before':
|
|
208
|
+
return normalizedValue.getTime() < normalizedRule.getTime();
|
|
209
|
+
|
|
210
|
+
case 'is-after':
|
|
211
|
+
return normalizedValue.getTime() > normalizedRule.getTime();
|
|
212
|
+
|
|
213
|
+
default:
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check status match
|
|
220
|
+
*/
|
|
221
|
+
function checkStatusMatch(value: any, rule: FilterRule): boolean {
|
|
222
|
+
const stringValue = String(value).toLowerCase().trim();
|
|
223
|
+
const ruleValue = rule.value.toLowerCase().trim();
|
|
224
|
+
|
|
225
|
+
switch (rule.matchType) {
|
|
226
|
+
case 'is':
|
|
227
|
+
case 'equals':
|
|
228
|
+
return stringValue === ruleValue;
|
|
229
|
+
|
|
230
|
+
case 'is-not':
|
|
231
|
+
return stringValue !== ruleValue;
|
|
232
|
+
|
|
233
|
+
default:
|
|
234
|
+
return stringValue === ruleValue;
|
|
235
|
+
}
|
|
236
|
+
}
|