quang 20.5.0 → 20.5.2-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.
Files changed (35) hide show
  1. package/components/autocomplete/README.md +38 -1
  2. package/components/autocomplete/index.d.ts +266 -37
  3. package/components/date/index.d.ts +3 -0
  4. package/components/radio-group/README.md +75 -0
  5. package/components/radio-group/index.d.ts +31 -0
  6. package/components/select/README.md +34 -1
  7. package/components/select/index.d.ts +25 -3
  8. package/components/shared/index.d.ts +23 -3
  9. package/fesm2022/quang-auth.mjs +18 -18
  10. package/fesm2022/quang-components-autocomplete.mjs +647 -196
  11. package/fesm2022/quang-components-autocomplete.mjs.map +1 -1
  12. package/fesm2022/quang-components-checkbox.mjs +5 -5
  13. package/fesm2022/quang-components-checkbox.mjs.map +1 -1
  14. package/fesm2022/quang-components-date.mjs +68 -16
  15. package/fesm2022/quang-components-date.mjs.map +1 -1
  16. package/fesm2022/quang-components-input.mjs +5 -5
  17. package/fesm2022/quang-components-input.mjs.map +1 -1
  18. package/fesm2022/quang-components-paginator.mjs +13 -13
  19. package/fesm2022/quang-components-radio-group.mjs +59 -0
  20. package/fesm2022/quang-components-radio-group.mjs.map +1 -0
  21. package/fesm2022/quang-components-select.mjs +73 -24
  22. package/fesm2022/quang-components-select.mjs.map +1 -1
  23. package/fesm2022/quang-components-shared.mjs +93 -59
  24. package/fesm2022/quang-components-shared.mjs.map +1 -1
  25. package/fesm2022/quang-components-table.mjs +3 -3
  26. package/fesm2022/quang-components-wysiwyg.mjs +3 -3
  27. package/fesm2022/quang-device.mjs +3 -3
  28. package/fesm2022/quang-loader.mjs +6 -6
  29. package/fesm2022/quang-overlay-modal.mjs +6 -6
  30. package/fesm2022/quang-overlay-popover.mjs +6 -6
  31. package/fesm2022/quang-overlay-shared.mjs +9 -9
  32. package/fesm2022/quang-overlay-toast.mjs +6 -6
  33. package/fesm2022/quang-overlay-tooltip.mjs +6 -6
  34. package/fesm2022/quang-translation.mjs +6 -6
  35. package/package.json +36 -32
