@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.0.0", ngImport: i0, type: UtilsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
50
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: UtilsService, providedIn: 'root' }); }
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.0.0", ngImport: i0, type: UtilsService, decorators: [{
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.0.0", ngImport: i0, type: ResolvePropertyPath, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
82
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.0.0", ngImport: i0, type: ResolvePropertyPath, isStandalone: true, name: "resolvePropertyPath" }); }
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.0.0", ngImport: i0, type: ResolvePropertyPath, decorators: [{
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
- this.wasSelected = false;
99
- this.firstCall = true;
100
- this.selectedElement = null;
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.restrictionsFilters = [];
103
- this.disabled = signal(false);
104
- this.loading = signal(false);
105
- this.required = signal(false);
106
- this.filteredOptions = signal([]);
107
- this.originalOptions = signal([]);
108
- this.notAllowedOption = signal(null);
109
- this.component = new UntypedFormControl({ value: null, disabled: false });
110
- /**
111
- * Possible values 'never', 'auto' or 'always'
112
- */
113
- this.floatLabel = 'auto';
114
- this.color = 'accent';
115
- this.appearance = 'outline';
116
- this.subscriptSizing = 'dynamic';
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 = 'Seleccione';
129
+ this.label = "Seleccione";
119
130
  this.showLabel = true;
120
- this.placeholder = 'Seleccione un elemento';
121
- this.field = ['name'];
122
- this.filterString = 'filter[$and][name][$like]';
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 = 'search';
139
+ this.suffixIcon = "search";
129
140
  this.removeProperties = [];
130
- this.modifyResultFn = () => null;
141
+ this.modifyResultFn = (options) => options;
142
+ // --- Outputs ---
131
143
  this.SelectElement = new EventEmitter();
132
144
  this.clearElement = new EventEmitter();
133
- this.propagateChange = (_) => {
134
- };
145
+ // --- Métodos de ControlValueAccessor ---
146
+ this.propagateChange = (_) => { };
135
147
  /**
136
- * Función para mostrar los elementos a seleccionar
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
- if (typeof value === 'string') {
143
- return value;
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
- let displayText = '';
146
- if (!this.displayOptions) {
147
- this.displayOptions = GENERAL_DISPLAY_OPTIONS;
165
+ else {
166
+ displayText += field?.divider || "";
148
167
  }
149
- this.displayOptions?.firthLabel?.forEach((field) => {
150
- if (field?.type === DisplayOptionItemType.PATH) {
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
- this.subscribeComponentChanges();
176
+ // Suscribirse a los cambios del componente una vez que la URL está disponible
177
+ this.subscribeToComponentChanges();
165
178
  }
166
179
  }
167
- set clearData(value) {
168
- this.clearData$ = value;
169
- if (this.clearData$) {
170
- this.clearData$
171
- .pipe(takeUntilDestroyed(this._destroyRef))
172
- .subscribe({
173
- next: () => {
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
- this.component.setValue(value);
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 para la búsqueda
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
- if (restrictions?.length) {
193
- this.restrictionsFilters = [...restrictions];
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
- else {
196
- this.restrictionsFilters = [];
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 que el campo sea requerido
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
- this.component.setValidators([Validators.required, autocompleteValidator]);
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 vamos a realizar una búsqueda al elemento estar en el focus de la aplicación
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.doFocusSubject$ = focusSubject;
222
- if (this.doFocusSubject$) {
223
- this.doFocusSubject$
224
- .pipe(debounceTime(this.debounceTimeValue), takeUntilDestroyed(this._destroyRef))
225
- .subscribe({
226
- next: () => {
227
- this._zone.run(() => {
228
- setTimeout(() => {
229
- this.inputText?.nativeElement?.focus();
230
- }, 500);
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
- if (this.originalOptions()?.length) {
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
- * Subscripción a los cambios del input de búsqueda
293
+ * Reinicia el estado de paginación y opciones del autocompletado.
294
+ * Centraliza la lógica para evitar duplicación.
246
295
  */
