@guajiritos/general-autocomplete 19.0.1 → 19.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, from, switchMap, 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: "19.2.14", ngImport: i0, type: UtilsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
50
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UtilsService, providedIn: 'root' }); }
49
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.12", ngImport: i0, type: UtilsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
50
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.12", ngImport: i0, type: UtilsService, providedIn: 'root' }); }
51
51
  }
52
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UtilsService, decorators: [{
52
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.12", 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: "19.2.14", ngImport: i0, type: ResolvePropertyPath, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
82
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ResolvePropertyPath, isStandalone: true, name: "resolvePropertyPath" }); }
81
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.12", ngImport: i0, type: ResolvePropertyPath, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
82
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.12", ngImport: i0, type: ResolvePropertyPath, isStandalone: true, name: "resolvePropertyPath" }); }
83
83
  }
84
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ResolvePropertyPath, decorators: [{
84
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.12", ngImport: i0, type: ResolvePropertyPath, decorators: [{
85
85
  type: Pipe,
86
86
  args: [{
87
87
  name: 'resolvePropertyPath',
@@ -95,276 +95,444 @@ 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
+ // --- Private Properties ---
99
+ // Indica si la última actualización del input fue por una selección explícita desde el autocomplete
100
+ this._isOptionSelected = 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 = [];
105
+ this._limit = 20;
106
+ this._offset = 0;
107
+ this._restrictionsFilters = [];
108
+ this._lastSearchText = ""; // Almacena el texto usado en la última búsqueda de API
109
+ // --- Public Signals ---
103
110
  this.disabled = signal(false);
104
111
  this.loading = signal(false);
105
112
  this.required = signal(false);
106
113
  this.filteredOptions = signal([]);
107
114
  this.originalOptions = signal([]);
108
115
  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';
116
+ this.component = new UntypedFormControl({
117
+ value: null,
118
+ disabled: false,
119
+ });
120
+ this.hasMore = signal(true); // Indica si hay más datos para cargar
121
+ // --- Inputs ---
122
+ this.floatLabel = "auto";
123
+ this.color = "accent";
124
+ this.appearance = "outline";
125
+ this.subscriptSizing = "dynamic";
117
126
  this.debounceTimeValue = 300;
118
- this.label = 'Seleccione';
127
+ this.label = "Seleccione";
119
128
  this.showLabel = true;
120
- this.placeholder = 'Seleccione un elemento';
121
- this.field = ['name'];
122
- this.filterString = 'filter[$and][name][$like]';
129
+ this.placeholder = "Seleccione un elemento";
130
+ this.field = ["name"];
131
+ this.filterString = "filter[$and][name][$like]";
123
132
  this.displayOptions = GENERAL_DISPLAY_OPTIONS;
124
133
  this.withoutPaddingBottom = true;
125
134
  this.valueId = false;
126
135
  this.showSuffix = false;
127
136
  this.requireSelection = false;
128
- this.suffixIcon = 'search';
137
+ this.suffixIcon = "search";
129
138
  this.removeProperties = [];
130
- this.modifyResultFn = () => null;
131
- this.SelectElement = new EventEmitter();
139
+ this.modifyResultFn = (options) => options;
140
+ // --- Outputs ---
141
+ this.selectElement = new EventEmitter();
132
142
  this.clearElement = new EventEmitter();
133
- this.propagateChange = (_) => {
134
- };
143
+ // --- ControlValueAccessor methods ---
144
+ this.propagateChange = (_) => { };
135
145
  /**
136
- * Función para mostrar los elementos a seleccionar
137
- *
138
- * @param value - Valor a mostrar
146
+ * Function to display selected elements
139
147
  */
140
148
  this.displayFn = (value) => {
141
- if (value) {
142
- if (typeof value === 'string') {
143
- return value;
149
+ if (!value) {
150
+ return "";
151
+ }
152
+ // Si el valor ya es una cadena, devuélvelo directamente
153
+ if (typeof value === "string") {
154
+ return value;
155
+ }
156
+ let displayText = "";
157
+ const options = this.displayOptions || GENERAL_DISPLAY_OPTIONS;
158
+ options?.firthLabel?.forEach((field) => {
159
+ if (field?.type === DisplayOptionItemType.PATH) {
160
+ displayText +=
161
+ UtilsService.resolvePropertyByPath(value, field?.path) || "";
144
162
  }
145
- let displayText = '';
146
- if (!this.displayOptions) {
147
- this.displayOptions = GENERAL_DISPLAY_OPTIONS;
163
+ else {
164
+ displayText += field?.divider || "";
148
165
  }
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
- }
166
+ });
167
+ return displayText;
159
168
  };
160
169
  }
170
+ // --- Setters for Inputs ---
161
171
  set url(data) {
162
172
  if (data) {
163
173
  this._url = data;
164
- this.subscribeComponentChanges();
174
+ // Suscribirse a los cambios del componente una vez que la URL está disponible
175
+ this.subscribeToComponentChanges();
165
176
  }
166
177
  }
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
- });
178
+ set limit(value) {
179
+ if (value && !isNaN(value)) {
180
+ this._limit = value;
181
181
  }
182
182
  }
183
+ set clearData(value) {
184
+ this._clearDataSubject = value;
185
+ this._clearDataSubject
186
+ .pipe(takeUntilDestroyed(this._destroyRef))
187
+ .subscribe({
188
+ next: () => {
189
+ this.resetAutocompleteState();
190
+ this.component.setValue(null, { emitEvent: false }); // No emitir para evitar ciclo de valueChanges
191
+ this.propagateChange(null);
192
+ this.selectElement.emit(null);
193
+ },
194
+ });
195
+ }
183
196
  set initialValue(value) {
184
- this.component.setValue(value);
197
+ // Si el valor inicial es el mismo que el valor actual del control, no hacer nada para evitar bucles
198
+ if (value === this.component.value) {
199
+ return;
200
+ }
201
+ this._selectedElement = value;
202
+ this._isOptionSelected = !!value; // Marcar como seleccionado si hay un valor inicial
203
+ // Establecer el valor en el formControl sin emitir el evento
204
+ // para evitar que subscribeToComponentChanges reaccione a esta carga inicial.
205
+ this.component.setValue(value, { emitEvent: false });
206
+ // Actualizar _lastSearchText basado en el valor inicial
207
+ if (value) {
208
+ this._lastSearchText = this.displayFn(value);
209
+ console.log("initialValue set: _lastSearchText =", this._lastSearchText);
210
+ }
211
+ else {
212
+ this._lastSearchText = "";
213
+ }
185
214
  }
186
215
  /**
187
- * Añade o elimina restricciones para la búsqueda
188
- *
189
- * @param restrictions - Restricciones para la búsqueda
216
+ * Adds or removes search restrictions
190
217
  */
191
218
  set restrictions(restrictions) {
192
- if (restrictions?.length) {
193
- this.restrictionsFilters = [...restrictions];
194
- }
195
- else {
196
- this.restrictionsFilters = [];
219
+ this._restrictionsFilters = restrictions?.length ? [...restrictions] : [];
220
+ this.resetAutocompleteState();
221
+ // Re-ejecutar búsqueda si el input tiene texto y hay restricciones
222
+ const currentSearchText = this.getAutocompleteSearchText();
223
+ if (currentSearchText) {
224
+ this.getAutocompleteByTextHandler(currentSearchText);
197
225
  }
198
226
  }
199
227
  /**
200
- * Añade o elimina la validación de que el campo sea requerido
201
- *
202
- * @param required - Define si es requerido o no
228
+ * Adds or removes required validation
203
229
  */
204
230
  set isRequired(required) {
205
231
  this.required.set(required);
232
+ const validators = [autocompleteValidator];
206
233
  if (required) {
207
- this.component.setValidators([Validators.required, autocompleteValidator]);
208
- }
209
- else {
210
- this.component.clearValidators();
211
- this.component.setValidators([autocompleteValidator]);
234
+ validators.push(Validators.required);
212
235
  }
236
+ this.component.setValidators(validators);
213
237
  this.component.updateValueAndValidity();
214
238
  }
215
239
  /**
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
240
+ * Defines whether to perform a search when the element is in focus
219
241
  */
220
242
  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
- });
232
- }
233
- });
234
- }
243
+ this._doFocusSubject = focusSubject;
244
+ this._doFocusSubject
245
+ .pipe(debounceTime(this.debounceTimeValue), // Retrasar para evitar llamadas excesivas
246
+ takeUntilDestroyed(this._destroyRef))
247
+ .subscribe({
248
+ next: () => {
249
+ this._zone.run(() => {
250
+ setTimeout(() => {
251
+ this.inputText?.nativeElement?.focus();
252
+ // Forzar la apertura del panel y una posible carga si es necesario
253
+ // Simular input para disparar valueChanges, lo que a menudo abre el panel si el texto cambia
254
+ this.inputText?.nativeElement?.dispatchEvent(new Event("input"));
255
+ // Abrir explícitamente el panel usando el MatAutocompleteTrigger
256
+ this.autocompleteTrigger?.openPanel(); // <--- EL CAMBIO ES AQUÍ
257
+ }, 50); // Un pequeño retraso para asegurar el foco y la apertura
258
+ });
259
+ },
260
+ });
235
261
  }