@@ -1,11 +1,10 @@
1
- import { NgClass, NgStyle } from '@angular/common';
1
+ import { NgClass, NgTemplateOutlet, NgStyle } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { input, viewChild, signal, computed, output, effect, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { input, output, viewChild, signal, computed, effect, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
4
4
  import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
5
5
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
6
6
  import { TranslocoPipe } from '@jsverse/transloco';
7
7
  import { QuangTooltipDirective } from 'quang/overlay/tooltip';
8
- import { Subject, debounceTime, distinctUntilChanged } from 'rxjs';
9
8
  import { QuangBaseComponent, OptionListParentType, QuangOptionListComponent } from 'quang/components/shared';
10
9
 
11
10
  /**
@@ -18,263 +17,715 @@ import { QuangBaseComponent, OptionListParentType, QuangOptionListComponent } fr
18
17
  * `searchTextDebounce` is by default set to 300ms.
19
18
  */
20
19
  class QuangAutocompleteComponent extends QuangBaseComponent {
20
+ /**
21
+ * Finds an option whose label exactly matches the given text (case-insensitive, trimmed).
22
+ * @param text The text to match against option labels
23
+ * @returns The matching option, or undefined if no match
24
+ */
25
+ findMatchingOption(text) {
26
+ if (!text)
27
+ return undefined;
28
+ const searchTextLower = text.trim().toLowerCase();
29
+ return this.selectOptions().find((x) => x.label.trim().toLowerCase() === searchTextLower);
30
+ }
31
+ // ============================================
32
+ // CONSTRUCTOR
33
+ // ============================================
21
34
  constructor() {
22
35
  super();
23
- // the form can't be a random text but must be one of the options if this is false
36
+ // ============================================
37
+ // INPUTS - Configuration properties
38
+ // ============================================
39
+ /**
40
+ * The list of options to display in the autocomplete dropdown.
41
+ */
42
+ this.selectOptions = input.required(...(ngDevMode ? [{ debugName: "selectOptions" }] : []));
43
+ /**
44
+ * When true, allows any text input as a valid form value, not just option values.
45
+ * The form value will sync with whatever text the user types.
46
+ * When false (default), the form value must match one of the option values.
47
+ * @default false
48
+ */
49
+ this.allowFreeText = input(false, ...(ngDevMode ? [{ debugName: "allowFreeText" }] : []));
50
+ /**
51
+ * When true and allowFreeText is false, automatically selects an option if the user's
52
+ * input text matches an option's label exactly (case-insensitive, trimmed).
53
+ * This provides a better UX by auto-selecting when users type a complete option label.
54
+ * @default true
55
+ */
56
+ this.autoSelectOnExactMatch = input(true, ...(ngDevMode ? [{ debugName: "autoSelectOnExactMatch" }] : []));
57
+ /**
58
+ * When true, updates the form value as the user types (after debounce).
59
+ * When false (default), the form value is only updated when:
60
+ * - User selects an option from the dropdown
61
+ * - User stops typing and the input loses focus (blur)
62
+ * @default false
63
+ */
64
+ this.updateValueOnType = input(false, ...(ngDevMode ? [{ debugName: "updateValueOnType" }] : []));
65
+ /**
66
+ * Whether the form value can be any text or must match one of the options.
67
+ * When true, the form value syncs with the input text.
68
+ * When false (default), the form value must be one of the option values.
69
+ * @default false
70
+ * @deprecated Use `allowFreeText` instead. This input will be removed in a future version.
71
+ */
24
72
  this.syncFormWithText = input(false, ...(ngDevMode ? [{ debugName: "syncFormWithText" }] : []));
73
+ /**
74
+ * Maximum height of the option list before scrolling.
75
+ * @default '200px'
76
+ */
25
77
  this.optionListMaxHeight = input('200px', ...(ngDevMode ? [{ debugName: "optionListMaxHeight" }] : []));
26
- this.selectOptions = input.required(...(ngDevMode ? [{ debugName: "selectOptions" }] : []));
78
+ /**
79
+ * Whether to translate option labels.
80
+ * @default true
81
+ */
27
82
  this.translateValue = input(true, ...(ngDevMode ? [{ debugName: "translateValue" }] : []));
83
+ /**
84
+ * Scroll behavior when the option list opens.
85
+ * @default 'smooth'
86
+ */
28
87
  this.scrollBehaviorOnOpen = input('smooth', ...(ngDevMode ? [{ debugName: "scrollBehaviorOnOpen" }] : []));
29
88
  /**
30
- * Only emits the value without saving it in ngControl
89
+ * When true, only emits the value without saving it to ngControl.
90
+ * @default false
31
91
  */
32
92
  this.emitOnly = input(false, ...(ngDevMode ? [{ debugName: "emitOnly" }] : []));
93
+ /**
94
+ * Enable multiple selection mode with chips.
95
+ * @default false
96
+ */
33
97
  this.multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
34
98
  /**
35
- * Set the maximum length in characters of the single chip.
99
+ * Maximum length in characters for chip display text.
100
+ * When set, chips will be truncated and show a tooltip with full text.
101
+ * @default 0 (no limit)
36
102
  */
37
103
  this.chipMaxLength = input(0, ...(ngDevMode ? [{ debugName: "chipMaxLength" }] : []));
104
+ /**
105
+ * Layout direction for chips in multiple selection mode.
106
+ * @default 'vertical'
107
+ */
38
108
  this.multiSelectDisplayMode = input('vertical', ...(ngDevMode ? [{ debugName: "multiSelectDisplayMode" }] : []));
109
+ /**
110
+ * Position of chips relative to the input in multiple selection mode.
111
+ * - 'top': Chips are displayed above the input (default)
112
+ * - 'bottom': Chips are displayed below the input
113
+ * @default 'top'
114
+ */
115
+ this.chipsPosition = input('top', ...(ngDevMode ? [{ debugName: "chipsPosition" }] : []));
116
+ /**
117
+ * Debounce time in milliseconds for search text changes.
118
+ * @default 300
119
+ */
120
+ this.searchTextDebounce = input(300, ...(ngDevMode ? [{ debugName: "searchTextDebounce" }] : []));
121
+ /**
122
+ * Whether to filter options internally based on input text.
123
+ * When false, filtering should be handled externally via searchTextChange event.
124
+ * @default true
125
+ */
126
+ this.internalFilterOptions = input(true, ...(ngDevMode ? [{ debugName: "internalFilterOptions" }] : []));
127
+ // ============================================
128
+ // OUTPUTS - Event emitters
129
+ // ============================================
130
+ /**
131
+ * Emitted when an option is selected.
132
+ * Emits the selected option's value, or null when cleared.
133
+ */
134
+ this.selectedOption = output();
135
+ /**
136
+ * Emitted when the search text changes (after debounce).
137
+ * Useful for external filtering or API calls.
138
+ */
139
+ this.searchTextChange = output();
140
+ // ============================================
141
+ // VIEW CHILDREN - Template references
142
+ // ============================================
143
+ /** Reference to the option list component */
39
144
  this.optionList = viewChild('optionList', ...(ngDevMode ? [{ debugName: "optionList" }] : []));
40
- this._showOptions = signal(null, ...(ngDevMode ? [{ debugName: "_showOptions" }] : []));
41
- this._inputValue = signal('', ...(ngDevMode ? [{ debugName: "_inputValue" }] : []));
42
- this._optionHideTimeout = signal(undefined, ...(ngDevMode ? [{ debugName: "_optionHideTimeout" }] : []));
43
- this._chipList = signal([], ...(ngDevMode ? [{ debugName: "_chipList" }] : []));
44
- this._selectedOptions = signal([], ...(ngDevMode ? [{ debugName: "_selectedOptions" }] : []));
45
- this.inputValue$ = new Subject();
46
- this.selectOptionsChange = toObservable(this.selectOptions)
47
- .pipe(takeUntilDestroyed())
48
- .subscribe(() => {
145
+ /** Reference to the input element */
146
+ this.selectInput = viewChild('selectInput', ...(ngDevMode ? [{ debugName: "selectInput" }] : []));
147
+ /** Reference to the chip container element */
148
+ this.chipContainer = viewChild('chipContainer', ...(ngDevMode ? [{ debugName: "chipContainer" }] : []));
149
+ /** Reference to the main autocomplete container */
150
+ this.autocompleteContainer = viewChild('autocompleteContainer', ...(ngDevMode ? [{ debugName: "autocompleteContainer" }] : []));
151
+ // ============================================
152
+ // PUBLIC STATE - Used in template
153
+ // ============================================
154
+ /** Constant for option list parent type */
155
+ this.ParentType = OptionListParentType.AUTOCOMPLETE;
156
+ /** Height of the input element (used for positioning) */
157
+ this.inputHeight = signal(0, ...(ngDevMode ? [{ debugName: "inputHeight" }] : []));
158
+ /**
159
+ * The display text for the input field.
160
+ * - When searching: shows what the user typed
161
+ * - When allowFreeText/syncFormWithText is true and no matching option: shows the raw value
162
+ * - When not searching: shows the selected option's label (derived from _value)
163
+ */
164
+ this._inputValue = computed(() => {
165
+ if (this._isSearching()) {
166
+ return this._userSearchText();
167
+ }
168
+ // Derive display text from _value by finding the matching option
49
169
  const value = this._value();
50
- if (!this.multiple() && !this._inputValue()) {
51
- this.setInputValue();
170
+ if (value === null || value === undefined || value === '' || Array.isArray(value)) {
171
+ return '';
52
172
  }
53
- else if (this.multiple() && Array.isArray(value) && value.length > 0) {
54
- for (const valueElement of value) {
55
- this.onSelectValue(valueElement);
56
- }
173
+ const option = this.selectOptions().find((x) => x.value === value);
174
+ // When free text is allowed and no matching option found, display the value itself
175
+ // (since the value IS the text the user typed)
176
+ if (!option && this._allowFreeTextInternal()) {
177
+ return String(value);
57
178
  }
58
- });
179
+ return option?.label ?? '';
180
+ }, ...(ngDevMode ? [{ debugName: "_inputValue" }] : []));
181
+ /** Whether the option list is currently visible */
182
+ this._showOptions = signal(null, ...(ngDevMode ? [{ debugName: "_showOptions" }] : []));
183
+ /** List of selected chip values (for multiple mode) */
184
+ this._chipList = signal([], ...(ngDevMode ? [{ debugName: "_chipList" }] : []));
185
+ /** List of selected option objects (for multiple mode) */
186
+ this._selectedOptions = signal([], ...(ngDevMode ? [{ debugName: "_selectedOptions" }] : []));
187
+ /** Filtered options based on search text and chip selection */
59
188
  this._filteredOptions = computed(() => {
60
- const text = this._inputValue();
189
+ const searchText = this._isSearching() ? this._userSearchText() : '';
61
190
  if (this.multiple()) {
62
- return this.filterOptions(text).filter((x) => !this._chipList().some((chip) => chip === x.value));
63
- }
64
- else {
65
- return text?.length ? this.filterOptions(text) : this.selectOptions();
191
+ return this.filterOptions(searchText).filter((x) => !this._chipList().some((chip) => chip === x.value));
66
192
  }
193
+ return searchText?.length ? this.filterOptions(searchText) : this.selectOptions();
67
194
  }, ...(ngDevMode ? [{ debugName: "_filteredOptions" }] : []));
68
- this.selectedOption = output();
69
- this.searchTextChange = output();
70
- this.searchTextDebounce = input(300, ...(ngDevMode ? [{ debugName: "searchTextDebounce" }] : []));
71
- this.internalFilterOptions = input(true, ...(ngDevMode ? [{ debugName: "internalFilterOptions" }] : []));
72
- this.ParentType = OptionListParentType.AUTOCOMPLETE;
73
- this.formValueChange$ = undefined;
74
- this.selectInput = viewChild('selectInput', ...(ngDevMode ? [{ debugName: "selectInput" }] : []));
75
- this.chipContainer = viewChild('chipContainer', ...(ngDevMode ? [{ debugName: "chipContainer" }] : []));
76
- this.autocompleteContainer = viewChild('autocompleteContainer', ...(ngDevMode ? [{ debugName: "autocompleteContainer" }] : []));
77
- this.inputHeight = signal(0, ...(ngDevMode ? [{ debugName: "inputHeight" }] : []));
78
- this.onChangeSelectInput = effect(() => {
79
- const selectInput = this.selectInput();
80
- if (selectInput) {
81
- this.inputHeight.set(selectInput?.nativeElement?.getBoundingClientRect().height);
82
- selectInput.nativeElement.addEventListener('keydown', (e) => {
83
- if (this.multiple() && this._chipList().length > 0 && !this._inputValue()?.length && e.key === 'Backspace') {
84
- e.preventDefault();
85
- // this.deleteChip(this._chipList()[this._chipList().length - 1])
86
- const chipContainer = this.chipContainer()?.nativeElement;
87
- if (chipContainer) {
88
- const chips = chipContainer.querySelectorAll('.chip button.btn-chip');
89
- if (chips.length > 0) {
90
- const focusChip = chips[chips.length - 1];
91
- focusChip.focus();
92
- focusChip.addEventListener('keydown', (event) => {
93
- if (event.key === 'Backspace') {
94
- event.preventDefault();
95
- this.deleteChip(this._chipList()[this._chipList().length - 1]);
96
- selectInput.nativeElement.focus();
97
- }
98
- else {
99
- event.preventDefault();
100
- }
101
- });
102
- }
103
- }
104
- }
105
- });
106
- }
107
- }, ...(ngDevMode ? [{ debugName: "onChangeSelectInput" }] : []));
108
- this.inputValue$
109
- .pipe(takeUntilDestroyed(), debounceTime(this.searchTextDebounce()), distinctUntilChanged())
110
- .subscribe((value) => {
111
- if (value !== this._inputValue()) {
112
- this.searchTextChange.emit(value?.toString() || '');
113
- if (this.syncFormWithText()) {
114
- this.onValueChange(value, false);
195
+ /**
196
+ * The value to use for highlighting in the option list.
197
+ * When searching: shows the matched option (if any) based on exact label match
198
+ * When not searching: shows the current form value
199
+ * This keeps highlighting in sync with what will be selected on blur.
200
+ */
201
+ this._highlightedValue = computed(() => {
202
+ if (this._isSearching() && !this.multiple()) {
203
+ const searchText = this._userSearchText();
204
+ if (!searchText?.trim()) {
205
+ // If search text is empty, don't highlight anything
206
+ return null;
115
207
  }
208
+ // Find exact match for highlighting
209
+ const matchingOption = this.findMatchingOption(searchText);
210
+ if (matchingOption && this.autoSelectOnExactMatch()) {
211
+ return matchingOption.value;
212
+ }
213
+ // If free text is allowed, don't highlight any option
214
+ // (the typed text itself will be the value)
215
+ return null;
116
216
  }
117
- this._inputValue.set(value?.toString() || '');
118
- if (!this._inputValue()?.length && !this.emitOnly() && !this.multiple()) {
119
- this._ngControl()?.control?.patchValue(null);
120
- }
217
+ // When not searching, use the current value
218
+ return this._value();
219
+ }, ...(ngDevMode ? [{ debugName: "_highlightedValue" }] : []));
220
+ // ============================================
221
+ // PROTECTED STATE - Internal but accessible to subclasses
222
+ // ============================================
223
+ /** Whether the user is actively typing/searching */
224
+ this._isSearching = signal(false, ...(ngDevMode ? [{ debugName: "_isSearching" }] : []));
225
+ /** The text the user is currently typing while searching */
226
+ this._userSearchText = signal('', ...(ngDevMode ? [{ debugName: "_userSearchText" }] : []));
227
+ /**
228
+ * Internal computed that returns true if free text input is allowed.
229
+ * Combines both `allowFreeText` and deprecated `syncFormWithText` inputs.
230
+ */
231
+ this._allowFreeTextInternal = computed(() => {
232
+ return this.allowFreeText() || this.syncFormWithText();
233
+ }, ...(ngDevMode ? [{ debugName: "_allowFreeTextInternal" }] : []));
234
+ // ============================================
235
+ // PRIVATE STATE - Internal implementation details
236
+ // ============================================
237
+ /** Timer for search text debounce */
238
+ this._searchDebounceTimer = null;
239
+ /** Last emitted search text (for distinctUntilChanged behavior) */
240
+ this._lastEmittedSearchText = null;
241
+ /** Whether the component has been destroyed */
242
+ this._isDestroyed = false;
243
+ /** Subscription to form value changes */
244
+ this.formValueChangeSubscription = undefined;
245
+ // ============================================
246
+ // EFFECTS - Reactive side effects
247
+ // ============================================
248
+ /** Effect to handle input element setup and keyboard events */
249
+ this.onChangeSelectInputEffect = effect(() => {
250
+ const selectInput = this.selectInput();
251
+ if (!selectInput)
252
+ return;
253
+ this.inputHeight.set(selectInput.nativeElement.getBoundingClientRect().height);
254
+ selectInput.nativeElement.addEventListener('keydown', (e) => {
255
+ this.handleInputKeydown(e, selectInput.nativeElement);
256
+ });
257
+ }, ...(ngDevMode ? [{ debugName: "onChangeSelectInputEffect" }] : []));
258
+ /** Subscription to options changes */
259
+ this.selectOptionsChangeSubscription = toObservable(this.selectOptions)
260
+ .pipe(takeUntilDestroyed())
261
+ .subscribe(() => {
262
+ this.handleOptionsChange();
121
263
  });
122
- toObservable(this._showOptions)
264
+ /** Subscription to show options changes */
265
+ this.showOptionsChangeSubscription = toObservable(this._showOptions)
123
266
  .pipe(takeUntilDestroyed())
124
267
  .subscribe((data) => {
125
- if (!data && data !== null) {
126
- this.checkInputValue();
127
- }
268
+ // Note: Form value processing is now handled directly in onBlurHandler
269
+ // for immediate processing. This subscription is kept for backwards compatibility
270
+ // but the _isSearching check prevents double-processing since onBlurHandler
271
+ // already sets _isSearching to false before this subscription fires.
272
+ if (!(!data && data !== null && this._isSearching()))
273
+ return;
274
+ // Only process if still in search mode (which means onBlurHandler didn't run)
275
+ this.processTextToFormValue(this._userSearchText(), {
276
+ exitSearchMode: true,
277
+ updateOnMatch: true,
278
+ clearSearchText: true,
279
+ });
280
+ });
281
+ this.destroyRef.onDestroy(() => {
282
+ this._isDestroyed = true;
283
+ if (!this._searchDebounceTimer)
284
+ return;
285
+ clearTimeout(this._searchDebounceTimer);
128
286
  });
129
287
  }
288
+ // ============================================
289
+ // LIFECYCLE HOOKS / OVERRIDES
290
+ // ============================================
130
291
  setupFormControl() {
131
292
  super.setupFormControl();
132
293
  const formControl = this._ngControl()?.control;
133
- if (this.formValueChange$) {
134
- this.formValueChange$.unsubscribe();
135
- this.formValueChange$ = undefined;
294
+ if (this.formValueChangeSubscription) {
295
+ this.formValueChangeSubscription.unsubscribe();
296
+ this.formValueChangeSubscription = undefined;
136
297
  }
137
- if (formControl) {
138
- this.formValueChange$ = formControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => {
139
- if (this.multiple() && Array.isArray(value)) {
140
- this._chipList.set([]);
141
- this._selectedOptions.set([]);
142
- value.forEach((x) => {
143
- this.onSelectValue(x);
144
- });
145
- }
146
- else if (!this.multiple() && (typeof value === 'string' || typeof value === 'number')) {
147
- this.setInputValue();
148
- }
149
- if (!this.syncFormWithText() && !value) {
150
- this._inputValue.set('');
151
- }
298
+ if (!formControl)
299
+ return;
300
+ this.formValueChangeSubscription = formControl.valueChanges
301
+ .pipe(takeUntilDestroyed(this.destroyRef))
302
+ .subscribe((value) => {
303
+ this.handleFormValueChange(value);
304
+ });
305
+ }
306
+ writeValue(val) {
307
+ // Simply update the value - _inputValue is computed and will automatically
308
+ // show the correct display text based on _value and _isSearching state.
309
+ super.writeValue(val);
310
+ // Handle array values for multiple mode
311
+ if (Array.isArray(val)) {
312
+ val.forEach((x) => {
313
+ this.handleSelectValue(x);
152
314
  });
153
315
  }
154
316
  }
317
+ onChangedHandler(value) {
318
+ super.onChangedHandler(value);
319
+ // Exit search mode - _inputValue will now derive from _value
320
+ // Note: Don't clear _userSearchText here - it's needed for processTextToFormValue matching
321
+ this._isSearching.set(false);
322
+ }
323
+ onBlurHandler() {
324
+ // Process form value and exit search mode immediately to avoid visual glitches
325
+ this.processTextToFormValue(this._userSearchText(), {
326
+ exitSearchMode: true,
327
+ updateOnMatch: true,
328
+ clearSearchText: true,
329
+ });
330
+ // Hide dropdown immediately - click events on options are protected by onBlurInput
331
+ // which checks if focus moved to the option list before calling this handler
332
+ this.hideOptionVisibility();
333
+ super.onBlurHandler();
334
+ }
335
+ // ============================================
336
+ // PUBLIC METHODS - Used in template
337
+ // ============================================
338
+ /**
339
+ * Shows the option list dropdown.
340
+ */
155
341
  showOptionVisibility() {
156
- if (this._optionHideTimeout()) {
157
- clearTimeout(this._optionHideTimeout());
158
- this._optionHideTimeout.set(null);
159
- }
160
342
  this._showOptions.set(true);
343
+ // Initialize _userSearchText with current input value when showing options
344
+ // This ensures that if user focuses and blurs without typing, the value is preserved
345
+ // Also enter search mode to enable filtering
346
+ if (this._isSearching())
347
+ return;
348
+ const currentInputValue = this._inputValue();
349
+ this._userSearchText.set(currentInputValue || '');
350
+ this._isSearching.set(true);
161
351
  }
162
- hideOptionVisibility(skipTimeout = false) {
163
- if (this._optionHideTimeout()) {
164
- clearTimeout(this._optionHideTimeout());
165
- }
166
- this._optionHideTimeout.set(setTimeout(() => {
167
- this._showOptions.set(false);
168
- }, skipTimeout ? 0 : 50));
352
+ /**
353
+ * Hides the option list dropdown.
354
+ */
355
+ hideOptionVisibility() {
356
+ this._showOptions.set(false);
169
357
  }
170
- onChangeInput(value) {
358
+ /**
359
+ * Handles input text changes (typing).
360
+ * @param event The input event
361
+ */
362
+ onChangeInput(event) {
171
363
  this.showOptionVisibility();
172
- this.inputValue$.next(value.target?.value);
173
- }
174
- filterOptions(value) {
175
- const options = this.selectOptions();
176
- if (this.internalFilterOptions()) {
177
- return options.filter((x) => x.label.toLowerCase().includes(value.toLowerCase()));
178
- }
179
- return options;
180
- }
181
- onChangedHandler(value) {
182
- super.onChangedHandler(value);
183
- this.setInputValue();
364
+ const value = event.target?.value ?? '';
365
+ this._isSearching.set(true);
366
+ this._userSearchText.set(value);
367
+ this.emitDebouncedSearchText(value);
184
368
  }
369
+ /**
370
+ * Handles option selection from the dropdown.
371
+ * @param value The selected option's value
372
+ * @param hideOptions Whether to hide the dropdown after selection
373
+ */
185
374
  onValueChange(value, hideOptions = true) {
375
+ // When allowFreeText is true and a null/undefined value is received (e.g., from selecting
376
+ // a non-existent option in the dropdown), use the typed text as the value instead of clearing
377
+ if ((value === null || value === undefined) && this._allowFreeTextInternal()) {
378
+ const typedText = this._userSearchText()?.trim();
379
+ if (typedText) {
380
+ this.onChangedHandler(typedText);
381
+ if (hideOptions) {
382
+ this.hideOptionVisibility();
383
+ this.focusInput();
384
+ }
385
+ this.selectedOption.emit(typedText);
386
+ return;
387
+ }
388
+ }
186
389
  if (this.multiple()) {
187
- this.onSelectValue(value);
390
+ this.handleSelectValue(value);
188
391
  this.onChangedHandler(this._chipList());
189
392
  if (this._chipList().some((x) => x === value)) {
190
- this.inputValue$.next('');
393
+ this._userSearchText.set('');
394
+ this._isSearching.set(false);
191
395
  }
396
+ return;
192
397
  }
193
- else {
194
- this.onChangedHandler(value);
195
- if (hideOptions) {
196
- this.hideOptionVisibility();
197
- }
198
- this.selectedOption.emit(value);
398
+ // Update _userSearchText to the selected option's label
399
+ // This enables processTextToFormValue to match correctly on blur
400
+ const selectedOption = this.selectOptions().find((x) => x.value === value);
401
+ if (selectedOption) {
402
+ this._userSearchText.set(selectedOption.label);
199
403
  }
200
- }
201
- checkInputValue() {
202
- const option = this.selectOptions().find((x) => x.label.toLowerCase() === this._inputValue()?.toLowerCase());
203
- if (!this.multiple()) {
204
- if (option?.value === this._value())
205
- return;
206
- if (option) {
207
- this.onChangedHandler(option.value ?? '');
208
- }
209
- else if (!this.syncFormWithText()) {
210
- this.onChangedHandler('');
211
- }
404
+ this.onChangedHandler(value);
405
+ if (hideOptions) {
406
+ this.hideOptionVisibility();
407
+ // Return focus to input after selection
408
+ this.focusInput();
212
409
  }
410
+ this.selectedOption.emit(value);
213
411
  }
214
- writeValue(val) {
215
- super.writeValue(val);
216
- this.setInputValue(true);
217
- if (Array.isArray(val)) {
218
- val.forEach((x) => {
219
- this.onSelectValue(x);
220
- });
412
+ /**
413
+ * Handles keydown events on the input element for accessibility.
414
+ * @param event The keyboard event
415
+ */
416
+ onInputKeydown(event) {
417
+ switch (event.key) {
418
+ case 'ArrowDown':
419
+ // Open dropdown if closed, or let option-list handle navigation
420
+ if (!this._showOptions()) {
421
+ event.preventDefault();
422
+ this.showOptionVisibility();
423
+ }
424
+ break;
425
+ case 'ArrowUp':
426
+ // Open dropdown if closed
427
+ if (!this._showOptions()) {
428
+ event.preventDefault();
429
+ this.showOptionVisibility();
430
+ }
431
+ break;
432
+ case 'Escape':
433
+ // Close dropdown and keep focus on input
434
+ if (this._showOptions()) {
435
+ event.preventDefault();
436
+ this.onEscapePressed();
437
+ }
438
+ break;
439
+ case 'Enter':
440
+ // When allowFreeText is true and dropdown is open, handle Enter specially
441
+ if (this._showOptions() && this._allowFreeTextInternal()) {
442
+ // Check if there are any filtered options
443
+ const filteredOptions = this._filteredOptions();
444
+ if (filteredOptions.length === 0) {
445
+ // No options to select - use the typed text as the value
446
+ event.preventDefault();
447
+ this.processTextToFormValue(this._userSearchText(), {
448
+ exitSearchMode: true,
449
+ updateOnMatch: true,
450
+ clearSearchText: false,
451
+ });
452
+ this.hideOptionVisibility();
453
+ }
454
+ // If there are filtered options, let option-list handle the selection
455
+ }
456
+ break;
221
457
  }
222
458
  }
223
- onBlurInput(event) {
224
- if (event?.relatedTarget?.id !== this.optionList()?.optionListContainer()?.nativeElement?.id)
225
- this.onBlurHandler();
459
+ /**
460
+ * Handles Escape key press from option list.
461
+ * Closes dropdown and returns focus to input.
462
+ */
463
+ onEscapePressed() {
464
+ this.hideOptionVisibility();
465
+ this.focusInput();
226
466
  }
227
- onBlurHandler() {
228
- setTimeout(() => {
229
- this.hideOptionVisibility();
230
- if (!this._inputValue()?.length && !this.emitOnly() && !this.multiple()) {
231
- this._ngControl()?.control?.patchValue(null);
232
- }
233
- super.onBlurHandler();
234
- }, 100);
467
+ /**
468
+ * Handles Tab key press from option list.
469
+ * Closes dropdown and allows natural tab navigation.
470
+ */
471
+ onTabPressed(_event) {
472
+ // Close the dropdown, tab will naturally move focus
473
+ this.hideOptionVisibility();
474
+ // Process any pending input value
475
+ this.processTextToFormValue(this._userSearchText(), {
476
+ exitSearchMode: true,
477
+ updateOnMatch: true,
478
+ clearSearchText: true,
479
+ });
480
+ }
481
+ /**
482
+ * Sets focus to the input element.
483
+ */
484
+ focusInput() {
485
+ const inputEl = this.selectInput()?.nativeElement;
486
+ if (!inputEl)
487
+ return;
488
+ inputEl.focus();
235
489
  }
490
+ /**
491
+ * Handles input blur event.
492
+ * @param event The focus event
493
+ */
494
+ onBlurInput(event) {
495
+ const relatedTarget = event.relatedTarget;
496
+ const optionListId = this.optionList()?.optionListContainer()?.nativeElement?.id;
497
+ if (relatedTarget?.id === optionListId)
498
+ return;
499
+ this.onBlurHandler();
500
+ }
501
+ /**
502
+ * Handles blur event on the option list.
503
+ * @param event The blur event (truthy if should hide)
504
+ */
236
505
  onBlurOptionList(event) {
237
- if (event)
238
- this.hideOptionVisibility();
506
+ if (!event)
507
+ return;
508
+ this.hideOptionVisibility();
239
509
  }
240
- setInputValue(resetOnMiss = false) {
241
- this._inputValue.set(this.selectOptions().find((x) => x.value === this._value())?.label ?? (resetOnMiss ? '' : this._inputValue()));
242
- if (!this.syncFormWithText())
243
- this.inputValue$.next(this._inputValue() ?? '');
510
+ /**
511
+ * Gets the display description for a chip value.
512
+ * @param chipValue The chip's value
513
+ * @returns The chip's display label
514
+ */
515
+ getDescription(chipValue) {
516
+ const option = this.selectOptions().find((x) => x.value?.toString() === chipValue?.toString());
517
+ return option?.label?.toString() ?? '';
244
518
  }
245
- getDescription(chip) {
246
- const valueChip = this.selectOptions().find((x) => x.value === chip);
247
- return valueChip ? valueChip.label.toString() : '';
519
+ getOptionByValue(value) {
520
+ return this.selectOptions().find((x) => x.value?.toString() === value?.toString());
248
521
  }
249
- onSelectValue(value) {
250
- const newChip = this.selectOptions().find((x) => x.value === value);
251
- if (newChip && !this._chipList().some((x) => x === newChip?.value)) {
252
- this.createChipList(newChip);
253
- this._selectedOptions.update((list) => [...list, newChip]);
254
- }
522
+ getOptionIndex(option) {
523
+ return this.selectOptions().findIndex((x) => x.value === option.value);
255
524
  }
256
525
  /**
257
- * remove chip from chips list
258
- * @param chipValue chip to delete
526
+ * Removes a chip from the selection (multiple mode).
527
+ * @param chipValue The chip value to remove
259
528
  */
260
529
  deleteChip(chipValue) {
261
530
  const stringChipValue = chipValue?.toString();
262
- const i = this._chipList()?.findIndex((x) => x.toString() === stringChipValue);
263
- if (i >= 0) {
264
- const currentList = this._chipList();
265
- if (Array.isArray(currentList) && currentList.length > 0) {
266
- this._chipList.update((list) => list.filter((_, index) => index !== i));
267
- this.onChangedHandler(this._chipList());
531
+ const index = this._chipList().findIndex((x) => x.toString() === stringChipValue);
532
+ if (index < 0)
533
+ return;
534
+ this._chipList.update((list) => list.filter((_, i) => i !== index));
535
+ this.onChangedHandler(this._chipList());
536
+ }
537
+ // ============================================
538
+ // PROTECTED METHODS - Internal logic, accessible to subclasses
539
+ // ============================================
540
+ /**
541
+ * Filters options based on input text.
542
+ * @param value The search text
543
+ * @returns Filtered options
544
+ */
545
+ filterOptions(value) {
546
+ const options = this.selectOptions();
547
+ const trimmedValue = value?.trim();
548
+ return this.internalFilterOptions() && trimmedValue
549
+ ? options.filter((x) => x.label.toLowerCase().includes(trimmedValue.toLowerCase()))
550
+ : options;
551
+ }
552
+ // ============================================
553
+ // PRIVATE METHODS - Internal implementation
554
+ // ============================================
555
+ /**
556
+ * Core method that processes text input and updates form value accordingly.
557
+ *
558
+ * Matching logic:
559
+ * - If text matches an option label (case-insensitive, trimmed) and autoSelectOnExactMatch is true, select that option
560
+ * - If no match and allowFreeText is true, use the typed text as value
561
+ * - If no match and allowFreeText is false, clear the value
562
+ *
563
+ * @param text The text to process
564
+ * @param options Configuration options:
565
+ * - exitSearchMode: If true, uses onChangedHandler which exits search mode. If false, stays in search mode.
566
+ * - updateOnMatch: If true, updates form when match found. If false, only clears on no-match.
567
+ * - clearSearchText: If true, clears _userSearchText after processing.
568
+ */
569
+ processTextToFormValue(text, options) {
570
+ const searchText = text?.trim();
571
+ // Find matching option: exact match (case-insensitive, trimmed)
572
+ const matchingOption = this.findMatchingOption(text);
573
+ if (!this.multiple()) {
574
+ // If the found option is already selected, nothing to do except exit search mode
575
+ if (matchingOption?.value === this._value()) {
576
+ if (options.exitSearchMode) {
577
+ this._isSearching.set(false);
578
+ }
579
+ if (options.clearSearchText) {
580
+ this._userSearchText.set('');
581
+ }
582
+ return;
583
+ }
584
+ // Determine what action to take based on match status and settings
585
+ const shouldAutoSelect = matchingOption && this.autoSelectOnExactMatch() && options.updateOnMatch;
586
+ const shouldUseFreeText = this._allowFreeTextInternal() && searchText && options.updateOnMatch;
587
+ // Clear logic differs between typing and blur:
588
+ // - On blur (exitSearchMode=true): clear when input is empty (regardless of allowFreeText setting)
589
+ // - On blur: also clear when no valid selection and free text not allowed
590
+ // - During typing (exitSearchMode=false): only clear when updateOnMatch is true and text doesn't match
591
+ const shouldClearOnBlurEmpty = options.exitSearchMode && !searchText;
592
+ const shouldClearOnBlurNoMatch = options.exitSearchMode && !this._allowFreeTextInternal() && (!matchingOption || !this.autoSelectOnExactMatch());
593
+ const shouldClearWhileTyping = !options.exitSearchMode && options.updateOnMatch && !matchingOption && !this._allowFreeTextInternal();
594
+ if (shouldAutoSelect) {
595
+ // Auto-select the matching option
596
+ if (options.exitSearchMode) {
597
+ this.onChangedHandler(matchingOption.value ?? '');
598
+ }
599
+ else {
600
+ this.onValueChange(matchingOption.value ?? '', false);
601
+ }
602
+ }
603
+ else if (shouldUseFreeText) {
604
+ // Free text allowed: use the typed text as value
605
+ if (options.exitSearchMode) {
606
+ this.onChangedHandler(searchText);
607
+ }
608
+ else {
609
+ this.onValueChange(searchText, false);
610
+ }
611
+ }
612
+ else if (shouldClearOnBlurEmpty || shouldClearOnBlurNoMatch) {
613
+ // On blur with empty input or no valid selection: clear the value to null
614
+ this.onChangedHandler(null);
615
+ }
616
+ else if (shouldClearWhileTyping) {
617
+ // While typing, text doesn't match any option: clear the value but stay in search mode
618
+ this.updateValueWithoutExitingSearchMode('');
268
619
  }
269
620
  }
621
+ if (options.clearSearchText) {
622
+ this._userSearchText.set('');
623
+ }
270
624
  }
271
- createChipList(chip) {
272
- if (chip) {
273
- this._chipList.update((list) => [...list, chip.value]);
625
+ /**
626
+ * Handles keyboard events on the input element.
627
+ */
628
+ handleInputKeydown(e, inputElement) {
629
+ if (!(this.multiple() && this._chipList().length > 0 && !this._inputValue()?.length && e.key === 'Backspace')) {
630
+ return;
631
+ }
632
+ e.preventDefault();
633
+ const chipContainerEl = this.chipContainer()?.nativeElement;
634
+ if (chipContainerEl) {
635
+ const chips = chipContainerEl.querySelectorAll('.chip button.btn-chip');
636
+ if (chips.length > 0) {
637
+ const lastChip = chips[chips.length - 1];
638
+ lastChip.focus();
639
+ lastChip.addEventListener('keydown', (event) => {
640
+ if (event.key === 'Backspace') {
641
+ event.preventDefault();
642
+ this.deleteChip(this._chipList()[this._chipList().length - 1]);
643
+ inputElement.focus();
644
+ return;
645
+ }
646
+ event.preventDefault();
647
+ });
648
+ }
649
+ }
650
+ }
651
+ /**
652
+ * Handles changes to the select options input.
653
+ */
654
+ handleOptionsChange() {
655
+ const value = this._value();
656
+ if (this.multiple() && Array.isArray(value) && value.length > 0) {
657
+ for (const valueElement of value) {
658
+ this.handleSelectValue(valueElement);
659
+ }
660
+ }
661
+ // For single mode: _inputValue is computed, so it automatically updates
662
+ }
663
+ /**
664
+ * Handles form value changes from external sources.
665
+ */
666
+ handleFormValueChange(value) {
667
+ if (!(this.multiple() && Array.isArray(value))) {
668
+ return;
669
+ }
670
+ this._chipList.set([]);
671
+ this._selectedOptions.set([]);
672
+ value.forEach((x) => {
673
+ this.handleSelectValue(x);
674
+ });
675
+ // Note: Don't clear _userSearchText here - it's managed by processTextToFormValue
676
+ // which runs when options are hidden and needs _userSearchText for matching.
677
+ }
678
+ /**
679
+ * Handles selecting a value (adding to chip list in multiple mode).
680
+ */
681
+ handleSelectValue(value) {
682
+ const option = this.selectOptions().find((x) => x.value === value);
683
+ if (option && !this._chipList().some((x) => x === option.value)) {
684
+ this._chipList.update((list) => [...list, option.value]);
685
+ this._selectedOptions.update((list) => [...list, option]);
686
+ }
687
+ }
688
+ /**
689
+ * Emits search text change after debounce.
690
+ * When `updateValueOnType` is true, also updates the form value using the same
691
+ * matching logic as processTextToFormValue (auto-select matching options, or use free text).
692
+ */
693
+ emitDebouncedSearchText(value) {
694
+ if (this._searchDebounceTimer) {
695
+ clearTimeout(this._searchDebounceTimer);
696
+ }
697
+ this._searchDebounceTimer = setTimeout(() => {
698
+ if (this._isDestroyed) {
699
+ return;
700
+ }
701
+ if (value === this._lastEmittedSearchText) {
702
+ return;
703
+ }
704
+ this._lastEmittedSearchText = value;
705
+ this.searchTextChange.emit(value || '');
706
+ // Update form value based on what the user typed
707
+ // - When updateValueOnType is true: update on both match and no-match
708
+ // - When updateValueOnType is false: only clear the value when text doesn't match
709
+ this.processTextToFormValue(value, {
710
+ exitSearchMode: false,
711
+ updateOnMatch: this.updateValueOnType(),
712
+ clearSearchText: false,
713
+ });
714
+ }, this.searchTextDebounce());
715
+ }
716
+ /**
717
+ * Updates the form value and internal _value signal without exiting search mode.
718
+ * This is used when clearing the value during typing - we want to update the form
719
+ * but keep the user in search mode so they can continue typing.
720
+ */
721
+ updateValueWithoutExitingSearchMode(value) {
722
+ this._value.set(value);
723
+ if (this.onChange) {
724
+ this.onChange(value);
274
725
  }
275
726
  }
276
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: QuangAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
277
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.10", type: QuangAutocompleteComponent, isStandalone: true, selector: "quang-autocomplete", inputs: { syncFormWithText: { classPropertyName: "syncFormWithText", publicName: "syncFormWithText", isSignal: true, isRequired: false, transformFunction: null }, optionListMaxHeight: { classPropertyName: "optionListMaxHeight", publicName: "optionListMaxHeight", isSignal: true, isRequired: false, transformFunction: null }, selectOptions: { classPropertyName: "selectOptions", publicName: "selectOptions", isSignal: true, isRequired: true, transformFunction: null }, translateValue: { classPropertyName: "translateValue", publicName: "translateValue", isSignal: true, isRequired: false, transformFunction: null }, scrollBehaviorOnOpen: { classPropertyName: "scrollBehaviorOnOpen", publicName: "scrollBehaviorOnOpen", isSignal: true, isRequired: false, transformFunction: null }, emitOnly: { classPropertyName: "emitOnly", publicName: "emitOnly", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, chipMaxLength: { classPropertyName: "chipMaxLength", publicName: "chipMaxLength", isSignal: true, isRequired: false, transformFunction: null }, multiSelectDisplayMode: { classPropertyName: "multiSelectDisplayMode", publicName: "multiSelectDisplayMode", isSignal: true, isRequired: false, transformFunction: null }, searchTextDebounce: { classPropertyName: "searchTextDebounce", publicName: "searchTextDebounce", isSignal: true, isRequired: false, transformFunction: null }, internalFilterOptions: { classPropertyName: "internalFilterOptions", publicName: "internalFilterOptions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedOption: "selectedOption", searchTextChange: "searchTextChange" }, providers: [
727
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: QuangAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
728
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: QuangAutocompleteComponent, isStandalone: true, selector: "quang-autocomplete", inputs: { selectOptions: { classPropertyName: "selectOptions", publicName: "selectOptions", isSignal: true, isRequired: true, transformFunction: null }, allowFreeText: { classPropertyName: "allowFreeText", publicName: "allowFreeText", isSignal: true, isRequired: false, transformFunction: null }, autoSelectOnExactMatch: { classPropertyName: "autoSelectOnExactMatch", publicName: "autoSelectOnExactMatch", isSignal: true, isRequired: false, transformFunction: null }, updateValueOnType: { classPropertyName: "updateValueOnType", publicName: "updateValueOnType", isSignal: true, isRequired: false, transformFunction: null }, syncFormWithText: { classPropertyName: "syncFormWithText", publicName: "syncFormWithText", isSignal: true, isRequired: false, transformFunction: null }, optionListMaxHeight: { classPropertyName: "optionListMaxHeight", publicName: "optionListMaxHeight", isSignal: true, isRequired: false, transformFunction: null }, translateValue: { classPropertyName: "translateValue", publicName: "translateValue", isSignal: true, isRequired: false, transformFunction: null }, scrollBehaviorOnOpen: { classPropertyName: "scrollBehaviorOnOpen", publicName: "scrollBehaviorOnOpen", isSignal: true, isRequired: false, transformFunction: null }, emitOnly: { classPropertyName: "emitOnly", publicName: "emitOnly", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, chipMaxLength: { classPropertyName: "chipMaxLength", publicName: "chipMaxLength", isSignal: true, isRequired: false, transformFunction: null }, multiSelectDisplayMode: { classPropertyName: "multiSelectDisplayMode", publicName: "multiSelectDisplayMode", isSignal: true, isRequired: false, transformFunction: null }, chipsPosition: { classPropertyName: "chipsPosition", publicName: "chipsPosition", isSignal: true, isRequired: false, transformFunction: null }, searchTextDebounce: { classPropertyName: "searchTextDebounce", publicName: "searchTextDebounce", isSignal: true, isRequired: false, transformFunction: null }, internalFilterOptions: { classPropertyName: "internalFilterOptions", publicName: "internalFilterOptions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedOption: "selectedOption", searchTextChange: "searchTextChange" }, providers: [
278
729
  {
279
730
  provide: NG_VALUE_ACCESSOR,
280
731
  useExisting: forwardRef(() => QuangAutocompleteComponent),
@@ -284,11 +735,11 @@ class QuangAutocompleteComponent extends QuangBaseComponent {
284
735
  provide: QuangOptionListComponent,
285
736
  multi: false,
286
737
  },
287
- ], viewQueries: [{ propertyName: "optionList", first: true, predicate: ["optionList"], descendants: true, isSignal: true }, { propertyName: "selectInput", first: true, predicate: ["selectInput"], descendants: true, isSignal: true }, { propertyName: "chipContainer", first: true, predicate: ["chipContainer"], descendants: true, isSignal: true }, { propertyName: "autocompleteContainer", first: true, predicate: ["autocompleteContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div\n [ngStyle]=\"{ '--chip-max-length': chipMaxLength() ? chipMaxLength() + 'ch' : 'none' }\"\n #autocompleteContainer\n class=\"autocomplete-container\"\n>\n @if (componentLabel()) {\n <label\n [htmlFor]=\"componentId()\"\n class=\"form-label d-flex gap-2\"\n >\n <div>\n <span>{{ componentLabel() | transloco }}</span>\n <span [hidden]=\"!_isRequired()\">*</span>\n </div>\n @if (helpMessage() && helpMessageTooltip()) {\n <div [quangTooltip]=\"helpMessage() | transloco\">\n <ng-content select=\"[help-icon]\" />\n </div>\n }\n </label>\n }\n <div\n [ngClass]=\"multiSelectDisplayMode() === 'horizontal' ? 'horizontal form-control' : ''\"\n #chipContainer\n class=\"container-wrap\"\n >\n @if (multiple() && _chipList().length > 0) {\n @for (chip of _chipList(); track chip) {\n @if (getDescription(chip)) {\n <div\n [quangTooltip]=\"chipMaxLength() ? getDescription(chip) : ''\"\n class=\"chip chip-hover\"\n >\n <p [ngClass]=\"{ 'm-0': isReadonly() || _isDisabled() }\">\n {{ getDescription(chip) }}\n </p>\n @if (!isReadonly() && !_isDisabled()) {\n <button\n [tabIndex]=\"$index + 1\"\n (click)=\"deleteChip(chip)\"\n class=\"btn btn-chip\"\n type=\"button\"\n >\n <svg\n class=\"ionicon\"\n fill=\"currentColor\"\n height=\"24\"\n viewBox=\"0 0 512 512\"\n width=\"24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M368 368L144 144M368 144L144 368\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"32\"\n />\n </svg>\n </button>\n }\n </div>\n }\n }\n }\n\n <input\n [attr.required]=\"getIsRequiredControl()\"\n [class.form-control]=\"multiSelectDisplayMode() !== 'horizontal'\"\n [class.is-invalid]=\"_showErrors()\"\n [class.is-valid]=\"_showSuccess()\"\n [disabled]=\"_isDisabled() || isReadonly()\"\n [id]=\"componentId()\"\n [ngClass]=\"componentClass()\"\n [placeholder]=\"componentPlaceholder() | transloco\"\n [tabIndex]=\"componentTabIndex()\"\n [value]=\"_inputValue()\"\n (blur)=\"onBlurInput($event)\"\n (input)=\"onChangeInput($event)\"\n (mousedown)=\"showOptionVisibility()\"\n #selectInput\n autocomplete=\"off\"\n type=\"text\"\n />\n </div>\n @if (_showOptions()) {\n <quang-option-list\n [_isDisabled]=\"_isDisabled()\"\n [_value]=\"_value()\"\n [componentClass]=\"componentClass()\"\n [componentLabel]=\"componentLabel()\"\n [componentTabIndex]=\"componentTabIndex()\"\n [nullOption]=\"false\"\n [optionListMaxHeight]=\"optionListMaxHeight()\"\n [parentID]=\"componentId()\"\n [parentType]=\"ParentType\"\n [scrollBehaviorOnOpen]=\"scrollBehaviorOnOpen()\"\n [selectButtonRef]=\"autocompleteContainer\"\n [selectOptions]=\"_filteredOptions()\"\n [translateValue]=\"translateValue()\"\n (blurHandler)=\"onBlurOptionList($event)\"\n (changedHandler)=\"onValueChange($event)\"\n #optionList\n selectionMode=\"single\"\n />\n }\n <div class=\"valid-feedback\">\n {{ successMessage() | transloco }}\n </div>\n <div class=\"invalid-feedback\">\n {{ _currentErrorMessage() | transloco: _currentErrorMessageExtraData() }}\n </div>\n @if (helpMessage() && !helpMessageTooltip()) {\n <small\n [hidden]=\"_showSuccess() || _showErrors()\"\n aria-live=\"assertive\"\n class=\"form-text text-muted\"\n >\n {{ helpMessage() | transloco }}\n </small>\n }\n</div>\n", styles: [":host{display:block;--chip-max-length: none}.autocomplete-container{margin-bottom:1rem;position:relative}.chip:has(.btn-chip:disabled):hover{filter:unset;cursor:unset}.container-wrap{display:flex;flex-wrap:wrap;gap:.5rem}.container-wrap.horizontal{display:flex}.container-wrap.horizontal .chip-container{max-width:70%;margin-bottom:0;margin-left:.5rem;flex-wrap:nowrap;white-space:nowrap;overflow-x:auto;position:absolute;align-items:center}.container-wrap.horizontal .chip-container .chip{white-space:nowrap}.container-wrap.horizontal input{min-width:30%;flex:1 1 0;width:auto;border:none}.container-wrap.horizontal input:focus-visible{outline:none}.chip{display:flex;justify-content:space-between;align-items:center;padding:.25rem .5rem;border-radius:16px;color:var(--bs-btn-color);background-color:rgba(var(--bs-primary-rgb),.1);border-width:1px;border-style:solid;border-color:var(--bs-primary-border-subtle);height:2rem}.chip p{margin:0;max-width:var(--chip-max-length);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.chip .btn-chip{text-align:end;padding:0;min-width:unset}.chip .btn-chip:hover{opacity:80%}.chip .btn-chip:active{border-color:transparent}.chip .btn-chip svg{color:var(--bs-primary);vertical-align:sub}.chip:has(.btn-chip:focus-visible){border-width:2px;filter:brightness(80%)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: QuangOptionListComponent, selector: "quang-option-list", inputs: ["selectionMode", "optionListMaxHeight", "selectOptions", "selectButtonRef", "_value", "_isDisabled", "componentClass", "componentLabel", "componentTabIndex", "translateValue", "nullOption", "scrollBehaviorOnOpen", "parentType", "parentID"], outputs: ["changedHandler", "blurHandler"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: QuangTooltipDirective, selector: "[quangTooltip]", inputs: ["quangTooltip", "showMethod"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
738
+ ], viewQueries: [{ propertyName: "optionList", first: true, predicate: ["optionList"], descendants: true, isSignal: true }, { propertyName: "selectInput", first: true, predicate: ["selectInput"], descendants: true, isSignal: true }, { propertyName: "chipContainer", first: true, predicate: ["chipContainer"], descendants: true, isSignal: true }, { propertyName: "autocompleteContainer", first: true, predicate: ["autocompleteContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div\n [ngStyle]=\"{ '--chip-max-length': chipMaxLength() ? chipMaxLength() + 'ch' : 'none' }\"\n #autocompleteContainer\n class=\"autocomplete-container\"\n>\n @if (componentLabel()) {\n <label\n [htmlFor]=\"componentId()\"\n class=\"form-label d-flex gap-2\"\n >\n <div>\n <span>{{ componentLabel() | transloco }}</span>\n <span [hidden]=\"!_isRequired()\">*</span>\n </div>\n @if (helpMessage() && helpMessageTooltip()) {\n <div [quangTooltip]=\"helpMessage() | transloco\">\n <ng-content select=\"[help-icon]\" />\n </div>\n }\n </label>\n }\n <div\n [ngClass]=\"{\n horizontal: multiSelectDisplayMode() === 'horizontal',\n 'form-control': multiSelectDisplayMode() === 'horizontal',\n 'chips-bottom': chipsPosition() === 'bottom',\n }\"\n #chipContainer\n class=\"container-wrap\"\n >\n @if (multiple() && _chipList().length > 0 && chipsPosition() === 'top') {\n <ng-container *ngTemplateOutlet=\"chipsTemplate\" />\n }\n\n <input\n [attr.aria-activedescendant]=\"_showOptions() ? optionList()?.getActiveDescendantId() : null\"\n [attr.aria-controls]=\"_showOptions() ? 'optionList' : null\"\n [attr.aria-expanded]=\"_showOptions()\"\n [attr.required]=\"getIsRequiredControl()\"\n [class.form-control]=\"multiSelectDisplayMode() !== 'horizontal'\"\n [class.is-invalid]=\"_showErrors()\"\n [class.is-valid]=\"_showSuccess()\"\n [disabled]=\"_isDisabled() || isReadonly()\"\n [id]=\"componentId()\"\n [ngClass]=\"componentClass()\"\n [placeholder]=\"componentPlaceholder() | transloco\"\n [tabIndex]=\"componentTabIndex()\"\n [value]=\"_inputValue()\"\n (blur)=\"onBlurInput($event)\"\n (input)=\"onChangeInput($event)\"\n (keydown)=\"onInputKeydown($event)\"\n (mousedown)=\"showOptionVisibility()\"\n #selectInput\n aria-autocomplete=\"list\"\n aria-haspopup=\"listbox\"\n autocomplete=\"off\"\n role=\"combobox\"\n type=\"text\"\n />\n\n @if (multiple() && _chipList().length > 0 && chipsPosition() === 'bottom') {\n <ng-container *ngTemplateOutlet=\"chipsTemplate\" />\n }\n </div>\n @if (_showOptions()) {\n <quang-option-list\n [_isDisabled]=\"_isDisabled()\"\n [_value]=\"_highlightedValue()\"\n [componentClass]=\"componentClass()\"\n [componentLabel]=\"componentLabel()\"\n [componentTabIndex]=\"componentTabIndex()\"\n [nullOption]=\"false\"\n [optionListMaxHeight]=\"optionListMaxHeight()\"\n [parentID]=\"componentId()\"\n [parentType]=\"ParentType\"\n [scrollBehaviorOnOpen]=\"scrollBehaviorOnOpen()\"\n [selectButtonRef]=\"autocompleteContainer\"\n [selectOptions]=\"_filteredOptions()\"\n [translateValue]=\"translateValue()\"\n (blurHandler)=\"onBlurOptionList($event)\"\n (changedHandler)=\"onValueChange($event)\"\n (escapePressed)=\"onEscapePressed()\"\n (tabPressed)=\"onTabPressed($event)\"\n #optionList\n selectionMode=\"single\"\n />\n }\n <div\n [class.d-block]=\"_showSuccess()\"\n class=\"valid-feedback\"\n >\n {{ successMessage() | transloco }}\n </div>\n <div\n [class.d-block]=\"_showErrors()\"\n class=\"invalid-feedback\"\n >\n {{ _currentErrorMessage() | transloco: _currentErrorMessageExtraData() }}\n </div>\n @if (helpMessage() && !helpMessageTooltip()) {\n <small\n [hidden]=\"_showSuccess() || _showErrors()\"\n aria-live=\"assertive\"\n class=\"form-text text-muted\"\n >\n {{ helpMessage() | transloco }}\n </small>\n }\n</div>\n\n<!-- Chips template for reuse in top/bottom positions -->\n<ng-template #chipsTemplate>\n <div class=\"chips-container\">\n @for (chip of _chipList(); track chip) {\n @if (getDescription(chip)) {\n <div\n [ngClass]=\"{ 'chip-truncate': chipMaxLength() }\"\n [quangTooltip]=\"chipMaxLength() ? getDescription(chip) : ''\"\n class=\"chip chip-hover\"\n >\n <p [ngClass]=\"{ 'm-0': isReadonly() || _isDisabled() }\">\n @if (getOptionByValue(chip); as opt) {\n @if (opt.renderer) {\n <ng-container\n [ngTemplateOutlet]=\"opt.renderer\"\n [ngTemplateOutletContext]=\"{ $implicit: opt, selected: true, index: getOptionIndex(opt) }\"\n ></ng-container>\n } @else {\n {{ getDescription(chip) }}\n }\n } @else {\n {{ getDescription(chip) }}\n }\n </p>\n @if (!isReadonly() && !_isDisabled()) {\n <button\n [tabIndex]=\"$index + 1\"\n (click)=\"deleteChip(chip)\"\n class=\"btn btn-chip\"\n type=\"button\"\n >\n <svg\n class=\"ionicon\"\n fill=\"currentColor\"\n height=\"24\"\n viewBox=\"0 0 512 512\"\n width=\"24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M368 368L144 144M368 144L144 368\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"32\"\n />\n </svg>\n </button>\n }\n </div>\n }\n }\n </div>\n</ng-template>\n", styles: [":host{display:block;--chip-max-length: none}.autocomplete-container{margin-bottom:1rem;position:relative}.chip:has(.btn-chip:disabled):hover{filter:unset;cursor:unset}.container-wrap,.container-wrap .chips-container{display:flex;flex-wrap:wrap;gap:.5rem}.container-wrap.chips-bottom{flex-direction:column}.container-wrap.chips-bottom input{order:-1}.container-wrap.horizontal{display:flex}.container-wrap.horizontal .chip-container{max-width:70%;margin-bottom:0;margin-left:.5rem;flex-wrap:nowrap;white-space:nowrap;overflow-x:auto;position:absolute;align-items:center}.container-wrap.horizontal .chip-container .chip{white-space:nowrap}.container-wrap.horizontal input{min-width:30%;flex:1 1 0;width:auto;border:none}.container-wrap.horizontal input:focus-visible{outline:none}.chip{display:flex;justify-content:space-between;align-items:center;gap:.25rem;padding:.25rem .5rem;border-radius:16px;color:var(--bs-btn-color);background-color:rgba(var(--bs-primary-rgb),.1);border-width:1px;border-style:solid;border-color:var(--bs-primary-border-subtle);min-height:2rem;max-width:100%}.chip p{margin:0;max-width:var(--chip-max-length);overflow:hidden;overflow-wrap:break-word;word-break:break-word}.chip.chip-truncate p{white-space:nowrap;text-overflow:ellipsis}.chip .btn-chip{text-align:end;padding:0;min-width:unset}.chip .btn-chip:hover{opacity:80%}.chip .btn-chip:active{border-color:transparent}.chip .btn-chip svg{color:var(--bs-primary);vertical-align:sub}.chip:has(.btn-chip:focus-visible){border-width:2px;filter:brightness(80%)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: QuangOptionListComponent, selector: "quang-option-list", inputs: ["selectionMode", "optionListMaxHeight", "selectOptions", "selectButtonRef", "_value", "_isDisabled", "componentClass", "componentLabel", "componentTabIndex", "translateValue", "nullOption", "scrollBehaviorOnOpen", "parentType", "parentID"], outputs: ["changedHandler", "blurHandler", "escapePressed", "tabPressed"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: QuangTooltipDirective, selector: "[quangTooltip]", inputs: ["quangTooltip", "showMethod"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
288
739
  }
289
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: QuangAutocompleteComponent, decorators: [{
740
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: QuangAutocompleteComponent, decorators: [{
290
741
  type: Component,
291
- args: [{ selector: 'quang-autocomplete', imports: [TranslocoPipe, NgClass, QuangOptionListComponent, NgStyle, QuangTooltipDirective], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
742
+ args: [{ selector: 'quang-autocomplete', imports: [TranslocoPipe, NgClass, NgTemplateOutlet, QuangOptionListComponent, NgStyle, QuangTooltipDirective], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
292
743
  {
293
744
  provide: NG_VALUE_ACCESSOR,
294
745
  useExisting: forwardRef(() => QuangAutocompleteComponent),
@@ -298,8 +749,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImpo
298
749
  provide: QuangOptionListComponent,
299
750
  multi: false,
300
751
  },
301
- ], template: "<div\n [ngStyle]=\"{ '--chip-max-length': chipMaxLength() ? chipMaxLength() + 'ch' : 'none' }\"\n #autocompleteContainer\n class=\"autocomplete-container\"\n>\n @if (componentLabel()) {\n <label\n [htmlFor]=\"componentId()\"\n class=\"form-label d-flex gap-2\"\n >\n <div>\n <span>{{ componentLabel() | transloco }}</span>\n <span [hidden]=\"!_isRequired()\">*</span>\n </div>\n @if (helpMessage() && helpMessageTooltip()) {\n <div [quangTooltip]=\"helpMessage() | transloco\">\n <ng-content select=\"[help-icon]\" />\n </div>\n }\n </label>\n }\n <div\n [ngClass]=\"multiSelectDisplayMode() === 'horizontal' ? 'horizontal form-control' : ''\"\n #chipContainer\n class=\"container-wrap\"\n >\n @if (multiple() && _chipList().length > 0) {\n @for (chip of _chipList(); track chip) {\n @if (getDescription(chip)) {\n <div\n [quangTooltip]=\"chipMaxLength() ? getDescription(chip) : ''\"\n class=\"chip chip-hover\"\n >\n <p [ngClass]=\"{ 'm-0': isReadonly() || _isDisabled() }\">\n {{ getDescription(chip) }}\n </p>\n @if (!isReadonly() && !_isDisabled()) {\n <button\n [tabIndex]=\"$index + 1\"\n (click)=\"deleteChip(chip)\"\n class=\"btn btn-chip\"\n type=\"button\"\n >\n <svg\n class=\"ionicon\"\n fill=\"currentColor\"\n height=\"24\"\n viewBox=\"0 0 512 512\"\n width=\"24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M368 368L144 144M368 144L144 368\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"32\"\n />\n </svg>\n </button>\n }\n </div>\n }\n }\n }\n\n <input\n [attr.required]=\"getIsRequiredControl()\"\n [class.form-control]=\"multiSelectDisplayMode() !== 'horizontal'\"\n [class.is-invalid]=\"_showErrors()\"\n [class.is-valid]=\"_showSuccess()\"\n [disabled]=\"_isDisabled() || isReadonly()\"\n [id]=\"componentId()\"\n [ngClass]=\"componentClass()\"\n [placeholder]=\"componentPlaceholder() | transloco\"\n [tabIndex]=\"componentTabIndex()\"\n [value]=\"_inputValue()\"\n (blur)=\"onBlurInput($event)\"\n (input)=\"onChangeInput($event)\"\n (mousedown)=\"showOptionVisibility()\"\n #selectInput\n autocomplete=\"off\"\n type=\"text\"\n />\n </div>\n @if (_showOptions()) {\n <quang-option-list\n [_isDisabled]=\"_isDisabled()\"\n [_value]=\"_value()\"\n [componentClass]=\"componentClass()\"\n [componentLabel]=\"componentLabel()\"\n [componentTabIndex]=\"componentTabIndex()\"\n [nullOption]=\"false\"\n [optionListMaxHeight]=\"optionListMaxHeight()\"\n [parentID]=\"componentId()\"\n [parentType]=\"ParentType\"\n [scrollBehaviorOnOpen]=\"scrollBehaviorOnOpen()\"\n [selectButtonRef]=\"autocompleteContainer\"\n [selectOptions]=\"_filteredOptions()\"\n [translateValue]=\"translateValue()\"\n (blurHandler)=\"onBlurOptionList($event)\"\n (changedHandler)=\"onValueChange($event)\"\n #optionList\n selectionMode=\"single\"\n />\n }\n <div class=\"valid-feedback\">\n {{ successMessage() | transloco }}\n </div>\n <div class=\"invalid-feedback\">\n {{ _currentErrorMessage() | transloco: _currentErrorMessageExtraData() }}\n </div>\n @if (helpMessage() && !helpMessageTooltip()) {\n <small\n [hidden]=\"_showSuccess() || _showErrors()\"\n aria-live=\"assertive\"\n class=\"form-text text-muted\"\n >\n {{ helpMessage() | transloco }}\n </small>\n }\n</div>\n", styles: [":host{display:block;--chip-max-length: none}.autocomplete-container{margin-bottom:1rem;position:relative}.chip:has(.btn-chip:disabled):hover{filter:unset;cursor:unset}.container-wrap{display:flex;flex-wrap:wrap;gap:.5rem}.container-wrap.horizontal{display:flex}.container-wrap.horizontal .chip-container{max-width:70%;margin-bottom:0;margin-left:.5rem;flex-wrap:nowrap;white-space:nowrap;overflow-x:auto;position:absolute;align-items:center}.container-wrap.horizontal .chip-container .chip{white-space:nowrap}.container-wrap.horizontal input{min-width:30%;flex:1 1 0;width:auto;border:none}.container-wrap.horizontal input:focus-visible{outline:none}.chip{display:flex;justify-content:space-between;align-items:center;padding:.25rem .5rem;border-radius:16px;color:var(--bs-btn-color);background-color:rgba(var(--bs-primary-rgb),.1);border-width:1px;border-style:solid;border-color:var(--bs-primary-border-subtle);height:2rem}.chip p{margin:0;max-width:var(--chip-max-length);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.chip .btn-chip{text-align:end;padding:0;min-width:unset}.chip .btn-chip:hover{opacity:80%}.chip .btn-chip:active{border-color:transparent}.chip .btn-chip svg{color:var(--bs-primary);vertical-align:sub}.chip:has(.btn-chip:focus-visible){border-width:2px;filter:brightness(80%)}\n"] }]
302
- }], ctorParameters: () => [], propDecorators: { syncFormWithText: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncFormWithText", required: false }] }], optionListMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionListMaxHeight", required: false }] }], selectOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectOptions", required: true }] }], translateValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "translateValue", required: false }] }], scrollBehaviorOnOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollBehaviorOnOpen", required: false }] }], emitOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "emitOnly", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], chipMaxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "chipMaxLength", required: false }] }], multiSelectDisplayMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiSelectDisplayMode", required: false }] }], optionList: [{ type: i0.ViewChild, args: ['optionList', { isSignal: true }] }], selectedOption: [{ type: i0.Output, args: ["selectedOption"] }], searchTextChange: [{ type: i0.Output, args: ["searchTextChange"] }], searchTextDebounce: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchTextDebounce", required: false }] }], internalFilterOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "internalFilterOptions", required: false }] }], selectInput: [{ type: i0.ViewChild, args: ['selectInput', { isSignal: true }] }], chipContainer: [{ type: i0.ViewChild, args: ['chipContainer', { isSignal: true }] }], autocompleteContainer: [{ type: i0.ViewChild, args: ['autocompleteContainer', { isSignal: true }] }] } });
752
+ ], template: "<div\n [ngStyle]=\"{ '--chip-max-length': chipMaxLength() ? chipMaxLength() + 'ch' : 'none' }\"\n #autocompleteContainer\n class=\"autocomplete-container\"\n>\n @if (componentLabel()) {\n <label\n [htmlFor]=\"componentId()\"\n class=\"form-label d-flex gap-2\"\n >\n <div>\n <span>{{ componentLabel() | transloco }}</span>\n <span [hidden]=\"!_isRequired()\">*</span>\n </div>\n @if (helpMessage() && helpMessageTooltip()) {\n <div [quangTooltip]=\"helpMessage() | transloco\">\n <ng-content select=\"[help-icon]\" />\n </div>\n }\n </label>\n }\n <div\n [ngClass]=\"{\n horizontal: multiSelectDisplayMode() === 'horizontal',\n 'form-control': multiSelectDisplayMode() === 'horizontal',\n 'chips-bottom': chipsPosition() === 'bottom',\n }\"\n #chipContainer\n class=\"container-wrap\"\n >\n @if (multiple() && _chipList().length > 0 && chipsPosition() === 'top') {\n <ng-container *ngTemplateOutlet=\"chipsTemplate\" />\n }\n\n <input\n [attr.aria-activedescendant]=\"_showOptions() ? optionList()?.getActiveDescendantId() : null\"\n [attr.aria-controls]=\"_showOptions() ? 'optionList' : null\"\n [attr.aria-expanded]=\"_showOptions()\"\n [attr.required]=\"getIsRequiredControl()\"\n [class.form-control]=\"multiSelectDisplayMode() !== 'horizontal'\"\n [class.is-invalid]=\"_showErrors()\"\n [class.is-valid]=\"_showSuccess()\"\n [disabled]=\"_isDisabled() || isReadonly()\"\n [id]=\"componentId()\"\n [ngClass]=\"componentClass()\"\n [placeholder]=\"componentPlaceholder() | transloco\"\n [tabIndex]=\"componentTabIndex()\"\n [value]=\"_inputValue()\"\n (blur)=\"onBlurInput($event)\"\n (input)=\"onChangeInput($event)\"\n (keydown)=\"onInputKeydown($event)\"\n (mousedown)=\"showOptionVisibility()\"\n #selectInput\n aria-autocomplete=\"list\"\n aria-haspopup=\"listbox\"\n autocomplete=\"off\"\n role=\"combobox\"\n type=\"text\"\n />\n\n @if (multiple() && _chipList().length > 0 && chipsPosition() === 'bottom') {\n <ng-container *ngTemplateOutlet=\"chipsTemplate\" />\n }\n </div>\n @if (_showOptions()) {\n <quang-option-list\n [_isDisabled]=\"_isDisabled()\"\n [_value]=\"_highlightedValue()\"\n [componentClass]=\"componentClass()\"\n [componentLabel]=\"componentLabel()\"\n [componentTabIndex]=\"componentTabIndex()\"\n [nullOption]=\"false\"\n [optionListMaxHeight]=\"optionListMaxHeight()\"\n [parentID]=\"componentId()\"\n [parentType]=\"ParentType\"\n [scrollBehaviorOnOpen]=\"scrollBehaviorOnOpen()\"\n [selectButtonRef]=\"autocompleteContainer\"\n [selectOptions]=\"_filteredOptions()\"\n [translateValue]=\"translateValue()\"\n (blurHandler)=\"onBlurOptionList($event)\"\n (changedHandler)=\"onValueChange($event)\"\n (escapePressed)=\"onEscapePressed()\"\n (tabPressed)=\"onTabPressed($event)\"\n #optionList\n selectionMode=\"single\"\n />\n }\n <div\n [class.d-block]=\"_showSuccess()\"\n class=\"valid-feedback\"\n >\n {{ successMessage() | transloco }}\n </div>\n <div\n [class.d-block]=\"_showErrors()\"\n class=\"invalid-feedback\"\n >\n {{ _currentErrorMessage() | transloco: _currentErrorMessageExtraData() }}\n </div>\n @if (helpMessage() && !helpMessageTooltip()) {\n <small\n [hidden]=\"_showSuccess() || _showErrors()\"\n aria-live=\"assertive\"\n class=\"form-text text-muted\"\n >\n {{ helpMessage() | transloco }}\n </small>\n }\n</div>\n\n<!-- Chips template for reuse in top/bottom positions -->\n<ng-template #chipsTemplate>\n <div class=\"chips-container\">\n @for (chip of _chipList(); track chip) {\n @if (getDescription(chip)) {\n <div\n [ngClass]=\"{ 'chip-truncate': chipMaxLength() }\"\n [quangTooltip]=\"chipMaxLength() ? getDescription(chip) : ''\"\n class=\"chip chip-hover\"\n >\n <p [ngClass]=\"{ 'm-0': isReadonly() || _isDisabled() }\">\n @if (getOptionByValue(chip); as opt) {\n @if (opt.renderer) {\n <ng-container\n [ngTemplateOutlet]=\"opt.renderer\"\n [ngTemplateOutletContext]=\"{ $implicit: opt, selected: true, index: getOptionIndex(opt) }\"\n ></ng-container>\n } @else {\n {{ getDescription(chip) }}\n }\n } @else {\n {{ getDescription(chip) }}\n }\n </p>\n @if (!isReadonly() && !_isDisabled()) {\n <button\n [tabIndex]=\"$index + 1\"\n (click)=\"deleteChip(chip)\"\n class=\"btn btn-chip\"\n type=\"button\"\n >\n <svg\n class=\"ionicon\"\n fill=\"currentColor\"\n height=\"24\"\n viewBox=\"0 0 512 512\"\n width=\"24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M368 368L144 144M368 144L144 368\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"32\"\n />\n </svg>\n </button>\n }\n </div>\n }\n }\n </div>\n</ng-template>\n", styles: [":host{display:block;--chip-max-length: none}.autocomplete-container{margin-bottom:1rem;position:relative}.chip:has(.btn-chip:disabled):hover{filter:unset;cursor:unset}.container-wrap,.container-wrap .chips-container{display:flex;flex-wrap:wrap;gap:.5rem}.container-wrap.chips-bottom{flex-direction:column}.container-wrap.chips-bottom input{order:-1}.container-wrap.horizontal{display:flex}.container-wrap.horizontal .chip-container{max-width:70%;margin-bottom:0;margin-left:.5rem;flex-wrap:nowrap;white-space:nowrap;overflow-x:auto;position:absolute;align-items:center}.container-wrap.horizontal .chip-container .chip{white-space:nowrap}.container-wrap.horizontal input{min-width:30%;flex:1 1 0;width:auto;border:none}.container-wrap.horizontal input:focus-visible{outline:none}.chip{display:flex;justify-content:space-between;align-items:center;gap:.25rem;padding:.25rem .5rem;border-radius:16px;color:var(--bs-btn-color);background-color:rgba(var(--bs-primary-rgb),.1);border-width:1px;border-style:solid;border-color:var(--bs-primary-border-subtle);min-height:2rem;max-width:100%}.chip p{margin:0;max-width:var(--chip-max-length);overflow:hidden;overflow-wrap:break-word;word-break:break-word}.chip.chip-truncate p{white-space:nowrap;text-overflow:ellipsis}.chip .btn-chip{text-align:end;padding:0;min-width:unset}.chip .btn-chip:hover{opacity:80%}.chip .btn-chip:active{border-color:transparent}.chip .btn-chip svg{color:var(--bs-primary);vertical-align:sub}.chip:has(.btn-chip:focus-visible){border-width:2px;filter:brightness(80%)}\n"] }]
753
+ }], ctorParameters: () => [], propDecorators: { selectOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectOptions", required: true }] }], allowFreeText: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowFreeText", required: false }] }], autoSelectOnExactMatch: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoSelectOnExactMatch", required: false }] }], updateValueOnType: [{ type: i0.Input, args: [{ isSignal: true, alias: "updateValueOnType", required: false }] }], syncFormWithText: [{ type: i0.Input, args: [{ isSignal: true, alias: "syncFormWithText", required: false }] }], optionListMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionListMaxHeight", required: false }] }], translateValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "translateValue", required: false }] }], scrollBehaviorOnOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollBehaviorOnOpen", required: false }] }], emitOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "emitOnly", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], chipMaxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "chipMaxLength", required: false }] }], multiSelectDisplayMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiSelectDisplayMode", required: false }] }], chipsPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "chipsPosition", required: false }] }], searchTextDebounce: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchTextDebounce", required: false }] }], internalFilterOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "internalFilterOptions", required: false }] }], selectedOption: [{ type: i0.Output, args: ["selectedOption"] }], searchTextChange: [{ type: i0.Output, args: ["searchTextChange"] }], optionList: [{ type: i0.ViewChild, args: ['optionList', { isSignal: true }] }], selectInput: [{ type: i0.ViewChild, args: ['selectInput', { isSignal: true }] }], chipContainer: [{ type: i0.ViewChild, args: ['chipContainer', { isSignal: true }] }], autocompleteContainer: [{ type: i0.ViewChild, args: ['autocompleteContainer', { isSignal: true }] }] } });
303
754
 
304
755
  /**
305
756
  * Generated bundle index. Do not edit.