ngx-com 0.0.1 → 0.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/fesm2022/ngx-com-components-avatar.mjs +772 -0
- package/fesm2022/ngx-com-components-avatar.mjs.map +1 -0
- package/fesm2022/ngx-com-components-badge.mjs +138 -0
- package/fesm2022/ngx-com-components-badge.mjs.map +1 -0
- package/fesm2022/ngx-com-components-button.mjs +146 -0
- package/fesm2022/ngx-com-components-button.mjs.map +1 -0
- package/fesm2022/ngx-com-components-calendar.mjs +5046 -0
- package/fesm2022/ngx-com-components-calendar.mjs.map +1 -0
- package/fesm2022/ngx-com-components-card.mjs +590 -0
- package/fesm2022/ngx-com-components-card.mjs.map +1 -0
- package/fesm2022/ngx-com-components-checkbox.mjs +344 -0
- package/fesm2022/ngx-com-components-checkbox.mjs.map +1 -0
- package/fesm2022/ngx-com-components-collapsible.mjs +612 -0
- package/fesm2022/ngx-com-components-collapsible.mjs.map +1 -0
- package/fesm2022/ngx-com-components-confirm.mjs +562 -0
- package/fesm2022/ngx-com-components-confirm.mjs.map +1 -0
- package/fesm2022/ngx-com-components-dropdown-testing.mjs +255 -0
- package/fesm2022/ngx-com-components-dropdown-testing.mjs.map +1 -0
- package/fesm2022/ngx-com-components-dropdown.mjs +2692 -0
- package/fesm2022/ngx-com-components-dropdown.mjs.map +1 -0
- package/fesm2022/ngx-com-components-empty-state.mjs +382 -0
- package/fesm2022/ngx-com-components-empty-state.mjs.map +1 -0
- package/fesm2022/ngx-com-components-form-field.mjs +924 -0
- package/fesm2022/ngx-com-components-form-field.mjs.map +1 -0
- package/fesm2022/ngx-com-components-icon.mjs +183 -0
- package/fesm2022/ngx-com-components-icon.mjs.map +1 -0
- package/fesm2022/ngx-com-components-item.mjs +578 -0
- package/fesm2022/ngx-com-components-item.mjs.map +1 -0
- package/fesm2022/ngx-com-components-menu.mjs +1200 -0
- package/fesm2022/ngx-com-components-menu.mjs.map +1 -0
- package/fesm2022/ngx-com-components-paginator.mjs +823 -0
- package/fesm2022/ngx-com-components-paginator.mjs.map +1 -0
- package/fesm2022/ngx-com-components-popover.mjs +901 -0
- package/fesm2022/ngx-com-components-popover.mjs.map +1 -0
- package/fesm2022/ngx-com-components-radio.mjs +621 -0
- package/fesm2022/ngx-com-components-radio.mjs.map +1 -0
- package/fesm2022/ngx-com-components-segmented-control.mjs +538 -0
- package/fesm2022/ngx-com-components-segmented-control.mjs.map +1 -0
- package/fesm2022/ngx-com-components-sort.mjs +368 -0
- package/fesm2022/ngx-com-components-sort.mjs.map +1 -0
- package/fesm2022/ngx-com-components-spinner.mjs +189 -0
- package/fesm2022/ngx-com-components-spinner.mjs.map +1 -0
- package/fesm2022/ngx-com-components-tabs.mjs +1522 -0
- package/fesm2022/ngx-com-components-tabs.mjs.map +1 -0
- package/fesm2022/ngx-com-components-tooltip.mjs +625 -0
- package/fesm2022/ngx-com-components-tooltip.mjs.map +1 -0
- package/fesm2022/ngx-com-components.mjs +17 -0
- package/fesm2022/ngx-com-components.mjs.map +1 -0
- package/fesm2022/ngx-com-tokens.mjs +12 -0
- package/fesm2022/ngx-com-tokens.mjs.map +1 -0
- package/fesm2022/ngx-com-utils.mjs +601 -0
- package/fesm2022/ngx-com-utils.mjs.map +1 -0
- package/fesm2022/ngx-com.mjs +9 -23
- package/fesm2022/ngx-com.mjs.map +1 -1
- package/package.json +105 -1
- package/types/ngx-com-components-avatar.d.ts +409 -0
- package/types/ngx-com-components-badge.d.ts +97 -0
- package/types/ngx-com-components-button.d.ts +69 -0
- package/types/ngx-com-components-calendar.d.ts +1665 -0
- package/types/ngx-com-components-card.d.ts +373 -0
- package/types/ngx-com-components-checkbox.d.ts +116 -0
- package/types/ngx-com-components-collapsible.d.ts +379 -0
- package/types/ngx-com-components-confirm.d.ts +160 -0
- package/types/ngx-com-components-dropdown-testing.d.ts +116 -0
- package/types/ngx-com-components-dropdown.d.ts +938 -0
- package/types/ngx-com-components-empty-state.d.ts +269 -0
- package/types/ngx-com-components-form-field.d.ts +531 -0
- package/types/ngx-com-components-icon.d.ts +94 -0
- package/types/ngx-com-components-item.d.ts +336 -0
- package/types/ngx-com-components-menu.d.ts +479 -0
- package/types/ngx-com-components-paginator.d.ts +265 -0
- package/types/ngx-com-components-popover.d.ts +309 -0
- package/types/ngx-com-components-radio.d.ts +258 -0
- package/types/ngx-com-components-segmented-control.d.ts +274 -0
- package/types/ngx-com-components-sort.d.ts +133 -0
- package/types/ngx-com-components-spinner.d.ts +120 -0
- package/types/ngx-com-components-tabs.d.ts +396 -0
- package/types/ngx-com-components-tooltip.d.ts +200 -0
- package/types/ngx-com-components.d.ts +12 -0
- package/types/ngx-com-tokens.d.ts +7 -0
- package/types/ngx-com-utils.d.ts +424 -0
- package/types/ngx-com.d.ts +10 -7
|
@@ -0,0 +1,2692 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { viewChild, input, output, computed, ChangeDetectionStrategy, Component, inject, DestroyRef, signal, TemplateRef, Directive, ElementRef, ViewContainerRef, contentChild, linkedSignal, forwardRef } from '@angular/core';
|
|
3
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
5
|
+
import { NgForm, FormGroupDirective, NgControl } from '@angular/forms';
|
|
6
|
+
import { ErrorStateMatcher, FormFieldControl } from 'ngx-com/components/form-field';
|
|
7
|
+
import { Overlay, OverlayModule } from '@angular/cdk/overlay';
|
|
8
|
+
import { TemplatePortal } from '@angular/cdk/portal';
|
|
9
|
+
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
|
10
|
+
import { cva } from 'class-variance-authority';
|
|
11
|
+
import { mergeClasses } from 'ngx-com/utils';
|
|
12
|
+
import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf } from '@angular/cdk/scrolling';
|
|
13
|
+
import { Subject } from 'rxjs';
|
|
14
|
+
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* CVA variants for the dropdown trigger button.
|
|
18
|
+
* Uses semantic theme tokens for consistent cross-theme styling.
|
|
19
|
+
*
|
|
20
|
+
* @tokens `--color-input-background`, `--color-input-foreground`, `--color-input-border`,
|
|
21
|
+
* `--color-input-placeholder`, `--color-ring`, `--color-muted`, `--color-muted-hover`,
|
|
22
|
+
* `--color-warn`, `--color-success`, `--color-primary`, `--color-border`, `--radius-input`
|
|
23
|
+
*/
|
|
24
|
+
const dropdownTriggerVariants = cva([
|
|
25
|
+
'inline-flex',
|
|
26
|
+
'items-center',
|
|
27
|
+
'justify-between',
|
|
28
|
+
'w-full',
|
|
29
|
+
'rounded-input',
|
|
30
|
+
'border',
|
|
31
|
+
'bg-input-background',
|
|
32
|
+
'text-input-foreground',
|
|
33
|
+
'ring-offset-background',
|
|
34
|
+
'transition-colors',
|
|
35
|
+
'duration-150',
|
|
36
|
+
'placeholder:text-input-placeholder',
|
|
37
|
+
'focus:outline-none',
|
|
38
|
+
'focus:ring-2',
|
|
39
|
+
'focus:ring-offset-2',
|
|
40
|
+
'focus:ring-ring',
|
|
41
|
+
'disabled:cursor-not-allowed',
|
|
42
|
+
'disabled:bg-disabled',
|
|
43
|
+
'disabled:text-disabled-foreground',
|
|
44
|
+
], {
|
|
45
|
+
variants: {
|
|
46
|
+
variant: {
|
|
47
|
+
default: [
|
|
48
|
+
'border-input-border',
|
|
49
|
+
'hover:border-border',
|
|
50
|
+
],
|
|
51
|
+
outline: [
|
|
52
|
+
'border-2',
|
|
53
|
+
'border-input-border',
|
|
54
|
+
'hover:border-foreground',
|
|
55
|
+
],
|
|
56
|
+
ghost: [
|
|
57
|
+
'border-transparent',
|
|
58
|
+
'bg-transparent',
|
|
59
|
+
'hover:bg-muted',
|
|
60
|
+
],
|
|
61
|
+
filled: [
|
|
62
|
+
'border-transparent',
|
|
63
|
+
'bg-muted',
|
|
64
|
+
'hover:bg-muted-hover',
|
|
65
|
+
],
|
|
66
|
+
naked: [
|
|
67
|
+
'border-transparent',
|
|
68
|
+
'bg-transparent',
|
|
69
|
+
'shadow-none',
|
|
70
|
+
'focus:ring-0',
|
|
71
|
+
'focus:ring-offset-0',
|
|
72
|
+
'rounded-none',
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
size: {
|
|
76
|
+
sm: ['h-8', 'px-2', 'text-xs', 'gap-1'],
|
|
77
|
+
default: ['h-10', 'px-3', 'text-sm', 'gap-2'],
|
|
78
|
+
lg: ['h-12', 'px-4', 'text-base', 'gap-3'],
|
|
79
|
+
},
|
|
80
|
+
state: {
|
|
81
|
+
default: [],
|
|
82
|
+
error: [
|
|
83
|
+
'border-warn',
|
|
84
|
+
'focus:ring-warn',
|
|
85
|
+
],
|
|
86
|
+
success: [
|
|
87
|
+
'border-success',
|
|
88
|
+
'focus:ring-success',
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
open: {
|
|
92
|
+
true: ['ring-2', 'ring-ring', 'border-primary'],
|
|
93
|
+
false: [],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
compoundVariants: [
|
|
97
|
+
{
|
|
98
|
+
open: true,
|
|
99
|
+
variant: 'default',
|
|
100
|
+
class: ['border-primary'],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
open: true,
|
|
104
|
+
variant: 'outline',
|
|
105
|
+
class: ['border-primary'],
|
|
106
|
+
},
|
|
107
|
+
// Naked variant should not show ring when open (form-field provides focus styling)
|
|
108
|
+
{
|
|
109
|
+
open: true,
|
|
110
|
+
variant: 'naked',
|
|
111
|
+
class: ['ring-0', 'border-transparent'],
|
|
112
|
+
},
|
|
113
|
+
// Naked variant should not show error border (form-field provides error styling)
|
|
114
|
+
{
|
|
115
|
+
state: 'error',
|
|
116
|
+
variant: 'naked',
|
|
117
|
+
class: ['border-transparent', 'focus:ring-0'],
|
|
118
|
+
},
|
|
119
|
+
// Naked variant should not show success border (form-field provides styling)
|
|
120
|
+
{
|
|
121
|
+
state: 'success',
|
|
122
|
+
variant: 'naked',
|
|
123
|
+
class: ['border-transparent', 'focus:ring-0'],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
defaultVariants: {
|
|
127
|
+
variant: 'default',
|
|
128
|
+
size: 'default',
|
|
129
|
+
state: 'default',
|
|
130
|
+
open: false,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
/**
|
|
134
|
+
* CVA variants for the dropdown panel (overlay).
|
|
135
|
+
*
|
|
136
|
+
* @tokens `--color-popover`, `--color-popover-foreground`, `--color-border-subtle`, `--radius-overlay`
|
|
137
|
+
*/
|
|
138
|
+
const dropdownPanelVariants = cva([
|
|
139
|
+
'z-50',
|
|
140
|
+
'overflow-hidden',
|
|
141
|
+
'rounded-overlay',
|
|
142
|
+
'border',
|
|
143
|
+
'border-border-subtle',
|
|
144
|
+
'bg-popover',
|
|
145
|
+
'text-popover-foreground',
|
|
146
|
+
'shadow-lg',
|
|
147
|
+
'outline-none',
|
|
148
|
+
], {
|
|
149
|
+
variants: {
|
|
150
|
+
size: {
|
|
151
|
+
sm: ['text-xs'],
|
|
152
|
+
default: ['text-sm'],
|
|
153
|
+
lg: ['text-base'],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
defaultVariants: {
|
|
157
|
+
size: 'default',
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
/**
|
|
161
|
+
* CVA variants for individual dropdown options.
|
|
162
|
+
*
|
|
163
|
+
* @tokens `--color-popover-foreground`, `--color-muted`, `--color-primary-subtle`,
|
|
164
|
+
* `--color-primary-subtle-foreground`, `--color-disabled-foreground`
|
|
165
|
+
*/
|
|
166
|
+
const dropdownOptionVariants = cva([
|
|
167
|
+
'relative',
|
|
168
|
+
'flex',
|
|
169
|
+
'w-full',
|
|
170
|
+
'cursor-pointer',
|
|
171
|
+
'select-none',
|
|
172
|
+
'items-center',
|
|
173
|
+
'outline-none',
|
|
174
|
+
'transition-colors',
|
|
175
|
+
'duration-100',
|
|
176
|
+
], {
|
|
177
|
+
variants: {
|
|
178
|
+
size: {
|
|
179
|
+
sm: ['px-2', 'py-1.5', 'text-xs'],
|
|
180
|
+
default: ['px-3', 'py-2', 'text-sm'],
|
|
181
|
+
lg: ['px-4', 'py-3', 'text-base'],
|
|
182
|
+
},
|
|
183
|
+
state: {
|
|
184
|
+
default: [
|
|
185
|
+
'text-popover-foreground',
|
|
186
|
+
'hover:bg-muted',
|
|
187
|
+
],
|
|
188
|
+
active: [
|
|
189
|
+
'bg-muted',
|
|
190
|
+
'text-popover-foreground',
|
|
191
|
+
],
|
|
192
|
+
selected: [
|
|
193
|
+
'bg-primary-subtle',
|
|
194
|
+
'text-primary-subtle-foreground',
|
|
195
|
+
],
|
|
196
|
+
'selected-active': [
|
|
197
|
+
'bg-primary-subtle',
|
|
198
|
+
'text-primary-subtle-foreground',
|
|
199
|
+
'brightness-95',
|
|
200
|
+
],
|
|
201
|
+
disabled: [
|
|
202
|
+
'cursor-not-allowed',
|
|
203
|
+
'text-disabled-foreground',
|
|
204
|
+
'hover:bg-transparent',
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
defaultVariants: {
|
|
209
|
+
size: 'default',
|
|
210
|
+
state: 'default',
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
/**
|
|
214
|
+
* CVA variants for the search input.
|
|
215
|
+
*
|
|
216
|
+
* @tokens `--color-border-subtle`, `--color-input-placeholder`, `--color-disabled-foreground`
|
|
217
|
+
*/
|
|
218
|
+
const dropdownSearchVariants = cva([
|
|
219
|
+
'flex',
|
|
220
|
+
'h-10',
|
|
221
|
+
'w-full',
|
|
222
|
+
'border-b',
|
|
223
|
+
'border-border-subtle',
|
|
224
|
+
'bg-transparent',
|
|
225
|
+
'px-3',
|
|
226
|
+
'py-2',
|
|
227
|
+
'text-sm',
|
|
228
|
+
'placeholder:text-input-placeholder',
|
|
229
|
+
'outline-none',
|
|
230
|
+
'disabled:cursor-not-allowed',
|
|
231
|
+
'disabled:text-disabled-foreground',
|
|
232
|
+
], {
|
|
233
|
+
variants: {
|
|
234
|
+
size: {
|
|
235
|
+
sm: ['h-8', 'px-2', 'text-xs'],
|
|
236
|
+
default: ['h-10', 'px-3', 'text-sm'],
|
|
237
|
+
lg: ['h-12', 'px-4', 'text-base'],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
defaultVariants: {
|
|
241
|
+
size: 'default',
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
/**
|
|
245
|
+
* CVA variants for multi-select tags.
|
|
246
|
+
*
|
|
247
|
+
* @tokens `--color-muted`, `--color-muted-foreground`, `--color-muted-hover`,
|
|
248
|
+
* `--color-primary-subtle`, `--color-primary-subtle-foreground`, `--radius-tag`
|
|
249
|
+
*/
|
|
250
|
+
const dropdownTagVariants = cva([
|
|
251
|
+
'inline-flex',
|
|
252
|
+
'items-center',
|
|
253
|
+
'gap-1',
|
|
254
|
+
'rounded-tag',
|
|
255
|
+
'font-medium',
|
|
256
|
+
'transition-colors',
|
|
257
|
+
'duration-100',
|
|
258
|
+
], {
|
|
259
|
+
variants: {
|
|
260
|
+
size: {
|
|
261
|
+
sm: ['h-5', 'px-1.5', 'text-xs'],
|
|
262
|
+
default: ['h-6', 'px-2', 'text-xs'],
|
|
263
|
+
lg: ['h-7', 'px-2.5', 'text-sm'],
|
|
264
|
+
},
|
|
265
|
+
variant: {
|
|
266
|
+
default: [
|
|
267
|
+
'bg-muted',
|
|
268
|
+
'text-muted-foreground',
|
|
269
|
+
'hover:bg-muted-hover',
|
|
270
|
+
],
|
|
271
|
+
primary: [
|
|
272
|
+
'bg-primary-subtle',
|
|
273
|
+
'text-primary-subtle-foreground',
|
|
274
|
+
'hover:brightness-95',
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
defaultVariants: {
|
|
279
|
+
size: 'default',
|
|
280
|
+
variant: 'primary',
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
/**
|
|
284
|
+
* CVA variants for the tag remove button.
|
|
285
|
+
*
|
|
286
|
+
* @tokens `--color-ring`, `--radius-interactive-sm`
|
|
287
|
+
*/
|
|
288
|
+
const dropdownTagRemoveVariants = cva([
|
|
289
|
+
'inline-flex',
|
|
290
|
+
'items-center',
|
|
291
|
+
'justify-center',
|
|
292
|
+
'rounded-interactive-sm',
|
|
293
|
+
'opacity-70',
|
|
294
|
+
'transition-opacity',
|
|
295
|
+
'hover:opacity-100',
|
|
296
|
+
'focus:outline-none',
|
|
297
|
+
'focus:ring-1',
|
|
298
|
+
'focus:ring-ring',
|
|
299
|
+
], {
|
|
300
|
+
variants: {
|
|
301
|
+
size: {
|
|
302
|
+
sm: ['h-3', 'w-3'],
|
|
303
|
+
default: ['h-3.5', 'w-3.5'],
|
|
304
|
+
lg: ['h-4', 'w-4'],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
defaultVariants: {
|
|
308
|
+
size: 'default',
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
/**
|
|
312
|
+
* CVA variants for the overflow badge (+N indicator).
|
|
313
|
+
*
|
|
314
|
+
* @tokens `--color-muted`, `--color-muted-foreground`, `--radius-tag`
|
|
315
|
+
*/
|
|
316
|
+
const dropdownOverflowBadgeVariants = cva([
|
|
317
|
+
'inline-flex',
|
|
318
|
+
'items-center',
|
|
319
|
+
'justify-center',
|
|
320
|
+
'rounded-tag',
|
|
321
|
+
'font-medium',
|
|
322
|
+
'text-muted-foreground',
|
|
323
|
+
'bg-muted',
|
|
324
|
+
], {
|
|
325
|
+
variants: {
|
|
326
|
+
size: {
|
|
327
|
+
sm: ['h-5', 'px-1.5', 'text-xs'],
|
|
328
|
+
default: ['h-6', 'px-2', 'text-xs'],
|
|
329
|
+
lg: ['h-7', 'px-2.5', 'text-sm'],
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
defaultVariants: {
|
|
333
|
+
size: 'default',
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
/**
|
|
337
|
+
* CVA variants for group headers.
|
|
338
|
+
*
|
|
339
|
+
* @tokens `--color-muted-foreground`
|
|
340
|
+
*/
|
|
341
|
+
const dropdownGroupVariants = cva([
|
|
342
|
+
'flex',
|
|
343
|
+
'items-center',
|
|
344
|
+
'px-3',
|
|
345
|
+
'py-2',
|
|
346
|
+
'text-xs',
|
|
347
|
+
'font-semibold',
|
|
348
|
+
'uppercase',
|
|
349
|
+
'tracking-wider',
|
|
350
|
+
'text-muted-foreground',
|
|
351
|
+
], {
|
|
352
|
+
variants: {
|
|
353
|
+
size: {
|
|
354
|
+
sm: ['px-2', 'py-1.5'],
|
|
355
|
+
default: ['px-3', 'py-2'],
|
|
356
|
+
lg: ['px-4', 'py-2.5'],
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
defaultVariants: {
|
|
360
|
+
size: 'default',
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
/**
|
|
364
|
+
* CVA variants for the empty state.
|
|
365
|
+
*
|
|
366
|
+
* @tokens `--color-muted-foreground`
|
|
367
|
+
*/
|
|
368
|
+
const dropdownEmptyVariants = cva([
|
|
369
|
+
'flex',
|
|
370
|
+
'items-center',
|
|
371
|
+
'justify-center',
|
|
372
|
+
'px-3',
|
|
373
|
+
'py-6',
|
|
374
|
+
'text-muted-foreground',
|
|
375
|
+
], {
|
|
376
|
+
variants: {
|
|
377
|
+
size: {
|
|
378
|
+
sm: ['px-2', 'py-4', 'text-xs'],
|
|
379
|
+
default: ['px-3', 'py-6', 'text-sm'],
|
|
380
|
+
lg: ['px-4', 'py-8', 'text-base'],
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
defaultVariants: {
|
|
384
|
+
size: 'default',
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
/**
|
|
388
|
+
* CVA variants for the clear button.
|
|
389
|
+
*
|
|
390
|
+
* @tokens `--color-ring`, `--radius-interactive-sm`
|
|
391
|
+
*/
|
|
392
|
+
const dropdownClearVariants = cva([
|
|
393
|
+
'inline-flex',
|
|
394
|
+
'items-center',
|
|
395
|
+
'justify-center',
|
|
396
|
+
'rounded-interactive-sm',
|
|
397
|
+
'opacity-50',
|
|
398
|
+
'transition-opacity',
|
|
399
|
+
'hover:opacity-100',
|
|
400
|
+
'focus:outline-none',
|
|
401
|
+
'focus:ring-1',
|
|
402
|
+
'focus:ring-ring',
|
|
403
|
+
], {
|
|
404
|
+
variants: {
|
|
405
|
+
size: {
|
|
406
|
+
sm: ['h-4', 'w-4'],
|
|
407
|
+
default: ['h-5', 'w-5'],
|
|
408
|
+
lg: ['h-6', 'w-6'],
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
defaultVariants: {
|
|
412
|
+
size: 'default',
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
/**
|
|
416
|
+
* CVA variants for the chevron icon.
|
|
417
|
+
*/
|
|
418
|
+
const dropdownChevronVariants = cva([
|
|
419
|
+
'shrink-0',
|
|
420
|
+
'opacity-50',
|
|
421
|
+
'transition-transform',
|
|
422
|
+
'duration-200',
|
|
423
|
+
], {
|
|
424
|
+
variants: {
|
|
425
|
+
size: {
|
|
426
|
+
sm: ['h-3', 'w-3'],
|
|
427
|
+
default: ['h-4', 'w-4'],
|
|
428
|
+
lg: ['h-5', 'w-5'],
|
|
429
|
+
},
|
|
430
|
+
open: {
|
|
431
|
+
true: ['rotate-180'],
|
|
432
|
+
false: ['rotate-0'],
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
defaultVariants: {
|
|
436
|
+
size: 'default',
|
|
437
|
+
open: false,
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* A single option in the dropdown list.
|
|
443
|
+
* Implements CDK's Highlightable interface for keyboard navigation.
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```html
|
|
447
|
+
* <com-dropdown-option
|
|
448
|
+
* [value]="user"
|
|
449
|
+
* [selected]="isSelected(user)"
|
|
450
|
+
* [disabled]="user.inactive"
|
|
451
|
+
* (select)="onSelect(user)"
|
|
452
|
+
* />
|
|
453
|
+
* ```
|
|
454
|
+
*
|
|
455
|
+
* @tokens `--color-popover-foreground`, `--color-muted`, `--color-primary-subtle`,
|
|
456
|
+
* `--color-primary-subtle-foreground`, `--color-disabled-foreground`
|
|
457
|
+
*/
|
|
458
|
+
class ComDropdownOption {
|
|
459
|
+
/** Reference to the option element for focus management. */
|
|
460
|
+
optionRef = viewChild('optionElement', ...(ngDevMode ? [{ debugName: "optionRef" }] : []));
|
|
461
|
+
/** The value this option represents. */
|
|
462
|
+
value = input.required(...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
463
|
+
/** Display text for this option (when no template is provided). */
|
|
464
|
+
displayText = input('', ...(ngDevMode ? [{ debugName: "displayText" }] : []));
|
|
465
|
+
/** Unique identifier for this option. */
|
|
466
|
+
id = input.required(...(ngDevMode ? [{ debugName: "id" }] : []));
|
|
467
|
+
/** Index of this option in the list. */
|
|
468
|
+
index = input(0, ...(ngDevMode ? [{ debugName: "index" }] : []));
|
|
469
|
+
/** Whether this option is currently selected. */
|
|
470
|
+
selected = input(false, ...(ngDevMode ? [{ debugName: "selected" }] : []));
|
|
471
|
+
/** Whether this option is currently active (keyboard focused). */
|
|
472
|
+
active = input(false, ...(ngDevMode ? [{ debugName: "active" }] : []));
|
|
473
|
+
/** Whether this option is disabled. */
|
|
474
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
475
|
+
/** Size variant for styling. */
|
|
476
|
+
size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
477
|
+
/** Custom template for rendering the option content. */
|
|
478
|
+
optionTemplate = input(null, ...(ngDevMode ? [{ debugName: "optionTemplate" }] : []));
|
|
479
|
+
/** Additional CSS classes to apply. */
|
|
480
|
+
userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
|
|
481
|
+
/** Emitted when the option is selected. */
|
|
482
|
+
select = output();
|
|
483
|
+
/** Emitted when the mouse enters the option. */
|
|
484
|
+
hover = output();
|
|
485
|
+
/** Computed option state for CVA styling. */
|
|
486
|
+
optionState = computed(() => {
|
|
487
|
+
if (this.disabled())
|
|
488
|
+
return 'disabled';
|
|
489
|
+
if (this.selected() && this.active())
|
|
490
|
+
return 'selected-active';
|
|
491
|
+
if (this.selected())
|
|
492
|
+
return 'selected';
|
|
493
|
+
if (this.active())
|
|
494
|
+
return 'active';
|
|
495
|
+
return 'default';
|
|
496
|
+
}, ...(ngDevMode ? [{ debugName: "optionState" }] : []));
|
|
497
|
+
/** Computed CSS classes for the option. */
|
|
498
|
+
optionClasses = computed(() => {
|
|
499
|
+
const baseClasses = dropdownOptionVariants({
|
|
500
|
+
size: this.size(),
|
|
501
|
+
state: this.optionState(),
|
|
502
|
+
});
|
|
503
|
+
return mergeClasses(baseClasses, this.userClass());
|
|
504
|
+
}, ...(ngDevMode ? [{ debugName: "optionClasses" }] : []));
|
|
505
|
+
/** Template context for custom option templates. */
|
|
506
|
+
templateContext = computed(() => ({
|
|
507
|
+
$implicit: this.value(),
|
|
508
|
+
index: this.index(),
|
|
509
|
+
selected: this.selected(),
|
|
510
|
+
active: this.active(),
|
|
511
|
+
disabled: this.disabled(),
|
|
512
|
+
}), ...(ngDevMode ? [{ debugName: "templateContext" }] : []));
|
|
513
|
+
onOptionClick(event) {
|
|
514
|
+
if (this.disabled()) {
|
|
515
|
+
event.preventDefault();
|
|
516
|
+
event.stopPropagation();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
this.select.emit(this.value());
|
|
520
|
+
}
|
|
521
|
+
onMouseEnter() {
|
|
522
|
+
if (!this.disabled()) {
|
|
523
|
+
this.hover.emit(this.value());
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/** Scrolls this option into view. */
|
|
527
|
+
scrollIntoView() {
|
|
528
|
+
this.optionRef()?.nativeElement.scrollIntoView({
|
|
529
|
+
block: 'nearest',
|
|
530
|
+
inline: 'nearest',
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/** Focuses this option element. */
|
|
534
|
+
focus() {
|
|
535
|
+
this.optionRef()?.nativeElement.focus();
|
|
536
|
+
}
|
|
537
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownOption, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
538
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdownOption, isStandalone: true, selector: "com-dropdown-option", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, displayText: { classPropertyName: "displayText", publicName: "displayText", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, optionTemplate: { classPropertyName: "optionTemplate", publicName: "optionTemplate", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select", hover: "hover" }, host: { classAttribute: "com-dropdown-option-host block" }, viewQueries: [{ propertyName: "optionRef", first: true, predicate: ["optionElement"], descendants: true, isSignal: true }], exportAs: ["comDropdownOption"], ngImport: i0, template: `
|
|
539
|
+
<div
|
|
540
|
+
#optionElement
|
|
541
|
+
[class]="optionClasses()"
|
|
542
|
+
[attr.role]="'option'"
|
|
543
|
+
[attr.id]="id()"
|
|
544
|
+
[attr.aria-selected]="selected()"
|
|
545
|
+
[attr.aria-disabled]="disabled() || null"
|
|
546
|
+
[attr.data-active]="active() || null"
|
|
547
|
+
[attr.data-selected]="selected() || null"
|
|
548
|
+
[attr.data-disabled]="disabled() || null"
|
|
549
|
+
(click)="onOptionClick($event)"
|
|
550
|
+
(mouseenter)="onMouseEnter()"
|
|
551
|
+
>
|
|
552
|
+
@if (optionTemplate()) {
|
|
553
|
+
<ng-container
|
|
554
|
+
[ngTemplateOutlet]="optionTemplate()!"
|
|
555
|
+
[ngTemplateOutletContext]="templateContext()"
|
|
556
|
+
/>
|
|
557
|
+
} @else {
|
|
558
|
+
<span class="truncate">{{ displayText() }}</span>
|
|
559
|
+
@if (selected()) {
|
|
560
|
+
<svg
|
|
561
|
+
class="ml-auto h-4 w-4 shrink-0"
|
|
562
|
+
viewBox="0 0 24 24"
|
|
563
|
+
fill="none"
|
|
564
|
+
stroke="currentColor"
|
|
565
|
+
stroke-width="2"
|
|
566
|
+
stroke-linecap="round"
|
|
567
|
+
stroke-linejoin="round"
|
|
568
|
+
>
|
|
569
|
+
<polyline points="20 6 9 17 4 12" />
|
|
570
|
+
</svg>
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
</div>
|
|
574
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
575
|
+
}
|
|
576
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownOption, decorators: [{
|
|
577
|
+
type: Component,
|
|
578
|
+
args: [{
|
|
579
|
+
selector: 'com-dropdown-option',
|
|
580
|
+
exportAs: 'comDropdownOption',
|
|
581
|
+
template: `
|
|
582
|
+
<div
|
|
583
|
+
#optionElement
|
|
584
|
+
[class]="optionClasses()"
|
|
585
|
+
[attr.role]="'option'"
|
|
586
|
+
[attr.id]="id()"
|
|
587
|
+
[attr.aria-selected]="selected()"
|
|
588
|
+
[attr.aria-disabled]="disabled() || null"
|
|
589
|
+
[attr.data-active]="active() || null"
|
|
590
|
+
[attr.data-selected]="selected() || null"
|
|
591
|
+
[attr.data-disabled]="disabled() || null"
|
|
592
|
+
(click)="onOptionClick($event)"
|
|
593
|
+
(mouseenter)="onMouseEnter()"
|
|
594
|
+
>
|
|
595
|
+
@if (optionTemplate()) {
|
|
596
|
+
<ng-container
|
|
597
|
+
[ngTemplateOutlet]="optionTemplate()!"
|
|
598
|
+
[ngTemplateOutletContext]="templateContext()"
|
|
599
|
+
/>
|
|
600
|
+
} @else {
|
|
601
|
+
<span class="truncate">{{ displayText() }}</span>
|
|
602
|
+
@if (selected()) {
|
|
603
|
+
<svg
|
|
604
|
+
class="ml-auto h-4 w-4 shrink-0"
|
|
605
|
+
viewBox="0 0 24 24"
|
|
606
|
+
fill="none"
|
|
607
|
+
stroke="currentColor"
|
|
608
|
+
stroke-width="2"
|
|
609
|
+
stroke-linecap="round"
|
|
610
|
+
stroke-linejoin="round"
|
|
611
|
+
>
|
|
612
|
+
<polyline points="20 6 9 17 4 12" />
|
|
613
|
+
</svg>
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
</div>
|
|
617
|
+
`,
|
|
618
|
+
imports: [NgTemplateOutlet],
|
|
619
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
620
|
+
host: {
|
|
621
|
+
class: 'com-dropdown-option-host block',
|
|
622
|
+
},
|
|
623
|
+
}]
|
|
624
|
+
}], propDecorators: { optionRef: [{ type: i0.ViewChild, args: ['optionElement', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], displayText: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayText", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], optionTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTemplate", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], select: [{ type: i0.Output, args: ["select"] }], hover: [{ type: i0.Output, args: ["hover"] }] } });
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* The overlay panel containing the dropdown options.
|
|
628
|
+
* Supports virtual scrolling for large lists.
|
|
629
|
+
*
|
|
630
|
+
* @example
|
|
631
|
+
* ```html
|
|
632
|
+
* <com-dropdown-panel
|
|
633
|
+
* [options]="filteredOptions()"
|
|
634
|
+
* [maxHeight]="'300px'"
|
|
635
|
+
* [virtualScrollEnabled]="true"
|
|
636
|
+
* >
|
|
637
|
+
* <ng-content />
|
|
638
|
+
* </com-dropdown-panel>
|
|
639
|
+
* ```
|
|
640
|
+
*
|
|
641
|
+
* @tokens `--color-popover`, `--color-popover-foreground`, `--color-border-subtle`,
|
|
642
|
+
* `--color-muted-foreground`, `--radius-overlay`
|
|
643
|
+
*/
|
|
644
|
+
class ComDropdownPanel {
|
|
645
|
+
/** Reference to the panel element. */
|
|
646
|
+
panelRef = viewChild('panelElement', ...(ngDevMode ? [{ debugName: "panelRef" }] : []));
|
|
647
|
+
/** Reference to the virtual scroll viewport (when enabled). */
|
|
648
|
+
viewport = viewChild('viewport', ...(ngDevMode ? [{ debugName: "viewport" }] : []));
|
|
649
|
+
/** Unique identifier for the panel. */
|
|
650
|
+
panelId = input('', ...(ngDevMode ? [{ debugName: "panelId" }] : []));
|
|
651
|
+
/** The processed options to display. */
|
|
652
|
+
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
653
|
+
/** Maximum height of the panel. */
|
|
654
|
+
maxHeight = input('256px', ...(ngDevMode ? [{ debugName: "maxHeight" }] : []));
|
|
655
|
+
/** Whether multiple selection is enabled. */
|
|
656
|
+
multiselectable = input(false, ...(ngDevMode ? [{ debugName: "multiselectable" }] : []));
|
|
657
|
+
/** Size variant for styling. */
|
|
658
|
+
size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
659
|
+
/** Additional CSS classes to apply to the panel. */
|
|
660
|
+
panelClass = input('', ...(ngDevMode ? [{ debugName: "panelClass" }] : []));
|
|
661
|
+
/** Whether virtual scrolling is enabled. */
|
|
662
|
+
virtualScrollEnabled = input(false, ...(ngDevMode ? [{ debugName: "virtualScrollEnabled" }] : []));
|
|
663
|
+
/** Item size for virtual scrolling (in pixels). */
|
|
664
|
+
itemSize = input(40, ...(ngDevMode ? [{ debugName: "itemSize" }] : []));
|
|
665
|
+
/** The current search query (for empty state context). */
|
|
666
|
+
searchQuery = input('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : []));
|
|
667
|
+
/** Custom empty state text. */
|
|
668
|
+
emptyText = input('No options available', ...(ngDevMode ? [{ debugName: "emptyText" }] : []));
|
|
669
|
+
/** Template for rendering each option. */
|
|
670
|
+
optionTemplate = input.required(...(ngDevMode ? [{ debugName: "optionTemplate" }] : []));
|
|
671
|
+
/** Custom template for the empty state. */
|
|
672
|
+
emptyTemplate = input(null, ...(ngDevMode ? [{ debugName: "emptyTemplate" }] : []));
|
|
673
|
+
/** Emitted when the panel is scrolled. */
|
|
674
|
+
scrolled = output();
|
|
675
|
+
/** Whether to show the empty state. */
|
|
676
|
+
showEmpty = computed(() => this.options().length === 0, ...(ngDevMode ? [{ debugName: "showEmpty" }] : []));
|
|
677
|
+
/** Computed CSS classes for the panel. */
|
|
678
|
+
panelClasses = computed(() => {
|
|
679
|
+
const baseClasses = dropdownPanelVariants({ size: this.size() });
|
|
680
|
+
return mergeClasses(baseClasses, this.panelClass());
|
|
681
|
+
}, ...(ngDevMode ? [{ debugName: "panelClasses" }] : []));
|
|
682
|
+
/** Computed CSS classes for the empty state. */
|
|
683
|
+
emptyClasses = computed(() => {
|
|
684
|
+
return dropdownEmptyVariants({ size: this.size() });
|
|
685
|
+
}, ...(ngDevMode ? [{ debugName: "emptyClasses" }] : []));
|
|
686
|
+
/** Template context for the empty state. */
|
|
687
|
+
emptyContext = computed(() => ({
|
|
688
|
+
$implicit: this.searchQuery(),
|
|
689
|
+
}), ...(ngDevMode ? [{ debugName: "emptyContext" }] : []));
|
|
690
|
+
/** Track function for options. */
|
|
691
|
+
trackByFn(_index, option) {
|
|
692
|
+
return option.id;
|
|
693
|
+
}
|
|
694
|
+
/** Scrolls to a specific index. */
|
|
695
|
+
scrollToIndex(index) {
|
|
696
|
+
const vp = this.viewport();
|
|
697
|
+
if (this.virtualScrollEnabled() && vp) {
|
|
698
|
+
vp.scrollToIndex(index);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
const panelEl = this.panelRef()?.nativeElement;
|
|
702
|
+
const optionEl = panelEl?.querySelector(`[data-index="${index}"]`);
|
|
703
|
+
optionEl?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/** Scrolls an option into view. */
|
|
707
|
+
scrollOptionIntoView(optionElement) {
|
|
708
|
+
optionElement.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
709
|
+
}
|
|
710
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
711
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdownPanel, isStandalone: true, selector: "com-dropdown-panel", inputs: { panelId: { classPropertyName: "panelId", publicName: "panelId", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, multiselectable: { classPropertyName: "multiselectable", publicName: "multiselectable", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null }, virtualScrollEnabled: { classPropertyName: "virtualScrollEnabled", publicName: "virtualScrollEnabled", isSignal: true, isRequired: false, transformFunction: null }, itemSize: { classPropertyName: "itemSize", publicName: "itemSize", isSignal: true, isRequired: false, transformFunction: null }, searchQuery: { classPropertyName: "searchQuery", publicName: "searchQuery", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, optionTemplate: { classPropertyName: "optionTemplate", publicName: "optionTemplate", isSignal: true, isRequired: true, transformFunction: null }, emptyTemplate: { classPropertyName: "emptyTemplate", publicName: "emptyTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { scrolled: "scrolled" }, host: { classAttribute: "com-dropdown-panel-host block" }, viewQueries: [{ propertyName: "panelRef", first: true, predicate: ["panelElement"], descendants: true, isSignal: true }, { propertyName: "viewport", first: true, predicate: ["viewport"], descendants: true, isSignal: true }], exportAs: ["comDropdownPanel"], ngImport: i0, template: `
|
|
712
|
+
<div
|
|
713
|
+
#panelElement
|
|
714
|
+
[class]="panelClasses()"
|
|
715
|
+
[attr.role]="'listbox'"
|
|
716
|
+
[attr.aria-multiselectable]="multiselectable() || null"
|
|
717
|
+
[attr.id]="panelId()"
|
|
718
|
+
>
|
|
719
|
+
<!-- Search slot -->
|
|
720
|
+
<ng-content select="[comDropdownSearch]" />
|
|
721
|
+
|
|
722
|
+
<!-- Options container -->
|
|
723
|
+
@if (virtualScrollEnabled()) {
|
|
724
|
+
<cdk-virtual-scroll-viewport
|
|
725
|
+
[itemSize]="itemSize()"
|
|
726
|
+
[maxBufferPx]="400"
|
|
727
|
+
[minBufferPx]="200"
|
|
728
|
+
[style.height]="maxHeight()"
|
|
729
|
+
class="overflow-auto"
|
|
730
|
+
>
|
|
731
|
+
<div
|
|
732
|
+
*cdkVirtualFor="let option of options(); trackBy: trackByFn; let i = index"
|
|
733
|
+
[attr.data-index]="i"
|
|
734
|
+
>
|
|
735
|
+
<ng-container
|
|
736
|
+
[ngTemplateOutlet]="optionTemplate()"
|
|
737
|
+
[ngTemplateOutletContext]="{ $implicit: option, index: i }"
|
|
738
|
+
/>
|
|
739
|
+
</div>
|
|
740
|
+
</cdk-virtual-scroll-viewport>
|
|
741
|
+
} @else {
|
|
742
|
+
<div
|
|
743
|
+
class="overflow-auto"
|
|
744
|
+
[style.maxHeight]="maxHeight()"
|
|
745
|
+
>
|
|
746
|
+
@for (option of options(); track option.id; let i = $index) {
|
|
747
|
+
<ng-container
|
|
748
|
+
[ngTemplateOutlet]="optionTemplate()"
|
|
749
|
+
[ngTemplateOutletContext]="{ $implicit: option, index: i }"
|
|
750
|
+
/>
|
|
751
|
+
}
|
|
752
|
+
</div>
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
<!-- Empty state -->
|
|
756
|
+
@if (showEmpty()) {
|
|
757
|
+
@if (emptyTemplate()) {
|
|
758
|
+
<ng-container
|
|
759
|
+
[ngTemplateOutlet]="emptyTemplate()!"
|
|
760
|
+
[ngTemplateOutletContext]="emptyContext()"
|
|
761
|
+
/>
|
|
762
|
+
} @else {
|
|
763
|
+
<div [class]="emptyClasses()">
|
|
764
|
+
{{ emptyText() }}
|
|
765
|
+
</div>
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
</div>
|
|
769
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "directive", type: CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
770
|
+
}
|
|
771
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownPanel, decorators: [{
|
|
772
|
+
type: Component,
|
|
773
|
+
args: [{
|
|
774
|
+
selector: 'com-dropdown-panel',
|
|
775
|
+
exportAs: 'comDropdownPanel',
|
|
776
|
+
template: `
|
|
777
|
+
<div
|
|
778
|
+
#panelElement
|
|
779
|
+
[class]="panelClasses()"
|
|
780
|
+
[attr.role]="'listbox'"
|
|
781
|
+
[attr.aria-multiselectable]="multiselectable() || null"
|
|
782
|
+
[attr.id]="panelId()"
|
|
783
|
+
>
|
|
784
|
+
<!-- Search slot -->
|
|
785
|
+
<ng-content select="[comDropdownSearch]" />
|
|
786
|
+
|
|
787
|
+
<!-- Options container -->
|
|
788
|
+
@if (virtualScrollEnabled()) {
|
|
789
|
+
<cdk-virtual-scroll-viewport
|
|
790
|
+
[itemSize]="itemSize()"
|
|
791
|
+
[maxBufferPx]="400"
|
|
792
|
+
[minBufferPx]="200"
|
|
793
|
+
[style.height]="maxHeight()"
|
|
794
|
+
class="overflow-auto"
|
|
795
|
+
>
|
|
796
|
+
<div
|
|
797
|
+
*cdkVirtualFor="let option of options(); trackBy: trackByFn; let i = index"
|
|
798
|
+
[attr.data-index]="i"
|
|
799
|
+
>
|
|
800
|
+
<ng-container
|
|
801
|
+
[ngTemplateOutlet]="optionTemplate()"
|
|
802
|
+
[ngTemplateOutletContext]="{ $implicit: option, index: i }"
|
|
803
|
+
/>
|
|
804
|
+
</div>
|
|
805
|
+
</cdk-virtual-scroll-viewport>
|
|
806
|
+
} @else {
|
|
807
|
+
<div
|
|
808
|
+
class="overflow-auto"
|
|
809
|
+
[style.maxHeight]="maxHeight()"
|
|
810
|
+
>
|
|
811
|
+
@for (option of options(); track option.id; let i = $index) {
|
|
812
|
+
<ng-container
|
|
813
|
+
[ngTemplateOutlet]="optionTemplate()"
|
|
814
|
+
[ngTemplateOutletContext]="{ $implicit: option, index: i }"
|
|
815
|
+
/>
|
|
816
|
+
}
|
|
817
|
+
</div>
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
<!-- Empty state -->
|
|
821
|
+
@if (showEmpty()) {
|
|
822
|
+
@if (emptyTemplate()) {
|
|
823
|
+
<ng-container
|
|
824
|
+
[ngTemplateOutlet]="emptyTemplate()!"
|
|
825
|
+
[ngTemplateOutletContext]="emptyContext()"
|
|
826
|
+
/>
|
|
827
|
+
} @else {
|
|
828
|
+
<div [class]="emptyClasses()">
|
|
829
|
+
{{ emptyText() }}
|
|
830
|
+
</div>
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
</div>
|
|
834
|
+
`,
|
|
835
|
+
imports: [
|
|
836
|
+
NgTemplateOutlet,
|
|
837
|
+
CdkVirtualScrollViewport,
|
|
838
|
+
CdkFixedSizeVirtualScroll,
|
|
839
|
+
CdkVirtualForOf,
|
|
840
|
+
],
|
|
841
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
842
|
+
host: {
|
|
843
|
+
class: 'com-dropdown-panel-host block',
|
|
844
|
+
},
|
|
845
|
+
}]
|
|
846
|
+
}], propDecorators: { panelRef: [{ type: i0.ViewChild, args: ['panelElement', { isSignal: true }] }], viewport: [{ type: i0.ViewChild, args: ['viewport', { isSignal: true }] }], panelId: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelId", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], multiselectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiselectable", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], virtualScrollEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScrollEnabled", required: false }] }], itemSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemSize", required: false }] }], searchQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchQuery", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], optionTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTemplate", required: true }] }], emptyTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyTemplate", required: false }] }], scrolled: [{ type: i0.Output, args: ["scrolled"] }] } });
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Search input component for filtering dropdown options.
|
|
850
|
+
* Includes debouncing for better performance.
|
|
851
|
+
*
|
|
852
|
+
* @example
|
|
853
|
+
* ```html
|
|
854
|
+
* <com-dropdown-search
|
|
855
|
+
* [placeholder]="'Search...'"
|
|
856
|
+
* [debounceTime]="300"
|
|
857
|
+
* (searchChange)="onSearch($event)"
|
|
858
|
+
* />
|
|
859
|
+
* ```
|
|
860
|
+
*
|
|
861
|
+
* @tokens `--color-border-subtle`, `--color-input-placeholder`, `--color-disabled-foreground`,
|
|
862
|
+
* `--color-ring`, `--radius-interactive-sm`
|
|
863
|
+
*/
|
|
864
|
+
class ComDropdownSearch {
|
|
865
|
+
destroyRef = inject(DestroyRef);
|
|
866
|
+
/** Reference to the input element. */
|
|
867
|
+
inputRef = viewChild('searchInput', ...(ngDevMode ? [{ debugName: "inputRef" }] : []));
|
|
868
|
+
/** Subject for debounced search. */
|
|
869
|
+
searchSubject = new Subject();
|
|
870
|
+
/** Current internal search value. */
|
|
871
|
+
internalValue = signal('', ...(ngDevMode ? [{ debugName: "internalValue" }] : []));
|
|
872
|
+
/** Placeholder text for the search input. */
|
|
873
|
+
placeholder = input('Search...', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
874
|
+
/** Aria label for accessibility. */
|
|
875
|
+
ariaLabel = input('Search options', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
876
|
+
/** Whether the search input is disabled. */
|
|
877
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
878
|
+
/** Debounce time in milliseconds. */
|
|
879
|
+
debounceMs = input(300, ...(ngDevMode ? [{ debugName: "debounceMs" }] : []));
|
|
880
|
+
/** Size variant for styling. */
|
|
881
|
+
size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
882
|
+
/** Additional CSS classes to apply. */
|
|
883
|
+
userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
|
|
884
|
+
/** Emitted when the search value changes (after debounce). */
|
|
885
|
+
searchChange = output();
|
|
886
|
+
/** Emitted when a navigation key is pressed (for focus management). */
|
|
887
|
+
keyNav = output();
|
|
888
|
+
/** Whether to show the clear button. */
|
|
889
|
+
showClear = computed(() => this.internalValue().length > 0, ...(ngDevMode ? [{ debugName: "showClear" }] : []));
|
|
890
|
+
/** Computed CSS classes for the search input. */
|
|
891
|
+
searchClasses = computed(() => {
|
|
892
|
+
const baseClasses = dropdownSearchVariants({ size: this.size() });
|
|
893
|
+
// Add left padding for the search icon
|
|
894
|
+
return mergeClasses(baseClasses, 'pl-9 pr-8', this.userClass());
|
|
895
|
+
}, ...(ngDevMode ? [{ debugName: "searchClasses" }] : []));
|
|
896
|
+
ngOnInit() {
|
|
897
|
+
// Set up debounced search
|
|
898
|
+
this.searchSubject
|
|
899
|
+
.pipe(debounceTime(this.debounceMs()), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
|
|
900
|
+
.subscribe((value) => {
|
|
901
|
+
this.searchChange.emit(value);
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
onInput(event) {
|
|
905
|
+
const value = event.target.value;
|
|
906
|
+
this.internalValue.set(value);
|
|
907
|
+
this.searchSubject.next(value);
|
|
908
|
+
}
|
|
909
|
+
onKeydown(event) {
|
|
910
|
+
// Let parent handle navigation keys
|
|
911
|
+
if (['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(event.key)) {
|
|
912
|
+
this.keyNav.emit(event);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
clearSearch() {
|
|
916
|
+
this.internalValue.set('');
|
|
917
|
+
this.searchSubject.next('');
|
|
918
|
+
this.focus();
|
|
919
|
+
}
|
|
920
|
+
/** Focuses the search input. */
|
|
921
|
+
focus() {
|
|
922
|
+
this.inputRef()?.nativeElement.focus();
|
|
923
|
+
}
|
|
924
|
+
/** Gets the current search value. */
|
|
925
|
+
getValue() {
|
|
926
|
+
return this.internalValue();
|
|
927
|
+
}
|
|
928
|
+
/** Sets the search value programmatically. */
|
|
929
|
+
setValue(value) {
|
|
930
|
+
this.internalValue.set(value);
|
|
931
|
+
const inputEl = this.inputRef()?.nativeElement;
|
|
932
|
+
if (inputEl) {
|
|
933
|
+
inputEl.value = value;
|
|
934
|
+
}
|
|
935
|
+
this.searchSubject.next(value);
|
|
936
|
+
}
|
|
937
|
+
/** Clears the search input. */
|
|
938
|
+
clear() {
|
|
939
|
+
this.clearSearch();
|
|
940
|
+
}
|
|
941
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownSearch, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
942
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdownSearch, isStandalone: true, selector: "com-dropdown-search", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, debounceMs: { classPropertyName: "debounceMs", publicName: "debounceMs", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { searchChange: "searchChange", keyNav: "keyNav" }, host: { properties: { "attr.comDropdownSearch": "true" }, classAttribute: "com-dropdown-search-host block" }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], exportAs: ["comDropdownSearch"], ngImport: i0, template: `
|
|
943
|
+
<div class="relative flex items-center">
|
|
944
|
+
<!-- Search icon -->
|
|
945
|
+
<svg
|
|
946
|
+
class="pointer-events-none absolute left-3 h-4 w-4 text-placeholder"
|
|
947
|
+
viewBox="0 0 24 24"
|
|
948
|
+
fill="none"
|
|
949
|
+
stroke="currentColor"
|
|
950
|
+
stroke-width="2"
|
|
951
|
+
stroke-linecap="round"
|
|
952
|
+
stroke-linejoin="round"
|
|
953
|
+
>
|
|
954
|
+
<circle cx="11" cy="11" r="8" />
|
|
955
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
956
|
+
</svg>
|
|
957
|
+
|
|
958
|
+
<!-- Input -->
|
|
959
|
+
<input
|
|
960
|
+
#searchInput
|
|
961
|
+
type="text"
|
|
962
|
+
[class]="searchClasses()"
|
|
963
|
+
[placeholder]="placeholder()"
|
|
964
|
+
[disabled]="disabled()"
|
|
965
|
+
[attr.aria-label]="ariaLabel()"
|
|
966
|
+
[attr.autocomplete]="'off'"
|
|
967
|
+
[attr.autocapitalize]="'off'"
|
|
968
|
+
[attr.autocorrect]="'off'"
|
|
969
|
+
[attr.spellcheck]="'false'"
|
|
970
|
+
(input)="onInput($event)"
|
|
971
|
+
(keydown)="onKeydown($event)"
|
|
972
|
+
/>
|
|
973
|
+
|
|
974
|
+
<!-- Clear button -->
|
|
975
|
+
@if (showClear()) {
|
|
976
|
+
<button
|
|
977
|
+
type="button"
|
|
978
|
+
class="absolute right-3 flex h-4 w-4 items-center justify-center rounded-interactive-sm text-placeholder opacity-70 hover:opacity-100 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
979
|
+
[attr.aria-label]="'Clear search'"
|
|
980
|
+
(click)="clearSearch()"
|
|
981
|
+
>
|
|
982
|
+
<svg
|
|
983
|
+
class="h-3 w-3"
|
|
984
|
+
viewBox="0 0 24 24"
|
|
985
|
+
fill="none"
|
|
986
|
+
stroke="currentColor"
|
|
987
|
+
stroke-width="2"
|
|
988
|
+
stroke-linecap="round"
|
|
989
|
+
stroke-linejoin="round"
|
|
990
|
+
>
|
|
991
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
992
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
993
|
+
</svg>
|
|
994
|
+
</button>
|
|
995
|
+
}
|
|
996
|
+
</div>
|
|
997
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
998
|
+
}
|
|
999
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownSearch, decorators: [{
|
|
1000
|
+
type: Component,
|
|
1001
|
+
args: [{
|
|
1002
|
+
selector: 'com-dropdown-search',
|
|
1003
|
+
exportAs: 'comDropdownSearch',
|
|
1004
|
+
template: `
|
|
1005
|
+
<div class="relative flex items-center">
|
|
1006
|
+
<!-- Search icon -->
|
|
1007
|
+
<svg
|
|
1008
|
+
class="pointer-events-none absolute left-3 h-4 w-4 text-placeholder"
|
|
1009
|
+
viewBox="0 0 24 24"
|
|
1010
|
+
fill="none"
|
|
1011
|
+
stroke="currentColor"
|
|
1012
|
+
stroke-width="2"
|
|
1013
|
+
stroke-linecap="round"
|
|
1014
|
+
stroke-linejoin="round"
|
|
1015
|
+
>
|
|
1016
|
+
<circle cx="11" cy="11" r="8" />
|
|
1017
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
1018
|
+
</svg>
|
|
1019
|
+
|
|
1020
|
+
<!-- Input -->
|
|
1021
|
+
<input
|
|
1022
|
+
#searchInput
|
|
1023
|
+
type="text"
|
|
1024
|
+
[class]="searchClasses()"
|
|
1025
|
+
[placeholder]="placeholder()"
|
|
1026
|
+
[disabled]="disabled()"
|
|
1027
|
+
[attr.aria-label]="ariaLabel()"
|
|
1028
|
+
[attr.autocomplete]="'off'"
|
|
1029
|
+
[attr.autocapitalize]="'off'"
|
|
1030
|
+
[attr.autocorrect]="'off'"
|
|
1031
|
+
[attr.spellcheck]="'false'"
|
|
1032
|
+
(input)="onInput($event)"
|
|
1033
|
+
(keydown)="onKeydown($event)"
|
|
1034
|
+
/>
|
|
1035
|
+
|
|
1036
|
+
<!-- Clear button -->
|
|
1037
|
+
@if (showClear()) {
|
|
1038
|
+
<button
|
|
1039
|
+
type="button"
|
|
1040
|
+
class="absolute right-3 flex h-4 w-4 items-center justify-center rounded-interactive-sm text-placeholder opacity-70 hover:opacity-100 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
1041
|
+
[attr.aria-label]="'Clear search'"
|
|
1042
|
+
(click)="clearSearch()"
|
|
1043
|
+
>
|
|
1044
|
+
<svg
|
|
1045
|
+
class="h-3 w-3"
|
|
1046
|
+
viewBox="0 0 24 24"
|
|
1047
|
+
fill="none"
|
|
1048
|
+
stroke="currentColor"
|
|
1049
|
+
stroke-width="2"
|
|
1050
|
+
stroke-linecap="round"
|
|
1051
|
+
stroke-linejoin="round"
|
|
1052
|
+
>
|
|
1053
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
1054
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
1055
|
+
</svg>
|
|
1056
|
+
</button>
|
|
1057
|
+
}
|
|
1058
|
+
</div>
|
|
1059
|
+
`,
|
|
1060
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1061
|
+
host: {
|
|
1062
|
+
class: 'com-dropdown-search-host block',
|
|
1063
|
+
'[attr.comDropdownSearch]': 'true',
|
|
1064
|
+
},
|
|
1065
|
+
}]
|
|
1066
|
+
}], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], keyNav: [{ type: i0.Output, args: ["keyNav"] }] } });
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Tag component for displaying selected items in multi-select mode.
|
|
1070
|
+
* Includes a remove button for deselection.
|
|
1071
|
+
*
|
|
1072
|
+
* @example
|
|
1073
|
+
* ```html
|
|
1074
|
+
* <com-dropdown-tag
|
|
1075
|
+
* [value]="user"
|
|
1076
|
+
* [displayText]="user.name"
|
|
1077
|
+
* (remove)="onRemove(user)"
|
|
1078
|
+
* />
|
|
1079
|
+
* ```
|
|
1080
|
+
*
|
|
1081
|
+
* @tokens `--color-muted`, `--color-muted-foreground`, `--color-muted-hover`,
|
|
1082
|
+
* `--color-primary-subtle`, `--color-primary-subtle-foreground`,
|
|
1083
|
+
* `--color-ring`, `--radius-tag`, `--radius-interactive-sm`
|
|
1084
|
+
*/
|
|
1085
|
+
class ComDropdownTag {
|
|
1086
|
+
/** The value this tag represents. */
|
|
1087
|
+
value = input.required(...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1088
|
+
/** Display text for this tag. */
|
|
1089
|
+
displayText = input('', ...(ngDevMode ? [{ debugName: "displayText" }] : []));
|
|
1090
|
+
/** Index of this tag in the list. */
|
|
1091
|
+
index = input(0, ...(ngDevMode ? [{ debugName: "index" }] : []));
|
|
1092
|
+
/** Whether the tag (and its remove button) is disabled. */
|
|
1093
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
1094
|
+
/** Size variant for styling. */
|
|
1095
|
+
size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
1096
|
+
/** Tag variant for styling. */
|
|
1097
|
+
variant = input('primary', ...(ngDevMode ? [{ debugName: "variant" }] : []));
|
|
1098
|
+
/** Additional CSS classes for the tag. */
|
|
1099
|
+
userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
|
|
1100
|
+
/** Custom template for rendering the tag. */
|
|
1101
|
+
tagTemplate = input(null, ...(ngDevMode ? [{ debugName: "tagTemplate" }] : []));
|
|
1102
|
+
/** Emitted when the remove button is clicked. */
|
|
1103
|
+
remove = output();
|
|
1104
|
+
/** Computed CSS classes for the tag. */
|
|
1105
|
+
tagClasses = computed(() => {
|
|
1106
|
+
const baseClasses = dropdownTagVariants({
|
|
1107
|
+
size: this.size(),
|
|
1108
|
+
variant: this.variant(),
|
|
1109
|
+
});
|
|
1110
|
+
return mergeClasses(baseClasses, this.userClass());
|
|
1111
|
+
}, ...(ngDevMode ? [{ debugName: "tagClasses" }] : []));
|
|
1112
|
+
/** Computed CSS classes for the remove button. */
|
|
1113
|
+
removeClasses = computed(() => {
|
|
1114
|
+
return dropdownTagRemoveVariants({ size: this.size() });
|
|
1115
|
+
}, ...(ngDevMode ? [{ debugName: "removeClasses" }] : []));
|
|
1116
|
+
/** Template context for custom tag templates. */
|
|
1117
|
+
templateContext = computed(() => ({
|
|
1118
|
+
$implicit: this.value(),
|
|
1119
|
+
index: this.index(),
|
|
1120
|
+
remove: () => this.emitRemove(),
|
|
1121
|
+
}), ...(ngDevMode ? [{ debugName: "templateContext" }] : []));
|
|
1122
|
+
onRemove(event) {
|
|
1123
|
+
event.preventDefault();
|
|
1124
|
+
event.stopPropagation();
|
|
1125
|
+
if (!this.disabled()) {
|
|
1126
|
+
this.emitRemove();
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
emitRemove() {
|
|
1130
|
+
this.remove.emit(this.value());
|
|
1131
|
+
}
|
|
1132
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownTag, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1133
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdownTag, isStandalone: true, selector: "com-dropdown-tag", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, displayText: { classPropertyName: "displayText", publicName: "displayText", isSignal: true, isRequired: false, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, tagTemplate: { classPropertyName: "tagTemplate", publicName: "tagTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { remove: "remove" }, host: { classAttribute: "com-dropdown-tag-host inline-flex" }, exportAs: ["comDropdownTag"], ngImport: i0, template: `
|
|
1134
|
+
@if (tagTemplate()) {
|
|
1135
|
+
<ng-container
|
|
1136
|
+
[ngTemplateOutlet]="tagTemplate()!"
|
|
1137
|
+
[ngTemplateOutletContext]="templateContext()"
|
|
1138
|
+
/>
|
|
1139
|
+
} @else {
|
|
1140
|
+
<span [class]="tagClasses()">
|
|
1141
|
+
<span class="truncate">{{ displayText() }}</span>
|
|
1142
|
+
@if (!disabled()) {
|
|
1143
|
+
<button
|
|
1144
|
+
type="button"
|
|
1145
|
+
[class]="removeClasses()"
|
|
1146
|
+
[attr.aria-label]="'Remove ' + displayText()"
|
|
1147
|
+
(click)="onRemove($event)"
|
|
1148
|
+
(keydown.enter)="onRemove($event)"
|
|
1149
|
+
(keydown.space)="onRemove($event)"
|
|
1150
|
+
>
|
|
1151
|
+
<svg
|
|
1152
|
+
viewBox="0 0 24 24"
|
|
1153
|
+
fill="none"
|
|
1154
|
+
stroke="currentColor"
|
|
1155
|
+
stroke-width="2"
|
|
1156
|
+
stroke-linecap="round"
|
|
1157
|
+
stroke-linejoin="round"
|
|
1158
|
+
class="h-full w-full"
|
|
1159
|
+
>
|
|
1160
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
1161
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
1162
|
+
</svg>
|
|
1163
|
+
</button>
|
|
1164
|
+
}
|
|
1165
|
+
</span>
|
|
1166
|
+
}
|
|
1167
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1168
|
+
}
|
|
1169
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownTag, decorators: [{
|
|
1170
|
+
type: Component,
|
|
1171
|
+
args: [{
|
|
1172
|
+
selector: 'com-dropdown-tag',
|
|
1173
|
+
exportAs: 'comDropdownTag',
|
|
1174
|
+
template: `
|
|
1175
|
+
@if (tagTemplate()) {
|
|
1176
|
+
<ng-container
|
|
1177
|
+
[ngTemplateOutlet]="tagTemplate()!"
|
|
1178
|
+
[ngTemplateOutletContext]="templateContext()"
|
|
1179
|
+
/>
|
|
1180
|
+
} @else {
|
|
1181
|
+
<span [class]="tagClasses()">
|
|
1182
|
+
<span class="truncate">{{ displayText() }}</span>
|
|
1183
|
+
@if (!disabled()) {
|
|
1184
|
+
<button
|
|
1185
|
+
type="button"
|
|
1186
|
+
[class]="removeClasses()"
|
|
1187
|
+
[attr.aria-label]="'Remove ' + displayText()"
|
|
1188
|
+
(click)="onRemove($event)"
|
|
1189
|
+
(keydown.enter)="onRemove($event)"
|
|
1190
|
+
(keydown.space)="onRemove($event)"
|
|
1191
|
+
>
|
|
1192
|
+
<svg
|
|
1193
|
+
viewBox="0 0 24 24"
|
|
1194
|
+
fill="none"
|
|
1195
|
+
stroke="currentColor"
|
|
1196
|
+
stroke-width="2"
|
|
1197
|
+
stroke-linecap="round"
|
|
1198
|
+
stroke-linejoin="round"
|
|
1199
|
+
class="h-full w-full"
|
|
1200
|
+
>
|
|
1201
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
1202
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
1203
|
+
</svg>
|
|
1204
|
+
</button>
|
|
1205
|
+
}
|
|
1206
|
+
</span>
|
|
1207
|
+
}
|
|
1208
|
+
`,
|
|
1209
|
+
imports: [NgTemplateOutlet],
|
|
1210
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1211
|
+
host: {
|
|
1212
|
+
class: 'com-dropdown-tag-host inline-flex',
|
|
1213
|
+
},
|
|
1214
|
+
}]
|
|
1215
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], displayText: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayText", required: false }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], tagTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "tagTemplate", required: false }] }], remove: [{ type: i0.Output, args: ["remove"] }] } });
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Group header component for categorized dropdown options.
|
|
1219
|
+
*
|
|
1220
|
+
* @example
|
|
1221
|
+
* ```html
|
|
1222
|
+
* <com-dropdown-group
|
|
1223
|
+
* [label]="'Fruits'"
|
|
1224
|
+
* [count]="5"
|
|
1225
|
+
* />
|
|
1226
|
+
* ```
|
|
1227
|
+
*
|
|
1228
|
+
* @tokens `--color-muted-foreground`
|
|
1229
|
+
*/
|
|
1230
|
+
class ComDropdownGroup {
|
|
1231
|
+
/** The group label/key. */
|
|
1232
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
1233
|
+
/** The number of options in this group. */
|
|
1234
|
+
count = input(0, ...(ngDevMode ? [{ debugName: "count" }] : []));
|
|
1235
|
+
/** Whether the group is expanded (for collapsible groups). */
|
|
1236
|
+
expanded = input(true, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
|
|
1237
|
+
/** Whether to show the count badge. */
|
|
1238
|
+
showCount = input(false, ...(ngDevMode ? [{ debugName: "showCount" }] : []));
|
|
1239
|
+
/** Size variant for styling. */
|
|
1240
|
+
size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
1241
|
+
/** Additional CSS classes for the group header. */
|
|
1242
|
+
userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
|
|
1243
|
+
/** Custom template for rendering the group header. */
|
|
1244
|
+
groupTemplate = input(null, ...(ngDevMode ? [{ debugName: "groupTemplate" }] : []));
|
|
1245
|
+
/** Computed CSS classes for the group header. */
|
|
1246
|
+
groupClasses = computed(() => {
|
|
1247
|
+
const baseClasses = dropdownGroupVariants({ size: this.size() });
|
|
1248
|
+
return mergeClasses(baseClasses, this.userClass());
|
|
1249
|
+
}, ...(ngDevMode ? [{ debugName: "groupClasses" }] : []));
|
|
1250
|
+
/** Template context for custom group templates. */
|
|
1251
|
+
templateContext = computed(() => ({
|
|
1252
|
+
$implicit: this.label(),
|
|
1253
|
+
expanded: this.expanded(),
|
|
1254
|
+
count: this.count(),
|
|
1255
|
+
}), ...(ngDevMode ? [{ debugName: "templateContext" }] : []));
|
|
1256
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1257
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdownGroup, isStandalone: true, selector: "com-dropdown-group", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, expanded: { classPropertyName: "expanded", publicName: "expanded", isSignal: true, isRequired: false, transformFunction: null }, showCount: { classPropertyName: "showCount", publicName: "showCount", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, groupTemplate: { classPropertyName: "groupTemplate", publicName: "groupTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "presentation" }, classAttribute: "com-dropdown-group-host block" }, exportAs: ["comDropdownGroup"], ngImport: i0, template: `
|
|
1258
|
+
@if (groupTemplate()) {
|
|
1259
|
+
<ng-container
|
|
1260
|
+
[ngTemplateOutlet]="groupTemplate()!"
|
|
1261
|
+
[ngTemplateOutletContext]="templateContext()"
|
|
1262
|
+
/>
|
|
1263
|
+
} @else {
|
|
1264
|
+
<div [class]="groupClasses()">
|
|
1265
|
+
<span>{{ label() }}</span>
|
|
1266
|
+
@if (showCount()) {
|
|
1267
|
+
<span class="ml-auto text-xs text-muted-foreground">
|
|
1268
|
+
({{ count() }})
|
|
1269
|
+
</span>
|
|
1270
|
+
}
|
|
1271
|
+
</div>
|
|
1272
|
+
}
|
|
1273
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1274
|
+
}
|
|
1275
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownGroup, decorators: [{
|
|
1276
|
+
type: Component,
|
|
1277
|
+
args: [{
|
|
1278
|
+
selector: 'com-dropdown-group',
|
|
1279
|
+
exportAs: 'comDropdownGroup',
|
|
1280
|
+
template: `
|
|
1281
|
+
@if (groupTemplate()) {
|
|
1282
|
+
<ng-container
|
|
1283
|
+
[ngTemplateOutlet]="groupTemplate()!"
|
|
1284
|
+
[ngTemplateOutletContext]="templateContext()"
|
|
1285
|
+
/>
|
|
1286
|
+
} @else {
|
|
1287
|
+
<div [class]="groupClasses()">
|
|
1288
|
+
<span>{{ label() }}</span>
|
|
1289
|
+
@if (showCount()) {
|
|
1290
|
+
<span class="ml-auto text-xs text-muted-foreground">
|
|
1291
|
+
({{ count() }})
|
|
1292
|
+
</span>
|
|
1293
|
+
}
|
|
1294
|
+
</div>
|
|
1295
|
+
}
|
|
1296
|
+
`,
|
|
1297
|
+
imports: [NgTemplateOutlet],
|
|
1298
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1299
|
+
host: {
|
|
1300
|
+
class: 'com-dropdown-group-host block',
|
|
1301
|
+
role: 'presentation',
|
|
1302
|
+
},
|
|
1303
|
+
}]
|
|
1304
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], expanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }], showCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCount", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], groupTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupTemplate", required: false }] }] } });
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Directive to mark a template as the custom option template.
|
|
1308
|
+
*
|
|
1309
|
+
* @example
|
|
1310
|
+
* ```html
|
|
1311
|
+
* <com-dropdown [options]="users()">
|
|
1312
|
+
* <ng-template comDropdownOption let-user let-selected="selected">
|
|
1313
|
+
* <div class="flex items-center gap-2">
|
|
1314
|
+
* <span>{{ user.name }}</span>
|
|
1315
|
+
* @if (selected) {
|
|
1316
|
+
* <svg class="h-4 w-4"><!-- check --></svg>
|
|
1317
|
+
* }
|
|
1318
|
+
* </div>
|
|
1319
|
+
* </ng-template>
|
|
1320
|
+
* </com-dropdown>
|
|
1321
|
+
* ```
|
|
1322
|
+
*/
|
|
1323
|
+
class ComDropdownOptionTpl {
|
|
1324
|
+
/** Reference to the template. */
|
|
1325
|
+
templateRef = inject(TemplateRef);
|
|
1326
|
+
/**
|
|
1327
|
+
* Static type guard for template type checking.
|
|
1328
|
+
* Enables type-safe access to context properties in templates.
|
|
1329
|
+
*/
|
|
1330
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1333
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownOptionTpl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1334
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComDropdownOptionTpl, isStandalone: true, selector: "ng-template[comDropdownOption]", ngImport: i0 });
|
|
1335
|
+
}
|
|
1336
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownOptionTpl, decorators: [{
|
|
1337
|
+
type: Directive,
|
|
1338
|
+
args: [{
|
|
1339
|
+
selector: 'ng-template[comDropdownOption]',
|
|
1340
|
+
}]
|
|
1341
|
+
}] });
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Directive to mark a template as the custom selected value template.
|
|
1345
|
+
*
|
|
1346
|
+
* @example
|
|
1347
|
+
* ```html
|
|
1348
|
+
* <com-dropdown [options]="users()">
|
|
1349
|
+
* <ng-template comDropdownSelected let-user>
|
|
1350
|
+
* @if (user) {
|
|
1351
|
+
* <div class="flex items-center gap-2">
|
|
1352
|
+
* <img [src]="user.avatar" class="h-5 w-5 rounded-full" />
|
|
1353
|
+
* {{ user.name }}
|
|
1354
|
+
* </div>
|
|
1355
|
+
* } @else {
|
|
1356
|
+
* <span class="text-input-placeholder">Select user...</span>
|
|
1357
|
+
* }
|
|
1358
|
+
* </ng-template>
|
|
1359
|
+
* </com-dropdown>
|
|
1360
|
+
* ```
|
|
1361
|
+
*/
|
|
1362
|
+
class ComDropdownSelectedTpl {
|
|
1363
|
+
/** Reference to the template. */
|
|
1364
|
+
templateRef = inject(TemplateRef);
|
|
1365
|
+
/**
|
|
1366
|
+
* Static type guard for template type checking.
|
|
1367
|
+
* Enables type-safe access to context properties in templates.
|
|
1368
|
+
*/
|
|
1369
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
1370
|
+
return true;
|
|
1371
|
+
}
|
|
1372
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownSelectedTpl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1373
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComDropdownSelectedTpl, isStandalone: true, selector: "ng-template[comDropdownSelected]", ngImport: i0 });
|
|
1374
|
+
}
|
|
1375
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownSelectedTpl, decorators: [{
|
|
1376
|
+
type: Directive,
|
|
1377
|
+
args: [{
|
|
1378
|
+
selector: 'ng-template[comDropdownSelected]',
|
|
1379
|
+
}]
|
|
1380
|
+
}] });
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Directive to mark a template as the custom empty state template.
|
|
1384
|
+
*
|
|
1385
|
+
* @example
|
|
1386
|
+
* ```html
|
|
1387
|
+
* <com-dropdown [options]="users()" [searchable]="true">
|
|
1388
|
+
* <ng-template comDropdownEmpty let-query>
|
|
1389
|
+
* <div class="flex flex-col items-center gap-2 py-6">
|
|
1390
|
+
* <svg class="h-8 w-8 text-muted-foreground"><!-- search icon --></svg>
|
|
1391
|
+
* <span>No results for "{{ query }}"</span>
|
|
1392
|
+
* </div>
|
|
1393
|
+
* </ng-template>
|
|
1394
|
+
* </com-dropdown>
|
|
1395
|
+
* ```
|
|
1396
|
+
*/
|
|
1397
|
+
class ComDropdownEmptyTpl {
|
|
1398
|
+
/** Reference to the template. */
|
|
1399
|
+
templateRef = inject(TemplateRef);
|
|
1400
|
+
/**
|
|
1401
|
+
* Static type guard for template type checking.
|
|
1402
|
+
* Enables type-safe access to context properties in templates.
|
|
1403
|
+
*/
|
|
1404
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
1405
|
+
return true;
|
|
1406
|
+
}
|
|
1407
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownEmptyTpl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1408
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComDropdownEmptyTpl, isStandalone: true, selector: "ng-template[comDropdownEmpty]", ngImport: i0 });
|
|
1409
|
+
}
|
|
1410
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownEmptyTpl, decorators: [{
|
|
1411
|
+
type: Directive,
|
|
1412
|
+
args: [{
|
|
1413
|
+
selector: 'ng-template[comDropdownEmpty]',
|
|
1414
|
+
}]
|
|
1415
|
+
}] });
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Directive to mark a template as the custom group header template.
|
|
1419
|
+
*
|
|
1420
|
+
* @example
|
|
1421
|
+
* ```html
|
|
1422
|
+
* <com-dropdown [options]="users()" [groupBy]="groupByDepartment">
|
|
1423
|
+
* <ng-template comDropdownGroup let-group let-count="count">
|
|
1424
|
+
* <div class="flex items-center justify-between">
|
|
1425
|
+
* <span class="font-semibold">{{ group }}</span>
|
|
1426
|
+
* <span class="text-xs text-muted-foreground">({{ count }})</span>
|
|
1427
|
+
* </div>
|
|
1428
|
+
* </ng-template>
|
|
1429
|
+
* </com-dropdown>
|
|
1430
|
+
* ```
|
|
1431
|
+
*/
|
|
1432
|
+
class ComDropdownGroupTpl {
|
|
1433
|
+
/** Reference to the template. */
|
|
1434
|
+
templateRef = inject(TemplateRef);
|
|
1435
|
+
/**
|
|
1436
|
+
* Static type guard for template type checking.
|
|
1437
|
+
* Enables type-safe access to context properties in templates.
|
|
1438
|
+
*/
|
|
1439
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
1440
|
+
return true;
|
|
1441
|
+
}
|
|
1442
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownGroupTpl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1443
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComDropdownGroupTpl, isStandalone: true, selector: "ng-template[comDropdownGroup]", ngImport: i0 });
|
|
1444
|
+
}
|
|
1445
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownGroupTpl, decorators: [{
|
|
1446
|
+
type: Directive,
|
|
1447
|
+
args: [{
|
|
1448
|
+
selector: 'ng-template[comDropdownGroup]',
|
|
1449
|
+
}]
|
|
1450
|
+
}] });
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* Directive to mark a template as the custom tag template (multi-select mode).
|
|
1454
|
+
*
|
|
1455
|
+
* @example
|
|
1456
|
+
* ```html
|
|
1457
|
+
* <com-dropdown [options]="users()" [multiple]="true">
|
|
1458
|
+
* <ng-template comDropdownTag let-user let-remove="remove">
|
|
1459
|
+
* <div class="flex items-center gap-1 rounded bg-primary-subtle px-2 py-0.5 text-primary-subtle-foreground">
|
|
1460
|
+
* <img [src]="user.avatar" class="h-4 w-4 rounded-full" />
|
|
1461
|
+
* <span>{{ user.name }}</span>
|
|
1462
|
+
* <button type="button" (click)="remove()" class="ml-1">
|
|
1463
|
+
* <svg class="h-3 w-3"><!-- x icon --></svg>
|
|
1464
|
+
* </button>
|
|
1465
|
+
* </div>
|
|
1466
|
+
* </ng-template>
|
|
1467
|
+
* </com-dropdown>
|
|
1468
|
+
* ```
|
|
1469
|
+
*/
|
|
1470
|
+
class ComDropdownTagTpl {
|
|
1471
|
+
/** Reference to the template. */
|
|
1472
|
+
templateRef = inject(TemplateRef);
|
|
1473
|
+
/**
|
|
1474
|
+
* Static type guard for template type checking.
|
|
1475
|
+
* Enables type-safe access to context properties in templates.
|
|
1476
|
+
*/
|
|
1477
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1480
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownTagTpl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1481
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComDropdownTagTpl, isStandalone: true, selector: "ng-template[comDropdownTag]", ngImport: i0 });
|
|
1482
|
+
}
|
|
1483
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownTagTpl, decorators: [{
|
|
1484
|
+
type: Directive,
|
|
1485
|
+
args: [{
|
|
1486
|
+
selector: 'ng-template[comDropdownTag]',
|
|
1487
|
+
}]
|
|
1488
|
+
}] });
|
|
1489
|
+
|
|
1490
|
+
/**
|
|
1491
|
+
* Default compare function for primitive values.
|
|
1492
|
+
* @param a First value.
|
|
1493
|
+
* @param b Second value.
|
|
1494
|
+
* @returns Whether the values are equal.
|
|
1495
|
+
*/
|
|
1496
|
+
function defaultCompareWith(a, b) {
|
|
1497
|
+
return a === b;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Default display function for converting values to strings.
|
|
1501
|
+
* @param value The value to display.
|
|
1502
|
+
* @returns The string representation.
|
|
1503
|
+
*/
|
|
1504
|
+
function defaultDisplayWith(value) {
|
|
1505
|
+
if (value === null || value === undefined) {
|
|
1506
|
+
return '';
|
|
1507
|
+
}
|
|
1508
|
+
return String(value);
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Default filter function for search.
|
|
1512
|
+
* @param option The option to test.
|
|
1513
|
+
* @param query The search query.
|
|
1514
|
+
* @param displayWith The display function to get the searchable text.
|
|
1515
|
+
* @returns Whether the option matches the query.
|
|
1516
|
+
*/
|
|
1517
|
+
function defaultFilterWith(option, query, displayWith) {
|
|
1518
|
+
const text = displayWith(option).toLowerCase();
|
|
1519
|
+
return text.includes(query.toLowerCase());
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Unique ID counter for generating option IDs.
|
|
1523
|
+
*/
|
|
1524
|
+
let uniqueIdCounter = 0;
|
|
1525
|
+
/**
|
|
1526
|
+
* Generates a unique ID for dropdown options.
|
|
1527
|
+
* @param prefix Optional prefix for the ID.
|
|
1528
|
+
* @returns A unique ID string.
|
|
1529
|
+
*/
|
|
1530
|
+
function generateDropdownId(prefix = 'com-dropdown') {
|
|
1531
|
+
return `${prefix}-${uniqueIdCounter++}`;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/** Default position for the dropdown panel. */
|
|
1535
|
+
const DEFAULT_POSITIONS = [
|
|
1536
|
+
// Below trigger, aligned start
|
|
1537
|
+
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
|
|
1538
|
+
// Above trigger, aligned start
|
|
1539
|
+
{ originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
|
|
1540
|
+
// Below trigger, aligned end
|
|
1541
|
+
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 4 },
|
|
1542
|
+
// Above trigger, aligned end
|
|
1543
|
+
{ originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -4 },
|
|
1544
|
+
];
|
|
1545
|
+
/** Threshold for enabling virtual scrolling. */
|
|
1546
|
+
const VIRTUAL_SCROLL_THRESHOLD = 50;
|
|
1547
|
+
/**
|
|
1548
|
+
* Reusable dropdown/select component with full accessibility support.
|
|
1549
|
+
* Implements ControlValueAccessor for Reactive Forms integration.
|
|
1550
|
+
*
|
|
1551
|
+
* @tokens `--color-input-background`, `--color-input-foreground`, `--color-input-border`,
|
|
1552
|
+
* `--color-input-placeholder`, `--color-ring`, `--color-muted`, `--color-muted-foreground`,
|
|
1553
|
+
* `--color-popover`, `--color-popover-foreground`, `--color-border-subtle`,
|
|
1554
|
+
* `--color-primary`, `--color-primary-subtle`, `--color-primary-subtle-foreground`,
|
|
1555
|
+
* `--color-warn`, `--color-success`, `--color-disabled`, `--color-disabled-foreground`,
|
|
1556
|
+
* `--color-placeholder`
|
|
1557
|
+
*
|
|
1558
|
+
* @example
|
|
1559
|
+
* ```html
|
|
1560
|
+
* <com-dropdown
|
|
1561
|
+
* [options]="users()"
|
|
1562
|
+
* [compareWith]="compareById"
|
|
1563
|
+
* formControlName="assignee"
|
|
1564
|
+
* placeholder="Select user..."
|
|
1565
|
+
* [searchable]="true"
|
|
1566
|
+
* >
|
|
1567
|
+
* <ng-template comDropdownOption let-user let-selected="selected">
|
|
1568
|
+
* <span>{{ user.name }}</span>
|
|
1569
|
+
* </ng-template>
|
|
1570
|
+
* </com-dropdown>
|
|
1571
|
+
* ```
|
|
1572
|
+
*/
|
|
1573
|
+
class ComDropdown {
|
|
1574
|
+
elementRef = inject(ElementRef);
|
|
1575
|
+
destroyRef = inject(DestroyRef);
|
|
1576
|
+
overlay = inject(Overlay);
|
|
1577
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
1578
|
+
liveAnnouncer = inject(LiveAnnouncer);
|
|
1579
|
+
defaultErrorStateMatcher = inject(ErrorStateMatcher);
|
|
1580
|
+
parentForm = inject(NgForm, { optional: true });
|
|
1581
|
+
parentFormGroup = inject(FormGroupDirective, { optional: true });
|
|
1582
|
+
/** NgControl bound to this dropdown (if using reactive forms). */
|
|
1583
|
+
ngControl = inject(NgControl, { optional: true, self: true });
|
|
1584
|
+
/** Reference to the trigger button element. */
|
|
1585
|
+
triggerRef = viewChild.required('triggerElement');
|
|
1586
|
+
/** Reference to the panel template. */
|
|
1587
|
+
panelTemplateRef = viewChild.required('panelTemplate');
|
|
1588
|
+
/** Content query for custom option template. */
|
|
1589
|
+
optionTemplate = contentChild((ComDropdownOptionTpl), ...(ngDevMode ? [{ debugName: "optionTemplate" }] : []));
|
|
1590
|
+
/** Content query for custom selected template. */
|
|
1591
|
+
selectedTemplate = contentChild((ComDropdownSelectedTpl), ...(ngDevMode ? [{ debugName: "selectedTemplate" }] : []));
|
|
1592
|
+
/** Content query for custom empty template. */
|
|
1593
|
+
emptyTemplate = contentChild(ComDropdownEmptyTpl, ...(ngDevMode ? [{ debugName: "emptyTemplate" }] : []));
|
|
1594
|
+
/** Content query for custom group template. */
|
|
1595
|
+
groupTemplate = contentChild(ComDropdownGroupTpl, ...(ngDevMode ? [{ debugName: "groupTemplate" }] : []));
|
|
1596
|
+
/** Content query for custom tag template. */
|
|
1597
|
+
tagTemplate = contentChild((ComDropdownTagTpl), ...(ngDevMode ? [{ debugName: "tagTemplate" }] : []));
|
|
1598
|
+
/** Overlay reference. */
|
|
1599
|
+
overlayRef = null;
|
|
1600
|
+
/** Unique ID for the dropdown. */
|
|
1601
|
+
dropdownId = generateDropdownId();
|
|
1602
|
+
// ============ INPUTS ============
|
|
1603
|
+
/** Array of options to display. */
|
|
1604
|
+
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
1605
|
+
/** Current value (single or array for multiple). */
|
|
1606
|
+
value = input(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
1607
|
+
/** Placeholder text when no value is selected. */
|
|
1608
|
+
placeholder = input('Select...', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
1609
|
+
/** Enable multi-select mode. */
|
|
1610
|
+
multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
|
|
1611
|
+
/** Enable search/filter input. */
|
|
1612
|
+
searchable = input(false, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
|
|
1613
|
+
/** Search input placeholder. */
|
|
1614
|
+
searchPlaceholder = input('Search...', ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : []));
|
|
1615
|
+
/** Disable the dropdown. */
|
|
1616
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
1617
|
+
/** Mark as required. */
|
|
1618
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
1619
|
+
/** Show clear button. */
|
|
1620
|
+
clearable = input(false, ...(ngDevMode ? [{ debugName: "clearable" }] : []));
|
|
1621
|
+
/** Custom equality function for comparing values. */
|
|
1622
|
+
compareWith = input(defaultCompareWith, ...(ngDevMode ? [{ debugName: "compareWith" }] : []));
|
|
1623
|
+
/** Function to get display text from a value. */
|
|
1624
|
+
displayWith = input(defaultDisplayWith, ...(ngDevMode ? [{ debugName: "displayWith" }] : []));
|
|
1625
|
+
/** Custom filter function for search. */
|
|
1626
|
+
filterWith = input(null, ...(ngDevMode ? [{ debugName: "filterWith" }] : []));
|
|
1627
|
+
/** Function to group options by a key. */
|
|
1628
|
+
groupBy = input(null, ...(ngDevMode ? [{ debugName: "groupBy" }] : []));
|
|
1629
|
+
/** CVA variant for trigger styling. */
|
|
1630
|
+
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : []));
|
|
1631
|
+
/** Size variant. */
|
|
1632
|
+
size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
1633
|
+
/** Validation state. */
|
|
1634
|
+
state = input('default', ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
1635
|
+
/** Additional CSS classes for the trigger. */
|
|
1636
|
+
userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
|
|
1637
|
+
/** Additional CSS classes for the panel. */
|
|
1638
|
+
panelClass = input('', ...(ngDevMode ? [{ debugName: "panelClass" }] : []));
|
|
1639
|
+
/** Maximum height of the panel. */
|
|
1640
|
+
maxHeight = input('256px', ...(ngDevMode ? [{ debugName: "maxHeight" }] : []));
|
|
1641
|
+
/** Panel width strategy. */
|
|
1642
|
+
panelWidth = input('trigger', ...(ngDevMode ? [{ debugName: "panelWidth" }] : []));
|
|
1643
|
+
/** Debounce time for search (ms). */
|
|
1644
|
+
searchDebounceMs = input(300, ...(ngDevMode ? [{ debugName: "searchDebounceMs" }] : []));
|
|
1645
|
+
/** Virtual scroll threshold. */
|
|
1646
|
+
virtualScrollThreshold = input(VIRTUAL_SCROLL_THRESHOLD, ...(ngDevMode ? [{ debugName: "virtualScrollThreshold" }] : []));
|
|
1647
|
+
/** Maximum number of tags to display in multi-select mode. Set to null for no limit. */
|
|
1648
|
+
maxVisibleTags = input(2, ...(ngDevMode ? [{ debugName: "maxVisibleTags" }] : []));
|
|
1649
|
+
/** Custom error state matcher for determining when to show errors. */
|
|
1650
|
+
errorStateMatcher = input(...(ngDevMode ? [undefined, { debugName: "errorStateMatcher" }] : []));
|
|
1651
|
+
// ============ OUTPUTS ============
|
|
1652
|
+
/** Emitted when the value changes. */
|
|
1653
|
+
valueChange = output();
|
|
1654
|
+
/** Emitted when search query changes. */
|
|
1655
|
+
searchChange = output();
|
|
1656
|
+
/** Emitted when panel opens. */
|
|
1657
|
+
opened = output();
|
|
1658
|
+
/** Emitted when panel closes. */
|
|
1659
|
+
closed = output();
|
|
1660
|
+
// ============ INTERNAL STATE ============
|
|
1661
|
+
/** Whether the panel is open. */
|
|
1662
|
+
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
1663
|
+
/** Whether the trigger button is focused. */
|
|
1664
|
+
_triggerFocused = signal(false, ...(ngDevMode ? [{ debugName: "_triggerFocused" }] : []));
|
|
1665
|
+
/** Current search query. */
|
|
1666
|
+
searchQuery = signal('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : []));
|
|
1667
|
+
/** Currently active (keyboard focused) option ID. */
|
|
1668
|
+
activeOptionId = signal(null, ...(ngDevMode ? [{ debugName: "activeOptionId" }] : []));
|
|
1669
|
+
/** Internal value state (managed by CVA or input). */
|
|
1670
|
+
internalValue = linkedSignal(() => this.value() ?? null, ...(ngDevMode ? [{ debugName: "internalValue" }] : []));
|
|
1671
|
+
/** Live announcements for screen readers. */
|
|
1672
|
+
liveAnnouncement = signal('', ...(ngDevMode ? [{ debugName: "liveAnnouncement" }] : []));
|
|
1673
|
+
/** IDs for aria-describedby (set by form-field). */
|
|
1674
|
+
_describedByIds = signal('', ...(ngDevMode ? [{ debugName: "_describedByIds" }] : []));
|
|
1675
|
+
/** Form field appearance (set by form-field). */
|
|
1676
|
+
_appearance = signal('outline', ...(ngDevMode ? [{ debugName: "_appearance" }] : []));
|
|
1677
|
+
// ============ COMPUTED STATE ============
|
|
1678
|
+
/** Trigger element ID. */
|
|
1679
|
+
triggerId = computed(() => `${this.dropdownId}-trigger`, ...(ngDevMode ? [{ debugName: "triggerId" }] : []));
|
|
1680
|
+
/** Panel element ID. */
|
|
1681
|
+
panelId = computed(() => `${this.dropdownId}-panel`, ...(ngDevMode ? [{ debugName: "panelId" }] : []));
|
|
1682
|
+
/** Currently active descendant ID (for ARIA). */
|
|
1683
|
+
activeDescendant = computed(() => {
|
|
1684
|
+
return this.isOpen() ? this.activeOptionId() : null;
|
|
1685
|
+
}, ...(ngDevMode ? [{ debugName: "activeDescendant" }] : []));
|
|
1686
|
+
/** Whether the dropdown has a value. */
|
|
1687
|
+
hasValue = computed(() => {
|
|
1688
|
+
const val = this.internalValue();
|
|
1689
|
+
if (this.multiple()) {
|
|
1690
|
+
return Array.isArray(val) && val.length > 0;
|
|
1691
|
+
}
|
|
1692
|
+
return val !== null && val !== undefined;
|
|
1693
|
+
}, ...(ngDevMode ? [{ debugName: "hasValue" }] : []));
|
|
1694
|
+
// ============ FormFieldControl SIGNALS ============
|
|
1695
|
+
/** Whether the dropdown is focused (trigger focused or panel open). Implements FormFieldControl. */
|
|
1696
|
+
focused = computed(() => this._triggerFocused() || this.isOpen(), ...(ngDevMode ? [{ debugName: "focused" }] : []));
|
|
1697
|
+
/** Whether the label should float. Label floats when focused or has a value. */
|
|
1698
|
+
shouldLabelFloat = computed(() => this.focused() || this.hasValue(), ...(ngDevMode ? [{ debugName: "shouldLabelFloat" }] : []));
|
|
1699
|
+
/** Whether the control is in an error state. Implements FormFieldControl. */
|
|
1700
|
+
errorState = computed(() => {
|
|
1701
|
+
// Read reactive dependencies to trigger re-evaluation
|
|
1702
|
+
this.isOpen();
|
|
1703
|
+
this.hasValue();
|
|
1704
|
+
const matcher = this.errorStateMatcher() ?? this.defaultErrorStateMatcher;
|
|
1705
|
+
const form = this.parentFormGroup ?? this.parentForm;
|
|
1706
|
+
return matcher.isErrorState(this.ngControl?.control ?? null, form);
|
|
1707
|
+
}, ...(ngDevMode ? [{ debugName: "errorState" }] : []));
|
|
1708
|
+
/** Unique ID for the control. Implements FormFieldControl. */
|
|
1709
|
+
id = this.triggerId;
|
|
1710
|
+
/**
|
|
1711
|
+
* Effective state combining manual state with automatic error detection.
|
|
1712
|
+
* Manual state takes precedence over auto-detected error state.
|
|
1713
|
+
*/
|
|
1714
|
+
effectiveState = computed(() => {
|
|
1715
|
+
const manualState = this.state();
|
|
1716
|
+
if (manualState !== 'default')
|
|
1717
|
+
return manualState;
|
|
1718
|
+
return this.errorState() ? 'error' : 'default';
|
|
1719
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveState" }] : []));
|
|
1720
|
+
/** Combined aria-describedby from form-field. */
|
|
1721
|
+
ariaDescribedBy = computed(() => this._describedByIds() || null, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
|
|
1722
|
+
/** Selected value (single mode). */
|
|
1723
|
+
selectedValue = computed(() => {
|
|
1724
|
+
const val = this.internalValue();
|
|
1725
|
+
if (this.multiple() || Array.isArray(val)) {
|
|
1726
|
+
return null;
|
|
1727
|
+
}
|
|
1728
|
+
return val;
|
|
1729
|
+
}, ...(ngDevMode ? [{ debugName: "selectedValue" }] : []));
|
|
1730
|
+
/** Selected values (multiple mode). */
|
|
1731
|
+
selectedValues = computed(() => {
|
|
1732
|
+
const val = this.internalValue();
|
|
1733
|
+
if (!this.multiple()) {
|
|
1734
|
+
return [];
|
|
1735
|
+
}
|
|
1736
|
+
return Array.isArray(val) ? val : val !== null ? [val] : [];
|
|
1737
|
+
}, ...(ngDevMode ? [{ debugName: "selectedValues" }] : []));
|
|
1738
|
+
/** Tags visible in the trigger (limited by maxVisibleTags). */
|
|
1739
|
+
visibleTags = computed(() => {
|
|
1740
|
+
const all = this.selectedValues();
|
|
1741
|
+
const max = this.maxVisibleTags();
|
|
1742
|
+
if (max === null || all.length <= max) {
|
|
1743
|
+
return all;
|
|
1744
|
+
}
|
|
1745
|
+
return all.slice(0, max);
|
|
1746
|
+
}, ...(ngDevMode ? [{ debugName: "visibleTags" }] : []));
|
|
1747
|
+
/** Count of hidden tags (for +N badge). */
|
|
1748
|
+
hiddenTagsCount = computed(() => {
|
|
1749
|
+
const all = this.selectedValues();
|
|
1750
|
+
const max = this.maxVisibleTags();
|
|
1751
|
+
if (max === null || all.length <= max) {
|
|
1752
|
+
return 0;
|
|
1753
|
+
}
|
|
1754
|
+
return all.length - max;
|
|
1755
|
+
}, ...(ngDevMode ? [{ debugName: "hiddenTagsCount" }] : []));
|
|
1756
|
+
/** Processed options with display text and IDs. */
|
|
1757
|
+
processedOptions = computed(() => {
|
|
1758
|
+
const opts = this.options();
|
|
1759
|
+
const display = this.displayWith();
|
|
1760
|
+
return opts.map((opt, index) => ({
|
|
1761
|
+
value: opt,
|
|
1762
|
+
displayText: display(opt),
|
|
1763
|
+
disabled: false, // Could be extended with disabledWith input
|
|
1764
|
+
id: `${this.dropdownId}-option-${index}`,
|
|
1765
|
+
}));
|
|
1766
|
+
}, ...(ngDevMode ? [{ debugName: "processedOptions" }] : []));
|
|
1767
|
+
/** Filtered options based on search query. */
|
|
1768
|
+
filteredOptions = computed(() => {
|
|
1769
|
+
const opts = this.processedOptions();
|
|
1770
|
+
const query = this.searchQuery().trim();
|
|
1771
|
+
if (!query) {
|
|
1772
|
+
return opts;
|
|
1773
|
+
}
|
|
1774
|
+
const filterFn = this.filterWith();
|
|
1775
|
+
const display = this.displayWith();
|
|
1776
|
+
return opts.filter((opt) => {
|
|
1777
|
+
if (filterFn) {
|
|
1778
|
+
return filterFn(opt.value, query);
|
|
1779
|
+
}
|
|
1780
|
+
return defaultFilterWith(opt.value, query, display);
|
|
1781
|
+
});
|
|
1782
|
+
}, ...(ngDevMode ? [{ debugName: "filteredOptions" }] : []));
|
|
1783
|
+
/** Grouped options (when groupBy is provided). */
|
|
1784
|
+
groupedOptions = computed(() => {
|
|
1785
|
+
const groupFn = this.groupBy();
|
|
1786
|
+
if (!groupFn) {
|
|
1787
|
+
return [];
|
|
1788
|
+
}
|
|
1789
|
+
const opts = this.filteredOptions();
|
|
1790
|
+
const groups = new Map();
|
|
1791
|
+
for (const opt of opts) {
|
|
1792
|
+
const key = groupFn(opt.value);
|
|
1793
|
+
const existing = groups.get(key) || [];
|
|
1794
|
+
existing.push(opt);
|
|
1795
|
+
groups.set(key, existing);
|
|
1796
|
+
}
|
|
1797
|
+
return Array.from(groups.entries()).map(([key, options]) => ({
|
|
1798
|
+
key,
|
|
1799
|
+
options,
|
|
1800
|
+
expanded: true,
|
|
1801
|
+
}));
|
|
1802
|
+
}, ...(ngDevMode ? [{ debugName: "groupedOptions" }] : []));
|
|
1803
|
+
/** Whether virtual scrolling should be enabled. */
|
|
1804
|
+
virtualScrollEnabled = computed(() => {
|
|
1805
|
+
return this.filteredOptions().length > this.virtualScrollThreshold();
|
|
1806
|
+
}, ...(ngDevMode ? [{ debugName: "virtualScrollEnabled" }] : []));
|
|
1807
|
+
/** Context for selected template. */
|
|
1808
|
+
selectedContext = computed(() => ({
|
|
1809
|
+
$implicit: this.internalValue(),
|
|
1810
|
+
placeholder: this.placeholder(),
|
|
1811
|
+
multiple: this.multiple(),
|
|
1812
|
+
}), ...(ngDevMode ? [{ debugName: "selectedContext" }] : []));
|
|
1813
|
+
/** Context for empty template. */
|
|
1814
|
+
emptyContext = computed(() => ({
|
|
1815
|
+
$implicit: this.searchQuery(),
|
|
1816
|
+
}), ...(ngDevMode ? [{ debugName: "emptyContext" }] : []));
|
|
1817
|
+
/** Computed trigger classes. */
|
|
1818
|
+
triggerClasses = computed(() => {
|
|
1819
|
+
const baseClasses = dropdownTriggerVariants({
|
|
1820
|
+
variant: this.variant(),
|
|
1821
|
+
size: this.size(),
|
|
1822
|
+
state: this.effectiveState(),
|
|
1823
|
+
open: this.isOpen(),
|
|
1824
|
+
});
|
|
1825
|
+
// For naked variant, add padding based on form-field appearance
|
|
1826
|
+
let paddingClasses = '';
|
|
1827
|
+
if (this.variant() === 'naked') {
|
|
1828
|
+
paddingClasses = this._appearance() === 'fill' ? 'pt-5 pb-1.5 px-3' : 'py-2.5 px-3';
|
|
1829
|
+
}
|
|
1830
|
+
return mergeClasses(baseClasses, paddingClasses, this.userClass());
|
|
1831
|
+
}, ...(ngDevMode ? [{ debugName: "triggerClasses" }] : []));
|
|
1832
|
+
/** Computed panel classes. */
|
|
1833
|
+
panelClasses = computed(() => {
|
|
1834
|
+
return mergeClasses('w-full z-50 overflow-hidden rounded-overlay border border-border-subtle bg-popover text-popover-foreground shadow-lg outline-none', this.panelClass());
|
|
1835
|
+
}, ...(ngDevMode ? [{ debugName: "panelClasses" }] : []));
|
|
1836
|
+
/** Computed chevron classes. */
|
|
1837
|
+
chevronClasses = computed(() => {
|
|
1838
|
+
return dropdownChevronVariants({
|
|
1839
|
+
size: this.size(),
|
|
1840
|
+
open: this.isOpen(),
|
|
1841
|
+
});
|
|
1842
|
+
}, ...(ngDevMode ? [{ debugName: "chevronClasses" }] : []));
|
|
1843
|
+
/** Computed clear button classes. */
|
|
1844
|
+
clearClasses = computed(() => {
|
|
1845
|
+
return dropdownClearVariants({ size: this.size() });
|
|
1846
|
+
}, ...(ngDevMode ? [{ debugName: "clearClasses" }] : []));
|
|
1847
|
+
/** Computed overflow badge classes. */
|
|
1848
|
+
overflowBadgeClasses = computed(() => {
|
|
1849
|
+
return dropdownOverflowBadgeVariants({ size: this.size() });
|
|
1850
|
+
}, ...(ngDevMode ? [{ debugName: "overflowBadgeClasses" }] : []));
|
|
1851
|
+
// ============ CVA CALLBACKS ============
|
|
1852
|
+
onChange = () => { };
|
|
1853
|
+
onTouched = () => { };
|
|
1854
|
+
constructor() {
|
|
1855
|
+
if (this.ngControl) {
|
|
1856
|
+
this.ngControl.valueAccessor = this;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
// ============ CVA IMPLEMENTATION ============
|
|
1860
|
+
writeValue(value) {
|
|
1861
|
+
this.internalValue.set(value);
|
|
1862
|
+
}
|
|
1863
|
+
registerOnChange(fn) {
|
|
1864
|
+
this.onChange = fn;
|
|
1865
|
+
}
|
|
1866
|
+
registerOnTouched(fn) {
|
|
1867
|
+
this.onTouched = fn;
|
|
1868
|
+
}
|
|
1869
|
+
setDisabledState(isDisabled) {
|
|
1870
|
+
// Disabled state is handled via the disabled input
|
|
1871
|
+
// When using forms, the form control's disabled state takes precedence
|
|
1872
|
+
}
|
|
1873
|
+
// ============ FormFieldControl IMPLEMENTATION ============
|
|
1874
|
+
/**
|
|
1875
|
+
* Called when the form field container is clicked.
|
|
1876
|
+
* Implements FormFieldControl.
|
|
1877
|
+
*/
|
|
1878
|
+
onContainerClick(event) {
|
|
1879
|
+
const target = event.target;
|
|
1880
|
+
// Only toggle if click was outside the trigger button (not on it or inside it)
|
|
1881
|
+
if (!this.disabled() && !this.triggerRef().nativeElement.contains(target)) {
|
|
1882
|
+
this.toggle();
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Sets the describedBy IDs from the form field.
|
|
1887
|
+
* Called by the parent form field component.
|
|
1888
|
+
*/
|
|
1889
|
+
setDescribedByIds(ids) {
|
|
1890
|
+
this._describedByIds.set(ids);
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Sets the appearance for styling.
|
|
1894
|
+
* Called by the parent form field component.
|
|
1895
|
+
*/
|
|
1896
|
+
setAppearance(appearance) {
|
|
1897
|
+
this._appearance.set(appearance);
|
|
1898
|
+
}
|
|
1899
|
+
// ============ PUBLIC METHODS ============
|
|
1900
|
+
/** Opens the dropdown panel. */
|
|
1901
|
+
open() {
|
|
1902
|
+
if (this.disabled() || this.isOpen()) {
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
this.createOverlay();
|
|
1906
|
+
this.isOpen.set(true);
|
|
1907
|
+
this.opened.emit();
|
|
1908
|
+
// Set initial active option
|
|
1909
|
+
const currentValue = this.internalValue();
|
|
1910
|
+
if (currentValue !== null && !this.multiple()) {
|
|
1911
|
+
const option = this.filteredOptions().find((opt) => this.compareWith()(opt.value, currentValue));
|
|
1912
|
+
if (option) {
|
|
1913
|
+
this.activeOptionId.set(option.id);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
else {
|
|
1917
|
+
const firstOption = this.filteredOptions()[0];
|
|
1918
|
+
if (firstOption) {
|
|
1919
|
+
this.activeOptionId.set(firstOption.id);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
// Announce opening
|
|
1923
|
+
this.announce(`${this.placeholder()} dropdown opened, ${this.filteredOptions().length} options available`);
|
|
1924
|
+
}
|
|
1925
|
+
/** Closes the dropdown panel. */
|
|
1926
|
+
close() {
|
|
1927
|
+
if (!this.isOpen()) {
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
this.destroyOverlay();
|
|
1931
|
+
this.isOpen.set(false);
|
|
1932
|
+
this.searchQuery.set('');
|
|
1933
|
+
this.activeOptionId.set(null);
|
|
1934
|
+
this.closed.emit();
|
|
1935
|
+
this.onTouched();
|
|
1936
|
+
}
|
|
1937
|
+
/** Toggles the dropdown panel. */
|
|
1938
|
+
toggle() {
|
|
1939
|
+
if (this.isOpen()) {
|
|
1940
|
+
this.close();
|
|
1941
|
+
}
|
|
1942
|
+
else {
|
|
1943
|
+
this.open();
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
/** Clears the selection. */
|
|
1947
|
+
clear(event) {
|
|
1948
|
+
event?.preventDefault();
|
|
1949
|
+
event?.stopPropagation();
|
|
1950
|
+
const newValue = this.multiple() ? [] : null;
|
|
1951
|
+
this.updateValue(newValue);
|
|
1952
|
+
this.announce('Selection cleared');
|
|
1953
|
+
}
|
|
1954
|
+
/** Checks if a value is selected. */
|
|
1955
|
+
isSelected(value) {
|
|
1956
|
+
const current = this.internalValue();
|
|
1957
|
+
const compare = this.compareWith();
|
|
1958
|
+
if (this.multiple()) {
|
|
1959
|
+
const arr = Array.isArray(current) ? current : [];
|
|
1960
|
+
return arr.some((v) => compare(v, value));
|
|
1961
|
+
}
|
|
1962
|
+
return current !== null && compare(current, value);
|
|
1963
|
+
}
|
|
1964
|
+
/** Checks if an option ID is the active one. */
|
|
1965
|
+
isActive(optionId) {
|
|
1966
|
+
return this.activeOptionId() === optionId;
|
|
1967
|
+
}
|
|
1968
|
+
// ============ EVENT HANDLERS ============
|
|
1969
|
+
onTriggerKeydown(event) {
|
|
1970
|
+
switch (event.key) {
|
|
1971
|
+
case 'ArrowDown':
|
|
1972
|
+
case 'ArrowUp':
|
|
1973
|
+
event.preventDefault();
|
|
1974
|
+
if (!this.isOpen()) {
|
|
1975
|
+
this.open();
|
|
1976
|
+
}
|
|
1977
|
+
else {
|
|
1978
|
+
this.navigateOptions(event.key === 'ArrowDown' ? 1 : -1);
|
|
1979
|
+
}
|
|
1980
|
+
break;
|
|
1981
|
+
case 'Enter':
|
|
1982
|
+
case ' ':
|
|
1983
|
+
event.preventDefault();
|
|
1984
|
+
if (this.isOpen() && this.activeOptionId()) {
|
|
1985
|
+
this.selectActiveOption();
|
|
1986
|
+
}
|
|
1987
|
+
else {
|
|
1988
|
+
this.toggle();
|
|
1989
|
+
}
|
|
1990
|
+
break;
|
|
1991
|
+
case 'Escape':
|
|
1992
|
+
if (this.isOpen()) {
|
|
1993
|
+
event.preventDefault();
|
|
1994
|
+
this.close();
|
|
1995
|
+
this.triggerRef().nativeElement.focus();
|
|
1996
|
+
}
|
|
1997
|
+
break;
|
|
1998
|
+
case 'Home':
|
|
1999
|
+
if (this.isOpen()) {
|
|
2000
|
+
event.preventDefault();
|
|
2001
|
+
this.navigateToFirst();
|
|
2002
|
+
}
|
|
2003
|
+
break;
|
|
2004
|
+
case 'End':
|
|
2005
|
+
if (this.isOpen()) {
|
|
2006
|
+
event.preventDefault();
|
|
2007
|
+
this.navigateToLast();
|
|
2008
|
+
}
|
|
2009
|
+
break;
|
|
2010
|
+
case 'Tab':
|
|
2011
|
+
// Close panel when tabbing away from trigger
|
|
2012
|
+
if (this.isOpen()) {
|
|
2013
|
+
this.close();
|
|
2014
|
+
}
|
|
2015
|
+
// Don't prevent default - let Tab naturally move focus
|
|
2016
|
+
break;
|
|
2017
|
+
default:
|
|
2018
|
+
// Type-ahead search
|
|
2019
|
+
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
|
|
2020
|
+
this.typeAhead(event.key);
|
|
2021
|
+
}
|
|
2022
|
+
break;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
onPanelKeydown(event) {
|
|
2026
|
+
switch (event.key) {
|
|
2027
|
+
case 'ArrowDown':
|
|
2028
|
+
event.preventDefault();
|
|
2029
|
+
this.navigateOptions(1);
|
|
2030
|
+
break;
|
|
2031
|
+
case 'ArrowUp':
|
|
2032
|
+
event.preventDefault();
|
|
2033
|
+
this.navigateOptions(-1);
|
|
2034
|
+
break;
|
|
2035
|
+
case 'Enter':
|
|
2036
|
+
case ' ':
|
|
2037
|
+
event.preventDefault();
|
|
2038
|
+
this.selectActiveOption();
|
|
2039
|
+
break;
|
|
2040
|
+
case 'Escape':
|
|
2041
|
+
event.preventDefault();
|
|
2042
|
+
this.close();
|
|
2043
|
+
this.triggerRef().nativeElement.focus();
|
|
2044
|
+
break;
|
|
2045
|
+
case 'Tab':
|
|
2046
|
+
this.close();
|
|
2047
|
+
break;
|
|
2048
|
+
case 'Home':
|
|
2049
|
+
event.preventDefault();
|
|
2050
|
+
this.navigateToFirst();
|
|
2051
|
+
break;
|
|
2052
|
+
case 'End':
|
|
2053
|
+
event.preventDefault();
|
|
2054
|
+
this.navigateToLast();
|
|
2055
|
+
break;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
onSearchChange(query) {
|
|
2059
|
+
this.searchQuery.set(query);
|
|
2060
|
+
this.searchChange.emit(query);
|
|
2061
|
+
// Reset active option to first result
|
|
2062
|
+
const filtered = this.filteredOptions();
|
|
2063
|
+
const firstFiltered = filtered[0];
|
|
2064
|
+
if (firstFiltered) {
|
|
2065
|
+
this.activeOptionId.set(firstFiltered.id);
|
|
2066
|
+
}
|
|
2067
|
+
else {
|
|
2068
|
+
this.activeOptionId.set(null);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
onSearchKeyNav(event) {
|
|
2072
|
+
// Delegate to panel keydown handler
|
|
2073
|
+
this.onPanelKeydown(event);
|
|
2074
|
+
}
|
|
2075
|
+
onTriggerFocus() {
|
|
2076
|
+
this._triggerFocused.set(true);
|
|
2077
|
+
}
|
|
2078
|
+
onTriggerBlur() {
|
|
2079
|
+
this._triggerFocused.set(false);
|
|
2080
|
+
}
|
|
2081
|
+
onOptionHover(optionId) {
|
|
2082
|
+
this.activeOptionId.set(optionId);
|
|
2083
|
+
}
|
|
2084
|
+
selectOption(value) {
|
|
2085
|
+
if (this.multiple()) {
|
|
2086
|
+
this.toggleMultipleValue(value);
|
|
2087
|
+
}
|
|
2088
|
+
else {
|
|
2089
|
+
this.updateValue(value);
|
|
2090
|
+
this.close();
|
|
2091
|
+
this.triggerRef().nativeElement.focus();
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
removeValue(value) {
|
|
2095
|
+
const current = this.selectedValues();
|
|
2096
|
+
const compare = this.compareWith();
|
|
2097
|
+
const newValues = current.filter((v) => !compare(v, value));
|
|
2098
|
+
this.updateValue(newValues.length > 0 ? newValues : []);
|
|
2099
|
+
this.announce(`${this.displayWith()(value)} removed`);
|
|
2100
|
+
}
|
|
2101
|
+
trackByValue(item, _index) {
|
|
2102
|
+
return item;
|
|
2103
|
+
}
|
|
2104
|
+
getGlobalIndex(groupKey, localIndex) {
|
|
2105
|
+
const groups = this.groupedOptions();
|
|
2106
|
+
let globalIndex = 0;
|
|
2107
|
+
for (const group of groups) {
|
|
2108
|
+
if (group.key === groupKey) {
|
|
2109
|
+
return globalIndex + localIndex;
|
|
2110
|
+
}
|
|
2111
|
+
globalIndex += group.options.length;
|
|
2112
|
+
}
|
|
2113
|
+
return localIndex;
|
|
2114
|
+
}
|
|
2115
|
+
// ============ PRIVATE METHODS ============
|
|
2116
|
+
createOverlay() {
|
|
2117
|
+
if (this.overlayRef) {
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
const hostEl = this.elementRef.nativeElement;
|
|
2121
|
+
const positionStrategy = this.overlay
|
|
2122
|
+
.position()
|
|
2123
|
+
.flexibleConnectedTo(hostEl)
|
|
2124
|
+
.withPositions(DEFAULT_POSITIONS)
|
|
2125
|
+
.withFlexibleDimensions(false)
|
|
2126
|
+
.withPush(true);
|
|
2127
|
+
const hostWidth = hostEl.getBoundingClientRect().width;
|
|
2128
|
+
this.overlayRef = this.overlay.create({
|
|
2129
|
+
positionStrategy,
|
|
2130
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
2131
|
+
...this.getPanelWidthConfig(hostWidth),
|
|
2132
|
+
hasBackdrop: true,
|
|
2133
|
+
backdropClass: 'cdk-overlay-transparent-backdrop',
|
|
2134
|
+
});
|
|
2135
|
+
// Attach panel template
|
|
2136
|
+
const portal = new TemplatePortal(this.panelTemplateRef(), this.viewContainerRef);
|
|
2137
|
+
this.overlayRef.attach(portal);
|
|
2138
|
+
// Close on backdrop click
|
|
2139
|
+
this.overlayRef
|
|
2140
|
+
.backdropClick()
|
|
2141
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
2142
|
+
.subscribe(() => this.close());
|
|
2143
|
+
// Close on outside click
|
|
2144
|
+
this.overlayRef
|
|
2145
|
+
.outsidePointerEvents()
|
|
2146
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
2147
|
+
.subscribe(() => this.close());
|
|
2148
|
+
}
|
|
2149
|
+
destroyOverlay() {
|
|
2150
|
+
if (this.overlayRef) {
|
|
2151
|
+
this.overlayRef.dispose();
|
|
2152
|
+
this.overlayRef = null;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Returns overlay width configuration based on panelWidth setting.
|
|
2157
|
+
* - 'trigger': min-width equals host, can grow wider
|
|
2158
|
+
* - 'auto': no width constraint
|
|
2159
|
+
* - specific value: exact width
|
|
2160
|
+
*/
|
|
2161
|
+
getPanelWidthConfig(hostWidth) {
|
|
2162
|
+
const config = this.panelWidth();
|
|
2163
|
+
if (config === 'trigger') {
|
|
2164
|
+
return { minWidth: hostWidth };
|
|
2165
|
+
}
|
|
2166
|
+
if (config === 'auto') {
|
|
2167
|
+
return {};
|
|
2168
|
+
}
|
|
2169
|
+
return { width: config };
|
|
2170
|
+
}
|
|
2171
|
+
updateValue(value) {
|
|
2172
|
+
this.internalValue.set(value);
|
|
2173
|
+
this.onChange(value);
|
|
2174
|
+
this.valueChange.emit(value);
|
|
2175
|
+
// Announce selection
|
|
2176
|
+
if (value !== null && !Array.isArray(value)) {
|
|
2177
|
+
this.announce(`${this.displayWith()(value)} selected`);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
toggleMultipleValue(value) {
|
|
2181
|
+
const current = this.selectedValues();
|
|
2182
|
+
const compare = this.compareWith();
|
|
2183
|
+
const isAlreadySelected = current.some((v) => compare(v, value));
|
|
2184
|
+
let newValues;
|
|
2185
|
+
if (isAlreadySelected) {
|
|
2186
|
+
newValues = current.filter((v) => !compare(v, value));
|
|
2187
|
+
this.announce(`${this.displayWith()(value)} deselected`);
|
|
2188
|
+
}
|
|
2189
|
+
else {
|
|
2190
|
+
newValues = [...current, value];
|
|
2191
|
+
this.announce(`${this.displayWith()(value)} selected`);
|
|
2192
|
+
}
|
|
2193
|
+
this.updateValue(newValues.length > 0 ? newValues : []);
|
|
2194
|
+
}
|
|
2195
|
+
navigateOptions(direction) {
|
|
2196
|
+
const options = this.groupBy() ? this.flattenGroupedOptions() : this.filteredOptions();
|
|
2197
|
+
if (options.length === 0) {
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
const currentId = this.activeOptionId();
|
|
2201
|
+
let currentIndex = options.findIndex((opt) => opt.id === currentId);
|
|
2202
|
+
if (currentIndex === -1) {
|
|
2203
|
+
currentIndex = direction === 1 ? -1 : options.length;
|
|
2204
|
+
}
|
|
2205
|
+
let nextIndex = currentIndex + direction;
|
|
2206
|
+
// Wrap around
|
|
2207
|
+
if (nextIndex < 0) {
|
|
2208
|
+
nextIndex = options.length - 1;
|
|
2209
|
+
}
|
|
2210
|
+
else if (nextIndex >= options.length) {
|
|
2211
|
+
nextIndex = 0;
|
|
2212
|
+
}
|
|
2213
|
+
// Skip disabled options
|
|
2214
|
+
const startIndex = nextIndex;
|
|
2215
|
+
let currentOption = options[nextIndex];
|
|
2216
|
+
while (currentOption?.disabled) {
|
|
2217
|
+
nextIndex += direction;
|
|
2218
|
+
if (nextIndex < 0)
|
|
2219
|
+
nextIndex = options.length - 1;
|
|
2220
|
+
else if (nextIndex >= options.length)
|
|
2221
|
+
nextIndex = 0;
|
|
2222
|
+
if (nextIndex === startIndex)
|
|
2223
|
+
return; // All disabled
|
|
2224
|
+
currentOption = options[nextIndex];
|
|
2225
|
+
}
|
|
2226
|
+
const targetOption = options[nextIndex];
|
|
2227
|
+
if (targetOption) {
|
|
2228
|
+
this.activeOptionId.set(targetOption.id);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
navigateToFirst() {
|
|
2232
|
+
const options = this.groupBy() ? this.flattenGroupedOptions() : this.filteredOptions();
|
|
2233
|
+
const firstEnabled = options.find((opt) => !opt.disabled);
|
|
2234
|
+
if (firstEnabled) {
|
|
2235
|
+
this.activeOptionId.set(firstEnabled.id);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
navigateToLast() {
|
|
2239
|
+
const options = this.groupBy() ? this.flattenGroupedOptions() : this.filteredOptions();
|
|
2240
|
+
const lastEnabled = [...options].reverse().find((opt) => !opt.disabled);
|
|
2241
|
+
if (lastEnabled) {
|
|
2242
|
+
this.activeOptionId.set(lastEnabled.id);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
selectActiveOption() {
|
|
2246
|
+
const activeId = this.activeOptionId();
|
|
2247
|
+
if (!activeId)
|
|
2248
|
+
return;
|
|
2249
|
+
const options = this.groupBy() ? this.flattenGroupedOptions() : this.filteredOptions();
|
|
2250
|
+
const option = options.find((opt) => opt.id === activeId);
|
|
2251
|
+
if (option && !option.disabled) {
|
|
2252
|
+
this.selectOption(option.value);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
flattenGroupedOptions() {
|
|
2256
|
+
return this.groupedOptions().flatMap((group) => group.options);
|
|
2257
|
+
}
|
|
2258
|
+
typeAhead(char) {
|
|
2259
|
+
// Simple type-ahead: find first option starting with the character
|
|
2260
|
+
const options = this.filteredOptions();
|
|
2261
|
+
const display = this.displayWith();
|
|
2262
|
+
const lowerChar = char.toLowerCase();
|
|
2263
|
+
const match = options.find((opt) => !opt.disabled && display(opt.value).toLowerCase().startsWith(lowerChar));
|
|
2264
|
+
if (match) {
|
|
2265
|
+
this.activeOptionId.set(match.id);
|
|
2266
|
+
if (!this.isOpen()) {
|
|
2267
|
+
this.selectOption(match.value);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
announce(message) {
|
|
2272
|
+
this.liveAnnouncement.set(message);
|
|
2273
|
+
this.liveAnnouncer.announce(message, 'polite');
|
|
2274
|
+
}
|
|
2275
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2276
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdown, isStandalone: true, selector: "com-dropdown", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: true, isRequired: false, transformFunction: null }, filterWith: { classPropertyName: "filterWith", publicName: "filterWith", isSignal: true, isRequired: false, transformFunction: null }, groupBy: { classPropertyName: "groupBy", publicName: "groupBy", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, panelWidth: { classPropertyName: "panelWidth", publicName: "panelWidth", isSignal: true, isRequired: false, transformFunction: null }, searchDebounceMs: { classPropertyName: "searchDebounceMs", publicName: "searchDebounceMs", isSignal: true, isRequired: false, transformFunction: null }, virtualScrollThreshold: { classPropertyName: "virtualScrollThreshold", publicName: "virtualScrollThreshold", isSignal: true, isRequired: false, transformFunction: null }, maxVisibleTags: { classPropertyName: "maxVisibleTags", publicName: "maxVisibleTags", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", searchChange: "searchChange", opened: "opened", closed: "closed" }, host: { properties: { "class.com-dropdown-disabled": "disabled()", "class.com-dropdown-open": "isOpen()" }, classAttribute: "com-dropdown-host inline-block" }, providers: [{ provide: FormFieldControl, useExisting: forwardRef(() => ComDropdown) }], queries: [{ propertyName: "optionTemplate", first: true, predicate: (ComDropdownOptionTpl), descendants: true, isSignal: true }, { propertyName: "selectedTemplate", first: true, predicate: (ComDropdownSelectedTpl), descendants: true, isSignal: true }, { propertyName: "emptyTemplate", first: true, predicate: ComDropdownEmptyTpl, descendants: true, isSignal: true }, { propertyName: "groupTemplate", first: true, predicate: ComDropdownGroupTpl, descendants: true, isSignal: true }, { propertyName: "tagTemplate", first: true, predicate: (ComDropdownTagTpl), descendants: true, isSignal: true }], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["triggerElement"], descendants: true, isSignal: true }, { propertyName: "panelTemplateRef", first: true, predicate: ["panelTemplate"], descendants: true, isSignal: true }], exportAs: ["comDropdown"], ngImport: i0, template: `
|
|
2277
|
+
<!-- Trigger button -->
|
|
2278
|
+
<button
|
|
2279
|
+
#triggerElement
|
|
2280
|
+
type="button"
|
|
2281
|
+
role="combobox"
|
|
2282
|
+
[class]="triggerClasses()"
|
|
2283
|
+
[attr.id]="triggerId()"
|
|
2284
|
+
[attr.aria-expanded]="isOpen()"
|
|
2285
|
+
[attr.aria-controls]="panelId()"
|
|
2286
|
+
[attr.aria-haspopup]="'listbox'"
|
|
2287
|
+
[attr.aria-activedescendant]="activeDescendant()"
|
|
2288
|
+
[attr.aria-required]="required() || null"
|
|
2289
|
+
[attr.aria-invalid]="effectiveState() === 'error' || null"
|
|
2290
|
+
[attr.aria-describedby]="ariaDescribedBy()"
|
|
2291
|
+
[attr.aria-disabled]="disabled() || null"
|
|
2292
|
+
[attr.tabindex]="disabled() ? -1 : 0"
|
|
2293
|
+
[disabled]="disabled()"
|
|
2294
|
+
(click)="toggle()"
|
|
2295
|
+
(keydown)="onTriggerKeydown($event)"
|
|
2296
|
+
(focus)="onTriggerFocus()"
|
|
2297
|
+
(blur)="onTriggerBlur()"
|
|
2298
|
+
>
|
|
2299
|
+
<!-- Selected value display -->
|
|
2300
|
+
<span class="flex-1 truncate text-left">
|
|
2301
|
+
@if (selectedTemplate()) {
|
|
2302
|
+
<ng-container
|
|
2303
|
+
[ngTemplateOutlet]="selectedTemplate()!.templateRef"
|
|
2304
|
+
[ngTemplateOutletContext]="selectedContext()"
|
|
2305
|
+
/>
|
|
2306
|
+
} @else if (multiple()) {
|
|
2307
|
+
@if (selectedValues().length > 0) {
|
|
2308
|
+
<span class="flex flex-wrap gap-1">
|
|
2309
|
+
@for (item of visibleTags(); track trackByValue(item, $index); let i = $index) {
|
|
2310
|
+
<com-dropdown-tag
|
|
2311
|
+
[value]="item"
|
|
2312
|
+
[displayText]="displayWith()(item)"
|
|
2313
|
+
[index]="i"
|
|
2314
|
+
[size]="size()"
|
|
2315
|
+
[disabled]="disabled()"
|
|
2316
|
+
[tagTemplate]="tagTemplate()?.templateRef ?? null"
|
|
2317
|
+
(remove)="removeValue($event)"
|
|
2318
|
+
/>
|
|
2319
|
+
}
|
|
2320
|
+
@if (hiddenTagsCount() > 0) {
|
|
2321
|
+
<span [class]="overflowBadgeClasses()">
|
|
2322
|
+
+{{ hiddenTagsCount() }}
|
|
2323
|
+
</span>
|
|
2324
|
+
}
|
|
2325
|
+
</span>
|
|
2326
|
+
} @else {
|
|
2327
|
+
<span class="text-placeholder">{{ placeholder() }}</span>
|
|
2328
|
+
}
|
|
2329
|
+
} @else {
|
|
2330
|
+
@if (selectedValue() !== null && selectedValue() !== undefined) {
|
|
2331
|
+
{{ displayWith()(selectedValue()!) }}
|
|
2332
|
+
} @else {
|
|
2333
|
+
<span class="text-placeholder">{{ placeholder() }}</span>
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
</span>
|
|
2337
|
+
|
|
2338
|
+
<!-- Clear button -->
|
|
2339
|
+
@if (clearable() && hasValue() && !disabled()) {
|
|
2340
|
+
<button
|
|
2341
|
+
type="button"
|
|
2342
|
+
[class]="clearClasses()"
|
|
2343
|
+
[attr.aria-label]="'Clear selection'"
|
|
2344
|
+
(click)="clear($event)"
|
|
2345
|
+
>
|
|
2346
|
+
<svg
|
|
2347
|
+
viewBox="0 0 24 24"
|
|
2348
|
+
fill="none"
|
|
2349
|
+
stroke="currentColor"
|
|
2350
|
+
stroke-width="2"
|
|
2351
|
+
stroke-linecap="round"
|
|
2352
|
+
stroke-linejoin="round"
|
|
2353
|
+
class="h-full w-full"
|
|
2354
|
+
>
|
|
2355
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
2356
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
2357
|
+
</svg>
|
|
2358
|
+
</button>
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
<!-- Chevron icon -->
|
|
2362
|
+
<svg
|
|
2363
|
+
[class]="chevronClasses()"
|
|
2364
|
+
viewBox="0 0 24 24"
|
|
2365
|
+
fill="none"
|
|
2366
|
+
stroke="currentColor"
|
|
2367
|
+
stroke-width="2"
|
|
2368
|
+
stroke-linecap="round"
|
|
2369
|
+
stroke-linejoin="round"
|
|
2370
|
+
>
|
|
2371
|
+
<polyline points="6 9 12 15 18 9" />
|
|
2372
|
+
</svg>
|
|
2373
|
+
</button>
|
|
2374
|
+
|
|
2375
|
+
<!-- Panel template (rendered in overlay) -->
|
|
2376
|
+
<ng-template #panelTemplate>
|
|
2377
|
+
<div
|
|
2378
|
+
#panelElement
|
|
2379
|
+
[class]="panelClasses()"
|
|
2380
|
+
[attr.id]="panelId()"
|
|
2381
|
+
[attr.role]="'listbox'"
|
|
2382
|
+
[attr.aria-multiselectable]="multiple() || null"
|
|
2383
|
+
[attr.aria-label]="placeholder()"
|
|
2384
|
+
(keydown)="onPanelKeydown($event)"
|
|
2385
|
+
>
|
|
2386
|
+
<!-- Search input -->
|
|
2387
|
+
@if (searchable()) {
|
|
2388
|
+
<com-dropdown-search
|
|
2389
|
+
[placeholder]="searchPlaceholder()"
|
|
2390
|
+
[size]="size()"
|
|
2391
|
+
[debounceMs]="searchDebounceMs()"
|
|
2392
|
+
(searchChange)="onSearchChange($event)"
|
|
2393
|
+
(keyNav)="onSearchKeyNav($event)"
|
|
2394
|
+
/>
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
<!-- Options list -->
|
|
2398
|
+
<div
|
|
2399
|
+
class="overflow-auto"
|
|
2400
|
+
[style.maxHeight]="maxHeight()"
|
|
2401
|
+
>
|
|
2402
|
+
@if (groupedOptions().length > 0) {
|
|
2403
|
+
@for (group of groupedOptions(); track group.key) {
|
|
2404
|
+
<!-- Group header -->
|
|
2405
|
+
<com-dropdown-group
|
|
2406
|
+
[label]="group.key"
|
|
2407
|
+
[count]="group.options.length"
|
|
2408
|
+
[expanded]="group.expanded"
|
|
2409
|
+
[size]="size()"
|
|
2410
|
+
[groupTemplate]="groupTemplate()?.templateRef ?? null"
|
|
2411
|
+
/>
|
|
2412
|
+
|
|
2413
|
+
<!-- Group options -->
|
|
2414
|
+
@for (option of group.options; track option.id; let i = $index) {
|
|
2415
|
+
<com-dropdown-option
|
|
2416
|
+
[value]="option.value"
|
|
2417
|
+
[displayText]="option.displayText"
|
|
2418
|
+
[id]="option.id"
|
|
2419
|
+
[index]="getGlobalIndex(group.key, i)"
|
|
2420
|
+
[selected]="isSelected(option.value)"
|
|
2421
|
+
[active]="isActive(option.id)"
|
|
2422
|
+
[disabled]="option.disabled"
|
|
2423
|
+
[size]="size()"
|
|
2424
|
+
[optionTemplate]="optionTemplate()?.templateRef ?? null"
|
|
2425
|
+
(select)="selectOption($event)"
|
|
2426
|
+
(hover)="onOptionHover(option.id)"
|
|
2427
|
+
/>
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
} @else if (filteredOptions().length > 0) {
|
|
2431
|
+
@for (option of filteredOptions(); track option.id; let i = $index) {
|
|
2432
|
+
<com-dropdown-option
|
|
2433
|
+
[value]="option.value"
|
|
2434
|
+
[displayText]="option.displayText"
|
|
2435
|
+
[id]="option.id"
|
|
2436
|
+
[index]="i"
|
|
2437
|
+
[selected]="isSelected(option.value)"
|
|
2438
|
+
[active]="isActive(option.id)"
|
|
2439
|
+
[disabled]="option.disabled"
|
|
2440
|
+
[size]="size()"
|
|
2441
|
+
[optionTemplate]="optionTemplate()?.templateRef ?? null"
|
|
2442
|
+
(select)="selectOption($event)"
|
|
2443
|
+
(hover)="onOptionHover(option.id)"
|
|
2444
|
+
/>
|
|
2445
|
+
}
|
|
2446
|
+
} @else {
|
|
2447
|
+
<!-- Empty state -->
|
|
2448
|
+
@if (emptyTemplate()) {
|
|
2449
|
+
<ng-container
|
|
2450
|
+
[ngTemplateOutlet]="emptyTemplate()!.templateRef"
|
|
2451
|
+
[ngTemplateOutletContext]="emptyContext()"
|
|
2452
|
+
/>
|
|
2453
|
+
} @else {
|
|
2454
|
+
<div class="flex items-center justify-center px-3 py-6 text-muted-foreground">
|
|
2455
|
+
@if (searchQuery()) {
|
|
2456
|
+
No results for "{{ searchQuery() }}"
|
|
2457
|
+
} @else {
|
|
2458
|
+
No options available
|
|
2459
|
+
}
|
|
2460
|
+
</div>
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
</div>
|
|
2464
|
+
</div>
|
|
2465
|
+
</ng-template>
|
|
2466
|
+
|
|
2467
|
+
<!-- Live announcer region -->
|
|
2468
|
+
<div class="sr-only" aria-live="polite" aria-atomic="true">
|
|
2469
|
+
{{ liveAnnouncement() }}
|
|
2470
|
+
</div>
|
|
2471
|
+
`, isInline: true, styles: [".sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "component", type: ComDropdownOption, selector: "com-dropdown-option", inputs: ["value", "displayText", "id", "index", "selected", "active", "disabled", "size", "optionTemplate", "class"], outputs: ["select", "hover"], exportAs: ["comDropdownOption"] }, { kind: "component", type: ComDropdownSearch, selector: "com-dropdown-search", inputs: ["placeholder", "ariaLabel", "disabled", "debounceMs", "size", "class"], outputs: ["searchChange", "keyNav"], exportAs: ["comDropdownSearch"] }, { kind: "component", type: ComDropdownTag, selector: "com-dropdown-tag", inputs: ["value", "displayText", "index", "disabled", "size", "variant", "class", "tagTemplate"], outputs: ["remove"], exportAs: ["comDropdownTag"] }, { kind: "component", type: ComDropdownGroup, selector: "com-dropdown-group", inputs: ["label", "count", "expanded", "showCount", "size", "class", "groupTemplate"], exportAs: ["comDropdownGroup"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2472
|
+
}
|
|
2473
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdown, decorators: [{
|
|
2474
|
+
type: Component,
|
|
2475
|
+
args: [{ selector: 'com-dropdown', exportAs: 'comDropdown', template: `
|
|
2476
|
+
<!-- Trigger button -->
|
|
2477
|
+
<button
|
|
2478
|
+
#triggerElement
|
|
2479
|
+
type="button"
|
|
2480
|
+
role="combobox"
|
|
2481
|
+
[class]="triggerClasses()"
|
|
2482
|
+
[attr.id]="triggerId()"
|
|
2483
|
+
[attr.aria-expanded]="isOpen()"
|
|
2484
|
+
[attr.aria-controls]="panelId()"
|
|
2485
|
+
[attr.aria-haspopup]="'listbox'"
|
|
2486
|
+
[attr.aria-activedescendant]="activeDescendant()"
|
|
2487
|
+
[attr.aria-required]="required() || null"
|
|
2488
|
+
[attr.aria-invalid]="effectiveState() === 'error' || null"
|
|
2489
|
+
[attr.aria-describedby]="ariaDescribedBy()"
|
|
2490
|
+
[attr.aria-disabled]="disabled() || null"
|
|
2491
|
+
[attr.tabindex]="disabled() ? -1 : 0"
|
|
2492
|
+
[disabled]="disabled()"
|
|
2493
|
+
(click)="toggle()"
|
|
2494
|
+
(keydown)="onTriggerKeydown($event)"
|
|
2495
|
+
(focus)="onTriggerFocus()"
|
|
2496
|
+
(blur)="onTriggerBlur()"
|
|
2497
|
+
>
|
|
2498
|
+
<!-- Selected value display -->
|
|
2499
|
+
<span class="flex-1 truncate text-left">
|
|
2500
|
+
@if (selectedTemplate()) {
|
|
2501
|
+
<ng-container
|
|
2502
|
+
[ngTemplateOutlet]="selectedTemplate()!.templateRef"
|
|
2503
|
+
[ngTemplateOutletContext]="selectedContext()"
|
|
2504
|
+
/>
|
|
2505
|
+
} @else if (multiple()) {
|
|
2506
|
+
@if (selectedValues().length > 0) {
|
|
2507
|
+
<span class="flex flex-wrap gap-1">
|
|
2508
|
+
@for (item of visibleTags(); track trackByValue(item, $index); let i = $index) {
|
|
2509
|
+
<com-dropdown-tag
|
|
2510
|
+
[value]="item"
|
|
2511
|
+
[displayText]="displayWith()(item)"
|
|
2512
|
+
[index]="i"
|
|
2513
|
+
[size]="size()"
|
|
2514
|
+
[disabled]="disabled()"
|
|
2515
|
+
[tagTemplate]="tagTemplate()?.templateRef ?? null"
|
|
2516
|
+
(remove)="removeValue($event)"
|
|
2517
|
+
/>
|
|
2518
|
+
}
|
|
2519
|
+
@if (hiddenTagsCount() > 0) {
|
|
2520
|
+
<span [class]="overflowBadgeClasses()">
|
|
2521
|
+
+{{ hiddenTagsCount() }}
|
|
2522
|
+
</span>
|
|
2523
|
+
}
|
|
2524
|
+
</span>
|
|
2525
|
+
} @else {
|
|
2526
|
+
<span class="text-placeholder">{{ placeholder() }}</span>
|
|
2527
|
+
}
|
|
2528
|
+
} @else {
|
|
2529
|
+
@if (selectedValue() !== null && selectedValue() !== undefined) {
|
|
2530
|
+
{{ displayWith()(selectedValue()!) }}
|
|
2531
|
+
} @else {
|
|
2532
|
+
<span class="text-placeholder">{{ placeholder() }}</span>
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
</span>
|
|
2536
|
+
|
|
2537
|
+
<!-- Clear button -->
|
|
2538
|
+
@if (clearable() && hasValue() && !disabled()) {
|
|
2539
|
+
<button
|
|
2540
|
+
type="button"
|
|
2541
|
+
[class]="clearClasses()"
|
|
2542
|
+
[attr.aria-label]="'Clear selection'"
|
|
2543
|
+
(click)="clear($event)"
|
|
2544
|
+
>
|
|
2545
|
+
<svg
|
|
2546
|
+
viewBox="0 0 24 24"
|
|
2547
|
+
fill="none"
|
|
2548
|
+
stroke="currentColor"
|
|
2549
|
+
stroke-width="2"
|
|
2550
|
+
stroke-linecap="round"
|
|
2551
|
+
stroke-linejoin="round"
|
|
2552
|
+
class="h-full w-full"
|
|
2553
|
+
>
|
|
2554
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
2555
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
2556
|
+
</svg>
|
|
2557
|
+
</button>
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
<!-- Chevron icon -->
|
|
2561
|
+
<svg
|
|
2562
|
+
[class]="chevronClasses()"
|
|
2563
|
+
viewBox="0 0 24 24"
|
|
2564
|
+
fill="none"
|
|
2565
|
+
stroke="currentColor"
|
|
2566
|
+
stroke-width="2"
|
|
2567
|
+
stroke-linecap="round"
|
|
2568
|
+
stroke-linejoin="round"
|
|
2569
|
+
>
|
|
2570
|
+
<polyline points="6 9 12 15 18 9" />
|
|
2571
|
+
</svg>
|
|
2572
|
+
</button>
|
|
2573
|
+
|
|
2574
|
+
<!-- Panel template (rendered in overlay) -->
|
|
2575
|
+
<ng-template #panelTemplate>
|
|
2576
|
+
<div
|
|
2577
|
+
#panelElement
|
|
2578
|
+
[class]="panelClasses()"
|
|
2579
|
+
[attr.id]="panelId()"
|
|
2580
|
+
[attr.role]="'listbox'"
|
|
2581
|
+
[attr.aria-multiselectable]="multiple() || null"
|
|
2582
|
+
[attr.aria-label]="placeholder()"
|
|
2583
|
+
(keydown)="onPanelKeydown($event)"
|
|
2584
|
+
>
|
|
2585
|
+
<!-- Search input -->
|
|
2586
|
+
@if (searchable()) {
|
|
2587
|
+
<com-dropdown-search
|
|
2588
|
+
[placeholder]="searchPlaceholder()"
|
|
2589
|
+
[size]="size()"
|
|
2590
|
+
[debounceMs]="searchDebounceMs()"
|
|
2591
|
+
(searchChange)="onSearchChange($event)"
|
|
2592
|
+
(keyNav)="onSearchKeyNav($event)"
|
|
2593
|
+
/>
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
<!-- Options list -->
|
|
2597
|
+
<div
|
|
2598
|
+
class="overflow-auto"
|
|
2599
|
+
[style.maxHeight]="maxHeight()"
|
|
2600
|
+
>
|
|
2601
|
+
@if (groupedOptions().length > 0) {
|
|
2602
|
+
@for (group of groupedOptions(); track group.key) {
|
|
2603
|
+
<!-- Group header -->
|
|
2604
|
+
<com-dropdown-group
|
|
2605
|
+
[label]="group.key"
|
|
2606
|
+
[count]="group.options.length"
|
|
2607
|
+
[expanded]="group.expanded"
|
|
2608
|
+
[size]="size()"
|
|
2609
|
+
[groupTemplate]="groupTemplate()?.templateRef ?? null"
|
|
2610
|
+
/>
|
|
2611
|
+
|
|
2612
|
+
<!-- Group options -->
|
|
2613
|
+
@for (option of group.options; track option.id; let i = $index) {
|
|
2614
|
+
<com-dropdown-option
|
|
2615
|
+
[value]="option.value"
|
|
2616
|
+
[displayText]="option.displayText"
|
|
2617
|
+
[id]="option.id"
|
|
2618
|
+
[index]="getGlobalIndex(group.key, i)"
|
|
2619
|
+
[selected]="isSelected(option.value)"
|
|
2620
|
+
[active]="isActive(option.id)"
|
|
2621
|
+
[disabled]="option.disabled"
|
|
2622
|
+
[size]="size()"
|
|
2623
|
+
[optionTemplate]="optionTemplate()?.templateRef ?? null"
|
|
2624
|
+
(select)="selectOption($event)"
|
|
2625
|
+
(hover)="onOptionHover(option.id)"
|
|
2626
|
+
/>
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
} @else if (filteredOptions().length > 0) {
|
|
2630
|
+
@for (option of filteredOptions(); track option.id; let i = $index) {
|
|
2631
|
+
<com-dropdown-option
|
|
2632
|
+
[value]="option.value"
|
|
2633
|
+
[displayText]="option.displayText"
|
|
2634
|
+
[id]="option.id"
|
|
2635
|
+
[index]="i"
|
|
2636
|
+
[selected]="isSelected(option.value)"
|
|
2637
|
+
[active]="isActive(option.id)"
|
|
2638
|
+
[disabled]="option.disabled"
|
|
2639
|
+
[size]="size()"
|
|
2640
|
+
[optionTemplate]="optionTemplate()?.templateRef ?? null"
|
|
2641
|
+
(select)="selectOption($event)"
|
|
2642
|
+
(hover)="onOptionHover(option.id)"
|
|
2643
|
+
/>
|
|
2644
|
+
}
|
|
2645
|
+
} @else {
|
|
2646
|
+
<!-- Empty state -->
|
|
2647
|
+
@if (emptyTemplate()) {
|
|
2648
|
+
<ng-container
|
|
2649
|
+
[ngTemplateOutlet]="emptyTemplate()!.templateRef"
|
|
2650
|
+
[ngTemplateOutletContext]="emptyContext()"
|
|
2651
|
+
/>
|
|
2652
|
+
} @else {
|
|
2653
|
+
<div class="flex items-center justify-center px-3 py-6 text-muted-foreground">
|
|
2654
|
+
@if (searchQuery()) {
|
|
2655
|
+
No results for "{{ searchQuery() }}"
|
|
2656
|
+
} @else {
|
|
2657
|
+
No options available
|
|
2658
|
+
}
|
|
2659
|
+
</div>
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
</div>
|
|
2663
|
+
</div>
|
|
2664
|
+
</ng-template>
|
|
2665
|
+
|
|
2666
|
+
<!-- Live announcer region -->
|
|
2667
|
+
<div class="sr-only" aria-live="polite" aria-atomic="true">
|
|
2668
|
+
{{ liveAnnouncement() }}
|
|
2669
|
+
</div>
|
|
2670
|
+
`, imports: [
|
|
2671
|
+
NgTemplateOutlet,
|
|
2672
|
+
OverlayModule,
|
|
2673
|
+
ComDropdownOption,
|
|
2674
|
+
ComDropdownSearch,
|
|
2675
|
+
ComDropdownTag,
|
|
2676
|
+
ComDropdownGroup,
|
|
2677
|
+
], providers: [{ provide: FormFieldControl, useExisting: forwardRef(() => ComDropdown) }], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
2678
|
+
class: 'com-dropdown-host inline-block',
|
|
2679
|
+
'[class.com-dropdown-disabled]': 'disabled()',
|
|
2680
|
+
'[class.com-dropdown-open]': 'isOpen()',
|
|
2681
|
+
}, styles: [".sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
|
|
2682
|
+
}], ctorParameters: () => [], propDecorators: { triggerRef: [{ type: i0.ViewChild, args: ['triggerElement', { isSignal: true }] }], panelTemplateRef: [{ type: i0.ViewChild, args: ['panelTemplate', { isSignal: true }] }], optionTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownOptionTpl), { isSignal: true }] }], selectedTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownSelectedTpl), { isSignal: true }] }], emptyTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownEmptyTpl), { isSignal: true }] }], groupTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownGroupTpl), { isSignal: true }] }], tagTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownTagTpl), { isSignal: true }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], displayWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayWith", required: false }] }], filterWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterWith", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], panelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelWidth", required: false }] }], searchDebounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchDebounceMs", required: false }] }], virtualScrollThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScrollThreshold", required: false }] }], maxVisibleTags: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxVisibleTags", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|
|
2683
|
+
|
|
2684
|
+
// Public API for the dropdown component
|
|
2685
|
+
// Main component
|
|
2686
|
+
|
|
2687
|
+
/**
|
|
2688
|
+
* Generated bundle index. Do not edit.
|
|
2689
|
+
*/
|
|
2690
|
+
|
|
2691
|
+
export { ComDropdown, ComDropdownEmptyTpl, ComDropdownGroup, ComDropdownGroupTpl, ComDropdownOption, ComDropdownOptionTpl, ComDropdownPanel, ComDropdownSearch, ComDropdownSelectedTpl, ComDropdownTag, ComDropdownTagTpl, defaultCompareWith, defaultDisplayWith, defaultFilterWith, dropdownChevronVariants, dropdownClearVariants, dropdownEmptyVariants, dropdownGroupVariants, dropdownOptionVariants, dropdownOverflowBadgeVariants, dropdownPanelVariants, dropdownSearchVariants, dropdownTagRemoveVariants, dropdownTagVariants, dropdownTriggerVariants, generateDropdownId };
|
|
2692
|
+
//# sourceMappingURL=ngx-com-components-dropdown.mjs.map
|