236
262
  set notAllowedElements(element) {
237
263
  if (element) {
238
264
  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
- }
265
+ this.filterOptionsBasedOnNotAllowed();
242
266
  }
243
267
  }
244
268
  /**
245
- * Subscripción a los cambios del input de búsqueda
269
+ * Resets the autocomplete pagination and options state.
270
+ * Centralizes the logic to avoid duplication.
246
271
  */
247
- subscribeComponentChanges() {
272
+ resetAutocompleteState() {
273
+ this._offset = 0;
274
+ this.originalOptions.set([]);
275
+ this.filteredOptions.set([]);
276
+ this.hasMore.set(true);
277
+ // IMPORTANTE: _selectedElement NO se resetea aquí, solo si se borra explícitamente
278
+ // _lastSearchText se actualiza en writeValue y optionSelected
279
+ console.log("Autocomplete state reset (pagination/options).");
280
+ }
281
+ /**
282
+ * Subscription to search input changes
283
+ */
284
+ subscribeToComponentChanges() {
248
285
  this.component.valueChanges
249
- .pipe(debounceTime(this.debounceTimeValue), takeUntilDestroyed(this._destroyRef))
286
+ .pipe(
287
+ // Utiliza tap para observar el valor antes del debounce y el switchMap
288
+ tap((value) => {
289
+ // Si el cambio viene de una selección programática (e.g., this.component.setValue(obj))
290
+ // O de una selección del usuario en el autocomplete, marcamos que no necesitamos buscar.
291
+ if (typeof value === "object" &&
292
+ value !== null &&
293
+ this._isOptionSelected) {
294
+ console.log("Value change detected: Object selected, skipping search.");
295
+ // Resetear la bandera después de procesar
296
+ this._isOptionSelected = false;
297
+ return; // Salir del tap, el debounce/switchMap no se ejecutará para este valor.
298
+ }
299
+ }), debounceTime(this.debounceTimeValue), takeUntilDestroyed(this._destroyRef))
250
300
  .subscribe({
251
- next: () => {
252
- if (!this.firstCall && !this.wasSelected) {
253
- this.getAutocompleteByTextHandler(this.getAutocompleteSearchText());
301
+ next: (value) => {
302
+ const currentSearchText = this.getAutocompleteSearchText();
303
+ // Si el valor actual es un objeto (ya seleccionado), no necesitamos buscar
304
+ // A menos que el texto en el input haya sido modificado por el usuario
305
+ if (typeof value === "object" && value !== null) {
306
+ const displayText = this.displayFn(value);
307
+ if (currentSearchText === displayText &&
308
+ this._selectedElement === value) {
309
+ console.log("Value is selected object and matches display text, skipping search.");
310
+ return;
311
+ }
312
+ }
313
+ // Si el valor del input es un string (el usuario está escribiendo o borró)
314
+ // Y el texto actual es diferente al último texto buscado
315
+ // O si el texto está vacío (para cargar las opciones iniciales si se borra)
316
+ if ((typeof value === "string" &&
317
+ currentSearchText !== this._lastSearchText) ||
318
+ (currentSearchText === "" && this._lastSearchText !== "") // Para cuando el usuario borra todo
319
+ ) {
320
+ this.resetAutocompleteState(); // Reiniciar paginación y opciones
321
+ this._lastSearchText = currentSearchText; // Actualizar el último texto buscado
322
+ console.log("Value changed, initiating new search for:", currentSearchText);
323
+ this.getAutocompleteByTextHandler(currentSearchText);
324
+ }
325
+ else if (currentSearchText === "" && this._lastSearchText === "") {
326
+ // Si el input está vacío y ya lo estaba, y el componente no tiene valor seleccionado,
327
+ // podría ser un buen momento para recargar las opciones iniciales si el panel está abierto
328
+ if (this.matAutocomplete._isOpen &&
329
+ this.originalOptions().length === 0 &&
330
+ !this.loading()) {
331
+ // <--- CORRECCIÓN APLICADA AQUÍ
332
+ console.log("Input empty, no options, panel open. Loading initial set.");
333
+ this.getAutocompleteByTextHandler("");
334
+ }
335
+ }
336
+ },
337
+ });
338
+ }
339
+ ngAfterViewInit() {
340
+ // Suscribirse a la apertura del panel para añadir el listener de scroll
341
+ this.matAutocomplete.opened
342
+ .pipe(takeUntilDestroyed(this._destroyRef))
343
+ .subscribe(() => {
344
+ if (this.matAutocomplete?.panel) {
345
+ // Asegurarse de que el listener sea removido antes de añadirlo para prevenir duplicados
346
+ this.matAutocomplete.panel.nativeElement.removeEventListener("scroll", this.onScroll.bind(this));
347
+ this.matAutocomplete.panel.nativeElement.addEventListener("scroll", this.onScroll.bind(this));
348
+ // Si el panel se abre y no hay opciones cargadas, o si el input está vacío
349
+ // y no hay una selección activa, cargar las opciones iniciales.
350
+ const currentSearchText = this.getAutocompleteSearchText();
351
+ if (this.originalOptions().length === 0 ||
352
+ (!this._selectedElement && currentSearchText === "")) {
353
+ console.log("Panel opened, loading initial set of options.");
354
+ this.getAutocompleteByTextHandler(currentSearchText);
355
+ }
356
+ }
357
+ });
358
+ // Suscribirse al cierre del panel para remover el listener de scroll y manejar la deselección
359
+ this.matAutocomplete.closed
360
+ .pipe(takeUntilDestroyed(this._destroyRef))
361
+ .subscribe(() => {
362
+ if (this.matAutocomplete?.panel) {
363
+ this.matAutocomplete.panel.nativeElement.removeEventListener("scroll", this.onScroll.bind(this));
364
+ }
365
+ const currentInputValue = this.component.value;
366
+ const currentInputText = this.getAutocompleteSearchText();
367
+ // Lógica para borrar el input si no hay una selección válida
368
+ // 1. Si se requiere selección Y el valor actual NO es un objeto (es texto libre)
369
+ // 2. O si no hay _selectedElement Y el input tiene texto
370
+ if ((this.requireSelection && typeof currentInputValue === "string") ||
371
+ (!this._selectedElement && currentInputText !== "")) {
372
+ // Además, verificar si el texto actual NO coincide con el displayFn del _selectedElement
373
+ // Esto maneja el caso donde el usuario edita el texto de un elemento seleccionado
374
+ if (!this._selectedElement ||
375
+ currentInputText !== this.displayFn(this._selectedElement)) {
376
+ console.warn("Autocomplete closed: Input cleared due to no valid selection or text mismatch.");
377
+ this.component.setValue(null);
378
+ this.propagateChange(null);
379
+ this.selectElement.emit(null);
380
+ this._selectedElement = null;
381
+ this._lastSearchText = "";
382
+ this.resetAutocompleteState(); // Resetear también las opciones cargadas
254
383
  }
255
- this.firstCall = false;
256
- this.wasSelected = false;
257
384
  }
258
385
  });
259
386
  }
260
387
  /**
261
- * Búsqueda de los elementos a mostrar en el componente de auto-completamiento
262
- *
263
- * @param text - Texto a buscar
388
+ * Searches for elements to display in the autocomplete component.
389
+ * Now supports pagination.
390
+ * @param text - Text to search
264
391
  */
265
392
  getAutocompleteByTextHandler(text) {
393
+ // Si no hay más datos Y ya cargamos algo (offset > 0) Y el texto de búsqueda no ha cambiado,
394
+ // y no estamos actualmente cargando, salir.
395
+ if (!this.hasMore() &&
396
+ this._offset > 0 &&
397
+ text === this._lastSearchText && // Verificar que el texto de búsqueda actual sea el mismo
398
+ !this.loading()) {
399
+ console.log("No more data for current search text, stopping further loads.");
400
+ return;
401
+ }
402
+ if (this.loading()) {
403
+ console.log("Already loading, exiting getAutocompleteByTextHandler.");
404
+ return;
405
+ }
266
406
  this.loading.set(true);
267
- this.propagateChange(null);
268
- this.selectedElement = null;
269
- 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
- });
289
- });
407
+ const currentOffset = this._offset; // Capturar el offset actual antes de la llamada
408
+ const currentSearchTextForApi = text; // Usar el texto pasado, no el global _lastSearchText
409
+ if (!this._url && !this.serviceConfig) {
410
+ console.warn("Autocomplete: 'url' or 'serviceConfig' input is required.");
411
+ this.loading.set(false);
412
+ this.hasMore.set(false);
413
+ return;
290
414
  }
