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,445 @@
|
|
|
1
|
+
import { Component, EventEmitter, Input, Output, ElementRef, HostListener, OnInit, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
|
2
|
+
import { FormsModule } from '@angular/forms';
|
|
3
|
+
import { CommonModule } from '@angular/common';
|
|
4
|
+
import { Subscription } from 'rxjs';
|
|
5
|
+
import { FilterRule, FilterConfig, MatchType, MatchTypeOption, GlobalMatchMode, FieldType } from '../../lib/models/filter.models';
|
|
6
|
+
import { ColumnFilterService } from '../../lib/services/column-filter.service';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'lib-column-filter',
|
|
10
|
+
standalone: true,
|
|
11
|
+
imports: [CommonModule, FormsModule],
|
|
12
|
+
templateUrl: './column-filter.component.html',
|
|
13
|
+
styleUrls: ['./column-filter.component.scss']
|
|
14
|
+
})
|
|
15
|
+
export class ColumnFilterComponent implements OnInit, OnDestroy {
|
|
16
|
+
/**
|
|
17
|
+
* Display name of the column (used in placeholder text)
|
|
18
|
+
*/
|
|
19
|
+
@Input() columnName: string = '';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Optional: Key identifier for the column (can be used by parent component)
|
|
23
|
+
*/
|
|
24
|
+
@Input() columnKey: string = '';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Optional: Initial filter configuration
|
|
28
|
+
*/
|
|
29
|
+
@Input() initialFilter?: FilterConfig;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Optional: Custom placeholder text. If not provided, uses "Search by {columnName}"
|
|
33
|
+
*/
|
|
34
|
+
@Input() placeholder?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Optional: Customize available match types. If not provided, uses all default match types.
|
|
38
|
+
*/
|
|
39
|
+
@Input() availableMatchTypes?: MatchType[];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Field type: 'text', 'currency', 'date', or 'status'
|
|
43
|
+
*/
|
|
44
|
+
@Input() fieldType: FieldType = 'text';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Status options for status field type
|
|
48
|
+
*/
|
|
49
|
+
@Input() statusOptions: string[] = [];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Currency symbol (default: '$')
|
|
53
|
+
*/
|
|
54
|
+
@Input() currencySymbol: string = '$';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Emitted when filter is applied with the filter configuration
|
|
58
|
+
*/
|
|
59
|
+
@Output() filterApplied = new EventEmitter<FilterConfig>();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Emitted when filter is cleared
|
|
63
|
+
*/
|
|
64
|
+
@Output() filterCleared = new EventEmitter<void>();
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Controls dropdown visibility
|
|
68
|
+
*/
|
|
69
|
+
showDropdown = false;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Unique ID for this filter instance
|
|
73
|
+
*/
|
|
74
|
+
private filterId: string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Subscription to filter service
|
|
78
|
+
*/
|
|
79
|
+
private filterServiceSubscription?: Subscription;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Array of filter rules
|
|
83
|
+
*/
|
|
84
|
+
filterRules: FilterRule[] = [];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Global match mode: how to combine multiple rules
|
|
88
|
+
*/
|
|
89
|
+
globalMatchMode: GlobalMatchMode = 'match-any-rule';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Default match types for text fields
|
|
93
|
+
*/
|
|
94
|
+
private textMatchTypes: MatchTypeOption[] = [
|
|
95
|
+
{ value: 'match-all', label: 'Match All' },
|
|
96
|
+
{ value: 'match-any', label: 'Match Any' },
|
|
97
|
+
{ value: 'starts-with', label: 'Starts with' },
|
|
98
|
+
{ value: 'ends-with', label: 'Ends with' },
|
|
99
|
+
{ value: 'contains', label: 'Contains' },
|
|
100
|
+
{ value: 'equals', label: 'Equals' }
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Match types for currency/number fields
|
|
105
|
+
*/
|
|
106
|
+
private currencyMatchTypes: MatchTypeOption[] = [
|
|
107
|
+
{ value: 'equals', label: 'Equals' },
|
|
108
|
+
{ value: 'greater-than', label: 'Greater than' },
|
|
109
|
+
{ value: 'less-than', label: 'Less than' },
|
|
110
|
+
{ value: 'greater-equal', label: 'Greater or equal' },
|
|
111
|
+
{ value: 'less-equal', label: 'Less or equal' }
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Match types for age/number fields (same as currency)
|
|
116
|
+
*/
|
|
117
|
+
private ageMatchTypes: MatchTypeOption[] = [
|
|
118
|
+
{ value: 'equals', label: 'Equals' },
|
|
119
|
+
{ value: 'greater-than', label: 'Greater than' },
|
|
120
|
+
{ value: 'less-than', label: 'Less than' },
|
|
121
|
+
{ value: 'greater-equal', label: 'Greater or equal' },
|
|
122
|
+
{ value: 'less-equal', label: 'Less or equal' }
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Match types for date fields
|
|
127
|
+
*/
|
|
128
|
+
private dateMatchTypes: MatchTypeOption[] = [
|
|
129
|
+
{ value: 'is', label: 'Date is' },
|
|
130
|
+
{ value: 'is-not', label: 'Date is not' },
|
|
131
|
+
{ value: 'is-before', label: 'Date is before' },
|
|
132
|
+
{ value: 'is-after', label: 'Date is after' },
|
|
133
|
+
{ value: 'is-on', label: 'Date is on' }
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Match types for status fields
|
|
138
|
+
*/
|
|
139
|
+
private statusMatchTypes: MatchTypeOption[] = [
|
|
140
|
+
{ value: 'is', label: 'Is' },
|
|
141
|
+
{ value: 'is-not', label: 'Is not' },
|
|
142
|
+
{ value: 'equals', label: 'Equals' }
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Getter for match types based on field type
|
|
147
|
+
*/
|
|
148
|
+
get matchTypes(): MatchTypeOption[] {
|
|
149
|
+
// If custom match types provided, use those
|
|
150
|
+
if (this.availableMatchTypes && this.availableMatchTypes.length > 0) {
|
|
151
|
+
const allTypes = [...this.textMatchTypes, ...this.currencyMatchTypes, ...this.dateMatchTypes, ...this.statusMatchTypes];
|
|
152
|
+
return allTypes.filter(type =>
|
|
153
|
+
this.availableMatchTypes!.includes(type.value)
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Return match types based on field type
|
|
158
|
+
switch (this.fieldType) {
|
|
159
|
+
case 'currency':
|
|
160
|
+
return this.currencyMatchTypes;
|
|
161
|
+
case 'age':
|
|
162
|
+
return this.ageMatchTypes;
|
|
163
|
+
case 'date':
|
|
164
|
+
return this.dateMatchTypes;
|
|
165
|
+
case 'status':
|
|
166
|
+
return this.statusMatchTypes;
|
|
167
|
+
case 'text':
|
|
168
|
+
default:
|
|
169
|
+
return this.textMatchTypes;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
constructor(
|
|
174
|
+
private elementRef: ElementRef,
|
|
175
|
+
private cdr: ChangeDetectorRef,
|
|
176
|
+
private filterService: ColumnFilterService
|
|
177
|
+
) {
|
|
178
|
+
// Generate unique ID for this filter instance
|
|
179
|
+
this.filterId = `filter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
180
|
+
// Initialize with one default rule
|
|
181
|
+
this.addRule();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Lifecycle hook: Initialize with initial filter if provided
|
|
186
|
+
*/
|
|
187
|
+
ngOnInit() {
|
|
188
|
+
if (this.initialFilter && this.initialFilter.rules && this.initialFilter.rules.length > 0) {
|
|
189
|
+
this.filterRules = [...this.initialFilter.rules];
|
|
190
|
+
}
|
|
191
|
+
if (this.initialFilter?.globalMatchMode) {
|
|
192
|
+
this.globalMatchMode = this.initialFilter.globalMatchMode;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Subscribe to filter service to handle only one filter open at a time
|
|
196
|
+
this.filterServiceSubscription = this.filterService.openFilterId$.subscribe((openFilterId: string | null) => {
|
|
197
|
+
if (openFilterId !== null && openFilterId !== this.filterId && this.showDropdown) {
|
|
198
|
+
this.showDropdown = false;
|
|
199
|
+
this.cdr.detectChanges();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Cleanup subscriptions
|
|
206
|
+
*/
|
|
207
|
+
ngOnDestroy(): void {
|
|
208
|
+
if (this.filterServiceSubscription) {
|
|
209
|
+
this.filterServiceSubscription.unsubscribe();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Close dropdown when clicking outside
|
|
215
|
+
*/
|
|
216
|
+
@HostListener('document:click', ['$event'])
|
|
217
|
+
onDocumentClick(event: MouseEvent): void {
|
|
218
|
+
if (!this.elementRef.nativeElement.contains(event.target)) {
|
|
219
|
+
if (this.showDropdown) {
|
|
220
|
+
this.showDropdown = false;
|
|
221
|
+
this.filterService.closeFilter(this.filterId);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Close dropdown on ESC key press
|
|
228
|
+
*/
|
|
229
|
+
@HostListener('document:keydown', ['$event'])
|
|
230
|
+
onEscapeKey(event: KeyboardEvent): void {
|
|
231
|
+
if (event.key === 'Escape' && this.showDropdown) {
|
|
232
|
+
this.showDropdown = false;
|
|
233
|
+
this.filterService.closeFilter(this.filterId);
|
|
234
|
+
this.cdr.detectChanges();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Toggle dropdown visibility
|
|
240
|
+
*/
|
|
241
|
+
toggleDropdown(event: Event): void {
|
|
242
|
+
event.stopPropagation();
|
|
243
|
+
const newState = !this.showDropdown;
|
|
244
|
+
this.showDropdown = newState;
|
|
245
|
+
|
|
246
|
+
// Notify service about filter state change
|
|
247
|
+
if (newState) {
|
|
248
|
+
this.filterService.openFilter(this.filterId);
|
|
249
|
+
} else {
|
|
250
|
+
this.filterService.closeFilter(this.filterId);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Add a new filter rule
|
|
256
|
+
*/
|
|
257
|
+
addRule(): void {
|
|
258
|
+
let defaultMatchType: MatchType = 'contains';
|
|
259
|
+
if (this.fieldType === 'currency' || this.fieldType === 'age') {
|
|
260
|
+
defaultMatchType = 'equals';
|
|
261
|
+
} else if (this.fieldType === 'date') {
|
|
262
|
+
defaultMatchType = 'is';
|
|
263
|
+
} else if (this.fieldType === 'status') {
|
|
264
|
+
defaultMatchType = 'is';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.filterRules.push({
|
|
268
|
+
id: this.generateId(),
|
|
269
|
+
matchType: defaultMatchType,
|
|
270
|
+
value: ''
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Remove a filter rule at the given index
|
|
276
|
+
*/
|
|
277
|
+
removeRule(index: number): void {
|
|
278
|
+
if (this.filterRules.length > 1) {
|
|
279
|
+
this.filterRules.splice(index, 1);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Apply the filter and emit the filter configuration
|
|
285
|
+
*/
|
|
286
|
+
applyFilter(): void {
|
|
287
|
+
const validRules = this.filterRules.filter(rule => {
|
|
288
|
+
if (this.fieldType === 'status') {
|
|
289
|
+
return rule.value !== '';
|
|
290
|
+
}
|
|
291
|
+
return rule.value.trim() !== '';
|
|
292
|
+
});
|
|
293
|
+
if (validRules.length > 0) {
|
|
294
|
+
this.filterApplied.emit({
|
|
295
|
+
rules: validRules,
|
|
296
|
+
globalMatchMode: this.globalMatchMode,
|
|
297
|
+
fieldType: this.fieldType,
|
|
298
|
+
statusOptions: this.statusOptions.length > 0 ? this.statusOptions : undefined
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
this.showDropdown = false;
|
|
302
|
+
this.filterService.closeFilter(this.filterId);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Clear all filter rules and emit clear event
|
|
307
|
+
* This method can be called programmatically from parent components
|
|
308
|
+
*/
|
|
309
|
+
clearFilter(): void {
|
|
310
|
+
this.filterRules = [];
|
|
311
|
+
this.addRule(); // Add one empty rule
|
|
312
|
+
this.globalMatchMode = 'match-any-rule'; // Reset to default
|
|
313
|
+
this.filterCleared.emit();
|
|
314
|
+
this.showDropdown = false;
|
|
315
|
+
this.filterService.closeFilter(this.filterId);
|
|
316
|
+
// Force change detection to update UI icon
|
|
317
|
+
this.cdr.detectChanges();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Toggle global match mode
|
|
322
|
+
*/
|
|
323
|
+
toggleGlobalMatchMode(): void {
|
|
324
|
+
this.globalMatchMode = this.globalMatchMode === 'match-any-rule'
|
|
325
|
+
? 'match-all-rules'
|
|
326
|
+
: 'match-any-rule';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Check if there are any active filters (non-empty rules)
|
|
331
|
+
*/
|
|
332
|
+
hasActiveFilter(): boolean {
|
|
333
|
+
if (!this.filterRules || this.filterRules.length === 0) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
return this.filterRules.some(rule => {
|
|
337
|
+
if (!rule || !rule.value) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
// For status fields, check if value is not empty string
|
|
341
|
+
if (this.fieldType === 'status') {
|
|
342
|
+
return rule.value.trim() !== '';
|
|
343
|
+
}
|
|
344
|
+
// For other fields, check if trimmed value is not empty
|
|
345
|
+
return rule.value.trim() !== '';
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get placeholder text for the input field
|
|
351
|
+
*/
|
|
352
|
+
getPlaceholder(): string {
|
|
353
|
+
if (this.placeholder) {
|
|
354
|
+
return this.placeholder;
|
|
355
|
+
}
|
|
356
|
+
return this.columnName ? `Search by ${this.columnName.toLowerCase()}` : 'Search';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get label for a match type value
|
|
361
|
+
*/
|
|
362
|
+
getMatchTypeLabel(value: MatchType): string {
|
|
363
|
+
const type = this.matchTypes.find(t => t.value === value);
|
|
364
|
+
return type ? type.label : '';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Check if there are multiple rules (to show global match mode toggle)
|
|
369
|
+
*/
|
|
370
|
+
hasMultipleRules(): boolean {
|
|
371
|
+
return this.filterRules.length > 1;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Get label for global match mode
|
|
376
|
+
*/
|
|
377
|
+
getGlobalMatchModeLabel(): string {
|
|
378
|
+
return this.globalMatchMode === 'match-all-rules' ? 'Match All Rules' : 'Match Any Rule';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Format currency value for display
|
|
383
|
+
*/
|
|
384
|
+
formatCurrency(value: string): string {
|
|
385
|
+
if (!value || value.trim() === '') return '';
|
|
386
|
+
const numValue = parseFloat(value.replace(/[^0-9.-]/g, ''));
|
|
387
|
+
if (isNaN(numValue)) return value;
|
|
388
|
+
// Format with currency symbol and commas, but don't add decimals if not present
|
|
389
|
+
const formatted = numValue.toLocaleString('en-US', {
|
|
390
|
+
minimumFractionDigits: 0,
|
|
391
|
+
maximumFractionDigits: 2
|
|
392
|
+
});
|
|
393
|
+
return this.currencySymbol + formatted;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Parse currency value from formatted string
|
|
398
|
+
*/
|
|
399
|
+
parseCurrency(value: string): string {
|
|
400
|
+
// Remove currency symbol and commas, keep numbers and decimal point
|
|
401
|
+
return value.replace(/[^0-9.-]/g, '');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Check if field type is status
|
|
406
|
+
*/
|
|
407
|
+
isStatusField(): boolean {
|
|
408
|
+
return this.fieldType === 'status';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Check if field type is currency
|
|
413
|
+
*/
|
|
414
|
+
isCurrencyField(): boolean {
|
|
415
|
+
return this.fieldType === 'currency';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Check if field type is age
|
|
420
|
+
*/
|
|
421
|
+
isAgeField(): boolean {
|
|
422
|
+
return this.fieldType === 'age';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Check if field type is date
|
|
427
|
+
*/
|
|
428
|
+
isDateField(): boolean {
|
|
429
|
+
return this.fieldType === 'date';
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Check if field type is text
|
|
434
|
+
*/
|
|
435
|
+
isTextField(): boolean {
|
|
436
|
+
return this.fieldType === 'text';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Generate a unique ID for a filter rule
|
|
441
|
+
*/
|
|
442
|
+
private generateId(): string {
|
|
443
|
+
return `rule-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { FormsModule } from '@angular/forms';
|
|
4
|
+
import { ColumnFilterComponent } from './column-filter.component';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Module for Column Filter Component
|
|
8
|
+
*
|
|
9
|
+
* This module provides the ColumnFilterComponent for use in Angular applications
|
|
10
|
+
* that prefer module-based imports over standalone components.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { ColumnFilterModule } from './components/column-filter/column-filter.module';
|
|
15
|
+
*
|
|
16
|
+
* @NgModule({
|
|
17
|
+
* imports: [ColumnFilterModule],
|
|
18
|
+
* // ...
|
|
19
|
+
* })
|
|
20
|
+
* export class YourModule {}
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
@NgModule({
|
|
24
|
+
imports: [
|
|
25
|
+
CommonModule,
|
|
26
|
+
FormsModule,
|
|
27
|
+
ColumnFilterComponent // Standalone component can be imported in module
|
|
28
|
+
],
|
|
29
|
+
exports: [ColumnFilterComponent]
|
|
30
|
+
})
|
|
31
|
+
export class ColumnFilterModule { }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field type for different column types
|
|
3
|
+
*/
|
|
4
|
+
export type FieldType = 'text' | 'currency' | 'date' | 'status' | 'age';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Match type options for filtering
|
|
8
|
+
*/
|
|
9
|
+
export type MatchType = 'match-all' | 'match-any' | 'starts-with' | 'ends-with' | 'contains' | 'equals' | 'greater-than' | 'less-than' | 'greater-equal' | 'less-equal' | 'is-before' | 'is-after' | 'is-on' | 'is-not' | 'is';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Individual filter rule
|
|
13
|
+
*/
|
|
14
|
+
export interface FilterRule {
|
|
15
|
+
id: string;
|
|
16
|
+
matchType: MatchType;
|
|
17
|
+
value: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Global match mode for combining multiple filter rules
|
|
22
|
+
*/
|
|
23
|
+
export type GlobalMatchMode = 'match-all-rules' | 'match-any-rule';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Filter configuration containing multiple rules
|
|
27
|
+
*/
|
|
28
|
+
export interface FilterConfig {
|
|
29
|
+
rules: FilterRule[];
|
|
30
|
+
/**
|
|
31
|
+
* Global match mode: 'match-all-rules' means ALL rules must match (AND logic),
|
|
32
|
+
* 'match-any-rule' means ANY rule can match (OR logic). Default is 'match-any-rule'.
|
|
33
|
+
*/
|
|
34
|
+
globalMatchMode?: GlobalMatchMode;
|
|
35
|
+
/**
|
|
36
|
+
* Field type for this column filter
|
|
37
|
+
*/
|
|
38
|
+
fieldType?: FieldType;
|
|
39
|
+
/**
|
|
40
|
+
* Status options (for status field type)
|
|
41
|
+
*/
|
|
42
|
+
statusOptions?: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Match type configuration for UI
|
|
47
|
+
*/
|
|
48
|
+
export interface MatchTypeOption {
|
|
49
|
+
value: MatchType;
|
|
50
|
+
label: string;
|
|
51
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API for Column Filter Library
|
|
3
|
+
*
|
|
4
|
+
* This file exports all public APIs that will be available when
|
|
5
|
+
* this library is packaged and published to npm.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Models
|
|
9
|
+
export * from './models/filter.models';
|
|
10
|
+
|
|
11
|
+
// Utilities
|
|
12
|
+
export * from './utils/column-filter.utils';
|
|
13
|
+
|
|
14
|
+
// Services
|
|
15
|
+
export * from './services/column-filter.service';
|
|
16
|
+
|
|
17
|
+
// Components
|
|
18
|
+
export * from '../components/column-filter/column-filter.component';
|
|
19
|
+
|
|
20
|
+
// Module (optional, for module-based imports)
|
|
21
|
+
export * from '../components/column-filter/column-filter.module';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Service to manage column filter state across multiple filter components
|
|
6
|
+
* Ensures only one filter dropdown is open at a time
|
|
7
|
+
*/
|
|
8
|
+
@Injectable({
|
|
9
|
+
providedIn: 'root'
|
|
10
|
+
})
|
|
11
|
+
export class ColumnFilterService {
|
|
12
|
+
private openFilterId = new Subject<string | null>();
|
|
13
|
+
openFilterId$ = this.openFilterId.asObservable();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Notify that a filter is now open
|
|
17
|
+
*/
|
|
18
|
+
openFilter(filterId: string): void {
|
|
19
|
+
this.openFilterId.next(filterId);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Notify that a filter is now closed
|
|
24
|
+
*/
|
|
25
|
+
closeFilter(filterId: string): void {
|
|
26
|
+
this.openFilterId.next(null);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Close all open filters
|
|
31
|
+
*/
|
|
32
|
+
closeAllFilters(): void {
|
|
33
|
+
this.openFilterId.next(null);
|
|
34
|
+
}
|
|
35
|
+
}
|