@alaarab/ogrid-angular-radix 2.0.4
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/dist/esm/column-chooser/column-chooser.component.js +199 -0
- package/dist/esm/column-header-filter/column-header-filter.component.js +497 -0
- package/dist/esm/datagrid-table/datagrid-table.component.js +573 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/ogrid/ogrid.component.js +77 -0
- package/dist/esm/pagination-controls/pagination-controls.component.js +189 -0
- package/dist/types/column-chooser/column-chooser.component.d.ts +26 -0
- package/dist/types/column-header-filter/column-header-filter.component.d.ts +67 -0
- package/dist/types/datagrid-table/datagrid-table.component.d.ts +131 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/ogrid/ogrid.component.d.ts +14 -0
- package/dist/types/pagination-controls/pagination-controls.component.d.ts +15 -0
- package/jest-mocks/angular-cdk-overlay.cjs.js +38 -0
- package/jest-mocks/style-mock.js +1 -0
- package/jest.config.js +43 -0
- package/package.json +37 -0
- package/scripts/compile-styles.js +53 -0
- package/src/__tests__/column-chooser.component.spec.ts.skip +195 -0
- package/src/__tests__/column-header-filter.component.spec.ts.skip +401 -0
- package/src/__tests__/datagrid-table.component.spec.ts.skip +417 -0
- package/src/__tests__/exports.test.ts +54 -0
- package/src/__tests__/ogrid.component.spec.ts.skip +236 -0
- package/src/__tests__/pagination-controls.component.spec.ts.skip +190 -0
- package/src/column-chooser/column-chooser.component.ts +204 -0
- package/src/column-header-filter/column-header-filter.component.ts +528 -0
- package/src/datagrid-table/datagrid-table.component.scss +289 -0
- package/src/datagrid-table/datagrid-table.component.ts +636 -0
- package/src/index.ts +16 -0
- package/src/ogrid/ogrid.component.ts +78 -0
- package/src/pagination-controls/pagination-controls.component.ts +187 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component, input, signal, computed,
|
|
3
|
+
ChangeDetectionStrategy, ElementRef, viewChild,
|
|
4
|
+
} from '@angular/core';
|
|
5
|
+
import type { ColumnFilterType, IDateFilterValue, UserLike } from '@alaarab/ogrid-angular';
|
|
6
|
+
|
|
7
|
+
export interface IColumnHeaderFilterProps {
|
|
8
|
+
columnKey: string;
|
|
9
|
+
columnName: string;
|
|
10
|
+
filterType: ColumnFilterType;
|
|
11
|
+
isSorted?: boolean;
|
|
12
|
+
isSortedDescending?: boolean;
|
|
13
|
+
onSort?: () => void;
|
|
14
|
+
selectedValues?: string[];
|
|
15
|
+
onFilterChange?: (values: string[]) => void;
|
|
16
|
+
options?: string[];
|
|
17
|
+
isLoadingOptions?: boolean;
|
|
18
|
+
textValue?: string;
|
|
19
|
+
onTextChange?: (value: string) => void;
|
|
20
|
+
selectedUser?: UserLike;
|
|
21
|
+
onUserChange?: (user: UserLike | undefined) => void;
|
|
22
|
+
peopleSearch?: (query: string) => Promise<UserLike[]>;
|
|
23
|
+
dateValue?: IDateFilterValue;
|
|
24
|
+
onDateChange?: (value: IDateFilterValue | undefined) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Column header filter component for Angular Radix (lightweight styling).
|
|
29
|
+
* Standalone component with inline template and positioned popovers.
|
|
30
|
+
*/
|
|
31
|
+
@Component({
|
|
32
|
+
selector: 'column-header-filter',
|
|
33
|
+
standalone: true,
|
|
34
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
35
|
+
template: `
|
|
36
|
+
<div class="ogrid-header-filter" #headerEl>
|
|
37
|
+
<div class="ogrid-header-filter__label">
|
|
38
|
+
<span class="ogrid-header-filter__name" [title]="columnName()" data-header-label>
|
|
39
|
+
{{ columnName() }}
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="ogrid-header-filter__actions">
|
|
44
|
+
@if (onSort()) {
|
|
45
|
+
<button
|
|
46
|
+
class="ogrid-header-filter__btn"
|
|
47
|
+
[class.ogrid-header-filter__btn--active]="isSorted()"
|
|
48
|
+
(click)="onSort()!()"
|
|
49
|
+
[attr.aria-label]="'Sort by ' + columnName()"
|
|
50
|
+
[title]="isSorted() ? (isSortedDescending() ? 'Sorted descending' : 'Sorted ascending') : 'Sort'"
|
|
51
|
+
>
|
|
52
|
+
@if (isSorted() && isSortedDescending()) {
|
|
53
|
+
▼
|
|
54
|
+
} @else if (isSorted()) {
|
|
55
|
+
▲
|
|
56
|
+
} @else {
|
|
57
|
+
↕
|
|
58
|
+
}
|
|
59
|
+
</button>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@if (filterType() !== 'none') {
|
|
63
|
+
<button
|
|
64
|
+
class="ogrid-header-filter__btn"
|
|
65
|
+
[class.ogrid-header-filter__btn--active]="hasActiveFilter() || isFilterOpen()"
|
|
66
|
+
(click)="toggleFilter($event)"
|
|
67
|
+
[attr.aria-label]="'Filter ' + columnName()"
|
|
68
|
+
[title]="'Filter ' + columnName()"
|
|
69
|
+
>
|
|
70
|
+
⏷
|
|
71
|
+
@if (hasActiveFilter()) {
|
|
72
|
+
<span class="ogrid-header-filter__dot"></span>
|
|
73
|
+
}
|
|
74
|
+
</button>
|
|
75
|
+
}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
@if (isFilterOpen() && filterType() !== 'none') {
|
|
80
|
+
<div
|
|
81
|
+
class="ogrid-header-filter__popover"
|
|
82
|
+
[style.top.px]="popoverTop()"
|
|
83
|
+
[style.left.px]="popoverLeft()"
|
|
84
|
+
(click)="$event.stopPropagation()"
|
|
85
|
+
>
|
|
86
|
+
<div class="ogrid-header-filter__popover-header">
|
|
87
|
+
Filter: {{ columnName() }}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
@switch (filterType()) {
|
|
91
|
+
@case ('text') {
|
|
92
|
+
<div class="ogrid-header-filter__popover-body" style="width: 260px;">
|
|
93
|
+
<div style="padding: 12px;">
|
|
94
|
+
<input
|
|
95
|
+
type="text"
|
|
96
|
+
class="ogrid-header-filter__input"
|
|
97
|
+
placeholder="Enter search term..."
|
|
98
|
+
[value]="tempTextValue()"
|
|
99
|
+
(input)="tempTextValue.set(asInputValue($event))"
|
|
100
|
+
(keydown)="onTextKeydown($event)"
|
|
101
|
+
autocomplete="off"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="ogrid-header-filter__popover-actions">
|
|
105
|
+
<button class="ogrid-header-filter__action-btn" [disabled]="!tempTextValue()" (click)="handleTextClear()">Clear</button>
|
|
106
|
+
<button class="ogrid-header-filter__action-btn ogrid-header-filter__action-btn--primary" (click)="handleTextApply()">Apply</button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
}
|
|
110
|
+
@case ('multiSelect') {
|
|
111
|
+
<div class="ogrid-header-filter__popover-body" style="width: 280px;">
|
|
112
|
+
<div style="padding: 12px 12px 4px;">
|
|
113
|
+
<input
|
|
114
|
+
type="text"
|
|
115
|
+
class="ogrid-header-filter__input"
|
|
116
|
+
placeholder="Search..."
|
|
117
|
+
[value]="searchText()"
|
|
118
|
+
(input)="searchText.set(asInputValue($event))"
|
|
119
|
+
(keydown)="$event.stopPropagation()"
|
|
120
|
+
autocomplete="off"
|
|
121
|
+
/>
|
|
122
|
+
<div class="ogrid-header-filter__options-info">
|
|
123
|
+
{{ filteredOptions().length }} of {{ (options() ?? []).length }} options
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="ogrid-header-filter__select-actions">
|
|
127
|
+
<button class="ogrid-header-filter__action-btn" (click)="handleSelectAllFiltered()">
|
|
128
|
+
Select All ({{ filteredOptions().length }})
|
|
129
|
+
</button>
|
|
130
|
+
<button class="ogrid-header-filter__action-btn" (click)="handleClearSelection()">Clear</button>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="ogrid-header-filter__options-list">
|
|
133
|
+
@if (isLoadingOptions()) {
|
|
134
|
+
<div class="ogrid-header-filter__loading">Loading...</div>
|
|
135
|
+
} @else if (filteredOptions().length === 0) {
|
|
136
|
+
<div class="ogrid-header-filter__empty">No options found</div>
|
|
137
|
+
} @else {
|
|
138
|
+
@for (option of filteredOptions(); track option) {
|
|
139
|
+
<label class="ogrid-header-filter__option">
|
|
140
|
+
<input
|
|
141
|
+
type="checkbox"
|
|
142
|
+
[checked]="tempSelected().has(option)"
|
|
143
|
+
(change)="handleCheckboxChange(option, $event)"
|
|
144
|
+
/>
|
|
145
|
+
<span>{{ option }}</span>
|
|
146
|
+
</label>
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
</div>
|
|
150
|
+
<div class="ogrid-header-filter__popover-actions" style="border-top: 1px solid var(--ogrid-border, #e0e0e0);">
|
|
151
|
+
<button class="ogrid-header-filter__action-btn" [disabled]="tempSelected().size === 0" (click)="handleMultiSelectClear()">Clear</button>
|
|
152
|
+
<button class="ogrid-header-filter__action-btn ogrid-header-filter__action-btn--primary" (click)="handleMultiSelectApply()">Apply</button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
}
|
|
156
|
+
@case ('date') {
|
|
157
|
+
<div class="ogrid-header-filter__popover-body" style="width: 280px;">
|
|
158
|
+
<div style="padding: 12px;">
|
|
159
|
+
<div style="margin-bottom: 8px;">
|
|
160
|
+
<label style="display: block; margin-bottom: 4px; font-size: 13px; font-weight: 600;">From</label>
|
|
161
|
+
<input
|
|
162
|
+
type="date"
|
|
163
|
+
class="ogrid-header-filter__input"
|
|
164
|
+
[value]="tempDateFrom()"
|
|
165
|
+
(change)="tempDateFrom.set(asInputValue($event))"
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
<div>
|
|
169
|
+
<label style="display: block; margin-bottom: 4px; font-size: 13px; font-weight: 600;">To</label>
|
|
170
|
+
<input
|
|
171
|
+
type="date"
|
|
172
|
+
class="ogrid-header-filter__input"
|
|
173
|
+
[value]="tempDateTo()"
|
|
174
|
+
(change)="tempDateTo.set(asInputValue($event))"
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="ogrid-header-filter__popover-actions">
|
|
179
|
+
<button class="ogrid-header-filter__action-btn" [disabled]="!tempDateFrom() && !tempDateTo()" (click)="handleDateClear()">Clear</button>
|
|
180
|
+
<button class="ogrid-header-filter__action-btn ogrid-header-filter__action-btn--primary" (click)="handleDateApply()">Apply</button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
</div>
|
|
186
|
+
}
|
|
187
|
+
`,
|
|
188
|
+
styles: [`
|
|
189
|
+
:host {
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-direction: column;
|
|
192
|
+
height: 100%;
|
|
193
|
+
}
|
|
194
|
+
.ogrid-header-filter {
|
|
195
|
+
display: flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: space-between;
|
|
198
|
+
gap: 4px;
|
|
199
|
+
height: 100%;
|
|
200
|
+
flex: 1;
|
|
201
|
+
}
|
|
202
|
+
.ogrid-header-filter__label {
|
|
203
|
+
flex: 1;
|
|
204
|
+
min-width: 0;
|
|
205
|
+
}
|
|
206
|
+
.ogrid-header-filter__name {
|
|
207
|
+
display: block;
|
|
208
|
+
overflow: hidden;
|
|
209
|
+
text-overflow: ellipsis;
|
|
210
|
+
white-space: nowrap;
|
|
211
|
+
}
|
|
212
|
+
.ogrid-header-filter__actions {
|
|
213
|
+
display: flex;
|
|
214
|
+
gap: 2px;
|
|
215
|
+
flex-shrink: 0;
|
|
216
|
+
}
|
|
217
|
+
.ogrid-header-filter__btn {
|
|
218
|
+
position: relative;
|
|
219
|
+
min-width: 20px;
|
|
220
|
+
height: 20px;
|
|
221
|
+
padding: 0;
|
|
222
|
+
border: none;
|
|
223
|
+
border-radius: 2px;
|
|
224
|
+
background: transparent;
|
|
225
|
+
color: var(--ogrid-fg, #242424);
|
|
226
|
+
cursor: pointer;
|
|
227
|
+
font-size: 12px;
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
justify-content: center;
|
|
231
|
+
opacity: 0.6;
|
|
232
|
+
transition: all 0.15s ease;
|
|
233
|
+
}
|
|
234
|
+
.ogrid-header-filter__btn:hover {
|
|
235
|
+
opacity: 1;
|
|
236
|
+
background: var(--ogrid-hover-bg, #f0f0f0);
|
|
237
|
+
}
|
|
238
|
+
.ogrid-header-filter__btn--active {
|
|
239
|
+
opacity: 1;
|
|
240
|
+
color: var(--ogrid-active-border, #0078d4);
|
|
241
|
+
font-weight: 700;
|
|
242
|
+
}
|
|
243
|
+
.ogrid-header-filter__dot {
|
|
244
|
+
position: absolute;
|
|
245
|
+
top: 2px;
|
|
246
|
+
right: 2px;
|
|
247
|
+
width: 4px;
|
|
248
|
+
height: 4px;
|
|
249
|
+
border-radius: 50%;
|
|
250
|
+
background: var(--ogrid-active-border, #0078d4);
|
|
251
|
+
}
|
|
252
|
+
.ogrid-header-filter__popover {
|
|
253
|
+
position: fixed;
|
|
254
|
+
z-index: 1000;
|
|
255
|
+
background: var(--ogrid-bg, #ffffff);
|
|
256
|
+
border: 1px solid var(--ogrid-border, #e0e0e0);
|
|
257
|
+
border-radius: 4px;
|
|
258
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
|
259
|
+
min-width: 200px;
|
|
260
|
+
}
|
|
261
|
+
.ogrid-header-filter__popover-header {
|
|
262
|
+
padding: 8px 12px;
|
|
263
|
+
font-size: 14px;
|
|
264
|
+
font-weight: 600;
|
|
265
|
+
color: var(--ogrid-fg, #242424);
|
|
266
|
+
border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
|
|
267
|
+
background: var(--ogrid-header-bg, #f5f5f5);
|
|
268
|
+
}
|
|
269
|
+
.ogrid-header-filter__popover-body {
|
|
270
|
+
display: flex;
|
|
271
|
+
flex-direction: column;
|
|
272
|
+
}
|
|
273
|
+
.ogrid-header-filter__input {
|
|
274
|
+
width: 100%;
|
|
275
|
+
padding: 6px 8px;
|
|
276
|
+
border: 1px solid var(--ogrid-border, #e0e0e0);
|
|
277
|
+
border-radius: 4px;
|
|
278
|
+
font-size: 14px;
|
|
279
|
+
background: var(--ogrid-bg, #ffffff);
|
|
280
|
+
color: var(--ogrid-fg, #242424);
|
|
281
|
+
}
|
|
282
|
+
.ogrid-header-filter__input:focus {
|
|
283
|
+
outline: 2px solid var(--ogrid-active-border, #0078d4);
|
|
284
|
+
outline-offset: 1px;
|
|
285
|
+
}
|
|
286
|
+
.ogrid-header-filter__options-info {
|
|
287
|
+
margin-top: 6px;
|
|
288
|
+
font-size: 12px;
|
|
289
|
+
color: var(--ogrid-fg, #242424);
|
|
290
|
+
opacity: 0.7;
|
|
291
|
+
}
|
|
292
|
+
.ogrid-header-filter__select-actions {
|
|
293
|
+
display: flex;
|
|
294
|
+
gap: 8px;
|
|
295
|
+
padding: 8px 12px;
|
|
296
|
+
border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
|
|
297
|
+
}
|
|
298
|
+
.ogrid-header-filter__options-list {
|
|
299
|
+
max-height: 240px;
|
|
300
|
+
overflow-y: auto;
|
|
301
|
+
padding: 4px 0;
|
|
302
|
+
}
|
|
303
|
+
.ogrid-header-filter__option {
|
|
304
|
+
display: flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
gap: 8px;
|
|
307
|
+
padding: 6px 12px;
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
font-size: 14px;
|
|
310
|
+
color: var(--ogrid-fg, #242424);
|
|
311
|
+
transition: background 0.15s ease;
|
|
312
|
+
}
|
|
313
|
+
.ogrid-header-filter__option:hover {
|
|
314
|
+
background: var(--ogrid-hover-bg, #f0f0f0);
|
|
315
|
+
}
|
|
316
|
+
.ogrid-header-filter__loading,
|
|
317
|
+
.ogrid-header-filter__empty {
|
|
318
|
+
padding: 16px;
|
|
319
|
+
text-align: center;
|
|
320
|
+
font-size: 14px;
|
|
321
|
+
color: var(--ogrid-fg, #242424);
|
|
322
|
+
opacity: 0.7;
|
|
323
|
+
}
|
|
324
|
+
.ogrid-header-filter__popover-actions {
|
|
325
|
+
display: flex;
|
|
326
|
+
justify-content: flex-end;
|
|
327
|
+
gap: 8px;
|
|
328
|
+
padding: 8px 12px;
|
|
329
|
+
background: var(--ogrid-header-bg, #f5f5f5);
|
|
330
|
+
}
|
|
331
|
+
.ogrid-header-filter__action-btn {
|
|
332
|
+
padding: 6px 12px;
|
|
333
|
+
border: 1px solid var(--ogrid-border, #e0e0e0);
|
|
334
|
+
border-radius: 4px;
|
|
335
|
+
background: var(--ogrid-bg, #ffffff);
|
|
336
|
+
color: var(--ogrid-fg, #242424);
|
|
337
|
+
cursor: pointer;
|
|
338
|
+
font-size: 13px;
|
|
339
|
+
transition: all 0.15s ease;
|
|
340
|
+
}
|
|
341
|
+
.ogrid-header-filter__action-btn:hover:not(:disabled) {
|
|
342
|
+
background: var(--ogrid-hover-bg, #f0f0f0);
|
|
343
|
+
}
|
|
344
|
+
.ogrid-header-filter__action-btn:disabled {
|
|
345
|
+
opacity: 0.4;
|
|
346
|
+
cursor: not-allowed;
|
|
347
|
+
}
|
|
348
|
+
.ogrid-header-filter__action-btn--primary {
|
|
349
|
+
background: var(--ogrid-active-border, #0078d4);
|
|
350
|
+
color: #ffffff;
|
|
351
|
+
border-color: var(--ogrid-active-border, #0078d4);
|
|
352
|
+
}
|
|
353
|
+
.ogrid-header-filter__action-btn--primary:hover:not(:disabled) {
|
|
354
|
+
opacity: 0.9;
|
|
355
|
+
}
|
|
356
|
+
`],
|
|
357
|
+
host: {
|
|
358
|
+
'(document:click)': 'onDocumentClick($event)',
|
|
359
|
+
},
|
|
360
|
+
})
|
|
361
|
+
export class ColumnHeaderFilterComponent {
|
|
362
|
+
readonly columnKey = input.required<string>();
|
|
363
|
+
readonly columnName = input.required<string>();
|
|
364
|
+
readonly filterType = input.required<ColumnFilterType>();
|
|
365
|
+
readonly isSorted = input<boolean>(false);
|
|
366
|
+
readonly isSortedDescending = input<boolean>(false);
|
|
367
|
+
readonly onSort = input<(() => void) | undefined>(undefined);
|
|
368
|
+
readonly selectedValues = input<string[]>([]);
|
|
369
|
+
readonly onFilterChange = input<((values: string[]) => void) | undefined>(undefined);
|
|
370
|
+
readonly options = input<string[] | undefined>(undefined);
|
|
371
|
+
readonly isLoadingOptions = input<boolean>(false);
|
|
372
|
+
readonly textValue = input<string>('');
|
|
373
|
+
readonly onTextChange = input<((value: string) => void) | undefined>(undefined);
|
|
374
|
+
readonly selectedUser = input<UserLike | undefined>(undefined);
|
|
375
|
+
readonly onUserChange = input<((user: UserLike | undefined) => void) | undefined>(undefined);
|
|
376
|
+
readonly peopleSearch = input<((query: string) => Promise<UserLike[]>) | undefined>(undefined);
|
|
377
|
+
readonly dateValue = input<IDateFilterValue | undefined>(undefined);
|
|
378
|
+
readonly onDateChange = input<((value: IDateFilterValue | undefined) => void) | undefined>(undefined);
|
|
379
|
+
|
|
380
|
+
private readonly headerRef = viewChild<ElementRef<HTMLElement>>('headerEl');
|
|
381
|
+
|
|
382
|
+
readonly isFilterOpen = signal(false);
|
|
383
|
+
readonly popoverTop = signal(0);
|
|
384
|
+
readonly popoverLeft = signal(0);
|
|
385
|
+
|
|
386
|
+
// Text filter
|
|
387
|
+
readonly tempTextValue = signal('');
|
|
388
|
+
|
|
389
|
+
// MultiSelect filter
|
|
390
|
+
readonly searchText = signal('');
|
|
391
|
+
readonly tempSelected = signal(new Set<string>());
|
|
392
|
+
|
|
393
|
+
// Date filter
|
|
394
|
+
readonly tempDateFrom = signal('');
|
|
395
|
+
readonly tempDateTo = signal('');
|
|
396
|
+
|
|
397
|
+
readonly hasActiveFilter = computed(() => {
|
|
398
|
+
const ft = this.filterType();
|
|
399
|
+
if (ft === 'text') return !!this.textValue();
|
|
400
|
+
if (ft === 'multiSelect') return (this.selectedValues() ?? []).length > 0;
|
|
401
|
+
if (ft === 'date') {
|
|
402
|
+
const dv = this.dateValue();
|
|
403
|
+
return !!dv && (!!dv.from || !!dv.to);
|
|
404
|
+
}
|
|
405
|
+
return false;
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
readonly filteredOptions = computed(() => {
|
|
409
|
+
const search = this.searchText().toLowerCase();
|
|
410
|
+
const opts = this.options() ?? [];
|
|
411
|
+
if (!search) return opts;
|
|
412
|
+
return opts.filter(o => o.toLowerCase().includes(search));
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
toggleFilter(event: MouseEvent): void {
|
|
416
|
+
event.stopPropagation();
|
|
417
|
+
|
|
418
|
+
if (this.isFilterOpen()) {
|
|
419
|
+
this.isFilterOpen.set(false);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Initialize temp values
|
|
424
|
+
if (this.filterType() === 'text') {
|
|
425
|
+
this.tempTextValue.set(this.textValue() ?? '');
|
|
426
|
+
} else if (this.filterType() === 'multiSelect') {
|
|
427
|
+
this.tempSelected.set(new Set(this.selectedValues() ?? []));
|
|
428
|
+
this.searchText.set('');
|
|
429
|
+
} else if (this.filterType() === 'date') {
|
|
430
|
+
const dv = this.dateValue();
|
|
431
|
+
this.tempDateFrom.set(dv?.from ?? '');
|
|
432
|
+
this.tempDateTo.set(dv?.to ?? '');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Calculate popover position
|
|
436
|
+
const headerEl = this.headerRef()?.nativeElement;
|
|
437
|
+
if (headerEl) {
|
|
438
|
+
const rect = headerEl.getBoundingClientRect();
|
|
439
|
+
this.popoverTop.set(rect.bottom + 4);
|
|
440
|
+
this.popoverLeft.set(rect.left);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
this.isFilterOpen.set(true);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
onDocumentClick(event: MouseEvent): void {
|
|
447
|
+
const el = event.target as HTMLElement;
|
|
448
|
+
if (!el.closest('column-header-filter')) {
|
|
449
|
+
this.isFilterOpen.set(false);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
asInputValue(event: Event): string {
|
|
454
|
+
return (event.target as HTMLInputElement).value;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Text filter handlers
|
|
458
|
+
onTextKeydown(event: KeyboardEvent): void {
|
|
459
|
+
if (event.key === 'Enter') {
|
|
460
|
+
this.handleTextApply();
|
|
461
|
+
} else if (event.key === 'Escape') {
|
|
462
|
+
this.isFilterOpen.set(false);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
handleTextApply(): void {
|
|
467
|
+
this.onTextChange()?.(this.tempTextValue());
|
|
468
|
+
this.isFilterOpen.set(false);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
handleTextClear(): void {
|
|
472
|
+
this.tempTextValue.set('');
|
|
473
|
+
this.onTextChange()?.('');
|
|
474
|
+
this.isFilterOpen.set(false);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// MultiSelect filter handlers
|
|
478
|
+
handleCheckboxChange(option: string, event: Event): void {
|
|
479
|
+
const checked = (event.target as HTMLInputElement).checked;
|
|
480
|
+
const newSet = new Set(this.tempSelected());
|
|
481
|
+
if (checked) {
|
|
482
|
+
newSet.add(option);
|
|
483
|
+
} else {
|
|
484
|
+
newSet.delete(option);
|
|
485
|
+
}
|
|
486
|
+
this.tempSelected.set(newSet);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
handleSelectAllFiltered(): void {
|
|
490
|
+
const newSet = new Set(this.tempSelected());
|
|
491
|
+
for (const opt of this.filteredOptions()) {
|
|
492
|
+
newSet.add(opt);
|
|
493
|
+
}
|
|
494
|
+
this.tempSelected.set(newSet);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
handleClearSelection(): void {
|
|
498
|
+
this.tempSelected.set(new Set());
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
handleMultiSelectApply(): void {
|
|
502
|
+
this.onFilterChange()?.([...this.tempSelected()]);
|
|
503
|
+
this.isFilterOpen.set(false);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
handleMultiSelectClear(): void {
|
|
507
|
+
this.tempSelected.set(new Set());
|
|
508
|
+
this.onFilterChange()?.([]);
|
|
509
|
+
this.isFilterOpen.set(false);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Date filter handlers
|
|
513
|
+
handleDateApply(): void {
|
|
514
|
+
const from = this.tempDateFrom();
|
|
515
|
+
const to = this.tempDateTo();
|
|
516
|
+
if (from || to) {
|
|
517
|
+
this.onDateChange()?.({ from, to });
|
|
518
|
+
}
|
|
519
|
+
this.isFilterOpen.set(false);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
handleDateClear(): void {
|
|
523
|
+
this.tempDateFrom.set('');
|
|
524
|
+
this.tempDateTo.set('');
|
|
525
|
+
this.onDateChange()?.({ from: '', to: '' });
|
|
526
|
+
this.isFilterOpen.set(false);
|
|
527
|
+
}
|
|
528
|
+
}
|