tailjng 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/cli/component-manager.js +45 -0
  2. package/cli/dependency-manager.js +52 -0
  3. package/cli/file-operations.js +88 -0
  4. package/cli/index.js +51 -0
  5. package/cli/settings/colors.js +17 -0
  6. package/cli/settings/components-list.js +87 -0
  7. package/cli/settings/header-generator.js +42 -0
  8. package/cli/settings/path-utils.js +50 -0
  9. package/cli/settings/prompt-utils.js +37 -0
  10. package/cli/settings/tailwind-check.js +21 -0
  11. package/fesm2022/tailjng.mjs +903 -25
  12. package/fesm2022/tailjng.mjs.map +1 -1
  13. package/lib/config/tailjng-config.token.d.ts +3 -0
  14. package/lib/interfaces/alert/dialog-alert.interface.d.ts +52 -0
  15. package/lib/interfaces/alert/toast-alert.interface.d.ts +52 -0
  16. package/lib/interfaces/config.interface.d.ts +5 -0
  17. package/lib/interfaces/crud/api-response.d.ts +29 -0
  18. package/lib/interfaces/crud/crud.interface.d.ts +103 -0
  19. package/lib/services/alert/dialog-alert.service.d.ts +24 -0
  20. package/lib/services/alert/toast-alert.service.d.ts +26 -0
  21. package/lib/services/crud/converter-crud.service.d.ts +41 -0
  22. package/lib/services/crud/generic-crud.service.d.ts +81 -0
  23. package/lib/services/http/error-handler-http.service.d.ts +26 -0
  24. package/lib/services/http/params-http.service.d.ts +13 -0
  25. package/lib/services/static/icons.service.d.ts +31 -0
  26. package/lib/services/transformer/calendar.service.d.ts +71 -0
  27. package/package.json +5 -3
  28. package/public-api.d.ts +10 -3
  29. package/src/lib/components/alert/dialog-alert/dialog-alert.component.css +0 -0
  30. package/src/lib/components/alert/dialog-alert/dialog-alert.component.html +72 -0
  31. package/src/lib/components/alert/dialog-alert/dialog-alert.component.ts +66 -0
  32. package/src/lib/components/alert/toast-alert/toast-alert.component.css +5 -0
  33. package/src/lib/components/alert/toast-alert/toast-alert.component.html +76 -0
  34. package/src/lib/components/alert/toast-alert/toast-alert.component.ts +87 -0
  35. package/src/lib/components/button/button.component.css +0 -0
  36. package/src/lib/components/button/button.component.html +36 -0
  37. package/src/lib/components/button/button.component.ts +95 -0
  38. package/src/lib/components/checkbox/input-checkbox/input-checkbox.component.css +0 -0
  39. package/src/lib/components/checkbox/input-checkbox/input-checkbox.component.html +23 -0
  40. package/src/lib/components/checkbox/input-checkbox/input-checkbox.component.ts +44 -0
  41. package/src/lib/components/checkbox/switch-checkbox/switch-checkbox.component.css +0 -0
  42. package/src/lib/components/checkbox/switch-checkbox/switch-checkbox.component.html +26 -0
  43. package/src/lib/components/checkbox/switch-checkbox/switch-checkbox.component.ts +29 -0
  44. package/src/lib/components/color/colors.service.ts +109 -0
  45. package/src/lib/components/dialog/dialog.component.css +8 -0
  46. package/src/lib/components/dialog/dialog.component.html +57 -0
  47. package/src/lib/components/dialog/dialog.component.ts +179 -0
  48. package/src/lib/components/image/viewer-image/viewer-image.component.css +4 -0
  49. package/src/lib/components/image/viewer-image/viewer-image.component.html +75 -0
  50. package/src/lib/components/image/viewer-image/viewer-image.component.ts +131 -0
  51. package/src/lib/components/input/file-input/file-input.component.css +0 -0
  52. package/src/lib/components/input/file-input/file-input.component.html +49 -0
  53. package/src/lib/components/input/file-input/file-input.component.ts +218 -0
  54. package/src/lib/components/input/input/input.component.css +0 -0
  55. package/src/lib/components/input/input/input.component.html +24 -0
  56. package/src/lib/components/input/input/input.component.ts +78 -0
  57. package/src/lib/components/input/range-input/range-input.component.css +0 -0
  58. package/src/lib/components/input/range-input/range-input.component.html +64 -0
  59. package/src/lib/components/input/range-input/range-input.component.ts +78 -0
  60. package/src/lib/components/input/textarea-input/textarea-input.component.css +0 -0
  61. package/src/lib/components/input/textarea-input/textarea-input.component.html +21 -0
  62. package/src/lib/components/input/textarea-input/textarea-input.component.ts +75 -0
  63. package/src/lib/components/label/label.component.html +1 -1
  64. package/src/lib/components/label/label.component.ts +1 -1
  65. package/src/lib/components/mode-toggle/mode-toggle.component.css +0 -0
  66. package/src/lib/components/mode-toggle/mode-toggle.component.html +8 -0
  67. package/src/lib/components/mode-toggle/mode-toggle.component.ts +61 -0
  68. package/src/lib/components/progress-bar/progress-bar.component.css +0 -0
  69. package/src/lib/components/progress-bar/progress-bar.component.html +22 -0
  70. package/src/lib/components/progress-bar/progress-bar.component.ts +20 -0
  71. package/src/lib/components/select/dropdown/dropdown.component.css +0 -0
  72. package/src/lib/components/select/dropdown/dropdown.component.html +95 -0
  73. package/src/lib/components/select/dropdown/dropdown.component.ts +562 -0
  74. package/src/lib/components/select/multi-dropdown/multi-dropdown.component.css +0 -0
  75. package/src/lib/components/select/multi-dropdown/multi-dropdown.component.html +87 -0
  76. package/src/lib/components/select/multi-dropdown/multi-dropdown.component.ts +315 -0
  77. package/src/lib/components/select/multi-table/multi-table.component.css +0 -0
  78. package/src/lib/components/select/multi-table/multi-table.component.html +83 -0
  79. package/src/lib/components/select/multi-table/multi-table.component.ts +230 -0
  80. package/src/lib/components/toggle-radio/toggle-radio.component.css +0 -0
  81. package/src/lib/components/toggle-radio/toggle-radio.component.html +51 -0
  82. package/src/lib/components/toggle-radio/toggle-radio.component.ts +203 -0
  83. package/src/styles.css +126 -0
  84. package/cli/tailjng.js +0 -105
  85. package/lib/services/icons.service.d.ts +0 -9
  86. package/lib/tailjng.component.d.ts +0 -5
  87. package/lib/tailjng.service.d.ts +0 -6
