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/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "ngx-column-filter-popup",
3
+ "version": "1.0.0",
4
+ "description": "A powerful, reusable Angular column filter component with support for multiple field types, advanced filtering rules, and customizable match modes",
5
+ "keywords": [
6
+ "angular",
7
+ "angular-library",
8
+ "angular-component",
9
+ "column-filter",
10
+ "filter",
11
+ "table-filter",
12
+ "data-filter",
13
+ "typescript",
14
+ "ngx",
15
+ "advanced-filter",
16
+ "multi-filter",
17
+ "data-grid",
18
+ "popup-filter"
19
+ ],
20
+ "author": "Shivam Sharma",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/shivamsharma/ngx-column-filter-popup.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/shivamsharma/ngx-column-filter-popup/issues"
28
+ },
29
+ "homepage": "https://github.com/shivamsharma/ngx-column-filter-popup#readme",
30
+ "main": "./src/app/lib/public-api.ts",
31
+ "types": "./src/app/lib/public-api.ts",
32
+ "files": [
33
+ "src/app/lib/**/*",
34
+ "src/app/components/column-filter/**/*",
35
+ "README.md",
36
+ "DOCUMENTATION.md",
37
+ "USAGE_EXAMPLES.md",
38
+ "LICENSE"
39
+ ],
40
+ "scripts": {
41
+ "ng": "ng",
42
+ "start": "ng serve",
43
+ "build": "ng build",
44
+ "build:lib": "ng build --configuration production",
45
+ "watch": "ng build --watch --configuration development",
46
+ "test": "ng test",
47
+ "publish:npm": "npm publish",
48
+ "pack:test": "npm pack"
49
+ },
50
+ "peerDependencies": {
51
+ "@angular/common": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
52
+ "@angular/core": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
53
+ "@angular/forms": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
54
+ "rxjs": "^6.0.0 || ^7.0.0"
55
+ },
56
+ "dependencies": {
57
+ "tslib": "^2.3.0"
58
+ },
59
+ "devDependencies": {
60
+ "@angular/build": "^21.0.5",
61
+ "@angular/cli": "^21.0.5",
62
+ "@angular/common": "^21.0.0",
63
+ "@angular/compiler": "^21.0.0",
64
+ "@angular/compiler-cli": "^21.0.0",
65
+ "@angular/core": "^21.0.0",
66
+ "@angular/forms": "^21.0.0",
67
+ "@angular/platform-browser": "^21.0.0",
68
+ "@angular/router": "^21.0.0",
69
+ "jsdom": "^27.1.0",
70
+ "rxjs": "~7.8.0",
71
+ "typescript": "~5.9.2",
72
+ "vitest": "^4.0.8"
73
+ },
74
+ "prettier": {
75
+ "printWidth": 100,
76
+ "singleQuote": true,
77
+ "overrides": [
78
+ {
79
+ "files": "*.html",
80
+ "options": {
81
+ "parser": "angular"
82
+ }
83
+ }
84
+ ]
85
+ }
86
+ }
@@ -0,0 +1,131 @@
1
+ <div class="column-filter-wrapper">
2
+ <button
3
+ class="filter-trigger"
4
+ [class.has-filter]="hasActiveFilter()"
5
+ (click)="toggleDropdown($event)"
6
+ [attr.aria-label]="'Filter by ' + columnName"
7
+ [attr.aria-expanded]="showDropdown">
8
+ <!-- <i class="fas fa-filter"></i> -->
9
+ <svg *ngIf="!hasActiveFilter()" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" data-p-icon="filter" class="ng-tns-c389539309-46 p-icon ng-star-inserted" pc164=""><g clip-path="url(#pui_id_9)"><path d="M8.64708 14H5.35296C5.18981 13.9979 5.03395 13.9321 4.91858 13.8167C4.8032 13.7014 4.73745 13.5455 4.73531 13.3824V7L0.329431 0.98C0.259794 0.889466 0.217389 0.780968 0.20718 0.667208C0.19697 0.553448 0.219379 0.439133 0.271783 0.337647C0.324282 0.236453 0.403423 0.151519 0.500663 0.0920138C0.597903 0.0325088 0.709548 0.000692754 0.823548 0H13.1765C13.2905 0.000692754 13.4021 0.0325088 13.4994 0.0920138C13.5966 0.151519 13.6758 0.236453 13.7283 0.337647C13.7807 0.439133 13.8031 0.553448 13.7929 0.667208C13.7826 0.780968 13.7402 0.889466 13.6706 0.98L9.26472 7V13.3824C9.26259 13.5455 9.19683 13.7014 9.08146 13.8167C8.96609 13.9321 8.81022 13.9979 8.64708 14ZM5.97061 12.7647H8.02943V6.79412C8.02878 6.66289 8.07229 6.53527 8.15296 6.43177L11.9412 1.23529H2.05884L5.86355 6.43177C5.94422 6.53527 5.98773 6.66289 5.98708 6.79412L5.97061 12.7647Z" fill="currentColor"></path></g><defs><clipPath id="url(#pui_id_9)"><rect width="14" height="14" fill="white"></rect></clipPath></defs></svg>
10
+
11
+ <svg *ngIf="hasActiveFilter()" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" data-p-icon="filter-fill" class="ng-tns-c389539309-50 p-icon ng-star-inserted" pc2469=""><path d="M13.7274 0.33847C13.6228 0.130941 13.4095 0 13.1764 0H0.82351C0.590451 0 0.377157 0.130941 0.272568 0.33847C0.167157 0.545999 0.187746 0.795529 0.325275 0.98247L4.73527 6.99588V13.3824C4.73527 13.7233 5.01198 14 5.35292 14H8.64704C8.98798 14 9.26469 13.7233 9.26469 13.3824V6.99588L13.6747 0.98247C13.8122 0.795529 13.8328 0.545999 13.7274 0.33847Z" fill="currentColor"></path></svg>
12
+ </button>
13
+
14
+
15
+
16
+ <div class="filter-dropdown" *ngIf="showDropdown">
17
+ <div class="filter-dropdown-content">
18
+ <!-- Global Match Mode Toggle (only shown when multiple rules exist) -->
19
+ <div class="global-match-mode-toggle" *ngIf="hasMultipleRules()">
20
+ <label class="match-mode-label">Combine rules:</label>
21
+ <div class="match-mode-switch-wrapper">
22
+ <div
23
+ class="match-mode-switch"
24
+ [class.match-all]="globalMatchMode === 'match-all-rules'"
25
+ (click)="toggleGlobalMatchMode()">
26
+ <span class="match-mode-text">{{ getGlobalMatchModeLabel() }}</span>
27
+ <i class="fas fa-chevron-down match-mode-arrow"></i>
28
+ </div>
29
+ <div class="match-mode-options" *ngIf="false">
30
+ <!-- Dropdown options if needed in future -->
31
+ </div>
32
+ </div>
33
+ </div>
34
+
35
+ <!-- Filter Rules -->
36
+ <div class="filter-rules">
37
+ <div *ngFor="let rule of filterRules; let i = index" class="filter-rule">
38
+
39
+ <!-- First dropdown - Match type -->
40
+ <div class="dropdown-select-wrapper" [class.first-rule]="i === 0">
41
+ <select
42
+ class="dropdown-select"
43
+ [(ngModel)]="rule.matchType"
44
+ [attr.aria-label]="'Match type for rule ' + (i + 1)">
45
+ <option *ngFor="let type of matchTypes" [value]="type.value">
46
+ {{ type.label }}
47
+ </option>
48
+ </select>
49
+ <i class="fas fa-chevron-down dropdown-arrow"></i>
50
+ </div>
51
+
52
+ <!-- Text Input -->
53
+ <input
54
+ *ngIf="isTextField()"
55
+ type="text"
56
+ class="filter-input"
57
+ [placeholder]="getPlaceholder()"
58
+ [(ngModel)]="rule.value"
59
+ [attr.aria-label]="'Filter value for rule ' + (i + 1)">
60
+
61
+ <!-- Currency Input -->
62
+ <div *ngIf="isCurrencyField()" class="currency-input-wrapper">
63
+ <span class="currency-symbol">{{ currencySymbol }}</span>
64
+ <input
65
+ type="text"
66
+ class="filter-input currency-input"
67
+ [placeholder]="'0.00'"
68
+ [value]="formatCurrency(rule.value)"
69
+ (input)="rule.value = parseCurrency($any($event.target).value)"
70
+ [attr.aria-label]="'Currency filter value for rule ' + (i + 1)">
71
+ </div>
72
+
73
+ <!-- Age/Number Input -->
74
+ <input
75
+ *ngIf="isAgeField()"
76
+ type="number"
77
+ class="filter-input age-input"
78
+ [placeholder]="'Enter number'"
79
+ [(ngModel)]="rule.value"
80
+ min="0"
81
+ step="1"
82
+ [attr.aria-label]="'Age/Number filter value for rule ' + (i + 1)">
83
+
84
+ <!-- Date Input -->
85
+ <input
86
+ *ngIf="isDateField()"
87
+ type="date"
88
+ class="filter-input date-input"
89
+ [placeholder]="'Select date'"
90
+ [(ngModel)]="rule.value"
91
+ [attr.aria-label]="'Date filter value for rule ' + (i + 1)">
92
+
93
+ <!-- Status Dropdown -->
94
+ <div *ngIf="isStatusField()" class="dropdown-select-wrapper status-dropdown-wrapper">
95
+ <select
96
+ class="dropdown-select status-select"
97
+ [(ngModel)]="rule.value"
98
+ [attr.aria-label]="'Status filter value for rule ' + (i + 1)">
99
+ <option value="">Select status</option>
100
+ <option *ngFor="let option of statusOptions" [value]="option">
101
+ {{ option }}
102
+ </option>
103
+ </select>
104
+ <i class="fas fa-chevron-down dropdown-arrow"></i>
105
+ </div>
106
+
107
+ <!-- Remove Rule button (only for additional rules) -->
108
+ <button
109
+ *ngIf="filterRules.length > 1 && i > 0"
110
+ class="remove-rule-btn"
111
+ (click)="removeRule(i)"
112
+ type="button"
113
+ [attr.aria-label]="'Remove filter rule ' + (i + 1)">
114
+ <i class="fas fa-trash"></i> Remove Rule
115
+ </button>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Add Rule button -->
120
+ <button class="add-rule-btn" (click)="addRule()" type="button" aria-label="Add another filter rule">
121
+ <span class="plus-icon">+</span> Add Rule
122
+ </button>
123
+
124
+ <!-- Action buttons -->
125
+ <div class="filter-actions">
126
+ <button class="btn-clear" (click)="clearFilter()" type="button">Clear</button>
127
+ <button class="btn-apply" (click)="applyFilter()" type="button">Apply</button>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </div>
@@ -0,0 +1,426 @@
1
+ .column-filter-wrapper {
2
+ font-family: Lato, sans-serif !important;
3
+ position: relative;
4
+ display: inline-block;
5
+ z-index: 10;
6
+ overflow: visible;
7
+ }
8
+
9
+ .filter-trigger {
10
+ background: transparent;
11
+ border: none;
12
+ padding: 4px 8px;
13
+ cursor: pointer;
14
+ color: #6c757d;
15
+ transition: all 0.2s ease;
16
+ display: inline-flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+
20
+ &:hover {
21
+ color: #495057;
22
+ }
23
+
24
+ &:focus {
25
+ // outline: 2px solid #28a745;
26
+ // outline-offset: 2px;
27
+ }
28
+
29
+ &.has-filter {
30
+ // color: #28a745;
31
+
32
+ i {
33
+ font-weight: bold;
34
+ }
35
+ }
36
+
37
+ i {
38
+ font-size: 14px;
39
+ }
40
+ }
41
+
42
+ .filter-dropdown {
43
+ position: absolute;
44
+ top: 100%;
45
+ left: 50%;
46
+ transform: translateX(-50%);
47
+ margin-top: 8px;
48
+ background: white;
49
+ border-radius: 8px;
50
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
51
+ padding: 11px;
52
+ min-width: 12.5rem;
53
+ max-width: 90vw;
54
+ z-index: 10000;
55
+ overflow: visible;
56
+
57
+ &::before {
58
+ content: '';
59
+ position: absolute;
60
+ top: -8px;
61
+ left: 50%;
62
+ transform: translateX(-50%);
63
+ width: 0;
64
+ height: 0;
65
+ border-left: 8px solid transparent;
66
+ border-right: 8px solid transparent;
67
+ border-bottom: 8px solid white;
68
+ }
69
+ }
70
+
71
+ .filter-dropdown-content {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 16px;
75
+ }
76
+
77
+ .global-match-mode-toggle {
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 8px;
81
+ padding-bottom: 12px;
82
+ border-bottom: 1px solid #f0f0f0;
83
+ margin-bottom: 4px;
84
+ }
85
+
86
+ .match-mode-label {
87
+ font-size: 12px;
88
+ color: #6c757d;
89
+ font-weight: 500;
90
+ font-family: Lato, sans-serif;
91
+ }
92
+
93
+ .match-mode-switch-wrapper {
94
+ position: relative;
95
+ }
96
+
97
+ .match-mode-switch {
98
+ width: 100%;
99
+ padding: 10px 40px 10px 16px;
100
+ border: 1px solid #e0e0e0;
101
+ border-radius: 8px;
102
+ background: white;
103
+ font-size: 14px;
104
+ color: #334155;
105
+ cursor: pointer;
106
+ transition: all 0.2s ease;
107
+ font-family: Lato, sans-serif;
108
+ position: relative;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: space-between;
112
+ box-sizing: border-box;
113
+
114
+ &:hover {
115
+ border-color: #28a745;
116
+ }
117
+
118
+ &.match-all {
119
+ border-color: #28a745;
120
+ background: #f0fdf4;
121
+ color: #15803d;
122
+
123
+ .match-mode-text {
124
+ color: #15803d;
125
+ font-weight: 500;
126
+ }
127
+
128
+ .match-mode-arrow {
129
+ color: #15803d;
130
+ }
131
+ }
132
+
133
+ &:focus {
134
+ outline: none;
135
+ border-color: #28a745;
136
+ box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
137
+ }
138
+ }
139
+
140
+ .match-mode-text {
141
+ flex: 1;
142
+ text-align: left;
143
+ color: #334155;
144
+ }
145
+
146
+ .match-mode-arrow {
147
+ position: absolute;
148
+ right: 16px;
149
+ color: #6c757d;
150
+ font-size: 12px;
151
+ pointer-events: none;
152
+ transition: transform 0.2s ease;
153
+ }
154
+
155
+ .filter-rules {
156
+ display: flex;
157
+ flex-direction: column;
158
+ gap: 12px;
159
+ }
160
+
161
+ .filter-rule {
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: 10px;
165
+ padding-bottom: 12px;
166
+ border-bottom: 1px solid #f0f0f0;
167
+
168
+ &:last-child {
169
+ border-bottom: none;
170
+ padding-bottom: 0;
171
+ }
172
+ }
173
+
174
+ .dropdown-select-wrapper {
175
+ position: relative;
176
+ }
177
+
178
+ .dropdown-select {
179
+ width: 100%;
180
+ padding: 10px 40px 10px 16px;
181
+ border: 1px solid #e0e0e0;
182
+ border-radius: 8px;
183
+ background: white;
184
+ font-size: 14px;
185
+ color: #334155;
186
+ appearance: none;
187
+ cursor: pointer;
188
+ transition: all 0.2s ease;
189
+ font-family: Lato, sans-serif;
190
+
191
+ &:hover {
192
+ border-color: #28a745;
193
+ }
194
+
195
+ &:focus {
196
+ outline: none;
197
+ border-color: #28a745;
198
+ box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
199
+ }
200
+ }
201
+
202
+ .dropdown-arrow {
203
+ position: absolute;
204
+ right: 16px;
205
+ top: 50%;
206
+ transform: translateY(-50%);
207
+ color: #6c757d;
208
+ pointer-events: none;
209
+ font-size: 12px;
210
+ }
211
+
212
+ .filter-input {
213
+ width: 100%;
214
+ padding: 10px 16px;
215
+ border: 1px solid #e0e0e0;
216
+ border-radius: 8px;
217
+ font-size: 14px;
218
+ color: #334155;
219
+ transition: all 0.2s ease;
220
+ font-family: Lato, sans-serif;
221
+ box-sizing: border-box;
222
+
223
+ &::placeholder {
224
+ color: #999;
225
+ }
226
+
227
+ &:focus {
228
+ outline: none;
229
+ border-color: #28a745;
230
+ box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
231
+ }
232
+
233
+ // Date input specific styling
234
+ &.date-input {
235
+ &::-webkit-calendar-picker-indicator {
236
+ cursor: pointer;
237
+ opacity: 0.6;
238
+ &:hover {
239
+ opacity: 1;
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ // Currency input wrapper
246
+ .currency-input-wrapper {
247
+ position: relative;
248
+ display: flex;
249
+ align-items: center;
250
+ width: 100%;
251
+
252
+ .currency-symbol {
253
+ position: absolute;
254
+ left: 16px;
255
+ color: #6c757d;
256
+ font-size: 14px;
257
+ font-weight: 500;
258
+ pointer-events: none;
259
+ z-index: 1;
260
+ }
261
+
262
+ .currency-input {
263
+ padding-left: 32px;
264
+ text-align: right;
265
+ }
266
+ }
267
+
268
+ // Status dropdown wrapper
269
+ .status-dropdown-wrapper {
270
+ width: 100%;
271
+
272
+ .status-select {
273
+ width: 100%;
274
+ appearance: none;
275
+ padding: 10px 40px 10px 16px;
276
+ border: 1px solid #e0e0e0;
277
+ border-radius: 8px;
278
+ font-size: 14px;
279
+ color: #334155;
280
+ background: white;
281
+ cursor: pointer;
282
+ transition: all 0.2s ease;
283
+ font-family: Lato, sans-serif;
284
+
285
+ &:hover {
286
+ border-color: #28a745;
287
+ }
288
+
289
+ &:focus {
290
+ outline: none;
291
+ border-color: #28a745;
292
+ box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
293
+ }
294
+
295
+ option {
296
+ padding: 8px;
297
+ }
298
+ }
299
+ }
300
+
301
+ .remove-rule-btn {
302
+ background: transparent;
303
+ border: none;
304
+ color: #dc3545;
305
+ font-size: 14px;
306
+ font-weight: 500;
307
+ padding: 8px 0;
308
+ cursor: pointer;
309
+ display: flex;
310
+ align-items: center;
311
+ gap: 6px;
312
+ transition: all 0.2s ease;
313
+ font-family: Lato, sans-serif;
314
+
315
+ i {
316
+ font-size: 12px;
317
+ }
318
+
319
+ &:hover {
320
+ color: #c82333;
321
+ }
322
+
323
+ &:focus {
324
+ outline: 2px solid #dc3545;
325
+ outline-offset: 2px;
326
+ }
327
+ }
328
+
329
+ .add-rule-btn {
330
+ background: transparent;
331
+ border: none;
332
+ color: #007bff;
333
+ font-size: 14px;
334
+ font-weight: 500;
335
+ padding: 10px 0;
336
+ cursor: pointer;
337
+ display: flex;
338
+ align-items: center;
339
+ justify-content: center;
340
+ gap: 6px;
341
+ transition: all 0.2s ease;
342
+ font-family: Lato, sans-serif;
343
+
344
+ .plus-icon {
345
+ font-size: 18px;
346
+ font-weight: 300;
347
+ }
348
+
349
+ &:hover {
350
+ color: #0056b3;
351
+ background: rgba(0, 123, 255, 0.05);
352
+ border-radius: 4px;
353
+ }
354
+
355
+ &:focus {
356
+ outline: 2px solid #007bff;
357
+ outline-offset: 2px;
358
+ }
359
+ }
360
+
361
+ .filter-actions {
362
+ display: flex;
363
+ gap: 12px;
364
+ justify-content: space-between;
365
+ margin-top: 8px;
366
+ }
367
+
368
+ .btn-clear,
369
+ .btn-apply {
370
+ font-family: Lato, sans-serif !important;
371
+ flex: 1;
372
+ padding: 0px 24px;
373
+ border-radius: 8px;
374
+ font-size: 12.25px;
375
+ font-weight: 500;
376
+ cursor: pointer;
377
+ transition: all 0.2s ease;
378
+ width: 47.6875px !important;
379
+ height: 27.1875px !important;
380
+ border: none !important;
381
+ display: inline-flex;
382
+ align-items: center;
383
+ justify-content: center;
384
+
385
+ &:focus {
386
+ outline: 2px solid currentColor;
387
+ outline-offset: 2px;
388
+ }
389
+ }
390
+
391
+ .btn-clear {
392
+ background: #f1f5f9;
393
+ border: 1px solid #10B981;
394
+ color: #10b981;
395
+
396
+ &:hover {
397
+ background: rgba(218, 219, 219, 0.969);
398
+ }
399
+ }
400
+
401
+ .btn-apply {
402
+ background: #0cab71 !important;
403
+ color: white;
404
+
405
+ &:hover {
406
+ background: #10b981 !important;
407
+ }
408
+ }
409
+
410
+ // Responsive adjustments
411
+ @media (max-width: 480px) {
412
+ .filter-dropdown {
413
+ min-width: 320px;
414
+ left: 0;
415
+ transform: translateX(0);
416
+ max-width: calc(100vw - 20px);
417
+ }
418
+ }
419
+
420
+ // Additional positioning adjustments to prevent clipping
421
+ @media (max-width: 768px) {
422
+ .filter-dropdown {
423
+ // Ensure dropdown stays within viewport on tablets
424
+ max-width: calc(100vw - 32px);
425
+ }
426
+ }