291
- else {
415
+ let apiCall;
416
+ if (this._url) {
417
+ apiCall = from(this._autocompleteService.getAutocompleteByText(this._url, currentSearchTextForApi, // Usar el texto de búsqueda actual
418
+ this.filterString, this._restrictionsFilters, this.removeProperties, this.order, this.bodyRequest, { limit: this._limit, offset: currentOffset })).pipe(switchMap((innerObservable) => innerObservable));
419
+ }
420
+ else if (this.serviceConfig) {
292
421
  let body = { ...this.serviceConfig };
293
- body[this.serviceConfig.searchProperty] = text;
294
- delete body.service;
295
- this.serviceConfig.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
- }
310
- },
311
- error: (err) => {
312
- console.log('aki esta el error =>', err);
422
+ body[this.serviceConfig.searchProperty] = currentSearchTextForApi; // Usar el texto de búsqueda actual
423
+ body["limit"] = this._limit;
424
+ body["offset"] = currentOffset;
425
+ apiCall = this.serviceConfig.service[this.serviceConfig.method](body);
426
+ }
427
+ else {
428
+ this.loading.set(false);
429
+ this.hasMore.set(false);
430
+ return;
431
+ }
432
+ // Guardar la posición de scroll antes de la actualización
433
+ let panelScrollTop = 0;
434
+ if (this.matAutocomplete?.panel) {
435
+ panelScrollTop = this.matAutocomplete.panel.nativeElement.scrollTop;
436
+ }
437
+ apiCall
438
+ .pipe(finalize(() => {
439
+ this.loading.set(false);
440
+ }), takeUntilDestroyed(this._destroyRef))
441
+ .subscribe({
442
+ next: (result) => {
443
+ const newData = result?.payload?.data ?? result?.data ?? [];
444
+ console.log("New Data received:", newData.length, "items. Current offset before update:", this._offset);
445
+ this._offset += newData.length; // Actualizar offset con la cantidad de nuevos ítems
446
+ if (newData.length < this._limit) {
447
+ this.hasMore.set(false);
448
+ console.log("No more data, hasMore = false");
313
449
  }
314
- });
450
+ else {
451
+ this.hasMore.set(true);
452
+ console.log("Potentially more data, hasMore = true");
453
+ }
454
+ // Actualizar las opciones originales
455
+ this.originalOptions.update((currentOptions) => {
456
+ const updatedOptions = [...currentOptions, ...newData];
457
+ console.log("Total options after merge:", updatedOptions.length);
458
+ return updatedOptions;
459
+ });
460
+ this.filterOptionsBasedOnNotAllowed();
461
+ // Restaurar la posición de scroll después de que Angular haya actualizado el DOM
462
+ // Esto debe hacerse fuera de la zona de Angular o con un pequeño retardo.
463
+ if (this.matAutocomplete?.panel && panelScrollTop > 0) {
464
+ this._zone.runOutsideAngular(() => {
465
+ setTimeout(() => {
466
+ this.matAutocomplete.panel.nativeElement.scrollTop =
467
+ panelScrollTop;
468
+ console.log("Scroll position restored to:", panelScrollTop);
469
+ }, 0); // Un pequeño retardo para asegurar que el DOM se haya renderizado
470
+ });
471
+ }
472
+ },
473
+ error: (error) => {
474
+ console.error("Error fetching autocomplete data:", error);
475
+ this.loading.set(false);
476
+ this.hasMore.set(false);
477
+ },
478
+ });
479
+ }
480
+ filterOptionsBasedOnNotAllowed() {
481
+ let currentOptions = this.originalOptions();
482
+ const modifiedOptions = this.modifyResultFn(currentOptions);
483
+ if (modifiedOptions !== null && modifiedOptions !== undefined) {
484
+ currentOptions = modifiedOptions;
485
+ }
486
+ if (this.notAllowedOption()) {
487
+ this.filteredOptions.set(currentOptions.filter((option) => JSON.stringify(option) !== JSON.stringify(this.notAllowedOption())));
488
+ }
489
+ else {
490
+ this.filteredOptions.set(currentOptions);
315
491
  }
316
492
  }
