shirkasoft-ui-components 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/shirkasoft-ui-components-breadcrumb.mjs +199 -0
- package/fesm2022/shirkasoft-ui-components-breadcrumb.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-button.mjs +148 -0
- package/fesm2022/shirkasoft-ui-components-button.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-chart.mjs +153 -0
- package/fesm2022/shirkasoft-ui-components-chart.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-confirm-dialog.mjs +143 -0
- package/fesm2022/shirkasoft-ui-components-confirm-dialog.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-date-picker.mjs +638 -0
- package/fesm2022/shirkasoft-ui-components-date-picker.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-fieldset.mjs +94 -0
- package/fesm2022/shirkasoft-ui-components-fieldset.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-file-upload.mjs +242 -0
- package/fesm2022/shirkasoft-ui-components-file-upload.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-knob.mjs +269 -0
- package/fesm2022/shirkasoft-ui-components-knob.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-loading.mjs +255 -0
- package/fesm2022/shirkasoft-ui-components-loading.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-modal.mjs +290 -0
- package/fesm2022/shirkasoft-ui-components-modal.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-notification.mjs +143 -0
- package/fesm2022/shirkasoft-ui-components-notification.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-price-input.mjs +64 -0
- package/fesm2022/shirkasoft-ui-components-price-input.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-rail-sidebar.mjs +205 -0
- package/fesm2022/shirkasoft-ui-components-rail-sidebar.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-select.mjs +737 -0
- package/fesm2022/shirkasoft-ui-components-select.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-sidebar.mjs +185 -0
- package/fesm2022/shirkasoft-ui-components-sidebar.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-skeleton.mjs +74 -0
- package/fesm2022/shirkasoft-ui-components-skeleton.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-table.mjs +689 -0
- package/fesm2022/shirkasoft-ui-components-table.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-tabs.mjs +249 -0
- package/fesm2022/shirkasoft-ui-components-tabs.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-tag.mjs +70 -0
- package/fesm2022/shirkasoft-ui-components-tag.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-text-field.mjs +370 -0
- package/fesm2022/shirkasoft-ui-components-text-field.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-timeline.mjs +222 -0
- package/fesm2022/shirkasoft-ui-components-timeline.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-toggle.mjs +186 -0
- package/fesm2022/shirkasoft-ui-components-toggle.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-tooltip.mjs +169 -0
- package/fesm2022/shirkasoft-ui-components-tooltip.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-utils.mjs +64 -0
- package/fesm2022/shirkasoft-ui-components-utils.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-wizard.mjs +167 -0
- package/fesm2022/shirkasoft-ui-components-wizard.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components.mjs +0 -5729
- package/fesm2022/shirkasoft-ui-components.mjs.map +1 -1
- package/package.json +101 -1
- package/types/shirkasoft-ui-components-breadcrumb.d.ts +68 -0
- package/types/shirkasoft-ui-components-button.d.ts +59 -0
- package/types/shirkasoft-ui-components-chart.d.ts +65 -0
- package/types/shirkasoft-ui-components-confirm-dialog.d.ts +88 -0
- package/types/shirkasoft-ui-components-date-picker.d.ts +200 -0
- package/types/shirkasoft-ui-components-fieldset.d.ts +72 -0
- package/types/shirkasoft-ui-components-file-upload.d.ts +87 -0
- package/types/shirkasoft-ui-components-knob.d.ts +69 -0
- package/types/shirkasoft-ui-components-loading.d.ts +94 -0
- package/types/shirkasoft-ui-components-modal.d.ts +139 -0
- package/types/shirkasoft-ui-components-notification.d.ts +82 -0
- package/types/shirkasoft-ui-components-price-input.d.ts +41 -0
- package/types/shirkasoft-ui-components-rail-sidebar.d.ts +121 -0
- package/types/shirkasoft-ui-components-select.d.ts +182 -0
- package/types/shirkasoft-ui-components-sidebar.d.ts +106 -0
- package/types/shirkasoft-ui-components-skeleton.d.ts +36 -0
- package/types/shirkasoft-ui-components-table.d.ts +293 -0
- package/types/shirkasoft-ui-components-tabs.d.ts +102 -0
- package/types/shirkasoft-ui-components-tag.d.ts +31 -0
- package/types/shirkasoft-ui-components-text-field.d.ts +120 -0
- package/types/shirkasoft-ui-components-timeline.d.ts +178 -0
- package/types/shirkasoft-ui-components-toggle.d.ts +48 -0
- package/types/shirkasoft-ui-components-tooltip.d.ts +47 -0
- package/types/shirkasoft-ui-components-utils.d.ts +9 -0
- package/types/shirkasoft-ui-components-wizard.d.ts +101 -0
- package/types/shirkasoft-ui-components.d.ts +1 -2333
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { CommonModule } from '@angular/common';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { input, output, viewChild, signal, computed, inject, DestroyRef, effect, forwardRef, HostListener, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
4
|
+
import { ReactiveFormsModule, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
5
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
6
|
+
import * as i1 from '@jsverse/transloco';
|
|
7
|
+
import { TranslocoService, TranslocoModule } from '@jsverse/transloco';
|
|
8
|
+
import { LucideChevronDown, LucideDynamicIcon } from '@lucide/angular';
|
|
9
|
+
import { getIcon } from 'shirkasoft-ui-components/utils';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Shirkasoft UI Components.
|
|
13
|
+
* Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
|
|
14
|
+
* Belongs to Shirkasoft.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Custom select that works as a ControlValueAccessor: single/multiple
|
|
18
|
+
* selection, searchable, keyboard navigation, pagination, custom entries and
|
|
19
|
+
* reactive-form integration.
|
|
20
|
+
*/
|
|
21
|
+
class SelectComponent {
|
|
22
|
+
constructor(elementRef) {
|
|
23
|
+
this.elementRef = elementRef;
|
|
24
|
+
/** Resolves icon names to lucide components, falling back to a chevron. */
|
|
25
|
+
this.getIcon = (name) => getIcon(name, LucideChevronDown);
|
|
26
|
+
/** Available options. */
|
|
27
|
+
this.options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
28
|
+
/** Enables multi-select mode. */
|
|
29
|
+
this.multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
|
|
30
|
+
/** Restricts multi-selection to the given value combinations. */
|
|
31
|
+
this.validCombinations = input(undefined, ...(ngDevMode ? [{ debugName: "validCombinations" }] : /* istanbul ignore next */ []));
|
|
32
|
+
/** Placeholder shown when there is no selection. */
|
|
33
|
+
this.placeholder = input('Seleccionar...', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
34
|
+
/** Label used to bind the control inside a `formGroup`. */
|
|
35
|
+
this.label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
36
|
+
/** Shows a loading state while options are being fetched. */
|
|
37
|
+
this.isLoading = input(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
|
|
38
|
+
/** Marks that all options have been loaded (used with pagination + search). */
|
|
39
|
+
this.isAllDataLoaded = input(false, ...(ngDevMode ? [{ debugName: "isAllDataLoaded" }] : /* istanbul ignore next */ []));
|
|
40
|
+
/** Allows creating new entries from the typed search term. */
|
|
41
|
+
this.allowCustomEntries = input(false, ...(ngDevMode ? [{ debugName: "allowCustomEntries" }] : /* istanbul ignore next */ []));
|
|
42
|
+
/** Parent form group to resolve the control from `label`. */
|
|
43
|
+
this.formGroup = input(undefined, ...(ngDevMode ? [{ debugName: "formGroup" }] : /* istanbul ignore next */ []));
|
|
44
|
+
/** Enables the search input inside the dropdown. */
|
|
45
|
+
this.isSearchable = input(false, ...(ngDevMode ? [{ debugName: "isSearchable" }] : /* istanbul ignore next */ []));
|
|
46
|
+
/** Number of options loaded per page when pagination is enabled. */
|
|
47
|
+
this.itemsPerPage = input(10, ...(ngDevMode ? [{ debugName: "itemsPerPage" }] : /* istanbul ignore next */ []));
|
|
48
|
+
/** FormControl bound externally (used instead of `formGroup`/`label`). */
|
|
49
|
+
this.control = input(undefined, ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
|
|
50
|
+
/** Paginates the option list instead of rendering it all at once. */
|
|
51
|
+
this.usePagination = input(false, ...(ngDevMode ? [{ debugName: "usePagination" }] : /* istanbul ignore next */ []));
|
|
52
|
+
/** Disables the whole select. */
|
|
53
|
+
this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
54
|
+
/** Custom per-validation-error messages shown under the select. */
|
|
55
|
+
this.errorMessages = input({
|
|
56
|
+
required: 'Este campo es requerido',
|
|
57
|
+
}, ...(ngDevMode ? [{ debugName: "errorMessages" }] : /* istanbul ignore next */ []));
|
|
58
|
+
/** Keeps the search term when options reload. */
|
|
59
|
+
this.preserveSearchOnLoad = input(false, ...(ngDevMode ? [{ debugName: "preserveSearchOnLoad" }] : /* istanbul ignore next */ []));
|
|
60
|
+
/** Translation key of the "no records" empty message. */
|
|
61
|
+
this.emptyMessageKey = input('common.no_records', ...(ngDevMode ? [{ debugName: "emptyMessageKey" }] : /* istanbul ignore next */ []));
|
|
62
|
+
/** Opens the dropdown above the trigger instead of below. */
|
|
63
|
+
this.dropdownUpward = input(false, ...(ngDevMode ? [{ debugName: "dropdownUpward" }] : /* istanbul ignore next */ []));
|
|
64
|
+
/** Shows the "no selection" option in single-select mode. */
|
|
65
|
+
this.showEmptyOption = input(true, ...(ngDevMode ? [{ debugName: "showEmptyOption" }] : /* istanbul ignore next */ []));
|
|
66
|
+
/** Emits when the selection changes (new value or array in multiple mode). */
|
|
67
|
+
this.selectionChange = output();
|
|
68
|
+
/** Emits the debounced search term while typing. */
|
|
69
|
+
this.search = output();
|
|
70
|
+
/** Reference to the filter input inside the dropdown (when searchable). */
|
|
71
|
+
this.filterInput = viewChild('filterInput', ...(ngDevMode ? [{ debugName: "filterInput" }] : /* istanbul ignore next */ []));
|
|
72
|
+
/** Reference to the select container element. */
|
|
73
|
+
this.selectContainer = viewChild('selectContainer', ...(ngDevMode ? [{ debugName: "selectContainer" }] : /* istanbul ignore next */ []));
|
|
74
|
+
/** Reference to the trigger button. */
|
|
75
|
+
this.selectTrigger = viewChild('selectTrigger', ...(ngDevMode ? [{ debugName: "selectTrigger" }] : /* istanbul ignore next */ []));
|
|
76
|
+
/** Reference to the dropdown list container. */
|
|
77
|
+
this.dropdownContainer = viewChild('dropdownContainer', ...(ngDevMode ? [{ debugName: "dropdownContainer" }] : /* istanbul ignore next */ []));
|
|
78
|
+
/** Underlying selection value (mirrors the reactive control). */
|
|
79
|
+
this._value = signal(null, ...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
|
|
80
|
+
/** Whether the dropdown is open. */
|
|
81
|
+
this.isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
82
|
+
/** Index of the currently focused option for keyboard navigation. */
|
|
83
|
+
this.focusedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "focusedIndex" }] : /* istanbul ignore next */ []));
|
|
84
|
+
/** Current search term (searchable mode). */
|
|
85
|
+
this.searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []));
|
|
86
|
+
/** Current page of the paginated dropdown. */
|
|
87
|
+
this.currentPage = signal(1, ...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
|
|
88
|
+
/** Tracks whether the user typed in the search input. */
|
|
89
|
+
this.searchTermChanged = signal(false, ...(ngDevMode ? [{ debugName: "searchTermChanged" }] : /* istanbul ignore next */ []));
|
|
90
|
+
/** Internal disabled state from the reactive form (setDisabledState). */
|
|
91
|
+
this._isDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_isDisabled" }] : /* istanbul ignore next */ []));
|
|
92
|
+
/** Debounce timer handle for the search output. */
|
|
93
|
+
this.debounceTimer = null;
|
|
94
|
+
/** Error-state counter used to recompute error helpers reactively. */
|
|
95
|
+
this.errorTracker = signal(0, ...(ngDevMode ? [{ debugName: "errorTracker" }] : /* istanbul ignore next */ []));
|
|
96
|
+
/** Whether an error has been shown at some point (for "fixed" messaging). */
|
|
97
|
+
this.hadError = signal(false, ...(ngDevMode ? [{ debugName: "hadError" }] : /* istanbul ignore next */ []));
|
|
98
|
+
/** Whether control status/value subscriptions have been attached. */
|
|
99
|
+
this.subscribed = signal(false, ...(ngDevMode ? [{ debugName: "subscribed" }] : /* istanbul ignore next */ []));
|
|
100
|
+
this.valueSubscribed = signal(false, ...(ngDevMode ? [{ debugName: "valueSubscribed" }] : /* istanbul ignore next */ []));
|
|
101
|
+
/** Current selection value. */
|
|
102
|
+
this.value = computed(() => this._value(), ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
103
|
+
/** True when the select is disabled via input or by the reactive form. */
|
|
104
|
+
this.isDisabled = computed(() => this.disabled() || this._isDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
105
|
+
/** Resolves the form control from `control` or from `formGroup`+`label`. */
|
|
106
|
+
this.actualControl = computed(() => {
|
|
107
|
+
const ctrl = this.control();
|
|
108
|
+
if (ctrl)
|
|
109
|
+
return ctrl;
|
|
110
|
+
const fg = this.formGroup();
|
|
111
|
+
const lbl = this.label();
|
|
112
|
+
if (fg && lbl) {
|
|
113
|
+
return fg.get(lbl);
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}, ...(ngDevMode ? [{ debugName: "actualControl" }] : /* istanbul ignore next */ []));
|
|
117
|
+
/** Whether the bound control should display its error (invalid + touched/dirty). */
|
|
118
|
+
this.shouldShowError = computed(() => {
|
|
119
|
+
this.errorTracker();
|
|
120
|
+
const ctrl = this.actualControl();
|
|
121
|
+
return ctrl?.invalid && (ctrl?.dirty || ctrl?.touched);
|
|
122
|
+
}, ...(ngDevMode ? [{ debugName: "shouldShowError" }] : /* istanbul ignore next */ []));
|
|
123
|
+
/** Whether an error was shown before and the control is now valid + touched. */
|
|
124
|
+
this.wasFixedError = computed(() => {
|
|
125
|
+
this.errorTracker();
|
|
126
|
+
const ctrl = this.actualControl();
|
|
127
|
+
return this.hadError() && ctrl?.valid && ctrl?.touched;
|
|
128
|
+
}, ...(ngDevMode ? [{ debugName: "wasFixedError" }] : /* istanbul ignore next */ []));
|
|
129
|
+
/** Text shown in the trigger: the selected label(s) or the placeholder. */
|
|
130
|
+
this.displayValue = computed(() => {
|
|
131
|
+
const val = this._value();
|
|
132
|
+
const opts = this.options();
|
|
133
|
+
const isMultiple = this.multiple();
|
|
134
|
+
const ph = this.placeholder();
|
|
135
|
+
if (!val || (Array.isArray(val) && val.length === 0)) {
|
|
136
|
+
return ph;
|
|
137
|
+
}
|
|
138
|
+
if (isMultiple) {
|
|
139
|
+
const values = Array.isArray(val) ? val : [];
|
|
140
|
+
const selectedLabels = opts
|
|
141
|
+
.filter((option) => values.includes(option.value))
|
|
142
|
+
.map((option) => option.label);
|
|
143
|
+
if (selectedLabels.length === 0) {
|
|
144
|
+
return values.toString();
|
|
145
|
+
}
|
|
146
|
+
return selectedLabels.join(', ');
|
|
147
|
+
}
|
|
148
|
+
const selectedOption = opts.find((opt) => opt.value === val);
|
|
149
|
+
return selectedOption ? selectedOption.label : val;
|
|
150
|
+
}, ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
|
|
151
|
+
/** List of validation error messages for the bound control. */
|
|
152
|
+
this.getErrorMessages = computed(() => {
|
|
153
|
+
this.errorTracker();
|
|
154
|
+
const ctrl = this.actualControl();
|
|
155
|
+
if (!ctrl?.errors)
|
|
156
|
+
return [];
|
|
157
|
+
const customErrors = this.errorMessages();
|
|
158
|
+
const t = this.transloco;
|
|
159
|
+
return Object.keys(ctrl.errors).map((errorKey) => {
|
|
160
|
+
if (customErrors[errorKey])
|
|
161
|
+
return customErrors[errorKey];
|
|
162
|
+
switch (errorKey) {
|
|
163
|
+
case 'required':
|
|
164
|
+
return t.translate('validation.required');
|
|
165
|
+
case 'email':
|
|
166
|
+
return t.translate('validation.email');
|
|
167
|
+
case 'minlength':
|
|
168
|
+
return t.translate('validation.minlength', {
|
|
169
|
+
min: ctrl.errors['minlength'].requiredLength,
|
|
170
|
+
});
|
|
171
|
+
case 'maxlength':
|
|
172
|
+
return t.translate('validation.maxlength', {
|
|
173
|
+
max: ctrl.errors['maxlength'].requiredLength,
|
|
174
|
+
});
|
|
175
|
+
case 'pattern':
|
|
176
|
+
return t.translate('validation.pattern');
|
|
177
|
+
default:
|
|
178
|
+
return t.translate('validation.invalid');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}, ...(ngDevMode ? [{ debugName: "getErrorMessages" }] : /* istanbul ignore next */ []));
|
|
182
|
+
/** Options filtered by the search term (matches first, then contains). */
|
|
183
|
+
this.filteredOptions = computed(() => {
|
|
184
|
+
const opts = this.options();
|
|
185
|
+
const term = this.searchTerm();
|
|
186
|
+
if (!term)
|
|
187
|
+
return opts;
|
|
188
|
+
const normalizedSearchTerm = this.normalizeText(term);
|
|
189
|
+
const matched = opts.filter((option) => this.normalizeText(option.label).includes(normalizedSearchTerm));
|
|
190
|
+
return matched.sort((a, b) => {
|
|
191
|
+
const aLbl = this.normalizeText(a.label);
|
|
192
|
+
const bLbl = this.normalizeText(b.label);
|
|
193
|
+
const aStarts = aLbl.startsWith(normalizedSearchTerm);
|
|
194
|
+
const bStarts = bLbl.startsWith(normalizedSearchTerm);
|
|
195
|
+
if (aStarts && !bStarts)
|
|
196
|
+
return -1;
|
|
197
|
+
if (!aStarts && bStarts)
|
|
198
|
+
return 1;
|
|
199
|
+
return 0;
|
|
200
|
+
});
|
|
201
|
+
}, ...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
|
|
202
|
+
/** Options for the current page, with a synthetic custom entry when allowed. */
|
|
203
|
+
this.paginatedOptions = computed(() => {
|
|
204
|
+
let filtered = this.filteredOptions();
|
|
205
|
+
const term = this.searchTerm();
|
|
206
|
+
const allowCustom = this.allowCustomEntries();
|
|
207
|
+
if (allowCustom && term && !filtered.some((o) => o.value === term)) {
|
|
208
|
+
filtered = [
|
|
209
|
+
{
|
|
210
|
+
label: `${term} (nuevo)`,
|
|
211
|
+
value: term,
|
|
212
|
+
custom: true,
|
|
213
|
+
},
|
|
214
|
+
...filtered,
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
if (!this.usePagination()) {
|
|
218
|
+
return filtered;
|
|
219
|
+
}
|
|
220
|
+
const page = this.currentPage();
|
|
221
|
+
const perPage = this.itemsPerPage();
|
|
222
|
+
const startIndex = (page - 1) * perPage;
|
|
223
|
+
const endIndex = startIndex + perPage;
|
|
224
|
+
return filtered.slice(startIndex, endIndex);
|
|
225
|
+
}, ...(ngDevMode ? [{ debugName: "paginatedOptions" }] : /* istanbul ignore next */ []));
|
|
226
|
+
/** Non-custom options, including the empty option in single-select mode. */
|
|
227
|
+
this.regularOptions = computed(() => {
|
|
228
|
+
const opts = this.paginatedOptions().filter((option) => !option.custom);
|
|
229
|
+
if (!this.multiple() && this.showEmptyOption()) {
|
|
230
|
+
return [{ label: '- Sin selección -', value: null }, ...opts];
|
|
231
|
+
}
|
|
232
|
+
return opts;
|
|
233
|
+
}, ...(ngDevMode ? [{ debugName: "regularOptions" }] : /* istanbul ignore next */ []));
|
|
234
|
+
/** Total pages of the filtered option list (1 when pagination is off). */
|
|
235
|
+
this.totalPages = computed(() => {
|
|
236
|
+
if (!this.usePagination()) {
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
const filtered = this.filteredOptions();
|
|
240
|
+
const perPage = this.itemsPerPage();
|
|
241
|
+
return Math.ceil(filtered.length / perPage);
|
|
242
|
+
}, ...(ngDevMode ? [{ debugName: "totalPages" }] : /* istanbul ignore next */ []));
|
|
243
|
+
/** Whether there is at least one selected value. */
|
|
244
|
+
this.hasSelectedValues = computed(() => {
|
|
245
|
+
const val = this._value();
|
|
246
|
+
if (this.multiple()) {
|
|
247
|
+
return val && val.length > 0;
|
|
248
|
+
}
|
|
249
|
+
return !!val;
|
|
250
|
+
}, ...(ngDevMode ? [{ debugName: "hasSelectedValues" }] : /* istanbul ignore next */ []));
|
|
251
|
+
/** Whether the custom optional entry should be offered for the current term. */
|
|
252
|
+
this.showCustomOption = computed(() => {
|
|
253
|
+
const term = this.searchTerm();
|
|
254
|
+
const opts = this.options();
|
|
255
|
+
return (this.allowCustomEntries() &&
|
|
256
|
+
term !== '' &&
|
|
257
|
+
!opts.some((o) => o.value === term));
|
|
258
|
+
}, ...(ngDevMode ? [{ debugName: "showCustomOption" }] : /* istanbul ignore next */ []));
|
|
259
|
+
this.transloco = inject(TranslocoService);
|
|
260
|
+
/** ControlValueAccessor onChange callback. */
|
|
261
|
+
this.onChange = (_) => { };
|
|
262
|
+
/** ControlValueAccessor onTouched callback. */
|
|
263
|
+
this.onTouched = () => { };
|
|
264
|
+
this.destroyRef = inject(DestroyRef);
|
|
265
|
+
// Debounced search emission whenever the term changes (searchable mode).
|
|
266
|
+
effect(() => {
|
|
267
|
+
const term = this.searchTerm();
|
|
268
|
+
if (term !== '' && this.isSearchable()) {
|
|
269
|
+
if (this.debounceTimer) {
|
|
270
|
+
clearTimeout(this.debounceTimer);
|
|
271
|
+
}
|
|
272
|
+
this.debounceTimer = setTimeout(() => {
|
|
273
|
+
this.search.emit(term);
|
|
274
|
+
}, 300);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
// Normalizes the value to arrays in multiple mode when empty.
|
|
278
|
+
effect(() => {
|
|
279
|
+
const isMultiple = this.multiple();
|
|
280
|
+
if (this._value() === null || this._value() === undefined) {
|
|
281
|
+
this._value.set(isMultiple ? [] : null);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
// Resets to the first page when options reload, clearing the search
|
|
285
|
+
// term unless it must be preserved.
|
|
286
|
+
effect(() => {
|
|
287
|
+
this.options();
|
|
288
|
+
this.currentPage.set(1);
|
|
289
|
+
if (!this.preserveSearchOnLoad() || !this.searchTermChanged()) {
|
|
290
|
+
this.searchTerm.set('');
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
// Subscribes to the bound control status/value changes to track errors.
|
|
294
|
+
effect(() => {
|
|
295
|
+
const ctrl = this.actualControl();
|
|
296
|
+
if (!ctrl) {
|
|
297
|
+
this.subscribed.set(false);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (this.subscribed())
|
|
301
|
+
return;
|
|
302
|
+
if (ctrl.invalid && ctrl.touched) {
|
|
303
|
+
this.hadError.set(true);
|
|
304
|
+
}
|
|
305
|
+
ctrl.statusChanges
|
|
306
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
307
|
+
.subscribe(() => {
|
|
308
|
+
this.errorTracker.update((v) => v + 1);
|
|
309
|
+
if (ctrl.invalid && ctrl.touched) {
|
|
310
|
+
this.hadError.set(true);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
ctrl.valueChanges
|
|
314
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
315
|
+
.subscribe(() => {
|
|
316
|
+
this.errorTracker.update((v) => v + 1);
|
|
317
|
+
});
|
|
318
|
+
this.subscribed.set(true);
|
|
319
|
+
});
|
|
320
|
+
// Forces error recomputation when the active translation changes.
|
|
321
|
+
this.transloco.langChanges$
|
|
322
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
323
|
+
.subscribe(() => {
|
|
324
|
+
this.errorTracker.update((v) => v + 1);
|
|
325
|
+
});
|
|
326
|
+
// One-way sync from the reactive control to the internal value signal.
|
|
327
|
+
effect(() => {
|
|
328
|
+
const ctrl = this.actualControl();
|
|
329
|
+
if (!ctrl)
|
|
330
|
+
return;
|
|
331
|
+
if (!this.valueSubscribed()) {
|
|
332
|
+
this.writeValue(ctrl.value);
|
|
333
|
+
ctrl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => {
|
|
334
|
+
this.writeValue(value);
|
|
335
|
+
});
|
|
336
|
+
this.valueSubscribed.set(true);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/** Sets the disabled state coming from the reactive form infrastructure. */
|
|
341
|
+
setDisabledState(isDisabled) {
|
|
342
|
+
this._isDisabled.set(isDisabled);
|
|
343
|
+
}
|
|
344
|
+
/** Marks the control as touched and updates validity and error state. */
|
|
345
|
+
markAsTouchedAndUpdate() {
|
|
346
|
+
const ctrl = this.actualControl();
|
|
347
|
+
if (ctrl) {
|
|
348
|
+
ctrl.markAsTouched();
|
|
349
|
+
ctrl.updateValueAndValidity({ emitEvent: false });
|
|
350
|
+
}
|
|
351
|
+
this.onTouched();
|
|
352
|
+
this.errorTracker.update((v) => v + 1);
|
|
353
|
+
if (ctrl?.invalid) {
|
|
354
|
+
this.hadError.set(true);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/** Strips accents and lowercases text for accent-insensitive matching. */
|
|
358
|
+
normalizeText(text) {
|
|
359
|
+
return text
|
|
360
|
+
.normalize('NFD')
|
|
361
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
362
|
+
.toLowerCase();
|
|
363
|
+
}
|
|
364
|
+
/** Keyboard handler for the trigger element (open/close/navigate/select). */
|
|
365
|
+
onKeydown(event) {
|
|
366
|
+
if (this.isDisabled())
|
|
367
|
+
return;
|
|
368
|
+
switch (event.key) {
|
|
369
|
+
case 'Enter':
|
|
370
|
+
case ' ':
|
|
371
|
+
event.preventDefault();
|
|
372
|
+
if (this.isOpen()) {
|
|
373
|
+
if (this.focusedIndex() >= 0) {
|
|
374
|
+
const options = this.regularOptions();
|
|
375
|
+
const selectedOption = options[this.focusedIndex()];
|
|
376
|
+
if (selectedOption && !this.isOptionDisabled(selectedOption)) {
|
|
377
|
+
this.selectOption(selectedOption);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
this.toggleDropdown();
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
case 'Escape':
|
|
386
|
+
if (this.isOpen()) {
|
|
387
|
+
this.isOpen.set(false);
|
|
388
|
+
this.searchTerm.set('');
|
|
389
|
+
this.focusedIndex.set(-1);
|
|
390
|
+
this.selectTrigger()?.nativeElement.focus();
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
case 'ArrowDown':
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
if (!this.isOpen()) {
|
|
396
|
+
this.toggleDropdown();
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
this.moveFocus(1);
|
|
400
|
+
}
|
|
401
|
+
break;
|
|
402
|
+
case 'ArrowUp':
|
|
403
|
+
event.preventDefault();
|
|
404
|
+
if (this.isOpen()) {
|
|
405
|
+
this.moveFocus(-1);
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
case 'Tab':
|
|
409
|
+
if (this.isOpen()) {
|
|
410
|
+
this.isOpen.set(false);
|
|
411
|
+
this.searchTerm.set('');
|
|
412
|
+
this.focusedIndex.set(-1);
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/** Keyboard handler for the search input inside a searchable dropdown. */
|
|
418
|
+
handleKeyDown(event) {
|
|
419
|
+
switch (event.key) {
|
|
420
|
+
case 'Enter':
|
|
421
|
+
event.preventDefault();
|
|
422
|
+
event.stopPropagation();
|
|
423
|
+
if (this.focusedIndex() >= 0) {
|
|
424
|
+
const options = this.regularOptions();
|
|
425
|
+
const option = options[this.focusedIndex()];
|
|
426
|
+
if (option && !this.isOptionDisabled(option)) {
|
|
427
|
+
this.selectOption(option);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
const filtered = this.filteredOptions();
|
|
432
|
+
if (filtered.length > 0) {
|
|
433
|
+
this.selectOption(filtered[0]);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
break;
|
|
437
|
+
case 'ArrowDown':
|
|
438
|
+
event.preventDefault();
|
|
439
|
+
event.stopPropagation();
|
|
440
|
+
this.moveFocus(1);
|
|
441
|
+
break;
|
|
442
|
+
case 'ArrowUp':
|
|
443
|
+
event.preventDefault();
|
|
444
|
+
event.stopPropagation();
|
|
445
|
+
this.moveFocus(-1);
|
|
446
|
+
break;
|
|
447
|
+
case 'Escape':
|
|
448
|
+
event.stopPropagation();
|
|
449
|
+
event.preventDefault();
|
|
450
|
+
this.isOpen.set(false);
|
|
451
|
+
this.searchTerm.set('');
|
|
452
|
+
this.focusedIndex.set(-1);
|
|
453
|
+
this.selectTrigger()?.nativeElement.focus();
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/** Moves the focused option index by `direction`, skipping disabled ones. */
|
|
458
|
+
moveFocus(direction) {
|
|
459
|
+
const options = this.regularOptions();
|
|
460
|
+
if (!options.length)
|
|
461
|
+
return;
|
|
462
|
+
let newIndex = this.focusedIndex();
|
|
463
|
+
do {
|
|
464
|
+
newIndex = newIndex + direction;
|
|
465
|
+
if (newIndex < 0)
|
|
466
|
+
newIndex = options.length - 1;
|
|
467
|
+
if (newIndex >= options.length)
|
|
468
|
+
newIndex = 0;
|
|
469
|
+
} while (this.isOptionDisabled(options[newIndex]) && options.length > 1);
|
|
470
|
+
this.focusedIndex.set(newIndex);
|
|
471
|
+
setTimeout(() => {
|
|
472
|
+
const container = this.dropdownContainer()?.nativeElement;
|
|
473
|
+
const activeItem = container?.querySelector('.select-option-active');
|
|
474
|
+
if (activeItem && container) {
|
|
475
|
+
const itemTop = activeItem.offsetTop;
|
|
476
|
+
const itemBottom = itemTop + activeItem.offsetHeight;
|
|
477
|
+
const containerScrollTop = container.scrollTop;
|
|
478
|
+
const containerHeight = container.clientHeight;
|
|
479
|
+
if (itemTop < containerScrollTop) {
|
|
480
|
+
container.scrollTop = itemTop;
|
|
481
|
+
}
|
|
482
|
+
else if (itemBottom > containerScrollTop + containerHeight) {
|
|
483
|
+
container.scrollTop = itemBottom - containerHeight;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
/** Index of the currently selected option, or -1. */
|
|
489
|
+
getSelectedIndex() {
|
|
490
|
+
const currentVal = this._value();
|
|
491
|
+
const options = this.regularOptions();
|
|
492
|
+
if (currentVal === null || currentVal === undefined)
|
|
493
|
+
return -1;
|
|
494
|
+
const index = options.findIndex((opt) => opt.value === currentVal);
|
|
495
|
+
return index >= 0 ? index : -1;
|
|
496
|
+
}
|
|
497
|
+
/** Whether an option is disabled based on the valid combinations rule. */
|
|
498
|
+
isOptionDisabled(option) {
|
|
499
|
+
if (option.value === null)
|
|
500
|
+
return false;
|
|
501
|
+
const validCombos = this.validCombinations();
|
|
502
|
+
if (!validCombos || !this.multiple()) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
const currentSelection = Array.isArray(this._value()) ? this._value() : [];
|
|
506
|
+
if (currentSelection.length === 0) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
if (currentSelection.includes(option.value)) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
const hypotheticalSelection = [...currentSelection, option.value];
|
|
513
|
+
return !validCombos.some((combination) => {
|
|
514
|
+
const allSelected = hypotheticalSelection.every((selected) => combination.includes(selected));
|
|
515
|
+
const withinLimit = hypotheticalSelection.length <= combination.length;
|
|
516
|
+
return allSelected && withinLimit;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
/** Whether an option is currently selected. */
|
|
520
|
+
isSelected(option) {
|
|
521
|
+
const val = this._value();
|
|
522
|
+
if (this.multiple()) {
|
|
523
|
+
return Array.isArray(val) && val.includes(option.value);
|
|
524
|
+
}
|
|
525
|
+
return val === option.value;
|
|
526
|
+
}
|
|
527
|
+
/** Goes to the next options page when possible. */
|
|
528
|
+
nextPage() {
|
|
529
|
+
const total = this.totalPages();
|
|
530
|
+
const current = this.currentPage();
|
|
531
|
+
if (current < total) {
|
|
532
|
+
this.currentPage.set(current + 1);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/** Goes to the previous options page when possible. */
|
|
536
|
+
previousPage() {
|
|
537
|
+
const current = this.currentPage();
|
|
538
|
+
if (current > 1) {
|
|
539
|
+
this.currentPage.set(current - 1);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/** Closes the dropdown when a click happens outside the component. */
|
|
543
|
+
onClickOutside(event) {
|
|
544
|
+
const clickedInside = this.elementRef.nativeElement.contains(event.target);
|
|
545
|
+
if (!clickedInside && this.isOpen()) {
|
|
546
|
+
this.markAsTouchedAndUpdate();
|
|
547
|
+
this.isOpen.set(false);
|
|
548
|
+
this.searchTerm.set('');
|
|
549
|
+
this.focusedIndex.set(-1);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/** Selects a typed free-text value as a new custom entry. */
|
|
553
|
+
onCustomOptionClick() {
|
|
554
|
+
const term = this.searchTerm();
|
|
555
|
+
const customOption = {
|
|
556
|
+
label: term,
|
|
557
|
+
value: term,
|
|
558
|
+
};
|
|
559
|
+
this._value.set(customOption.value);
|
|
560
|
+
this.onChange(customOption.value);
|
|
561
|
+
this.selectionChange.emit(customOption.value);
|
|
562
|
+
this.isOpen.set(false);
|
|
563
|
+
this.searchTerm.set('');
|
|
564
|
+
const ctrl = this.actualControl();
|
|
565
|
+
if (ctrl) {
|
|
566
|
+
ctrl.setValue(customOption.value);
|
|
567
|
+
}
|
|
568
|
+
this.markAsTouchedAndUpdate();
|
|
569
|
+
}
|
|
570
|
+
/** Handles typing in the search input (updates term and emits search). */
|
|
571
|
+
onInputChange(event) {
|
|
572
|
+
const previousSearch = this.searchTerm();
|
|
573
|
+
const target = event.target;
|
|
574
|
+
this.searchTerm.set(target.value);
|
|
575
|
+
this.searchTermChanged.set(true);
|
|
576
|
+
if (previousSearch !== this.searchTerm()) {
|
|
577
|
+
this.search.emit(this.searchTerm());
|
|
578
|
+
if (this.allowCustomEntries() && this.searchTerm()) {
|
|
579
|
+
this._value.set(this.searchTerm());
|
|
580
|
+
this.onChange(this.searchTerm());
|
|
581
|
+
this.selectionChange.emit(this.searchTerm());
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
this.currentPage.set(1);
|
|
585
|
+
}
|
|
586
|
+
/** Selects or toggles the clicked option (custom entries included). */
|
|
587
|
+
onOptionClick(option) {
|
|
588
|
+
if (this.isDisabled())
|
|
589
|
+
return;
|
|
590
|
+
if (this.allowCustomEntries() && option.value === this.searchTerm()) {
|
|
591
|
+
const customOption = {
|
|
592
|
+
label: this.searchTerm(),
|
|
593
|
+
value: this.searchTerm(),
|
|
594
|
+
};
|
|
595
|
+
this._value.set(customOption.value);
|
|
596
|
+
this.onChange(customOption.value);
|
|
597
|
+
this.selectionChange.emit(customOption.value);
|
|
598
|
+
this.isOpen.set(false);
|
|
599
|
+
this.searchTerm.set('');
|
|
600
|
+
const ctrl = this.actualControl();
|
|
601
|
+
if (ctrl) {
|
|
602
|
+
ctrl.setValue(customOption.value);
|
|
603
|
+
}
|
|
604
|
+
this.markAsTouchedAndUpdate();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (this.multiple()) {
|
|
608
|
+
this.toggleOption(option);
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
this.selectOption(option);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/** Selects a single option and updates the control/outputs. */
|
|
615
|
+
selectOption(option) {
|
|
616
|
+
if (this.multiple()) {
|
|
617
|
+
this.toggleOption(option);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
this._value.set(option.value);
|
|
621
|
+
this.onChange(option.value);
|
|
622
|
+
this.onTouched();
|
|
623
|
+
this.selectionChange.emit(option.value);
|
|
624
|
+
this.isOpen.set(false);
|
|
625
|
+
const ctrl = this.actualControl();
|
|
626
|
+
if (ctrl) {
|
|
627
|
+
ctrl.setValue(option.value);
|
|
628
|
+
}
|
|
629
|
+
this.markAsTouchedAndUpdate();
|
|
630
|
+
this.focusedIndex.set(-1);
|
|
631
|
+
this.selectTrigger()?.nativeElement.focus();
|
|
632
|
+
}
|
|
633
|
+
/** Toggles an option in multiple mode, honoring the valid combinations. */
|
|
634
|
+
toggleOption(option) {
|
|
635
|
+
if (this.isOptionDisabled(option))
|
|
636
|
+
return;
|
|
637
|
+
if (!this.multiple())
|
|
638
|
+
return;
|
|
639
|
+
let currentValue = this._value();
|
|
640
|
+
if (!Array.isArray(currentValue)) {
|
|
641
|
+
currentValue = [];
|
|
642
|
+
}
|
|
643
|
+
const index = currentValue.indexOf(option.value);
|
|
644
|
+
let newValue;
|
|
645
|
+
if (index === -1) {
|
|
646
|
+
newValue = [...currentValue, option.value];
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
newValue = currentValue.filter((v) => v !== option.value);
|
|
650
|
+
}
|
|
651
|
+
this._value.set(newValue);
|
|
652
|
+
this.onChange(newValue);
|
|
653
|
+
this.onTouched();
|
|
654
|
+
this.selectionChange.emit(newValue);
|
|
655
|
+
this.markAsTouchedAndUpdate();
|
|
656
|
+
}
|
|
657
|
+
/** Opens/closes the dropdown and manages focus/selection state. */
|
|
658
|
+
toggleDropdown() {
|
|
659
|
+
if (this.isDisabled())
|
|
660
|
+
return;
|
|
661
|
+
const wasOpen = this.isOpen();
|
|
662
|
+
this.isOpen.update((v) => !v);
|
|
663
|
+
if (!wasOpen && this.isOpen()) {
|
|
664
|
+
const selectedIdx = this.getSelectedIndex();
|
|
665
|
+
this.focusedIndex.set(selectedIdx);
|
|
666
|
+
}
|
|
667
|
+
else if (wasOpen && !this.isOpen()) {
|
|
668
|
+
this.searchTerm.set('');
|
|
669
|
+
this.focusedIndex.set(-1);
|
|
670
|
+
this.selectTrigger()?.nativeElement.focus();
|
|
671
|
+
}
|
|
672
|
+
if (this.isOpen() && this.isSearchable()) {
|
|
673
|
+
setTimeout(() => {
|
|
674
|
+
this.filterInput()?.nativeElement?.focus();
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
this.onTouched();
|
|
678
|
+
}
|
|
679
|
+
/** trackBy for rendered options so Angular reuses DOM nodes by value. */
|
|
680
|
+
trackByFn(index, option) {
|
|
681
|
+
return option.value;
|
|
682
|
+
}
|
|
683
|
+
/** Writes an external value into the internal signal (ControlValueAccessor). */
|
|
684
|
+
writeValue(value) {
|
|
685
|
+
if (value === null ||
|
|
686
|
+
value === undefined ||
|
|
687
|
+
value === '' ||
|
|
688
|
+
(Array.isArray(value) && value.length === 0)) {
|
|
689
|
+
this._value.set(this.multiple() ? [] : null);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
this._value.set(value);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/** Stores the onChange callback (ControlValueAccessor). */
|
|
696
|
+
registerOnChange(fn) {
|
|
697
|
+
this.onChange = fn;
|
|
698
|
+
}
|
|
699
|
+
/** Stores the onTouched callback (ControlValueAccessor). */
|
|
700
|
+
registerOnTouched(fn) {
|
|
701
|
+
this.onTouched = fn;
|
|
702
|
+
}
|
|
703
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: SelectComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
704
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: SelectComponent, isStandalone: true, selector: "shk-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, validCombinations: { classPropertyName: "validCombinations", publicName: "validCombinations", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: false, transformFunction: null }, isAllDataLoaded: { classPropertyName: "isAllDataLoaded", publicName: "isAllDataLoaded", isSignal: true, isRequired: false, transformFunction: null }, allowCustomEntries: { classPropertyName: "allowCustomEntries", publicName: "allowCustomEntries", isSignal: true, isRequired: false, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: false, transformFunction: null }, isSearchable: { classPropertyName: "isSearchable", publicName: "isSearchable", isSignal: true, isRequired: false, transformFunction: null }, itemsPerPage: { classPropertyName: "itemsPerPage", publicName: "itemsPerPage", isSignal: true, isRequired: false, transformFunction: null }, control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null }, usePagination: { classPropertyName: "usePagination", publicName: "usePagination", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: true, isRequired: false, transformFunction: null }, preserveSearchOnLoad: { classPropertyName: "preserveSearchOnLoad", publicName: "preserveSearchOnLoad", isSignal: true, isRequired: false, transformFunction: null }, emptyMessageKey: { classPropertyName: "emptyMessageKey", publicName: "emptyMessageKey", isSignal: true, isRequired: false, transformFunction: null }, dropdownUpward: { classPropertyName: "dropdownUpward", publicName: "dropdownUpward", isSignal: true, isRequired: false, transformFunction: null }, showEmptyOption: { classPropertyName: "showEmptyOption", publicName: "showEmptyOption", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", search: "search" }, host: { listeners: { "document:mousedown": "onClickOutside($event)" } }, providers: [
|
|
705
|
+
{
|
|
706
|
+
provide: NG_VALUE_ACCESSOR,
|
|
707
|
+
useExisting: forwardRef(() => SelectComponent),
|
|
708
|
+
multi: true,
|
|
709
|
+
},
|
|
710
|
+
], viewQueries: [{ propertyName: "filterInput", first: true, predicate: ["filterInput"], descendants: true, isSignal: true }, { propertyName: "selectContainer", first: true, predicate: ["selectContainer"], descendants: true, isSignal: true }, { propertyName: "selectTrigger", first: true, predicate: ["selectTrigger"], descendants: true, isSignal: true }, { propertyName: "dropdownContainer", first: true, predicate: ["dropdownContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n #selectContainer\n class=\"relative w-full font-sans outline-none\"\n>\n @if (label()) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)] mb-1\">\n {{ label() }}\n </div>\n }\n\n <div\n #selectTrigger\n class=\"flex justify-between items-center w-full px-3 py-2 border _shk-border rounded-lg _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] transition-colors duration-150 cursor-pointer min-h-[2.5rem] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus-within:outline-none focus-within:[background-color:var(--shk-surface-50)] dark:focus-within:[background-color:var(--shk-surface-100)] focus-within:ring-1 focus-within:[--tw-ring-color:var(--shk-primary-500)] focus-within:[border-color:var(--shk-primary-500)] focus:outline-none focus:ring-1 focus:[--tw-ring-color:var(--shk-primary-500)] focus:[border-color:var(--shk-primary-500)] sm:text-sm\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n (keydown)=\"onKeydown($event)\"\n (click)=\"toggleDropdown()\"\n [class.open]=\"isOpen()\"\n [class.disabled-select]=\"isDisabled()\"\n [class._shk-border-red]=\"shouldShowError()\"\n >\n <div class=\"selected-value truncate\">\n @if (isSearchable() && isOpen()) {\n <input\n #filterInput\n type=\"text\"\n class=\"w-full bg-transparent border-0 outline-none p-0 _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"\n placeholder=\"Buscar...\"\n [value]=\"searchTerm()\"\n (input)=\"onInputChange($event)\"\n (keydown)=\"handleKeyDown($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n } @else {\n <span\n [class._shk-text-surface-400]=\"!hasSelectedValues()\"\n [class._shk-text-surface-800]=\"hasSelectedValues()\"\n >\n {{ displayValue() }}\n </span>\n }\n </div>\n <div\n class=\"text-xs transition-transform duration-200 shrink-0 ml-2\"\n [class.rotate-180]=\"isOpen()\"\n >\n <svg\n [lucideIcon]=\"getIcon('chevron-down')\"\n class=\"w-4 h-4 _shk-text-surface-400 dark:[color:var(--shk-surface-400)]\"\n ></svg>\n </div>\n </div>\n\n @if (isOpen()) {\n <div\n #dropdownContainer\n (keydown)=\"onKeydown($event)\"\n class=\"absolute left-0 right-0 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border _shk-border rounded-lg max-h-[240px] overflow-y-auto z-50 shadow-lg _shk-scrollbar\"\n [class.top-full]=\"!dropdownUpward()\"\n [class.bottom-full]=\"dropdownUpward()\"\n [class.mt-1]=\"!dropdownUpward()\"\n [class.mb-1]=\"dropdownUpward()\"\n >\n @if (showCustomOption()) {\n <div\n class=\"flex items-center gap-2 px-3 py-2 cursor-pointer hover:[background-color:var(--shk-surface-50)] dark:hover:[background-color:var(--shk-surface-200)] text-sm\"\n (click)=\"onCustomOptionClick()\"\n >\n <span class=\"font-semibold _shk-text-primary\">Crear nuevo:</span>\n <span class=\"_shk-text-surface-700 dark:[color:var(--shk-surface-700)] ml-1\">{{ searchTerm() }}</span>\n </div>\n }\n\n @for (option of regularOptions(); track trackByFn($index, option); let i = $index) {\n <div\n class=\"flex items-center gap-2 px-3 py-2 cursor-pointer hover:[background-color:var(--shk-surface-50)] dark:hover:[background-color:var(--shk-surface-200)] text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"\n [class.opacity-50]=\"isOptionDisabled(option)\"\n [class.select-option-active]=\"focusedIndex() === i\"\n [class._shk-text-surface-400]=\"option.value === null\"\n (click)=\"!isOptionDisabled(option) && onOptionClick(option)\"\n >\n @if (multiple()) {\n <input\n type=\"checkbox\"\n [checked]=\"isSelected(option)\"\n [disabled]=\"isOptionDisabled(option)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"!isOptionDisabled(option) && toggleOption(option)\"\n class=\"flex-shrink-0 [accent-color:var(--shk-primary-600)]\"\n />\n {{ option.label }}\n } @else {\n {{ option.label }}\n }\n </div>\n }\n\n <div class=\"px-3 py-2 text-center\">\n @if (isLoading() && regularOptions().length === 0) {\n <div class=\"flex items-center justify-center gap-2 text-sm _shk-text-surface-500\">\n <svg class=\"spinner\" viewBox=\"0 0 50 50\">\n <circle\n class=\"path\"\n cx=\"25\"\n cy=\"25\"\n r=\"20\"\n fill=\"none\"\n stroke-width=\"5\"\n ></circle>\n </svg>\n Cargando m\u00E1s resultados...\n </div>\n }\n\n @if (!isLoading() && isAllDataLoaded() && regularOptions().length === 0) {\n <div class=\"text-sm _shk-text-surface-400 italic\">\n No se encontraron coincidencias\n </div>\n }\n\n @if (options().length === 0) {\n <div class=\"text-sm _shk-text-surface-400 italic\">\n {{ emptyMessageKey() | transloco }}\n </div>\n }\n </div>\n\n @if (usePagination() && totalPages() > 1) {\n <div class=\"flex justify-between items-center px-3 py-2 border-t _shk-border\">\n <button\n class=\"bg-transparent border-0 cursor-pointer px-2 py-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-50 disabled:cursor-not-allowed _shk-text-surface-600 dark:[color:var(--shk-surface-600)]\"\n [disabled]=\"currentPage() === 1\"\n (click)=\"$event.stopPropagation(); previousPage()\"\n >\n <svg [lucideIcon]=\"getIcon('chevron-left')\" class=\"w-4 h-4\"></svg>\n </button>\n <span class=\"text-sm _shk-text-surface-600 dark:[color:var(--shk-surface-600)]\">\n {{ currentPage() }} de {{ totalPages() }}\n </span>\n <button\n class=\"bg-transparent border-0 cursor-pointer px-2 py-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-50 disabled:cursor-not-allowed _shk-text-surface-600 dark:[color:var(--shk-surface-600)]\"\n [disabled]=\"currentPage() === totalPages()\"\n (click)=\"$event.stopPropagation(); nextPage()\"\n >\n <svg [lucideIcon]=\"getIcon('chevron-right')\" class=\"w-4 h-4\"></svg>\n </button>\n </div>\n }\n </div>\n }\n\n @if (shouldShowError()) {\n <div class=\"mt-1 text-xs text-red-500 max-h-20 overflow-y-auto _shk-scrollbar\" tabindex=\"-1\">\n <ul class=\"list-disc pl-4 space-y-1\">\n @for (error of getErrorMessages(); track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n }\n</div>", styles: [".select-option-active{background-color:var(--shk-primary-50);outline:2px solid var(--shk-primary-500);outline-offset:-2px;color:var(--shk-primary-700)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "pipe", type: i1.TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
711
|
+
}
|
|
712
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: SelectComponent, decorators: [{
|
|
713
|
+
type: Component,
|
|
714
|
+
args: [{ selector: 'shk-select', standalone: true, imports: [
|
|
715
|
+
CommonModule,
|
|
716
|
+
ReactiveFormsModule,
|
|
717
|
+
FormsModule,
|
|
718
|
+
TranslocoModule,
|
|
719
|
+
LucideDynamicIcon,
|
|
720
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
721
|
+
{
|
|
722
|
+
provide: NG_VALUE_ACCESSOR,
|
|
723
|
+
useExisting: forwardRef(() => SelectComponent),
|
|
724
|
+
multi: true,
|
|
725
|
+
},
|
|
726
|
+
], template: "<div\n #selectContainer\n class=\"relative w-full font-sans outline-none\"\n>\n @if (label()) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)] mb-1\">\n {{ label() }}\n </div>\n }\n\n <div\n #selectTrigger\n class=\"flex justify-between items-center w-full px-3 py-2 border _shk-border rounded-lg _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] transition-colors duration-150 cursor-pointer min-h-[2.5rem] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus-within:outline-none focus-within:[background-color:var(--shk-surface-50)] dark:focus-within:[background-color:var(--shk-surface-100)] focus-within:ring-1 focus-within:[--tw-ring-color:var(--shk-primary-500)] focus-within:[border-color:var(--shk-primary-500)] focus:outline-none focus:ring-1 focus:[--tw-ring-color:var(--shk-primary-500)] focus:[border-color:var(--shk-primary-500)] sm:text-sm\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n (keydown)=\"onKeydown($event)\"\n (click)=\"toggleDropdown()\"\n [class.open]=\"isOpen()\"\n [class.disabled-select]=\"isDisabled()\"\n [class._shk-border-red]=\"shouldShowError()\"\n >\n <div class=\"selected-value truncate\">\n @if (isSearchable() && isOpen()) {\n <input\n #filterInput\n type=\"text\"\n class=\"w-full bg-transparent border-0 outline-none p-0 _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"\n placeholder=\"Buscar...\"\n [value]=\"searchTerm()\"\n (input)=\"onInputChange($event)\"\n (keydown)=\"handleKeyDown($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n } @else {\n <span\n [class._shk-text-surface-400]=\"!hasSelectedValues()\"\n [class._shk-text-surface-800]=\"hasSelectedValues()\"\n >\n {{ displayValue() }}\n </span>\n }\n </div>\n <div\n class=\"text-xs transition-transform duration-200 shrink-0 ml-2\"\n [class.rotate-180]=\"isOpen()\"\n >\n <svg\n [lucideIcon]=\"getIcon('chevron-down')\"\n class=\"w-4 h-4 _shk-text-surface-400 dark:[color:var(--shk-surface-400)]\"\n ></svg>\n </div>\n </div>\n\n @if (isOpen()) {\n <div\n #dropdownContainer\n (keydown)=\"onKeydown($event)\"\n class=\"absolute left-0 right-0 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border _shk-border rounded-lg max-h-[240px] overflow-y-auto z-50 shadow-lg _shk-scrollbar\"\n [class.top-full]=\"!dropdownUpward()\"\n [class.bottom-full]=\"dropdownUpward()\"\n [class.mt-1]=\"!dropdownUpward()\"\n [class.mb-1]=\"dropdownUpward()\"\n >\n @if (showCustomOption()) {\n <div\n class=\"flex items-center gap-2 px-3 py-2 cursor-pointer hover:[background-color:var(--shk-surface-50)] dark:hover:[background-color:var(--shk-surface-200)] text-sm\"\n (click)=\"onCustomOptionClick()\"\n >\n <span class=\"font-semibold _shk-text-primary\">Crear nuevo:</span>\n <span class=\"_shk-text-surface-700 dark:[color:var(--shk-surface-700)] ml-1\">{{ searchTerm() }}</span>\n </div>\n }\n\n @for (option of regularOptions(); track trackByFn($index, option); let i = $index) {\n <div\n class=\"flex items-center gap-2 px-3 py-2 cursor-pointer hover:[background-color:var(--shk-surface-50)] dark:hover:[background-color:var(--shk-surface-200)] text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"\n [class.opacity-50]=\"isOptionDisabled(option)\"\n [class.select-option-active]=\"focusedIndex() === i\"\n [class._shk-text-surface-400]=\"option.value === null\"\n (click)=\"!isOptionDisabled(option) && onOptionClick(option)\"\n >\n @if (multiple()) {\n <input\n type=\"checkbox\"\n [checked]=\"isSelected(option)\"\n [disabled]=\"isOptionDisabled(option)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"!isOptionDisabled(option) && toggleOption(option)\"\n class=\"flex-shrink-0 [accent-color:var(--shk-primary-600)]\"\n />\n {{ option.label }}\n } @else {\n {{ option.label }}\n }\n </div>\n }\n\n <div class=\"px-3 py-2 text-center\">\n @if (isLoading() && regularOptions().length === 0) {\n <div class=\"flex items-center justify-center gap-2 text-sm _shk-text-surface-500\">\n <svg class=\"spinner\" viewBox=\"0 0 50 50\">\n <circle\n class=\"path\"\n cx=\"25\"\n cy=\"25\"\n r=\"20\"\n fill=\"none\"\n stroke-width=\"5\"\n ></circle>\n </svg>\n Cargando m\u00E1s resultados...\n </div>\n }\n\n @if (!isLoading() && isAllDataLoaded() && regularOptions().length === 0) {\n <div class=\"text-sm _shk-text-surface-400 italic\">\n No se encontraron coincidencias\n </div>\n }\n\n @if (options().length === 0) {\n <div class=\"text-sm _shk-text-surface-400 italic\">\n {{ emptyMessageKey() | transloco }}\n </div>\n }\n </div>\n\n @if (usePagination() && totalPages() > 1) {\n <div class=\"flex justify-between items-center px-3 py-2 border-t _shk-border\">\n <button\n class=\"bg-transparent border-0 cursor-pointer px-2 py-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-50 disabled:cursor-not-allowed _shk-text-surface-600 dark:[color:var(--shk-surface-600)]\"\n [disabled]=\"currentPage() === 1\"\n (click)=\"$event.stopPropagation(); previousPage()\"\n >\n <svg [lucideIcon]=\"getIcon('chevron-left')\" class=\"w-4 h-4\"></svg>\n </button>\n <span class=\"text-sm _shk-text-surface-600 dark:[color:var(--shk-surface-600)]\">\n {{ currentPage() }} de {{ totalPages() }}\n </span>\n <button\n class=\"bg-transparent border-0 cursor-pointer px-2 py-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-50 disabled:cursor-not-allowed _shk-text-surface-600 dark:[color:var(--shk-surface-600)]\"\n [disabled]=\"currentPage() === totalPages()\"\n (click)=\"$event.stopPropagation(); nextPage()\"\n >\n <svg [lucideIcon]=\"getIcon('chevron-right')\" class=\"w-4 h-4\"></svg>\n </button>\n </div>\n }\n </div>\n }\n\n @if (shouldShowError()) {\n <div class=\"mt-1 text-xs text-red-500 max-h-20 overflow-y-auto _shk-scrollbar\" tabindex=\"-1\">\n <ul class=\"list-disc pl-4 space-y-1\">\n @for (error of getErrorMessages(); track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n }\n</div>", styles: [".select-option-active{background-color:var(--shk-primary-50);outline:2px solid var(--shk-primary-500);outline-offset:-2px;color:var(--shk-primary-700)}\n"] }]
|
|
727
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], validCombinations: [{ type: i0.Input, args: [{ isSignal: true, alias: "validCombinations", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: false }] }], isAllDataLoaded: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAllDataLoaded", required: false }] }], allowCustomEntries: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomEntries", required: false }] }], formGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "formGroup", required: false }] }], isSearchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isSearchable", required: false }] }], itemsPerPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemsPerPage", required: false }] }], control: [{ type: i0.Input, args: [{ isSignal: true, alias: "control", required: false }] }], usePagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "usePagination", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], errorMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessages", required: false }] }], preserveSearchOnLoad: [{ type: i0.Input, args: [{ isSignal: true, alias: "preserveSearchOnLoad", required: false }] }], emptyMessageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessageKey", required: false }] }], dropdownUpward: [{ type: i0.Input, args: [{ isSignal: true, alias: "dropdownUpward", required: false }] }], showEmptyOption: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEmptyOption", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], search: [{ type: i0.Output, args: ["search"] }], filterInput: [{ type: i0.ViewChild, args: ['filterInput', { isSignal: true }] }], selectContainer: [{ type: i0.ViewChild, args: ['selectContainer', { isSignal: true }] }], selectTrigger: [{ type: i0.ViewChild, args: ['selectTrigger', { isSignal: true }] }], dropdownContainer: [{ type: i0.ViewChild, args: ['dropdownContainer', { isSignal: true }] }], onClickOutside: [{
|
|
728
|
+
type: HostListener,
|
|
729
|
+
args: ['document:mousedown', ['$event']]
|
|
730
|
+
}] } });
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Generated bundle index. Do not edit.
|
|
734
|
+
*/
|
|
735
|
+
|
|
736
|
+
export { SelectComponent };
|
|
737
|
+
//# sourceMappingURL=shirkasoft-ui-components-select.mjs.map
|