ngx-virtual-select-field-filterable 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,877 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Input, Directive, InjectionToken, EventEmitter, signal, booleanAttribute, Output, Inject, ChangeDetectionStrategy, Component, output, inject, ChangeDetectorRef, DestroyRef, ElementRef, computed, numberAttribute, ContentChildren, ContentChild, ViewChild, Optional } from '@angular/core';
3
+ import { BehaviorSubject, Subject, tap, startWith, map, merge, switchMap, debounceTime, take } from 'rxjs';
4
+ import * as i1$1 from '@angular/common';
5
+ import { CommonModule } from '@angular/common';
6
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
+ import { NgControl } from '@angular/forms';
8
+ import { SelectionModel } from '@angular/cdk/collections';
9
+ import { ListKeyManager } from '@angular/cdk/a11y';
10
+ import * as i2 from '@angular/cdk/overlay';
11
+ import { ViewportRuler, CdkOverlayOrigin, OverlayModule, CdkConnectedOverlay } from '@angular/cdk/overlay';
12
+ import * as i3 from '@angular/cdk/scrolling';
13
+ import { ScrollingModule, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
14
+ import * as i4 from '@angular/material/form-field';
15
+ import { MAT_FORM_FIELD, MatFormFieldModule, MatFormFieldControl } from '@angular/material/form-field';
16
+ import * as i5 from '@angular/material/input';
17
+ import { MatInputModule } from '@angular/material/input';
18
+ import { hasModifierKey } from '@angular/cdk/keycodes';
19
+ import * as i1 from '@angular/material/core';
20
+ import { MatPseudoCheckboxModule, MatRippleModule } from '@angular/material/core';
21
+
22
+ class NgxVirtualSelectFieldOptionForDirective {
23
+ /**
24
+ * The options collection to render.
25
+ * @required
26
+ */
27
+ set options(options) {
28
+ this.options$.next(options);
29
+ }
30
+ constructor(template) {
31
+ this.template = template;
32
+ this.options$ = new BehaviorSubject([]);
33
+ }
34
+ static ngTemplateContextGuard(_dir, ctx) {
35
+ return true;
36
+ }
37
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: NgxVirtualSelectFieldOptionForDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); }
38
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.0", type: NgxVirtualSelectFieldOptionForDirective, isStandalone: true, selector: "[ngxVirtualSelectFieldOptionFor]", inputs: { options: ["ngxVirtualSelectFieldOptionForOf", "options"] }, ngImport: i0 }); }
39
+ }
40
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: NgxVirtualSelectFieldOptionForDirective, decorators: [{
41
+ type: Directive,
42
+ args: [{
43
+ selector: '[ngxVirtualSelectFieldOptionFor]',
44
+ standalone: true,
45
+ }]
46
+ }], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { options: [{
47
+ type: Input,
48
+ args: [{ required: true, alias: 'ngxVirtualSelectFieldOptionForOf' }]
49
+ }] } });
50
+
51
+ const NGX_VIRTUAL_SELECT_FIELD_TRIGGER = new InjectionToken('NGX_VIRTUAL_SELECT_FIELD_TRIGGER');
52
+ class NgxVirtualSelectFieldTriggerDirective {
53
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: NgxVirtualSelectFieldTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
54
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.0", type: NgxVirtualSelectFieldTriggerDirective, isStandalone: true, selector: "ngx-virtual-select-field-trigger", providers: [
55
+ {
56
+ provide: NGX_VIRTUAL_SELECT_FIELD_TRIGGER,
57
+ useExisting: NgxVirtualSelectFieldTriggerDirective,
58
+ },
59
+ ], ngImport: i0 }); }
60
+ }
61
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: NgxVirtualSelectFieldTriggerDirective, decorators: [{
62
+ type: Directive,
63
+ args: [{
64
+ selector: 'ngx-virtual-select-field-trigger',
65
+ providers: [
66
+ {
67
+ provide: NGX_VIRTUAL_SELECT_FIELD_TRIGGER,
68
+ useExisting: NgxVirtualSelectFieldTriggerDirective,
69
+ },
70
+ ],
71
+ standalone: true,
72
+ }]
73
+ }] });
74
+
75
+ const NGX_VIRTUAL_SELECT_FIELD_OPTION_PARENT = new InjectionToken('NGX_VIRTUAL_SELECT_FIELD_OPTION_PARENT');
76
+
77
+ class NgxVirtualSelectFieldOptionComponent {
78
+ constructor(_optionParent, _elementRef) {
79
+ this._optionParent = _optionParent;
80
+ this._elementRef = _elementRef;
81
+ /**
82
+ * Whether the option is disabled.
83
+ */
84
+ this.disabled = false;
85
+ this.selectedChange = new EventEmitter();
86
+ this.multiple = this._optionParent?.multiple ?? false;
87
+ this.active = signal(false);
88
+ this.selected = signal(false);
89
+ this.hostNativeElement = this._elementRef.nativeElement;
90
+ }
91
+ // #region Highlightable
92
+ setActiveStyles() {
93
+ if (!this.active()) {
94
+ this.active.set(true);
95
+ }
96
+ }
97
+ setInactiveStyles() {
98
+ if (this.active()) {
99
+ this.active.set(false);
100
+ }
101
+ }
102
+ // #endregion Highlightable
103
+ deselect() {
104
+ this.selected.set(false);
105
+ }
106
+ select() {
107
+ this.selected.set(true);
108
+ }
109
+ onClick() {
110
+ if (this.disabled) {
111
+ return;
112
+ }
113
+ this.selected.set(this.multiple ? !this.selected() : true);
114
+ this.selectedChange.emit({
115
+ source: this,
116
+ value: this.value,
117
+ selected: this.selected(),
118
+ });
119
+ }
120
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: NgxVirtualSelectFieldOptionComponent, deps: [{ token: NGX_VIRTUAL_SELECT_FIELD_OPTION_PARENT }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); }
121
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: NgxVirtualSelectFieldOptionComponent, isStandalone: true, selector: "ngx-virtual-select-field-option", inputs: { value: "value", disabled: ["disabled", "disabled", booleanAttribute] }, outputs: { selectedChange: "selectedChange" }, host: { attributes: { "role": "option" }, listeners: { "click": "onClick()" }, properties: { "class.ngx-virtual-select-field-option--active": "active()", "class.ngx-virtual-select-field-option--selected": "selected()", "class.ngx-virtual-select-field-option--multiple": "multiple", "class.ngx-virtual-select-field-option--disabled": "disabled" }, classAttribute: "ngx-virtual-select-field-option" }, ngImport: i0, template: "@if(multiple){\n<mat-pseudo-checkbox\n [state]=\"selected() ? 'checked' : 'unchecked'\"\n></mat-pseudo-checkbox>\n}\n\n<span class=\"ngx-virtual-select-field-option__label\"\n ><ng-content></ng-content\n></span>\n\n@if (!multiple && selected() ) {\n<mat-pseudo-checkbox state=\"checked\" appearance=\"minimal\"></mat-pseudo-checkbox>\n}\n\n<div\n class=\"ngx-virtual-select-field-option__ripple\"\n matRipple\n [matRippleTrigger]=\"hostNativeElement\"\n [matRippleDisabled]=\"disabled\"\n></div>\n", styles: [":host{cursor:pointer;position:relative;display:inline-flex;flex-direction:row;gap:var(--ngx-virtual-select-field-option-gap);padding:var(--ngx-virtual-select-field-option-padding);line-height:var(--ngx-virtual-select-field-option-line-height);height:var(--ngx-virtual-select-field__viewport-option-height);color:var(--ngx-virtual-select-field-option-color);font-family:var(--ngx-virtual-select-field-option-font-family);font-size:var(--ngx-virtual-select-field-option-font-size);font-weight:var(--ngx-virtual-select-field-option-font-weight);letter-spacing:var(--ngx-virtual-select-field-option-letter-spacing)}:host.ngx-virtual-select-field-option{display:flex;align-items:center;box-sizing:border-box;width:100%}:host.ngx-virtual-select-field-option--active{background-color:var(--ngx-virtual-select-field-option-background-color--active)}:host.ngx-virtual-select-field-option--selected{color:var(--ngx-virtual-select-field-option-selected-state-label-text-color)}:host.ngx-virtual-select-field-option--selected:not(.ngx-virtual-select-field-option--multiple){background-color:var(--ngx-virtual-select-field-option-background-color--selected)}:host.ngx-virtual-select-field-option--disabled{pointer-events:none;opacity:var(--ngx-virtual-select-field-option-disabled-state-opacity)}:host.ngx-virtual-select-field-option:hover{background-color:var(--ngx-virtual-select-field-option-background-color--hover)}:host.ngx-virtual-select-field-option :host.ngx-virtual-select-field-option__label{flex:1;display:inline-flex;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host.ngx-virtual-select-field-option :host.ngx-virtual-select-field-option__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatPseudoCheckboxModule }, { kind: "component", type: i1.MatPseudoCheckbox, selector: "mat-pseudo-checkbox", inputs: ["state", "disabled", "appearance"] }, { kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i1.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
122
+ }
123
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: NgxVirtualSelectFieldOptionComponent, decorators: [{
124
+ type: Component,
125
+ args: [{ selector: 'ngx-virtual-select-field-option', standalone: true, imports: [CommonModule, MatPseudoCheckboxModule, MatRippleModule], changeDetection: ChangeDetectionStrategy.OnPush, host: {
126
+ role: 'option',
127
+ '(click)': 'onClick()',
128
+ '[class.ngx-virtual-select-field-option--active]': 'active()',
129
+ '[class.ngx-virtual-select-field-option--selected]': 'selected()',
130
+ '[class.ngx-virtual-select-field-option--multiple]': 'multiple',
131
+ '[class.ngx-virtual-select-field-option--disabled]': 'disabled',
132
+ class: 'ngx-virtual-select-field-option',
133
+ }, template: "@if(multiple){\n<mat-pseudo-checkbox\n [state]=\"selected() ? 'checked' : 'unchecked'\"\n></mat-pseudo-checkbox>\n}\n\n<span class=\"ngx-virtual-select-field-option__label\"\n ><ng-content></ng-content\n></span>\n\n@if (!multiple && selected() ) {\n<mat-pseudo-checkbox state=\"checked\" appearance=\"minimal\"></mat-pseudo-checkbox>\n}\n\n<div\n class=\"ngx-virtual-select-field-option__ripple\"\n matRipple\n [matRippleTrigger]=\"hostNativeElement\"\n [matRippleDisabled]=\"disabled\"\n></div>\n", styles: [":host{cursor:pointer;position:relative;display:inline-flex;flex-direction:row;gap:var(--ngx-virtual-select-field-option-gap);padding:var(--ngx-virtual-select-field-option-padding);line-height:var(--ngx-virtual-select-field-option-line-height);height:var(--ngx-virtual-select-field__viewport-option-height);color:var(--ngx-virtual-select-field-option-color);font-family:var(--ngx-virtual-select-field-option-font-family);font-size:var(--ngx-virtual-select-field-option-font-size);font-weight:var(--ngx-virtual-select-field-option-font-weight);letter-spacing:var(--ngx-virtual-select-field-option-letter-spacing)}:host.ngx-virtual-select-field-option{display:flex;align-items:center;box-sizing:border-box;width:100%}:host.ngx-virtual-select-field-option--active{background-color:var(--ngx-virtual-select-field-option-background-color--active)}:host.ngx-virtual-select-field-option--selected{color:var(--ngx-virtual-select-field-option-selected-state-label-text-color)}:host.ngx-virtual-select-field-option--selected:not(.ngx-virtual-select-field-option--multiple){background-color:var(--ngx-virtual-select-field-option-background-color--selected)}:host.ngx-virtual-select-field-option--disabled{pointer-events:none;opacity:var(--ngx-virtual-select-field-option-disabled-state-opacity)}:host.ngx-virtual-select-field-option:hover{background-color:var(--ngx-virtual-select-field-option-background-color--hover)}:host.ngx-virtual-select-field-option :host.ngx-virtual-select-field-option__label{flex:1;display:inline-flex;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host.ngx-virtual-select-field-option :host.ngx-virtual-select-field-option__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}\n"] }]
134
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
135
+ type: Inject,
136
+ args: [NGX_VIRTUAL_SELECT_FIELD_OPTION_PARENT]
137
+ }] }, { type: i0.ElementRef }], propDecorators: { value: [{
138
+ type: Input,
139
+ args: [{ required: true }]
140
+ }], disabled: [{
141
+ type: Input,
142
+ args: [{ transform: booleanAttribute }]
143
+ }], selectedChange: [{
144
+ type: Output
145
+ }] } });
146
+
147
+ const POSITIONS = [
148
+ {
149
+ originX: 'start',
150
+ originY: 'bottom',
151
+ overlayX: 'start',
152
+ overlayY: 'top',
153
+ },
154
+ {
155
+ originX: 'end',
156
+ originY: 'bottom',
157
+ overlayX: 'end',
158
+ overlayY: 'top',
159
+ },
160
+ {
161
+ originX: 'start',
162
+ originY: 'top',
163
+ overlayX: 'start',
164
+ overlayY: 'bottom',
165
+ panelClass: 'ngx-virtual-select-field-overlay--above',
166
+ },
167
+ {
168
+ originX: 'end',
169
+ originY: 'top',
170
+ overlayX: 'end',
171
+ overlayY: 'bottom',
172
+ panelClass: 'ngx-virtual-select-field-overlay--above',
173
+ },
174
+ ];
175
+ const NGX_VIRTUAL_SELECT_FIELD_CONFIG = new InjectionToken('NGX_VIRTUAL_SELECT_FIELD_CONFIG');
176
+ const PANEL_WIDTH_AUTO = 'auto';
177
+ const PANEL_VIEWPORT_PAGE_SIZE = 8;
178
+ const OPTION_HEIGHT = 48;
179
+
180
+ const ARROW_DOWN_KEY = 'ArrowDown';
181
+ const ARROW_UP_KEY = 'ArrowUp';
182
+ const ARROW_RIGHT_KEY = 'ArrowRight';
183
+ const ARROW_LEFT_KEY = 'ArrowLeft';
184
+ const ENTER_CODE = 'Enter';
185
+ const SPACE_CODE = 'Space';
186
+ const KEY_A_CODE = 'KeyA';
187
+
188
+ //#region imports
189
+ //#endregion imports
190
+ class NgxVirtualSelectFieldComponent {
191
+ /**
192
+ * Value of the select field
193
+ * @default null
194
+ */
195
+ set value(value) {
196
+ if (this._value === value) {
197
+ return;
198
+ }
199
+ value = value || [];
200
+ if (!Array.isArray(value)) {
201
+ value = [value];
202
+ }
203
+ this._value = value;
204
+ this._selectionModel?.setSelection(...this._value.map((v) => this.optionFor.options$.value.find((o) => o.value === v)));
205
+ this._stateChanges.next();
206
+ }
207
+ /**
208
+ * Placeholder for the select field
209
+ * @default none
210
+ */
211
+ set placeholder(placeholder) {
212
+ this._placeholder = placeholder;
213
+ this._stateChanges.next();
214
+ }
215
+ get placeholder() {
216
+ return this._placeholder;
217
+ }
218
+ /**
219
+ * Define if fields is required
220
+ * @default false
221
+ */
222
+ set required(req) {
223
+ this._required = req;
224
+ this._stateChanges.next();
225
+ }
226
+ get required() {
227
+ return this._required;
228
+ }
229
+ /**
230
+ * Define if field is disabled
231
+ * @default false
232
+ */
233
+ set disabled(value) {
234
+ this._disabled = value;
235
+ this._stateChanges.next();
236
+ }
237
+ get disabled() {
238
+ return this._disabled;
239
+ }
240
+ constructor(_parentFormField, _defaultOptions) {
241
+ this._parentFormField = _parentFormField;
242
+ this._defaultOptions = _defaultOptions;
243
+ //#region Inputs/Outputs
244
+ this.userAriaDescribedBy = '';
245
+ /**
246
+ * Width for overlay panel
247
+ * @default 'auto'
248
+ */
249
+ this.panelWidth = this._defaultOptions?.panelWidth ?? PANEL_WIDTH_AUTO;
250
+ /**
251
+ * Height for an option element
252
+ * @default 48
253
+ */
254
+ this.optionHeight = this._defaultOptions?.optionHeight ?? OPTION_HEIGHT;
255
+ /**
256
+ * Amount of visible items in list
257
+ * @default 8
258
+ */
259
+ this.panelViewportPageSize = this._defaultOptions?.panelViewportPageSize ?? PANEL_VIEWPORT_PAGE_SIZE;
260
+ /**
261
+ * Enable multiple selection
262
+ * @default false
263
+ */
264
+ this.multiple = false;
265
+ /**
266
+ * Tab index for keyboard navigation
267
+ * @default 0
268
+ */
269
+ this.tabIndex = 0;
270
+ /**
271
+ * Milliseconds to wait before navigating to active element after keyboard search
272
+ * @default 300
273
+ */
274
+ this.typeaheadDebounceInterval = 300;
275
+ /**
276
+ * CSS class to be added to the panel element
277
+ * @default none
278
+ */
279
+ this.panelClass = null;
280
+ /**
281
+ * Enable filtering of options
282
+ * @default false
283
+ */
284
+ this.filterable = false;
285
+ /**
286
+ * Placeholder text for the filter input
287
+ * @default 'Search...'
288
+ */
289
+ this.filterPlaceholder = 'Search...';
290
+ this._value = [];
291
+ this._placeholder = '';
292
+ this._required = false;
293
+ this._disabled = false;
294
+ /**
295
+ * Value change event
296
+ */
297
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
298
+ this.valueChange = output();
299
+ /**
300
+ * Selection change event
301
+ * Emits after value change and form control update
302
+ */
303
+ this.selectionChange = output();
304
+ this.customTrigger = null;
305
+ this.optionsQuery = null;
306
+ this.id = `ngx-virtual-select-field-${NgxVirtualSelectFieldComponent.nextId++}`;
307
+ this.controlType = 'ngx-virtual-select-field';
308
+ this.ngControl = inject(NgControl, {
309
+ optional: true,
310
+ });
311
+ this.autofilled = false;
312
+ this.POSITIONS = POSITIONS;
313
+ this.overlayPanelClass = this._defaultOptions?.overlayPanelClass || '';
314
+ this.isPanelOpened = signal(false);
315
+ this.filterText = signal('');
316
+ this.triggerValue$ = null;
317
+ this.filteredOptions$ = null;
318
+ this._changeDetectorRef = inject(ChangeDetectorRef);
319
+ this._destroyRef = inject(DestroyRef);
320
+ this._elRef = inject(ElementRef);
321
+ this._stateChanges = new Subject();
322
+ this._scrolledIndexChange = new Subject();
323
+ this._onChange = () => void 0;
324
+ this._onTouched = () => void 0;
325
+ this._keyManager = null;
326
+ this._filterTextSubject = null;
327
+ this._focused = false;
328
+ this.optionTrackBy = (_index, option) => {
329
+ return option.value;
330
+ };
331
+ if (this.ngControl != null) {
332
+ this.ngControl.valueAccessor = this;
333
+ this._disabled = this.ngControl.disabled ?? false;
334
+ }
335
+ this.overlayWidth = this.createOverlayWidthSignal();
336
+ this.inheritedColorTheme = this._parentFormField
337
+ ? `mat-${this._parentFormField.color}`
338
+ : '';
339
+ }
340
+ createOverlayWidthSignal() {
341
+ const changeDetectorRef = inject(ChangeDetectorRef);
342
+ // NOTE: View port ruler change stream runs outside the zone.
343
+ // Need to run change detection manually to trigger computed signal below.
344
+ const viewPortRulerChange = toSignal(inject(ViewportRuler)
345
+ .change()
346
+ .pipe(takeUntilDestroyed(this._destroyRef), tap(() => changeDetectorRef.detectChanges())));
347
+ return computed(() => {
348
+ viewPortRulerChange();
349
+ return this.resolveOverlayWidth(this.preferredOverlayOrigin);
350
+ });
351
+ }
352
+ resolveOverlayWidth(preferredOrigin) {
353
+ if (!this.isPanelOpened()) {
354
+ return 0;
355
+ }
356
+ if (this.panelWidth !== PANEL_WIDTH_AUTO) {
357
+ return this.panelWidth ?? '';
358
+ }
359
+ const refToMeasure = preferredOrigin instanceof CdkOverlayOrigin
360
+ ? preferredOrigin.elementRef
361
+ : preferredOrigin || this._elRef;
362
+ return refToMeasure.nativeElement.getBoundingClientRect().width;
363
+ }
364
+ get shouldLabelFloat() {
365
+ return this.focused || !this.empty;
366
+ }
367
+ get empty() {
368
+ return !this._selectionModel || this._selectionModel.isEmpty();
369
+ }
370
+ get stateChanges() {
371
+ return this._stateChanges.asObservable();
372
+ }
373
+ get errorState() {
374
+ return !!this.ngControl?.invalid && !!this.ngControl?.touched;
375
+ }
376
+ get focused() {
377
+ // NOTE: panel open is needed to keep form field in focused state during interaction with options
378
+ return this._focused || this.isPanelOpened();
379
+ }
380
+ get maxPageSize() {
381
+ return Math.min(this.panelViewportPageSize, this.optionFor.options$.value.length);
382
+ }
383
+ ngOnInit() {
384
+ this._selectionModel = new SelectionModel(this.multiple, [], true);
385
+ }
386
+ ngAfterContentInit() {
387
+ this.assertIsDefined(this.optionsQuery, `optionsQuery is not defined`);
388
+ if (!this.customTrigger) {
389
+ this.triggerValue$ = this._selectionModel.changed.pipe(startWith(null), map((_selected) => this._selectionModel.selected
390
+ .map((option) => option?.label ?? '')
391
+ .join(', ')));
392
+ }
393
+ // Create filtered options observable that reacts to filter text changes
394
+ const filterText$ = new Subject();
395
+ this.filteredOptions$ = merge(this.optionFor.options$, filterText$.pipe(switchMap(() => this.optionFor.options$))).pipe(map((options) => {
396
+ const searchText = this.filterText().toLowerCase().trim();
397
+ if (!searchText || !this.filterable) {
398
+ return options;
399
+ }
400
+ return options.filter((option) => {
401
+ const label = option.getLabel?.() ?? option.label;
402
+ return label.toLowerCase().includes(searchText);
403
+ });
404
+ }));
405
+ // Trigger filter updates when filter text changes
406
+ this._filterTextSubject = filterText$;
407
+ this.optionFor.options$
408
+ .pipe(takeUntilDestroyed(this._destroyRef))
409
+ .subscribe((options) => {
410
+ this._selectionModel?.setSelection(...this._value.map((v) => options.find((o) => o.value === v)));
411
+ this.initListKeyManager(options);
412
+ });
413
+ this.optionsQuery.changes
414
+ .pipe(switchMap(() => merge(...this.optionsQuery.map((option) => option.selectedChange))), takeUntilDestroyed(this._destroyRef))
415
+ .subscribe((selectionEvent) => this.updateOptionSelection(selectionEvent, this.optionFor.options$.value));
416
+ merge(this._scrolledIndexChange, this._selectionModel.changed)
417
+ .pipe(takeUntilDestroyed(this._destroyRef), debounceTime(20))
418
+ .subscribe(() => this.updateRenderedOptionsState(this.optionFor.options$.value));
419
+ }
420
+ updateOptionSelection(selectionEvent, options) {
421
+ this.assertIsDefined(this.optionsQuery, `optionsQuery is not defined`);
422
+ const { option: changedOption, index: selectedIndex } = this.findOptionByValue(options, selectionEvent.value);
423
+ if (this.multiple) {
424
+ this._selectionModel.toggle(changedOption);
425
+ }
426
+ else if (changedOption.value === null) {
427
+ this._selectionModel.clear();
428
+ this.close();
429
+ }
430
+ else {
431
+ this._selectionModel.select(changedOption);
432
+ this.close();
433
+ }
434
+ if (this._selectionModel.isSelected(changedOption)) {
435
+ this._keyManager?.setActiveItem(selectedIndex);
436
+ }
437
+ // NOTE: this need to keep form field in focus state
438
+ this.focus();
439
+ this.emitValue();
440
+ }
441
+ ngOnDestroy() {
442
+ this._scrolledIndexChange.complete();
443
+ this._keyManager?.destroy();
444
+ this._stateChanges.complete();
445
+ }
446
+ // #region ControlValueAccessor
447
+ writeValue(value) {
448
+ this.value = value;
449
+ // after settting a value on empty fornControl local `empty` does not update
450
+ // as result the field continue to show placeholder.
451
+ // needed to trigger change detection for the empty state and trigger value updates
452
+ this._changeDetectorRef.markForCheck();
453
+ }
454
+ registerOnChange(fn) {
455
+ this._onChange = fn;
456
+ }
457
+ registerOnTouched(fn) {
458
+ this._onTouched = fn;
459
+ }
460
+ setDisabledState(isDisabled) {
461
+ this.disabled = isDisabled;
462
+ }
463
+ // #endregion ControlValueAccessor
464
+ setDescribedByIds(ids) {
465
+ const controlElement = this._elRef.nativeElement;
466
+ controlElement.setAttribute('aria-describedby', ids.join(' '));
467
+ }
468
+ onContainerClick() {
469
+ if (this.disabled) {
470
+ return;
471
+ }
472
+ this.focus();
473
+ this.open();
474
+ }
475
+ onOverlayAttached() {
476
+ this.cdkConnectedOverlay.positionChange
477
+ .pipe(take(1), switchMap(() => this._scrolledIndexChange.pipe(take(1))), takeUntilDestroyed(this._destroyRef))
478
+ .subscribe(() => this.navigateToFirstSelectedOption());
479
+ }
480
+ navigateToFirstSelectedOption() {
481
+ if (this._selectionModel.isEmpty()) {
482
+ return;
483
+ }
484
+ let targetIndex = this.optionFor.options$.value.findIndex((option) => option === this._selectionModel.selected[0]);
485
+ targetIndex = targetIndex - this.maxPageSize / 2;
486
+ targetIndex = Math.max(0, targetIndex);
487
+ this.cdkVirtualScrollViewport.scrollToIndex(targetIndex);
488
+ }
489
+ onFocusIn() {
490
+ if (!this.focused) {
491
+ this._focused = true;
492
+ this._stateChanges.next();
493
+ }
494
+ }
495
+ onFocusOut() {
496
+ this._focused = false;
497
+ if (!this.isPanelOpened()) {
498
+ this._onTouched();
499
+ this._stateChanges.next();
500
+ }
501
+ }
502
+ onScrolledIndexChange() {
503
+ this._scrolledIndexChange.next();
504
+ }
505
+ onFilterInput(event) {
506
+ const input = event.target;
507
+ this.filterText.set(input.value);
508
+ this._filterTextSubject?.next(input.value);
509
+ }
510
+ onFilterKeyDown(event) {
511
+ const isArrowKey = event.key === ARROW_DOWN_KEY ||
512
+ event.key === ARROW_UP_KEY ||
513
+ event.key === ARROW_LEFT_KEY ||
514
+ event.key === ARROW_RIGHT_KEY;
515
+ // Prevent arrow keys from propagating when there's text in the input
516
+ if (isArrowKey && this.filterText()) {
517
+ if (event.key === ARROW_DOWN_KEY || event.key === ARROW_UP_KEY) {
518
+ // Arrow down/up should move to the options list
519
+ event.preventDefault();
520
+ this.cdkVirtualScrollViewport.elementRef.nativeElement.focus();
521
+ this._keyManager?.onKeydown(event);
522
+ }
523
+ // Left/Right arrows should work normally in the input for cursor movement
524
+ return;
525
+ }
526
+ // Allow other keys like Escape, Enter to work
527
+ if (event.key === 'Escape') {
528
+ event.preventDefault();
529
+ this.close();
530
+ }
531
+ else if (event.key === 'Tab') {
532
+ // Tab should close the panel
533
+ this.close();
534
+ }
535
+ }
536
+ open() {
537
+ if (this.isPanelOpened()) {
538
+ return;
539
+ }
540
+ if (this._parentFormField) {
541
+ this.preferredOverlayOrigin =
542
+ this._parentFormField.getConnectedOverlayOrigin();
543
+ }
544
+ this.isPanelOpened.set(true);
545
+ // Focus the filter input when panel opens if filterable is enabled
546
+ if (this.filterable) {
547
+ setTimeout(() => {
548
+ this.filterInput?.nativeElement.focus();
549
+ }, 0);
550
+ }
551
+ }
552
+ close() {
553
+ this.isPanelOpened.set(false);
554
+ this.filterText.set(''); // Clear filter when closing
555
+ this._onTouched();
556
+ this._stateChanges.next();
557
+ }
558
+ //#region Keyboard navigation
559
+ onKeyDown(event) {
560
+ if (this.disabled) {
561
+ return;
562
+ }
563
+ if (this.isPanelOpened()) {
564
+ this.doPanelOpenedKeydown(event);
565
+ }
566
+ else {
567
+ this.doPanelClosedKeydown(event);
568
+ }
569
+ }
570
+ doPanelOpenedKeydown(event) {
571
+ this.assertIsDefined(this.optionsQuery, `optionsQuery is not defined`);
572
+ const keyManager = this._keyManager;
573
+ const activeItem = keyManager.activeItem;
574
+ const isTyping = keyManager.isTyping();
575
+ const options = this.optionFor.options$.value;
576
+ const isArrowKey = event.key === ARROW_DOWN_KEY || event.key === ARROW_UP_KEY;
577
+ if (isArrowKey && event.altKey) {
578
+ event.preventDefault();
579
+ this.close();
580
+ }
581
+ else if (!isTyping &&
582
+ (event.code === ENTER_CODE || event.code === SPACE_CODE) &&
583
+ activeItem &&
584
+ !hasModifierKey(event)) {
585
+ event.preventDefault();
586
+ const { option } = this.findOptionByValue(options, activeItem.value);
587
+ this._selectionModel.toggle(option);
588
+ this.emitValue();
589
+ }
590
+ else if (!isTyping &&
591
+ this.multiple &&
592
+ event.code === KEY_A_CODE &&
593
+ event.ctrlKey) {
594
+ event.preventDefault();
595
+ this.toggleAllOptions(options);
596
+ this.emitValue();
597
+ }
598
+ else {
599
+ const previouslyFocusedIndex = keyManager.activeItemIndex;
600
+ keyManager.onKeydown(event);
601
+ if (this.multiple &&
602
+ isArrowKey &&
603
+ event.shiftKey &&
604
+ keyManager.activeItem &&
605
+ keyManager.activeItemIndex !== previouslyFocusedIndex) {
606
+ this.selectOptionByValue(options, keyManager.activeItem.value);
607
+ }
608
+ }
609
+ }
610
+ toggleAllOptions(options) {
611
+ const enabledOptionValues = options.filter((option) => !option.disabled);
612
+ const hasDeselectedOptions = enabledOptionValues.length > this._selectionModel.selected.length;
613
+ if (hasDeselectedOptions) {
614
+ this._selectionModel.select(...enabledOptionValues);
615
+ }
616
+ else {
617
+ this._selectionModel.clear();
618
+ }
619
+ }
620
+ doPanelClosedKeydown(event) {
621
+ const keyManager = this._keyManager;
622
+ const isTyping = keyManager.isTyping();
623
+ const isArrowKey = event.key === ARROW_DOWN_KEY ||
624
+ event.key === ARROW_UP_KEY ||
625
+ event.key === ARROW_RIGHT_KEY ||
626
+ event.key === ARROW_LEFT_KEY;
627
+ if ((!isTyping &&
628
+ (event.code === SPACE_CODE || event.code === ENTER_CODE) &&
629
+ !hasModifierKey(event)) ||
630
+ ((this.multiple || event.altKey) && isArrowKey)) {
631
+ event.preventDefault(); // prevents the page from scrolling down when pressing space
632
+ this.open();
633
+ }
634
+ else if (!this.multiple) {
635
+ const previouslySelectedOptionIndex = keyManager.activeItemIndex;
636
+ keyManager.onKeydown(event);
637
+ const selectedOptionIndex = keyManager.activeItemIndex;
638
+ if (selectedOptionIndex &&
639
+ previouslySelectedOptionIndex !== selectedOptionIndex) {
640
+ //TODO: arrow navigation should start from selected options. Currently it starts from the first option
641
+ this.selectOptionByValue(this.optionFor.options$.value, keyManager.activeItem.value);
642
+ // TODO: Add live announcer
643
+ // We set a duration on the live announcement, because we want the live element to be
644
+ // cleared after a while so that users can't navigate to it using the arrow keys.
645
+ // this._liveAnnouncer.announce((selectedOption as MatOption).viewValue, 10000);
646
+ }
647
+ }
648
+ }
649
+ //#endregion Keyboard navigation
650
+ //#region Key manager
651
+ initListKeyManager(options) {
652
+ this._keyManager?.destroy();
653
+ this._keyManager = new ListKeyManager(this.normalizeKeyManagerOptions(options))
654
+ .withTypeAhead(this.typeaheadDebounceInterval)
655
+ .withVerticalOrientation()
656
+ .withHomeAndEnd()
657
+ .withPageUpDown()
658
+ .withAllowedModifierKeys(['shiftKey']);
659
+ this._keyManager.tabOut.subscribe(() => {
660
+ if (!this.isPanelOpened()) {
661
+ return;
662
+ }
663
+ if (this._keyManager?.activeItem) {
664
+ this.selectOptionByValue(options, this._keyManager.activeItem.value);
665
+ }
666
+ this.focus();
667
+ this.close();
668
+ });
669
+ this._keyManager.change.subscribe((index) => {
670
+ this.assertIsDefined(this.optionsQuery, `optionsQuery is not defined`);
671
+ this.updateActiveOptionComponent(this.optionsQuery.toArray(), options[index], index);
672
+ });
673
+ }
674
+ normalizeKeyManagerOptions(options) {
675
+ return options.map((option) => ({
676
+ value: option.value,
677
+ label: option.label,
678
+ disabled: option.disabled ?? false,
679
+ getLabel: () => option.getLabel?.() ?? option.label,
680
+ }));
681
+ }
682
+ updateActiveOptionComponent(optionComponents, activeOption, index) {
683
+ optionComponents.forEach((option) => option.setInactiveStyles());
684
+ const shouldScrollToActiveItem = this.shouldScrollToActiveItem(index);
685
+ if (shouldScrollToActiveItem) {
686
+ this.cdkVirtualScrollViewport.scrolledIndexChange
687
+ .pipe(take(1))
688
+ .subscribe(() => {
689
+ this.assertIsDefined(this.optionsQuery, `optionsQuery is not defined`);
690
+ this.setActiveOptionComponentByValue(this.optionsQuery.toArray(), activeOption.value);
691
+ });
692
+ this.cdkVirtualScrollViewport.scrollToIndex(index);
693
+ }
694
+ else {
695
+ this.setActiveOptionComponentByValue(optionComponents, activeOption.value);
696
+ }
697
+ }
698
+ shouldScrollToActiveItem(targetIndex) {
699
+ if (!this.isPanelOpened()) {
700
+ return false;
701
+ }
702
+ const scrollTop = this.cdkVirtualScrollViewport.elementRef.nativeElement.scrollTop;
703
+ // NOTE: -1 is needed to prevent scrolling to next item out of the viewport
704
+ const bottomScroll = scrollTop + this.optionHeight * this.maxPageSize - 1;
705
+ const targetScroll = this.optionHeight * targetIndex;
706
+ return scrollTop > targetScroll || bottomScroll < targetScroll;
707
+ }
708
+ setActiveOptionComponentByValue(optionComponents, value) {
709
+ const optionComponent = optionComponents.find((option) => option.value === value);
710
+ this.assertIsDefined(optionComponent, `Option component with value ${value} not found`);
711
+ optionComponent.setActiveStyles();
712
+ }
713
+ // #endregion Key manager
714
+ focus() {
715
+ this._elRef.nativeElement.focus();
716
+ }
717
+ selectOptionByValue(options, value) {
718
+ const { option } = this.findOptionByValue(options, value);
719
+ this._selectionModel.select(option);
720
+ this.emitValue();
721
+ }
722
+ updateRenderedOptionsState(options) {
723
+ this.assertIsDefined(this.optionsQuery, `optionsQuery is not defined`);
724
+ this.optionsQuery.forEach((optionComponent) => {
725
+ const { option } = this.findOptionByValue(options, optionComponent.value);
726
+ if (this._selectionModel.isSelected(option)) {
727
+ optionComponent.select();
728
+ }
729
+ else {
730
+ // NOTE: deselect for all is needed because of virtual scroll and reusing options
731
+ optionComponent.deselect();
732
+ }
733
+ });
734
+ }
735
+ findOptionByValue(options, value) {
736
+ const index = options.findIndex((option) => option.value === value);
737
+ const option = options[index];
738
+ this.assertIsDefined(option, `Option with value ${value} not found`);
739
+ return { option, index };
740
+ }
741
+ emitValue() {
742
+ this._value = this._selectionModel.selected.map((option) => option.value);
743
+ const outputValue = this.multiple ? this._value : this._value[0];
744
+ this.valueChange.emit(outputValue);
745
+ this._onChange(outputValue);
746
+ this.selectionChange.emit(new NgxVirtualSelectFieldChange(this, outputValue));
747
+ }
748
+ assertIsDefined(value, message) {
749
+ if (value === undefined || value === null) {
750
+ throw new Error(message);
751
+ }
752
+ }
753
+ static { this.nextId = 0; }
754
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: NgxVirtualSelectFieldComponent, deps: [{ token: MAT_FORM_FIELD, optional: true }, { token: NGX_VIRTUAL_SELECT_FIELD_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
755
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: NgxVirtualSelectFieldComponent, isStandalone: true, selector: "ngx-virtual-select-field", inputs: { userAriaDescribedBy: ["aria-describedby", "userAriaDescribedBy"], panelWidth: "panelWidth", optionHeight: ["optionHeight", "optionHeight", (value) => numberAttribute(value, OPTION_HEIGHT)], panelViewportPageSize: ["panelViewportPageSize", "panelViewportPageSize", (value) => numberAttribute(value, PANEL_VIEWPORT_PAGE_SIZE)], multiple: ["multiple", "multiple", booleanAttribute], tabIndex: ["tabIndex", "tabIndex", (value) => numberAttribute(value, 0)], typeaheadDebounceInterval: ["typeaheadDebounceInterval", "typeaheadDebounceInterval", numberAttribute], panelClass: "panelClass", filterable: ["filterable", "filterable", booleanAttribute], filterPlaceholder: "filterPlaceholder", value: "value", placeholder: "placeholder", required: ["required", "required", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute] }, outputs: { valueChange: "valueChange", selectionChange: "selectionChange" }, host: { listeners: { "focus": "onFocusIn()", "blur": "onFocusOut()", "keydown": "onKeyDown($event)" }, properties: { "attr.tabindex": "this.disabled ? -1 : tabIndex", "class.ngx-virtual-select-field-disabled": "disabled", "class.ngx-virtual-select-field-invalid": "errorState" }, classAttribute: "ngx-virtual-select-field" }, providers: [
756
+ {
757
+ provide: MatFormFieldControl,
758
+ useExisting: NgxVirtualSelectFieldComponent,
759
+ },
760
+ {
761
+ provide: NGX_VIRTUAL_SELECT_FIELD_OPTION_PARENT,
762
+ useExisting: NgxVirtualSelectFieldComponent,
763
+ },
764
+ ], queries: [{ propertyName: "optionFor", first: true, predicate: NgxVirtualSelectFieldOptionForDirective, descendants: true }, { propertyName: "customTrigger", first: true, predicate: NGX_VIRTUAL_SELECT_FIELD_TRIGGER, descendants: true }, { propertyName: "optionsQuery", predicate: NgxVirtualSelectFieldOptionComponent }], viewQueries: [{ propertyName: "cdkVirtualScrollViewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true }, { propertyName: "cdkConnectedOverlay", first: true, predicate: CdkConnectedOverlay, descendants: true }, { propertyName: "filterInput", first: true, predicate: ["filterInput"], descendants: true }], exportAs: ["ngxVirtualSelectField"], ngImport: i0, template: "<div\n class=\"ngx-virtual-select-field-trigger\"\n cdk-overlay-origin\n (click)=\"open()\"\n #fallbackOverlayOrigin=\"cdkOverlayOrigin\"\n #trigger\n>\n <div class=\"ngx-virtual-select-field-value\">\n @if (empty) {\n <span class=\"ngx-virtual-select-field-placeholder\">{{\n placeholder\n }}</span>\n } @else {\n <span>\n @if (customTrigger) {\n <ng-content select=\"ngx-virtual-select-field-trigger\"></ng-content>\n } @else {\n <span>{{ triggerValue$ | async }}</span>\n }\n </span>\n }\n </div>\n\n <div class=\"ngx-virtual-select-field-arrow-wrapper\">\n <div class=\"ngx-virtual-select-field-arrow\">\n <!-- Use an inline SVG, because it works better than a CSS triangle in high contrast mode. -->\n <svg\n viewBox=\"0 0 24 24\"\n width=\"24px\"\n height=\"24px\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path d=\"M7 10l5 5 5-5z\" />\n </svg>\n </div>\n </div>\n</div>\n\n<ng-template\n cdk-connected-overlay\n cdkConnectedOverlayLockPosition\n cdkConnectedOverlayHasBackdrop\n cdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n [cdkConnectedOverlayOpen]=\"isPanelOpened()\"\n [cdkConnectedOverlayOrigin]=\"preferredOverlayOrigin || fallbackOverlayOrigin\"\n [cdkConnectedOverlayPositions]=\"POSITIONS\"\n [cdkConnectedOverlayPanelClass]=\"overlayPanelClass\"\n [cdkConnectedOverlayWidth]=\"overlayWidth()\"\n (backdropClick)=\"close()\"\n (detach)=\"close()\"\n (attach)=\"onOverlayAttached()\"\n>\n <!--\n [attr.aria-multiselectable]=\"multiple\"\n [attr.aria-label]=\"ariaLabel || null\"\n [attr.aria-labelledby]=\"_getPanelAriaLabelledby()\"\n [@transformPanel]=\"'showing'\"\n (@transformPanel.done)=\"_panelDoneAnimatingStream.next($event.toState)\"\n -->\n <div class=\"ngx-virtual-select-field-panel {{ inheritedColorTheme }}\" [ngClass]=\"panelClass\">\n @if (filterable) {\n <div class=\"ngx-virtual-select-field-filter-wrapper\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"ngx-virtual-select-field-filter-field\">\n <input\n matInput\n #filterInput\n type=\"text\"\n [placeholder]=\"filterPlaceholder\"\n [value]=\"filterText()\"\n (input)=\"onFilterInput($event)\"\n (keydown)=\"onFilterKeyDown($event)\"\n />\n </mat-form-field>\n </div>\n }\n <cdk-virtual-scroll-viewport\n role=\"listbox\"\n tabindex=\"-1\"\n class=\"ngx-virtual-select-field-viewport\"\n [style.--ngx-virtual-select-field__viewport-page-size]=\"maxPageSize\"\n [style.--ngx-virtual-select-field__viewport-option-height.px]=\"optionHeight\"\n [itemSize]=\"optionHeight\"\n (scrolledIndexChange)=\"onScrolledIndexChange()\"\n (keydown)=\"onKeyDown($event)\"\n >\n <div class=\"ngx-virtual-select-field-list-wrapper\">\n <ng-container\n *cdkVirtualFor=\"\n let option of filteredOptions$ | async;\n trackBy: optionTrackBy\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"optionFor.template\"\n [ngTemplateOutletContext]=\"{ $implicit: option }\"\n >\n </ng-container>\n </ng-container>\n </div>\n </cdk-virtual-scroll-viewport>\n </div>\n</ng-template>\n", styles: [":host{color:var(--ngx-virtual-select-field-trigger-text-color);font-family:var(--ngx-virtual-select-field-trigger-font-family);line-height:var(--ngx-virtual-select-field-trigger-line-height);font-size:var(--ngx-virtual-select-field-trigger-font-size);font-weight:var(--ngx-virtual-select-field-trigger-font-weight);letter-spacing:var(--ngx-virtual-select-field-trigger-letter-spacing);outline:none}.ngx-virtual-select-field-trigger{display:inline-flex;align-items:center;cursor:pointer;position:relative;box-sizing:border-box;width:100%}:host-context(.ngx-virtual-select-field-disabled) .ngx-virtual-select-field-trigger{cursor:default;color:var(--ngx-virtual-select-field-trigger-text-color--disabled)}.ngx-virtual-select-field-value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%}.ngx-virtual-select-field-placeholder{transition:var(--ngx-virtual-select-field-placeholder-transition);color:var(--ngx-virtual-select-field-placeholder-text-color)}._mat-animation-noopable .ngx-virtual-select-field-placeholder{transition:none}:host-context(.mat-form-field-hide-placeholder) .ngx-virtual-select-field-placeholder{color:transparent;-webkit-text-fill-color:transparent;transition:none;display:block}.ngx-virtual-select-field-placeholder:empty:before{content:\" \";white-space:pre;width:1px;display:inline-block}.ngx-virtual-select-field-arrow{width:calc(var(--ngx-virtual-select-field-arrow-size) * 2);height:var(--ngx-virtual-select-field-arrow-size);position:relative;color:var(--ngx-virtual-select-field-arrow-color--enabled)}:host-context(.ngx-virtual-select-field-invalid) .ngx-virtual-select-field-arrow{color:var(--ngx-virtual-select-field-arrow-color--invalid)}:host-context(.mat-focused):not(.ngx-virtual-select-field-invalid) .ngx-virtual-select-field-arrow{color:var(--ngx-virtual-select-field-arrow-color--focused)}:host-context(.ngx-virtual-select-field-disabled) .ngx-virtual-select-field-arrow{color:var(--ngx-virtual-select-field-arrow-color--disabled)}.ngx-virtual-select-field-arrow svg{fill:currentColor;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}@media(forced-colors:active){.ngx-virtual-select-field-arrow svg{fill:CanvasText}:host-context(.ngx-virtual-select-field-disabled) .ngx-virtual-select-field-arrow svg{fill:GrayText}}.ngx-virtual-select-field-arrow-wrapper{height:24px;flex-shrink:0;display:inline-flex;align-items:center}:host-context(.mat-form-field-appearance-fill) .ngx-virtual-select-field-arrow-wrapper{transform:translateY(-8px)}:host-context(.mat-form-field-appearance-fill .mdc-text-field--no-label) .ngx-virtual-select-field-arrow-wrapper{transform:none}.ngx-virtual-select-field-panel{width:100%;background:var(--ngx-virtual-select-field-panel-background);box-shadow:var(--ngx-virtual-select-field-panel-box-shadow);display:flex;flex-direction:column}@media(forced-colors:active){.ngx-virtual-select-field-panel{outline:solid 1px}}.ngx-virtual-select-field-filter-wrapper{padding:8px;border-bottom:1px solid var(--ngx-virtual-select-field-divider-color, rgba(0, 0, 0, .12));background:var(--ngx-virtual-select-field-panel-background)}.ngx-virtual-select-field-filter-field{width:100%;display:block}.ngx-virtual-select-field-viewport{width:100%;height:calc(var(--ngx-virtual-select-field__viewport-option-height) * var(--ngx-virtual-select-field__viewport-page-size) + var(--ngx-virtual-select-field-panel-list-wrapper-padding) * 2)}.ngx-virtual-select-field-list-wrapper{display:flex;flex-direction:column;padding-top:var(--ngx-virtual-select-field-panel-list-wrapper-padding);padding-bottom:var(--ngx-virtual-select-field-panel-list-wrapper-padding)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i2.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i2.CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "directive", type: i3.CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i3.CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i3.CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "ngmodule", type: ScrollingModule }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
765
+ }
766
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: NgxVirtualSelectFieldComponent, decorators: [{
767
+ type: Component,
768
+ args: [{ selector: 'ngx-virtual-select-field', exportAs: 'ngxVirtualSelectField', standalone: true, imports: [CommonModule, OverlayModule, ScrollingModule, MatFormFieldModule, MatInputModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
769
+ {
770
+ provide: MatFormFieldControl,
771
+ useExisting: NgxVirtualSelectFieldComponent,
772
+ },
773
+ {
774
+ provide: NGX_VIRTUAL_SELECT_FIELD_OPTION_PARENT,
775
+ useExisting: NgxVirtualSelectFieldComponent,
776
+ },
777
+ ], host: {
778
+ '[attr.tabindex]': 'this.disabled ? -1 : tabIndex',
779
+ '(focus)': 'onFocusIn()',
780
+ '(blur)': 'onFocusOut()',
781
+ '(keydown)': 'onKeyDown($event)',
782
+ class: 'ngx-virtual-select-field',
783
+ '[class.ngx-virtual-select-field-disabled]': 'disabled',
784
+ '[class.ngx-virtual-select-field-invalid]': 'errorState',
785
+ }, template: "<div\n class=\"ngx-virtual-select-field-trigger\"\n cdk-overlay-origin\n (click)=\"open()\"\n #fallbackOverlayOrigin=\"cdkOverlayOrigin\"\n #trigger\n>\n <div class=\"ngx-virtual-select-field-value\">\n @if (empty) {\n <span class=\"ngx-virtual-select-field-placeholder\">{{\n placeholder\n }}</span>\n } @else {\n <span>\n @if (customTrigger) {\n <ng-content select=\"ngx-virtual-select-field-trigger\"></ng-content>\n } @else {\n <span>{{ triggerValue$ | async }}</span>\n }\n </span>\n }\n </div>\n\n <div class=\"ngx-virtual-select-field-arrow-wrapper\">\n <div class=\"ngx-virtual-select-field-arrow\">\n <!-- Use an inline SVG, because it works better than a CSS triangle in high contrast mode. -->\n <svg\n viewBox=\"0 0 24 24\"\n width=\"24px\"\n height=\"24px\"\n focusable=\"false\"\n aria-hidden=\"true\"\n >\n <path d=\"M7 10l5 5 5-5z\" />\n </svg>\n </div>\n </div>\n</div>\n\n<ng-template\n cdk-connected-overlay\n cdkConnectedOverlayLockPosition\n cdkConnectedOverlayHasBackdrop\n cdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n [cdkConnectedOverlayOpen]=\"isPanelOpened()\"\n [cdkConnectedOverlayOrigin]=\"preferredOverlayOrigin || fallbackOverlayOrigin\"\n [cdkConnectedOverlayPositions]=\"POSITIONS\"\n [cdkConnectedOverlayPanelClass]=\"overlayPanelClass\"\n [cdkConnectedOverlayWidth]=\"overlayWidth()\"\n (backdropClick)=\"close()\"\n (detach)=\"close()\"\n (attach)=\"onOverlayAttached()\"\n>\n <!--\n [attr.aria-multiselectable]=\"multiple\"\n [attr.aria-label]=\"ariaLabel || null\"\n [attr.aria-labelledby]=\"_getPanelAriaLabelledby()\"\n [@transformPanel]=\"'showing'\"\n (@transformPanel.done)=\"_panelDoneAnimatingStream.next($event.toState)\"\n -->\n <div class=\"ngx-virtual-select-field-panel {{ inheritedColorTheme }}\" [ngClass]=\"panelClass\">\n @if (filterable) {\n <div class=\"ngx-virtual-select-field-filter-wrapper\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"ngx-virtual-select-field-filter-field\">\n <input\n matInput\n #filterInput\n type=\"text\"\n [placeholder]=\"filterPlaceholder\"\n [value]=\"filterText()\"\n (input)=\"onFilterInput($event)\"\n (keydown)=\"onFilterKeyDown($event)\"\n />\n </mat-form-field>\n </div>\n }\n <cdk-virtual-scroll-viewport\n role=\"listbox\"\n tabindex=\"-1\"\n class=\"ngx-virtual-select-field-viewport\"\n [style.--ngx-virtual-select-field__viewport-page-size]=\"maxPageSize\"\n [style.--ngx-virtual-select-field__viewport-option-height.px]=\"optionHeight\"\n [itemSize]=\"optionHeight\"\n (scrolledIndexChange)=\"onScrolledIndexChange()\"\n (keydown)=\"onKeyDown($event)\"\n >\n <div class=\"ngx-virtual-select-field-list-wrapper\">\n <ng-container\n *cdkVirtualFor=\"\n let option of filteredOptions$ | async;\n trackBy: optionTrackBy\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"optionFor.template\"\n [ngTemplateOutletContext]=\"{ $implicit: option }\"\n >\n </ng-container>\n </ng-container>\n </div>\n </cdk-virtual-scroll-viewport>\n </div>\n</ng-template>\n", styles: [":host{color:var(--ngx-virtual-select-field-trigger-text-color);font-family:var(--ngx-virtual-select-field-trigger-font-family);line-height:var(--ngx-virtual-select-field-trigger-line-height);font-size:var(--ngx-virtual-select-field-trigger-font-size);font-weight:var(--ngx-virtual-select-field-trigger-font-weight);letter-spacing:var(--ngx-virtual-select-field-trigger-letter-spacing);outline:none}.ngx-virtual-select-field-trigger{display:inline-flex;align-items:center;cursor:pointer;position:relative;box-sizing:border-box;width:100%}:host-context(.ngx-virtual-select-field-disabled) .ngx-virtual-select-field-trigger{cursor:default;color:var(--ngx-virtual-select-field-trigger-text-color--disabled)}.ngx-virtual-select-field-value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%}.ngx-virtual-select-field-placeholder{transition:var(--ngx-virtual-select-field-placeholder-transition);color:var(--ngx-virtual-select-field-placeholder-text-color)}._mat-animation-noopable .ngx-virtual-select-field-placeholder{transition:none}:host-context(.mat-form-field-hide-placeholder) .ngx-virtual-select-field-placeholder{color:transparent;-webkit-text-fill-color:transparent;transition:none;display:block}.ngx-virtual-select-field-placeholder:empty:before{content:\" \";white-space:pre;width:1px;display:inline-block}.ngx-virtual-select-field-arrow{width:calc(var(--ngx-virtual-select-field-arrow-size) * 2);height:var(--ngx-virtual-select-field-arrow-size);position:relative;color:var(--ngx-virtual-select-field-arrow-color--enabled)}:host-context(.ngx-virtual-select-field-invalid) .ngx-virtual-select-field-arrow{color:var(--ngx-virtual-select-field-arrow-color--invalid)}:host-context(.mat-focused):not(.ngx-virtual-select-field-invalid) .ngx-virtual-select-field-arrow{color:var(--ngx-virtual-select-field-arrow-color--focused)}:host-context(.ngx-virtual-select-field-disabled) .ngx-virtual-select-field-arrow{color:var(--ngx-virtual-select-field-arrow-color--disabled)}.ngx-virtual-select-field-arrow svg{fill:currentColor;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}@media(forced-colors:active){.ngx-virtual-select-field-arrow svg{fill:CanvasText}:host-context(.ngx-virtual-select-field-disabled) .ngx-virtual-select-field-arrow svg{fill:GrayText}}.ngx-virtual-select-field-arrow-wrapper{height:24px;flex-shrink:0;display:inline-flex;align-items:center}:host-context(.mat-form-field-appearance-fill) .ngx-virtual-select-field-arrow-wrapper{transform:translateY(-8px)}:host-context(.mat-form-field-appearance-fill .mdc-text-field--no-label) .ngx-virtual-select-field-arrow-wrapper{transform:none}.ngx-virtual-select-field-panel{width:100%;background:var(--ngx-virtual-select-field-panel-background);box-shadow:var(--ngx-virtual-select-field-panel-box-shadow);display:flex;flex-direction:column}@media(forced-colors:active){.ngx-virtual-select-field-panel{outline:solid 1px}}.ngx-virtual-select-field-filter-wrapper{padding:8px;border-bottom:1px solid var(--ngx-virtual-select-field-divider-color, rgba(0, 0, 0, .12));background:var(--ngx-virtual-select-field-panel-background)}.ngx-virtual-select-field-filter-field{width:100%;display:block}.ngx-virtual-select-field-viewport{width:100%;height:calc(var(--ngx-virtual-select-field__viewport-option-height) * var(--ngx-virtual-select-field__viewport-page-size) + var(--ngx-virtual-select-field-panel-list-wrapper-padding) * 2)}.ngx-virtual-select-field-list-wrapper{display:flex;flex-direction:column;padding-top:var(--ngx-virtual-select-field-panel-list-wrapper-padding);padding-bottom:var(--ngx-virtual-select-field-panel-list-wrapper-padding)}\n"] }]
786
+ }], ctorParameters: () => [{ type: i4.MatFormField, decorators: [{
787
+ type: Optional
788
+ }, {
789
+ type: Inject,
790
+ args: [MAT_FORM_FIELD]
791
+ }] }, { type: undefined, decorators: [{
792
+ type: Optional
793
+ }, {
794
+ type: Inject,
795
+ args: [NGX_VIRTUAL_SELECT_FIELD_CONFIG]
796
+ }] }], propDecorators: { userAriaDescribedBy: [{
797
+ type: Input,
798
+ args: ['aria-describedby']
799
+ }], panelWidth: [{
800
+ type: Input
801
+ }], optionHeight: [{
802
+ type: Input,
803
+ args: [{
804
+ transform: (value) => numberAttribute(value, OPTION_HEIGHT),
805
+ }]
806
+ }], panelViewportPageSize: [{
807
+ type: Input,
808
+ args: [{
809
+ transform: (value) => numberAttribute(value, PANEL_VIEWPORT_PAGE_SIZE),
810
+ }]
811
+ }], multiple: [{
812
+ type: Input,
813
+ args: [{ transform: booleanAttribute }]
814
+ }], tabIndex: [{
815
+ type: Input,
816
+ args: [{
817
+ transform: (value) => numberAttribute(value, 0),
818
+ }]
819
+ }], typeaheadDebounceInterval: [{
820
+ type: Input,
821
+ args: [{ transform: numberAttribute }]
822
+ }], panelClass: [{
823
+ type: Input
824
+ }], filterable: [{
825
+ type: Input,
826
+ args: [{ transform: booleanAttribute }]
827
+ }], filterPlaceholder: [{
828
+ type: Input
829
+ }], value: [{
830
+ type: Input
831
+ }], placeholder: [{
832
+ type: Input
833
+ }], required: [{
834
+ type: Input,
835
+ args: [{ transform: booleanAttribute }]
836
+ }], disabled: [{
837
+ type: Input,
838
+ args: [{ transform: booleanAttribute }]
839
+ }], cdkVirtualScrollViewport: [{
840
+ type: ViewChild,
841
+ args: [CdkVirtualScrollViewport, { static: false }]
842
+ }], cdkConnectedOverlay: [{
843
+ type: ViewChild,
844
+ args: [CdkConnectedOverlay, { static: false }]
845
+ }], filterInput: [{
846
+ type: ViewChild,
847
+ args: ['filterInput', { static: false }]
848
+ }], optionFor: [{
849
+ type: ContentChild,
850
+ args: [NgxVirtualSelectFieldOptionForDirective]
851
+ }], customTrigger: [{
852
+ type: ContentChild,
853
+ args: [NGX_VIRTUAL_SELECT_FIELD_TRIGGER]
854
+ }], optionsQuery: [{
855
+ type: ContentChildren,
856
+ args: [NgxVirtualSelectFieldOptionComponent]
857
+ }] } });
858
+ class NgxVirtualSelectFieldChange {
859
+ constructor(source, value) {
860
+ this.source = source;
861
+ this.value = value;
862
+ }
863
+ }
864
+
865
+ const NgxVirtualSelectFieldBundle = [
866
+ NgxVirtualSelectFieldComponent,
867
+ NgxVirtualSelectFieldOptionForDirective,
868
+ NgxVirtualSelectFieldTriggerDirective,
869
+ NgxVirtualSelectFieldOptionComponent,
870
+ ];
871
+
872
+ /**
873
+ * Generated bundle index. Do not edit.
874
+ */
875
+
876
+ export { NGX_VIRTUAL_SELECT_FIELD_CONFIG, NgxVirtualSelectFieldBundle, NgxVirtualSelectFieldChange, NgxVirtualSelectFieldComponent, NgxVirtualSelectFieldOptionComponent, NgxVirtualSelectFieldOptionForDirective, NgxVirtualSelectFieldTriggerDirective };
877
+ //# sourceMappingURL=ngx-virtual-select-field-filterable.mjs.map