@guajiritos/general-autocomplete 20.0.0 → 20.0.2
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.
|
@@ -7,7 +7,7 @@ import { Injectable, Pipe, signal, EventEmitter, forwardRef, Input, Output, View
|
|
|
7
7
|
import * as i6 from '@angular/forms';
|
|
8
8
|
import { UntypedFormControl, Validators, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
9
9
|
import * as i10 from '@angular/material/autocomplete';
|
|
10
|
-
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
|
10
|
+
import { MatAutocompleteModule, MatAutocompleteTrigger } from '@angular/material/autocomplete';
|
|
11
11
|
import * as i8 from '@angular/material/button';
|
|
12
12
|
import { MatButtonModule } from '@angular/material/button';
|
|
13
13
|
import * as i4 from '@angular/material/form-field';
|
|
@@ -20,7 +20,7 @@ import * as i9 from '@angular/material/progress-spinner';
|
|
|
20
20
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
21
21
|
import * as i2 from '@ngx-translate/core';
|
|
22
22
|
import { TranslateModule } from '@ngx-translate/core';
|
|
23
|
-
import { debounceTime, finalize } from 'rxjs';
|
|
23
|
+
import { Subject, debounceTime, tap, switchMap, of, catchError, from, takeUntil, finalize } from 'rxjs';
|
|
24
24
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
25
25
|
|
|
26
26
|
const GENERAL_DISPLAY_OPTIONS = {
|
|
@@ -46,10 +46,10 @@ class UtilsService {
|
|
|
46
46
|
return prev ? prev[curr] : null;
|
|
47
47
|
}, obj || self);
|
|
48
48
|
}
|
|
49
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
50
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
49
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UtilsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
50
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UtilsService, providedIn: 'root' }); }
|
|
51
51
|
}
|
|
52
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
52
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UtilsService, decorators: [{
|
|
53
53
|
type: Injectable,
|
|
54
54
|
args: [{
|
|
55
55
|
providedIn: 'root'
|
|
@@ -78,10 +78,10 @@ class ResolvePropertyPath {
|
|
|
78
78
|
});
|
|
79
79
|
return result;
|
|
80
80
|
}
|
|
81
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
82
|
-
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.
|
|
81
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: ResolvePropertyPath, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
82
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.2", ngImport: i0, type: ResolvePropertyPath, isStandalone: true, name: "resolvePropertyPath" }); }
|
|
83
83
|
}
|
|
84
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
84
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: ResolvePropertyPath, decorators: [{
|
|
85
85
|
type: Pipe,
|
|
86
86
|
args: [{
|
|
87
87
|
name: 'resolvePropertyPath',
|
|
@@ -95,273 +95,557 @@ class GuajiritosGeneralAutocomplete {
|
|
|
95
95
|
this._zone = _zone;
|
|
96
96
|
this._destroyRef = _destroyRef;
|
|
97
97
|
this.translateService = translateService;
|
|
98
|
-
|
|
99
|
-
this.
|
|
100
|
-
this.
|
|
98
|
+
// --- Propiedades Privadas ---
|
|
99
|
+
this._isOptionSelected = false; // Indica si la última actualización del input fue por una selección explícita desde el autocompletado
|
|
100
|
+
this._userInteracted = false;
|
|
101
|
+
this._clearDataSubject = new Subject();
|
|
102
|
+
this._doFocusSubject = new Subject();
|
|
103
|
+
this._selectedElement = null; // Almacena el objeto completo seleccionado
|
|
101
104
|
this._url = null;
|
|
102
|
-
this.
|
|
103
|
-
this.
|
|
104
|
-
this.
|
|
105
|
-
this.
|
|
106
|
-
this.
|
|
107
|
-
this.
|
|
108
|
-
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this.
|
|
114
|
-
this.
|
|
115
|
-
this.
|
|
116
|
-
|
|
105
|
+
this._serviceConfig = null;
|
|
106
|
+
this._limit = 20;
|
|
107
|
+
this._offset = 0;
|
|
108
|
+
this._restrictionsFilters = [];
|
|
109
|
+
this._lastSearchText = ""; // Almacena el texto usado en la última búsqueda de API exitosa
|
|
110
|
+
this._cancelPendingRequest$ = new Subject();
|
|
111
|
+
// --- Señales Públicas (Signals) ---
|
|
112
|
+
this.disabled = signal(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
113
|
+
this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
114
|
+
this.required = signal(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
115
|
+
this.filteredOptions = signal([], ...(ngDevMode ? [{ debugName: "filteredOptions" }] : []));
|
|
116
|
+
this.originalOptions = signal([], ...(ngDevMode ? [{ debugName: "originalOptions" }] : []));
|
|
117
|
+
this.notAllowedOption = signal(null, ...(ngDevMode ? [{ debugName: "notAllowedOption" }] : []));
|
|
118
|
+
this.component = new UntypedFormControl({
|
|
119
|
+
value: null,
|
|
120
|
+
disabled: false,
|
|
121
|
+
});
|
|
122
|
+
this.hasMore = signal(true, ...(ngDevMode ? [{ debugName: "hasMore" }] : [])); // Indica si hay más datos para cargar
|
|
123
|
+
// --- Inputs ---
|
|
124
|
+
this.floatLabel = "auto";
|
|
125
|
+
this.color = "accent";
|
|
126
|
+
this.appearance = "outline";
|
|
127
|
+
this.subscriptSizing = "dynamic";
|
|
117
128
|
this.debounceTimeValue = 300;
|
|
118
|
-
this.label =
|
|
129
|
+
this.label = "Seleccione";
|
|
119
130
|
this.showLabel = true;
|
|
120
|
-
this.placeholder =
|
|
121
|
-
this.field = [
|
|
122
|
-
this.filterString =
|
|
131
|
+
this.placeholder = "Seleccione un elemento";
|
|
132
|
+
this.field = ["name"];
|
|
133
|
+
this.filterString = "filter[$and][name][$like]";
|
|
123
134
|
this.displayOptions = GENERAL_DISPLAY_OPTIONS;
|
|
124
135
|
this.withoutPaddingBottom = true;
|
|
125
136
|
this.valueId = false;
|
|
126
137
|
this.showSuffix = false;
|
|
127
138
|
this.requireSelection = false;
|
|
128
|
-
this.suffixIcon =
|
|
139
|
+
this.suffixIcon = "search";
|
|
129
140
|
this.removeProperties = [];
|
|
130
|
-
this.modifyResultFn = () =>
|
|
141
|
+
this.modifyResultFn = (options) => options;
|
|
142
|
+
// --- Outputs ---
|
|
131
143
|
this.SelectElement = new EventEmitter();
|
|
132
144
|
this.clearElement = new EventEmitter();
|
|
133
|
-
|
|
134
|
-
};
|
|
145
|
+
// --- Métodos de ControlValueAccessor ---
|
|
146
|
+
this.propagateChange = (_) => { };
|
|
135
147
|
/**
|
|
136
|
-
* Función para mostrar los elementos
|
|
137
|
-
*
|
|
138
|
-
* @param value - Valor a mostrar
|
|
148
|
+
* Función para mostrar los elementos seleccionados
|
|
139
149
|
*/
|
|
140
150
|
this.displayFn = (value) => {
|
|
141
|
-
if (value) {
|
|
142
|
-
|
|
143
|
-
|
|
151
|
+
if (!value) {
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
// Si el valor ya es una cadena, devuélvelo directamente
|
|
155
|
+
if (typeof value === "string") {
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
let displayText = "";
|
|
159
|
+
const options = this.displayOptions || GENERAL_DISPLAY_OPTIONS;
|
|
160
|
+
options?.firthLabel?.forEach((field) => {
|
|
161
|
+
if (field?.type === DisplayOptionItemType.PATH) {
|
|
162
|
+
displayText +=
|
|
163
|
+
UtilsService.resolvePropertyByPath(value, field?.path) || "";
|
|
144
164
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this.displayOptions = GENERAL_DISPLAY_OPTIONS;
|
|
165
|
+
else {
|
|
166
|
+
displayText += field?.divider || "";
|
|
148
167
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
displayText += UtilsService.resolvePropertyByPath(value, field?.path);
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
displayText += field?.divider;
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
return displayText;
|
|
158
|
-
}
|
|
168
|
+
});
|
|
169
|
+
return displayText;
|
|
159
170
|
};
|
|
160
171
|
}
|
|
172
|
+
// --- Setters para Inputs ---
|
|
161
173
|
set url(data) {
|
|
162
174
|
if (data) {
|
|
163
175
|
this._url = data;
|
|
164
|
-
|
|
176
|
+
// Suscribirse a los cambios del componente una vez que la URL está disponible
|
|
177
|
+
this.subscribeToComponentChanges();
|
|
165
178
|
}
|
|
166
179
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
this.component.setValue(null, { emitEvent: false });
|
|
175
|
-
this.selectedElement = null;
|
|
176
|
-
this.SelectElement.emit(null);
|
|
177
|
-
this.filteredOptions.set([]);
|
|
178
|
-
this.propagateChange(null);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
180
|
+
// --- Setters para Inputs ---
|
|
181
|
+
set serviceConfig(data) {
|
|
182
|
+
if (data) {
|
|
183
|
+
console.log(data);
|
|
184
|
+
this._serviceConfig = data;
|
|
185
|
+
// Suscribirse a los cambios del componente una vez que la URL está disponible
|
|
186
|
+
this.subscribeToComponentChanges();
|
|
181
187
|
}
|
|
182
188
|
}
|
|
189
|
+
set limit(value) {
|
|
190
|
+
if (value && !isNaN(value)) {
|
|
191
|
+
this._limit = value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
set clearData(value) {
|
|
195
|
+
this._clearDataSubject = value;
|
|
196
|
+
this._clearDataSubject
|
|
197
|
+
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
198
|
+
.subscribe({
|
|
199
|
+
next: () => {
|
|
200
|
+
this.clearInternalState();
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
183
204
|
set initialValue(value) {
|
|
184
|
-
|
|
205
|
+
// Si el valor inicial es el mismo que el valor actual del control, no hacer nada para evitar bucles
|
|
206
|
+
if (value === this.component.value) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this._selectedElement = value;
|
|
210
|
+
this._isOptionSelected = !!value; // Marcar como seleccionado si hay un valor inicial
|
|
211
|
+
// Establecer el valor en el formControl sin emitir el evento
|
|
212
|
+
// para evitar que subscribeToComponentChanges reaccione a esta carga inicial.
|
|
213
|
+
this.component.setValue(value, { emitEvent: false });
|
|
214
|
+
// Actualizar _lastSearchText basado en el valor inicial
|
|
215
|
+
if (value) {
|
|
216
|
+
this._lastSearchText = this.displayFn(value);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
this._lastSearchText = "";
|
|
220
|
+
}
|
|
185
221
|
}
|
|
186
222
|
/**
|
|
187
|
-
* Añade o elimina restricciones
|
|
188
|
-
*
|
|
189
|
-
* @param restrictions - Restricciones para la búsqueda
|
|
223
|
+
* Añade o elimina restricciones de búsqueda
|
|
190
224
|
*/
|
|
191
225
|
set restrictions(restrictions) {
|
|
192
|
-
|
|
193
|
-
|
|
226
|
+
// Using stringify for simple object comparison. If restrictions become complex, a more robust method is needed.
|
|
227
|
+
if (JSON.stringify(this._restrictionsFilters) === JSON.stringify(restrictions)) {
|
|
228
|
+
return;
|
|
194
229
|
}
|
|
195
|
-
|
|
196
|
-
|
|
230
|
+
this._restrictionsFilters = restrictions?.length ? [...restrictions] : [];
|
|
231
|
+
this.resetAutocompleteState();
|
|
232
|
+
const currentSearchText = this.getAutocompleteSearchText();
|
|
233
|
+
// Only re-trigger a search if there was already text in the input.
|
|
234
|
+
// If the input was empty, we'll wait for user interaction (opening the panel).
|
|
235
|
+
if (currentSearchText) {
|
|
236
|
+
// No actualizar _lastSearchText aquí, se actualiza después de la API.
|
|
237
|
+
this.getAutocompleteByTextHandler(currentSearchText);
|
|
197
238
|
}
|
|
198
239
|
}
|
|
199
240
|
/**
|
|
200
|
-
* Añade o elimina la validación de
|
|
201
|
-
*
|
|
202
|
-
* @param required - Define si es requerido o no
|
|
241
|
+
* Añade o elimina la validación de requerido
|
|
203
242
|
*/
|
|
204
243
|
set isRequired(required) {
|
|
205
244
|
this.required.set(required);
|
|
245
|
+
const validators = [autocompleteValidator];
|
|
206
246
|
if (required) {
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
this.component.clearValidators();
|
|
211
|
-
this.component.setValidators([autocompleteValidator]);
|
|
247
|
+
validators.push(Validators.required);
|
|
212
248
|
}
|
|
249
|
+
this.component.setValidators(validators);
|
|
213
250
|
this.component.updateValueAndValidity();
|
|
251
|
+
this.component.markAsDirty();
|
|
252
|
+
this.component.markAsTouched();
|
|
214
253
|
}
|
|
215
254
|
/**
|
|
216
|
-
* Define si
|
|
217
|
-
*
|
|
218
|
-
* @param focusSubject - Observable para la subscripción al evento Focus
|
|
255
|
+
* Define si se realiza una búsqueda cuando el elemento está en foco
|
|
219
256
|
*/
|
|
220
257
|
set doFocus(focusSubject) {
|
|
221
|
-
this.
|
|
222
|
-
|
|
223
|
-
this.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
258
|
+
this._doFocusSubject = focusSubject;
|
|
259
|
+
this._doFocusSubject
|
|
260
|
+
.pipe(debounceTime(this.debounceTimeValue), // Retrasar para evitar llamadas excesivas
|
|
261
|
+
takeUntilDestroyed(this._destroyRef))
|
|
262
|
+
.subscribe({
|
|
263
|
+
next: () => {
|
|
264
|
+
// SOLO ejecutar si el usuario ya ha interactu-ado con el componente.
|
|
265
|
+
// Esto previene que un focus programático inicial (ej: al renderizar una tabla)
|
|
266
|
+
// dispare una búsqueda no deseada.
|
|
267
|
+
if (!this._userInteracted) {
|
|
268
|
+
return;
|
|
232
269
|
}
|
|
233
|
-
|
|
234
|
-
|
|
270
|
+
this._zone.run(() => {
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
this.inputText?.nativeElement?.focus();
|
|
273
|
+
// Simular evento 'input' para disparar valueChanges, pero solo si el input está vacío
|
|
274
|
+
// o si ya hay un valor pero no es el seleccionado (usuario queriendo buscar de nuevo).
|
|
275
|
+
if (!this.component.value ||
|
|
276
|
+
(typeof this.component.value === "string" &&
|
|
277
|
+
!this._selectedElement)) {
|
|
278
|
+
this.inputText?.nativeElement?.dispatchEvent(new Event("input"));
|
|
279
|
+
}
|
|
280
|
+
this.autocompleteTrigger?.openPanel();
|
|
281
|
+
}, 50); // Un pequeño retraso para asegurar el foco y la apertura
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
});
|
|
235
285
|
}
|
|
236
286
|
set notAllowedElements(element) {
|
|
237
287
|
if (element) {
|
|
238
288
|
this.notAllowedOption.set(element);
|
|
239
|
-
|
|
240
|
-
this.filteredOptions.set(this.originalOptions()?.filter((option) => JSON.stringify(option) !== JSON.stringify(this.notAllowedOption())));
|
|
241
|
-
}
|
|
289
|
+
this.filterOptionsBasedOnNotAllowed();
|
|
242
290
|
}
|
|
243
291
|
}
|
|
244
292
|
/**
|
|
245
|
-
*
|
|
293
|
+
* Reinicia el estado de paginación y opciones del autocompletado.
|
|
294
|
+
* Centraliza la lógica para evitar duplicación.
|
|
246
295
|
*/
|
|
247
|
-
|
|
296
|
+
resetAutocompleteState() {
|
|
297
|
+
this._offset = 0;
|
|
298
|
+
this.originalOptions.set([]);
|
|
299
|
+
this.filteredOptions.set([]);
|
|
300
|
+
this.hasMore.set(true);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Suscripción a los cambios del input de búsqueda
|
|
304
|
+
*/
|
|
305
|
+
subscribeToComponentChanges() {
|
|
248
306
|
this.component.valueChanges
|
|
249
|
-
.pipe(
|
|
307
|
+
.pipe(
|
|
308
|
+
// TAP se ejecuta inmediatamente ANTES del debounce.
|
|
309
|
+
// Esto es crucial para detectar borrados de texto en tiempo real y desvincular _selectedElement.
|
|
310
|
+
tap((value) => {
|
|
311
|
+
const currentInputText = this.getAutocompleteSearchText();
|
|
312
|
+
// Si el valor actual del control es un STRING (usuario escribiendo/borrando)
|
|
313
|
+
// Y previamente había un objeto seleccionado (_selectedElement)
|
|
314
|
+
// Y el texto en el input ya no coincide con el displayFn del _selectedElement,
|
|
315
|
+
// entonces desvinculamos el _selectedElement y marcamos que no hay selección.
|
|
316
|
+
if (typeof value === "string" &&
|
|
317
|
+
this._selectedElement &&
|
|
318
|
+
currentInputText !== this.displayFn(this._selectedElement)) {
|
|
319
|
+
this._selectedElement = null; // Quitar la referencia al objeto seleccionado
|
|
320
|
+
this._isOptionSelected = false; // Marcar que no hay una opción seleccionada
|
|
321
|
+
}
|
|
322
|
+
else if (typeof value === "string") {
|
|
323
|
+
// Si el valor es un string, simplemente marcamos que no hay opción seleccionada.
|
|
324
|
+
// Esto cubre los casos donde el usuario escribe por primera vez o borra sin haber seleccionado.
|
|
325
|
+
this._isOptionSelected = false;
|
|
326
|
+
}
|
|
327
|
+
}), debounceTime(this.debounceTimeValue), // El debounce se aplica después de la lógica del tap
|
|
328
|
+
switchMap((value) => {
|
|
329
|
+
this._cancelPendingRequest$.next(); // Cancelar cualquier petición anterior
|
|
330
|
+
const currentSearchText = this.getAutocompleteSearchText();
|
|
331
|
+
// Caso 1: El valor es un objeto y coincide con el elemento seleccionado.
|
|
332
|
+
if (typeof value === "object" &&
|
|
333
|
+
value !== null &&
|
|
334
|
+
this._selectedElement === value) {
|
|
335
|
+
this._lastSearchText = this.displayFn(value);
|
|
336
|
+
return of(null);
|
|
337
|
+
}
|
|
338
|
+
// Caso 2: El usuario ha terminado de escribir o borrar texto.
|
|
339
|
+
if (currentSearchText !== this._lastSearchText) {
|
|
340
|
+
this.resetAutocompleteState();
|
|
341
|
+
this._lastSearchText = currentSearchText;
|
|
342
|
+
const source$ = this.getAutocompleteByTextHandler(currentSearchText, true);
|
|
343
|
+
return source$.pipe(catchError(() => {
|
|
344
|
+
// Al atrapar el error aquí, evitamos que la cadena principal de valueChanges se rompa.
|
|
345
|
+
// Devolvemos un observable vacío para que la suscripción continúe escuchando cambios.
|
|
346
|
+
return of(null);
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
// Si el texto es el mismo, pero el panel está abierto, sin opciones y hay más datos potenciales,
|
|
351
|
+
// podemos necesitar una recarga.
|
|
352
|
+
if (this.matAutocomplete._isOpen &&
|
|
353
|
+
this.originalOptions().length === 0 &&
|
|
354
|
+
this.hasMore() &&
|
|
355
|
+
!this.loading()) {
|
|
356
|
+
return this.getAutocompleteByTextHandler(currentSearchText, true);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return of(null);
|
|
360
|
+
}), takeUntilDestroyed(this._destroyRef))
|
|
250
361
|
.subscribe({
|
|
251
|
-
next: () => {
|
|
252
|
-
if (!
|
|
253
|
-
|
|
362
|
+
next: (result) => {
|
|
363
|
+
if (!result) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const newData = result?.payload?.data ?? result?.data ?? [];
|
|
367
|
+
if (this._offset === 0) {
|
|
368
|
+
this.originalOptions.set(newData);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
this.originalOptions.update((currentOptions) => [
|
|
372
|
+
...currentOptions,
|
|
373
|
+
...newData,
|
|
374
|
+
]);
|
|
375
|
+
}
|
|
376
|
+
this._offset += newData.length;
|
|
377
|
+
if (newData.length < this._limit) {
|
|
378
|
+
this.hasMore.set(false);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
this.hasMore.set(true);
|
|
382
|
+
}
|
|
383
|
+
this.filterOptionsBasedOnNotAllowed();
|
|
384
|
+
},
|
|
385
|
+
error: (error) => {
|
|
386
|
+
this.loading.set(false);
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
ngAfterViewInit() {
|
|
391
|
+
// Suscribirse a la apertura del panel para añadir el listener de scroll y cargar datos
|
|
392
|
+
this.matAutocomplete.opened
|
|
393
|
+
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
394
|
+
.subscribe(() => {
|
|
395
|
+
this._cancelPendingRequest$.next(); // Cancelar cualquier petición anterior
|
|
396
|
+
if (this.matAutocomplete?.panel) {
|
|
397
|
+
// Asegurarse de que el listener sea removido antes de añadirlo para prevenir duplicados
|
|
398
|
+
this.matAutocomplete.panel.nativeElement.removeEventListener("scroll", this.onScroll.bind(this));
|
|
399
|
+
this.matAutocomplete.panel.nativeElement.addEventListener("scroll", this.onScroll.bind(this));
|
|
400
|
+
const currentSearchText = this.getAutocompleteSearchText();
|
|
401
|
+
// **********************************************
|
|
402
|
+
// MODIFICACIÓN CLAVE AQUÍ:
|
|
403
|
+
// Cargar opciones iniciales SOLO SI:
|
|
404
|
+
// 1. No hay opciones cargadas (primera apertura o después de un clear/reset).
|
|
405
|
+
// 2. O el texto actual en el input es diferente del _lastSearchText (el usuario escribió algo nuevo o borró y abrió).
|
|
406
|
+
// 3. O el input está vacío Y no hay un _selectedElement Y hay más datos por cargar Y el offset es 0 (lo que implica que es la primera carga para este estado vacío).
|
|
407
|
+
// Esto evita la doble llamada si subscribeToComponentChanges ya disparó una búsqueda
|
|
408
|
+
// para el mismo texto, o si ya se sabe que no hay más datos para un input vacío.
|
|
409
|
+
// **********************************************
|
|
410
|
+
if (this.originalOptions().length === 0 || // No hay opciones, cargar siempre.
|
|
411
|
+
currentSearchText !== this._lastSearchText || // El texto ha cambiado, nueva búsqueda.
|
|
412
|
+
(currentSearchText === "" &&
|
|
413
|
+
!this._selectedElement &&
|
|
414
|
+
this.hasMore() &&
|
|
415
|
+
this._offset === 0) // Input vacío, sin selección, con potencial de datos, y es la primera carga.
|
|
416
|
+
) {
|
|
417
|
+
this.resetAutocompleteState(); // Asegurarse de resetear antes de una nueva carga
|
|
418
|
+
this._lastSearchText = currentSearchText; // Sincronizar _lastSearchText con la búsqueda que se va a realizar
|
|
419
|
+
this.getAutocompleteByTextHandler(currentSearchText);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
254
422
|
}
|
|
255
|
-
|
|
256
|
-
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
// Suscribirse al cierre del panel para remover el listener de scroll y manejar la deselección
|
|
426
|
+
this.matAutocomplete.closed
|
|
427
|
+
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
428
|
+
.subscribe(() => {
|
|
429
|
+
if (this.matAutocomplete?.panel) {
|
|
430
|
+
this.matAutocomplete.panel.nativeElement.removeEventListener("scroll", this.onScroll.bind(this));
|
|
431
|
+
}
|
|
432
|
+
const currentInputValue = this.component.value;
|
|
433
|
+
const currentInputText = this.getAutocompleteSearchText();
|
|
434
|
+
// Lógica para manejar el borrado del input si no hay una selección válida
|
|
435
|
+
// o si el texto no coincide con el elemento seleccionado.
|
|
436
|
+
if (typeof currentInputValue === "string") {
|
|
437
|
+
// Si el input está vacío Y no hay un elemento seleccionado
|
|
438
|
+
if (currentInputText === "" && !this._selectedElement) {
|
|
439
|
+
this.clearInternalState(); // Usa un método para limpiar
|
|
440
|
+
}
|
|
441
|
+
// Si se requiere selección y el texto no coincide con el elemento seleccionado,
|
|
442
|
+
// o no hay ningún elemento seleccionado, entonces se borra.
|
|
443
|
+
else if (this.requireSelection &&
|
|
444
|
+
(!this._selectedElement ||
|
|
445
|
+
currentInputText !== this.displayFn(this._selectedElement))) {
|
|
446
|
+
console.warn("Autocompletado cerrado: Selección requerida y no se encontró una coincidencia válida. Limpiando input.");
|
|
447
|
+
this.clearInternalState(); // Usa un método para limpiar
|
|
448
|
+
}
|
|
449
|
+
// Si NO se requiere selección, pero el usuario ha editado el texto de un elemento previamente seleccionado.
|
|
450
|
+
// En este caso, NO borramos, sino que revertimos al texto del elemento seleccionado si existe.
|
|
451
|
+
else if (this._selectedElement &&
|
|
452
|
+
currentInputText !== this.displayFn(this._selectedElement) &&
|
|
453
|
+
!this.requireSelection) {
|
|
454
|
+
// Revertir al texto del elemento seleccionado.
|
|
455
|
+
this.component.setValue(this._selectedElement, {
|
|
456
|
+
emitEvent: false,
|
|
457
|
+
});
|
|
458
|
+
this._lastSearchText = this.displayFn(this._selectedElement);
|
|
459
|
+
this.propagateChange(this._selectedElement); // Propagar el valor original si se revierte
|
|
460
|
+
}
|
|
461
|
+
// Si el texto del input es un string y no se requiere selección,
|
|
462
|
+
// y no hay _selectedElement (porque se deseleccionó en el tap o nunca hubo),
|
|
463
|
+
// asumimos que el usuario quiere el texto libre.
|
|
464
|
+
// No hacemos nada, el texto permanece.
|
|
465
|
+
else if (typeof currentInputValue === "string" &&
|
|
466
|
+
!this.requireSelection &&
|
|
467
|
+
!this._selectedElement &&
|
|
468
|
+
currentInputText !== "") {
|
|
469
|
+
// Si no hay un elemento seleccionado pero el usuario escribió algo
|
|
470
|
+
// y no se requiere selección, dejamos el texto tal cual.
|
|
471
|
+
// Aseguramos que _lastSearchText se actualice para evitar re-búsquedas innecesarias.
|
|
472
|
+
this._lastSearchText = currentInputText;
|
|
473
|
+
// this.propagateChange(currentInputText);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// Si el valor ya es un objeto (porque se seleccionó) pero el texto del input fue borrado
|
|
477
|
+
// y el _selectedElement es null (ya se desvinculó en el tap), significa que debe limpiarse.
|
|
478
|
+
else if (typeof currentInputValue === "object" &&
|
|
479
|
+
currentInputText === "" &&
|
|
480
|
+
!this._selectedElement) {
|
|
481
|
+
this.clearInternalState();
|
|
257
482
|
}
|
|
258
483
|
});
|
|
259
484
|
}
|
|
260
485
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* @param
|
|
486
|
+
* Método auxiliar para limpiar el estado interno del autocompletado.
|
|
487
|
+
* Este método ahora se encarga de la limpieza profunda, incluyendo el valor del FormControl y la propagación.
|
|
488
|
+
* @param resetLastSearchText - Si es true, reinicia _lastSearchText a una cadena vacía. Por defecto es true.
|
|
264
489
|
*/
|
|
265
|
-
|
|
266
|
-
|
|
490
|
+
clearInternalState(resetLastSearchText = true) {
|
|
491
|
+
// Solo si el valor del control NO es null, lo seteamos a null para evitar bucles.
|
|
492
|
+
// Esto es importante porque en el tap, solo desvinculamos `_selectedElement` pero no el control.
|
|
493
|
+
if (this.component.value !== null) {
|
|
494
|
+
this.component.setValue(null, { emitEvent: false });
|
|
495
|
+
}
|
|
267
496
|
this.propagateChange(null);
|
|
268
|
-
this.selectedElement = null;
|
|
269
497
|
this.SelectElement.emit(null);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
498
|
+
this._selectedElement = null; // Asegurar que el objeto seleccionado esté a null
|
|
499
|
+
this._isOptionSelected = false; // Asegurar que la bandera de selección esté a false
|
|
500
|
+
if (resetLastSearchText) {
|
|
501
|
+
this._lastSearchText = "";
|
|
502
|
+
}
|
|
503
|
+
this.resetAutocompleteState(); // Resetear opciones y paginación
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Busca elementos para mostrar en el componente de autocompletado.
|
|
507
|
+
* Ahora soporta paginación.
|
|
508
|
+
* @param text - Texto a buscar
|
|
509
|
+
*/
|
|
510
|
+
getAutocompleteByTextHandler(text, returnObservable = false) {
|
|
511
|
+
const currentSearchText = text ?? "";
|
|
512
|
+
if (this.loading() || !this._userInteracted) {
|
|
513
|
+
return returnObservable ? of(null) : undefined;
|
|
514
|
+
}
|
|
515
|
+
if (!this.hasMore() &&
|
|
516
|
+
this._offset > 0 &&
|
|
517
|
+
currentSearchText === this._lastSearchText) {
|
|
518
|
+
return returnObservable ? of(null) : undefined;
|
|
519
|
+
}
|
|
520
|
+
this.loading.set(true);
|
|
521
|
+
const currentOffset = this._offset;
|
|
522
|
+
if (!this._url && !this._serviceConfig) {
|
|
523
|
+
this.loading.set(false);
|
|
524
|
+
this.hasMore.set(false);
|
|
525
|
+
return returnObservable ? of(null) : undefined;
|
|
526
|
+
}
|
|
527
|
+
let apiCall;
|
|
528
|
+
if (this._url) {
|
|
529
|
+
apiCall = from(this._autocompleteService.getAutocompleteByText(this._url, currentSearchText, this.filterString, this._restrictionsFilters, this.removeProperties, this.order, this.bodyRequest, { limit: this._limit, offset: currentOffset })).pipe(switchMap((innerObservable) => innerObservable));
|
|
530
|
+
}
|
|
531
|
+
else if (this._serviceConfig) {
|
|
532
|
+
const body = { ...this._serviceConfig };
|
|
533
|
+
body[this._serviceConfig.searchProperty] = currentSearchText;
|
|
534
|
+
body["limit"] = this._limit;
|
|
535
|
+
body["offset"] = currentOffset;
|
|
536
|
+
Object.keys(this._serviceConfig.postBody).forEach((item) => {
|
|
537
|
+
body[item] = this._serviceConfig.postBody[item];
|
|
289
538
|
});
|
|
539
|
+
console.log(body);
|
|
540
|
+
delete body.service;
|
|
541
|
+
delete body.postBody;
|
|
542
|
+
apiCall = this._serviceConfig.service[this._serviceConfig.method](body);
|
|
290
543
|
}
|
|
291
544
|
else {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
545
|
+
this.loading.set(false);
|
|
546
|
+
this.hasMore.set(false);
|
|
547
|
+
return returnObservable ? of(null) : undefined;
|
|
548
|
+
}
|
|
549
|
+
let panelScrollTop = 0;
|
|
550
|
+
if (this.matAutocomplete?.panel) {
|
|
551
|
+
panelScrollTop = this.matAutocomplete.panel.nativeElement.scrollTop;
|
|
552
|
+
}
|
|
553
|
+
const finalApiCall$ = apiCall.pipe(takeUntil(this._cancelPendingRequest$), finalize(() => {
|
|
554
|
+
this.loading.set(false);
|
|
555
|
+
}));
|
|
556
|
+
if (returnObservable) {
|
|
557
|
+
return finalApiCall$;
|
|
558
|
+
}
|
|
559
|
+
finalApiCall$.pipe(takeUntilDestroyed(this._destroyRef)).subscribe({
|
|
560
|
+
next: (result) => {
|
|
561
|
+
const newData = result?.payload?.data ?? result?.data ?? [];
|
|
562
|
+
if (currentOffset === 0) {
|
|
563
|
+
this.originalOptions.set(newData);
|
|
310
564
|
}
|
|
311
|
-
|
|
565
|
+
else {
|
|
566
|
+
this.originalOptions.update((currentOptions) => [
|
|
567
|
+
...currentOptions,
|
|
568
|
+
...newData,
|
|
569
|
+
]);
|
|
570
|
+
}
|
|
571
|
+
this._offset += newData.length;
|
|
572
|
+
if (newData.length < this._limit) {
|
|
573
|
+
this.hasMore.set(false);
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
this.hasMore.set(true);
|
|
577
|
+
}
|
|
578
|
+
this.filterOptionsBasedOnNotAllowed();
|
|
579
|
+
if (this.matAutocomplete?.panel && panelScrollTop > 0) {
|
|
580
|
+
this._zone.runOutsideAngular(() => {
|
|
581
|
+
setTimeout(() => {
|
|
582
|
+
this.matAutocomplete.panel.nativeElement.scrollTop =
|
|
583
|
+
panelScrollTop;
|
|
584
|
+
}, 0);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
error: (error) => {
|
|
589
|
+
console.error("Error al obtener datos de autocompletado:", error);
|
|
590
|
+
this.loading.set(false);
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
filterOptionsBasedOnNotAllowed() {
|
|
595
|
+
let currentOptions = this.originalOptions();
|
|
596
|
+
const modifiedOptions = this.modifyResultFn(currentOptions);
|
|
597
|
+
if (modifiedOptions !== null && modifiedOptions !== undefined) {
|
|
598
|
+
currentOptions = modifiedOptions;
|
|
599
|
+
}
|
|
600
|
+
if (this.notAllowedOption()) {
|
|
601
|
+
this.filteredOptions.set(currentOptions.filter((option) => JSON.stringify(option) !== JSON.stringify(this.notAllowedOption())));
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
this.filteredOptions.set(currentOptions);
|
|
312
605
|
}
|
|
313
606
|
}
|
|
314
607
|
/**
|
|
315
|
-
* Define el texto
|
|
316
|
-
*
|
|
317
|
-
* @return {string} Texto de la búsqueda
|
|
608
|
+
* Define el texto a utilizar para la búsqueda
|
|
318
609
|
*/
|
|
319
610
|
getAutocompleteSearchText() {
|
|
320
|
-
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (typeof componentValue === 'object') {
|
|
325
|
-
let lang = 'es';
|
|
326
|
-
if (this.field?.[1]) {
|
|
327
|
-
lang = this.field?.[1];
|
|
328
|
-
}
|
|
329
|
-
text = componentValue?.[lang];
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
else if (typeof this.component.value === 'string') {
|
|
333
|
-
text = this.component.value;
|
|
334
|
-
}
|
|
611
|
+
const value = this.component.value;
|
|
612
|
+
if (typeof value === "object" && value !== null) {
|
|
613
|
+
// Si el valor es un objeto, usa displayFn para obtener el texto
|
|
614
|
+
return this.displayFn(value);
|
|
335
615
|
}
|
|
336
|
-
|
|
616
|
+
else if (typeof value === "string") {
|
|
617
|
+
return value;
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
337
620
|
}
|
|
338
621
|
registerOnChange(fn) {
|
|
339
622
|
this.propagateChange = fn;
|
|
340
623
|
}
|
|
341
|
-
registerOnTouched() {
|
|
342
|
-
}
|
|
624
|
+
registerOnTouched() { }
|
|
343
625
|
/**
|
|
344
|
-
* Recibe el valor
|
|
345
|
-
*
|
|
346
|
-
* @param value - Valor entrado por FormControl
|
|
626
|
+
* Recibe el valor desde el FormControl externo.
|
|
347
627
|
*/
|
|
348
628
|
writeValue(value) {
|
|
629
|
+
// Establecer la bandera para indicar que el cambio viene de un setValue programático
|
|
630
|
+
this._isOptionSelected = typeof value === "object" && value !== null;
|
|
631
|
+
this.component.setValue(value, { emitEvent: false }); // No emitir evento para evitar bucles.
|
|
632
|
+
this._selectedElement = value
|
|
633
|
+
? this.valueId && typeof value === "object"
|
|
634
|
+
? value?.id
|
|
635
|
+
: value
|
|
636
|
+
: null;
|
|
637
|
+
// Actualizar _lastSearchText basado en el valor que se está escribiendo.
|
|
638
|
+
// Si el valor es nulo, _lastSearchText también debe ser nulo o vacío
|
|
349
639
|
if (value) {
|
|
350
|
-
this.
|
|
351
|
-
if (typeof value === 'object' && this.valueId) {
|
|
352
|
-
this.selectedElement = value?.id;
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
this.selectedElement = value;
|
|
356
|
-
}
|
|
640
|
+
this._lastSearchText = this.displayFn(value);
|
|
357
641
|
}
|
|
358
642
|
else {
|
|
359
|
-
this.
|
|
643
|
+
this._lastSearchText = "";
|
|
360
644
|
}
|
|
361
645
|
}
|
|
362
646
|
setDisabledState(isDisabled) {
|
|
363
647
|
this.disabled.set(isDisabled);
|
|
364
|
-
if (
|
|
648
|
+
if (isDisabled) {
|
|
365
649
|
this.component.disable();
|
|
366
650
|
}
|
|
367
651
|
else {
|
|
@@ -370,57 +654,77 @@ class GuajiritosGeneralAutocomplete {
|
|
|
370
654
|
}
|
|
371
655
|
/**
|
|
372
656
|
* Acción al limpiar el valor del input
|
|
373
|
-
*
|
|
374
|
-
* @param trigger
|
|
375
657
|
*/
|
|
376
658
|
clear(trigger) {
|
|
377
659
|
this.clearElement.emit(this.component.value);
|
|
378
|
-
this.
|
|
379
|
-
this.selectedElement = null;
|
|
380
|
-
this.SelectElement.emit(null);
|
|
381
|
-
this.propagateChange(null);
|
|
660
|
+
this.clearInternalState(); // Usa el método auxiliar para limpiar
|
|
382
661
|
this._zone.run(() => {
|
|
383
662
|
setTimeout(() => {
|
|
384
|
-
trigger.openPanel();
|
|
663
|
+
trigger.openPanel(); // Reabrir el panel después de borrar
|
|
664
|
+
this.component.setValue("", { emitEvent: true }); // Cargar opciones iniciales (o todas) después de borrar
|
|
385
665
|
}, 100);
|
|
386
666
|
});
|
|
387
667
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
668
|
+
onUserInteraction() {
|
|
669
|
+
this._userInteracted = true;
|
|
670
|
+
// Si el panel ya está abierto, no hacer nada para evitar bucles o recargas innecesarias al volver a hacer clic.
|
|
671
|
+
if (this.autocompleteTrigger?.panelOpen) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
this.resetAutocompleteState();
|
|
675
|
+
const searchText = this.getAutocompleteSearchText() ?? "";
|
|
676
|
+
if (!this._selectedElement) {
|
|
677
|
+
this.getAutocompleteByTextHandler(searchText);
|
|
394
678
|
}
|
|
395
679
|
}
|
|
396
680
|
optionSelected($event) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
this.
|
|
400
|
-
this.
|
|
401
|
-
|
|
402
|
-
|
|
681
|
+
const selectedValue = $event?.option?.value;
|
|
682
|
+
if (selectedValue) {
|
|
683
|
+
this._isOptionSelected = true; // Establecer la bandera de selección
|
|
684
|
+
this._selectedElement = selectedValue;
|
|
685
|
+
this.SelectElement.emit(selectedValue);
|
|
686
|
+
if (this.valueId && typeof selectedValue === "object") {
|
|
687
|
+
this.propagateChange(selectedValue?.id);
|
|
403
688
|
}
|
|
404
689
|
else {
|
|
405
|
-
this.propagateChange(
|
|
690
|
+
this.propagateChange(selectedValue);
|
|
406
691
|
}
|
|
692
|
+
// Cuando un ítem es seleccionado, actualizar _lastSearchText con su valor de display.
|
|
693
|
+
// Esto es crucial para que el valueChanges no vuelva a disparar una búsqueda para este valor.
|
|
694
|
+
this._lastSearchText = this.displayFn(selectedValue);
|
|
407
695
|
}
|
|
408
696
|
else {
|
|
409
|
-
|
|
697
|
+
// Si se deselecciona o selecciona nulo
|
|
698
|
+
this.clearInternalState();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Maneja el evento de scroll en el panel del autocompletado para cargar más datos.
|
|
703
|
+
*/
|
|
704
|
+
onScroll(event) {
|
|
705
|
+
const element = event.target;
|
|
706
|
+
const scrollPosition = element.scrollTop + element.clientHeight;
|
|
707
|
+
const scrollHeight = element.scrollHeight;
|
|
708
|
+
const threshold = 50; // Píxeles antes del final para cargar más
|
|
709
|
+
if (scrollHeight - scrollPosition <= threshold &&
|
|
710
|
+
!this.loading() &&
|
|
711
|
+
this.hasMore()) {
|
|
712
|
+
// Usar el texto de búsqueda actual (que puede ser el texto del ítem seleccionado o lo que el usuario escribió)
|
|
713
|
+
this.getAutocompleteByTextHandler(this.getAutocompleteSearchText());
|
|
410
714
|
}
|
|
411
715
|
}
|
|
412
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
413
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.
|
|
716
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: GuajiritosGeneralAutocomplete, deps: [{ token: i1.AutocompleteService }, { token: i0.NgZone }, { token: i0.DestroyRef }, { token: i2.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
717
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.2", type: GuajiritosGeneralAutocomplete, isStandalone: true, selector: "guajiritos-general-autocomplete", inputs: { floatLabel: "floatLabel", color: "color", appearance: "appearance", subscriptSizing: "subscriptSizing", bodyRequest: "bodyRequest", debounceTimeValue: "debounceTimeValue", detailsTemplate: "detailsTemplate", label: "label", showLabel: "showLabel", placeholder: "placeholder", field: "field", filterString: "filterString", displayOptions: "displayOptions", withoutPaddingBottom: "withoutPaddingBottom", valueId: "valueId", showSuffix: "showSuffix", requireSelection: "requireSelection", order: "order", suffixIcon: "suffixIcon", removeProperties: "removeProperties", modifyResultFn: "modifyResultFn", url: "url", serviceConfig: "serviceConfig", limit: "limit", clearData: "clearData", initialValue: "initialValue", restrictions: "restrictions", isRequired: "isRequired", doFocus: "doFocus", notAllowedElements: "notAllowedElements" }, outputs: { SelectElement: "SelectElement", clearElement: "clearElement" }, providers: [
|
|
414
718
|
{
|
|
415
719
|
provide: NG_VALUE_ACCESSOR,
|
|
416
720
|
useExisting: forwardRef(() => GuajiritosGeneralAutocomplete),
|
|
417
|
-
multi: true
|
|
418
|
-
}
|
|
419
|
-
], viewQueries: [{ propertyName: "inputText", first: true, predicate: ["inputText"], descendants: true, static: true }], ngImport: i0, template: "<mat-form-field
|
|
721
|
+
multi: true,
|
|
722
|
+
},
|
|
723
|
+
], viewQueries: [{ propertyName: "inputText", first: true, predicate: ["inputText"], descendants: true, static: true }, { propertyName: "matAutocomplete", first: true, predicate: ["auto"], descendants: true }, { propertyName: "autocompleteTrigger", first: true, predicate: MatAutocompleteTrigger, descendants: true }], ngImport: i0, template: "<mat-form-field\n [floatLabel]=\"floatLabel\"\n class=\"w-100\"\n [appearance]=\"appearance\"\n [color]=\"color\"\n [subscriptSizing]=\"subscriptSizing\"\n>\n @if (showLabel) {\n <mat-label>{{ label | translate }}</mat-label>\n } @if (showSuffix) {\n <mat-icon matSuffix>{{ suffixIcon ?? \"search\" }}</mat-icon>\n }\n <input\n #inputText\n #trigger=\"matAutocompleteTrigger\"\n (focus)=\"onUserInteraction()\"\n (click)=\"onUserInteraction()\"\n (keydown)=\"onUserInteraction()\"\n [formControl]=\"component\"\n type=\"text\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"placeholder | translate\"\n aria-label=\"autocomplete\"\n autocomplete=\"off\"\n matInput\n [required]=\"required()\"\n />\n @if (!loading() && component.value) {\n <button\n (click)=\"clear(trigger)\"\n [disabled]=\"disabled()\"\n aria-label=\"Clear\"\n mat-icon-button\n matSuffix\n >\n <mat-icon>close</mat-icon>\n </button>\n } @if (loading()) {\n <button aria-label=\"search\" mat-icon-button matSuffix>\n <mat-spinner [value]=\"90\" color=\"accent\" diameter=\"25\"></mat-spinner>\n </button>\n }\n <mat-autocomplete\n #auto=\"matAutocomplete\"\n [displayWith]=\"displayFn\"\n [requireSelection]=\"requireSelection\"\n (optionSelected)=\"optionSelected($event)\"\n >\n @for (option of filteredOptions(); track option) {\n <mat-option [value]=\"option\">\n @if (!displayOptions && !detailsTemplate) {\n {{ option?.name | i18n: translateService.currentLang }}\n } @if (!detailsTemplate) {\n <div class=\"display-options\">\n <span [ngStyle]=\"{'line-height': displayOptions?.secondLabel ? '16px' : ''}\">\n {{\n option | resolvePropertyPath : displayOptions.firthLabel\n | i18n : translateService.currentLang\n }}\n </span>\n @if (displayOptions?.secondLabel) {\n <span class=\"mat-caption\">\n {{\n option | resolvePropertyPath : displayOptions.secondLabel\n | i18n : translateService.currentLang\n }}\n </span>\n }\n </div>\n } @if (detailsTemplate) {\n <ng-container\n *ngTemplateOutlet=\"detailsTemplate; context: { $implicit: option }\"\n ></ng-container>\n }\n </mat-option>\n }\n </mat-autocomplete>\n\n @if (component.invalid && component.touched) {\n <mat-error>\n {{ 'Este campo es requerido.' | translate }}\n </mat-error>\n }\n</mat-form-field>\n", styles: [".w-100{width:100%}.display-options{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i4.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i6.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i6.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i6.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i6.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i7.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i8.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i9.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "ngmodule", type: MatAutocompleteModule }, { kind: "component", type: i10.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "component", type: i10.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i10.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }, { kind: "pipe", type: I18nPipe, name: "i18n" }, { kind: "pipe", type: ResolvePropertyPath, name: "resolvePropertyPath" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
420
724
|
}
|
|
421
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
725
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: GuajiritosGeneralAutocomplete, decorators: [{
|
|
422
726
|
type: Component,
|
|
423
|
-
args: [{ selector:
|
|
727
|
+
args: [{ selector: "guajiritos-general-autocomplete", changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
|
|
424
728
|
CommonModule,
|
|
425
729
|
MatFormFieldModule,
|
|
426
730
|
TranslateModule,
|
|
@@ -431,17 +735,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
|
|
|
431
735
|
MatProgressSpinnerModule,
|
|
432
736
|
MatAutocompleteModule,
|
|
433
737
|
I18nPipe,
|
|
434
|
-
ResolvePropertyPath
|
|
738
|
+
ResolvePropertyPath,
|
|
435
739
|
], providers: [
|
|
436
740
|
{
|
|
437
741
|
provide: NG_VALUE_ACCESSOR,
|
|
438
742
|
useExisting: forwardRef(() => GuajiritosGeneralAutocomplete),
|
|
439
|
-
multi: true
|
|
440
|
-
}
|
|
441
|
-
], template: "<mat-form-field
|
|
743
|
+
multi: true,
|
|
744
|
+
},
|
|
745
|
+
], template: "<mat-form-field\n [floatLabel]=\"floatLabel\"\n class=\"w-100\"\n [appearance]=\"appearance\"\n [color]=\"color\"\n [subscriptSizing]=\"subscriptSizing\"\n>\n @if (showLabel) {\n <mat-label>{{ label | translate }}</mat-label>\n } @if (showSuffix) {\n <mat-icon matSuffix>{{ suffixIcon ?? \"search\" }}</mat-icon>\n }\n <input\n #inputText\n #trigger=\"matAutocompleteTrigger\"\n (focus)=\"onUserInteraction()\"\n (click)=\"onUserInteraction()\"\n (keydown)=\"onUserInteraction()\"\n [formControl]=\"component\"\n type=\"text\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"placeholder | translate\"\n aria-label=\"autocomplete\"\n autocomplete=\"off\"\n matInput\n [required]=\"required()\"\n />\n @if (!loading() && component.value) {\n <button\n (click)=\"clear(trigger)\"\n [disabled]=\"disabled()\"\n aria-label=\"Clear\"\n mat-icon-button\n matSuffix\n >\n <mat-icon>close</mat-icon>\n </button>\n } @if (loading()) {\n <button aria-label=\"search\" mat-icon-button matSuffix>\n <mat-spinner [value]=\"90\" color=\"accent\" diameter=\"25\"></mat-spinner>\n </button>\n }\n <mat-autocomplete\n #auto=\"matAutocomplete\"\n [displayWith]=\"displayFn\"\n [requireSelection]=\"requireSelection\"\n (optionSelected)=\"optionSelected($event)\"\n >\n @for (option of filteredOptions(); track option) {\n <mat-option [value]=\"option\">\n @if (!displayOptions && !detailsTemplate) {\n {{ option?.name | i18n: translateService.currentLang }}\n } @if (!detailsTemplate) {\n <div class=\"display-options\">\n <span [ngStyle]=\"{'line-height': displayOptions?.secondLabel ? '16px' : ''}\">\n {{\n option | resolvePropertyPath : displayOptions.firthLabel\n | i18n : translateService.currentLang\n }}\n </span>\n @if (displayOptions?.secondLabel) {\n <span class=\"mat-caption\">\n {{\n option | resolvePropertyPath : displayOptions.secondLabel\n | i18n : translateService.currentLang\n }}\n </span>\n }\n </div>\n } @if (detailsTemplate) {\n <ng-container\n *ngTemplateOutlet=\"detailsTemplate; context: { $implicit: option }\"\n ></ng-container>\n }\n </mat-option>\n }\n </mat-autocomplete>\n\n @if (component.invalid && component.touched) {\n <mat-error>\n {{ 'Este campo es requerido.' | translate }}\n </mat-error>\n }\n</mat-form-field>\n", styles: [".w-100{width:100%}.display-options{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start}\n"] }]
|
|
442
746
|
}], ctorParameters: () => [{ type: i1.AutocompleteService }, { type: i0.NgZone }, { type: i0.DestroyRef }, { type: i2.TranslateService }], propDecorators: { inputText: [{
|
|
443
747
|
type: ViewChild,
|
|
444
|
-
args: [
|
|
748
|
+
args: ["inputText", { static: true }]
|
|
749
|
+
}], matAutocomplete: [{
|
|
750
|
+
type: ViewChild,
|
|
751
|
+
args: ["auto"]
|
|
752
|
+
}], autocompleteTrigger: [{
|
|
753
|
+
type: ViewChild,
|
|
754
|
+
args: [MatAutocompleteTrigger]
|
|
445
755
|
}], floatLabel: [{
|
|
446
756
|
type: Input
|
|
447
757
|
}], color: [{
|
|
@@ -478,8 +788,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
|
|
|
478
788
|
type: Input
|
|
479
789
|
}], order: [{
|
|
480
790
|
type: Input
|
|
481
|
-
}], serviceConfig: [{
|
|
482
|
-
type: Input
|
|
483
791
|
}], suffixIcon: [{
|
|
484
792
|
type: Input
|
|
485
793
|
}], removeProperties: [{
|
|
@@ -492,6 +800,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
|
|
|
492
800
|
type: Output
|
|
493
801
|
}], url: [{
|
|
494
802
|
type: Input
|
|
803
|
+
}], serviceConfig: [{
|
|
804
|
+
type: Input
|
|
805
|
+
}], limit: [{
|
|
806
|
+
type: Input
|
|
495
807
|
}], clearData: [{
|
|
496
808
|
type: Input
|
|
497
809
|
}], initialValue: [{
|
|
@@ -506,13 +818,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
|
|
|
506
818
|
type: Input
|
|
507
819
|
}] } });
|
|
508
820
|
/**
|
|
509
|
-
* Validación
|
|
821
|
+
* Validación personalizada para la selección de elementos
|
|
510
822
|
*/
|
|
511
823
|
function autocompleteValidator(control) {
|
|
512
|
-
|
|
513
|
-
|
|
824
|
+
// Un valor válido es un objeto no nulo que tiene una propiedad 'id'.
|
|
825
|
+
// Esto asegura que realmente se ha seleccionado un objeto del autocompletado, no solo texto libre.
|
|
826
|
+
// Si el control está vacío, no se aplica esta validación aquí, sino la de 'Validators.required' si está presente.
|
|
827
|
+
if (control.value === null ||
|
|
828
|
+
typeof control.value === "undefined" ||
|
|
829
|
+
control.value === "") {
|
|
830
|
+
return null; // Deja que Validators.required maneje si está vacío.
|
|
831
|
+
}
|
|
832
|
+
if (typeof control?.value === "object" &&
|
|
833
|
+
control?.value !== null &&
|
|
834
|
+
"id" in control.value) {
|
|
835
|
+
return null;
|
|
514
836
|
}
|
|
515
|
-
|
|
837
|
+
// Si el valor es una cadena (texto libre) y debería ser un objeto seleccionado.
|
|
838
|
+
return { invalidSelection: true };
|
|
516
839
|
}
|
|
517
840
|
|
|
518
841
|
/*
|