tailjng 0.1.6 → 0.1.7
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/README.md +12 -4
- package/cli/execute/init-app.js +5 -2
- package/cli/execute/sync-app.js +14 -2
- package/cli/settings/colors-config-utils.js +43 -8
- package/cli/settings/icons-config-utils.js +62 -0
- package/cli/settings/path-utils.js +32 -2
- package/cli/settings/project-utils.js +7 -1
- package/cli/templates/app.generator.js +2 -2
- package/fesm2022/tailjng.mjs +247 -80
- package/fesm2022/tailjng.mjs.map +1 -1
- package/lib/services/static/theme.service.d.ts +39 -1
- package/lib/utils/theme/theme-variables.util.d.ts +31 -0
- package/package.json +1 -1
- package/public-api.d.ts +2 -1
- package/registry/components.json +41 -18
- package/src/colors.safelist.css +2 -2
- package/src/lib/components/.config/README.md +11 -0
- package/src/lib/components/.config/colors/README.md +38 -0
- package/src/lib/components/{colors-config → .config/colors}/colors.config.ts +5 -5
- package/src/lib/components/{colors-config → .config/colors}/colors.safelist.css +2 -2
- package/src/lib/components/.config/icons/README.md +26 -0
- package/src/lib/components/.config/icons/icons.lucide.ts +134 -0
- package/src/lib/components/.config/input/README.md +24 -0
- package/src/lib/components/.config/input/input.classes.ts +119 -0
- package/src/lib/components/alert/alert-dialog/dialog-alert.component.css +244 -2
- package/src/lib/components/alert/alert-dialog/dialog-alert.component.html +25 -38
- package/src/lib/components/alert/alert-dialog/dialog-alert.component.ts +66 -56
- package/src/lib/components/alert/alert-dialog/dialog-alert.types.ts +19 -0
- package/src/lib/components/alert/alert-toast/toast-alert.component.css +630 -12
- package/src/lib/components/alert/alert-toast/toast-alert.component.html +103 -102
- package/src/lib/components/alert/alert-toast/toast-alert.component.ts +485 -128
- package/src/lib/components/alert/alert-toast/toast-alert.types.ts +25 -0
- package/src/lib/components/badge/badge.component.html +34 -21
- package/src/lib/components/badge/badge.component.ts +140 -31
- package/src/lib/components/button/button.component.html +16 -10
- package/src/lib/components/button/button.component.ts +162 -22
- package/src/lib/components/card/card-complete/complete-card.component.html +2 -2
- package/src/lib/components/card/card-complete/complete-card.component.ts +26 -16
- package/src/lib/components/card/card-crud-complete/complete-crud-card.component.html +2 -2
- package/src/lib/components/card/card-crud-complete/complete-crud-card.component.ts +26 -16
- package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.css +97 -0
- package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.html +54 -46
- package/src/lib/components/checkbox/checkbox-input/input-checkbox.component.ts +135 -64
- package/src/lib/components/checkbox/checkbox-input/input-checkbox.types.ts +3 -0
- package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.css +112 -0
- package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.html +28 -25
- package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.component.ts +67 -15
- package/src/lib/components/checkbox/checkbox-switch/switch-checkbox.types.ts +1 -0
- package/src/lib/components/coach-mark/coach-mark.component.html +4 -22
- package/src/lib/components/coach-mark/coach-mark.component.scss +1 -1
- package/src/lib/components/coach-mark/coach-mark.component.ts +51 -18
- package/src/lib/components/coach-mark/coach-mark.directive.ts +133 -78
- package/src/lib/components/coach-mark/coach-mark.types.ts +12 -0
- package/src/lib/components/dialog/dialog.component.css +103 -1
- package/src/lib/components/dialog/dialog.component.html +46 -66
- package/src/lib/components/dialog/dialog.component.ts +136 -110
- package/src/lib/components/dialog/dialog.types.ts +19 -0
- package/src/lib/components/filter/filter-complete/complete-filter.component.html +16 -19
- package/src/lib/components/filter/filter-complete/complete-filter.component.scss +35 -0
- package/src/lib/components/filter/filter-complete/complete-filter.component.ts +58 -34
- package/src/lib/components/filter/filter-complete/complete-filter.types.ts +7 -0
- package/src/lib/components/filter/filter-complete/complete-filter.util.ts +16 -0
- package/src/lib/components/form/form-container/container-form.component.css +4 -0
- package/src/lib/components/form/form-container/container-form.component.html +2 -2
- package/src/lib/components/form/form-container/container-form.component.ts +72 -16
- package/src/lib/components/form/form-container/container-form.types.ts +42 -0
- package/src/lib/components/form/form-container/form-col-span.directive.ts +25 -0
- package/src/lib/components/form/form-sidebar/sidebar-form.component.css +276 -0
- package/src/lib/components/form/form-sidebar/sidebar-form.component.html +117 -125
- package/src/lib/components/form/form-sidebar/sidebar-form.component.ts +109 -34
- package/src/lib/components/form/form-sidebar/sidebar-form.types.ts +3 -0
- package/src/lib/components/{toggle-radio/toggle-radio.component.css → form/form-validation/validation-form.component.css} +0 -1
- package/src/lib/components/form/form-validation/validation-form.component.html +10 -6
- package/src/lib/components/form/form-validation/validation-form.component.ts +99 -12
- package/src/lib/components/form/form-validation/validation-form.types.ts +33 -0
- package/src/lib/components/icon/icon.component.html +8 -5
- package/src/lib/components/icon/icon.component.ts +111 -9
- package/src/lib/components/input/input/input.component.html +19 -16
- package/src/lib/components/input/input/input.component.ts +130 -53
- package/src/lib/components/input/input/input.types.ts +8 -0
- package/src/lib/components/input/input-file/file-input.component.html +65 -56
- package/src/lib/components/input/input-file/file-input.component.ts +276 -173
- package/src/lib/components/input/input-file/file-input.types.ts +2 -0
- package/src/lib/components/input/input-range/range-input.component.css +67 -0
- package/src/lib/components/input/input-range/range-input.component.html +50 -58
- package/src/lib/components/input/input-range/range-input.component.ts +148 -60
- package/src/lib/components/input/input-range/range-input.types.ts +7 -0
- package/src/lib/components/input/input-textarea/textarea-input.component.html +16 -7
- package/src/lib/components/input/input-textarea/textarea-input.component.ts +140 -50
- package/src/lib/components/input/input-textarea/textarea-input.types.ts +2 -0
- package/src/lib/components/label/label.component.html +17 -16
- package/src/lib/components/label/label.component.ts +70 -16
- package/src/lib/components/label/label.types.ts +2 -0
- package/src/lib/components/menu/menu-options-table/menu-options-defaults.ts +34 -0
- package/src/lib/components/menu/menu-options-table/options-table-menu.component.html +34 -20
- package/src/lib/components/menu/menu-options-table/options-table-menu.component.ts +211 -58
- package/src/lib/components/menu/menu-options-table/options-table-menu.types.ts +38 -0
- package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.html +49 -52
- package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.ts +112 -24
- package/src/lib/components/menu/options-coach-menu/options-coach-menu.types.ts +9 -0
- package/src/lib/components/mode-toggle/mode-toggle.component.html +11 -16
- package/src/lib/components/mode-toggle/mode-toggle.component.ts +69 -33
- package/src/lib/components/paginator/paginator-complete/complete-paginator.component.html +4 -4
- package/src/lib/components/paginator/paginator-complete/complete-paginator.component.ts +31 -7
- package/src/lib/components/paginator/paginator-complete/complete-paginator.types.ts +12 -0
- package/src/lib/components/paginator/paginator-complete/complete-paginator.util.ts +36 -0
- package/src/lib/components/progress-bar/progress-bar.component.css +11 -0
- package/src/lib/components/progress-bar/progress-bar.component.html +41 -40
- package/src/lib/components/progress-bar/progress-bar.component.ts +95 -11
- package/src/lib/components/progress-bar/progress-bar.types.ts +2 -0
- package/src/lib/components/select/select-dropdown/dropdown-select.component.css +6 -0
- package/src/lib/components/select/select-dropdown/dropdown-select.component.html +54 -44
- package/src/lib/components/select/select-dropdown/dropdown-select.component.ts +450 -509
- package/src/lib/components/select/select-dropdown/dropdown-select.types.ts +43 -0
- package/src/lib/components/select/select-dropdown/dropdown-select.util.ts +179 -0
- package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.css +6 -0
- package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.html +131 -42
- package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.ts +491 -475
- package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.types.ts +22 -0
- package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.util.ts +20 -0
- package/src/lib/components/select/select-multi-table/multi-table-select.component.css +10 -0
- package/src/lib/components/select/select-multi-table/multi-table-select.component.html +76 -60
- package/src/lib/components/select/select-multi-table/multi-table-select.component.ts +250 -313
- package/src/lib/components/select/select-multi-table/multi-table-select.types.ts +10 -0
- package/src/lib/components/select/select-multi-table/multi-table-select.util.ts +5 -0
- package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.css +212 -0
- package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.html +62 -53
- package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.ts +84 -27
- package/src/lib/components/sidebar/sidebar-static/static-sidebar.types.ts +2 -0
- package/src/lib/components/table/table-complete/complete-table.component.html +15 -17
- package/src/lib/components/table/table-complete/complete-table.component.ts +190 -338
- package/src/lib/components/table/table-complete/complete-table.types.ts +28 -0
- package/src/lib/components/table/table-complete/complete-table.util.ts +236 -0
- package/src/lib/components/table/table-complete/index.ts +2 -0
- package/src/lib/components/table/table-crud-complete/complete-crud-table.animations.ts +34 -0
- package/src/lib/components/table/table-crud-complete/complete-crud-table.component.html +73 -128
- package/src/lib/components/table/table-crud-complete/complete-crud-table.component.ts +542 -829
- package/src/lib/components/table/table-crud-complete/complete-crud-table.types.ts +57 -0
- package/src/lib/components/table/table-crud-complete/complete-crud-table.util.ts +723 -0
- package/src/lib/components/table/table-crud-complete/index.ts +3 -0
- package/src/lib/components/theme-generator/theme-generator.component.css +21 -0
- package/src/lib/components/theme-generator/theme-generator.component.html +146 -116
- package/src/lib/components/theme-generator/theme-generator.component.ts +44 -24
- package/src/lib/components/toggle-radio/shared/toggle-options.types.ts +8 -0
- package/src/lib/components/toggle-radio/shared/toggle-options.util.ts +44 -0
- package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.css +135 -0
- package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.html +52 -0
- package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.ts +198 -0
- package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.types.ts +1 -0
- package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.css +108 -0
- package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.html +37 -0
- package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.ts +193 -0
- package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.types.ts +1 -0
- package/src/lib/components/tooltip/tooltip.directive.ts +12 -9
- package/src/lib/components/tooltip/tooltip.service.ts +331 -133
- package/src/lib/components/tooltip/tooltip.types.ts +9 -0
- package/src/lib/components/viewer/viewer-image/image-viewer.component.css +90 -4
- package/src/lib/components/viewer/viewer-image/image-viewer.component.html +52 -103
- package/src/lib/components/viewer/viewer-image/image-viewer.component.ts +182 -177
- package/src/lib/components/viewer/viewer-image/image-viewer.types.ts +3 -0
- package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.css +177 -0
- package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.html +74 -24
- package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.ts +168 -15
- package/src/lib/components/viewer/viewer-pdf/pdf-viewer.types.ts +1 -0
- package/src/styles.css +2 -2
- package/lib/services/static/icons.service.d.ts +0 -65
- package/src/lib/components/colors-config/README.md +0 -38
- package/src/lib/components/form/form-sidebar/sidebar-form.component.scss +0 -0
- package/src/lib/components/form/form-validation/validation-form.component.scss +0 -0
- package/src/lib/components/menu/menu-options-table/options-table-menu.component.scss +0 -0
- package/src/lib/components/menu/options-coach-menu/options-coach-menu.component.scss +0 -12
- package/src/lib/components/sidebar/sidebar-static/static-sidebar.component.scss +0 -0
- package/src/lib/components/toggle-radio/toggle-radio.component.html +0 -51
- package/src/lib/components/toggle-radio/toggle-radio.component.ts +0 -222
- package/src/lib/components/viewer/viewer-pdf/pdf-viewer.component.scss +0 -0
- package/tailjng-0.1.6.tgz +0 -0
|
@@ -1,130 +1,249 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { NgClass, NgStyle } from '@angular/common';
|
|
2
|
+
import {
|
|
3
|
+
Component,
|
|
4
|
+
ElementRef,
|
|
5
|
+
Input,
|
|
6
|
+
OnDestroy,
|
|
7
|
+
OnInit,
|
|
8
|
+
ViewChild,
|
|
9
|
+
forwardRef,
|
|
10
|
+
} from '@angular/core';
|
|
11
|
+
import {
|
|
12
|
+
ControlValueAccessor,
|
|
13
|
+
FormsModule,
|
|
14
|
+
NG_VALUE_ACCESSOR,
|
|
15
|
+
ReactiveFormsModule,
|
|
16
|
+
} from '@angular/forms';
|
|
17
|
+
import { JAlertToastService } from 'tailjng';
|
|
18
|
+
import { Icons } from '../../.config/icons/icons.lucide';
|
|
19
|
+
import {
|
|
20
|
+
INPUT_DISABLED_OVERLAY_CLASSES,
|
|
21
|
+
INPUT_FILE_LABEL_LAYOUT_CLASSES,
|
|
22
|
+
INPUT_FOCUS_WITHIN_CLASSES,
|
|
23
|
+
INPUT_PADDING_X_SINGLE,
|
|
24
|
+
INPUT_SINGLE_LINE_HEIGHT_CLASSES,
|
|
25
|
+
INPUT_SURFACE_CLASSES,
|
|
26
|
+
inputActionButtonClasses,
|
|
27
|
+
mergeInputClasses,
|
|
28
|
+
} from '../../.config/input/input.classes';
|
|
29
|
+
import { JIconComponent } from '../../icon/icon.component';
|
|
30
|
+
import { FileInputValue } from './file-input.types';
|
|
31
|
+
|
|
32
|
+
export type { FileInputValue } from './file-input.types';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* File input with image preview, clipboard paste, and optional clear button.
|
|
36
|
+
*
|
|
37
|
+
* Install: `npx tailjng add input-file`
|
|
38
|
+
*/
|
|
7
39
|
@Component({
|
|
8
40
|
selector: 'JFileInput',
|
|
9
|
-
imports: [FormsModule, ReactiveFormsModule, NgClass,
|
|
41
|
+
imports: [FormsModule, ReactiveFormsModule, NgClass, NgStyle, JIconComponent],
|
|
10
42
|
templateUrl: './file-input.component.html',
|
|
11
43
|
styleUrl: './file-input.component.css',
|
|
12
44
|
providers: [
|
|
13
45
|
{
|
|
14
46
|
provide: NG_VALUE_ACCESSOR,
|
|
15
47
|
useExisting: forwardRef(() => JFileInputComponent),
|
|
16
|
-
multi: true
|
|
17
|
-
}
|
|
18
|
-
]
|
|
48
|
+
multi: true,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
19
51
|
})
|
|
20
|
-
export class JFileInputComponent implements ControlValueAccessor,
|
|
52
|
+
export class JFileInputComponent implements ControlValueAccessor, OnInit, OnDestroy {
|
|
53
|
+
// Project Icons registry — template usage: Icons.Upload, Icons.X, Icons.Clipboard
|
|
54
|
+
readonly Icons = Icons;
|
|
21
55
|
|
|
22
56
|
@Input() id?: string;
|
|
23
57
|
@Input() name?: string;
|
|
24
|
-
@Input() accept: string = '';
|
|
25
|
-
@Input() multiple: boolean = false;
|
|
26
|
-
@Input() isDisabled: boolean = false;
|
|
27
|
-
@Input() required: boolean = false;
|
|
28
|
-
@Input() clearButton: boolean = false;
|
|
29
|
-
@Input() showImage: boolean = false;
|
|
30
|
-
@Input() widthImgFile: number = 0;
|
|
31
|
-
@Input() heightImgFile: number = 0;
|
|
32
58
|
|
|
33
|
-
|
|
59
|
+
// Native accept attribute (e.g. "image/*", ".pdf")
|
|
60
|
+
@Input() accept = '';
|
|
34
61
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
private _fileInputCleared = false;
|
|
62
|
+
// Allow selecting multiple files
|
|
63
|
+
@Input() multiple = false;
|
|
38
64
|
|
|
39
|
-
|
|
65
|
+
@Input() isDisabled = false;
|
|
66
|
+
@Input() required = false;
|
|
40
67
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
return this._innerValue;
|
|
44
|
-
}
|
|
68
|
+
// Show clear (X) button when a file is selected
|
|
69
|
+
@Input() clearButton = false;
|
|
45
70
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
71
|
+
// Show image preview below the field when value is an image
|
|
72
|
+
@Input() showImage = false;
|
|
49
73
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
74
|
+
// Preview dimensions in px; 0 = width 100% / height auto
|
|
75
|
+
@Input() widthImgFile = 0;
|
|
76
|
+
@Input() heightImgFile = 0;
|
|
53
77
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
78
|
+
// Placeholder when no file is selected
|
|
79
|
+
@Input() placeholder = 'Select file...';
|
|
57
80
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
private _onTouched: () => void = () => { };
|
|
81
|
+
// Extra Tailwind classes on the visible label
|
|
82
|
+
@Input() classes = '';
|
|
61
83
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
84
|
+
// Extra conditional classes on the visible label
|
|
85
|
+
@Input() ngClasses: Record<string, boolean> = {};
|
|
86
|
+
|
|
87
|
+
@ViewChild('fileInput') fileInputRef!: ElementRef<HTMLInputElement>;
|
|
88
|
+
|
|
89
|
+
private innerValue: FileInputValue = null;
|
|
90
|
+
private previewUrl: string | null = null;
|
|
91
|
+
private fileInputCleared = false;
|
|
92
|
+
private readonly uniqueId: string;
|
|
93
|
+
|
|
94
|
+
private onChange: (value: FileInputValue) => void = () => {};
|
|
95
|
+
private onTouched: () => void = () => {};
|
|
96
|
+
|
|
97
|
+
constructor(private readonly alertToastService: JAlertToastService) {
|
|
98
|
+
this.uniqueId = this.generateUniqueId();
|
|
68
99
|
}
|
|
69
100
|
|
|
70
|
-
|
|
71
|
-
if (this.
|
|
72
|
-
|
|
101
|
+
ngOnInit(): void {
|
|
102
|
+
if (!this.id) {
|
|
103
|
+
this.id = this.uniqueId;
|
|
73
104
|
}
|
|
74
105
|
}
|
|
75
106
|
|
|
76
|
-
|
|
77
|
-
|
|
107
|
+
ngOnDestroy(): void {
|
|
108
|
+
this.revokePreviewUrl();
|
|
78
109
|
}
|
|
79
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Whether a file (or files) is currently selected.
|
|
113
|
+
* @returns `true` when `innerValue` is set.
|
|
114
|
+
*/
|
|
115
|
+
get hasValue(): boolean {
|
|
116
|
+
if (this.innerValue == null) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(this.innerValue)) {
|
|
120
|
+
return this.innerValue.length > 0;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
80
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Whether the clear button should be visible.
|
|
127
|
+
* @returns `true` when `clearButton` is enabled and a file is selected.
|
|
128
|
+
*/
|
|
129
|
+
get showClearButton(): boolean {
|
|
130
|
+
return this.clearButton && this.hasValue;
|
|
131
|
+
}
|
|
81
132
|
|
|
82
133
|
/**
|
|
83
|
-
*
|
|
134
|
+
* Blob URL for the current image preview.
|
|
135
|
+
* @returns Preview URL or `null`.
|
|
84
136
|
*/
|
|
85
|
-
|
|
86
|
-
this.
|
|
137
|
+
get previewImageUrl(): string | null {
|
|
138
|
+
return this.previewUrl;
|
|
139
|
+
}
|
|
87
140
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Whether the image preview block should render.
|
|
143
|
+
* @returns `true` when preview URL exists and `showImage` is enabled.
|
|
144
|
+
*/
|
|
145
|
+
get showPreview(): boolean {
|
|
146
|
+
return !!this.previewUrl && this.showImage;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Label text for the selected file(s).
|
|
151
|
+
* @returns File name, count summary, or `null` when empty.
|
|
152
|
+
*/
|
|
153
|
+
get displayFileName(): string | null {
|
|
154
|
+
if (!this.hasValue || this.innerValue == null) {
|
|
155
|
+
return null;
|
|
91
156
|
}
|
|
92
157
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (this.fileInputRef?.nativeElement) {
|
|
97
|
-
this.fileInputRef.nativeElement.value = "";
|
|
98
|
-
this._fileInputCleared = true;
|
|
158
|
+
if (Array.isArray(this.innerValue)) {
|
|
159
|
+
if (this.innerValue.length === 1) {
|
|
160
|
+
return this.innerValue[0].name;
|
|
99
161
|
}
|
|
100
|
-
|
|
101
|
-
this.createPreview(val);
|
|
162
|
+
return `${this.innerValue.length} files selected`;
|
|
102
163
|
}
|
|
164
|
+
|
|
165
|
+
return this.innerValue.name;
|
|
103
166
|
}
|
|
104
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Inline styles for the preview image.
|
|
170
|
+
* @returns Width/height based on `widthImgFile` and `heightImgFile`.
|
|
171
|
+
*/
|
|
172
|
+
get previewStyles(): Record<string, string> {
|
|
173
|
+
return {
|
|
174
|
+
width: this.widthImgFile ? `${this.widthImgFile}px` : '100%',
|
|
175
|
+
height: this.heightImgFile ? `${this.heightImgFile}px` : 'auto',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
105
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Class map for the visible file picker label.
|
|
181
|
+
* @returns Base styles merged with `ngClasses` and disabled state.
|
|
182
|
+
*/
|
|
183
|
+
get computedClasses(): Record<string, boolean> {
|
|
184
|
+
return mergeInputClasses(
|
|
185
|
+
INPUT_SURFACE_CLASSES,
|
|
186
|
+
INPUT_SINGLE_LINE_HEIGHT_CLASSES,
|
|
187
|
+
INPUT_FOCUS_WITHIN_CLASSES,
|
|
188
|
+
INPUT_PADDING_X_SINGLE,
|
|
189
|
+
INPUT_FILE_LABEL_LAYOUT_CLASSES,
|
|
190
|
+
this.ngClasses,
|
|
191
|
+
this.isDisabled
|
|
192
|
+
? mergeInputClasses(INPUT_DISABLED_OVERLAY_CLASSES, { 'pointer-events-none': true })
|
|
193
|
+
: {},
|
|
194
|
+
);
|
|
195
|
+
}
|
|
106
196
|
|
|
107
197
|
/**
|
|
108
|
-
*
|
|
198
|
+
* Class map for icon action buttons (clear, clipboard).
|
|
199
|
+
* @returns Theme-aware hover utilities.
|
|
109
200
|
*/
|
|
110
|
-
|
|
111
|
-
this.
|
|
201
|
+
get actionButtonClasses(): Record<string, boolean> {
|
|
202
|
+
return inputActionButtonClasses(this.isDisabled);
|
|
112
203
|
}
|
|
113
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Writes the value from Angular forms into the view.
|
|
207
|
+
* @param value File, array of files, or null.
|
|
208
|
+
*/
|
|
209
|
+
writeValue(value: FileInputValue): void {
|
|
210
|
+
this.innerValue = value;
|
|
211
|
+
this.revokePreviewUrl();
|
|
212
|
+
|
|
213
|
+
if (!value) {
|
|
214
|
+
this.previewUrl = null;
|
|
215
|
+
if (this.fileInputRef?.nativeElement) {
|
|
216
|
+
this.fileInputRef.nativeElement.value = '';
|
|
217
|
+
this.fileInputCleared = true;
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
114
221
|
|
|
222
|
+
const file = Array.isArray(value) ? value[0] : value;
|
|
223
|
+
if (file?.type?.startsWith('image/')) {
|
|
224
|
+
this.createPreview(file);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
115
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Registers the change callback for Angular forms.
|
|
230
|
+
* @param fn Callback invoked when the value changes.
|
|
231
|
+
*/
|
|
232
|
+
registerOnChange(fn: (value: FileInputValue) => void): void {
|
|
233
|
+
this.onChange = fn;
|
|
234
|
+
}
|
|
116
235
|
|
|
117
236
|
/**
|
|
118
|
-
* Registers
|
|
237
|
+
* Registers the touched callback for Angular forms.
|
|
238
|
+
* @param fn Callback invoked when the control is touched.
|
|
119
239
|
*/
|
|
120
240
|
registerOnTouched(fn: () => void): void {
|
|
121
|
-
this.
|
|
241
|
+
this.onTouched = fn;
|
|
122
242
|
}
|
|
123
243
|
|
|
124
|
-
|
|
125
|
-
|
|
126
244
|
/**
|
|
127
|
-
*
|
|
245
|
+
* Syncs disabled state from Angular forms.
|
|
246
|
+
* @param isDisabled Whether the control is disabled.
|
|
128
247
|
*/
|
|
129
248
|
setDisabledState(isDisabled: boolean): void {
|
|
130
249
|
this.isDisabled = isDisabled;
|
|
@@ -133,122 +252,102 @@ export class JFileInputComponent implements ControlValueAccessor, OnChanges, OnD
|
|
|
133
252
|
}
|
|
134
253
|
}
|
|
135
254
|
|
|
136
|
-
|
|
137
|
-
|
|
138
255
|
/**
|
|
139
|
-
* Handles
|
|
140
|
-
|
|
141
|
-
ngOnChanges(changes: SimpleChanges): void {
|
|
142
|
-
// Si no hay ID proporcionado, usar el único generado
|
|
143
|
-
if (!this.id) {
|
|
144
|
-
this.id = this._uniqueId;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Handles the file selection event.
|
|
256
|
+
* Handles native file selection.
|
|
257
|
+
* @param event Change event from the hidden file input.
|
|
152
258
|
*/
|
|
153
259
|
onFileSelected(event: Event): void {
|
|
154
260
|
const input = event.target as HTMLInputElement;
|
|
155
261
|
const files = input.files;
|
|
156
262
|
|
|
157
|
-
if (files
|
|
158
|
-
|
|
159
|
-
this._innerValue = value;
|
|
160
|
-
this._onChange(value);
|
|
161
|
-
this._onTouched();
|
|
162
|
-
this._fileInputCleared = false;
|
|
163
|
-
|
|
164
|
-
if (!this.multiple && value instanceof File && value.type?.startsWith("image")) {
|
|
165
|
-
this.createPreview(value);
|
|
166
|
-
} else {
|
|
167
|
-
if (this._previewUrl && this._previewUrl.startsWith('blob:')) {
|
|
168
|
-
URL.revokeObjectURL(this._previewUrl);
|
|
169
|
-
}
|
|
170
|
-
this._previewUrl = null;
|
|
171
|
-
}
|
|
263
|
+
if (!files?.length) {
|
|
264
|
+
return;
|
|
172
265
|
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
266
|
|
|
267
|
+
const value: FileInputValue = this.multiple ? Array.from(files) : files[0];
|
|
268
|
+
this.innerValue = value;
|
|
269
|
+
this.onChange(value);
|
|
270
|
+
this.onTouched();
|
|
271
|
+
this.fileInputCleared = false;
|
|
272
|
+
|
|
273
|
+
const previewFile = Array.isArray(value) ? value[0] : value;
|
|
274
|
+
if (!this.multiple && previewFile?.type?.startsWith('image/')) {
|
|
275
|
+
this.createPreview(previewFile);
|
|
276
|
+
} else {
|
|
277
|
+
this.revokePreviewUrl();
|
|
278
|
+
this.previewUrl = null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
176
281
|
|
|
177
282
|
/**
|
|
178
|
-
*
|
|
283
|
+
* Marks the control as touched on blur.
|
|
179
284
|
*/
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
URL.revokeObjectURL(this._previewUrl);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
this._previewUrl = URL.createObjectURL(file);
|
|
285
|
+
onBlur(): void {
|
|
286
|
+
this.onTouched();
|
|
186
287
|
}
|
|
187
288
|
|
|
188
|
-
|
|
189
|
-
|
|
190
289
|
/**
|
|
191
|
-
* Clears the file
|
|
290
|
+
* Clears the selected file(s) and resets the native input.
|
|
291
|
+
* @param event Click event to avoid triggering the file dialog.
|
|
192
292
|
*/
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this._innerValue = null;
|
|
199
|
-
this._previewUrl = null;
|
|
200
|
-
this._onChange(this.multiple ? [] : null);
|
|
201
|
-
this._onTouched();
|
|
202
|
-
|
|
203
|
-
// Reset the file input field
|
|
204
|
-
if (this.fileInputRef?.nativeElement) {
|
|
205
|
-
this.fileInputRef.nativeElement.value = "";
|
|
206
|
-
this._fileInputCleared = true;
|
|
207
|
-
}
|
|
293
|
+
onClearClick(event: MouseEvent): void {
|
|
294
|
+
event.preventDefault();
|
|
295
|
+
event.stopPropagation();
|
|
296
|
+
this.clearFile();
|
|
208
297
|
}
|
|
209
298
|
|
|
210
|
-
|
|
211
|
-
|
|
212
299
|
/**
|
|
213
|
-
*
|
|
300
|
+
* Pastes an image from the clipboard into the field.
|
|
301
|
+
* @param event Click event — stopped so the label does not open the file dialog.
|
|
214
302
|
*/
|
|
215
|
-
|
|
216
|
-
|
|
303
|
+
onClipboardClick(event: MouseEvent): void {
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
event.stopPropagation();
|
|
306
|
+
void this.getImageFromClipboard();
|
|
217
307
|
}
|
|
218
308
|
|
|
219
|
-
|
|
220
|
-
|
|
221
309
|
/**
|
|
222
|
-
*
|
|
310
|
+
* Clears the selected file(s) and notifies Angular forms.
|
|
223
311
|
*/
|
|
312
|
+
clearFile(): void {
|
|
313
|
+
this.revokePreviewUrl();
|
|
314
|
+
this.innerValue = null;
|
|
315
|
+
this.previewUrl = null;
|
|
316
|
+
this.onChange(this.multiple ? [] : null);
|
|
317
|
+
this.onTouched();
|
|
318
|
+
|
|
319
|
+
if (this.fileInputRef?.nativeElement) {
|
|
320
|
+
this.fileInputRef.nativeElement.value = '';
|
|
321
|
+
this.fileInputCleared = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Alias for {@link clearFile}. */
|
|
224
326
|
resetFileInput(): void {
|
|
225
327
|
this.clearFile();
|
|
226
328
|
}
|
|
227
329
|
|
|
228
|
-
|
|
229
|
-
|
|
230
330
|
/**
|
|
231
|
-
*
|
|
232
|
-
* @returns
|
|
331
|
+
* Reads an image from the clipboard and sets it as the field value.
|
|
233
332
|
*/
|
|
234
333
|
async getImageFromClipboard(): Promise<void> {
|
|
235
|
-
|
|
334
|
+
if (this.isDisabled) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
236
337
|
|
|
237
|
-
|
|
338
|
+
try {
|
|
238
339
|
if (!('clipboard' in navigator) || typeof navigator.clipboard.read !== 'function') {
|
|
239
340
|
this.alertToastService.AlertToast({
|
|
240
341
|
type: 'warning',
|
|
241
342
|
title: 'No soportado',
|
|
242
343
|
description: 'Tu navegador no soporta leer imágenes del portapapeles.',
|
|
243
|
-
})
|
|
344
|
+
});
|
|
244
345
|
return;
|
|
245
346
|
}
|
|
246
347
|
|
|
247
348
|
const clipboardItems = await navigator.clipboard.read();
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const imageItem = clipboardItems.find(item =>
|
|
251
|
-
item.types.some(t => t.startsWith('image/'))
|
|
349
|
+
const imageItem = clipboardItems.find((item) =>
|
|
350
|
+
item.types.some((type) => type.startsWith('image/')),
|
|
252
351
|
);
|
|
253
352
|
|
|
254
353
|
if (!imageItem) {
|
|
@@ -256,42 +355,30 @@ export class JFileInputComponent implements ControlValueAccessor, OnChanges, OnD
|
|
|
256
355
|
type: 'info',
|
|
257
356
|
title: 'Imagen no encontrada',
|
|
258
357
|
description: 'No se encontró una imagen en el portapapeles.',
|
|
259
|
-
})
|
|
358
|
+
});
|
|
260
359
|
return;
|
|
261
360
|
}
|
|
262
361
|
|
|
263
|
-
|
|
264
|
-
const imageType = imageItem.types.find(t => t.startsWith('image/')) || 'image/png';
|
|
362
|
+
const imageType = imageItem.types.find((type) => type.startsWith('image/')) ?? 'image/png';
|
|
265
363
|
const blob = await imageItem.getType(imageType);
|
|
266
|
-
|
|
267
|
-
// Create a File from the Blob so the form has a real file
|
|
268
364
|
const extension = imageType.split('/')[1] || 'png';
|
|
269
365
|
const fileFromClipboard = new File(
|
|
270
366
|
[blob],
|
|
271
367
|
`clipboard-${Date.now()}.${extension}`,
|
|
272
|
-
{ type: imageType }
|
|
368
|
+
{ type: imageType },
|
|
273
369
|
);
|
|
274
370
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.
|
|
278
|
-
this.
|
|
279
|
-
|
|
280
|
-
// Notify the reactive form
|
|
281
|
-
this._onChange(value);
|
|
282
|
-
this._onTouched();
|
|
283
|
-
|
|
284
|
-
// Update the preview reusing existing logic
|
|
285
|
-
if (!this.multiple && fileFromClipboard.type?.startsWith('image')) {
|
|
286
|
-
if (this._previewUrl && this._previewUrl.startsWith('blob:')) {
|
|
287
|
-
URL.revokeObjectURL(this._previewUrl);
|
|
288
|
-
}
|
|
289
|
-
this._previewUrl = URL.createObjectURL(fileFromClipboard);
|
|
290
|
-
// this.showImage = true;
|
|
291
|
-
}
|
|
371
|
+
const value: FileInputValue = this.multiple ? [fileFromClipboard] : fileFromClipboard;
|
|
372
|
+
this.innerValue = value;
|
|
373
|
+
this.fileInputCleared = false;
|
|
374
|
+
this.onChange(value);
|
|
375
|
+
this.onTouched();
|
|
292
376
|
|
|
377
|
+
if (!this.multiple && fileFromClipboard.type.startsWith('image/')) {
|
|
378
|
+
this.createPreview(fileFromClipboard);
|
|
379
|
+
}
|
|
293
380
|
} catch (err) {
|
|
294
|
-
console.error('Error
|
|
381
|
+
console.error('Error reading image from clipboard:', err);
|
|
295
382
|
this.alertToastService.AlertToast({
|
|
296
383
|
type: 'error',
|
|
297
384
|
title: 'Error',
|
|
@@ -300,5 +387,21 @@ export class JFileInputComponent implements ControlValueAccessor, OnChanges, OnD
|
|
|
300
387
|
}
|
|
301
388
|
}
|
|
302
389
|
|
|
390
|
+
/** Creates an object URL preview for an image file. */
|
|
391
|
+
private createPreview(file: File): void {
|
|
392
|
+
this.revokePreviewUrl();
|
|
393
|
+
this.previewUrl = URL.createObjectURL(file);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Revokes a blob preview URL when present. */
|
|
397
|
+
private revokePreviewUrl(): void {
|
|
398
|
+
if (this.previewUrl?.startsWith('blob:')) {
|
|
399
|
+
URL.revokeObjectURL(this.previewUrl);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
303
402
|
|
|
403
|
+
/** Generates a stable fallback id when `[id]` is omitted. */
|
|
404
|
+
private generateUniqueId(): string {
|
|
405
|
+
return `file-input-${Math.random().toString(36).slice(2, 11)}-${Date.now()}`;
|
|
406
|
+
}
|
|
304
407
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
.j-range-input {
|
|
2
|
+
width: 100%;
|
|
3
|
+
height: 1.25rem;
|
|
4
|
+
appearance: none;
|
|
5
|
+
cursor: pointer;
|
|
6
|
+
border-radius: 0.5rem;
|
|
7
|
+
border: 1px solid var(--color-border);
|
|
8
|
+
background-color: var(--color-background);
|
|
9
|
+
transition: box-shadow 200ms;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.dark .j-range-input {
|
|
13
|
+
border-color: var(--color-dark-border);
|
|
14
|
+
background-color: var(--color-dark-background);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.j-range-input:focus {
|
|
18
|
+
outline: none;
|
|
19
|
+
box-shadow: 0 0 0 2px var(--color-primary);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.dark .j-range-input:focus {
|
|
23
|
+
box-shadow: 0 0 0 2px var(--color-dark-primary);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.j-range-input:disabled {
|
|
27
|
+
cursor: not-allowed;
|
|
28
|
+
opacity: 0.5;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.j-range-input::-webkit-slider-thumb {
|
|
32
|
+
appearance: none;
|
|
33
|
+
height: 2rem;
|
|
34
|
+
width: 2rem;
|
|
35
|
+
border-radius: 9999px;
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
background-color: var(--color-dark-primary);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.dark .j-range-input::-webkit-slider-thumb {
|
|
41
|
+
background-color: var(--color-primary);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.j-range-input::-webkit-slider-thumb:hover {
|
|
45
|
+
background-color: var(--color-primary);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.dark .j-range-input::-webkit-slider-thumb:hover {
|
|
49
|
+
background-color: var(--color-dark-primary);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.j-range-input::-moz-range-thumb {
|
|
53
|
+
height: 1rem;
|
|
54
|
+
width: 1rem;
|
|
55
|
+
border: none;
|
|
56
|
+
border-radius: 9999px;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
background-color: var(--color-primary);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.j-range-input::-moz-range-thumb:hover {
|
|
62
|
+
background-color: var(--color-primary);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.dark .j-range-input::-moz-range-thumb:hover {
|
|
66
|
+
background-color: var(--color-dark-primary);
|
|
67
|
+
}
|