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.
- package/cli/component-manager.js +45 -0
- package/cli/dependency-manager.js +52 -0
- package/cli/file-operations.js +88 -0
- package/cli/index.js +51 -0
- package/cli/settings/colors.js +17 -0
- package/cli/settings/components-list.js +87 -0
- package/cli/settings/header-generator.js +42 -0
- package/cli/settings/path-utils.js +50 -0
- package/cli/settings/prompt-utils.js +37 -0
- package/cli/settings/tailwind-check.js +21 -0
- package/fesm2022/tailjng.mjs +903 -25
- package/fesm2022/tailjng.mjs.map +1 -1
- package/lib/config/tailjng-config.token.d.ts +3 -0
- package/lib/interfaces/alert/dialog-alert.interface.d.ts +52 -0
- package/lib/interfaces/alert/toast-alert.interface.d.ts +52 -0
- package/lib/interfaces/config.interface.d.ts +5 -0
- package/lib/interfaces/crud/api-response.d.ts +29 -0
- package/lib/interfaces/crud/crud.interface.d.ts +103 -0
- package/lib/services/alert/dialog-alert.service.d.ts +24 -0
- package/lib/services/alert/toast-alert.service.d.ts +26 -0
- package/lib/services/crud/converter-crud.service.d.ts +41 -0
- package/lib/services/crud/generic-crud.service.d.ts +81 -0
- package/lib/services/http/error-handler-http.service.d.ts +26 -0
- package/lib/services/http/params-http.service.d.ts +13 -0
- package/lib/services/static/icons.service.d.ts +31 -0
- package/lib/services/transformer/calendar.service.d.ts +71 -0
- package/package.json +5 -3
- package/public-api.d.ts +10 -3
- package/src/lib/components/alert/dialog-alert/dialog-alert.component.css +0 -0
- package/src/lib/components/alert/dialog-alert/dialog-alert.component.html +72 -0
- package/src/lib/components/alert/dialog-alert/dialog-alert.component.ts +66 -0
- package/src/lib/components/alert/toast-alert/toast-alert.component.css +5 -0
- package/src/lib/components/alert/toast-alert/toast-alert.component.html +76 -0
- package/src/lib/components/alert/toast-alert/toast-alert.component.ts +87 -0
- package/src/lib/components/button/button.component.css +0 -0
- package/src/lib/components/button/button.component.html +36 -0
- package/src/lib/components/button/button.component.ts +95 -0
- package/src/lib/components/checkbox/input-checkbox/input-checkbox.component.css +0 -0
- package/src/lib/components/checkbox/input-checkbox/input-checkbox.component.html +23 -0
- package/src/lib/components/checkbox/input-checkbox/input-checkbox.component.ts +44 -0
- package/src/lib/components/checkbox/switch-checkbox/switch-checkbox.component.css +0 -0
- package/src/lib/components/checkbox/switch-checkbox/switch-checkbox.component.html +26 -0
- package/src/lib/components/checkbox/switch-checkbox/switch-checkbox.component.ts +29 -0
- package/src/lib/components/color/colors.service.ts +109 -0
- package/src/lib/components/dialog/dialog.component.css +8 -0
- package/src/lib/components/dialog/dialog.component.html +57 -0
- package/src/lib/components/dialog/dialog.component.ts +179 -0
- package/src/lib/components/image/viewer-image/viewer-image.component.css +4 -0
- package/src/lib/components/image/viewer-image/viewer-image.component.html +75 -0
- package/src/lib/components/image/viewer-image/viewer-image.component.ts +131 -0
- package/src/lib/components/input/file-input/file-input.component.css +0 -0
- package/src/lib/components/input/file-input/file-input.component.html +49 -0
- package/src/lib/components/input/file-input/file-input.component.ts +218 -0
- package/src/lib/components/input/input/input.component.css +0 -0
- package/src/lib/components/input/input/input.component.html +24 -0
- package/src/lib/components/input/input/input.component.ts +78 -0
- package/src/lib/components/input/range-input/range-input.component.css +0 -0
- package/src/lib/components/input/range-input/range-input.component.html +64 -0
- package/src/lib/components/input/range-input/range-input.component.ts +78 -0
- package/src/lib/components/input/textarea-input/textarea-input.component.css +0 -0
- package/src/lib/components/input/textarea-input/textarea-input.component.html +21 -0
- package/src/lib/components/input/textarea-input/textarea-input.component.ts +75 -0
- package/src/lib/components/label/label.component.html +1 -1
- package/src/lib/components/label/label.component.ts +1 -1
- package/src/lib/components/mode-toggle/mode-toggle.component.css +0 -0
- package/src/lib/components/mode-toggle/mode-toggle.component.html +8 -0
- package/src/lib/components/mode-toggle/mode-toggle.component.ts +61 -0
- package/src/lib/components/progress-bar/progress-bar.component.css +0 -0
- package/src/lib/components/progress-bar/progress-bar.component.html +22 -0
- package/src/lib/components/progress-bar/progress-bar.component.ts +20 -0
- package/src/lib/components/select/dropdown/dropdown.component.css +0 -0
- package/src/lib/components/select/dropdown/dropdown.component.html +95 -0
- package/src/lib/components/select/dropdown/dropdown.component.ts +562 -0
- package/src/lib/components/select/multi-dropdown/multi-dropdown.component.css +0 -0
- package/src/lib/components/select/multi-dropdown/multi-dropdown.component.html +87 -0
- package/src/lib/components/select/multi-dropdown/multi-dropdown.component.ts +315 -0
- package/src/lib/components/select/multi-table/multi-table.component.css +0 -0
- package/src/lib/components/select/multi-table/multi-table.component.html +83 -0
- package/src/lib/components/select/multi-table/multi-table.component.ts +230 -0
- package/src/lib/components/toggle-radio/toggle-radio.component.css +0 -0
- package/src/lib/components/toggle-radio/toggle-radio.component.html +51 -0
- package/src/lib/components/toggle-radio/toggle-radio.component.ts +203 -0
- package/src/styles.css +126 -0
- package/cli/tailjng.js +0 -105
- package/lib/services/icons.service.d.ts +0 -9
- package/lib/tailjng.component.d.ts +0 -5
- 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
|
+
}
|
|
File without changes
|
|
@@ -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
|
+
}
|
|
File without changes
|