@@ -0,0 +1,20 @@
1
+ import { Component, Input } from '@angular/core';
2
+ import { NgClass } from '@angular/common';
3
+
4
+ @Component({
5
+ selector: 'JProgressBar',
6
+ imports: [NgClass],
7
+ templateUrl: './progress-bar.component.html',
8
+ styleUrl: './progress-bar.component.css'
9
+ })
10
+ export class JProgressBarComponent {
11
+
12
+ @Input() value: number = 0;
13
+ @Input() max: number = 100;
14
+ @Input() simbol: string = '%';
15
+
16
+ @Input() height: number = 30;
17
+ @Input() borderRadius: number = 50;
18
+ @Input() ngClasses: string[] = [];
19
+
20
+ }
@@ -0,0 +1,95 @@
1
+ <div class="relative w-full h-full">
2
+ <!-- Botón para dropdown y searchable -->
3
+ <div #selectButton class="w-auto">
4
+ <button type="button"
5
+ [disabled]="disabled || isLoading"
6
+ (click)="toggleColumnSelector()"
7
+ class="flex w-full h-[40px] items-center justify-between px-3 py-2 text-sm bg-background dark:bg-dark-background border border-border dark:border-dark-border rounded focus:outline-none focus:ring-2 focus:ring-primary select-none"
8
+ [ngClass]="{
9
+ 'opacity-50 cursor-not-allowed pointer-events-none': disabled || isLoading
10
+ }">
11
+ <span class="truncate text-black dark:text-white"
12
+ [ngClass]="{'opacity-50' : selectedValue === null}">{{selectedLabel}}</span>
13
+ <div class="flex items-center">
14
+ @if (showClear && selectedValue !== null) {
15
+ <button type="button"
16
+ (click)="clearSelection($event)"
17
+ class="pr-1 mr-1 text-gray-400 hover:text-gray-600 focus:outline-none cursor-pointer">
18
+ <lucide-icon [name]="icons.x" size="14"></lucide-icon>
19
+ </button>
20
+ }
21
+ @if (!isLoading) {
22
+ <lucide-icon [name]="icons.chevronDown"
23
+ size="16"
24
+ class="transition duration-300 ease-in-out text-gray-400"
25
+ [ngClass]="{'rotate-180': isColumnSelectorOpen}"></lucide-icon>
26
+ } @else {
27
+ <lucide-icon [name]="icons.loading"
28
+ size="16"
29
+ class="text-gray-400 animate-spin"></lucide-icon>
30
+ }
31
+ </div>
32
+ </button>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- Dropdown positioned outside the flow -->
37
+ @if (isColumnSelectorOpen) {
38
+ <div @modalTransition
39
+ class="absolute z-[100] min-w-[250px] mt-1 bg-background dark:bg-dark-background rounded-lg shadow-lg border border-border border-dark-border"
40
+ [style.width.px]="dropdownWidth"
41
+ [style.top.px]="dropdownTop"
42
+ [style.left.px]="dropdownLeft">
43
+ <div class="pt-1 pl-3 pr-3 pb-3">
44
+ <div class="text-[10px] font-medium text-gray-500 dark:text-gray-500 mb-1">{{title}}</div>
45
+
46
+ <!-- Campo de búsqueda para tipo searchable -->
47
+ @if (type === 'searchable' && isSearch) {
48
+ <div class="mb-2 relative">
49
+ <input type="text"
50
+ [(ngModel)]="searchTerm"
51
+ (input)="onSearchInput()"
52
+ placeholder="Buscar..."
53
+ class="input text-black dark:text-white w-full px-3 py-2 text-sm border border-border dark:border-dark-border rounded focus:outline-none focus:ring-2 focus:ring-primary" />
54
+ <div class="absolute flex right-3 top-1/2 transform -translate-y-1/2 ">
55
+ @if (searchTerm) {
56
+ <button type="button"
57
+ (click)="clearSearchTerm()"
58
+ class="pr-1 mr-1 text-gray-400 hover:text-gray-600 focus:outline-none cursor-pointer">
59
+ <lucide-icon [name]="icons.x" size="16"></lucide-icon>
60
+ </button>
61
+ }
62
+ <lucide-icon [name]="icons.search"
63
+ size="16"
64
+ class="text-gray-400">
65
+ </lucide-icon>
66
+ </div>
67
+ </div>
68
+ }
69
+
70
+ <!-- Dropdown con opciones -->
71
+ <div class="max-h-40 overflow-x-hidden overflow-y-auto flex flex-col gap-1 scroll-element">
72
+ @if (isLoading) {
73
+ <div class="flex gap-3 text-black/50 dark:text-white/50 items-center justify-center py-4">
74
+ <lucide-icon [name]="icons.loading" size="20" class="animate-spin"></lucide-icon>
75
+ Cargando...
76
+ </div>
77
+ } @else {
78
+ @for (option of filteredOptions; track option.value) {
79
+ <div onKeyDown
80
+ (click)="selectOption(option)"
81
+ class="px-3 py-2 rounded text-sm cursor-pointer text-black! dark:text-white! hover:bg-accent hover:dark:bg-dark-accent/50"
82
+ [ngClass]="{'bg-accent dark:bg-dark-accent/50': selectedValue === option.value, 'text-black': selectedValue === option.value}">
83
+ <div class="flex items-center break-words whitespace-normal overflow-hidden">
84
+ {{option.text}}
85
+ </div>
86
+ </div>
87
+ }
88
+ @if (filteredOptions.length === 0) {
89
+ <div class="px-3 py-2 text-sm text-gray-500">No hay opciones disponibles</div>
90
+ }
91
+ }
92
+ </div>
93
+ </div>
94
+ </div>
95
+ }
@@ -0,0 +1,562 @@
1
+ import { Component, Input, Output, EventEmitter, ElementRef, ViewChild, OnDestroy, ChangeDetectorRef, AfterViewInit, OnInit, SimpleChanges, OnChanges, } from "@angular/core"
2
+ import { FormsModule, ControlValueAccessor, ReactiveFormsModule, NG_VALUE_ACCESSOR } from "@angular/forms"
3
+ import { CommonModule } from "@angular/common"
4
+ import { X, LucideAngularModule, ChevronDown, Loader2, Search } from "lucide-angular"
5
+ import { animate, style, transition, trigger } from "@angular/animations"
6
+ import { debounceTime, distinctUntilChanged, Subject, Subscription } from "rxjs"
7
+ import { JGenericCrudService } from "tailjng"
8
+
9
+ @Component({
10
+ selector: "JDropdownSelect",
11
+ imports: [LucideAngularModule, CommonModule, FormsModule, ReactiveFormsModule],
12
+ templateUrl: "./dropdown.component.html",
13
+ styleUrl: "./dropdown.component.css",
14
+ animations: [
15
+ trigger("modalTransition", [
16
+ transition(":enter", [
17
+ style({ transform: "translateX(1rem)", opacity: 0 }),
18
+ animate("300ms ease-out", style({ transform: "translateY(0)", opacity: 1 })),
19
+ ]),
20
+ transition(":leave", [animate("150ms ease-in", style({ transform: "translateX(1rem)", opacity: 0 }))]),
21
+ ]),
22
+ ],
23
+ providers: [
24
+ {
25
+ provide: NG_VALUE_ACCESSOR,
26
+ useExisting: JDropdownSelectComponent,
27
+ multi: true,
28
+ },
29
+ ],
30
+ })
31
+ export class JDropdownSelectComponent implements ControlValueAccessor, AfterViewInit, OnDestroy, OnInit, OnChanges {
32
+ // Lucide icons
33
+ icons = {
34
+ chevronDown: ChevronDown,
35
+ x: X,
36
+ search: Search,
37
+ loading: Loader2,
38
+ }
39
+
40
+ @Input() type: "dropdown" | "searchable" = "dropdown" // Solo dropdown y searchable
41
+ @Input() title = "Seleccionar" // Titulo del dropdown
42
+ @Input() placeholder = "Seleccione una opción" // Texto cuando no hay selección
43
+ @Input() showClear = false // Mostrar botón para limpiar selección
44
+ @Input() options: any[] = [] // Opciones para el dropdown
45
+ @Input() optionLabel: string | string[] = "text" // Propiedad a mostrar en el dropdown
46
+ @Input() optionValue = "value" // Propiedad a usar como valor en el dropdown
47
+ @Input() labelSeparator = " "
48
+ @Input() isLoading = false
49
+ @Input() disabled = false
50
+
51
+ // Datos para el searchable
52
+ @Input() loadOnInit = false // Cargar datos al inicializar
53
+ @Input() loadOpen = false // Cargar al abrir
54
+ @Input() defaultFilters: { [key: string]: any } = {} // Parámetros adicionales para la api
55
+ @Input() searchFields: any[] = [] // Filtros adicionales para la búsqueda
56
+ @Input() isSearch = true // Habilitar la búsqueda
57
+ @Input() isFilterSelect = false // Es un select de filtro
58
+ @Input() sort: "ASC" | "DESC" = "ASC"
59
+
60
+ @Output() selectionChange = new EventEmitter<any>()
61
+ @Output() fullData = new EventEmitter<any[]>()
62
+
63
+ @ViewChild("selectButton") selectButton!: ElementRef
64
+
65
+ // Selectores
66
+ isColumnSelectorOpen = false
67
+ selectedValue: any = null
68
+ selectedLabel = ""
69
+ internalOptions: Array<{ value: any; text: string }> = []
70
+
71
+ // Para la búsqueda
72
+ searchTerm = ""
73
+ private readonly searchSubject = new Subject<string>()
74
+ private searchSubscription?: Subscription
75
+ filteredOptions: Array<{ value: any; text: string; original?: any }> = []
76
+
77
+ // Dropdown positioning
78
+ dropdownTop = 0
79
+ dropdownLeft = 0
80
+ dropdownWidth = 0
81
+
82
+ // Para implementar ControlValueAccessor
83
+ private onChange: any = () => { }
84
+ private onTouched: any = () => { }
85
+
86
+ // Para detectar clicks fuera del componente
87
+ private clickOutsideListener: any
88
+
89
+ // Asignacion dinamica de parametros en el endpoint
90
+ private _rawEndpoint = ""
91
+ _finalEndpoint = ""
92
+ @Input() set endpoint(value: string) {
93
+ this._rawEndpoint = value
94
+ this.rebuildEndpoint()
95
+ }
96
+ get endpoint(): string {
97
+ return this._rawEndpoint
98
+ }
99
+
100
+ mainEndpoint!: string
101
+ private _dynamicParams: { [key: string]: any } = {}
102
+ @Input() set dynamicParams(value: { [key: string]: any }) {
103
+ this._dynamicParams = value
104
+ this.rebuildEndpoint()
105
+ }
106
+ get dynamicParams(): { [key: string]: any } {
107
+ return this._dynamicParams
108
+ }
109
+
110
+ private rebuildEndpoint() {
111
+ if (!this._rawEndpoint) return
112
+ this._finalEndpoint = this._rawEndpoint.replace(/\{([^}]+)\}/g, (_, key) => {
113
+ return this._dynamicParams?.[key] ?? ""
114
+ })
115
+ }
116
+
117
+ constructor(
118
+ private readonly cdr: ChangeDetectorRef,
119
+ private readonly elementRef: ElementRef,
120
+ private readonly genericService: JGenericCrudService,
121
+ ) { }
122
+
123
+ ngOnInit() {
124
+ this.mainEndpoint = this.endpoint.split("/")[0] ?? this.endpoint
125
+
126
+ // Configurar el debounce para la búsqueda
127
+ this.searchSubscription = this.searchSubject.pipe(debounceTime(1000), distinctUntilChanged()).subscribe(() => {
128
+ if (this.type === "searchable") {
129
+ this.loadData()
130
+ }
131
+ })
132
+
133
+ // Cargar datos al inicializar si es necesario
134
+ if (this.loadOnInit && this.type === "searchable") {
135
+ this.loadData()
136
+ }
137
+
138
+ // Asignar el texto del botón si no se ha proporcionado
139
+ this.updateSelectedLabel()
140
+ }
141
+
142
+ ngAfterViewInit() {
143
+ this.setupClickOutsideListener()
144
+ setTimeout(() => {
145
+ this.processOptions()
146
+ })
147
+ }
148
+
149
+ ngOnChanges(changes: SimpleChanges): void {
150
+ if (changes["options"]) {
151
+ this.processOptions()
152
+ }
153
+ }
154
+
155
+ ngOnDestroy() {
156
+ if (this.clickOutsideListener) {
157
+ document.removeEventListener("click", this.clickOutsideListener)
158
+ }
159
+ if (this.searchSubscription) {
160
+ this.searchSubscription.unsubscribe()
161
+ }
162
+ }
163
+
164
+ // ======================================================
165
+ // Métodos
166
+ // ======================================================
167
+
168
+ // Método para procesar las opciones del dropdown
169
+ processOptions() {
170
+ this.internalOptions = []
171
+
172
+ if (this.options && this.options.length > 0 && typeof this.options[0] !== "object") {
173
+ this.internalOptions = this.options.map((option) => ({
174
+ value: option,
175
+ text: option.toString(),
176
+ }))
177
+ } else if (this.options && this.options.length > 0) {
178
+ this.internalOptions = this.options.map((option) => {
179
+ const text = Array.isArray(this.optionLabel)
180
+ ? this.optionLabel.map((k) => this.getNestedValue(option, k)).join(" ")
181
+ : this.getNestedValue(option, this.optionLabel)
182
+ return {
183
+ value: option[this.optionValue],
184
+ text,
185
+ original: option,
186
+ }
187
+ })
188
+ }
189
+
190
+ this.filteredOptions = [...this.internalOptions]
191
+ this.updateSelectedLabel()
192
+ this.cdr.detectChanges()
193
+ }
194
+
195
+ // Seleccionar una opción
196
+ selectOption(option: { value: any; text: string; original?: any }) {
197
+ this.selectedValue = option.value
198
+ this.selectedLabel = option.text
199
+ this.onChange(this.selectedValue)
200
+ this.selectionChange.emit(option.original ?? option.value)
201
+ this.isColumnSelectorOpen = false
202
+ }
203
+
204
+ // Limpiar la selección
205
+ clearSelection(event: Event) {
206
+ event.stopPropagation()
207
+ this.writeValue(null)
208
+ this.onChange(null)
209
+ this.selectionChange.emit(null)
210
+ }
211
+
212
+ // Limpiar el término de búsqueda
213
+ clearSearchTerm(): void {
214
+ this.searchTerm = ""
215
+ this.onSearchInput()
216
+ }
217
+
218
+ // Actualizar el texto del botón
219
+ updateSelectedLabel() {
220
+ if (this.selectedValue === null) {
221
+ this.selectedLabel = this.placeholder
222
+ return
223
+ }
224
+
225
+ const selectedOption = this.internalOptions.find((opt) => opt.value === this.selectedValue)
226
+ this.selectedLabel = selectedOption ? selectedOption.text : this.placeholder
227
+ }
228
+
229
+ // Obtener el valor anidado de un objeto
230
+ getNestedValue(obj: any, path: string): any {
231
+ return path.split(".").reduce((acc, part) => acc && acc[part], obj) ?? ""
232
+ }
233
+
234
+ // ======================================================
235
+ // Search input service
236
+ // ======================================================
237
+
238
+ // Cargar datos desde el servicio
239
+ loadData() {
240
+ if (!this.endpoint) return
241
+
242
+ this.isLoading = true
243
+ const params: any = {}
244
+ params["sortOrder"] = this.sort
245
+
246
+ // Aplicar los filtros predeterminados enviados desde el padre
247
+ Object.keys(this.defaultFilters).forEach((key) => {
248
+ params[`filter[${key}]`] = this.defaultFilters[key]
249
+ })
250
+
251
+ // Añadir término de búsqueda a los parámetros
252
+ if (this.searchTerm && this.searchTerm.trim() !== "") {
253
+ params["search"] = this.searchTerm
254
+ // Aplicar filtros de búsqueda adicionales
255
+ const optionLabelsArray = Array.isArray(this.optionLabel) ? this.optionLabel : [this.optionLabel]
256
+ const allSearchFields = [...optionLabelsArray, ...this.searchFields]
257
+ params["searchFields"] = allSearchFields
258
+ }
259
+
260
+ this.genericService.getAll<any>(this._finalEndpoint, params).subscribe({
261
+ next: (response) => {
262
+ // Procesar la respuesta según la estructura de tu API
263
+ const data = response.data[this.mainEndpoint] || []
264
+ this.options = data
265
+
266
+ // Procesar las opciones para el dropdown
267
+ this.internalOptions = this.options.map((option) => ({
268
+ value: option[this.optionValue],
269
+ text: this.resolveLabel(option),
270
+ original: option,
271
+ }))
272
+
273
+ this.filteredOptions = [...this.internalOptions]
274
+ this.fullData.emit(this.options)
275
+ this.isLoading = false
276
+ this.updateSelectedLabel()
277
+ this.cdr.detectChanges()
278
+ },
279
+ error: (error) => {
280
+ console.error("Error fetching data:", error)
281
+ this.isLoading = false
282
+ this.cdr.detectChanges()
283
+ },
284
+ })
285
+ }
286
+
287
+ // Manejar la entrada de búsqueda
288
+ onSearchInput() {
289
+ if (this.type === "searchable") {
290
+ // Para el tipo searchable, enviar la búsqueda al servicio
291
+ this.searchSubject.next(this.searchTerm)
292
+ } else {
293
+ // Para otros tipos, filtrar localmente
294
+ this.filterOptions()
295
+ }
296
+ }
297
+
298
+ // Filtrar opciones localmente
299
+ filterOptions() {
300
+ if (!this.searchTerm || this.searchTerm.trim() === "") {
301
+ this.filteredOptions = [...this.internalOptions]
302
+ return
303
+ }
304
+
305
+ const searchTermLower = this.searchTerm.toLowerCase()
306
+ this.filteredOptions = this.internalOptions.filter((option) => option.text.toLowerCase().includes(searchTermLower))
307
+ }
308
+
309
+ resolveLabel(option: any): string {
310
+ if (Array.isArray(this.optionLabel)) {
311
+ return this.optionLabel
312
+ .map((key) => this.getNestedValue(option, key))
313
+ .filter(Boolean)
314
+ .join(this.labelSeparator)
315
+ }
316
+ return this.getNestedValue(option, this.optionLabel)
317
+ }
318
+
319
+ // ======================================================
320
+ // Elemento
321
+ // ======================================================
322
+
323
+ // Abrir o cerrar el dropdown
324
+ toggleColumnSelector() {
325
+ if (this.disabled) return
326
+
327
+ this.isColumnSelectorOpen = !this.isColumnSelectorOpen
328
+ if (this.isColumnSelectorOpen) {
329
+ this.onTouched()
330
+ this.updateDropdownPosition()
331
+
332
+ // Cargar cada que se abre el dropdown
333
+ if (this.type === "searchable" && this.loadOpen) {
334
+ this.loadData()
335
+ }
336
+
337
+ if (this.type === "searchable" && !this.loadOnInit && this.shouldTriggerLoad()) {
338
+ this.loadData()
339
+ }
340
+ }
341
+ }
342
+
343
+ // Detectar clicks fuera del componente
344
+ setupClickOutsideListener() {
345
+ this.clickOutsideListener = (event: MouseEvent) => {
346
+ const clickedElement = event.target as HTMLElement
347
+ const isOutsideDropdown = !this.elementRef.nativeElement.contains(clickedElement)
348
+ if (this.isColumnSelectorOpen && isOutsideDropdown) {
349
+ this.isColumnSelectorOpen = false
350
+ this.cdr.detectChanges()
351
+ }
352
+ }
353
+ document.addEventListener("click", this.clickOutsideListener)
354
+ }
355
+
356
+ // Actualizar la posición del dropdown
357
+ updateDropdownPosition() {
358
+ setTimeout(() => {
359
+ if (!this.selectButton) return
360
+
361
+ // Get button position
362
+ const button = this.selectButton.nativeElement
363
+ const buttonRect = button.getBoundingClientRect()
364
+
365
+ // Find the closest form container or dialog
366
+ let offsetParent: HTMLElement | null = this.selectButton.nativeElement
367
+ let isInSidebar = false
368
+
369
+ // Check if we're inside a sidebar form
370
+ while (
371
+ offsetParent &&
372
+ !offsetParent.classList.contains("content_form") &&
373
+ !offsetParent.classList.contains("p-dialog")
374
+ ) {
375
+ if (offsetParent.classList.contains("fixed") && offsetParent.classList.contains("right-0")) {
376
+ isInSidebar = true
377
+ break
378
+ }
379
+ offsetParent = offsetParent.parentElement
380
+ }
381
+
382
+ // Get offsets based on container
383
+ let offsetTop = 0
384
+ let offsetLeft = 0
385
+ if (
386
+ isInSidebar ||
387
+ (offsetParent &&
388
+ (offsetParent.classList.contains("content_form") || offsetParent.classList.contains("p-dialog")))
389
+ ) {
390
+ offsetTop = offsetParent ? offsetParent.getBoundingClientRect().top : 0
391
+ offsetLeft = offsetParent ? offsetParent.getBoundingClientRect().left : 0
392
+ }
393
+
394
+ // Position directly below the button by default
395
+ this.dropdownTop = buttonRect.bottom - offsetTop
396
+ this.dropdownLeft = buttonRect.left - offsetLeft
397
+ this.dropdownWidth = buttonRect.width
398
+ this.cdr.detectChanges()
399
+
400
+ // Wait for dropdown to be in DOM
401
+ setTimeout(() => {
402
+ // Get dropdown element
403
+ const dropdown = this.elementRef.nativeElement.querySelector(".absolute.z-\\[100\\]")
404
+ if (!dropdown) return
405
+
406
+ // First use fixed positioning to handle scroll position correctly
407
+ dropdown.style.position = "fixed"
408
+ dropdown.style.top = buttonRect.bottom + "px"
409
+ dropdown.style.left = buttonRect.left + "px"
410
+ dropdown.style.width = buttonRect.width + "px"
411
+ dropdown.style.zIndex = "600"
412
+
413
+ // Wait for dropdown to render with fixed positioning
414
+ setTimeout(() => {
415
+ // Get dropdown dimensions
416
+ const dropdownRect = dropdown.getBoundingClientRect()
417
+ const viewportHeight = window.innerHeight
418
+ const documentWidth = document.documentElement.clientWidth
419
+
420
+ // Determine if we need to flip or adjust position
421
+ let newFixedTop = buttonRect.bottom
422
+ let newFixedLeft = buttonRect.left
423
+
424
+ // Check if dropdown goes below viewport and flip it if needed
425
+ if (buttonRect.bottom + dropdownRect.height > viewportHeight) {
426
+ newFixedTop = buttonRect.top - dropdownRect.height
427
+ }
428
+
429
+ // Check if dropdown goes beyond right edge
430
+ if (buttonRect.left + dropdownRect.width > documentWidth) {
431
+ newFixedLeft = buttonRect.left - dropdownRect.width + buttonRect.width
432
+ if (newFixedLeft < 0) {
433
+ newFixedLeft = 5
434
+ if (dropdownRect.width > documentWidth) {
435
+ dropdown.classList.add("constrain-width")
436
+ }
437
+ }
438
+ }
439
+
440
+ // Apply adjusted fixed position
441
+ dropdown.style.top = newFixedTop - 5 + "px"
442
+ dropdown.style.left = newFixedLeft - 15 + "px"
443
+
444
+ // Es boton de filtro
445
+ if (this.isFilterSelect) {
446
+ setTimeout(() => {
447
+ // Calculate absolute position based on the current fixed position
448
+ let newAbsoluteTop
449
+ let newAbsoluteLeft
450
+
451
+ if (newFixedTop === buttonRect.bottom) {
452
+ // Dropdown is below the button
453
+ newAbsoluteTop = this.dropdownTop
454
+ } else {
455
+ // Dropdown is above the button (flipped)
456
+ newAbsoluteTop = buttonRect.top - dropdownRect.height - offsetTop
457
+ }
458
+
459
+ if (newFixedLeft === buttonRect.left) {
460
+ // Dropdown is aligned with left edge of button
461
+ newAbsoluteLeft = this.dropdownLeft
462
+ } else if (newFixedLeft === buttonRect.left - dropdownRect.width + buttonRect.width) {
463
+ // Dropdown is aligned with right edge of button
464
+ newAbsoluteLeft = buttonRect.left - dropdownRect.width + buttonRect.width - offsetLeft
465
+ } else {
466
+ // Dropdown is at a fixed position from left edge
467
+ newAbsoluteLeft = newFixedLeft
468
+ }
469
+
470
+ // Switch to absolute positioning
471
+ dropdown.style.position = "absolute"
472
+ dropdown.style.top = newAbsoluteTop + "px"
473
+ dropdown.style.left = newAbsoluteLeft - 2 + "px"
474
+
475
+ // Update internal state
476
+ this.dropdownTop = newAbsoluteTop
477
+ this.dropdownLeft = newAbsoluteLeft
478
+ this.cdr.detectChanges()
479
+ }, 0)
480
+ } else {
481
+ // After positioning is done, switch to absolute positioning
482
+ setTimeout(() => {
483
+ // Get the current visual position of the dropdown (relative to viewport)
484
+ const dropdownRect = dropdown.getBoundingClientRect()
485
+
486
+ // Find the dropdown's offset parent for absolute positioning
487
+ const dropdownOffsetParent = this.findPositionedParent(dropdown) || document.body
488
+ const parentRect = dropdownOffsetParent.getBoundingClientRect()
489
+
490
+ // Calculate absolute position that will maintain the same visual position
491
+ // Absolute positioning is relative to the offset parent
492
+ const absoluteTop = dropdownRect.top - parentRect.top + dropdownOffsetParent.scrollTop
493
+ const absoluteLeft = dropdownRect.left - parentRect.left + dropdownOffsetParent.scrollLeft
494
+
495
+ // Switch to absolute positioning with calculated coordinates
496
+ dropdown.style.position = "absolute"
497
+ dropdown.style.top = absoluteTop + "px"
498
+ dropdown.style.left = absoluteLeft + "px"
499
+
500
+ // Update internal state
501
+ this.dropdownTop = absoluteTop
502
+ this.dropdownLeft = absoluteLeft
503
+ this.cdr.detectChanges()
504
+ }, 0)
505
+ }
506
+ }, 0)
507
+ }, 0)
508
+ })
509
+ }
510
+
511
+ // Helper method to find the offset parent for absolute positioning
512
+ private findPositionedParent(element: HTMLElement): HTMLElement | null {
513
+ if (!element) return null
514
+ let parent = element.parentElement
515
+ while (parent) {
516
+ const position = window.getComputedStyle(parent).position
517
+ if (position === "relative" || position === "absolute" || position === "fixed") {
518
+ return parent
519
+ }
520
+ parent = parent.parentElement
521
+ }
522
+ return document.body // Default to body if no positioned parent found
523
+ }
524
+
525
+ // Leer el valor seleccionado
526
+ writeValue(value: any): void {
527
+ this.selectedValue = value
528
+
529
+ // Si ya hay opciones disponibles, actualiza el label
530
+ if (this.internalOptions.length > 0) {
531
+ this.updateSelectedLabel()
532
+ } else {
533
+ // Esperar a que se carguen y luego actualizar el label
534
+ setTimeout(() => {
535
+ this.updateSelectedLabel()
536
+ this.cdr.detectChanges()
537
+ })
538
+ }
539
+ this.cdr.markForCheck()
540
+ }
541
+
542
+ // ================================================
543
+ // Registrar cambios
544
+ // ================================================
545
+
546
+ // Registrar el cambio del valor seleccionado
547
+ registerOnChange(fn: any): void {
548
+ this.onChange = fn
549
+ }
550
+
551
+ // Registrar el evento de tocar
552
+ registerOnTouched(fn: any): void {
553
+ this.onTouched = fn
554
+ }
555
+
556
+ // Cargar elementos del searchable cuando es necesario
557
+ private shouldTriggerLoad(): boolean {
558
+ const isSearchActive = this.searchTerm && this.searchTerm.trim() !== ""
559
+ const isInitialState = this.selectedValue === null
560
+ return isSearchActive || isInitialState
561
+ }
562
+ }