shirkasoft-ui-components 1.2.0 → 1.3.1
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/fesm2022/shirkasoft-ui-components-breadcrumb.mjs +199 -0
- package/fesm2022/shirkasoft-ui-components-breadcrumb.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-button.mjs +148 -0
- package/fesm2022/shirkasoft-ui-components-button.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-chart.mjs +153 -0
- package/fesm2022/shirkasoft-ui-components-chart.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-confirm-dialog.mjs +143 -0
- package/fesm2022/shirkasoft-ui-components-confirm-dialog.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-date-picker.mjs +638 -0
- package/fesm2022/shirkasoft-ui-components-date-picker.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-fieldset.mjs +94 -0
- package/fesm2022/shirkasoft-ui-components-fieldset.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-file-upload.mjs +294 -0
- package/fesm2022/shirkasoft-ui-components-file-upload.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-knob.mjs +269 -0
- package/fesm2022/shirkasoft-ui-components-knob.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-loading.mjs +255 -0
- package/fesm2022/shirkasoft-ui-components-loading.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-modal.mjs +290 -0
- package/fesm2022/shirkasoft-ui-components-modal.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-notification.mjs +143 -0
- package/fesm2022/shirkasoft-ui-components-notification.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-price-input.mjs +243 -0
- package/fesm2022/shirkasoft-ui-components-price-input.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-rail-sidebar.mjs +205 -0
- package/fesm2022/shirkasoft-ui-components-rail-sidebar.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-select.mjs +737 -0
- package/fesm2022/shirkasoft-ui-components-select.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-sidebar.mjs +185 -0
- package/fesm2022/shirkasoft-ui-components-sidebar.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-skeleton.mjs +74 -0
- package/fesm2022/shirkasoft-ui-components-skeleton.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-table.mjs +689 -0
- package/fesm2022/shirkasoft-ui-components-table.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-tabs.mjs +249 -0
- package/fesm2022/shirkasoft-ui-components-tabs.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-tag.mjs +70 -0
- package/fesm2022/shirkasoft-ui-components-tag.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-text-field.mjs +370 -0
- package/fesm2022/shirkasoft-ui-components-text-field.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-timeline.mjs +222 -0
- package/fesm2022/shirkasoft-ui-components-timeline.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-toggle.mjs +186 -0
- package/fesm2022/shirkasoft-ui-components-toggle.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-tooltip.mjs +169 -0
- package/fesm2022/shirkasoft-ui-components-tooltip.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-utils.mjs +69 -0
- package/fesm2022/shirkasoft-ui-components-utils.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components-wizard.mjs +167 -0
- package/fesm2022/shirkasoft-ui-components-wizard.mjs.map +1 -0
- package/fesm2022/shirkasoft-ui-components.mjs +0 -5729
- package/fesm2022/shirkasoft-ui-components.mjs.map +1 -1
- package/package.json +101 -1
- package/types/shirkasoft-ui-components-breadcrumb.d.ts +68 -0
- package/types/shirkasoft-ui-components-button.d.ts +59 -0
- package/types/shirkasoft-ui-components-chart.d.ts +65 -0
- package/types/shirkasoft-ui-components-confirm-dialog.d.ts +88 -0
- package/types/shirkasoft-ui-components-date-picker.d.ts +200 -0
- package/types/shirkasoft-ui-components-fieldset.d.ts +72 -0
- package/types/shirkasoft-ui-components-file-upload.d.ts +121 -0
- package/types/shirkasoft-ui-components-knob.d.ts +69 -0
- package/types/shirkasoft-ui-components-loading.d.ts +94 -0
- package/types/shirkasoft-ui-components-modal.d.ts +139 -0
- package/types/shirkasoft-ui-components-notification.d.ts +82 -0
- package/types/shirkasoft-ui-components-price-input.d.ts +90 -0
- package/types/shirkasoft-ui-components-rail-sidebar.d.ts +121 -0
- package/types/shirkasoft-ui-components-select.d.ts +182 -0
- package/types/shirkasoft-ui-components-sidebar.d.ts +106 -0
- package/types/shirkasoft-ui-components-skeleton.d.ts +36 -0
- package/types/shirkasoft-ui-components-table.d.ts +293 -0
- package/types/shirkasoft-ui-components-tabs.d.ts +102 -0
- package/types/shirkasoft-ui-components-tag.d.ts +31 -0
- package/types/shirkasoft-ui-components-text-field.d.ts +120 -0
- package/types/shirkasoft-ui-components-timeline.d.ts +178 -0
- package/types/shirkasoft-ui-components-toggle.d.ts +48 -0
- package/types/shirkasoft-ui-components-tooltip.d.ts +47 -0
- package/types/shirkasoft-ui-components-utils.d.ts +9 -0
- package/types/shirkasoft-ui-components-wizard.d.ts +101 -0
- package/types/shirkasoft-ui-components.d.ts +1 -2333
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, input, output, signal, computed, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { CommonModule } from '@angular/common';
|
|
4
|
+
import { TranslocoService } from '@jsverse/transloco';
|
|
5
|
+
import { LucideUpload, LucideDynamicIcon } from '@lucide/angular';
|
|
6
|
+
import { getIcon } from 'shirkasoft-ui-components/utils';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shirkasoft UI Components.
|
|
10
|
+
* Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
|
|
11
|
+
* Belongs to Shirkasoft.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Drag & drop file uploader with file type/size validation, image previews,
|
|
15
|
+
* multiple files support and localized error messages.
|
|
16
|
+
*/
|
|
17
|
+
class FileUploadComponent {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.getIcon = (name) => getIcon(name, LucideUpload);
|
|
20
|
+
this.transloco = inject(TranslocoService);
|
|
21
|
+
/** Main label of the uploader. */
|
|
22
|
+
this.label = input('Cargar archivo', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
23
|
+
/** Accepted MIME types/extensions (e.g. 'image/*', '.pdf'). */
|
|
24
|
+
this.accept = input('image/*', ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
|
|
25
|
+
/** Maximum allowed size per file (bytes). */
|
|
26
|
+
this.maxFileSize = input(2 * 1024 * 1024, ...(ngDevMode ? [{ debugName: "maxFileSize" }] : /* istanbul ignore next */ []));
|
|
27
|
+
/** Maximum number of files (0 = unlimited). */
|
|
28
|
+
this.maxFiles = input(0, ...(ngDevMode ? [{ debugName: "maxFiles" }] : /* istanbul ignore next */ []));
|
|
29
|
+
/** Text of the file picker button. */
|
|
30
|
+
this.fileUploadText = input('Seleccionar archivo', ...(ngDevMode ? [{ debugName: "fileUploadText" }] : /* istanbul ignore next */ []));
|
|
31
|
+
/** Helper text shown next to the label. */
|
|
32
|
+
this.fileRecommendation = input('', ...(ngDevMode ? [{ debugName: "fileRecommendation" }] : /* istanbul ignore next */ []));
|
|
33
|
+
/** Text shown when files are already loaded. */
|
|
34
|
+
this.changeFilesText = input('Cambiar archivos', ...(ngDevMode ? [{ debugName: "changeFilesText" }] : /* istanbul ignore next */ []));
|
|
35
|
+
/** Enables selecting more than one file at once. */
|
|
36
|
+
this.multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
|
|
37
|
+
/** Files already stored by the parent (read-only; the component only emits events). */
|
|
38
|
+
this.existingFiles = input([], ...(ngDevMode ? [{ debugName: "existingFiles" }] : /* istanbul ignore next */ []));
|
|
39
|
+
/** How existing files are rendered. */
|
|
40
|
+
this.existingDisplay = input('chips', ...(ngDevMode ? [{ debugName: "existingDisplay" }] : /* istanbul ignore next */ []));
|
|
41
|
+
/** Emitted with the currently selected files. */
|
|
42
|
+
this.fileSelected = output();
|
|
43
|
+
/** Emitted when the last selected file is removed. */
|
|
44
|
+
this.fileRemoved = output();
|
|
45
|
+
/** Emitted when a file fails validation (type/size). */
|
|
46
|
+
this.fileError = output();
|
|
47
|
+
/** Emitted when a file is near the size limit. */
|
|
48
|
+
this.fileWarning = output();
|
|
49
|
+
/** Emitted when an existing file is requested for download/view. */
|
|
50
|
+
this.existingFileAction = output();
|
|
51
|
+
/** Emitted when an existing file should be removed (parent deletes it). */
|
|
52
|
+
this.existingFileRemove = output();
|
|
53
|
+
/** Currently selected files. */
|
|
54
|
+
this.selectedFiles = signal([], ...(ngDevMode ? [{ debugName: "selectedFiles" }] : /* istanbul ignore next */ []));
|
|
55
|
+
/** Localized error message displayed to the user. */
|
|
56
|
+
this.errorMessage = signal('', ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
|
|
57
|
+
/** Whether the user is dragging files over the zone. */
|
|
58
|
+
this.isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : /* istanbul ignore next */ []));
|
|
59
|
+
/** Object URLs used for image previews. */
|
|
60
|
+
this.previewUrls = new Map();
|
|
61
|
+
/** Whether another selection is allowed. */
|
|
62
|
+
this.canAddMore = computed(() => {
|
|
63
|
+
if (!this.multiple())
|
|
64
|
+
return this.selectedFiles().length === 0;
|
|
65
|
+
const max = this.maxFiles();
|
|
66
|
+
if (max <= 0)
|
|
67
|
+
return true;
|
|
68
|
+
return this.selectedFiles().length < max;
|
|
69
|
+
}, ...(ngDevMode ? [{ debugName: "canAddMore" }] : /* istanbul ignore next */ []));
|
|
70
|
+
/** True when the maximum number of files has been reached. */
|
|
71
|
+
this.maxReached = computed(() => {
|
|
72
|
+
const max = this.maxFiles();
|
|
73
|
+
if (max <= 0)
|
|
74
|
+
return false;
|
|
75
|
+
return this.selectedFiles().length >= max;
|
|
76
|
+
}, ...(ngDevMode ? [{ debugName: "maxReached" }] : /* istanbul ignore next */ []));
|
|
77
|
+
}
|
|
78
|
+
/** Whether the file is an image (used for previews). */
|
|
79
|
+
isImageFile(file) {
|
|
80
|
+
return file.type.startsWith('image/');
|
|
81
|
+
}
|
|
82
|
+
/** Returns (creating on demand) the object URL for a file preview. */
|
|
83
|
+
previewUrl(file) {
|
|
84
|
+
if (!this.previewUrls.has(file)) {
|
|
85
|
+
this.previewUrls.set(file, URL.createObjectURL(file));
|
|
86
|
+
}
|
|
87
|
+
return this.previewUrls.get(file) ?? '';
|
|
88
|
+
}
|
|
89
|
+
/** Marks the drop zone as active while dragging over it. */
|
|
90
|
+
onDragOver(event) {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
event.stopPropagation();
|
|
93
|
+
this.isDragOver.set(true);
|
|
94
|
+
}
|
|
95
|
+
/** Clears the drag-over state when the drag leaves the zone. */
|
|
96
|
+
onDragLeave(event) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
event.stopPropagation();
|
|
99
|
+
this.isDragOver.set(false);
|
|
100
|
+
}
|
|
101
|
+
/** Processes the files dropped on the zone. */
|
|
102
|
+
onDrop(event) {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
event.stopPropagation();
|
|
105
|
+
this.isDragOver.set(false);
|
|
106
|
+
const files = event.dataTransfer?.files;
|
|
107
|
+
if (files && files.length > 0) {
|
|
108
|
+
this.processFiles(Array.from(files));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Processes the files picked from the native file input. */
|
|
112
|
+
onFileChange(event) {
|
|
113
|
+
const input = event.target;
|
|
114
|
+
const files = input.files;
|
|
115
|
+
if (files && files.length > 0) {
|
|
116
|
+
this.processFiles(Array.from(files));
|
|
117
|
+
}
|
|
118
|
+
input.value = '';
|
|
119
|
+
}
|
|
120
|
+
/** Validates and appends the received files, emitting errors/warnings. */
|
|
121
|
+
processFiles(files) {
|
|
122
|
+
this.errorMessage.set('');
|
|
123
|
+
const validFiles = [];
|
|
124
|
+
for (const file of files) {
|
|
125
|
+
const error = this.validateFile(file);
|
|
126
|
+
if (error) {
|
|
127
|
+
this.fileError.emit(error);
|
|
128
|
+
this.errorMessage.set(error.message);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const warning = this.checkSizeWarning(file);
|
|
132
|
+
if (warning) {
|
|
133
|
+
this.fileWarning.emit(warning);
|
|
134
|
+
}
|
|
135
|
+
validFiles.push(file);
|
|
136
|
+
}
|
|
137
|
+
const max = this.maxFiles();
|
|
138
|
+
if (max > 0) {
|
|
139
|
+
const currentCount = this.selectedFiles().length;
|
|
140
|
+
const allowed = max - currentCount;
|
|
141
|
+
if (validFiles.length > allowed) {
|
|
142
|
+
const errorMsg = this.transloco.translate('fileUpload.maxFiles', undefined, `Máximo ${max} archivos permitidos`);
|
|
143
|
+
this.errorMessage.set(errorMsg);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (this.multiple()) {
|
|
148
|
+
this.selectedFiles.set([...this.selectedFiles(), ...validFiles]);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
this.clearPreviews();
|
|
152
|
+
this.selectedFiles.set(validFiles);
|
|
153
|
+
}
|
|
154
|
+
this.fileSelected.emit(this.selectedFiles());
|
|
155
|
+
}
|
|
156
|
+
/** Validates a file against `accept` and `maxFileSize`. */
|
|
157
|
+
validateFile(file) {
|
|
158
|
+
const acceptStr = this.accept();
|
|
159
|
+
if (acceptStr && acceptStr !== '*') {
|
|
160
|
+
const acceptTypes = acceptStr.split(',').map((t) => t.trim());
|
|
161
|
+
const valid = acceptTypes.some((type) => {
|
|
162
|
+
if (type.endsWith('/*')) {
|
|
163
|
+
const category = type.replace('/*', '');
|
|
164
|
+
return file.type.startsWith(category);
|
|
165
|
+
}
|
|
166
|
+
if (type.startsWith('.')) {
|
|
167
|
+
const ext = type.slice(1).toLowerCase();
|
|
168
|
+
return file.name.toLowerCase().endsWith('.' + ext);
|
|
169
|
+
}
|
|
170
|
+
if (type.includes('/')) {
|
|
171
|
+
return file.type === type;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
});
|
|
175
|
+
if (!valid) {
|
|
176
|
+
return {
|
|
177
|
+
type: 'type',
|
|
178
|
+
message: this.transloco.translate('fileUpload.invalidType', { accept: acceptStr }, `Tipo de archivo no válido. Formatos aceptados: ${acceptStr}`),
|
|
179
|
+
file,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (this.maxFileSize() > 0 && file.size > this.maxFileSize()) {
|
|
184
|
+
return {
|
|
185
|
+
type: 'size',
|
|
186
|
+
message: this.transloco.translate('fileUpload.fileTooLarge', { size: this.formatFileSize(this.maxFileSize()) }, `El archivo excede el tamaño máximo de ${this.formatFileSize(this.maxFileSize())}`),
|
|
187
|
+
file,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
/** Emits a warning when the file is close to the size limit (>90%). */
|
|
193
|
+
checkSizeWarning(file) {
|
|
194
|
+
if (this.maxFileSize() > 0 && file.size > this.maxFileSize() * 0.9) {
|
|
195
|
+
return {
|
|
196
|
+
type: 'size',
|
|
197
|
+
message: this.transloco.translate('fileUpload.sizeWarning', { size: this.formatFileSize(this.maxFileSize()) }, `El archivo está cerca del límite de tamaño (${this.formatFileSize(this.maxFileSize())})`),
|
|
198
|
+
file,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
/** Removes a file, revoking its preview URL. */
|
|
204
|
+
removeFile(file) {
|
|
205
|
+
const url = this.previewUrls.get(file);
|
|
206
|
+
if (url) {
|
|
207
|
+
URL.revokeObjectURL(url);
|
|
208
|
+
this.previewUrls.delete(file);
|
|
209
|
+
}
|
|
210
|
+
this.selectedFiles.update((files) => files.filter((f) => f !== file));
|
|
211
|
+
this.errorMessage.set('');
|
|
212
|
+
if (this.selectedFiles().length === 0) {
|
|
213
|
+
this.fileRemoved.emit();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Clears all selected files and previews. */
|
|
217
|
+
changeFiles() {
|
|
218
|
+
this.clearPreviews();
|
|
219
|
+
this.selectedFiles.set([]);
|
|
220
|
+
this.errorMessage.set('');
|
|
221
|
+
}
|
|
222
|
+
/** Revokes every stored preview object URL. */
|
|
223
|
+
clearPreviews() {
|
|
224
|
+
for (const [file, url] of this.previewUrls) {
|
|
225
|
+
URL.revokeObjectURL(url);
|
|
226
|
+
}
|
|
227
|
+
this.previewUrls.clear();
|
|
228
|
+
}
|
|
229
|
+
/** Revokes pending preview URLs when the component is destroyed. */
|
|
230
|
+
ngOnDestroy() {
|
|
231
|
+
this.clearPreviews();
|
|
232
|
+
}
|
|
233
|
+
/** Emits the action (download/view) for an existing file. */
|
|
234
|
+
onExistingAction(file) {
|
|
235
|
+
this.existingFileAction.emit(file);
|
|
236
|
+
}
|
|
237
|
+
/** Emits the removal request for an existing file. */
|
|
238
|
+
onExistingRemove(file) {
|
|
239
|
+
this.existingFileRemove.emit(file);
|
|
240
|
+
}
|
|
241
|
+
/** Whether an existing file is an image (by mime type or extension). */
|
|
242
|
+
isExistingImage(file) {
|
|
243
|
+
const mime = (file.mimeType || '').toLowerCase();
|
|
244
|
+
if (mime.startsWith('image/'))
|
|
245
|
+
return true;
|
|
246
|
+
const ext = this.getExtension(file.name).toLowerCase();
|
|
247
|
+
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext);
|
|
248
|
+
}
|
|
249
|
+
/** Resolves the extension of a file name. */
|
|
250
|
+
getExtension(name) {
|
|
251
|
+
return name.split('.').pop()?.toLowerCase() || '';
|
|
252
|
+
}
|
|
253
|
+
/** Icon + colored badge shown for non-image files, hinting at the type. */
|
|
254
|
+
getExistingBadge(file) {
|
|
255
|
+
const ext = this.getExtension(file.name);
|
|
256
|
+
switch (ext) {
|
|
257
|
+
case 'pdf':
|
|
258
|
+
return { label: 'PDF', color: 'bg-red-100 text-red-600', icon: 'file-text' };
|
|
259
|
+
case 'doc':
|
|
260
|
+
case 'docx':
|
|
261
|
+
return { label: 'DOC', color: 'bg-blue-100 text-blue-600', icon: 'file-text' };
|
|
262
|
+
case 'xls':
|
|
263
|
+
case 'xlsx':
|
|
264
|
+
return { label: 'XLS', color: 'bg-green-100 text-green-600', icon: 'file-spreadsheet' };
|
|
265
|
+
default:
|
|
266
|
+
return { label: ext.toUpperCase() || 'FILE', color: 'bg-slate-100 text-slate-500', icon: 'file' };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** trackBy for existing files. */
|
|
270
|
+
trackExistingFile(file) {
|
|
271
|
+
return file.id ?? file.name;
|
|
272
|
+
}
|
|
273
|
+
/** Human readable size (B/KB/MB). */
|
|
274
|
+
formatFileSize(bytes) {
|
|
275
|
+
if (bytes < 1024)
|
|
276
|
+
return bytes + ' B';
|
|
277
|
+
if (bytes < 1024 * 1024)
|
|
278
|
+
return (bytes / 1024).toFixed(1) + ' KB';
|
|
279
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
280
|
+
}
|
|
281
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: FileUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
282
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: FileUploadComponent, isStandalone: true, selector: "shk-file-upload", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxFileSize: { classPropertyName: "maxFileSize", publicName: "maxFileSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, fileUploadText: { classPropertyName: "fileUploadText", publicName: "fileUploadText", isSignal: true, isRequired: false, transformFunction: null }, fileRecommendation: { classPropertyName: "fileRecommendation", publicName: "fileRecommendation", isSignal: true, isRequired: false, transformFunction: null }, changeFilesText: { classPropertyName: "changeFilesText", publicName: "changeFilesText", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, existingFiles: { classPropertyName: "existingFiles", publicName: "existingFiles", isSignal: true, isRequired: false, transformFunction: null }, existingDisplay: { classPropertyName: "existingDisplay", publicName: "existingDisplay", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileSelected: "fileSelected", fileRemoved: "fileRemoved", fileError: "fileError", fileWarning: "fileWarning", existingFileAction: "existingFileAction", existingFileRemove: "existingFileRemove" }, ngImport: i0, template: "<div class=\"flex flex-col gap-2\">\n @if (label()) {\n <label class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ label() }}\n </label>\n }\n\n @if (canAddMore()) {\n <div\n class=\"relative border-2 border-dashed _shk-border dark:[border-color:var(--shk-surface-700)] rounded-lg p-4 transition-colors hover:[border-color:var(--shk-primary-400)] dark:hover:[border-color:var(--shk-primary-500)]\"\n [class.border-red-400]=\"errorMessage()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n >\n <input\n #fileInput\n type=\"file\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n (change)=\"onFileChange($event)\"\n class=\"hidden\"\n />\n\n <div\n class=\"flex flex-col items-center gap-2 cursor-pointer\"\n (click)=\"fileInput.click()\"\n [class.drag-over]=\"isDragOver()\"\n >\n <svg [lucideIcon]=\"getIcon('upload')\" class=\"w-8 h-8 _shk-icon-subtle dark:[color:var(--shk-surface-600)]\"></svg>\n <span class=\"text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ fileUploadText() }}\n </span>\n @if (fileRecommendation()) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n {{ fileRecommendation() }}\n </span>\n }\n </div>\n </div>\n }\n\n @if (maxReached()) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] text-center py-2\">\n M\u00E1ximo {{ maxFiles() }} archivos alcanzado\n </div>\n }\n\n @if (selectedFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of selectedFiles(); track file.name + file.size + file.lastModified) {\n <div class=\"relative group\">\n @if (isImageFile(file)) {\n <img\n [src]=\"previewUrl(file)\"\n [alt]=\"file.name\"\n class=\"w-20 h-20 object-cover rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)]\"\n />\n } @else {\n <div class=\"w-20 h-20 rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)] flex flex-col items-center justify-center _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)] gap-1\">\n <svg [lucideIcon]=\"getIcon('file')\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"text-[10px] _shk-text-surface-400 truncate max-w-[70px] px-1 text-center\">{{ file.name }}</span>\n </div>\n }\n <button\n type=\"button\"\n (click)=\"removeFile(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full text-xs flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n </div>\n }\n </div>\n @if (!multiple()) {\n <button\n type=\"button\"\n (click)=\"changeFiles()\"\n class=\"text-xs _shk-text-primary hover:[color:var(--shk-primary-700)] dark:[color:var(--shk-primary-400)] dark:hover:[color:var(--shk-primary-300)] underline cursor-pointer bg-transparent border-0\"\n >\n {{ changeFilesText() }}\n </button>\n }\n }\n\n @if (existingFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of existingFiles(); track trackExistingFile(file)) {\n @if (existingDisplay() === 'tiles') {\n @let badge = getExistingBadge(file);\n <div class=\"relative group w-[80px]\">\n <div\n class=\"w-20 h-20 rounded-lg border _shk-border dark:[border-color:var(--shk-surface-700)] overflow-hidden cursor-pointer relative\"\n (click)=\"onExistingAction(file)\"\n [title]=\"file.name\"\n >\n @if (isExistingImage(file) && file.url) {\n <img [src]=\"file.url\" [alt]=\"file.name\" class=\"w-full h-full object-cover\" />\n } @else {\n <div class=\"w-full h-full flex flex-col items-center justify-center gap-1 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)]\">\n <svg [lucideIcon]=\"getIcon(badge.icon)\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"px-1.5 py-0.5 rounded text-[9px] font-semibold leading-none\" [class]=\"badge.color\">\n {{ badge.label }}\n </span>\n </div>\n }\n <div\n class=\"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 flex items-center justify-center transition rounded-lg\"\n >\n <svg [lucideIcon]=\"getIcon('download')\" class=\"w-5 h-5 text-white\"></svg>\n </div>\n </div>\n <button\n type=\"button\"\n (click)=\"onExistingRemove(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n aria-label=\"Remove\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n <span class=\"block text-[10px] _shk-text-surface-400 truncate text-center mt-1 max-w-[80px]\" [title]=\"file.name\">\n {{ file.name }}\n </span>\n </div>\n } @else {\n @let badge = getExistingBadge(file);\n <div class=\"flex items-center gap-2 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border _shk-border dark:[border-color:var(--shk-surface-200)] rounded-full pl-3 pr-1.5 py-1 text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">\n <svg [lucideIcon]=\"getIcon(badge.icon)\" class=\"w-4 h-4 _shk-text-surface-400 shrink-0\"></svg>\n <span class=\"max-w-[200px] truncate\" [title]=\"file.name\">{{ file.name }}</span>\n <button\n type=\"button\"\n (click)=\"onExistingAction(file)\"\n [title]=\"file.name\"\n class=\"p-1 rounded-full hover:[background-color:var(--shk-surface-200)] _shk-text-surface-500 hover:[color:var(--shk-primary-600)]\"\n >\n <svg [lucideIcon]=\"getIcon('download')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <button\n type=\"button\"\n (click)=\"onExistingRemove(file)\"\n class=\"p-1 rounded-full hover:[background-color:var(--shk-error-50)] _shk-text-surface-500 hover:[color:var(--shk-error-600)]\"\n aria-label=\"Remove\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n </div>\n }\n }\n </div>\n }\n\n @if (errorMessage()) {\n <p class=\"text-xs _shk-text-red-500\">{{ errorMessage() }}</p>\n }\n</div>", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
283
|
+
}
|
|
284
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: FileUploadComponent, decorators: [{
|
|
285
|
+
type: Component,
|
|
286
|
+
args: [{ selector: 'shk-file-upload', standalone: true, imports: [CommonModule, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-2\">\n @if (label()) {\n <label class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ label() }}\n </label>\n }\n\n @if (canAddMore()) {\n <div\n class=\"relative border-2 border-dashed _shk-border dark:[border-color:var(--shk-surface-700)] rounded-lg p-4 transition-colors hover:[border-color:var(--shk-primary-400)] dark:hover:[border-color:var(--shk-primary-500)]\"\n [class.border-red-400]=\"errorMessage()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n >\n <input\n #fileInput\n type=\"file\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n (change)=\"onFileChange($event)\"\n class=\"hidden\"\n />\n\n <div\n class=\"flex flex-col items-center gap-2 cursor-pointer\"\n (click)=\"fileInput.click()\"\n [class.drag-over]=\"isDragOver()\"\n >\n <svg [lucideIcon]=\"getIcon('upload')\" class=\"w-8 h-8 _shk-icon-subtle dark:[color:var(--shk-surface-600)]\"></svg>\n <span class=\"text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ fileUploadText() }}\n </span>\n @if (fileRecommendation()) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n {{ fileRecommendation() }}\n </span>\n }\n </div>\n </div>\n }\n\n @if (maxReached()) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] text-center py-2\">\n M\u00E1ximo {{ maxFiles() }} archivos alcanzado\n </div>\n }\n\n @if (selectedFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of selectedFiles(); track file.name + file.size + file.lastModified) {\n <div class=\"relative group\">\n @if (isImageFile(file)) {\n <img\n [src]=\"previewUrl(file)\"\n [alt]=\"file.name\"\n class=\"w-20 h-20 object-cover rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)]\"\n />\n } @else {\n <div class=\"w-20 h-20 rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)] flex flex-col items-center justify-center _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)] gap-1\">\n <svg [lucideIcon]=\"getIcon('file')\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"text-[10px] _shk-text-surface-400 truncate max-w-[70px] px-1 text-center\">{{ file.name }}</span>\n </div>\n }\n <button\n type=\"button\"\n (click)=\"removeFile(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full text-xs flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n </div>\n }\n </div>\n @if (!multiple()) {\n <button\n type=\"button\"\n (click)=\"changeFiles()\"\n class=\"text-xs _shk-text-primary hover:[color:var(--shk-primary-700)] dark:[color:var(--shk-primary-400)] dark:hover:[color:var(--shk-primary-300)] underline cursor-pointer bg-transparent border-0\"\n >\n {{ changeFilesText() }}\n </button>\n }\n }\n\n @if (existingFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of existingFiles(); track trackExistingFile(file)) {\n @if (existingDisplay() === 'tiles') {\n @let badge = getExistingBadge(file);\n <div class=\"relative group w-[80px]\">\n <div\n class=\"w-20 h-20 rounded-lg border _shk-border dark:[border-color:var(--shk-surface-700)] overflow-hidden cursor-pointer relative\"\n (click)=\"onExistingAction(file)\"\n [title]=\"file.name\"\n >\n @if (isExistingImage(file) && file.url) {\n <img [src]=\"file.url\" [alt]=\"file.name\" class=\"w-full h-full object-cover\" />\n } @else {\n <div class=\"w-full h-full flex flex-col items-center justify-center gap-1 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)]\">\n <svg [lucideIcon]=\"getIcon(badge.icon)\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"px-1.5 py-0.5 rounded text-[9px] font-semibold leading-none\" [class]=\"badge.color\">\n {{ badge.label }}\n </span>\n </div>\n }\n <div\n class=\"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 flex items-center justify-center transition rounded-lg\"\n >\n <svg [lucideIcon]=\"getIcon('download')\" class=\"w-5 h-5 text-white\"></svg>\n </div>\n </div>\n <button\n type=\"button\"\n (click)=\"onExistingRemove(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n aria-label=\"Remove\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n <span class=\"block text-[10px] _shk-text-surface-400 truncate text-center mt-1 max-w-[80px]\" [title]=\"file.name\">\n {{ file.name }}\n </span>\n </div>\n } @else {\n @let badge = getExistingBadge(file);\n <div class=\"flex items-center gap-2 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border _shk-border dark:[border-color:var(--shk-surface-200)] rounded-full pl-3 pr-1.5 py-1 text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">\n <svg [lucideIcon]=\"getIcon(badge.icon)\" class=\"w-4 h-4 _shk-text-surface-400 shrink-0\"></svg>\n <span class=\"max-w-[200px] truncate\" [title]=\"file.name\">{{ file.name }}</span>\n <button\n type=\"button\"\n (click)=\"onExistingAction(file)\"\n [title]=\"file.name\"\n class=\"p-1 rounded-full hover:[background-color:var(--shk-surface-200)] _shk-text-surface-500 hover:[color:var(--shk-primary-600)]\"\n >\n <svg [lucideIcon]=\"getIcon('download')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <button\n type=\"button\"\n (click)=\"onExistingRemove(file)\"\n class=\"p-1 rounded-full hover:[background-color:var(--shk-error-50)] _shk-text-surface-500 hover:[color:var(--shk-error-600)]\"\n aria-label=\"Remove\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n </div>\n }\n }\n </div>\n }\n\n @if (errorMessage()) {\n <p class=\"text-xs _shk-text-red-500\">{{ errorMessage() }}</p>\n }\n</div>" }]
|
|
287
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxFileSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], fileUploadText: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileUploadText", required: false }] }], fileRecommendation: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileRecommendation", required: false }] }], changeFilesText: [{ type: i0.Input, args: [{ isSignal: true, alias: "changeFilesText", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], existingFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "existingFiles", required: false }] }], existingDisplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "existingDisplay", required: false }] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }], fileError: [{ type: i0.Output, args: ["fileError"] }], fileWarning: [{ type: i0.Output, args: ["fileWarning"] }], existingFileAction: [{ type: i0.Output, args: ["existingFileAction"] }], existingFileRemove: [{ type: i0.Output, args: ["existingFileRemove"] }] } });
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Generated bundle index. Do not edit.
|
|
291
|
+
*/
|
|
292
|
+
|
|
293
|
+
export { FileUploadComponent };
|
|
294
|
+
//# sourceMappingURL=shirkasoft-ui-components-file-upload.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shirkasoft-ui-components-file-upload.mjs","sources":["../../../projects/ui-components/file-upload/file-upload.component.ts","../../../projects/ui-components/file-upload/file-upload.component.html","../../../projects/ui-components/file-upload/shirkasoft-ui-components-file-upload.ts"],"sourcesContent":["import {\n Component,\n input,\n output,\n signal,\n computed,\n ChangeDetectionStrategy,\n inject,\n OnDestroy,\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { TranslocoService } from '@jsverse/transloco';\nimport { LucideDynamicIcon, LucideUpload } from '@lucide/angular';\nimport { getIcon as getIconFromCentral } from 'shirkasoft-ui-components/utils';\n\nexport interface FileUploadError {\n type: 'size' | 'type';\n message: string;\n file: File;\n}\n\nexport interface ExistingFile {\n id?: number | string;\n name: string;\n url?: string;\n mimeType?: string;\n size?: number;\n}\n\n/**\n * Shirkasoft UI Components.\n * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).\n * Belongs to Shirkasoft.\n */\n\n/**\n * Drag & drop file uploader with file type/size validation, image previews,\n * multiple files support and localized error messages.\n */\n@Component({\n selector: 'shk-file-upload',\n standalone: true,\n imports: [CommonModule, LucideDynamicIcon],\n templateUrl: './file-upload.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class FileUploadComponent implements OnDestroy {\n protected getIcon = (name: any) => getIconFromCentral(name, LucideUpload);\n private transloco = inject(TranslocoService);\n\n /** Main label of the uploader. */\n label = input<string>('Cargar archivo');\n /** Accepted MIME types/extensions (e.g. 'image/*', '.pdf'). */\n accept = input<string>('image/*');\n /** Maximum allowed size per file (bytes). */\n maxFileSize = input<number>(2 * 1024 * 1024);\n /** Maximum number of files (0 = unlimited). */\n maxFiles = input<number>(0);\n /** Text of the file picker button. */\n fileUploadText = input<string>('Seleccionar archivo');\n /** Helper text shown next to the label. */\n fileRecommendation = input<string>('');\n /** Text shown when files are already loaded. */\n changeFilesText = input<string>('Cambiar archivos');\n /** Enables selecting more than one file at once. */\n multiple = input<boolean>(false);\n /** Files already stored by the parent (read-only; the component only emits events). */\n existingFiles = input<ExistingFile[]>([]);\n /** How existing files are rendered. */\n existingDisplay = input<'chips' | 'tiles'>('chips');\n\n /** Emitted with the currently selected files. */\n fileSelected = output<File[]>();\n /** Emitted when the last selected file is removed. */\n fileRemoved = output<void>();\n /** Emitted when a file fails validation (type/size). */\n fileError = output<FileUploadError>();\n /** Emitted when a file is near the size limit. */\n fileWarning = output<FileUploadError>();\n /** Emitted when an existing file is requested for download/view. */\n existingFileAction = output<ExistingFile>();\n /** Emitted when an existing file should be removed (parent deletes it). */\n existingFileRemove = output<ExistingFile>();\n\n /** Currently selected files. */\n selectedFiles = signal<File[]>([]);\n /** Localized error message displayed to the user. */\n errorMessage = signal<string>('');\n /** Whether the user is dragging files over the zone. */\n isDragOver = signal<boolean>(false);\n\n /** Object URLs used for image previews. */\n private previewUrls = new Map<File, string>();\n\n /** Whether another selection is allowed. */\n canAddMore = computed(() => {\n if (!this.multiple()) return this.selectedFiles().length === 0;\n const max = this.maxFiles();\n if (max <= 0) return true;\n return this.selectedFiles().length < max;\n });\n\n /** True when the maximum number of files has been reached. */\n maxReached = computed(() => {\n const max = this.maxFiles();\n if (max <= 0) return false;\n return this.selectedFiles().length >= max;\n });\n\n /** Whether the file is an image (used for previews). */\n isImageFile(file: File): boolean {\n return file.type.startsWith('image/');\n }\n\n /** Returns (creating on demand) the object URL for a file preview. */\n previewUrl(file: File): string {\n if (!this.previewUrls.has(file)) {\n this.previewUrls.set(file, URL.createObjectURL(file));\n }\n return this.previewUrls.get(file) ?? '';\n }\n\n /** Marks the drop zone as active while dragging over it. */\n onDragOver(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(true);\n }\n\n /** Clears the drag-over state when the drag leaves the zone. */\n onDragLeave(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(false);\n }\n\n /** Processes the files dropped on the zone. */\n onDrop(event: DragEvent): void {\n event.preventDefault();\n event.stopPropagation();\n this.isDragOver.set(false);\n\n const files = event.dataTransfer?.files;\n if (files && files.length > 0) {\n this.processFiles(Array.from(files));\n }\n }\n\n /** Processes the files picked from the native file input. */\n onFileChange(event: Event): void {\n const input = event.target as HTMLInputElement;\n const files = input.files;\n if (files && files.length > 0) {\n this.processFiles(Array.from(files));\n }\n input.value = '';\n }\n\n /** Validates and appends the received files, emitting errors/warnings. */\n private processFiles(files: File[]): void {\n this.errorMessage.set('');\n const validFiles: File[] = [];\n\n for (const file of files) {\n const error = this.validateFile(file);\n if (error) {\n this.fileError.emit(error);\n this.errorMessage.set(error.message);\n return;\n }\n\n const warning = this.checkSizeWarning(file);\n if (warning) {\n this.fileWarning.emit(warning);\n }\n\n validFiles.push(file);\n }\n\n const max = this.maxFiles();\n if (max > 0) {\n const currentCount = this.selectedFiles().length;\n const allowed = max - currentCount;\n if (validFiles.length > allowed) {\n const errorMsg = this.transloco.translate('fileUpload.maxFiles', undefined, `Máximo ${max} archivos permitidos`);\n this.errorMessage.set(errorMsg);\n return;\n }\n }\n\n if (this.multiple()) {\n this.selectedFiles.set([...this.selectedFiles(), ...validFiles]);\n } else {\n this.clearPreviews();\n this.selectedFiles.set(validFiles);\n }\n\n this.fileSelected.emit(this.selectedFiles());\n }\n\n /** Validates a file against `accept` and `maxFileSize`. */\n private validateFile(file: File): FileUploadError | null {\n const acceptStr = this.accept();\n if (acceptStr && acceptStr !== '*') {\n const acceptTypes = acceptStr.split(',').map((t) => t.trim());\n const valid = acceptTypes.some((type) => {\n if (type.endsWith('/*')) {\n const category = type.replace('/*', '');\n return file.type.startsWith(category);\n }\n if (type.startsWith('.')) {\n const ext = type.slice(1).toLowerCase();\n return file.name.toLowerCase().endsWith('.' + ext);\n }\n if (type.includes('/')) {\n return file.type === type;\n }\n return false;\n });\n\n if (!valid) {\n return {\n type: 'type',\n message: this.transloco.translate('fileUpload.invalidType', { accept: acceptStr }, `Tipo de archivo no válido. Formatos aceptados: ${acceptStr}`),\n file,\n };\n }\n }\n\n if (this.maxFileSize() > 0 && file.size > this.maxFileSize()) {\n return {\n type: 'size',\n message: this.transloco.translate('fileUpload.fileTooLarge', { size: this.formatFileSize(this.maxFileSize()) }, `El archivo excede el tamaño máximo de ${this.formatFileSize(this.maxFileSize())}`),\n file,\n };\n }\n\n return null;\n }\n\n /** Emits a warning when the file is close to the size limit (>90%). */\n private checkSizeWarning(file: File): FileUploadError | null {\n if (this.maxFileSize() > 0 && file.size > this.maxFileSize() * 0.9) {\n return {\n type: 'size',\n message: this.transloco.translate('fileUpload.sizeWarning', { size: this.formatFileSize(this.maxFileSize()) }, `El archivo está cerca del límite de tamaño (${this.formatFileSize(this.maxFileSize())})`),\n file,\n };\n }\n return null;\n }\n\n /** Removes a file, revoking its preview URL. */\n removeFile(file: File): void {\n const url = this.previewUrls.get(file);\n if (url) {\n URL.revokeObjectURL(url);\n this.previewUrls.delete(file);\n }\n\n this.selectedFiles.update((files) => files.filter((f) => f !== file));\n this.errorMessage.set('');\n\n if (this.selectedFiles().length === 0) {\n this.fileRemoved.emit();\n }\n }\n\n /** Clears all selected files and previews. */\n changeFiles(): void {\n this.clearPreviews();\n this.selectedFiles.set([]);\n this.errorMessage.set('');\n }\n\n /** Revokes every stored preview object URL. */\n private clearPreviews(): void {\n for (const [file, url] of this.previewUrls) {\n URL.revokeObjectURL(url);\n }\n this.previewUrls.clear();\n }\n\n /** Revokes pending preview URLs when the component is destroyed. */\n ngOnDestroy(): void {\n this.clearPreviews();\n }\n\n /** Emits the action (download/view) for an existing file. */\n onExistingAction(file: ExistingFile): void {\n this.existingFileAction.emit(file);\n }\n\n /** Emits the removal request for an existing file. */\n onExistingRemove(file: ExistingFile): void {\n this.existingFileRemove.emit(file);\n }\n\n /** Whether an existing file is an image (by mime type or extension). */\n isExistingImage(file: ExistingFile): boolean {\n const mime = (file.mimeType || '').toLowerCase();\n if (mime.startsWith('image/')) return true;\n const ext = this.getExtension(file.name).toLowerCase();\n return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext);\n }\n\n /** Resolves the extension of a file name. */\n getExtension(name: string): string {\n return name.split('.').pop()?.toLowerCase() || '';\n }\n\n /** Icon + colored badge shown for non-image files, hinting at the type. */\n getExistingBadge(file: ExistingFile): { label: string; color: string; icon: string } {\n const ext = this.getExtension(file.name);\n switch (ext) {\n case 'pdf':\n return { label: 'PDF', color: 'bg-red-100 text-red-600', icon: 'file-text' };\n case 'doc':\n case 'docx':\n return { label: 'DOC', color: 'bg-blue-100 text-blue-600', icon: 'file-text' };\n case 'xls':\n case 'xlsx':\n return { label: 'XLS', color: 'bg-green-100 text-green-600', icon: 'file-spreadsheet' };\n default:\n return { label: ext.toUpperCase() || 'FILE', color: 'bg-slate-100 text-slate-500', icon: 'file' };\n }\n }\n\n /** trackBy for existing files. */\n trackExistingFile(file: ExistingFile): any {\n return file.id ?? file.name;\n }\n\n /** Human readable size (B/KB/MB). */\n private formatFileSize(bytes: number): string {\n if (bytes < 1024) return bytes + ' B';\n if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';\n return (bytes / (1024 * 1024)).toFixed(1) + ' MB';\n }\n}","<div class=\"flex flex-col gap-2\">\n @if (label()) {\n <label class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ label() }}\n </label>\n }\n\n @if (canAddMore()) {\n <div\n class=\"relative border-2 border-dashed _shk-border dark:[border-color:var(--shk-surface-700)] rounded-lg p-4 transition-colors hover:[border-color:var(--shk-primary-400)] dark:hover:[border-color:var(--shk-primary-500)]\"\n [class.border-red-400]=\"errorMessage()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n >\n <input\n #fileInput\n type=\"file\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n (change)=\"onFileChange($event)\"\n class=\"hidden\"\n />\n\n <div\n class=\"flex flex-col items-center gap-2 cursor-pointer\"\n (click)=\"fileInput.click()\"\n [class.drag-over]=\"isDragOver()\"\n >\n <svg [lucideIcon]=\"getIcon('upload')\" class=\"w-8 h-8 _shk-icon-subtle dark:[color:var(--shk-surface-600)]\"></svg>\n <span class=\"text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ fileUploadText() }}\n </span>\n @if (fileRecommendation()) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n {{ fileRecommendation() }}\n </span>\n }\n </div>\n </div>\n }\n\n @if (maxReached()) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] text-center py-2\">\n Máximo {{ maxFiles() }} archivos alcanzado\n </div>\n }\n\n @if (selectedFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of selectedFiles(); track file.name + file.size + file.lastModified) {\n <div class=\"relative group\">\n @if (isImageFile(file)) {\n <img\n [src]=\"previewUrl(file)\"\n [alt]=\"file.name\"\n class=\"w-20 h-20 object-cover rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)]\"\n />\n } @else {\n <div class=\"w-20 h-20 rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)] flex flex-col items-center justify-center _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)] gap-1\">\n <svg [lucideIcon]=\"getIcon('file')\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"text-[10px] _shk-text-surface-400 truncate max-w-[70px] px-1 text-center\">{{ file.name }}</span>\n </div>\n }\n <button\n type=\"button\"\n (click)=\"removeFile(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full text-xs flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n </div>\n }\n </div>\n @if (!multiple()) {\n <button\n type=\"button\"\n (click)=\"changeFiles()\"\n class=\"text-xs _shk-text-primary hover:[color:var(--shk-primary-700)] dark:[color:var(--shk-primary-400)] dark:hover:[color:var(--shk-primary-300)] underline cursor-pointer bg-transparent border-0\"\n >\n {{ changeFilesText() }}\n </button>\n }\n }\n\n @if (existingFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of existingFiles(); track trackExistingFile(file)) {\n @if (existingDisplay() === 'tiles') {\n @let badge = getExistingBadge(file);\n <div class=\"relative group w-[80px]\">\n <div\n class=\"w-20 h-20 rounded-lg border _shk-border dark:[border-color:var(--shk-surface-700)] overflow-hidden cursor-pointer relative\"\n (click)=\"onExistingAction(file)\"\n [title]=\"file.name\"\n >\n @if (isExistingImage(file) && file.url) {\n <img [src]=\"file.url\" [alt]=\"file.name\" class=\"w-full h-full object-cover\" />\n } @else {\n <div class=\"w-full h-full flex flex-col items-center justify-center gap-1 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)]\">\n <svg [lucideIcon]=\"getIcon(badge.icon)\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"px-1.5 py-0.5 rounded text-[9px] font-semibold leading-none\" [class]=\"badge.color\">\n {{ badge.label }}\n </span>\n </div>\n }\n <div\n class=\"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 flex items-center justify-center transition rounded-lg\"\n >\n <svg [lucideIcon]=\"getIcon('download')\" class=\"w-5 h-5 text-white\"></svg>\n </div>\n </div>\n <button\n type=\"button\"\n (click)=\"onExistingRemove(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n aria-label=\"Remove\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n <span class=\"block text-[10px] _shk-text-surface-400 truncate text-center mt-1 max-w-[80px]\" [title]=\"file.name\">\n {{ file.name }}\n </span>\n </div>\n } @else {\n @let badge = getExistingBadge(file);\n <div class=\"flex items-center gap-2 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border _shk-border dark:[border-color:var(--shk-surface-200)] rounded-full pl-3 pr-1.5 py-1 text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">\n <svg [lucideIcon]=\"getIcon(badge.icon)\" class=\"w-4 h-4 _shk-text-surface-400 shrink-0\"></svg>\n <span class=\"max-w-[200px] truncate\" [title]=\"file.name\">{{ file.name }}</span>\n <button\n type=\"button\"\n (click)=\"onExistingAction(file)\"\n [title]=\"file.name\"\n class=\"p-1 rounded-full hover:[background-color:var(--shk-surface-200)] _shk-text-surface-500 hover:[color:var(--shk-primary-600)]\"\n >\n <svg [lucideIcon]=\"getIcon('download')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <button\n type=\"button\"\n (click)=\"onExistingRemove(file)\"\n class=\"p-1 rounded-full hover:[background-color:var(--shk-error-50)] _shk-text-surface-500 hover:[color:var(--shk-error-600)]\"\n aria-label=\"Remove\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n </div>\n }\n }\n </div>\n }\n\n @if (errorMessage()) {\n <p class=\"text-xs _shk-text-red-500\">{{ errorMessage() }}</p>\n }\n</div>","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":["getIconFromCentral"],"mappings":"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"}
|