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,87 @@
|
|
|
1
|
+
<div class="relative w-full h-full">
|
|
2
|
+
<!-- Botón para multi-dropdown -->
|
|
3
|
+
<div class="w-auto" #selectButton>
|
|
4
|
+
<button type="button"
|
|
5
|
+
[disabled]="disabled || isLoading"
|
|
6
|
+
(click)="toggleDropdown()"
|
|
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]="{ 'opacity-50 cursor-not-allowed pointer-events-none': disabled || isLoading }">
|
|
9
|
+
<span class="truncate text-black dark:text-white"
|
|
10
|
+
[ngClass]="{ 'opacity-50': selectedValues.length === 0 }">
|
|
11
|
+
{{ displayLabel }}
|
|
12
|
+
</span>
|
|
13
|
+
<div class="flex items-center">
|
|
14
|
+
@if (showClear && selectedValues.length > 0) {
|
|
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': isDropdownOpen }"></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 (isDropdownOpen) {
|
|
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
|
+
<!-- Multi-Dropdown Options -->
|
|
47
|
+
<div class="max-h-60 overflow-auto flex flex-col gap-1 scroll-element">
|
|
48
|
+
<!-- Select All Option -->
|
|
49
|
+
|
|
50
|
+
@if (processedOptions.length > 0) {
|
|
51
|
+
@if (enableSelectAll) {
|
|
52
|
+
<div (click)="toggleSelectAll()"
|
|
53
|
+
class="flex gap-5 items-center border border-accent dark:border-dark-accent/50 px-4 py-2 cursor-pointer group rounded-md transition-all hover:bg-accent dark:hover:bg-dark-accent/50"
|
|
54
|
+
[ngClass]="{'bg-accent dark:bg-dark-accent/50': isAllSelected()}">
|
|
55
|
+
<input type="checkbox" class="hidden" [checked]="isAllSelected()" />
|
|
56
|
+
@if (isAllSelected()) {
|
|
57
|
+
<lucide-icon [name]="icons.check" size="15" class="transition text-black dark:text-white" />
|
|
58
|
+
} @else {
|
|
59
|
+
<lucide-icon [name]="icons.squareDashedMousePointer" size="15" class="transition text-black dark:text-white opacity-40" />
|
|
60
|
+
}
|
|
61
|
+
<label class="text-black dark:text-white text-sm font-medium w-full">{{ labelSelectAll }}</label>
|
|
62
|
+
</div>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
<!-- Individual Options -->
|
|
66
|
+
@for (option of processedOptions; track option.value) {
|
|
67
|
+
<div (click)="toggleOption(option)"
|
|
68
|
+
class="flex gap-5 items-center border border-accent dark:border-dark-accent/50 px-4 py-2 cursor-pointer group rounded-md transition-all hover:bg-accent dark:hover:bg-dark-accent/50"
|
|
69
|
+
[ngClass]="{'bg-accent dark:bg-dark-accent/50' : selectedValues.includes(option.value)}">
|
|
70
|
+
<input type="checkbox" class="hidden" [checked]="selectedValues.includes(option.value)" />
|
|
71
|
+
@if (selectedValues.includes(option.value)) {
|
|
72
|
+
<lucide-icon [name]="icons.check" size="15" class="transition text-black dark:text-white" />
|
|
73
|
+
} @else {
|
|
74
|
+
<lucide-icon [name]="icons.squareDashedMousePointer" size="15" class="transition text-black dark:text-white opacity-40" />
|
|
75
|
+
}
|
|
76
|
+
<label class="text-black dark:text-white text-sm font-medium w-full">{{ option.text }}</label>
|
|
77
|
+
</div>
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@if (processedOptions.length === 0) {
|
|
82
|
+
<div class="px-3 py-2 text-sm text-gray-500">No hay opciones disponibles</div>
|
|
83
|
+
}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { Component, Input, Output, EventEmitter, ElementRef, ViewChild, OnDestroy, ChangeDetectorRef, AfterViewInit, OnInit, SimpleChanges, OnChanges, } from "@angular/core"
|
|
2
|
+
import { FormsModule, type ControlValueAccessor, ReactiveFormsModule, NG_VALUE_ACCESSOR } from "@angular/forms"
|
|
3
|
+
import { CommonModule } from "@angular/common"
|
|
4
|
+
import { X, LucideAngularModule, ChevronDown, Check, Loader2, SquareDashedMousePointer } from "lucide-angular"
|
|
5
|
+
import { animate, style, transition, trigger } from "@angular/animations"
|
|
6
|
+
|
|
7
|
+
interface ProcessedOption {
|
|
8
|
+
value: any
|
|
9
|
+
text: string
|
|
10
|
+
original?: any
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
selector: "JMultiDropdownSelect",
|
|
15
|
+
imports: [LucideAngularModule, CommonModule, FormsModule, ReactiveFormsModule],
|
|
16
|
+
templateUrl: "./multi-dropdown.component.html",
|
|
17
|
+
styleUrl: "./multi-dropdown.component.css",
|
|
18
|
+
animations: [
|
|
19
|
+
trigger("modalTransition", [
|
|
20
|
+
transition(":enter", [
|
|
21
|
+
style({ transform: "translateX(1rem)", opacity: 0 }),
|
|
22
|
+
animate("300ms ease-out", style({ transform: "translateY(0)", opacity: 1 })),
|
|
23
|
+
]),
|
|
24
|
+
transition(":leave", [animate("150ms ease-in", style({ transform: "translateX(1rem)", opacity: 0 }))]),
|
|
25
|
+
]),
|
|
26
|
+
],
|
|
27
|
+
providers: [
|
|
28
|
+
{
|
|
29
|
+
provide: NG_VALUE_ACCESSOR,
|
|
30
|
+
useExisting: JMultiDropdownSelectComponent,
|
|
31
|
+
multi: true,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
})
|
|
35
|
+
export class JMultiDropdownSelectComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnChanges, OnDestroy {
|
|
36
|
+
// Lucide icons
|
|
37
|
+
icons = {
|
|
38
|
+
chevronDown: ChevronDown,
|
|
39
|
+
x: X,
|
|
40
|
+
check: Check,
|
|
41
|
+
loading: Loader2,
|
|
42
|
+
squareDashedMousePointer: SquareDashedMousePointer,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Input() title = "Seleccionar opciones"
|
|
46
|
+
@Input() placeholder = "Seleccione opciones"
|
|
47
|
+
@Input() showClear = true
|
|
48
|
+
@Input() disabled = false
|
|
49
|
+
@Input() isLoading = false
|
|
50
|
+
|
|
51
|
+
// Opciones y configuración
|
|
52
|
+
@Input() options: any[] = []
|
|
53
|
+
@Input() optionLabel: string | string[] = "text"
|
|
54
|
+
@Input() optionValue = "value"
|
|
55
|
+
@Input() labelSeparator = " "
|
|
56
|
+
|
|
57
|
+
// Multi-selection específico
|
|
58
|
+
@Input() enableSelectAll = true
|
|
59
|
+
@Input() labelSelectAll = "TODOS"
|
|
60
|
+
@Input() multipleSeparator = ", "
|
|
61
|
+
@Input() maxDisplayItems = 3 // Máximo de items a mostrar antes de "X más"
|
|
62
|
+
|
|
63
|
+
@Output() selectionChange = new EventEmitter<any[]>()
|
|
64
|
+
|
|
65
|
+
@ViewChild("selectButton") selectButton!: ElementRef
|
|
66
|
+
|
|
67
|
+
// Estado del componente
|
|
68
|
+
isDropdownOpen = false
|
|
69
|
+
selectedValues: any[] = []
|
|
70
|
+
processedOptions: ProcessedOption[] = []
|
|
71
|
+
displayLabel = ""
|
|
72
|
+
|
|
73
|
+
// Dropdown positioning
|
|
74
|
+
dropdownTop = 0
|
|
75
|
+
dropdownLeft = 0
|
|
76
|
+
dropdownWidth = 0
|
|
77
|
+
|
|
78
|
+
// ControlValueAccessor
|
|
79
|
+
private onChange: any = () => { }
|
|
80
|
+
private onTouched: any = () => { }
|
|
81
|
+
|
|
82
|
+
// Click outside listener
|
|
83
|
+
private clickOutsideListener: any
|
|
84
|
+
|
|
85
|
+
constructor(
|
|
86
|
+
private readonly cdr: ChangeDetectorRef,
|
|
87
|
+
private readonly elementRef: ElementRef,
|
|
88
|
+
) { }
|
|
89
|
+
|
|
90
|
+
ngOnInit() {
|
|
91
|
+
this.updateDisplayLabel()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ngAfterViewInit() {
|
|
95
|
+
this.setupClickOutsideListener()
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
this.processOptions()
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ngOnChanges(changes: SimpleChanges): void {
|
|
102
|
+
if (changes["options"]) {
|
|
103
|
+
this.processOptions()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
ngOnDestroy() {
|
|
108
|
+
if (this.clickOutsideListener) {
|
|
109
|
+
document.removeEventListener("click", this.clickOutsideListener)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ======================================================
|
|
114
|
+
// Procesamiento de opciones
|
|
115
|
+
// ======================================================
|
|
116
|
+
|
|
117
|
+
processOptions() {
|
|
118
|
+
this.processedOptions = []
|
|
119
|
+
|
|
120
|
+
if (this.options && this.options.length > 0 && typeof this.options[0] !== "object") {
|
|
121
|
+
// Opciones simples (string, number)
|
|
122
|
+
this.processedOptions = this.options.map((option) => ({
|
|
123
|
+
value: option,
|
|
124
|
+
text: option.toString(),
|
|
125
|
+
}))
|
|
126
|
+
} else if (this.options && this.options.length > 0) {
|
|
127
|
+
// Opciones complejas (objetos)
|
|
128
|
+
this.processedOptions = this.options.map((option) => {
|
|
129
|
+
const text = Array.isArray(this.optionLabel)
|
|
130
|
+
? this.optionLabel.map((k) => this.getNestedValue(option, k)).join(this.labelSeparator)
|
|
131
|
+
: this.getNestedValue(option, this.optionLabel)
|
|
132
|
+
return {
|
|
133
|
+
value: option[this.optionValue],
|
|
134
|
+
text,
|
|
135
|
+
original: option,
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.updateDisplayLabel()
|
|
141
|
+
this.cdr.detectChanges()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ======================================================
|
|
145
|
+
// Manejo de selección múltiple
|
|
146
|
+
// ======================================================
|
|
147
|
+
|
|
148
|
+
toggleOption(option: ProcessedOption) {
|
|
149
|
+
const index = this.selectedValues.findIndex((v) => v === option.value)
|
|
150
|
+
|
|
151
|
+
if (index > -1) {
|
|
152
|
+
// Deseleccionar
|
|
153
|
+
this.selectedValues.splice(index, 1)
|
|
154
|
+
} else {
|
|
155
|
+
// Seleccionar
|
|
156
|
+
this.selectedValues.push(option.value)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.updateDisplayLabel()
|
|
160
|
+
this.onChange(this.selectedValues)
|
|
161
|
+
this.selectionChange.emit(this.selectedValues)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
toggleSelectAll() {
|
|
165
|
+
const allValues = this.processedOptions.map((opt) => opt.value)
|
|
166
|
+
|
|
167
|
+
if (this.isAllSelected()) {
|
|
168
|
+
// Deseleccionar todos
|
|
169
|
+
this.selectedValues = []
|
|
170
|
+
} else {
|
|
171
|
+
// Seleccionar todos
|
|
172
|
+
this.selectedValues = [...allValues]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.updateDisplayLabel()
|
|
176
|
+
this.onChange(this.selectedValues)
|
|
177
|
+
this.selectionChange.emit(this.selectedValues)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
isAllSelected(): boolean {
|
|
181
|
+
const allValues = this.processedOptions.map((opt) => opt.value)
|
|
182
|
+
return allValues.length > 0 && allValues.every((v) => this.selectedValues.includes(v))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
clearSelection(event: Event) {
|
|
186
|
+
event.stopPropagation()
|
|
187
|
+
this.selectedValues = []
|
|
188
|
+
this.updateDisplayLabel()
|
|
189
|
+
this.onChange(this.selectedValues)
|
|
190
|
+
this.selectionChange.emit(this.selectedValues)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ======================================================
|
|
194
|
+
// Display y UI
|
|
195
|
+
// ======================================================
|
|
196
|
+
|
|
197
|
+
updateDisplayLabel() {
|
|
198
|
+
if (this.selectedValues.length === 0) {
|
|
199
|
+
this.displayLabel = this.placeholder
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Verificar si todos están seleccionados
|
|
204
|
+
if (this.enableSelectAll && this.isAllSelected()) {
|
|
205
|
+
this.displayLabel = this.labelSelectAll
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Obtener textos de las opciones seleccionadas
|
|
210
|
+
const selectedTexts = this.processedOptions
|
|
211
|
+
.filter((opt) => this.selectedValues.includes(opt.value))
|
|
212
|
+
.map((opt) => opt.text)
|
|
213
|
+
|
|
214
|
+
if (selectedTexts.length <= this.maxDisplayItems) {
|
|
215
|
+
// Mostrar todos los elementos
|
|
216
|
+
this.displayLabel = selectedTexts.join(this.multipleSeparator)
|
|
217
|
+
} else {
|
|
218
|
+
// Mostrar algunos elementos + "X más"
|
|
219
|
+
const visibleItems = selectedTexts.slice(0, this.maxDisplayItems)
|
|
220
|
+
const remainingCount = selectedTexts.length - this.maxDisplayItems
|
|
221
|
+
this.displayLabel = `${visibleItems.join(this.multipleSeparator)} y ${remainingCount} más`
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
toggleDropdown() {
|
|
226
|
+
if (this.disabled || this.isLoading) return
|
|
227
|
+
|
|
228
|
+
this.isDropdownOpen = !this.isDropdownOpen
|
|
229
|
+
if (this.isDropdownOpen) {
|
|
230
|
+
this.onTouched()
|
|
231
|
+
this.updateDropdownPosition()
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ======================================================
|
|
236
|
+
// Utilidades
|
|
237
|
+
// ======================================================
|
|
238
|
+
|
|
239
|
+
getNestedValue(obj: any, path: string): any {
|
|
240
|
+
return path.split(".").reduce((acc, part) => acc && acc[part], obj) ?? ""
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
setupClickOutsideListener() {
|
|
244
|
+
this.clickOutsideListener = (event: MouseEvent) => {
|
|
245
|
+
const clickedElement = event.target as HTMLElement
|
|
246
|
+
const isOutsideDropdown = !this.elementRef.nativeElement.contains(clickedElement)
|
|
247
|
+
if (this.isDropdownOpen && isOutsideDropdown) {
|
|
248
|
+
this.isDropdownOpen = false
|
|
249
|
+
this.cdr.detectChanges()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
document.addEventListener("click", this.clickOutsideListener)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
updateDropdownPosition() {
|
|
256
|
+
setTimeout(() => {
|
|
257
|
+
if (!this.selectButton) return
|
|
258
|
+
|
|
259
|
+
const button = this.selectButton.nativeElement
|
|
260
|
+
const buttonRect = button.getBoundingClientRect()
|
|
261
|
+
|
|
262
|
+
// Posición básica
|
|
263
|
+
this.dropdownTop = buttonRect.bottom + window.scrollY
|
|
264
|
+
this.dropdownLeft = buttonRect.left + window.scrollX
|
|
265
|
+
this.dropdownWidth = buttonRect.width
|
|
266
|
+
|
|
267
|
+
this.cdr.detectChanges()
|
|
268
|
+
|
|
269
|
+
// Ajustar posición si es necesario
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
const dropdown = this.elementRef.nativeElement.querySelector(".absolute.z-\\[100\\]")
|
|
272
|
+
if (!dropdown) return
|
|
273
|
+
|
|
274
|
+
const dropdownRect = dropdown.getBoundingClientRect()
|
|
275
|
+
const viewportHeight = window.innerHeight
|
|
276
|
+
const viewportWidth = window.innerWidth
|
|
277
|
+
|
|
278
|
+
// Ajustar si se sale por abajo
|
|
279
|
+
if (buttonRect.bottom + dropdownRect.height > viewportHeight) {
|
|
280
|
+
this.dropdownTop = buttonRect.top + window.scrollY - dropdownRect.height
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Ajustar si se sale por la derecha
|
|
284
|
+
if (buttonRect.left + dropdownRect.width > viewportWidth) {
|
|
285
|
+
this.dropdownLeft = buttonRect.right + window.scrollX - dropdownRect.width
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.cdr.detectChanges()
|
|
289
|
+
}, 0)
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ======================================================
|
|
294
|
+
// ControlValueAccessor Implementation
|
|
295
|
+
// ======================================================
|
|
296
|
+
|
|
297
|
+
writeValue(value: any[]): void {
|
|
298
|
+
this.selectedValues = Array.isArray(value) ? value : []
|
|
299
|
+
this.updateDisplayLabel()
|
|
300
|
+
this.cdr.markForCheck()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
registerOnChange(fn: any): void {
|
|
304
|
+
this.onChange = fn
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
registerOnTouched(fn: any): void {
|
|
308
|
+
this.onTouched = fn
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
setDisabledState(isDisabled: boolean): void {
|
|
312
|
+
this.disabled = isDisabled
|
|
313
|
+
this.cdr.markForCheck()
|
|
314
|
+
}
|
|
315
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<div class="relative w-full h-full">
|
|
2
|
+
<!-- Botón para multi-table -->
|
|
3
|
+
<div #selectButton class="min-w-[40px]">
|
|
4
|
+
<JButton
|
|
5
|
+
(clicked)="toggleColumnSelector()"
|
|
6
|
+
classes="secondary"
|
|
7
|
+
[icon]="btnIcon"
|
|
8
|
+
[disabled]="disabled">
|
|
9
|
+
{{btnText}}
|
|
10
|
+
</JButton>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<!-- Dropdown positioned outside the flow -->
|
|
15
|
+
@if (isColumnSelectorOpen) {
|
|
16
|
+
<div @modalTransition
|
|
17
|
+
class="absolute z-[100] min-w-[250px] max-w-[250px] mt-1 bg-background dark:bg-dark-background rounded-lg shadow-lg border border-border border-dark-border"
|
|
18
|
+
[style.width.px]="dropdownWidth"
|
|
19
|
+
[style.top.px]="dropdownTop"
|
|
20
|
+
[style.left.px]="dropdownLeft">
|
|
21
|
+
<div class="pt-1 pl-3 pr-3 pb-3">
|
|
22
|
+
<div class="text-[10px] font-medium text-gray-500 dark:text-gray-500 mb-1">{{title}}</div>
|
|
23
|
+
|
|
24
|
+
<!-- Multi-checkbox para columnas -->
|
|
25
|
+
<div class="max-h-60 overflow-auto flex flex-col gap-1 scroll-element">
|
|
26
|
+
@for (column of columns; track column.key) {
|
|
27
|
+
<div onKeyDown
|
|
28
|
+
class="flex gap-5 items-center border border-accent dark:border-dark-accent/50 px-4 py-2 cursor-pointer group rounded-md transition-all hover:bg-accent dark:hover:bg-dark-accent/50"
|
|
29
|
+
[ngClass]="{'bg-accent dark:bg-dark-accent/50' : column.visible}"
|
|
30
|
+
(click)="toggleColumnVisibility(column)">
|
|
31
|
+
<!-- Checkbox oculto -->
|
|
32
|
+
<input type="checkbox"
|
|
33
|
+
[id]="'col-' + column.key"
|
|
34
|
+
[(ngModel)]="column.visible"
|
|
35
|
+
class="hidden">
|
|
36
|
+
|
|
37
|
+
<!-- Ícono Check Visible Solo al Seleccionar -->
|
|
38
|
+
@if (column.visible) {
|
|
39
|
+
<lucide-icon [name]="icons.check"
|
|
40
|
+
size="15"
|
|
41
|
+
class="transition text-black dark:text-white" />
|
|
42
|
+
} @else {
|
|
43
|
+
<lucide-icon [name]="icons.squareDashedMousePointer"
|
|
44
|
+
size="15"
|
|
45
|
+
class="transition text-black dark:text-white opacity-40" />
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
<!-- Etiqueta con Efectos de Hover e Iluminación -->
|
|
49
|
+
<label [for]="'col-' + column.key"
|
|
50
|
+
class="text-black dark:text-white text-sm font-medium w-full cursor-pointer">
|
|
51
|
+
{{ column.label }}
|
|
52
|
+
</label>
|
|
53
|
+
</div>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@if (columns.length === 0) {
|
|
57
|
+
<div class="px-3 py-2 text-sm text-gray-500">No hay columnas disponibles</div>
|
|
58
|
+
}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Acciones adicionales -->
|
|
62
|
+
@if (showActions) {
|
|
63
|
+
<div class="mt-3 pt-2 border-t border-border dark:border-dark-border flex gap-2">
|
|
64
|
+
<button type="button"
|
|
65
|
+
(click)="selectAllColumns()"
|
|
66
|
+
class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors">
|
|
67
|
+
Mostrar todas
|
|
68
|
+
</button>
|
|
69
|
+
<button type="button"
|
|
70
|
+
(click)="deselectAllColumns()"
|
|
71
|
+
class="px-3 py-1 text-xs bg-secondary text-secondary-foreground rounded hover:bg-secondary/90 transition-colors">
|
|
72
|
+
Ocultar todas
|
|
73
|
+
</button>
|
|
74
|
+
<button type="button"
|
|
75
|
+
(click)="resetToDefault()"
|
|
76
|
+
class="px-3 py-1 text-xs bg-outline text-foreground rounded hover:bg-accent transition-colors">
|
|
77
|
+
Restablecer
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
}
|