317
493
  /**
318
- * Define el texto por el que se va a realizar la búsqueda
319
- *
320
- * @return {string} Texto de la búsqueda
494
+ * Defines the text to be used for the search
321
495
  */
322
496
  getAutocompleteSearchText() {
323
- let text = '';
324
- if (this.component.value) {
325
- if (typeof this.component.value === 'object') {
326
- const componentValue = this.component.value?.[this.field?.[0]];
327
- if (typeof componentValue === 'object') {
328
- let lang = 'es';
329
- if (this.field?.[1]) {
330
- lang = this.field?.[1];
331
- }
332
- text = componentValue?.[lang];
333
- }
334
- }
335
- else if (typeof this.component.value === 'string') {
336
- text = this.component.value;
337
- }
497
+ const value = this.component.value;
498
+ if (typeof value === "object" && value !== null) {
499
+ // Si el valor es un objeto, usa displayFn para obtener el texto
500
+ return this.displayFn(value);
338
501
  }
339
- return text;
502
+ else if (typeof value === "string") {
503
+ return value;
504
+ }
505
+ return "";
340
506
  }
341
507
  registerOnChange(fn) {
342
508
  this.propagateChange = fn;
343
509
  }
344
- registerOnTouched() {
345
- }
510
+ registerOnTouched() { }
346
511
  /**
347
- * Recibe el valor del FormControl
348
- *
349
- * @param value - Valor entrado por FormControl
512
+ * Recibe el valor desde el FormControl externo.
350
513
  */
351
514
  writeValue(value) {
515
+ // Establecer la bandera para indicar que el cambio viene de un setValue programático
516
+ this._isOptionSelected = typeof value === "object" && value !== null;
517
+ this.component.setValue(value, { emitEvent: false }); // No emitir evento para evitar bucles.
518
+ this._selectedElement = value
519
+ ? this.valueId && typeof value === "object"
520
+ ? value?.id
521
+ : value
522
+ : null;
523
+ // Actualizar _lastSearchText basado en el valor que se está escribiendo.
352
524
  if (value) {
353
- this.component.setValue(value, { emitEvent: false });
354
- if (typeof value === 'object' && this.valueId) {
355
- this.selectedElement = value?.id;
356
- }
357
- else {
358
- this.selectedElement = value;
359
- }
525
+ this._lastSearchText = this.displayFn(value);
526
+ console.log("writeValue: Last search text set to:", this._lastSearchText);
360
527
  }
361
528
  else {
362
- this.selectedElement = null;
529
+ this._lastSearchText = "";
530
+ console.log("writeValue: Last search text cleared.");
363
531
  }
364
532
  }
365
533
  setDisabledState(isDisabled) {
366
534
  this.disabled.set(isDisabled);
367
- if (this.disabled()) {
535
+ if (isDisabled) {
368
536
  this.component.disable();
369
537
  }
370
538
  else {
@@ -372,58 +540,108 @@ class GuajiritosGeneralAutocomplete {
372
540
  }
373
541
  }
374
542
  /**
375
- * Acción al limpiar el valor del input
376
- *
377
- * @param trigger
543
+ * Action on clearing the input value
378
544
  */
379
545
  clear(trigger) {
380
546
  this.clearElement.emit(this.component.value);
381
- this.component.setValue(null);
382
- this.selectedElement = null;
383
- this.SelectElement.emit(null);
547
+ this.resetAutocompleteState(); // Resetear paginación y opciones
548
+ this.component.setValue(null); // Esto disparará valueChanges
384
549
  this.propagateChange(null);
550
+ this.selectElement.emit(null);
551
+ this._selectedElement = null; // Asegurarse de que el elemento seleccionado se borre
552
+ this._lastSearchText = ""; // Borrar el último texto de búsqueda
385
553
  this._zone.run(() => {
386
554
  setTimeout(() => {
387
- trigger.openPanel();
555
+ trigger.openPanel(); // Reabrir el panel después de borrar
556
+ this.getAutocompleteByTextHandler(""); // Cargar opciones iniciales (o todas) después de borrar
388
557
  }, 100);
389
558
  });
390
559
  }
391
560
  /**
392
- * Acción en el Focus del elemento
561
+ * Action on element Focus
393
562
  */
394
563
  onFocus() {
395
- if (!this.selectedElement) {
396
- this.getAutocompleteByTextHandler(this.getAutocompleteSearchText());
564
+ const currentSearchText = this.getAutocompleteSearchText();
565
+ // Si no hay opciones cargadas O el input está vacío y no hay un elemento seleccionado
566
+ if (this.originalOptions().length === 0 ||
567
+ (currentSearchText === "" && !this._selectedElement)) {
568
+ this.resetAutocompleteState(); // Reiniciar estado antes de una nueva búsqueda
569
+ this._lastSearchText = currentSearchText;
570
+ console.log("On focus, initiating search for:", currentSearchText);
571
+ this.getAutocompleteByTextHandler(currentSearchText);
572
+ }
573
+ else if (this.matAutocomplete._isOpen &&
574
+ currentSearchText === this._lastSearchText) {
575
+ // Si el panel ya está abierto y el texto no ha cambiado, no hacer nada para evitar re-cargas innecesarias.
576
+ // Si el panel no estaba abierto, la apertura de `matAutocomplete.opened` se encargará de la carga.
577
+ console.log("On focus, panel already open with same text, skipping search.");
397
578
  }
398
579
  }
399
580
  optionSelected($event) {
400
- if ($event?.option?.value) {
401
- this.wasSelected = true;
402
- this.selectedElement = $event.option.value;
403
- this.SelectElement.emit($event.option.value);
404
- if (this.valueId) {
405
- this.propagateChange(typeof $event.option.value === 'object' ? $event.option.value?.id : $event.option.value);
581
+ const selectedValue = $event?.option?.value;
582
+ if (selectedValue) {
583
+ this._isOptionSelected = true; // Establecer la bandera de selección
584
+ this._selectedElement = selectedValue;
585
+ this.selectElement.emit(selectedValue);
586
+ if (this.valueId && typeof selectedValue === "object") {
587
+ this.propagateChange(selectedValue?.id);
406
588
  }
407
589
  else {
408
- this.propagateChange($event.option.value);
590
+ this.propagateChange(selectedValue);
409
591
  }
592
+ // Cuando un ítem es seleccionado, actualizar _lastSearchText con su valor de display.
593
+ // Esto es crucial para que el valueChanges no vuelva a disparar una búsqueda para este valor.
594
+ this._lastSearchText = this.displayFn(selectedValue);
595
+ console.log("Option selected. Last search text set to:", this._lastSearchText);
410
596
  }
411
597
  else {
412
- this.propagateChange($event.option.value);
598
+ // Si se deselecciona o selecciona nulo
599
+ this.propagateChange(null);
600
+ this.selectElement.emit(null);
601
+ this._selectedElement = null;
602
+ this._lastSearchText = "";
603
+ this.resetAutocompleteState(); // Resetear estado si se "deselecciona" explícitamente
413
604
  }
605
+ // Después de la selección, cerrar el panel de forma programática.
606
+ // Esto asegura que la lógica de `closed` se ejecute correctamente.
607
+ // this.matAutocomplete.closePanel(); // Descomentar si el panel no se cierra automáticamente
414
608
  }
415
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: GuajiritosGeneralAutocomplete, deps: [{ token: i1.AutocompleteService }, { token: i0.NgZone }, { token: i0.DestroyRef }, { token: i2.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); }
416
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", 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: [
609
+ /**
610
+ * Handles the scroll event in the autocomplete panel to load more data.
611
+ */
612
+ onScroll(event) {
613
+ const element = event.target;
614
+ const scrollPosition = element.scrollTop + element.clientHeight;
615
+ const scrollHeight = element.scrollHeight;
616
+ const threshold = 50; // Pixels before the end to load more
617
+ // console.log("--- Scroll Event ---");
618
+ // console.log("Scroll Top:", element.scrollTop);
619
+ // console.log("Client Height:", element.clientHeight);
620
+ // console.log("Scroll Position (Top + Client):", scrollPosition);
621
+ // console.log("Scroll Height (Total):", scrollHeight);
622
+ // console.log("Remaining distance to bottom:", scrollHeight - scrollPosition);
623
+ // console.log("Is Loading:", this.loading());
624
+ // console.log("Has More:", this.hasMore());
625
+ if (scrollHeight - scrollPosition <= threshold &&
626
+ !this.loading() &&
627
+ this.hasMore()) {
628
+ console.log("Loading more data for infinity scroll...");
629
+ // Usar el texto de búsqueda actual (que puede ser el texto del ítem seleccionado o lo que el usuario escribió)
630
+ this.getAutocompleteByTextHandler(this.getAutocompleteSearchText());
631
+ }
632
+ }
633
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.12", ngImport: i0, type: GuajiritosGeneralAutocomplete, deps: [{ token: i1.AutocompleteService }, { token: i0.NgZone }, { token: i0.DestroyRef }, { token: i2.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); }
634
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.12", 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", limit: "limit", clearData: "clearData", initialValue: "initialValue", restrictions: "restrictions", isRequired: "isRequired", doFocus: "doFocus", notAllowedElements: "notAllowedElements" }, outputs: { selectElement: "selectElement", clearElement: "clearElement" }, providers: [
417
635
  {
418
636
  provide: NG_VALUE_ACCESSOR,
419
637
  useExisting: forwardRef(() => GuajiritosGeneralAutocomplete),
420
- multi: true
421
- }
422
- ], 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]", exportAs: ["matButton"] }, { 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 }); }
638
+ multi: true,
639
+ },
640
+ ], 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 [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]=\"auto\" [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 #auto=\"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]", exportAs: ["matButton"] }, { 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 }); }
423
641
  }