247
- subscribeComponentChanges() {
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(debounceTime(this.debounceTimeValue), takeUntilDestroyed(this._destroyRef))
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 (!this.firstCall && !this.wasSelected) {
253
- this.getAutocompleteByTextHandler(this.getAutocompleteSearchText());
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
- this.firstCall = false;
256
- this.wasSelected = false;
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
- * Búsqueda de los elementos a mostrar en el componente de auto-completamiento
262
- *
263
- * @param text - Texto a buscar
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
- getAutocompleteByTextHandler(text) {
266
- this.loading.set(true);
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
- if (!this.serviceConfig) {
271
- this._autocompleteService
272
- .getAutocompleteByText(this._url, text, this.filterString, this.restrictionsFilters, this.removeProperties, this.order, this.bodyRequest)
273
- .then((resp) => {
274
- resp?.pipe(finalize(() => this.loading.set(false))).subscribe({
275
- next: (result) => {
276
- this.originalOptions.set(result?.payload?.data ?? result?.data);
277
- // Modifica los options con una función que se le pase como parámetro
278
- const modifiedOptions = this.modifyResultFn(this.originalOptions());
279
- if (modifiedOptions)
280
- this.filteredOptions.set(modifiedOptions);
281
- if (this.notAllowedOption()) {
282
- this.filteredOptions.set(this.originalOptions()?.filter((option) => JSON.stringify(option) !== JSON.stringify(this.notAllowedOption())));
283
- }
284
- else {
285
- this.filteredOptions.set(this.originalOptions());
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
- let body = this.serviceConfig;
293
- body[this.serviceConfig.searchProperty] = text;
294
- this.serviceConfig
295
- .service[this.serviceConfig.method](body)
296
- .pipe(takeUntilDestroyed(this._destroyRef), debounceTime(this.debounceTimeValue || 300), finalize(() => this.loading.set(false)))
297
- .subscribe({
298
- next: (result) => {
299
- this.originalOptions.set(result?.payload?.data ?? result?.data);
300
- // Modifica los options con una función que se le pase como parámetro
301
- const modifiedOptions = this.modifyResultFn(this.originalOptions());
302
- if (modifiedOptions)
303
- this.filteredOptions.set(modifiedOptions);
304
- if (this.notAllowedOption()) {
305
- this.filteredOptions.set(this.originalOptions()?.filter((option) => JSON.stringify(option) !== JSON.stringify(this.notAllowedOption())));
306
- }
307
- else {
308
- this.filteredOptions.set(this.originalOptions());
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 por el que se va a realizar la búsqueda
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
- let text = '';
321
- if (this.component.value) {
322
- if (typeof this.component.value === 'object') {
323
- const componentValue = this.component.value?.[this.field?.[0]];
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
- return text;
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 del FormControl
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.component.setValue(value, { emitEvent: false });
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.selectedElement = null;
643
+ this._lastSearchText = "";
360
644
  }
361
645
  }
362
646
  setDisabledState(isDisabled) {
363
647
  this.disabled.set(isDisabled);
364
- if (this.disabled()) {
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.component.setValue(null);
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
- * Acción en el Focus del elemento
390
- */
391
- onFocus() {
392
- if (!this.selectedElement) {
393
- this.getAutocompleteByTextHandler(this.getAutocompleteSearchText());
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
- if ($event?.option?.value) {
398
- this.wasSelected = true;
399
- this.selectedElement = $event.option.value;
400
- this.SelectElement.emit($event.option.value);
401
- if (this.valueId) {
402
- this.propagateChange(typeof $event.option.value === 'object' ? $event.option.value?.id : $event.option.value);
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($event.option.value);
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
- this.propagateChange($event.option.value);
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.0.0", ngImport: i0, type: GuajiritosGeneralAutocomplete, deps: [{ token: i1.AutocompleteService }, { token: i0.NgZone }, { token: i0.DestroyRef }, { token: i2.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); }
413
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", 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", serviceConfig: "serviceConfig", suffixIcon: "suffixIcon", removeProperties: "removeProperties", modifyResultFn: "modifyResultFn", url: "url", clearData: "clearData", initialValue: "initialValue", restrictions: "restrictions", isRequired: "isRequired", doFocus: "doFocus", notAllowedElements: "notAllowedElements" }, outputs: { SelectElement: "SelectElement", clearElement: "clearElement" }, providers: [
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 [floatLabel]=\"floatLabel\" class=\"w-100\" [appearance]=\"appearance\" [color]=\"color\"\n [subscriptSizing]=\"subscriptSizing\">\n\n @if (showLabel) {\n <mat-label>{{ label | translate }}</mat-label>\n }\n @if (showSuffix) {\n <mat-icon matSuffix>{{ suffixIcon ?? \"search\" }}</mat-icon>\n }\n <input #inputText #trigger=\"matAutocompleteTrigger\" (focus)=\"onFocus()\" [formControl]=\"component\" type=\"text\"\n [matAutocomplete]=\"autocomplete\" [placeholder]=\"placeholder | translate\" aria-label=\"autocomplete\"\n autocomplete=\"off\" matInput [required]=\"required()\">\n @if (!loading() && component.value) {\n <button (click)=\"clear(trigger)\" [disabled]=\"disabled()\"\n aria-label=\"Clear\" mat-icon-button matSuffix>\n <mat-icon>close</mat-icon>\n </button>\n }\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 #autocomplete=\"matAutocomplete\" [displayWith]=\"displayFn\" [requireSelection]=\"requireSelection\"\n (optionSelected)=\"optionSelected($event)\">\n @for (option of filteredOptions(); track option) {\n <mat-option [value]=\"option\">\n @if (!displayOptions && !detailsTemplate) {\n {{ option?.name | i18n: translateService.currentLang }}\n }\n @if (!detailsTemplate) {\n <div class=\"display-options\">\n <span [ngStyle]=\"{'line-height': displayOptions?.secondLabel ? '16px' : ''}\">\n {{ option | resolvePropertyPath:displayOptions.firthLabel | i18n: translateService.currentLang }}\n </span>\n @if (displayOptions?.secondLabel) {\n <span class=\"mat-caption\">\n {{ option | resolvePropertyPath: displayOptions.secondLabel | i18n: translateService.currentLang }}\n </span>\n }\n </div>\n }\n @if (detailsTemplate) {\n <ng-container *ngTemplateOutlet=\"detailsTemplate;context:{$implicit: option }\"></ng-container>\n }\n </mat-option>\n }\n </mat-autocomplete>\n\n @if (component.invalid) {\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: "pipe", type: i2.TranslatePipe, name: "translate" }, { 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: I18nPipe, name: "i18n" }, { kind: "pipe", type: ResolvePropertyPath, name: "resolvePropertyPath" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
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.0.0", ngImport: i0, type: GuajiritosGeneralAutocomplete, decorators: [{
725
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: GuajiritosGeneralAutocomplete, decorators: [{
422
726
  type: Component,
423
- args: [{ selector: 'guajiritos-general-autocomplete', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
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 [floatLabel]=\"floatLabel\" class=\"w-100\" [appearance]=\"appearance\" [color]=\"color\"\n [subscriptSizing]=\"subscriptSizing\">\n\n @if (showLabel) {\n <mat-label>{{ label | translate }}</mat-label>\n }\n @if (showSuffix) {\n <mat-icon matSuffix>{{ suffixIcon ?? \"search\" }}</mat-icon>\n }\n <input #inputText #trigger=\"matAutocompleteTrigger\" (focus)=\"onFocus()\" [formControl]=\"component\" type=\"text\"\n [matAutocomplete]=\"autocomplete\" [placeholder]=\"placeholder | translate\" aria-label=\"autocomplete\"\n autocomplete=\"off\" matInput [required]=\"required()\">\n @if (!loading() && component.value) {\n <button (click)=\"clear(trigger)\" [disabled]=\"disabled()\"\n aria-label=\"Clear\" mat-icon-button matSuffix>\n <mat-icon>close</mat-icon>\n </button>\n }\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 #autocomplete=\"matAutocomplete\" [displayWith]=\"displayFn\" [requireSelection]=\"requireSelection\"\n (optionSelected)=\"optionSelected($event)\">\n @for (option of filteredOptions(); track option) {\n <mat-option [value]=\"option\">\n @if (!displayOptions && !detailsTemplate) {\n {{ option?.name | i18n: translateService.currentLang }}\n }\n @if (!detailsTemplate) {\n <div class=\"display-options\">\n <span [ngStyle]=\"{'line-height': displayOptions?.secondLabel ? '16px' : ''}\">\n {{ option | resolvePropertyPath:displayOptions.firthLabel | i18n: translateService.currentLang }}\n </span>\n @if (displayOptions?.secondLabel) {\n <span class=\"mat-caption\">\n {{ option | resolvePropertyPath: displayOptions.secondLabel | i18n: translateService.currentLang }}\n </span>\n }\n </div>\n }\n @if (detailsTemplate) {\n <ng-container *ngTemplateOutlet=\"detailsTemplate;context:{$implicit: option }\"></ng-container>\n }\n </mat-option>\n }\n </mat-autocomplete>\n\n @if (component.invalid) {\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"] }]
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: ['inputText', { static: true }]
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 customizada para la selección de elementos
821
+ * Validación personalizada para la selección de elementos
510
822
  */
511
823
  function autocompleteValidator(control) {
512
- if (control?.value?.constructor !== Object || !control?.value?.id) {
513
- return { invalidSelection: true };
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
- return null;
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
  /*