424
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: GuajiritosGeneralAutocomplete, decorators: [{
642
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.12", ngImport: i0, type: GuajiritosGeneralAutocomplete, decorators: [{
425
643
  type: Component,
426
- args: [{ selector: 'guajiritos-general-autocomplete', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
644
+ args: [{ selector: "guajiritos-general-autocomplete", changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
427
645
  CommonModule,
428
646
  MatFormFieldModule,
429
647
  TranslateModule,
@@ -434,17 +652,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
434
652
  MatProgressSpinnerModule,
435
653
  MatAutocompleteModule,
436
654
  I18nPipe,
437
- ResolvePropertyPath
655
+ ResolvePropertyPath,
438
656
  ], providers: [
439
657
  {
440
658
  provide: NG_VALUE_ACCESSOR,
441
659
  useExisting: forwardRef(() => GuajiritosGeneralAutocomplete),
442
- multi: true
443
- }
444
- ], 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"] }]
660
+ multi: true,
661
+ },
662
+ ], 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]=\"auto\" [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 #auto=\"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"] }]
445
663
  }], ctorParameters: () => [{ type: i1.AutocompleteService }, { type: i0.NgZone }, { type: i0.DestroyRef }, { type: i2.TranslateService }], propDecorators: { inputText: [{
446
664
  type: ViewChild,
447
- args: ['inputText', { static: true }]
665
+ args: ["inputText", { static: true }]
666
+ }], matAutocomplete: [{
667
+ type: ViewChild,
668
+ args: ["auto"]
669
+ }], autocompleteTrigger: [{
670
+ type: ViewChild,
671
+ args: [MatAutocompleteTrigger]
448
672
  }], floatLabel: [{
449
673
  type: Input
450
674
  }], color: [{
@@ -489,12 +713,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
489
713
  type: Input
490
714
  }], modifyResultFn: [{
491
715
  type: Input
492
- }], SelectElement: [{
716
+ }], selectElement: [{
493
717
  type: Output
494
718
  }], clearElement: [{
495
719
  type: Output
496
720
  }], url: [{
497
721
  type: Input
722
+ }], limit: [{
723
+ type: Input
498
724
  }], clearData: [{
499
725
  type: Input
500
726
  }], initialValue: [{
@@ -509,13 +735,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
509
735
  type: Input
510
736
  }] } });
511
737
  /**
512
- * Validación customizada para la selección de elementos
738
+ * Custom validation for element selection
513
739
  */
514
740
  function autocompleteValidator(control) {
515
- if (control?.value?.constructor !== Object || !control?.value?.id) {
516
- return { invalidSelection: true };
741
+ // Un valor válido es un objeto no nulo que tiene una propiedad 'id'.
742
+ // Esto asegura que realmente se ha seleccionado un objeto del autocomplete, no solo texto libre.
743
+ if (typeof control?.value === "object" &&
744
+ control?.value !== null &&
745
+ "id" in control.value) {
746
+ return null;
517
747
  }
518
- return null;
748
+ return { invalidSelection: true };
519
749
  }
520
750
 
521
751
  /*