cloud-ide-element 1.0.81 → 1.0.83
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/cloud-ide-element.mjs +1644 -1438
- package/fesm2022/cloud-ide-element.mjs.map +1 -1
- package/index.d.ts +59 -4
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i1 from '@angular/common';
|
|
2
2
|
import { CommonModule, NgTemplateOutlet } from '@angular/common';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { Pipe, Injectable, inject, EventEmitter, ViewContainerRef, forwardRef, ViewChild, Output, Input, Component, HostListener, ContentChildren, signal, DestroyRef, computed, afterRenderEffect, afterNextRender,
|
|
4
|
+
import { Pipe, Injectable, inject, EventEmitter, ViewContainerRef, forwardRef, ViewChild, Output, Input, Component, HostListener, ContentChildren, signal, DestroyRef, computed, effect, afterRenderEffect, afterNextRender, ElementRef, Directive, viewChild } from '@angular/core';
|
|
5
5
|
import * as i2 from '@angular/forms';
|
|
6
6
|
import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
|
|
7
7
|
import { BehaviorSubject, Subject, debounceTime, takeUntil, distinctUntilChanged, Observable, retry, catchError, finalize, throwError } from 'rxjs';
|
|
@@ -3148,1682 +3148,1888 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
|
|
|
3148
3148
|
}]
|
|
3149
3149
|
}], ctorParameters: () => [] });
|
|
3150
3150
|
|
|
3151
|
-
class
|
|
3152
|
-
fileManagerService = inject(CideEleFileManagerService);
|
|
3153
|
-
notificationService = inject(NotificationService);
|
|
3154
|
-
elementService = inject(CideElementsService);
|
|
3151
|
+
class CideEleFloatingFileUploaderComponent {
|
|
3155
3152
|
destroyRef = inject(DestroyRef);
|
|
3156
|
-
|
|
3157
|
-
//
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
multipleSignal = signal(this.multiple, ...(ngDevMode ? [{ debugName: "multipleSignal" }] : []));
|
|
3183
|
-
disabledSignal = signal(this.disabled, ...(ngDevMode ? [{ debugName: "disabledSignal" }] : []));
|
|
3184
|
-
requiredSignal = signal(this.required, ...(ngDevMode ? [{ debugName: "requiredSignal" }] : []));
|
|
3185
|
-
helperTextSignal = signal(this.helperText, ...(ngDevMode ? [{ debugName: "helperTextSignal" }] : []));
|
|
3186
|
-
errorTextSignal = signal(this.errorText, ...(ngDevMode ? [{ debugName: "errorTextSignal" }] : []));
|
|
3187
|
-
showPreviewSignal = signal(this.showPreview, ...(ngDevMode ? [{ debugName: "showPreviewSignal" }] : []));
|
|
3188
|
-
previewWidthSignal = signal(this.previewWidth, ...(ngDevMode ? [{ debugName: "previewWidthSignal" }] : []));
|
|
3189
|
-
previewHeightSignal = signal(this.previewHeight, ...(ngDevMode ? [{ debugName: "previewHeightSignal" }] : []));
|
|
3190
|
-
previewBoxModeSignal = signal(this.previewBoxMode, ...(ngDevMode ? [{ debugName: "previewBoxModeSignal" }] : []));
|
|
3191
|
-
showFileNameSignal = signal(this.showFileName, ...(ngDevMode ? [{ debugName: "showFileNameSignal" }] : []));
|
|
3192
|
-
placeholderTextSignal = signal(this.placeholderText, ...(ngDevMode ? [{ debugName: "placeholderTextSignal" }] : []));
|
|
3193
|
-
placeholderIconSignal = signal(this.placeholderIcon, ...(ngDevMode ? [{ debugName: "placeholderIconSignal" }] : []));
|
|
3194
|
-
autoUploadSignal = signal(this.autoUpload, ...(ngDevMode ? [{ debugName: "autoUploadSignal" }] : []));
|
|
3195
|
-
uploadDataSignal = signal(this.uploadData, ...(ngDevMode ? [{ debugName: "uploadDataSignal" }] : []));
|
|
3196
|
-
// Reactive state with signals
|
|
3197
|
-
id = signal(Math.random().toString(36).substring(2, 10), ...(ngDevMode ? [{ debugName: "id" }] : []));
|
|
3198
|
-
isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
|
|
3199
|
-
uploadProgress = signal(0, ...(ngDevMode ? [{ debugName: "uploadProgress" }] : []));
|
|
3200
|
-
uploadStatus = signal('idle', ...(ngDevMode ? [{ debugName: "uploadStatus" }] : []));
|
|
3201
|
-
files = signal(null, ...(ngDevMode ? [{ debugName: "files" }] : []));
|
|
3202
|
-
fileNames = signal([], ...(ngDevMode ? [{ debugName: "fileNames" }] : []));
|
|
3203
|
-
previewUrls = signal([], ...(ngDevMode ? [{ debugName: "previewUrls" }] : []));
|
|
3204
|
-
uploadNotificationId = signal(null, ...(ngDevMode ? [{ debugName: "uploadNotificationId" }] : []));
|
|
3205
|
-
isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
|
|
3206
|
-
groupId = signal(null, ...(ngDevMode ? [{ debugName: "groupId" }] : [])); // Group ID for multiple file uploads
|
|
3207
|
-
isMultipleUploadMode = signal(false, ...(ngDevMode ? [{ debugName: "isMultipleUploadMode" }] : [])); // Flag to track if we're in multiple upload mode
|
|
3208
|
-
// Computed signals for better relationships
|
|
3209
|
-
hasFiles = computed(() => this.files() !== null && this.files().length > 0, ...(ngDevMode ? [{ debugName: "hasFiles" }] : []));
|
|
3210
|
-
canUpload = computed(() => this.hasFiles() && !this.isUploading() && !this.disabledSignal(), ...(ngDevMode ? [{ debugName: "canUpload" }] : []));
|
|
3211
|
-
isInErrorState = computed(() => this.uploadStatus() === 'error', ...(ngDevMode ? [{ debugName: "isInErrorState" }] : []));
|
|
3212
|
-
isInSuccessState = computed(() => this.uploadStatus() === 'success', ...(ngDevMode ? [{ debugName: "isInSuccessState" }] : []));
|
|
3213
|
-
// Angular 20: Computed values using new features
|
|
3214
|
-
totalFileSize = computed(() => {
|
|
3215
|
-
if (!this.files())
|
|
3216
|
-
return 0;
|
|
3217
|
-
return Array.from(this.files()).reduce((total, file) => total + file.size, 0);
|
|
3218
|
-
}, ...(ngDevMode ? [{ debugName: "totalFileSize" }] : []));
|
|
3219
|
-
fileSizeInMB = computed(() => {
|
|
3220
|
-
// Angular 20: Using ** operator for exponentiation
|
|
3221
|
-
return (this.totalFileSize() / (1024 ** 2)).toFixed(2);
|
|
3222
|
-
}, ...(ngDevMode ? [{ debugName: "fileSizeInMB" }] : []));
|
|
3223
|
-
uploadProgressPercentage = computed(() => {
|
|
3224
|
-
// Angular 20: Using ** operator for exponentiation
|
|
3225
|
-
return Math.round(this.uploadProgress() ** 1); // Simple power operation
|
|
3226
|
-
}, ...(ngDevMode ? [{ debugName: "uploadProgressPercentage" }] : []));
|
|
3227
|
-
// ControlValueAccessor callbacks
|
|
3228
|
-
onChange = (value) => { };
|
|
3229
|
-
onTouched = () => { };
|
|
3230
|
-
onValidatorChange = () => { };
|
|
3231
|
-
// Computed values
|
|
3232
|
-
hasImages = computed(() => this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "hasImages" }] : []));
|
|
3233
|
-
isPreviewBoxMode = computed(() => this.previewBoxModeSignal() && this.showPreviewSignal(), ...(ngDevMode ? [{ debugName: "isPreviewBoxMode" }] : []));
|
|
3234
|
-
isImagePreviewAvailable = computed(() => this.showPreviewSignal() && this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "isImagePreviewAvailable" }] : []));
|
|
3153
|
+
fileManagerService = inject(CideEleFileManagerService);
|
|
3154
|
+
// Signals for reactive state
|
|
3155
|
+
isVisible = signal(false, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
|
|
3156
|
+
isMinimized = signal(false, ...(ngDevMode ? [{ debugName: "isMinimized" }] : []));
|
|
3157
|
+
currentUserId = signal('', ...(ngDevMode ? [{ debugName: "currentUserId" }] : []));
|
|
3158
|
+
currentGroupId = signal(null, ...(ngDevMode ? [{ debugName: "currentGroupId" }] : []));
|
|
3159
|
+
// Local state to maintain completed files visibility
|
|
3160
|
+
completedFiles = signal(new Map(), ...(ngDevMode ? [{ debugName: "completedFiles" }] : []));
|
|
3161
|
+
// Use file manager service's upload queue as the single source of truth
|
|
3162
|
+
uploadQueue = computed(() => this.fileManagerService.uploadQueue(), ...(ngDevMode ? [{ debugName: "uploadQueue" }] : []));
|
|
3163
|
+
activeUploads = computed(() => this.fileManagerService.activeUploads(), ...(ngDevMode ? [{ debugName: "activeUploads" }] : []));
|
|
3164
|
+
// Computed values based on file manager service state and local completed files
|
|
3165
|
+
hasUploads = computed(() => this.uploadQueue().length > 0 || this.activeUploads().size > 0 || this.completedFiles().size > 0, ...(ngDevMode ? [{ debugName: "hasUploads" }] : []));
|
|
3166
|
+
pendingUploads = computed(() => this.uploadQueue().length > 0, ...(ngDevMode ? [{ debugName: "pendingUploads" }] : [])); // All files in queue are pending
|
|
3167
|
+
completedUploads = computed(() => {
|
|
3168
|
+
const active = this.activeUploads();
|
|
3169
|
+
const activeCompleted = Array.from(active.values()).filter(upload => upload.stage === 'complete');
|
|
3170
|
+
const localCompleted = Array.from(this.completedFiles().values());
|
|
3171
|
+
return [...activeCompleted, ...localCompleted];
|
|
3172
|
+
}, ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
|
|
3173
|
+
failedUploads = computed(() => {
|
|
3174
|
+
const active = this.activeUploads();
|
|
3175
|
+
return Array.from(active.values()).filter(upload => upload.stage === 'error');
|
|
3176
|
+
}, ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
|
|
3177
|
+
// Animation states
|
|
3178
|
+
isAnimating = signal(false, ...(ngDevMode ? [{ debugName: "isAnimating" }] : []));
|
|
3235
3179
|
constructor() {
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3180
|
+
console.log('🚀 [FloatingFileUploader] Component initialized');
|
|
3181
|
+
// Set up effect to monitor file manager service's upload queue
|
|
3182
|
+
effect(() => {
|
|
3183
|
+
const uploadQueue = this.fileManagerService.uploadQueue();
|
|
3184
|
+
const activeUploads = this.fileManagerService.activeUploads();
|
|
3185
|
+
const hasUploads = uploadQueue.length > 0 || activeUploads.size > 0 || this.completedFiles().size > 0;
|
|
3186
|
+
console.log('🔄 [FloatingFileUploader] File manager service queue changed:', {
|
|
3187
|
+
queueLength: uploadQueue.length,
|
|
3188
|
+
activeUploadsCount: activeUploads.size,
|
|
3189
|
+
completedFilesCount: this.completedFiles().size,
|
|
3190
|
+
hasUploads,
|
|
3191
|
+
currentVisible: this.isVisible(),
|
|
3192
|
+
queueFiles: uploadQueue
|
|
3193
|
+
});
|
|
3194
|
+
// Show floating uploader when files are added to the file manager service queue
|
|
3195
|
+
// But don't auto-hide when uploads complete - let user manually close
|
|
3196
|
+
if (hasUploads && !this.isVisible()) {
|
|
3197
|
+
console.log('👁️ [FloatingFileUploader] Showing floating uploader due to file manager queue');
|
|
3198
|
+
this.showWithAnimation();
|
|
3245
3199
|
}
|
|
3200
|
+
// Note: Removed auto-hide logic - floating uploader stays visible until manually closed
|
|
3246
3201
|
});
|
|
3247
|
-
//
|
|
3248
|
-
|
|
3249
|
-
|
|
3202
|
+
// Set up effect to track completed uploads
|
|
3203
|
+
effect(() => {
|
|
3204
|
+
const activeUploads = this.fileManagerService.activeUploads();
|
|
3205
|
+
const currentCompleted = this.completedFiles();
|
|
3206
|
+
// Check for newly completed uploads
|
|
3207
|
+
activeUploads.forEach((upload, fileId) => {
|
|
3208
|
+
if (upload.stage === 'complete' && !currentCompleted.has(fileId)) {
|
|
3209
|
+
console.log('✅ [FloatingFileUploader] File completed, adding to local completed files:', fileId);
|
|
3210
|
+
this.addCompletedFile(fileId, upload);
|
|
3211
|
+
}
|
|
3212
|
+
});
|
|
3250
3213
|
});
|
|
3251
3214
|
}
|
|
3252
3215
|
ngOnInit() {
|
|
3253
|
-
//
|
|
3254
|
-
this.
|
|
3255
|
-
|
|
3256
|
-
this.
|
|
3257
|
-
this.disabledSignal.set(this.disabled);
|
|
3258
|
-
this.requiredSignal.set(this.required);
|
|
3259
|
-
this.helperTextSignal.set(this.helperText);
|
|
3260
|
-
this.errorTextSignal.set(this.errorText);
|
|
3261
|
-
this.showPreviewSignal.set(this.showPreview);
|
|
3262
|
-
this.previewWidthSignal.set(this.previewWidth);
|
|
3263
|
-
this.previewHeightSignal.set(this.previewHeight);
|
|
3264
|
-
this.previewBoxModeSignal.set(this.previewBoxMode);
|
|
3265
|
-
this.showFileNameSignal.set(this.showFileName);
|
|
3266
|
-
this.placeholderTextSignal.set(this.placeholderText);
|
|
3267
|
-
this.placeholderIconSignal.set(this.placeholderIcon);
|
|
3268
|
-
this.autoUploadSignal.set(this.autoUpload);
|
|
3269
|
-
this.uploadDataSignal.set(this.uploadData);
|
|
3270
|
-
}
|
|
3271
|
-
ngOnChanges(changes) {
|
|
3272
|
-
// Angular 20: Update signals when @Input() values change
|
|
3273
|
-
if (changes['label'])
|
|
3274
|
-
this.labelSignal.set(this.label);
|
|
3275
|
-
if (changes['accept'])
|
|
3276
|
-
this.acceptSignal.set(this.accept);
|
|
3277
|
-
if (changes['multiple'])
|
|
3278
|
-
this.multipleSignal.set(this.multiple);
|
|
3279
|
-
if (changes['disabled'])
|
|
3280
|
-
this.disabledSignal.set(this.disabled);
|
|
3281
|
-
if (changes['required'])
|
|
3282
|
-
this.requiredSignal.set(this.required);
|
|
3283
|
-
if (changes['helperText'])
|
|
3284
|
-
this.helperTextSignal.set(this.helperText);
|
|
3285
|
-
if (changes['errorText'])
|
|
3286
|
-
this.errorTextSignal.set(this.errorText);
|
|
3287
|
-
if (changes['showPreview'])
|
|
3288
|
-
this.showPreviewSignal.set(this.showPreview);
|
|
3289
|
-
if (changes['previewWidth'])
|
|
3290
|
-
this.previewWidthSignal.set(this.previewWidth);
|
|
3291
|
-
if (changes['previewHeight'])
|
|
3292
|
-
this.previewHeightSignal.set(this.previewHeight);
|
|
3293
|
-
if (changes['previewBoxMode'])
|
|
3294
|
-
this.previewBoxModeSignal.set(this.previewBoxMode);
|
|
3295
|
-
if (changes['showFileName'])
|
|
3296
|
-
this.showFileNameSignal.set(this.showFileName);
|
|
3297
|
-
if (changes['placeholderText'])
|
|
3298
|
-
this.placeholderTextSignal.set(this.placeholderText);
|
|
3299
|
-
if (changes['placeholderIcon'])
|
|
3300
|
-
this.placeholderIconSignal.set(this.placeholderIcon);
|
|
3301
|
-
if (changes['autoUpload'])
|
|
3302
|
-
this.autoUploadSignal.set(this.autoUpload);
|
|
3303
|
-
if (changes['uploadData'])
|
|
3304
|
-
this.uploadDataSignal.set(this.uploadData);
|
|
3216
|
+
// Set up drag and drop listeners
|
|
3217
|
+
this.setupDragAndDrop();
|
|
3218
|
+
// Set up file input change listeners
|
|
3219
|
+
this.setupFileInputListeners();
|
|
3305
3220
|
}
|
|
3306
|
-
|
|
3307
|
-
console.log('
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
console.log('📝 [FileInput] Value is uploaded file ID:', value);
|
|
3311
|
-
this.files.set(null);
|
|
3312
|
-
this.fileNames.set([]);
|
|
3313
|
-
this.clearPreviews();
|
|
3314
|
-
// Fetch file details to get base64 and set preview
|
|
3315
|
-
this.loadFileDetailsFromId(value);
|
|
3316
|
-
}
|
|
3317
|
-
else if (value instanceof FileList) {
|
|
3318
|
-
// Value is a FileList
|
|
3319
|
-
console.log('📝 [FileInput] Value is FileList:', Array.from(value).map(f => f.name));
|
|
3320
|
-
this.files.set(value);
|
|
3321
|
-
this.fileNames.set(Array.from(value).map(f => f.name));
|
|
3322
|
-
this.generatePreviews();
|
|
3323
|
-
}
|
|
3324
|
-
else {
|
|
3325
|
-
// Value is null
|
|
3326
|
-
console.log('📝 [FileInput] Value is null');
|
|
3327
|
-
this.files.set(null);
|
|
3328
|
-
this.fileNames.set([]);
|
|
3329
|
-
this.clearPreviews();
|
|
3330
|
-
}
|
|
3221
|
+
ngOnDestroy() {
|
|
3222
|
+
console.log('🧹 [FloatingFileUploader] Component destroyed');
|
|
3223
|
+
this.removeDragAndDropListeners();
|
|
3224
|
+
this.removeFileInputListeners();
|
|
3331
3225
|
}
|
|
3332
|
-
|
|
3333
|
-
|
|
3226
|
+
/**
|
|
3227
|
+
* Set up drag and drop functionality
|
|
3228
|
+
*/
|
|
3229
|
+
setupDragAndDrop() {
|
|
3230
|
+
document.addEventListener('dragover', this.handleDragOver.bind(this));
|
|
3231
|
+
document.addEventListener('dragleave', this.handleDragLeave.bind(this));
|
|
3232
|
+
document.addEventListener('drop', this.handleDrop.bind(this));
|
|
3334
3233
|
}
|
|
3335
|
-
|
|
3336
|
-
|
|
3234
|
+
/**
|
|
3235
|
+
* Remove drag and drop listeners
|
|
3236
|
+
*/
|
|
3237
|
+
removeDragAndDropListeners() {
|
|
3238
|
+
document.removeEventListener('dragover', this.handleDragOver.bind(this));
|
|
3239
|
+
document.removeEventListener('dragleave', this.handleDragLeave.bind(this));
|
|
3240
|
+
document.removeEventListener('drop', this.handleDrop.bind(this));
|
|
3337
3241
|
}
|
|
3338
|
-
|
|
3339
|
-
|
|
3242
|
+
/**
|
|
3243
|
+
* Set up file input change listeners
|
|
3244
|
+
*/
|
|
3245
|
+
setupFileInputListeners() {
|
|
3246
|
+
// Listen for file input change events globally
|
|
3247
|
+
document.addEventListener('change', this.handleFileInputChange.bind(this));
|
|
3340
3248
|
}
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3249
|
+
/**
|
|
3250
|
+
* Remove file input listeners
|
|
3251
|
+
*/
|
|
3252
|
+
removeFileInputListeners() {
|
|
3253
|
+
document.removeEventListener('change', this.handleFileInputChange.bind(this));
|
|
3345
3254
|
}
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
this.uploadStatus.set('idle');
|
|
3356
|
-
console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
|
|
3357
|
-
this.onChange(selectedFiles);
|
|
3358
|
-
this.fileChange.emit(selectedFiles);
|
|
3359
|
-
this.onTouched();
|
|
3360
|
-
// Auto upload if enabled
|
|
3361
|
-
if (this.autoUploadSignal() && selectedFiles && selectedFiles.length > 0) {
|
|
3362
|
-
if (this.multipleSignal()) {
|
|
3363
|
-
console.log('🚀 [FileInput] Auto upload enabled for multiple files mode:', selectedFiles.length, 'files');
|
|
3364
|
-
this.uploadMultipleFiles(Array.from(selectedFiles));
|
|
3365
|
-
}
|
|
3366
|
-
else {
|
|
3367
|
-
console.log('🚀 [FileInput] Auto upload enabled for single file mode:', selectedFiles[0].name);
|
|
3368
|
-
this.uploadFile(selectedFiles[0]);
|
|
3369
|
-
}
|
|
3370
|
-
}
|
|
3371
|
-
else {
|
|
3372
|
-
console.log('⏸️ [FileInput] Auto upload disabled or no files');
|
|
3373
|
-
}
|
|
3374
|
-
}
|
|
3375
|
-
clearFiles() {
|
|
3376
|
-
console.log('🗑️ [FileInput] clearFiles called');
|
|
3377
|
-
this.files.set(null);
|
|
3378
|
-
this.fileNames.set([]);
|
|
3379
|
-
this.clearPreviews();
|
|
3380
|
-
this.uploadStatus.set('idle');
|
|
3381
|
-
console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
|
|
3382
|
-
this.onChange(null);
|
|
3383
|
-
this.fileChange.emit(null);
|
|
3384
|
-
}
|
|
3385
|
-
uploadFile(file) {
|
|
3386
|
-
console.log('📤 [FileInput] uploadFile called for:', file.name, 'Size:', file.size, 'bytes');
|
|
3387
|
-
// Angular 20: Use PendingTasks for better loading state management
|
|
3388
|
-
// const uploadTask = this.pendingTasks.add(); // TODO: Fix PendingTasks API usage
|
|
3389
|
-
// console.log('⏳ [FileInput] Pending task added for upload tracking');
|
|
3390
|
-
// Set upload status to 'start' before starting upload
|
|
3391
|
-
this.uploadStatus.set('start');
|
|
3392
|
-
console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
|
|
3393
|
-
this.isUploading.set(true);
|
|
3394
|
-
this.uploadProgress.set(0);
|
|
3395
|
-
this.uploadProgressChange.emit(0);
|
|
3396
|
-
console.log('📊 [FileInput] Upload progress initialized to 0%');
|
|
3397
|
-
// Make form control invalid during upload - this prevents form submission
|
|
3398
|
-
this.onChange(null);
|
|
3399
|
-
console.log('🚫 [FileInput] Form control value set to null to prevent submission');
|
|
3400
|
-
// Show initial progress notification with spinner (persistent - no auto-dismiss)
|
|
3401
|
-
const notificationId = this.notificationService.showProgress('🔄 Preparing file upload...', 0, { duration: 0 });
|
|
3402
|
-
this.uploadNotificationId.set(notificationId);
|
|
3403
|
-
console.log('🔔 [FileInput] Progress notification started with ID:', notificationId);
|
|
3404
|
-
this.fileManagerService.uploadFile(file, this.uploadDataSignal(), (progress) => {
|
|
3405
|
-
// Real progress callback from file manager service
|
|
3406
|
-
this.uploadProgress.set(progress);
|
|
3407
|
-
this.uploadProgressChange.emit(progress);
|
|
3408
|
-
console.log('📈 [FileInput] Upload progress:', Math.round(progress) + '%');
|
|
3409
|
-
// Set upload status to 'uploading' when progress starts
|
|
3410
|
-
if (this.uploadStatus() === 'start') {
|
|
3411
|
-
this.uploadStatus.set('uploading');
|
|
3412
|
-
console.log('🔄 [FileInput] Upload status changed to:', this.uploadStatus());
|
|
3413
|
-
}
|
|
3414
|
-
// Update progress notification with spinner
|
|
3415
|
-
const notificationId = this.uploadNotificationId();
|
|
3416
|
-
if (notificationId) {
|
|
3417
|
-
let progressMessage = '';
|
|
3418
|
-
if (progress < 10) {
|
|
3419
|
-
progressMessage = '🔄 Starting upload...';
|
|
3420
|
-
}
|
|
3421
|
-
else if (progress < 25) {
|
|
3422
|
-
progressMessage = '🔄 Uploading file...';
|
|
3423
|
-
}
|
|
3424
|
-
else if (progress < 50) {
|
|
3425
|
-
progressMessage = '🔄 Upload in progress...';
|
|
3426
|
-
}
|
|
3427
|
-
else if (progress < 75) {
|
|
3428
|
-
progressMessage = '🔄 Almost done...';
|
|
3429
|
-
}
|
|
3430
|
-
else if (progress < 95) {
|
|
3431
|
-
progressMessage = '🔄 Finishing upload...';
|
|
3432
|
-
}
|
|
3433
|
-
else {
|
|
3434
|
-
progressMessage = '🔄 Finalizing...';
|
|
3435
|
-
}
|
|
3436
|
-
this.notificationService.updateProgress(notificationId, progress, progressMessage);
|
|
3437
|
-
}
|
|
3438
|
-
}).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
3439
|
-
next: (response) => {
|
|
3440
|
-
console.log('🎉 [FileInput] Upload SUCCESS - Response received:', response);
|
|
3441
|
-
// Angular 20: Complete the pending task
|
|
3442
|
-
// this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
|
|
3443
|
-
// console.log('✅ [FileInput] Pending task completed for successful upload');
|
|
3444
|
-
// Set upload status to 'success'
|
|
3445
|
-
this.uploadStatus.set('success');
|
|
3446
|
-
console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
|
|
3447
|
-
// Complete the progress
|
|
3448
|
-
this.uploadProgress.set(100);
|
|
3449
|
-
this.uploadProgressChange.emit(100);
|
|
3450
|
-
console.log('📊 [FileInput] Upload progress completed: 100%');
|
|
3451
|
-
// Update progress notification to complete
|
|
3452
|
-
const notificationId = this.uploadNotificationId();
|
|
3453
|
-
if (notificationId) {
|
|
3454
|
-
this.notificationService.remove(notificationId);
|
|
3455
|
-
console.log('🔔 [FileInput] Progress notification removed');
|
|
3456
|
-
}
|
|
3457
|
-
this.notificationService.success('✅ File uploaded successfully!', { duration: 0 });
|
|
3458
|
-
this.uploadNotificationId.set(null);
|
|
3459
|
-
// Extract ID from CoreFileManagerInsertUpdateResponse
|
|
3460
|
-
const uploadedId = response?.data?.core_file_manager?.[0]?.cyfm_id;
|
|
3461
|
-
if (uploadedId) {
|
|
3462
|
-
console.log('✅ [FileInput] File uploaded successfully with ID:', uploadedId);
|
|
3463
|
-
// Set the uploaded ID as the form control value
|
|
3464
|
-
this.onChange(uploadedId);
|
|
3465
|
-
console.log('📝 [FileInput] Form control value set to uploaded ID:', uploadedId);
|
|
3466
|
-
// Only emit individual uploadSuccess if not in multiple upload mode
|
|
3467
|
-
if (!this.isMultipleUploadMode()) {
|
|
3468
|
-
this.uploadSuccess.emit(uploadedId);
|
|
3469
|
-
console.log('📝 [FileInput] Upload success event emitted with file ID:', uploadedId);
|
|
3470
|
-
}
|
|
3471
|
-
else {
|
|
3472
|
-
console.log('📝 [FileInput] Individual upload success suppressed (multiple upload mode) - file ID:', uploadedId);
|
|
3473
|
-
}
|
|
3474
|
-
}
|
|
3475
|
-
else {
|
|
3476
|
-
console.error('❌ [FileInput] Upload successful but no ID returned:', response);
|
|
3477
|
-
this.uploadError.emit('Upload successful but no ID returned');
|
|
3478
|
-
}
|
|
3479
|
-
this.isUploading.set(false);
|
|
3480
|
-
console.log('🔄 [FileInput] isUploading set to false');
|
|
3481
|
-
},
|
|
3482
|
-
error: (error) => {
|
|
3483
|
-
console.error('💥 [FileInput] Upload FAILED:', error);
|
|
3484
|
-
// Angular 20: Complete the pending task even on error
|
|
3485
|
-
// this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
|
|
3486
|
-
// console.log('❌ [FileInput] Pending task completed for failed upload');
|
|
3487
|
-
// Set upload status to 'error' and remove upload validation error
|
|
3488
|
-
this.uploadStatus.set('error');
|
|
3489
|
-
console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
|
|
3490
|
-
// Remove progress notification and show error
|
|
3491
|
-
const notificationId = this.uploadNotificationId();
|
|
3492
|
-
if (notificationId) {
|
|
3493
|
-
this.notificationService.remove(notificationId);
|
|
3494
|
-
console.log('🔔 [FileInput] Progress notification removed due to error');
|
|
3495
|
-
}
|
|
3496
|
-
this.notificationService.error(`❌ File upload failed: ${error.message || error.error?.message || 'Unknown error occurred'}`, { duration: 0 });
|
|
3497
|
-
this.uploadNotificationId.set(null);
|
|
3498
|
-
this.uploadError.emit(error.message || error.error?.message || 'Upload failed');
|
|
3499
|
-
this.isUploading.set(false);
|
|
3500
|
-
this.uploadProgress.set(0);
|
|
3501
|
-
this.uploadProgressChange.emit(0);
|
|
3502
|
-
console.log('🔄 [FileInput] Upload state reset - isUploading: false, progress: 0%');
|
|
3503
|
-
}
|
|
3504
|
-
});
|
|
3505
|
-
}
|
|
3506
|
-
/**
|
|
3507
|
-
* Upload multiple files with group ID support
|
|
3508
|
-
* FLOW: 1) Generate group ID first, 2) Upload all files with same group ID, 3) Emit group ID on completion
|
|
3509
|
-
*/
|
|
3510
|
-
uploadMultipleFiles(files) {
|
|
3511
|
-
console.log('📤 [FileInput] uploadMultipleFiles called for:', files.length, 'files');
|
|
3512
|
-
console.log('🔄 [FileInput] STEP 1: Generate group ID before starting any file uploads');
|
|
3513
|
-
// Set multiple upload mode flag
|
|
3514
|
-
this.isMultipleUploadMode.set(true);
|
|
3515
|
-
// Set upload status to 'start' before starting upload
|
|
3516
|
-
this.uploadStatus.set('start');
|
|
3517
|
-
this.isUploading.set(true);
|
|
3518
|
-
this.uploadProgress.set(0);
|
|
3519
|
-
this.uploadProgressChange.emit(0);
|
|
3520
|
-
// Make form control invalid during upload
|
|
3521
|
-
this.onChange(null);
|
|
3522
|
-
// Show initial progress notification
|
|
3523
|
-
const notificationId = this.notificationService.showProgress('🔄 Preparing multiple file upload...', 0, { duration: 0 });
|
|
3524
|
-
this.uploadNotificationId.set(notificationId);
|
|
3525
|
-
// STEP 1: Generate or get group ID BEFORE starting any file uploads
|
|
3526
|
-
const existingGroupId = this.uploadDataSignal().groupId;
|
|
3527
|
-
if (existingGroupId) {
|
|
3528
|
-
console.log('🆔 [FileInput] STEP 1 COMPLETE: Using existing group ID:', existingGroupId);
|
|
3529
|
-
console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', existingGroupId);
|
|
3530
|
-
this.groupId.set(existingGroupId);
|
|
3531
|
-
this.startMulti(files, existingGroupId);
|
|
3532
|
-
}
|
|
3533
|
-
else {
|
|
3534
|
-
console.log('🆔 [FileInput] No existing group ID, generating new one...');
|
|
3535
|
-
// Generate group ID BEFORE starting any file uploads
|
|
3536
|
-
this.fileManagerService.generateObjectId().subscribe({
|
|
3537
|
-
next: (response) => {
|
|
3538
|
-
const newGroupId = response.data?.objectId;
|
|
3539
|
-
console.log('🆔 [FileInput] STEP 1 COMPLETE: Generated new group ID:', newGroupId);
|
|
3540
|
-
console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', newGroupId);
|
|
3541
|
-
this.groupId.set(newGroupId);
|
|
3542
|
-
this.startMulti(files, newGroupId);
|
|
3543
|
-
},
|
|
3544
|
-
error: (error) => {
|
|
3545
|
-
console.error('❌ [FileInput] Failed to generate group ID:', error);
|
|
3546
|
-
this.uploadError.emit('Failed to generate group ID');
|
|
3547
|
-
this.isUploading.set(false);
|
|
3548
|
-
this.uploadStatus.set('error');
|
|
3549
|
-
const notificationId = this.uploadNotificationId();
|
|
3550
|
-
if (notificationId) {
|
|
3551
|
-
this.notificationService.remove(notificationId);
|
|
3552
|
-
}
|
|
3553
|
-
this.notificationService.error('❌ Failed to generate group ID for multiple file upload', { duration: 0 });
|
|
3554
|
-
this.uploadNotificationId.set(null);
|
|
3555
|
-
}
|
|
3556
|
-
});
|
|
3557
|
-
}
|
|
3558
|
-
}
|
|
3559
|
-
/**
|
|
3560
|
-
* Start uploading multiple files with the provided group ID
|
|
3561
|
-
* All files will be uploaded with the SAME group ID that was generated before this method
|
|
3562
|
-
*/
|
|
3563
|
-
startMulti(files, groupId) {
|
|
3564
|
-
console.log('🚀 [FileInput] STEP 2: Starting upload for', files.length, 'files with group ID:', groupId);
|
|
3565
|
-
console.log('📋 [FileInput] All files will use the same group ID:', groupId);
|
|
3566
|
-
let completedUploads = 0;
|
|
3567
|
-
let failedUploads = 0;
|
|
3568
|
-
const totalFiles = files.length;
|
|
3569
|
-
// IMPORTANT: All files use the SAME group ID that was generated before starting uploads
|
|
3570
|
-
const uploadDataWithGroupId = {
|
|
3571
|
-
...this.uploadDataSignal(),
|
|
3572
|
-
groupId: groupId
|
|
3573
|
-
};
|
|
3574
|
-
files.forEach((file, index) => {
|
|
3575
|
-
console.log(`📤 [FileInput] Uploading file ${index + 1}/${totalFiles}: "${file.name}" with group ID: ${groupId}`);
|
|
3576
|
-
this.fileManagerService.uploadFile(file, uploadDataWithGroupId, (progress) => {
|
|
3577
|
-
// Calculate overall progress
|
|
3578
|
-
const fileProgress = progress / totalFiles;
|
|
3579
|
-
const overallProgress = ((completedUploads * 100) + fileProgress) / totalFiles;
|
|
3580
|
-
this.uploadProgress.set(overallProgress);
|
|
3581
|
-
this.uploadProgressChange.emit(overallProgress);
|
|
3582
|
-
// Update progress notification
|
|
3583
|
-
const notificationId = this.uploadNotificationId();
|
|
3584
|
-
if (notificationId) {
|
|
3585
|
-
const progressMessage = `🔄 Uploading file ${index + 1} of ${totalFiles}...`;
|
|
3586
|
-
this.notificationService.updateProgress(notificationId, overallProgress, progressMessage);
|
|
3587
|
-
}
|
|
3588
|
-
}).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
3589
|
-
next: (response) => {
|
|
3590
|
-
completedUploads++;
|
|
3591
|
-
console.log(`✅ [FileInput] File ${index + 1}/${totalFiles} uploaded successfully`);
|
|
3592
|
-
// Check if all files are completed
|
|
3593
|
-
if (completedUploads + failedUploads === totalFiles) {
|
|
3594
|
-
this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
|
|
3595
|
-
}
|
|
3596
|
-
},
|
|
3597
|
-
error: (error) => {
|
|
3598
|
-
failedUploads++;
|
|
3599
|
-
console.error(`❌ [FileInput] File ${index + 1}/${totalFiles} upload failed:`, error);
|
|
3600
|
-
// Check if all files are completed
|
|
3601
|
-
if (completedUploads + failedUploads === totalFiles) {
|
|
3602
|
-
this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
|
|
3603
|
-
}
|
|
3604
|
-
}
|
|
3605
|
-
});
|
|
3606
|
-
});
|
|
3607
|
-
}
|
|
3608
|
-
/**
|
|
3609
|
-
* Handle completion of multiple file upload
|
|
3610
|
-
*/
|
|
3611
|
-
handleMultipleUploadComplete(completed, failed, total, groupId) {
|
|
3612
|
-
console.log(`📊 [FileInput] Multiple upload complete: ${completed}/${total} successful, ${failed} failed`);
|
|
3613
|
-
this.isUploading.set(false);
|
|
3614
|
-
this.uploadProgress.set(100);
|
|
3615
|
-
this.uploadProgressChange.emit(100);
|
|
3616
|
-
// Remove progress notification
|
|
3617
|
-
const notificationId = this.uploadNotificationId();
|
|
3618
|
-
if (notificationId) {
|
|
3619
|
-
this.notificationService.remove(notificationId);
|
|
3620
|
-
}
|
|
3621
|
-
this.uploadNotificationId.set(null);
|
|
3622
|
-
if (failed === 0) {
|
|
3623
|
-
// All files uploaded successfully
|
|
3624
|
-
this.uploadStatus.set('success');
|
|
3625
|
-
this.notificationService.success(`✅ All ${total} files uploaded successfully!`, { duration: 0 });
|
|
3626
|
-
// STEP 3: For multiple file upload, emit the group ID (not individual file IDs)
|
|
3627
|
-
this.onChange(groupId);
|
|
3628
|
-
this.uploadSuccess.emit(groupId);
|
|
3629
|
-
console.log('📝 [FileInput] STEP 3 COMPLETE: Form control value set to group ID:', groupId);
|
|
3630
|
-
console.log('✅ [FileInput] Multiple upload SUCCESS - Group ID emitted:', groupId);
|
|
3631
|
-
}
|
|
3632
|
-
else if (completed > 0) {
|
|
3633
|
-
// Some files uploaded successfully
|
|
3634
|
-
this.uploadStatus.set('error');
|
|
3635
|
-
this.notificationService.warning(`⚠️ ${completed}/${total} files uploaded. ${failed} failed.`, { duration: 0 });
|
|
3636
|
-
this.uploadError.emit(`${failed} out of ${total} files failed to upload`);
|
|
3637
|
-
}
|
|
3638
|
-
else {
|
|
3639
|
-
// All files failed
|
|
3640
|
-
this.uploadStatus.set('error');
|
|
3641
|
-
this.notificationService.error(`❌ All ${total} files failed to upload.`, { duration: 0 });
|
|
3642
|
-
this.uploadError.emit('All files failed to upload');
|
|
3643
|
-
}
|
|
3644
|
-
// Reset multiple upload mode flag
|
|
3645
|
-
this.isMultipleUploadMode.set(false);
|
|
3646
|
-
}
|
|
3647
|
-
generatePreviews() {
|
|
3648
|
-
// Clear existing previews
|
|
3649
|
-
this.clearPreviews();
|
|
3650
|
-
if (!this.showPreviewSignal() || !this.files()) {
|
|
3651
|
-
return;
|
|
3652
|
-
}
|
|
3653
|
-
Array.from(this.files()).forEach(file => {
|
|
3654
|
-
if (this.isImageFile(file)) {
|
|
3655
|
-
const reader = new FileReader();
|
|
3656
|
-
reader.onload = (e) => {
|
|
3657
|
-
if (e.target?.result) {
|
|
3658
|
-
this.previewUrls.update(urls => [...urls, e.target.result]);
|
|
3659
|
-
}
|
|
3660
|
-
};
|
|
3661
|
-
reader.readAsDataURL(file);
|
|
3662
|
-
}
|
|
3663
|
-
});
|
|
3664
|
-
}
|
|
3665
|
-
clearPreviews() {
|
|
3666
|
-
// Revoke object URLs to prevent memory leaks
|
|
3667
|
-
this.previewUrls().forEach(url => {
|
|
3668
|
-
if (url.startsWith('blob:')) {
|
|
3669
|
-
URL.revokeObjectURL(url);
|
|
3670
|
-
}
|
|
3671
|
-
});
|
|
3672
|
-
this.previewUrls.set([]);
|
|
3673
|
-
}
|
|
3674
|
-
isImageFile(file) {
|
|
3675
|
-
return file.type.startsWith('image/');
|
|
3676
|
-
}
|
|
3677
|
-
loadFileDetailsFromId(fileId) {
|
|
3678
|
-
console.log('🔍 [FileInput] Loading file details for ID:', fileId);
|
|
3679
|
-
if (!fileId)
|
|
3680
|
-
return;
|
|
3681
|
-
this.fileManagerService?.getFileDetails({ cyfm_id: fileId })?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
3682
|
-
next: (fileDetails) => {
|
|
3683
|
-
console.log('📋 [FileInput] File details received:', fileDetails);
|
|
3684
|
-
if (fileDetails?.data?.length) {
|
|
3685
|
-
const fileData = fileDetails.data[0];
|
|
3686
|
-
console.log('📁 [FileInput] File data:', fileData);
|
|
3687
|
-
// Set file name from the details
|
|
3688
|
-
if (fileData.cyfm_name) {
|
|
3689
|
-
this.fileNames.set([fileData.cyfm_name]);
|
|
3690
|
-
console.log('📝 [FileInput] File name set:', fileData.cyfm_name);
|
|
3691
|
-
}
|
|
3692
|
-
// If it's an image and we have base64 data, set preview
|
|
3693
|
-
if (this.showPreviewSignal() && fileData.cyfm_file_base64) {
|
|
3694
|
-
// Check if it's an image file based on file name or type
|
|
3695
|
-
const isImage = this.isImageFileFromName(fileData.cyfm_name || '') ||
|
|
3696
|
-
this.isImageFileFromType(fileData.cyfm_type || '');
|
|
3697
|
-
if (isImage) {
|
|
3698
|
-
// Add data URL prefix if not already present
|
|
3699
|
-
let base64Data = fileData.cyfm_file_base64;
|
|
3700
|
-
if (!base64Data.startsWith('data:')) {
|
|
3701
|
-
const mimeType = fileData.cyfm_type || 'image/jpeg';
|
|
3702
|
-
base64Data = `data:${mimeType};base64,${base64Data}`;
|
|
3703
|
-
}
|
|
3704
|
-
this.previewUrls.set([base64Data]);
|
|
3705
|
-
console.log('🖼️ [FileInput] Preview set from base64 data');
|
|
3706
|
-
}
|
|
3707
|
-
}
|
|
3708
|
-
}
|
|
3709
|
-
else {
|
|
3710
|
-
console.warn('⚠️ [FileInput] No file data found for ID:', fileId);
|
|
3711
|
-
}
|
|
3712
|
-
},
|
|
3713
|
-
error: (error) => {
|
|
3714
|
-
console.error('❌ [FileInput] Error loading file details:', error);
|
|
3715
|
-
this.notificationService.error(`Failed to load file details: ${error.message || 'Unknown error'}`, { duration: 0 });
|
|
3716
|
-
}
|
|
3255
|
+
/**
|
|
3256
|
+
* Handle file input change events
|
|
3257
|
+
*/
|
|
3258
|
+
handleFileInputChange(event) {
|
|
3259
|
+
const target = event.target;
|
|
3260
|
+
console.log('🔍 [FloatingFileUploader] File input change event detected:', {
|
|
3261
|
+
type: target.type,
|
|
3262
|
+
filesLength: target.files?.length || 0,
|
|
3263
|
+
element: target
|
|
3717
3264
|
});
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
}
|
|
3726
|
-
isImageFileFromType(fileType) {
|
|
3727
|
-
if (!fileType)
|
|
3728
|
-
return false;
|
|
3729
|
-
return fileType.startsWith('image/');
|
|
3730
|
-
}
|
|
3731
|
-
removePreview(index) {
|
|
3732
|
-
const currentFiles = this.files();
|
|
3733
|
-
const currentUrls = this.previewUrls();
|
|
3734
|
-
if (currentFiles && currentFiles.length > index) {
|
|
3735
|
-
// Handle FileList case - remove file from FileList
|
|
3736
|
-
const dt = new DataTransfer();
|
|
3737
|
-
Array.from(currentFiles).forEach((file, i) => {
|
|
3738
|
-
if (i !== index) {
|
|
3739
|
-
dt.items.add(file);
|
|
3740
|
-
}
|
|
3741
|
-
});
|
|
3742
|
-
const newFiles = dt.files;
|
|
3743
|
-
this.files.set(newFiles);
|
|
3744
|
-
this.fileNames.set(Array.from(newFiles).map(f => f.name));
|
|
3745
|
-
// Remove the preview URL
|
|
3746
|
-
if (currentUrls[index] && currentUrls[index].startsWith('blob:')) {
|
|
3747
|
-
URL.revokeObjectURL(currentUrls[index]);
|
|
3265
|
+
// Check if this is a file input with files
|
|
3266
|
+
if (target.type === 'file' && target.files && target.files.length > 0) {
|
|
3267
|
+
console.log('📁 [FloatingFileUploader] File input change detected:', target.files.length, 'files');
|
|
3268
|
+
// Check if the input has a data-user-id attribute for user context
|
|
3269
|
+
const userId = target.getAttribute('data-user-id');
|
|
3270
|
+
if (userId && userId !== this.currentUserId()) {
|
|
3271
|
+
this.setCurrentUserId(userId);
|
|
3748
3272
|
}
|
|
3749
|
-
|
|
3750
|
-
this.
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
else if (currentUrls.length > index) {
|
|
3754
|
-
// Handle uploaded file ID case - clear the preview and set control value to null
|
|
3755
|
-
console.log('🗑️ [FileInput] Removing preview for uploaded file ID');
|
|
3756
|
-
// Clear preview
|
|
3757
|
-
this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
|
|
3758
|
-
this.fileNames.set([]);
|
|
3759
|
-
// Set control value to null since we're removing the uploaded file
|
|
3760
|
-
this.onChange(null);
|
|
3761
|
-
this.fileChange.emit(null);
|
|
3762
|
-
}
|
|
3763
|
-
}
|
|
3764
|
-
ngOnDestroy() {
|
|
3765
|
-
// Clean up preview URLs to prevent memory leaks
|
|
3766
|
-
this.clearPreviews();
|
|
3767
|
-
// Clean up any active upload notification
|
|
3768
|
-
const notificationId = this.uploadNotificationId();
|
|
3769
|
-
if (notificationId) {
|
|
3770
|
-
this.notificationService.remove(notificationId);
|
|
3771
|
-
this.uploadNotificationId.set(null);
|
|
3772
|
-
}
|
|
3773
|
-
}
|
|
3774
|
-
triggerFileSelect() {
|
|
3775
|
-
const fileInput = document.getElementById('cide-file-input-' + this.id());
|
|
3776
|
-
if (fileInput && !this.disabledSignal()) {
|
|
3777
|
-
fileInput.click();
|
|
3273
|
+
// Handle the files
|
|
3274
|
+
this.handleFiles(Array.from(target.files));
|
|
3275
|
+
// Reset the input to allow selecting the same files again
|
|
3276
|
+
target.value = '';
|
|
3778
3277
|
}
|
|
3779
3278
|
}
|
|
3780
|
-
|
|
3781
|
-
|
|
3279
|
+
/**
|
|
3280
|
+
* Handle drag over event
|
|
3281
|
+
*/
|
|
3282
|
+
handleDragOver(event) {
|
|
3782
3283
|
event.preventDefault();
|
|
3783
3284
|
event.stopPropagation();
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3285
|
+
// Show floating uploader when files are dragged over
|
|
3286
|
+
if (event.dataTransfer?.types.includes('Files')) {
|
|
3287
|
+
this.showWithAnimation();
|
|
3787
3288
|
}
|
|
3788
3289
|
}
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
console.log('🔄 [FileInput] Drag leave detected');
|
|
3794
|
-
}
|
|
3795
|
-
onDragEnter(event) {
|
|
3290
|
+
/**
|
|
3291
|
+
* Handle drag leave event
|
|
3292
|
+
*/
|
|
3293
|
+
handleDragLeave(event) {
|
|
3796
3294
|
event.preventDefault();
|
|
3797
3295
|
event.stopPropagation();
|
|
3798
|
-
if
|
|
3799
|
-
|
|
3800
|
-
|
|
3296
|
+
// Only hide if leaving the entire document
|
|
3297
|
+
if (!event.relatedTarget || event.relatedTarget === document.body) {
|
|
3298
|
+
this.updateVisibility();
|
|
3801
3299
|
}
|
|
3802
3300
|
}
|
|
3803
|
-
|
|
3301
|
+
/**
|
|
3302
|
+
* Handle drop event
|
|
3303
|
+
*/
|
|
3304
|
+
handleDrop(event) {
|
|
3804
3305
|
event.preventDefault();
|
|
3805
3306
|
event.stopPropagation();
|
|
3806
|
-
this.isDragOver.set(false);
|
|
3807
|
-
if (this.disabledSignal()) {
|
|
3808
|
-
console.log('⏸️ [FileInput] Drop ignored - component is disabled');
|
|
3809
|
-
return;
|
|
3810
|
-
}
|
|
3811
3307
|
const files = event.dataTransfer?.files;
|
|
3812
3308
|
if (files && files.length > 0) {
|
|
3813
|
-
|
|
3814
|
-
// Validate file types if accept is specified
|
|
3815
|
-
if (this.acceptSignal() && !this.validateFileTypes(files)) {
|
|
3816
|
-
console.log('❌ [FileInput] Invalid file types dropped');
|
|
3817
|
-
this.notificationService.error('❌ Invalid file type. Please select files of the correct type.', { duration: 0 });
|
|
3818
|
-
return;
|
|
3819
|
-
}
|
|
3820
|
-
// Handle single vs multiple files
|
|
3821
|
-
if (!this.multipleSignal() && files.length > 1) {
|
|
3822
|
-
console.log('⚠️ [FileInput] Multiple files dropped but multiple is disabled');
|
|
3823
|
-
this.notificationService.warning('⚠️ Only one file is allowed. Using the first file.', { duration: 0 });
|
|
3824
|
-
// Create a new FileList with only the first file
|
|
3825
|
-
const dt = new DataTransfer();
|
|
3826
|
-
dt.items.add(files[0]);
|
|
3827
|
-
this.handleFileSelection(dt.files);
|
|
3828
|
-
}
|
|
3829
|
-
else {
|
|
3830
|
-
this.handleFileSelection(files);
|
|
3831
|
-
}
|
|
3309
|
+
this.handleFiles(Array.from(files));
|
|
3832
3310
|
}
|
|
3833
3311
|
}
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
// Extension-based validation
|
|
3842
|
-
return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
|
|
3843
|
-
}
|
|
3844
|
-
else if (acceptType.includes('/')) {
|
|
3845
|
-
// MIME type validation
|
|
3846
|
-
return file.type === acceptType || file.type.startsWith(acceptType.replace('*', ''));
|
|
3847
|
-
}
|
|
3848
|
-
return false;
|
|
3849
|
-
});
|
|
3850
|
-
});
|
|
3312
|
+
/**
|
|
3313
|
+
* Handle files from drag and drop or file input
|
|
3314
|
+
*/
|
|
3315
|
+
handleFiles(files) {
|
|
3316
|
+
console.log('📁 [FloatingFileUploader] Handling files:', files.length);
|
|
3317
|
+
// Use handleExternalFiles to process the files
|
|
3318
|
+
this.handleExternalFiles(files, this.currentUserId());
|
|
3851
3319
|
}
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
//
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3320
|
+
/**
|
|
3321
|
+
* Update visibility - simplified for notification only
|
|
3322
|
+
*/
|
|
3323
|
+
updateVisibility() {
|
|
3324
|
+
// This is just a notification component now
|
|
3325
|
+
// The actual uploads are handled by the global uploader
|
|
3326
|
+
}
|
|
3327
|
+
/**
|
|
3328
|
+
* Show with animation
|
|
3329
|
+
*/
|
|
3330
|
+
showWithAnimation() {
|
|
3331
|
+
console.log('🎬 [FloatingFileUploader] showWithAnimation called - setting isVisible to true');
|
|
3332
|
+
this.isAnimating.set(true);
|
|
3333
|
+
this.isVisible.set(true);
|
|
3334
|
+
// Remove animation class after animation completes
|
|
3335
|
+
setTimeout(() => {
|
|
3336
|
+
this.isAnimating.set(false);
|
|
3337
|
+
console.log('🎬 [FloatingFileUploader] Animation completed, isVisible:', this.isVisible());
|
|
3338
|
+
}, 300);
|
|
3339
|
+
}
|
|
3340
|
+
/**
|
|
3341
|
+
* Hide with animation
|
|
3342
|
+
*/
|
|
3343
|
+
hideWithAnimation() {
|
|
3344
|
+
this.isAnimating.set(true);
|
|
3345
|
+
// Wait for animation to complete before hiding
|
|
3346
|
+
setTimeout(() => {
|
|
3347
|
+
this.isVisible.set(false);
|
|
3348
|
+
this.isAnimating.set(false);
|
|
3349
|
+
}, 300);
|
|
3350
|
+
}
|
|
3351
|
+
/**
|
|
3352
|
+
* Toggle minimize state
|
|
3353
|
+
*/
|
|
3354
|
+
toggleMinimize() {
|
|
3355
|
+
this.isMinimized.set(!this.isMinimized());
|
|
3356
|
+
}
|
|
3357
|
+
/**
|
|
3358
|
+
* Close the floating uploader
|
|
3359
|
+
*/
|
|
3360
|
+
close() {
|
|
3361
|
+
// Clear completed files when closing
|
|
3362
|
+
this.clearCompletedFiles();
|
|
3363
|
+
this.hideWithAnimation();
|
|
3364
|
+
}
|
|
3365
|
+
/**
|
|
3366
|
+
* Get upload summary text
|
|
3367
|
+
*/
|
|
3368
|
+
getUploadSummary() {
|
|
3369
|
+
const queueLength = this.uploadQueue().length;
|
|
3370
|
+
const activeCount = this.activeUploads().size;
|
|
3371
|
+
const completed = this.completedUploads().length;
|
|
3372
|
+
const failed = this.failedUploads().length;
|
|
3373
|
+
if (activeCount > 0) {
|
|
3374
|
+
return `${activeCount} uploading`;
|
|
3873
3375
|
}
|
|
3874
|
-
else {
|
|
3875
|
-
|
|
3376
|
+
else if (completed > 0 && failed === 0) {
|
|
3377
|
+
return `${completed} completed`;
|
|
3876
3378
|
}
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3379
|
+
else if (failed > 0) {
|
|
3380
|
+
return `${completed} completed, ${failed} failed`;
|
|
3381
|
+
}
|
|
3382
|
+
else if (queueLength > 0) {
|
|
3383
|
+
return `${queueLength} pending`;
|
|
3384
|
+
}
|
|
3385
|
+
return 'No uploads';
|
|
3880
3386
|
}
|
|
3881
3387
|
/**
|
|
3882
|
-
*
|
|
3883
|
-
* @returns Properly typed upload data
|
|
3388
|
+
* Get overall progress percentage
|
|
3884
3389
|
*/
|
|
3885
|
-
|
|
3886
|
-
|
|
3390
|
+
getOverallProgress() {
|
|
3391
|
+
const activeUploads = this.activeUploads();
|
|
3392
|
+
if (activeUploads.size === 0)
|
|
3393
|
+
return 0;
|
|
3394
|
+
const totalProgress = Array.from(activeUploads.values()).reduce((sum, upload) => sum + upload.percentage, 0);
|
|
3395
|
+
return Math.round(totalProgress / activeUploads.size);
|
|
3887
3396
|
}
|
|
3888
3397
|
/**
|
|
3889
|
-
*
|
|
3890
|
-
* @param data Partial upload data to merge with existing data
|
|
3398
|
+
* Get status icon based on upload stage
|
|
3891
3399
|
*/
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
getCurrentState() {
|
|
3900
|
-
return {
|
|
3901
|
-
id: this.id(),
|
|
3902
|
-
label: this.labelSignal(),
|
|
3903
|
-
required: this.requiredSignal(),
|
|
3904
|
-
disabled: this.disabledSignal(),
|
|
3905
|
-
accept: this.acceptSignal(),
|
|
3906
|
-
multiple: this.multipleSignal(),
|
|
3907
|
-
showPreview: this.showPreviewSignal(),
|
|
3908
|
-
autoUpload: this.autoUploadSignal(),
|
|
3909
|
-
uploadStatus: this.uploadStatus(),
|
|
3910
|
-
isUploading: this.isUploading(),
|
|
3911
|
-
uploadProgress: this.uploadProgress(),
|
|
3912
|
-
files: this.files() ? Array.from(this.files()).map(f => ({ name: f.name, size: f.size, type: f.type })) : null,
|
|
3913
|
-
fileNames: this.fileNames(),
|
|
3914
|
-
previewUrls: this.previewUrls().length,
|
|
3915
|
-
helperText: this.helperTextSignal(),
|
|
3916
|
-
errorText: this.errorTextSignal(),
|
|
3917
|
-
placeholderText: this.placeholderTextSignal(),
|
|
3918
|
-
placeholderIcon: this.placeholderIconSignal(),
|
|
3919
|
-
previewWidth: this.previewWidthSignal(),
|
|
3920
|
-
previewHeight: this.previewHeightSignal(),
|
|
3921
|
-
previewBoxMode: this.previewBoxModeSignal(),
|
|
3922
|
-
showFileName: this.showFileNameSignal(),
|
|
3923
|
-
uploadData: this.uploadDataSignal()
|
|
3924
|
-
};
|
|
3925
|
-
}
|
|
3926
|
-
async getControlData() {
|
|
3927
|
-
console.log('🔍 [FileInput] getControlData called');
|
|
3928
|
-
const cide_element_data = await this.elementService?.getElementData({ sype_key: this.id() });
|
|
3929
|
-
if (cide_element_data) {
|
|
3930
|
-
console.log('📋 [FileInput] Element data loaded:', cide_element_data);
|
|
3931
|
-
// Note: Since we're using input signals, we can't directly set their values
|
|
3932
|
-
// This method would need to be refactored to work with the new signal-based approach
|
|
3933
|
-
// For now, we'll log the data and trigger validation
|
|
3934
|
-
console.log('✅ [FileInput] Control data received from element service');
|
|
3935
|
-
console.log('⚠️ [FileInput] Note: Input signals cannot be modified after component initialization');
|
|
3936
|
-
// Trigger validation update
|
|
3937
|
-
this.onValidatorChange();
|
|
3938
|
-
}
|
|
3939
|
-
else {
|
|
3940
|
-
console.log('⚠️ [FileInput] No element data found for key:', this.id());
|
|
3400
|
+
getStatusIcon(stage) {
|
|
3401
|
+
switch (stage) {
|
|
3402
|
+
case 'reading': return 'schedule';
|
|
3403
|
+
case 'uploading': return 'cloud_upload';
|
|
3404
|
+
case 'complete': return 'check_circle';
|
|
3405
|
+
case 'error': return 'error';
|
|
3406
|
+
default: return 'help';
|
|
3941
3407
|
}
|
|
3942
3408
|
}
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3409
|
+
/**
|
|
3410
|
+
* Get status class based on upload stage
|
|
3411
|
+
*/
|
|
3412
|
+
getStatusClass(stage) {
|
|
3413
|
+
switch (stage) {
|
|
3414
|
+
case 'reading': return 'status-pending';
|
|
3415
|
+
case 'uploading': return 'status-uploading';
|
|
3416
|
+
case 'complete': return 'status-completed';
|
|
3417
|
+
case 'error': return 'status-error';
|
|
3418
|
+
default: return 'status-unknown';
|
|
3950
3419
|
}
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3420
|
+
}
|
|
3421
|
+
/**
|
|
3422
|
+
* Cancel upload
|
|
3423
|
+
*/
|
|
3424
|
+
cancelUpload(fileId) {
|
|
3425
|
+
console.log('🚫 [FloatingFileUploader] Cancelling upload:', fileId);
|
|
3426
|
+
this.fileManagerService.cancelUpload(fileId);
|
|
3427
|
+
}
|
|
3428
|
+
/**
|
|
3429
|
+
* Get file name from file ID (extract from the ID format)
|
|
3430
|
+
*/
|
|
3431
|
+
getFileNameFromId(fileId) {
|
|
3432
|
+
// Extract filename from the fileId format: filename_size_timestamp
|
|
3433
|
+
const parts = fileId.split('_');
|
|
3434
|
+
if (parts.length >= 3) {
|
|
3435
|
+
// Remove the last two parts (size and timestamp) to get the filename
|
|
3436
|
+
return parts.slice(0, -2).join('_');
|
|
3955
3437
|
}
|
|
3956
|
-
|
|
3957
|
-
return null; // No validation errors
|
|
3438
|
+
return fileId;
|
|
3958
3439
|
}
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
{
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
}
|
|
3971
|
-
], usesOnChanges: true, ngImport: i0, template: "<div class=\"cide-file-input\">\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\n @if (labelSignal() && !isPreviewBoxMode()) {\n <label class=\"cide-file-input-label\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"cide-file-input-required\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview-box-container\">\n <!-- Hidden file input -->\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"cide-file-input-preview-box\"\n [class.cide-file-input-preview-box-disabled]=\"disabledSignal()\"\n [class.cide-file-input-preview-box-has-image]=\"hasImages()\"\n [class.cide-file-input-preview-box-drag-over]=\"isDragOver()\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n [attr.title]=\"disabledSignal() ? 'File selection disabled' : placeholderTextSignal()\">\n \n <!-- No Image State -->\n @if (!hasImages()) {\n <div class=\"cide-file-input-preview-box-placeholder\">\n <div class=\"cide-file-input-preview-box-icon\">\n <cide-ele-icon>{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\n </div>\n <div class=\"cide-file-input-preview-box-text\">\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"cide-file-input-preview-box-content\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"cide-file-input-preview-box-image\">\n <div class=\"cide-file-input-preview-box-overlay\">\n <div class=\"cide-file-input-preview-box-overlay-text\">Click to change</div>\n </div>\n @if (!disabledSignal()) {\n <button \n type=\"button\" \n class=\"cide-file-input-preview-box-remove\"\n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Remove image\">\n \u00D7\n </button>\n }\n </div>\n }\n </div>\n \n <!-- File name display for preview box mode -->\n @if (hasImages() && fileNames().length && showFileNameSignal()) {\n <div class=\"cide-file-input-preview-box-filename\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\n <div \n class=\"cide-file-input-wrapper\"\n [class.cide-file-input-drag-over]=\"isDragOver()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-element\"\n />\n @if (hasFiles()) {\n <button type=\"button\" class=\"cide-file-input-clear\" (click)=\"clearFiles()\">\n Clear\n </button>\n }\n </div>\n @if (hasFiles() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-files\">\n @for (name of fileNames(); track name) {\n <span>{{ name }}</span>\n }\n <!-- Angular 20: Display file size using new computed values -->\n @if (totalFileSize() > 0) {\n <div class=\"cide-file-input-size\">\n Total size: {{ fileSizeInMB() }} MB\n </div>\n }\n </div>\n }\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview\">\n <div class=\"cide-file-input-preview-label\">Preview:</div>\n <div class=\"cide-file-input-preview-container\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"cide-file-input-preview-item\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\">\n <button \n type=\"button\" \n class=\"cide-file-input-preview-remove\"\n (click)=\"removePreview(i)\"\n title=\"Remove image\">\n \u00D7\n </button>\n <img \n [src]=\"previewUrl\" \n [alt]=\"fileNames()[i] || 'Preview image'\"\n class=\"cide-file-input-preview-image\"\n loading=\"lazy\">\n <div class=\"cide-file-input-preview-filename\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n @if (errorTextSignal()) {\n <div class=\"cide-file-input-error\">{{ errorTextSignal() }}</div>\n }\n @if (helperTextSignal() && !errorTextSignal()) {\n <div class=\"cide-file-input-helper\">{{ helperTextSignal() }}</div>\n }\n</div> ", styles: [".cide-file-input{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-label{font-weight:500;margin-bottom:.25rem}.cide-file-input-required{color:#d32f2f;font-weight:700}.cide-file-input-wrapper{display:flex;align-items:center;gap:.5rem;border:2px dashed transparent;border-radius:.5rem;padding:.5rem;transition:all .2s ease-in-out}.cide-file-input-wrapper.cide-file-input-drag-over{border-color:#3b82f6;background-color:#eff6ff;transform:scale(1.02)}.cide-file-input-element{flex:1}.cide-file-input-clear{background:none;border:none;color:#d32f2f;cursor:pointer;font-size:.9rem}.cide-file-input-files{font-size:.95rem;color:#333;margin-top:.25rem}.cide-file-input-size{margin-top:.5rem;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;font-size:.75rem;color:#4b5563;font-weight:500}.cide-file-input-error{color:#d32f2f;font-size:.9rem}.cide-file-input-helper{color:#666;font-size:.9rem}.cide-file-input-preview{margin-top:.75rem;padding:.75rem;background-color:#f8f9fa;border:1px solid #e1e5e9;border-radius:.375rem}.cide-file-input-preview-label{font-weight:500;margin-bottom:.5rem;color:#374151;font-size:.875rem}.cide-file-input-preview-container{display:flex;flex-wrap:wrap;gap:.75rem}.cide-file-input-preview-item{position:relative;display:flex;flex-direction:column;border:1px solid #d1d5db;border-radius:.5rem;overflow:hidden;background-color:#fff;box-shadow:0 1px 3px #0000001a;transition:box-shadow .2s ease-in-out}.cide-file-input-preview-item:hover{box-shadow:0 4px 6px -1px #0000001a}.cide-file-input-preview-image{width:100%;height:calc(100% - 2rem);object-fit:cover;object-position:center;background-color:#f3f4f6}.cide-file-input-preview-filename{padding:.375rem .5rem;background-color:#f9fafbf2;border-top:1px solid #e5e7eb;font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:2rem;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-remove{position:absolute;top:.25rem;right:.25rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;transition:all .2s ease-in-out}.cide-file-input-preview-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-hidden{display:none}.cide-file-input-preview-box-container{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-preview-box{position:relative;border:2px dashed #d1d5db;border-radius:.5rem;cursor:pointer;background-color:#f9fafb;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:all .2s ease-in-out}.cide-file-input-preview-box:hover{border-color:#3b82f6;background-color:#eff6ff}.cide-file-input-preview-box.cide-file-input-preview-box-disabled{cursor:not-allowed;opacity:.6;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-disabled:hover{border-color:#d1d5db;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image{border-style:solid;border-color:#e5e7eb;padding:0}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover{border-color:#3b82f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover .cide-file-input-preview-box-overlay{opacity:1}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over{border-color:#3b82f6!important;background-color:#eff6ff!important;transform:scale(1.02);box-shadow:0 0 0 4px #3b82f61a}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-icon{color:#3b82f6;transform:scale(1.1)}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-text{color:#3b82f6;font-weight:600}.cide-file-input-preview-box-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1rem;text-align:center}.cide-file-input-preview-box-icon{font-size:2rem;color:#6b7280}.cide-file-input-preview-box-text{font-size:.875rem;color:#6b7280;font-weight:500}.cide-file-input-preview-box-content{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-box-image{width:100%;height:100%;object-fit:cover;object-position:center}.cide-file-input-preview-box-overlay{position:absolute;inset:0;background-color:#0009;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s ease-in-out}.cide-file-input-preview-box-overlay-text{color:#fff;font-size:.875rem;font-weight:500;text-align:center}.cide-file-input-preview-box-remove{position:absolute;top:.375rem;right:.375rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:20;transition:all .2s ease-in-out}.cide-file-input-preview-box-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-box-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-preview-box-filename{font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;margin-top:.25rem}@media (max-width: 640px){.cide-file-input-preview-container{justify-content:center}.cide-file-input-preview-item{min-width:120px;max-width:150px}.cide-file-input-preview-box-icon{font-size:1.5rem}.cide-file-input-preview-box-text{font-size:.75rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
|
|
3972
|
-
}
|
|
3973
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, decorators: [{
|
|
3974
|
-
type: Component,
|
|
3975
|
-
args: [{ selector: 'cide-ele-file-input', standalone: true, imports: [CommonModule, FormsModule, CideIconComponent], providers: [
|
|
3976
|
-
{
|
|
3977
|
-
provide: NG_VALUE_ACCESSOR,
|
|
3978
|
-
useExisting: CideEleFileInputComponent,
|
|
3979
|
-
multi: true
|
|
3980
|
-
},
|
|
3981
|
-
{
|
|
3982
|
-
provide: NG_VALIDATORS,
|
|
3983
|
-
useExisting: CideEleFileInputComponent,
|
|
3984
|
-
multi: true
|
|
3985
|
-
}
|
|
3986
|
-
], template: "<div class=\"cide-file-input\">\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\n @if (labelSignal() && !isPreviewBoxMode()) {\n <label class=\"cide-file-input-label\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"cide-file-input-required\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview-box-container\">\n <!-- Hidden file input -->\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"cide-file-input-preview-box\"\n [class.cide-file-input-preview-box-disabled]=\"disabledSignal()\"\n [class.cide-file-input-preview-box-has-image]=\"hasImages()\"\n [class.cide-file-input-preview-box-drag-over]=\"isDragOver()\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n [attr.title]=\"disabledSignal() ? 'File selection disabled' : placeholderTextSignal()\">\n \n <!-- No Image State -->\n @if (!hasImages()) {\n <div class=\"cide-file-input-preview-box-placeholder\">\n <div class=\"cide-file-input-preview-box-icon\">\n <cide-ele-icon>{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\n </div>\n <div class=\"cide-file-input-preview-box-text\">\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"cide-file-input-preview-box-content\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"cide-file-input-preview-box-image\">\n <div class=\"cide-file-input-preview-box-overlay\">\n <div class=\"cide-file-input-preview-box-overlay-text\">Click to change</div>\n </div>\n @if (!disabledSignal()) {\n <button \n type=\"button\" \n class=\"cide-file-input-preview-box-remove\"\n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Remove image\">\n \u00D7\n </button>\n }\n </div>\n }\n </div>\n \n <!-- File name display for preview box mode -->\n @if (hasImages() && fileNames().length && showFileNameSignal()) {\n <div class=\"cide-file-input-preview-box-filename\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\n <div \n class=\"cide-file-input-wrapper\"\n [class.cide-file-input-drag-over]=\"isDragOver()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-element\"\n />\n @if (hasFiles()) {\n <button type=\"button\" class=\"cide-file-input-clear\" (click)=\"clearFiles()\">\n Clear\n </button>\n }\n </div>\n @if (hasFiles() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-files\">\n @for (name of fileNames(); track name) {\n <span>{{ name }}</span>\n }\n <!-- Angular 20: Display file size using new computed values -->\n @if (totalFileSize() > 0) {\n <div class=\"cide-file-input-size\">\n Total size: {{ fileSizeInMB() }} MB\n </div>\n }\n </div>\n }\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview\">\n <div class=\"cide-file-input-preview-label\">Preview:</div>\n <div class=\"cide-file-input-preview-container\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"cide-file-input-preview-item\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\">\n <button \n type=\"button\" \n class=\"cide-file-input-preview-remove\"\n (click)=\"removePreview(i)\"\n title=\"Remove image\">\n \u00D7\n </button>\n <img \n [src]=\"previewUrl\" \n [alt]=\"fileNames()[i] || 'Preview image'\"\n class=\"cide-file-input-preview-image\"\n loading=\"lazy\">\n <div class=\"cide-file-input-preview-filename\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n @if (errorTextSignal()) {\n <div class=\"cide-file-input-error\">{{ errorTextSignal() }}</div>\n }\n @if (helperTextSignal() && !errorTextSignal()) {\n <div class=\"cide-file-input-helper\">{{ helperTextSignal() }}</div>\n }\n</div> ", styles: [".cide-file-input{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-label{font-weight:500;margin-bottom:.25rem}.cide-file-input-required{color:#d32f2f;font-weight:700}.cide-file-input-wrapper{display:flex;align-items:center;gap:.5rem;border:2px dashed transparent;border-radius:.5rem;padding:.5rem;transition:all .2s ease-in-out}.cide-file-input-wrapper.cide-file-input-drag-over{border-color:#3b82f6;background-color:#eff6ff;transform:scale(1.02)}.cide-file-input-element{flex:1}.cide-file-input-clear{background:none;border:none;color:#d32f2f;cursor:pointer;font-size:.9rem}.cide-file-input-files{font-size:.95rem;color:#333;margin-top:.25rem}.cide-file-input-size{margin-top:.5rem;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;font-size:.75rem;color:#4b5563;font-weight:500}.cide-file-input-error{color:#d32f2f;font-size:.9rem}.cide-file-input-helper{color:#666;font-size:.9rem}.cide-file-input-preview{margin-top:.75rem;padding:.75rem;background-color:#f8f9fa;border:1px solid #e1e5e9;border-radius:.375rem}.cide-file-input-preview-label{font-weight:500;margin-bottom:.5rem;color:#374151;font-size:.875rem}.cide-file-input-preview-container{display:flex;flex-wrap:wrap;gap:.75rem}.cide-file-input-preview-item{position:relative;display:flex;flex-direction:column;border:1px solid #d1d5db;border-radius:.5rem;overflow:hidden;background-color:#fff;box-shadow:0 1px 3px #0000001a;transition:box-shadow .2s ease-in-out}.cide-file-input-preview-item:hover{box-shadow:0 4px 6px -1px #0000001a}.cide-file-input-preview-image{width:100%;height:calc(100% - 2rem);object-fit:cover;object-position:center;background-color:#f3f4f6}.cide-file-input-preview-filename{padding:.375rem .5rem;background-color:#f9fafbf2;border-top:1px solid #e5e7eb;font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:2rem;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-remove{position:absolute;top:.25rem;right:.25rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;transition:all .2s ease-in-out}.cide-file-input-preview-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-hidden{display:none}.cide-file-input-preview-box-container{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-preview-box{position:relative;border:2px dashed #d1d5db;border-radius:.5rem;cursor:pointer;background-color:#f9fafb;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:all .2s ease-in-out}.cide-file-input-preview-box:hover{border-color:#3b82f6;background-color:#eff6ff}.cide-file-input-preview-box.cide-file-input-preview-box-disabled{cursor:not-allowed;opacity:.6;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-disabled:hover{border-color:#d1d5db;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image{border-style:solid;border-color:#e5e7eb;padding:0}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover{border-color:#3b82f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover .cide-file-input-preview-box-overlay{opacity:1}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over{border-color:#3b82f6!important;background-color:#eff6ff!important;transform:scale(1.02);box-shadow:0 0 0 4px #3b82f61a}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-icon{color:#3b82f6;transform:scale(1.1)}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-text{color:#3b82f6;font-weight:600}.cide-file-input-preview-box-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1rem;text-align:center}.cide-file-input-preview-box-icon{font-size:2rem;color:#6b7280}.cide-file-input-preview-box-text{font-size:.875rem;color:#6b7280;font-weight:500}.cide-file-input-preview-box-content{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-box-image{width:100%;height:100%;object-fit:cover;object-position:center}.cide-file-input-preview-box-overlay{position:absolute;inset:0;background-color:#0009;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s ease-in-out}.cide-file-input-preview-box-overlay-text{color:#fff;font-size:.875rem;font-weight:500;text-align:center}.cide-file-input-preview-box-remove{position:absolute;top:.375rem;right:.375rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:20;transition:all .2s ease-in-out}.cide-file-input-preview-box-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-box-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-preview-box-filename{font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;margin-top:.25rem}@media (max-width: 640px){.cide-file-input-preview-container{justify-content:center}.cide-file-input-preview-item{min-width:120px;max-width:150px}.cide-file-input-preview-box-icon{font-size:1.5rem}.cide-file-input-preview-box-text{font-size:.75rem}}\n"] }]
|
|
3987
|
-
}], ctorParameters: () => [], propDecorators: { label: [{
|
|
3988
|
-
type: Input
|
|
3989
|
-
}], accept: [{
|
|
3990
|
-
type: Input
|
|
3991
|
-
}], multiple: [{
|
|
3992
|
-
type: Input
|
|
3993
|
-
}], disabled: [{
|
|
3994
|
-
type: Input
|
|
3995
|
-
}], required: [{
|
|
3996
|
-
type: Input
|
|
3997
|
-
}], helperText: [{
|
|
3998
|
-
type: Input
|
|
3999
|
-
}], errorText: [{
|
|
4000
|
-
type: Input
|
|
4001
|
-
}], showPreview: [{
|
|
4002
|
-
type: Input
|
|
4003
|
-
}], previewWidth: [{
|
|
4004
|
-
type: Input
|
|
4005
|
-
}], previewHeight: [{
|
|
4006
|
-
type: Input
|
|
4007
|
-
}], previewBoxMode: [{
|
|
4008
|
-
type: Input
|
|
4009
|
-
}], showFileName: [{
|
|
4010
|
-
type: Input
|
|
4011
|
-
}], placeholderText: [{
|
|
4012
|
-
type: Input
|
|
4013
|
-
}], placeholderIcon: [{
|
|
4014
|
-
type: Input
|
|
4015
|
-
}], autoUpload: [{
|
|
4016
|
-
type: Input
|
|
4017
|
-
}], uploadData: [{
|
|
4018
|
-
type: Input
|
|
4019
|
-
}], fileChange: [{
|
|
4020
|
-
type: Output
|
|
4021
|
-
}], uploadSuccess: [{
|
|
4022
|
-
type: Output
|
|
4023
|
-
}], uploadError: [{
|
|
4024
|
-
type: Output
|
|
4025
|
-
}], uploadProgressChange: [{
|
|
4026
|
-
type: Output
|
|
4027
|
-
}] } });
|
|
4028
|
-
|
|
4029
|
-
class CideEleGlobalFileUploaderComponent {
|
|
4030
|
-
// Dependency injection
|
|
4031
|
-
destroyRef = inject(DestroyRef);
|
|
4032
|
-
fileService = inject(CideEleFileManagerService);
|
|
4033
|
-
// Input properties
|
|
4034
|
-
userId = '';
|
|
4035
|
-
multiple = true;
|
|
4036
|
-
accept = '';
|
|
4037
|
-
maxFileSize = 10; // MB
|
|
4038
|
-
allowedTypes = [];
|
|
4039
|
-
// Output events
|
|
4040
|
-
uploadComplete = new EventEmitter();
|
|
4041
|
-
uploadError = new EventEmitter();
|
|
4042
|
-
uploadCancelled = new EventEmitter();
|
|
4043
|
-
allUploadsComplete = new EventEmitter();
|
|
4044
|
-
// Signals for reactive state management
|
|
4045
|
-
isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
|
|
4046
|
-
isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
|
|
4047
|
-
uploadQueue = signal([], ...(ngDevMode ? [{ debugName: "uploadQueue" }] : []));
|
|
4048
|
-
currentGroupId = signal(null, ...(ngDevMode ? [{ debugName: "currentGroupId" }] : []));
|
|
4049
|
-
// Computed values
|
|
4050
|
-
hasUploads = computed(() => this.uploadQueue().length > 0, ...(ngDevMode ? [{ debugName: "hasUploads" }] : []));
|
|
4051
|
-
pendingUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'pending'), ...(ngDevMode ? [{ debugName: "pendingUploads" }] : []));
|
|
4052
|
-
activeUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'uploading'), ...(ngDevMode ? [{ debugName: "activeUploads" }] : []));
|
|
4053
|
-
completedUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'completed'), ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
|
|
4054
|
-
failedUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'error'), ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
|
|
4055
|
-
constructor() {
|
|
4056
|
-
console.log('🚀 [GlobalFileUploader] Component initialized');
|
|
3440
|
+
/**
|
|
3441
|
+
* Add completed file to local state
|
|
3442
|
+
*/
|
|
3443
|
+
addCompletedFile(fileId, upload) {
|
|
3444
|
+
const fileName = this.getFileNameFromId(fileId);
|
|
3445
|
+
this.completedFiles.update(completed => {
|
|
3446
|
+
const newCompleted = new Map(completed);
|
|
3447
|
+
newCompleted.set(fileId, {
|
|
3448
|
+
fileName,
|
|
3449
|
+
completedAt: new Date(),
|
|
3450
|
+
groupId: this.currentGroupId() || undefined
|
|
3451
|
+
});
|
|
3452
|
+
return newCompleted;
|
|
3453
|
+
});
|
|
4057
3454
|
}
|
|
4058
3455
|
/**
|
|
4059
|
-
*
|
|
3456
|
+
* Clear all completed files
|
|
4060
3457
|
*/
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
event.stopPropagation();
|
|
4064
|
-
this.isDragOver.set(true);
|
|
3458
|
+
clearCompletedFiles() {
|
|
3459
|
+
this.completedFiles.set(new Map());
|
|
4065
3460
|
}
|
|
4066
3461
|
/**
|
|
4067
|
-
*
|
|
3462
|
+
* Set current user ID
|
|
4068
3463
|
*/
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
this.isDragOver.set(false);
|
|
3464
|
+
setCurrentUserId(userId) {
|
|
3465
|
+
this.currentUserId.set(userId);
|
|
3466
|
+
this.fileManagerService.setUserId(userId);
|
|
4073
3467
|
}
|
|
4074
3468
|
/**
|
|
4075
|
-
*
|
|
3469
|
+
* Public method to handle files from external sources
|
|
3470
|
+
* This can be called by other components to trigger the floating uploader
|
|
4076
3471
|
*/
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
this.
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
3472
|
+
handleExternalFiles(files, userId, groupId) {
|
|
3473
|
+
console.log('📁 [FloatingFileUploader] External files received:', files.length, 'files');
|
|
3474
|
+
// Set user ID if provided
|
|
3475
|
+
if (userId && userId !== this.currentUserId()) {
|
|
3476
|
+
this.setCurrentUserId(userId);
|
|
3477
|
+
}
|
|
3478
|
+
// Set group ID if provided
|
|
3479
|
+
if (groupId) {
|
|
3480
|
+
this.currentGroupId.set(groupId);
|
|
4084
3481
|
}
|
|
3482
|
+
// Upload files using file manager service
|
|
3483
|
+
// The file manager service will handle adding to its queue and the effect will show the floating uploader
|
|
3484
|
+
files.forEach((file, index) => {
|
|
3485
|
+
console.log(`📁 [FloatingFileUploader] Starting upload for file ${index + 1}/${files.length}:`, file.name);
|
|
3486
|
+
this.fileManagerService.uploadFile(file, {
|
|
3487
|
+
userId: this.currentUserId(),
|
|
3488
|
+
groupId: groupId,
|
|
3489
|
+
permissions: ['read', 'write'],
|
|
3490
|
+
tags: []
|
|
3491
|
+
})
|
|
3492
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
3493
|
+
.subscribe({
|
|
3494
|
+
next: (response) => {
|
|
3495
|
+
console.log('✅ [FloatingFileUploader] Upload completed:', response);
|
|
3496
|
+
},
|
|
3497
|
+
error: (error) => {
|
|
3498
|
+
console.error('❌ [FloatingFileUploader] Upload failed:', error);
|
|
3499
|
+
}
|
|
3500
|
+
});
|
|
3501
|
+
});
|
|
4085
3502
|
}
|
|
4086
3503
|
/**
|
|
4087
|
-
*
|
|
3504
|
+
* Manually show the floating uploader
|
|
3505
|
+
* This can be called from file input components or other triggers
|
|
4088
3506
|
*/
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
this.addFilesToQueue(Array.from(files));
|
|
3507
|
+
showUploader(groupId) {
|
|
3508
|
+
console.log('👁️ [FloatingFileUploader] Manually showing uploader', groupId ? `for group: ${groupId}` : '');
|
|
3509
|
+
if (groupId) {
|
|
3510
|
+
this.currentGroupId.set(groupId);
|
|
4094
3511
|
}
|
|
4095
|
-
|
|
4096
|
-
target.value = '';
|
|
3512
|
+
this.showWithAnimation();
|
|
4097
3513
|
}
|
|
4098
3514
|
/**
|
|
4099
|
-
*
|
|
3515
|
+
* Check if there are any uploads for the current group
|
|
4100
3516
|
*/
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
return;
|
|
3517
|
+
hasUploadsForCurrentGroup() {
|
|
3518
|
+
const groupId = this.currentGroupId();
|
|
3519
|
+
if (!groupId) {
|
|
3520
|
+
// If no group filter, show all uploads
|
|
3521
|
+
return this.hasUploads();
|
|
4107
3522
|
}
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
progress: 0,
|
|
4112
|
-
status: 'pending',
|
|
4113
|
-
file: file
|
|
4114
|
-
}));
|
|
4115
|
-
this.uploadQueue.update(queue => [...queue, ...newUploads]);
|
|
4116
|
-
this.processUploadQueue();
|
|
3523
|
+
// Check if any uploads belong to the current group
|
|
3524
|
+
// Note: This would need to be enhanced based on how group IDs are stored in the file manager service
|
|
3525
|
+
return this.hasUploads();
|
|
4117
3526
|
}
|
|
4118
|
-
|
|
4119
|
-
* Validate file
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
return false;
|
|
3527
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3528
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: CideEleFloatingFileUploaderComponent, isStandalone: true, selector: "cide-ele-floating-file-uploader", ngImport: i0, template: "<!-- Floating File Uploader Container -->\r\n@if (isVisible()) {\r\n<div class=\"floating-uploader\" \r\n [class.minimized]=\"isMinimized()\" \r\n [class.animating]=\"isAnimating()\">\r\n\r\n <!-- Header -->\r\n <div class=\"uploader-header\">\r\n <div class=\"header-left\">\r\n <div class=\"upload-icon\">\r\n <cide-ele-icon size=\"sm\">cloud_upload</cide-ele-icon>\r\n </div>\r\n <div class=\"upload-info\">\r\n <div class=\"upload-title\">File Upload</div>\r\n <div class=\"upload-summary\">{{ getUploadSummary() }}</div>\r\n </div>\r\n </div>\r\n \r\n <div class=\"header-actions\">\r\n <button class=\"action-btn minimize-btn\" (click)=\"toggleMinimize()\" [title]=\"isMinimized() ? 'Expand' : 'Minimize'\">\r\n <cide-ele-icon size=\"xs\">{{ isMinimized() ? 'expand_more' : 'expand_less' }}</cide-ele-icon>\r\n </button>\r\n <button class=\"action-btn close-btn\" (click)=\"close()\" title=\"Close\">\r\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Content (hidden when minimized) -->\r\n @if (!isMinimized()) {\r\n <div class=\"uploader-content\">\r\n \r\n <!-- Overall Progress Bar -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0) {\r\n <div class=\"overall-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"getOverallProgress()\"></div>\r\n </div>\r\n <div class=\"progress-text\">{{ getOverallProgress() }}%</div>\r\n </div>\r\n }\r\n\r\n <!-- Upload Queue - Show files from file manager service -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0 || completedFiles().size > 0) {\r\n <div class=\"upload-queue\">\r\n <!-- Show files in queue (pending) -->\r\n @for (fileId of uploadQueue(); track fileId) {\r\n <div class=\"upload-item status-pending\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">schedule</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(fileId) }}</div>\r\n <div class=\"file-status\">\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"upload-actions\">\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(fileId)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Show active uploads -->\r\n @for (entry of activeUploads() | keyvalue; track entry.key) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(entry.value.stage)\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">{{ getStatusIcon(entry.value.stage) }}</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(entry.key) }}</div>\r\n <div class=\"file-status\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <span class=\"text-yellow-600\">Reading...</span>\r\n }\r\n @case ('uploading') {\r\n <span class=\"text-blue-600\">Uploading...</span>\r\n }\r\n @case ('complete') {\r\n <span class=\"text-green-600\">Completed</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">Failed</span>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar (for uploading files) -->\r\n @if (entry.value.stage === 'uploading') {\r\n <div class=\"file-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"entry.value.percentage\"></div>\r\n </div>\r\n <span class=\"progress-text\">{{ entry.value.percentage }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('complete') {\r\n <button class=\"action-btn success-btn\" title=\"Completed\">\r\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button class=\"action-btn retry-btn\" title=\"Retry\">\r\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Show completed files from local state -->\r\n @for (entry of completedFiles() | keyvalue; track entry.key) {\r\n <div class=\"upload-item status-completed\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">check_circle</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ entry.value.fileName }}</div>\r\n <div class=\"file-status\">\r\n <span class=\"text-green-600\">Completed</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"upload-actions\">\r\n <button class=\"action-btn success-btn\" title=\"Completed\">\r\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <!-- No uploads message when manually opened -->\r\n <div class=\"no-uploads-message\">\r\n <div class=\"message-content\">\r\n <cide-ele-icon size=\"md\" class=\"message-icon\">cloud_upload</cide-ele-icon>\r\n <div class=\"message-text\">\r\n <h4>No active uploads</h4>\r\n <p>Upload files to see their progress here</p>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n</div>\r\n}\r\n", styles: [".floating-uploader{position:fixed;bottom:20px;right:20px;width:320px;max-height:500px;background:#fff;border-radius:12px;box-shadow:0 8px 32px #0000001f;border:1px solid rgba(0,0,0,.08);z-index:1000;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1);transform:translateY(0);opacity:1}.floating-uploader.animating{transition:all .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.minimized .uploader-content{display:none}.floating-uploader.minimized .uploader-footer{border-top:none}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f626}.floating-uploader .uploader-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#f8fafc;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-header .header-left{display:flex;align-items:center;gap:8px}.floating-uploader .uploader-header .header-left .upload-icon{display:flex;align-items:center;justify-content:center;width:24px;height:24px;background:#3b82f6;border-radius:6px;color:#fff}.floating-uploader .uploader-header .header-left .upload-info .upload-title{font-size:14px;font-weight:600;color:#1e293b;margin:0}.floating-uploader .uploader-header .header-left .upload-info .upload-summary{font-size:12px;color:#64748b;margin:0}.floating-uploader .uploader-header .header-actions{display:flex;gap:4px}.floating-uploader .uploader-header .header-actions .action-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:background-color .2s;color:#64748b}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#e2e8f0;color:#1e293b}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content{max-height:400px;overflow-y:auto}.floating-uploader .uploader-content .overall-progress{padding:12px 16px;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-content .overall-progress .progress-bar{width:100%;height:4px;background:#e2e8f0;border-radius:2px;overflow:hidden;margin-bottom:4px}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .overall-progress .progress-text{font-size:12px;color:#64748b;text-align:center;display:block}.floating-uploader .uploader-content .upload-queue .upload-item{display:flex;align-items:center;padding:8px 16px;border-bottom:1px solid #f1f5f9;transition:background-color .2s}.floating-uploader .uploader-content .upload-queue .upload-item:last-child{border-bottom:none}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#f0f9ff}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#f0fdf4}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#fef2f2}.floating-uploader .uploader-content .upload-queue .upload-item .file-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .status-icon{flex-shrink:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details{min-width:0;flex:1}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{font-size:13px;font-weight:500;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status{font-size:11px;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status span{font-weight:500}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress{display:flex;align-items:center;gap:8px;margin:0 8px;min-width:80px}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{flex:1;height:3px;background:#e2e8f0;border-radius:2px;overflow:hidden}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text{font-size:10px;color:#64748b;min-width:24px;text-align:right}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions{display:flex;gap:4px}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:all .2s;color:#64748b}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#e2e8f0}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#f0f9ff;color:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#16a34a}.floating-uploader .uploader-content .hidden-uploader{display:none}.floating-uploader .uploader-footer{padding:8px 16px;background:#f8fafc;border-top:1px solid #e2e8f0}.floating-uploader .uploader-footer .footer-stats{display:flex;gap:12px;font-size:11px}.floating-uploader .uploader-footer .footer-stats .stat{display:flex;align-items:center;gap:4px;color:#64748b}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#3b82f6}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#16a34a}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#dc2626}@media (max-width: 640px){.floating-uploader{bottom:10px;right:10px;left:10px;width:auto;max-width:none}}@media (prefers-color-scheme: dark){.floating-uploader{background:#1e293b;border-color:#334155;box-shadow:0 8px 32px #0000004d}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f633}.floating-uploader .uploader-header{background:#334155;border-bottom-color:#475569}.floating-uploader .uploader-header .header-left .upload-icon{background:#3b82f6}.floating-uploader .uploader-header .header-left .upload-info .upload-title{color:#f1f5f9}.floating-uploader .uploader-header .header-left .upload-info .upload-summary,.floating-uploader .uploader-header .header-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#475569;color:#f1f5f9}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .overall-progress{border-bottom-color:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .overall-progress .progress-text{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item{border-bottom-color:#334155}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#1e3a8a}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#14532d}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#7f1d1d}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{color:#f1f5f9}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text,.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#1e3a8a;color:#60a5fa}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#4ade80}.floating-uploader .uploader-footer{background:#334155;border-top-color:#475569}.floating-uploader .uploader-footer .footer-stats .stat{color:#94a3b8}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#60a5fa}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#4ade80}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#fca5a5}}@keyframes slideInUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes slideOutDown{0%{transform:translateY(0);opacity:1}to{transform:translateY(100%);opacity:0}}.floating-uploader.animating{animation:slideInUp .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.animating.hiding{animation:slideOutDown .3s cubic-bezier(.4,0,.2,1)}.no-uploads-message{padding:2rem;text-align:center;color:#6b7280}.no-uploads-message .message-content{display:flex;flex-direction:column;align-items:center;gap:1rem}.no-uploads-message .message-content .message-icon{color:#9ca3af;opacity:.7}.no-uploads-message .message-content .message-text h4{margin:0 0 .5rem;font-size:1.1rem;font-weight:600;color:#374151}.no-uploads-message .message-content .message-text p{margin:0;font-size:.9rem;color:#6b7280}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
|
|
3529
|
+
}
|
|
3530
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, decorators: [{
|
|
3531
|
+
type: Component,
|
|
3532
|
+
args: [{ selector: 'cide-ele-floating-file-uploader', standalone: true, imports: [
|
|
3533
|
+
CommonModule,
|
|
3534
|
+
CideIconComponent
|
|
3535
|
+
], template: "<!-- Floating File Uploader Container -->\r\n@if (isVisible()) {\r\n<div class=\"floating-uploader\" \r\n [class.minimized]=\"isMinimized()\" \r\n [class.animating]=\"isAnimating()\">\r\n\r\n <!-- Header -->\r\n <div class=\"uploader-header\">\r\n <div class=\"header-left\">\r\n <div class=\"upload-icon\">\r\n <cide-ele-icon size=\"sm\">cloud_upload</cide-ele-icon>\r\n </div>\r\n <div class=\"upload-info\">\r\n <div class=\"upload-title\">File Upload</div>\r\n <div class=\"upload-summary\">{{ getUploadSummary() }}</div>\r\n </div>\r\n </div>\r\n \r\n <div class=\"header-actions\">\r\n <button class=\"action-btn minimize-btn\" (click)=\"toggleMinimize()\" [title]=\"isMinimized() ? 'Expand' : 'Minimize'\">\r\n <cide-ele-icon size=\"xs\">{{ isMinimized() ? 'expand_more' : 'expand_less' }}</cide-ele-icon>\r\n </button>\r\n <button class=\"action-btn close-btn\" (click)=\"close()\" title=\"Close\">\r\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Content (hidden when minimized) -->\r\n @if (!isMinimized()) {\r\n <div class=\"uploader-content\">\r\n \r\n <!-- Overall Progress Bar -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0) {\r\n <div class=\"overall-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"getOverallProgress()\"></div>\r\n </div>\r\n <div class=\"progress-text\">{{ getOverallProgress() }}%</div>\r\n </div>\r\n }\r\n\r\n <!-- Upload Queue - Show files from file manager service -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0 || completedFiles().size > 0) {\r\n <div class=\"upload-queue\">\r\n <!-- Show files in queue (pending) -->\r\n @for (fileId of uploadQueue(); track fileId) {\r\n <div class=\"upload-item status-pending\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">schedule</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(fileId) }}</div>\r\n <div class=\"file-status\">\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"upload-actions\">\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(fileId)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Show active uploads -->\r\n @for (entry of activeUploads() | keyvalue; track entry.key) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(entry.value.stage)\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">{{ getStatusIcon(entry.value.stage) }}</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(entry.key) }}</div>\r\n <div class=\"file-status\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <span class=\"text-yellow-600\">Reading...</span>\r\n }\r\n @case ('uploading') {\r\n <span class=\"text-blue-600\">Uploading...</span>\r\n }\r\n @case ('complete') {\r\n <span class=\"text-green-600\">Completed</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">Failed</span>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar (for uploading files) -->\r\n @if (entry.value.stage === 'uploading') {\r\n <div class=\"file-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"entry.value.percentage\"></div>\r\n </div>\r\n <span class=\"progress-text\">{{ entry.value.percentage }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('complete') {\r\n <button class=\"action-btn success-btn\" title=\"Completed\">\r\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button class=\"action-btn retry-btn\" title=\"Retry\">\r\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Show completed files from local state -->\r\n @for (entry of completedFiles() | keyvalue; track entry.key) {\r\n <div class=\"upload-item status-completed\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">check_circle</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ entry.value.fileName }}</div>\r\n <div class=\"file-status\">\r\n <span class=\"text-green-600\">Completed</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"upload-actions\">\r\n <button class=\"action-btn success-btn\" title=\"Completed\">\r\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <!-- No uploads message when manually opened -->\r\n <div class=\"no-uploads-message\">\r\n <div class=\"message-content\">\r\n <cide-ele-icon size=\"md\" class=\"message-icon\">cloud_upload</cide-ele-icon>\r\n <div class=\"message-text\">\r\n <h4>No active uploads</h4>\r\n <p>Upload files to see their progress here</p>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n</div>\r\n}\r\n", styles: [".floating-uploader{position:fixed;bottom:20px;right:20px;width:320px;max-height:500px;background:#fff;border-radius:12px;box-shadow:0 8px 32px #0000001f;border:1px solid rgba(0,0,0,.08);z-index:1000;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1);transform:translateY(0);opacity:1}.floating-uploader.animating{transition:all .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.minimized .uploader-content{display:none}.floating-uploader.minimized .uploader-footer{border-top:none}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f626}.floating-uploader .uploader-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#f8fafc;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-header .header-left{display:flex;align-items:center;gap:8px}.floating-uploader .uploader-header .header-left .upload-icon{display:flex;align-items:center;justify-content:center;width:24px;height:24px;background:#3b82f6;border-radius:6px;color:#fff}.floating-uploader .uploader-header .header-left .upload-info .upload-title{font-size:14px;font-weight:600;color:#1e293b;margin:0}.floating-uploader .uploader-header .header-left .upload-info .upload-summary{font-size:12px;color:#64748b;margin:0}.floating-uploader .uploader-header .header-actions{display:flex;gap:4px}.floating-uploader .uploader-header .header-actions .action-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:background-color .2s;color:#64748b}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#e2e8f0;color:#1e293b}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content{max-height:400px;overflow-y:auto}.floating-uploader .uploader-content .overall-progress{padding:12px 16px;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-content .overall-progress .progress-bar{width:100%;height:4px;background:#e2e8f0;border-radius:2px;overflow:hidden;margin-bottom:4px}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .overall-progress .progress-text{font-size:12px;color:#64748b;text-align:center;display:block}.floating-uploader .uploader-content .upload-queue .upload-item{display:flex;align-items:center;padding:8px 16px;border-bottom:1px solid #f1f5f9;transition:background-color .2s}.floating-uploader .uploader-content .upload-queue .upload-item:last-child{border-bottom:none}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#f0f9ff}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#f0fdf4}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#fef2f2}.floating-uploader .uploader-content .upload-queue .upload-item .file-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .status-icon{flex-shrink:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details{min-width:0;flex:1}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{font-size:13px;font-weight:500;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status{font-size:11px;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status span{font-weight:500}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress{display:flex;align-items:center;gap:8px;margin:0 8px;min-width:80px}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{flex:1;height:3px;background:#e2e8f0;border-radius:2px;overflow:hidden}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text{font-size:10px;color:#64748b;min-width:24px;text-align:right}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions{display:flex;gap:4px}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:all .2s;color:#64748b}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#e2e8f0}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#f0f9ff;color:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#16a34a}.floating-uploader .uploader-content .hidden-uploader{display:none}.floating-uploader .uploader-footer{padding:8px 16px;background:#f8fafc;border-top:1px solid #e2e8f0}.floating-uploader .uploader-footer .footer-stats{display:flex;gap:12px;font-size:11px}.floating-uploader .uploader-footer .footer-stats .stat{display:flex;align-items:center;gap:4px;color:#64748b}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#3b82f6}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#16a34a}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#dc2626}@media (max-width: 640px){.floating-uploader{bottom:10px;right:10px;left:10px;width:auto;max-width:none}}@media (prefers-color-scheme: dark){.floating-uploader{background:#1e293b;border-color:#334155;box-shadow:0 8px 32px #0000004d}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f633}.floating-uploader .uploader-header{background:#334155;border-bottom-color:#475569}.floating-uploader .uploader-header .header-left .upload-icon{background:#3b82f6}.floating-uploader .uploader-header .header-left .upload-info .upload-title{color:#f1f5f9}.floating-uploader .uploader-header .header-left .upload-info .upload-summary,.floating-uploader .uploader-header .header-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#475569;color:#f1f5f9}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .overall-progress{border-bottom-color:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .overall-progress .progress-text{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item{border-bottom-color:#334155}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#1e3a8a}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#14532d}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#7f1d1d}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{color:#f1f5f9}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text,.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#1e3a8a;color:#60a5fa}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#4ade80}.floating-uploader .uploader-footer{background:#334155;border-top-color:#475569}.floating-uploader .uploader-footer .footer-stats .stat{color:#94a3b8}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#60a5fa}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#4ade80}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#fca5a5}}@keyframes slideInUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes slideOutDown{0%{transform:translateY(0);opacity:1}to{transform:translateY(100%);opacity:0}}.floating-uploader.animating{animation:slideInUp .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.animating.hiding{animation:slideOutDown .3s cubic-bezier(.4,0,.2,1)}.no-uploads-message{padding:2rem;text-align:center;color:#6b7280}.no-uploads-message .message-content{display:flex;flex-direction:column;align-items:center;gap:1rem}.no-uploads-message .message-content .message-icon{color:#9ca3af;opacity:.7}.no-uploads-message .message-content .message-text h4{margin:0 0 .5rem;font-size:1.1rem;font-weight:600;color:#374151}.no-uploads-message .message-content .message-text p{margin:0;font-size:.9rem;color:#6b7280}\n"] }]
|
|
3536
|
+
}], ctorParameters: () => [] });
|
|
3537
|
+
|
|
3538
|
+
class CideEleFileInputComponent {
|
|
3539
|
+
fileManagerService = inject(CideEleFileManagerService);
|
|
3540
|
+
notificationService = inject(NotificationService);
|
|
3541
|
+
elementService = inject(CideElementsService);
|
|
3542
|
+
destroyRef = inject(DestroyRef);
|
|
3543
|
+
floatingUploader = inject(CideEleFloatingFileUploaderComponent, { optional: true });
|
|
3544
|
+
// private readonly pendingTasks = inject(PendingTasks); // TODO: Fix PendingTasks API usage
|
|
3545
|
+
// Traditional @Input() decorators
|
|
3546
|
+
label = 'Choose file';
|
|
3547
|
+
accept = '';
|
|
3548
|
+
multiple = false;
|
|
3549
|
+
disabled = false;
|
|
3550
|
+
required = false;
|
|
3551
|
+
helperText = '';
|
|
3552
|
+
errorText = '';
|
|
3553
|
+
showPreview = false;
|
|
3554
|
+
previewWidth = '200px';
|
|
3555
|
+
previewHeight = '200px';
|
|
3556
|
+
previewBoxMode = false;
|
|
3557
|
+
showFileName = true;
|
|
3558
|
+
placeholderText = 'Click to select image';
|
|
3559
|
+
placeholderIcon = '📷';
|
|
3560
|
+
autoUpload = false;
|
|
3561
|
+
uploadData = {};
|
|
3562
|
+
showFloatingUploader = true;
|
|
3563
|
+
floatingUploaderGroupId;
|
|
3564
|
+
// Traditional @Output() decorators
|
|
3565
|
+
fileChange = new EventEmitter();
|
|
3566
|
+
uploadSuccess = new EventEmitter();
|
|
3567
|
+
uploadError = new EventEmitter();
|
|
3568
|
+
uploadProgressChange = new EventEmitter();
|
|
3569
|
+
// Readable signals created from @Input() decorator values
|
|
3570
|
+
labelSignal = signal(this.label, ...(ngDevMode ? [{ debugName: "labelSignal" }] : []));
|
|
3571
|
+
acceptSignal = signal(this.accept, ...(ngDevMode ? [{ debugName: "acceptSignal" }] : []));
|
|
3572
|
+
multipleSignal = signal(this.multiple, ...(ngDevMode ? [{ debugName: "multipleSignal" }] : []));
|
|
3573
|
+
disabledSignal = signal(this.disabled, ...(ngDevMode ? [{ debugName: "disabledSignal" }] : []));
|
|
3574
|
+
requiredSignal = signal(this.required, ...(ngDevMode ? [{ debugName: "requiredSignal" }] : []));
|
|
3575
|
+
helperTextSignal = signal(this.helperText, ...(ngDevMode ? [{ debugName: "helperTextSignal" }] : []));
|
|
3576
|
+
errorTextSignal = signal(this.errorText, ...(ngDevMode ? [{ debugName: "errorTextSignal" }] : []));
|
|
3577
|
+
showPreviewSignal = signal(this.showPreview, ...(ngDevMode ? [{ debugName: "showPreviewSignal" }] : []));
|
|
3578
|
+
previewWidthSignal = signal(this.previewWidth, ...(ngDevMode ? [{ debugName: "previewWidthSignal" }] : []));
|
|
3579
|
+
previewHeightSignal = signal(this.previewHeight, ...(ngDevMode ? [{ debugName: "previewHeightSignal" }] : []));
|
|
3580
|
+
previewBoxModeSignal = signal(this.previewBoxMode, ...(ngDevMode ? [{ debugName: "previewBoxModeSignal" }] : []));
|
|
3581
|
+
showFileNameSignal = signal(this.showFileName, ...(ngDevMode ? [{ debugName: "showFileNameSignal" }] : []));
|
|
3582
|
+
placeholderTextSignal = signal(this.placeholderText, ...(ngDevMode ? [{ debugName: "placeholderTextSignal" }] : []));
|
|
3583
|
+
placeholderIconSignal = signal(this.placeholderIcon, ...(ngDevMode ? [{ debugName: "placeholderIconSignal" }] : []));
|
|
3584
|
+
autoUploadSignal = signal(this.autoUpload, ...(ngDevMode ? [{ debugName: "autoUploadSignal" }] : []));
|
|
3585
|
+
uploadDataSignal = signal(this.uploadData, ...(ngDevMode ? [{ debugName: "uploadDataSignal" }] : []));
|
|
3586
|
+
showFloatingUploaderSignal = signal(this.showFloatingUploader, ...(ngDevMode ? [{ debugName: "showFloatingUploaderSignal" }] : []));
|
|
3587
|
+
floatingUploaderGroupIdSignal = signal(this.floatingUploaderGroupId, ...(ngDevMode ? [{ debugName: "floatingUploaderGroupIdSignal" }] : []));
|
|
3588
|
+
// Reactive state with signals
|
|
3589
|
+
id = signal(Math.random().toString(36).substring(2, 10), ...(ngDevMode ? [{ debugName: "id" }] : []));
|
|
3590
|
+
isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
|
|
3591
|
+
uploadProgress = signal(0, ...(ngDevMode ? [{ debugName: "uploadProgress" }] : []));
|
|
3592
|
+
uploadStatus = signal('idle', ...(ngDevMode ? [{ debugName: "uploadStatus" }] : []));
|
|
3593
|
+
files = signal(null, ...(ngDevMode ? [{ debugName: "files" }] : []));
|
|
3594
|
+
fileNames = signal([], ...(ngDevMode ? [{ debugName: "fileNames" }] : []));
|
|
3595
|
+
previewUrls = signal([], ...(ngDevMode ? [{ debugName: "previewUrls" }] : []));
|
|
3596
|
+
uploadNotificationId = signal(null, ...(ngDevMode ? [{ debugName: "uploadNotificationId" }] : []));
|
|
3597
|
+
isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
|
|
3598
|
+
groupId = signal(null, ...(ngDevMode ? [{ debugName: "groupId" }] : [])); // Group ID for multiple file uploads
|
|
3599
|
+
isMultipleUploadMode = signal(false, ...(ngDevMode ? [{ debugName: "isMultipleUploadMode" }] : [])); // Flag to track if we're in multiple upload mode
|
|
3600
|
+
// Computed signals for better relationships
|
|
3601
|
+
hasFiles = computed(() => this.files() !== null && this.files().length > 0, ...(ngDevMode ? [{ debugName: "hasFiles" }] : []));
|
|
3602
|
+
canUpload = computed(() => this.hasFiles() && !this.isUploading() && !this.disabledSignal(), ...(ngDevMode ? [{ debugName: "canUpload" }] : []));
|
|
3603
|
+
isInErrorState = computed(() => this.uploadStatus() === 'error', ...(ngDevMode ? [{ debugName: "isInErrorState" }] : []));
|
|
3604
|
+
isInSuccessState = computed(() => this.uploadStatus() === 'success', ...(ngDevMode ? [{ debugName: "isInSuccessState" }] : []));
|
|
3605
|
+
// Angular 20: Computed values using new features
|
|
3606
|
+
totalFileSize = computed(() => {
|
|
3607
|
+
if (!this.files())
|
|
3608
|
+
return 0;
|
|
3609
|
+
return Array.from(this.files()).reduce((total, file) => total + file.size, 0);
|
|
3610
|
+
}, ...(ngDevMode ? [{ debugName: "totalFileSize" }] : []));
|
|
3611
|
+
fileSizeInMB = computed(() => {
|
|
3612
|
+
// Angular 20: Using ** operator for exponentiation
|
|
3613
|
+
return (this.totalFileSize() / (1024 ** 2)).toFixed(2);
|
|
3614
|
+
}, ...(ngDevMode ? [{ debugName: "fileSizeInMB" }] : []));
|
|
3615
|
+
uploadProgressPercentage = computed(() => {
|
|
3616
|
+
// Angular 20: Using ** operator for exponentiation
|
|
3617
|
+
return Math.round(this.uploadProgress() ** 1); // Simple power operation
|
|
3618
|
+
}, ...(ngDevMode ? [{ debugName: "uploadProgressPercentage" }] : []));
|
|
3619
|
+
// ControlValueAccessor callbacks
|
|
3620
|
+
onChange = (value) => { };
|
|
3621
|
+
onTouched = () => { };
|
|
3622
|
+
onValidatorChange = () => { };
|
|
3623
|
+
// Computed values
|
|
3624
|
+
hasImages = computed(() => this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "hasImages" }] : []));
|
|
3625
|
+
isPreviewBoxMode = computed(() => this.previewBoxModeSignal() && this.showPreviewSignal(), ...(ngDevMode ? [{ debugName: "isPreviewBoxMode" }] : []));
|
|
3626
|
+
isImagePreviewAvailable = computed(() => this.showPreviewSignal() && this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "isImagePreviewAvailable" }] : []));
|
|
3627
|
+
constructor() {
|
|
3628
|
+
// Angular 20: afterRenderEffect for DOM operations
|
|
3629
|
+
afterRenderEffect(() => {
|
|
3630
|
+
// Update file input element when files change
|
|
3631
|
+
if (this.files()) {
|
|
3632
|
+
const fileInput = document.getElementById('cide-file-input-' + this.id());
|
|
3633
|
+
if (fileInput) {
|
|
3634
|
+
// Ensure the input reflects the current state
|
|
3635
|
+
fileInput.files = this.files();
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
});
|
|
3639
|
+
// Angular 20: afterNextRender for one-time DOM operations
|
|
3640
|
+
afterNextRender(() => {
|
|
3641
|
+
console.log('🎯 [FileInput] Component rendered and DOM is ready');
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
ngOnInit() {
|
|
3645
|
+
// Update signals with initial @Input() values
|
|
3646
|
+
this.labelSignal.set(this.label);
|
|
3647
|
+
this.acceptSignal.set(this.accept);
|
|
3648
|
+
this.multipleSignal.set(this.multiple);
|
|
3649
|
+
this.disabledSignal.set(this.disabled);
|
|
3650
|
+
this.requiredSignal.set(this.required);
|
|
3651
|
+
this.helperTextSignal.set(this.helperText);
|
|
3652
|
+
this.errorTextSignal.set(this.errorText);
|
|
3653
|
+
this.showPreviewSignal.set(this.showPreview);
|
|
3654
|
+
this.previewWidthSignal.set(this.previewWidth);
|
|
3655
|
+
this.previewHeightSignal.set(this.previewHeight);
|
|
3656
|
+
this.previewBoxModeSignal.set(this.previewBoxMode);
|
|
3657
|
+
this.showFileNameSignal.set(this.showFileName);
|
|
3658
|
+
this.placeholderTextSignal.set(this.placeholderText);
|
|
3659
|
+
this.placeholderIconSignal.set(this.placeholderIcon);
|
|
3660
|
+
this.autoUploadSignal.set(this.autoUpload);
|
|
3661
|
+
this.uploadDataSignal.set(this.uploadData);
|
|
3662
|
+
}
|
|
3663
|
+
ngOnChanges(changes) {
|
|
3664
|
+
// Angular 20: Update signals when @Input() values change
|
|
3665
|
+
if (changes['label'])
|
|
3666
|
+
this.labelSignal.set(this.label);
|
|
3667
|
+
if (changes['accept'])
|
|
3668
|
+
this.acceptSignal.set(this.accept);
|
|
3669
|
+
if (changes['multiple'])
|
|
3670
|
+
this.multipleSignal.set(this.multiple);
|
|
3671
|
+
if (changes['disabled'])
|
|
3672
|
+
this.disabledSignal.set(this.disabled);
|
|
3673
|
+
if (changes['required'])
|
|
3674
|
+
this.requiredSignal.set(this.required);
|
|
3675
|
+
if (changes['helperText'])
|
|
3676
|
+
this.helperTextSignal.set(this.helperText);
|
|
3677
|
+
if (changes['errorText'])
|
|
3678
|
+
this.errorTextSignal.set(this.errorText);
|
|
3679
|
+
if (changes['showPreview'])
|
|
3680
|
+
this.showPreviewSignal.set(this.showPreview);
|
|
3681
|
+
if (changes['previewWidth'])
|
|
3682
|
+
this.previewWidthSignal.set(this.previewWidth);
|
|
3683
|
+
if (changes['previewHeight'])
|
|
3684
|
+
this.previewHeightSignal.set(this.previewHeight);
|
|
3685
|
+
if (changes['previewBoxMode'])
|
|
3686
|
+
this.previewBoxModeSignal.set(this.previewBoxMode);
|
|
3687
|
+
if (changes['showFileName'])
|
|
3688
|
+
this.showFileNameSignal.set(this.showFileName);
|
|
3689
|
+
if (changes['placeholderText'])
|
|
3690
|
+
this.placeholderTextSignal.set(this.placeholderText);
|
|
3691
|
+
if (changes['placeholderIcon'])
|
|
3692
|
+
this.placeholderIconSignal.set(this.placeholderIcon);
|
|
3693
|
+
if (changes['autoUpload'])
|
|
3694
|
+
this.autoUploadSignal.set(this.autoUpload);
|
|
3695
|
+
if (changes['uploadData'])
|
|
3696
|
+
this.uploadDataSignal.set(this.uploadData);
|
|
3697
|
+
}
|
|
3698
|
+
writeValue(value) {
|
|
3699
|
+
console.log('📝 [FileInput] writeValue called with:', value);
|
|
3700
|
+
if (typeof value === 'string') {
|
|
3701
|
+
// Value is an uploaded file ID - fetch file details and set preview
|
|
3702
|
+
console.log('📝 [FileInput] Value is uploaded file ID:', value);
|
|
3703
|
+
this.files.set(null);
|
|
3704
|
+
this.fileNames.set([]);
|
|
3705
|
+
this.clearPreviews();
|
|
3706
|
+
// Fetch file details to get base64 and set preview
|
|
3707
|
+
this.loadFileDetailsFromId(value);
|
|
4127
3708
|
}
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
console.
|
|
4131
|
-
|
|
3709
|
+
else if (value instanceof FileList) {
|
|
3710
|
+
// Value is a FileList
|
|
3711
|
+
console.log('📝 [FileInput] Value is FileList:', Array.from(value).map(f => f.name));
|
|
3712
|
+
this.files.set(value);
|
|
3713
|
+
this.fileNames.set(Array.from(value).map(f => f.name));
|
|
3714
|
+
this.generatePreviews();
|
|
4132
3715
|
}
|
|
4133
|
-
|
|
3716
|
+
else {
|
|
3717
|
+
// Value is null
|
|
3718
|
+
console.log('📝 [FileInput] Value is null');
|
|
3719
|
+
this.files.set(null);
|
|
3720
|
+
this.fileNames.set([]);
|
|
3721
|
+
this.clearPreviews();
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
registerOnChange(fn) {
|
|
3725
|
+
this.onChange = fn;
|
|
3726
|
+
}
|
|
3727
|
+
registerOnTouched(fn) {
|
|
3728
|
+
this.onTouched = fn;
|
|
3729
|
+
}
|
|
3730
|
+
registerOnValidatorChange(fn) {
|
|
3731
|
+
this.onValidatorChange = fn;
|
|
4134
3732
|
}
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
3733
|
+
setDisabledState(isDisabled) {
|
|
3734
|
+
// Note: With input signals, disabled state is controlled by the parent component
|
|
3735
|
+
// This method is kept for ControlValueAccessor compatibility but doesn't modify the signal
|
|
3736
|
+
console.log('🔧 [FileInput] setDisabledState called with:', isDisabled, '(controlled by parent component)');
|
|
3737
|
+
}
|
|
3738
|
+
onFileSelected(event) {
|
|
3739
|
+
console.log('🔍 [FileInput] onFileSelected called');
|
|
3740
|
+
const input = event.target;
|
|
3741
|
+
const selectedFiles = input.files;
|
|
3742
|
+
this.files.set(selectedFiles);
|
|
3743
|
+
this.fileNames.set(selectedFiles ? Array.from(selectedFiles).map(f => f.name) : []);
|
|
3744
|
+
console.log('📁 [FileInput] Files selected:', this.fileNames());
|
|
3745
|
+
this.generatePreviews();
|
|
3746
|
+
// Reset upload status when new file is selected
|
|
3747
|
+
this.uploadStatus.set('idle');
|
|
3748
|
+
console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
|
|
3749
|
+
this.onChange(selectedFiles);
|
|
3750
|
+
this.fileChange.emit(selectedFiles);
|
|
3751
|
+
this.onTouched();
|
|
3752
|
+
// Show floating uploader if enabled and files are selected
|
|
3753
|
+
if (this.showFloatingUploaderSignal() && selectedFiles && selectedFiles.length > 0 && this.floatingUploader) {
|
|
3754
|
+
console.log('👁️ [FileInput] Showing floating uploader for', selectedFiles.length, 'files');
|
|
3755
|
+
this.floatingUploader.showUploader(this.floatingUploaderGroupIdSignal());
|
|
3756
|
+
}
|
|
3757
|
+
// Auto upload if enabled
|
|
3758
|
+
if (this.autoUploadSignal() && selectedFiles && selectedFiles.length > 0) {
|
|
3759
|
+
if (this.multipleSignal()) {
|
|
3760
|
+
console.log('🚀 [FileInput] Auto upload enabled for multiple files mode:', selectedFiles.length, 'files');
|
|
3761
|
+
this.uploadMultipleFiles(Array.from(selectedFiles));
|
|
4145
3762
|
}
|
|
4146
3763
|
else {
|
|
4147
|
-
|
|
4148
|
-
this.
|
|
3764
|
+
console.log('🚀 [FileInput] Auto upload enabled for single file mode:', selectedFiles[0].name);
|
|
3765
|
+
this.uploadFile(selectedFiles[0]);
|
|
4149
3766
|
}
|
|
4150
3767
|
}
|
|
3768
|
+
else {
|
|
3769
|
+
console.log('⏸️ [FileInput] Auto upload disabled or no files');
|
|
3770
|
+
}
|
|
4151
3771
|
}
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
this.
|
|
4159
|
-
|
|
4160
|
-
|
|
3772
|
+
clearFiles() {
|
|
3773
|
+
console.log('🗑️ [FileInput] clearFiles called');
|
|
3774
|
+
this.files.set(null);
|
|
3775
|
+
this.fileNames.set([]);
|
|
3776
|
+
this.clearPreviews();
|
|
3777
|
+
this.uploadStatus.set('idle');
|
|
3778
|
+
console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
|
|
3779
|
+
this.onChange(null);
|
|
3780
|
+
this.fileChange.emit(null);
|
|
3781
|
+
}
|
|
3782
|
+
uploadFile(file) {
|
|
3783
|
+
console.log('📤 [FileInput] uploadFile called for:', file.name, 'Size:', file.size, 'bytes');
|
|
3784
|
+
// Angular 20: Use PendingTasks for better loading state management
|
|
3785
|
+
// const uploadTask = this.pendingTasks.add(); // TODO: Fix PendingTasks API usage
|
|
3786
|
+
// console.log('⏳ [FileInput] Pending task added for upload tracking');
|
|
3787
|
+
// Set upload status to 'start' before starting upload
|
|
3788
|
+
this.uploadStatus.set('start');
|
|
3789
|
+
console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
|
|
3790
|
+
this.isUploading.set(true);
|
|
3791
|
+
this.uploadProgress.set(0);
|
|
3792
|
+
this.uploadProgressChange.emit(0);
|
|
3793
|
+
console.log('📊 [FileInput] Upload progress initialized to 0%');
|
|
3794
|
+
// Make form control invalid during upload - this prevents form submission
|
|
3795
|
+
this.onChange(null);
|
|
3796
|
+
console.log('🚫 [FileInput] Form control value set to null to prevent submission');
|
|
3797
|
+
// Show initial progress notification with spinner (persistent - no auto-dismiss)
|
|
3798
|
+
const notificationId = this.notificationService.showProgress('🔄 Preparing file upload...', 0, { duration: 0 });
|
|
3799
|
+
this.uploadNotificationId.set(notificationId);
|
|
3800
|
+
console.log('🔔 [FileInput] Progress notification started with ID:', notificationId);
|
|
3801
|
+
this.fileManagerService.uploadFile(file, this.uploadDataSignal(), (progress) => {
|
|
3802
|
+
// Real progress callback from file manager service
|
|
3803
|
+
this.uploadProgress.set(progress);
|
|
3804
|
+
this.uploadProgressChange.emit(progress);
|
|
3805
|
+
console.log('📈 [FileInput] Upload progress:', Math.round(progress) + '%');
|
|
3806
|
+
// Set upload status to 'uploading' when progress starts
|
|
3807
|
+
if (this.uploadStatus() === 'start') {
|
|
3808
|
+
this.uploadStatus.set('uploading');
|
|
3809
|
+
console.log('🔄 [FileInput] Upload status changed to:', this.uploadStatus());
|
|
3810
|
+
}
|
|
3811
|
+
// Update progress notification with spinner
|
|
3812
|
+
const notificationId = this.uploadNotificationId();
|
|
3813
|
+
if (notificationId) {
|
|
3814
|
+
let progressMessage = '';
|
|
3815
|
+
if (progress < 10) {
|
|
3816
|
+
progressMessage = '🔄 Starting upload...';
|
|
3817
|
+
}
|
|
3818
|
+
else if (progress < 25) {
|
|
3819
|
+
progressMessage = '🔄 Uploading file...';
|
|
3820
|
+
}
|
|
3821
|
+
else if (progress < 50) {
|
|
3822
|
+
progressMessage = '🔄 Upload in progress...';
|
|
3823
|
+
}
|
|
3824
|
+
else if (progress < 75) {
|
|
3825
|
+
progressMessage = '🔄 Almost done...';
|
|
3826
|
+
}
|
|
3827
|
+
else if (progress < 95) {
|
|
3828
|
+
progressMessage = '🔄 Finishing upload...';
|
|
3829
|
+
}
|
|
3830
|
+
else {
|
|
3831
|
+
progressMessage = '🔄 Finalizing...';
|
|
3832
|
+
}
|
|
3833
|
+
this.notificationService.updateProgress(notificationId, progress, progressMessage);
|
|
3834
|
+
}
|
|
3835
|
+
}).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
4161
3836
|
next: (response) => {
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
this.
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
3837
|
+
console.log('🎉 [FileInput] Upload SUCCESS - Response received:', response);
|
|
3838
|
+
// Angular 20: Complete the pending task
|
|
3839
|
+
// this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
|
|
3840
|
+
// console.log('✅ [FileInput] Pending task completed for successful upload');
|
|
3841
|
+
// Set upload status to 'success'
|
|
3842
|
+
this.uploadStatus.set('success');
|
|
3843
|
+
console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
|
|
3844
|
+
// Complete the progress
|
|
3845
|
+
this.uploadProgress.set(100);
|
|
3846
|
+
this.uploadProgressChange.emit(100);
|
|
3847
|
+
console.log('📊 [FileInput] Upload progress completed: 100%');
|
|
3848
|
+
// Update progress notification to complete
|
|
3849
|
+
const notificationId = this.uploadNotificationId();
|
|
3850
|
+
if (notificationId) {
|
|
3851
|
+
this.notificationService.remove(notificationId);
|
|
3852
|
+
console.log('🔔 [FileInput] Progress notification removed');
|
|
3853
|
+
}
|
|
3854
|
+
// Success notification removed for cleaner UX
|
|
3855
|
+
this.uploadNotificationId.set(null);
|
|
3856
|
+
// Extract ID from CoreFileManagerInsertUpdateResponse
|
|
3857
|
+
const uploadedId = response?.data?.core_file_manager?.[0]?.cyfm_id;
|
|
3858
|
+
if (uploadedId) {
|
|
3859
|
+
console.log('✅ [FileInput] File uploaded successfully with ID:', uploadedId);
|
|
3860
|
+
// Set the uploaded ID as the form control value
|
|
3861
|
+
this.onChange(uploadedId);
|
|
3862
|
+
console.log('📝 [FileInput] Form control value set to uploaded ID:', uploadedId);
|
|
3863
|
+
// Only emit individual uploadSuccess if not in multiple upload mode
|
|
3864
|
+
if (!this.isMultipleUploadMode()) {
|
|
3865
|
+
this.uploadSuccess.emit(uploadedId);
|
|
3866
|
+
console.log('📝 [FileInput] Upload success event emitted with file ID:', uploadedId);
|
|
3867
|
+
}
|
|
3868
|
+
else {
|
|
3869
|
+
console.log('📝 [FileInput] Individual upload success suppressed (multiple upload mode) - file ID:', uploadedId);
|
|
3870
|
+
}
|
|
4168
3871
|
}
|
|
4169
3872
|
else {
|
|
4170
|
-
console.error('❌ [
|
|
4171
|
-
this.
|
|
3873
|
+
console.error('❌ [FileInput] Upload successful but no ID returned:', response);
|
|
3874
|
+
this.uploadError.emit('Upload successful but no ID returned');
|
|
4172
3875
|
}
|
|
3876
|
+
this.isUploading.set(false);
|
|
3877
|
+
console.log('🔄 [FileInput] isUploading set to false');
|
|
4173
3878
|
},
|
|
4174
3879
|
error: (error) => {
|
|
4175
|
-
console.error('
|
|
4176
|
-
|
|
3880
|
+
console.error('💥 [FileInput] Upload FAILED:', error);
|
|
3881
|
+
// Angular 20: Complete the pending task even on error
|
|
3882
|
+
// this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
|
|
3883
|
+
// console.log('❌ [FileInput] Pending task completed for failed upload');
|
|
3884
|
+
// Set upload status to 'error' and remove upload validation error
|
|
3885
|
+
this.uploadStatus.set('error');
|
|
3886
|
+
console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
|
|
3887
|
+
// Remove progress notification and show error
|
|
3888
|
+
const notificationId = this.uploadNotificationId();
|
|
3889
|
+
if (notificationId) {
|
|
3890
|
+
this.notificationService.remove(notificationId);
|
|
3891
|
+
console.log('🔔 [FileInput] Progress notification removed due to error');
|
|
3892
|
+
}
|
|
3893
|
+
this.notificationService.error(`❌ File upload failed: ${error.message || error.error?.message || 'Unknown error occurred'}`, { duration: 0 });
|
|
3894
|
+
this.uploadNotificationId.set(null);
|
|
3895
|
+
this.uploadError.emit(error.message || error.error?.message || 'Upload failed');
|
|
3896
|
+
this.isUploading.set(false);
|
|
3897
|
+
this.uploadProgress.set(0);
|
|
3898
|
+
this.uploadProgressChange.emit(0);
|
|
3899
|
+
console.log('🔄 [FileInput] Upload state reset - isUploading: false, progress: 0%');
|
|
4177
3900
|
}
|
|
4178
3901
|
});
|
|
4179
3902
|
}
|
|
4180
3903
|
/**
|
|
4181
|
-
* Upload files with group ID
|
|
3904
|
+
* Upload multiple files with group ID support
|
|
3905
|
+
* FLOW: 1) Generate group ID first, 2) Upload all files with same group ID, 3) Emit group ID on completion
|
|
4182
3906
|
*/
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
this.
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
uploadedFile: response
|
|
4226
|
-
});
|
|
4227
|
-
if (responseFile) {
|
|
4228
|
-
uploadedFiles.push(responseFile);
|
|
3907
|
+
uploadMultipleFiles(files) {
|
|
3908
|
+
console.log('📤 [FileInput] uploadMultipleFiles called for:', files.length, 'files');
|
|
3909
|
+
console.log('🔄 [FileInput] STEP 1: Generate group ID before starting any file uploads');
|
|
3910
|
+
// Set multiple upload mode flag
|
|
3911
|
+
this.isMultipleUploadMode.set(true);
|
|
3912
|
+
// Set upload status to 'start' before starting upload
|
|
3913
|
+
this.uploadStatus.set('start');
|
|
3914
|
+
this.isUploading.set(true);
|
|
3915
|
+
this.uploadProgress.set(0);
|
|
3916
|
+
this.uploadProgressChange.emit(0);
|
|
3917
|
+
// Make form control invalid during upload
|
|
3918
|
+
this.onChange(null);
|
|
3919
|
+
// Show initial progress notification
|
|
3920
|
+
const notificationId = this.notificationService.showProgress('🔄 Preparing multiple file upload...', 0, { duration: 0 });
|
|
3921
|
+
this.uploadNotificationId.set(notificationId);
|
|
3922
|
+
// STEP 1: Generate or get group ID BEFORE starting any file uploads
|
|
3923
|
+
const existingGroupId = this.uploadDataSignal().groupId;
|
|
3924
|
+
if (existingGroupId) {
|
|
3925
|
+
console.log('🆔 [FileInput] STEP 1 COMPLETE: Using existing group ID:', existingGroupId);
|
|
3926
|
+
console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', existingGroupId);
|
|
3927
|
+
this.groupId.set(existingGroupId);
|
|
3928
|
+
this.startMulti(files, existingGroupId);
|
|
3929
|
+
}
|
|
3930
|
+
else {
|
|
3931
|
+
console.log('🆔 [FileInput] No existing group ID, generating new one...');
|
|
3932
|
+
// Generate group ID BEFORE starting any file uploads
|
|
3933
|
+
this.fileManagerService.generateObjectId().subscribe({
|
|
3934
|
+
next: (response) => {
|
|
3935
|
+
const newGroupId = response.data?.objectId;
|
|
3936
|
+
console.log('🆔 [FileInput] STEP 1 COMPLETE: Generated new group ID:', newGroupId);
|
|
3937
|
+
console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', newGroupId);
|
|
3938
|
+
this.groupId.set(newGroupId);
|
|
3939
|
+
this.startMulti(files, newGroupId);
|
|
3940
|
+
},
|
|
3941
|
+
error: (error) => {
|
|
3942
|
+
console.error('❌ [FileInput] Failed to generate group ID:', error);
|
|
3943
|
+
this.uploadError.emit('Failed to generate group ID');
|
|
3944
|
+
this.isUploading.set(false);
|
|
3945
|
+
this.uploadStatus.set('error');
|
|
3946
|
+
const notificationId = this.uploadNotificationId();
|
|
3947
|
+
if (notificationId) {
|
|
3948
|
+
this.notificationService.remove(notificationId);
|
|
4229
3949
|
}
|
|
4230
|
-
|
|
4231
|
-
|
|
3950
|
+
this.notificationService.error('❌ Failed to generate group ID for multiple file upload', { duration: 0 });
|
|
3951
|
+
this.uploadNotificationId.set(null);
|
|
3952
|
+
}
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
/**
|
|
3957
|
+
* Start uploading multiple files with the provided group ID
|
|
3958
|
+
* All files will be uploaded with the SAME group ID that was generated before this method
|
|
3959
|
+
*/
|
|
3960
|
+
startMulti(files, groupId) {
|
|
3961
|
+
console.log('🚀 [FileInput] STEP 2: Starting upload for', files.length, 'files with group ID:', groupId);
|
|
3962
|
+
console.log('📋 [FileInput] All files will use the same group ID:', groupId);
|
|
3963
|
+
let completedUploads = 0;
|
|
3964
|
+
let failedUploads = 0;
|
|
3965
|
+
const totalFiles = files.length;
|
|
3966
|
+
// IMPORTANT: All files use the SAME group ID that was generated before starting uploads
|
|
3967
|
+
const uploadDataWithGroupId = {
|
|
3968
|
+
...this.uploadDataSignal(),
|
|
3969
|
+
groupId: groupId
|
|
3970
|
+
};
|
|
3971
|
+
files.forEach((file, index) => {
|
|
3972
|
+
console.log(`📤 [FileInput] Uploading file ${index + 1}/${totalFiles}: "${file.name}" with group ID: ${groupId}`);
|
|
3973
|
+
this.fileManagerService.uploadFile(file, uploadDataWithGroupId, (progress) => {
|
|
3974
|
+
// Calculate overall progress
|
|
3975
|
+
const fileProgress = progress / totalFiles;
|
|
3976
|
+
const overallProgress = ((completedUploads * 100) + fileProgress) / totalFiles;
|
|
3977
|
+
this.uploadProgress.set(overallProgress);
|
|
3978
|
+
this.uploadProgressChange.emit(overallProgress);
|
|
3979
|
+
// Update progress notification
|
|
3980
|
+
const notificationId = this.uploadNotificationId();
|
|
3981
|
+
if (notificationId) {
|
|
3982
|
+
const progressMessage = `🔄 Uploading file ${index + 1} of ${totalFiles}...`;
|
|
3983
|
+
this.notificationService.updateProgress(notificationId, overallProgress, progressMessage);
|
|
3984
|
+
}
|
|
3985
|
+
}).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
3986
|
+
next: (response) => {
|
|
3987
|
+
completedUploads++;
|
|
3988
|
+
console.log(`✅ [FileInput] File ${index + 1}/${totalFiles} uploaded`);
|
|
4232
3989
|
// Check if all files are completed
|
|
4233
|
-
if (
|
|
4234
|
-
this.handleMultipleUploadComplete(
|
|
3990
|
+
if (completedUploads + failedUploads === totalFiles) {
|
|
3991
|
+
this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
|
|
4235
3992
|
}
|
|
4236
3993
|
},
|
|
4237
3994
|
error: (error) => {
|
|
4238
|
-
|
|
4239
|
-
console.error(`❌ [
|
|
4240
|
-
this.updateUploadStatus(upload.fileId, 'error', 0, error.message || 'Upload failed');
|
|
4241
|
-
this.uploadError.emit({
|
|
4242
|
-
fileId: upload.fileId,
|
|
4243
|
-
error: error.message || 'Upload failed'
|
|
4244
|
-
});
|
|
3995
|
+
failedUploads++;
|
|
3996
|
+
console.error(`❌ [FileInput] File ${index + 1}/${totalFiles} upload failed:`, error);
|
|
4245
3997
|
// Check if all files are completed
|
|
4246
|
-
if (
|
|
4247
|
-
this.handleMultipleUploadComplete(
|
|
3998
|
+
if (completedUploads + failedUploads === totalFiles) {
|
|
3999
|
+
this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
|
|
4248
4000
|
}
|
|
4249
4001
|
}
|
|
4250
4002
|
});
|
|
4251
4003
|
});
|
|
4252
4004
|
}
|
|
4253
4005
|
/**
|
|
4254
|
-
* Handle multiple upload
|
|
4006
|
+
* Handle completion of multiple file upload
|
|
4255
4007
|
*/
|
|
4256
|
-
handleMultipleUploadComplete(completed, failed,
|
|
4257
|
-
console.log(`📊 [
|
|
4008
|
+
handleMultipleUploadComplete(completed, failed, total, groupId) {
|
|
4009
|
+
console.log(`📊 [FileInput] Multiple upload complete: ${completed}/${total} successful, ${failed} failed`);
|
|
4258
4010
|
this.isUploading.set(false);
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4011
|
+
this.uploadProgress.set(100);
|
|
4012
|
+
this.uploadProgressChange.emit(100);
|
|
4013
|
+
// Remove progress notification
|
|
4014
|
+
const notificationId = this.uploadNotificationId();
|
|
4015
|
+
if (notificationId) {
|
|
4016
|
+
this.notificationService.remove(notificationId);
|
|
4017
|
+
}
|
|
4018
|
+
this.uploadNotificationId.set(null);
|
|
4019
|
+
if (failed === 0) {
|
|
4020
|
+
// All files uploaded successfully
|
|
4021
|
+
this.uploadStatus.set('success');
|
|
4022
|
+
// Success notification removed for cleaner UX
|
|
4023
|
+
// STEP 3: For multiple file upload, emit the group ID (not individual file IDs)
|
|
4024
|
+
this.onChange(groupId);
|
|
4025
|
+
this.uploadSuccess.emit(groupId);
|
|
4026
|
+
console.log('📝 [FileInput] Multiple upload completed with group ID:', groupId);
|
|
4027
|
+
}
|
|
4028
|
+
else if (completed > 0) {
|
|
4029
|
+
// Some files uploaded successfully
|
|
4030
|
+
this.uploadStatus.set('error');
|
|
4031
|
+
this.notificationService.warning(`⚠️ ${completed}/${total} files uploaded. ${failed} failed.`, { duration: 0 });
|
|
4032
|
+
this.uploadError.emit(`${failed} out of ${total} files failed to upload`);
|
|
4033
|
+
}
|
|
4034
|
+
else {
|
|
4035
|
+
// All files failed
|
|
4036
|
+
this.uploadStatus.set('error');
|
|
4037
|
+
this.notificationService.error(`❌ All ${total} files failed to upload.`, { duration: 0 });
|
|
4038
|
+
this.uploadError.emit('All files failed to upload');
|
|
4264
4039
|
}
|
|
4040
|
+
// Reset multiple upload mode flag
|
|
4041
|
+
this.isMultipleUploadMode.set(false);
|
|
4265
4042
|
}
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
const pendingUpload = this.uploadQueue().find(upload => upload.status === 'pending');
|
|
4271
|
-
if (!pendingUpload) {
|
|
4272
|
-
this.isUploading.set(false);
|
|
4043
|
+
generatePreviews() {
|
|
4044
|
+
// Clear existing previews
|
|
4045
|
+
this.clearPreviews();
|
|
4046
|
+
if (!this.showPreviewSignal() || !this.files()) {
|
|
4273
4047
|
return;
|
|
4274
4048
|
}
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
})
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4049
|
+
Array.from(this.files()).forEach(file => {
|
|
4050
|
+
if (this.isImageFile(file)) {
|
|
4051
|
+
const reader = new FileReader();
|
|
4052
|
+
reader.onload = (e) => {
|
|
4053
|
+
if (e.target?.result) {
|
|
4054
|
+
this.previewUrls.update(urls => [...urls, e.target.result]);
|
|
4055
|
+
}
|
|
4056
|
+
};
|
|
4057
|
+
reader.readAsDataURL(file);
|
|
4058
|
+
}
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
clearPreviews() {
|
|
4062
|
+
// Revoke object URLs to prevent memory leaks
|
|
4063
|
+
this.previewUrls().forEach(url => {
|
|
4064
|
+
if (url.startsWith('blob:')) {
|
|
4065
|
+
URL.revokeObjectURL(url);
|
|
4066
|
+
}
|
|
4067
|
+
});
|
|
4068
|
+
this.previewUrls.set([]);
|
|
4069
|
+
}
|
|
4070
|
+
isImageFile(file) {
|
|
4071
|
+
return file.type.startsWith('image/');
|
|
4072
|
+
}
|
|
4073
|
+
loadFileDetailsFromId(fileId) {
|
|
4074
|
+
console.log('🔍 [FileInput] Loading file details for ID:', fileId);
|
|
4075
|
+
if (!fileId)
|
|
4076
|
+
return;
|
|
4077
|
+
this.fileManagerService?.getFileDetails({ cyfm_id: fileId })?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
4078
|
+
next: (fileDetails) => {
|
|
4079
|
+
console.log('📋 [FileInput] File details received:', fileDetails);
|
|
4080
|
+
if (fileDetails?.data?.length) {
|
|
4081
|
+
const fileData = fileDetails.data[0];
|
|
4082
|
+
console.log('📁 [FileInput] File data:', fileData);
|
|
4083
|
+
// Set file name from the details
|
|
4084
|
+
if (fileData.cyfm_name) {
|
|
4085
|
+
this.fileNames.set([fileData.cyfm_name]);
|
|
4086
|
+
console.log('📝 [FileInput] File name set:', fileData.cyfm_name);
|
|
4087
|
+
}
|
|
4088
|
+
// If it's an image and we have base64 data, set preview
|
|
4089
|
+
if (this.showPreviewSignal() && fileData.cyfm_file_base64) {
|
|
4090
|
+
// Check if it's an image file based on file name or type
|
|
4091
|
+
const isImage = this.isImageFileFromName(fileData.cyfm_name || '') ||
|
|
4092
|
+
this.isImageFileFromType(fileData.cyfm_type || '');
|
|
4093
|
+
if (isImage) {
|
|
4094
|
+
// Add data URL prefix if not already present
|
|
4095
|
+
let base64Data = fileData.cyfm_file_base64;
|
|
4096
|
+
if (!base64Data.startsWith('data:')) {
|
|
4097
|
+
const mimeType = fileData.cyfm_type || 'image/jpeg';
|
|
4098
|
+
base64Data = `data:${mimeType};base64,${base64Data}`;
|
|
4099
|
+
}
|
|
4100
|
+
this.previewUrls.set([base64Data]);
|
|
4101
|
+
console.log('🖼️ [FileInput] Preview set from base64 data');
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4298
4104
|
}
|
|
4299
4105
|
else {
|
|
4300
|
-
|
|
4301
|
-
this.updateUploadStatus(pendingUpload.fileId, 'completed', 100, undefined, response);
|
|
4106
|
+
console.warn('⚠️ [FileInput] No file data found for ID:', fileId);
|
|
4302
4107
|
}
|
|
4303
|
-
this.uploadComplete.emit({
|
|
4304
|
-
fileId: tempUniqueId || pendingUpload.fileId,
|
|
4305
|
-
fileName: pendingUpload.fileName,
|
|
4306
|
-
progress: 100,
|
|
4307
|
-
status: 'completed',
|
|
4308
|
-
uploadedFile: response
|
|
4309
|
-
});
|
|
4310
|
-
// Process next file
|
|
4311
|
-
setTimeout(() => this.uploadNextFile(), 500);
|
|
4312
4108
|
},
|
|
4313
4109
|
error: (error) => {
|
|
4314
|
-
console.error('❌ [
|
|
4315
|
-
this.
|
|
4316
|
-
this.uploadError.emit({
|
|
4317
|
-
fileId: pendingUpload.fileId,
|
|
4318
|
-
error: error.message || 'Upload failed'
|
|
4319
|
-
});
|
|
4320
|
-
// Process next file
|
|
4321
|
-
setTimeout(() => this.uploadNextFile(), 500);
|
|
4110
|
+
console.error('❌ [FileInput] Error loading file details:', error);
|
|
4111
|
+
this.notificationService.error(`Failed to load file details: ${error.message || 'Unknown error'}`, { duration: 0 });
|
|
4322
4112
|
}
|
|
4323
4113
|
});
|
|
4324
4114
|
}
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
: upload));
|
|
4332
|
-
}
|
|
4333
|
-
/**
|
|
4334
|
-
* Update upload status
|
|
4335
|
-
*/
|
|
4336
|
-
updateUploadStatus(fileId, status, progress, error, uploadedFile) {
|
|
4337
|
-
console.log('🔄 [GlobalFileUploader] Updating upload status:', { fileId, status, progress, error: !!error, uploadedFile: !!uploadedFile });
|
|
4338
|
-
this.uploadQueue.update(queue => {
|
|
4339
|
-
const updatedQueue = queue.map(upload => {
|
|
4340
|
-
if (upload.fileId === fileId) {
|
|
4341
|
-
console.log('🔄 [GlobalFileUploader] Found upload to update:', upload.fileName, 'from', upload.status, 'to', status);
|
|
4342
|
-
return { ...upload, status, progress, error, uploadedFile };
|
|
4343
|
-
}
|
|
4344
|
-
return upload;
|
|
4345
|
-
});
|
|
4346
|
-
console.log('🔄 [GlobalFileUploader] Updated queue:', updatedQueue.map(u => ({ fileName: u.fileName, status: u.status })));
|
|
4347
|
-
return updatedQueue;
|
|
4348
|
-
});
|
|
4115
|
+
isImageFileFromName(fileName) {
|
|
4116
|
+
if (!fileName)
|
|
4117
|
+
return false;
|
|
4118
|
+
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
|
|
4119
|
+
const lowerFileName = fileName.toLowerCase();
|
|
4120
|
+
return imageExtensions.some(ext => lowerFileName.endsWith(ext));
|
|
4349
4121
|
}
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4122
|
+
isImageFileFromType(fileType) {
|
|
4123
|
+
if (!fileType)
|
|
4124
|
+
return false;
|
|
4125
|
+
return fileType.startsWith('image/');
|
|
4126
|
+
}
|
|
4127
|
+
removePreview(index) {
|
|
4128
|
+
const currentFiles = this.files();
|
|
4129
|
+
const currentUrls = this.previewUrls();
|
|
4130
|
+
if (currentFiles && currentFiles.length > index) {
|
|
4131
|
+
// Handle FileList case - remove file from FileList
|
|
4132
|
+
const dt = new DataTransfer();
|
|
4133
|
+
Array.from(currentFiles).forEach((file, i) => {
|
|
4134
|
+
if (i !== index) {
|
|
4135
|
+
dt.items.add(file);
|
|
4361
4136
|
}
|
|
4362
|
-
return upload;
|
|
4363
4137
|
});
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4138
|
+
const newFiles = dt.files;
|
|
4139
|
+
this.files.set(newFiles);
|
|
4140
|
+
this.fileNames.set(Array.from(newFiles).map(f => f.name));
|
|
4141
|
+
// Remove the preview URL
|
|
4142
|
+
if (currentUrls[index] && currentUrls[index].startsWith('blob:')) {
|
|
4143
|
+
URL.revokeObjectURL(currentUrls[index]);
|
|
4144
|
+
}
|
|
4145
|
+
this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
|
|
4146
|
+
this.onChange(newFiles);
|
|
4147
|
+
this.fileChange.emit(newFiles);
|
|
4148
|
+
}
|
|
4149
|
+
else if (currentUrls.length > index) {
|
|
4150
|
+
// Handle uploaded file ID case - clear the preview and set control value to null
|
|
4151
|
+
console.log('🗑️ [FileInput] Removing preview for uploaded file ID');
|
|
4152
|
+
// Clear preview
|
|
4153
|
+
this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
|
|
4154
|
+
this.fileNames.set([]);
|
|
4155
|
+
// Set control value to null since we're removing the uploaded file
|
|
4156
|
+
this.onChange(null);
|
|
4157
|
+
this.fileChange.emit(null);
|
|
4158
|
+
}
|
|
4367
4159
|
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
this.
|
|
4160
|
+
ngOnDestroy() {
|
|
4161
|
+
// Clean up preview URLs to prevent memory leaks
|
|
4162
|
+
this.clearPreviews();
|
|
4163
|
+
// Clean up any active upload notification
|
|
4164
|
+
const notificationId = this.uploadNotificationId();
|
|
4165
|
+
if (notificationId) {
|
|
4166
|
+
this.notificationService.remove(notificationId);
|
|
4167
|
+
this.uploadNotificationId.set(null);
|
|
4375
4168
|
}
|
|
4376
|
-
this.isUploading.set(false);
|
|
4377
4169
|
}
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
this.uploadCancelled.emit(fileId);
|
|
4384
|
-
// Remove from queue after a delay
|
|
4385
|
-
setTimeout(() => {
|
|
4386
|
-
this.uploadQueue.update(queue => queue.filter(upload => upload.fileId !== fileId));
|
|
4387
|
-
}, 1000);
|
|
4170
|
+
triggerFileSelect() {
|
|
4171
|
+
const fileInput = document.getElementById('cide-file-input-' + this.id());
|
|
4172
|
+
if (fileInput && !this.disabledSignal()) {
|
|
4173
|
+
fileInput.click();
|
|
4174
|
+
}
|
|
4388
4175
|
}
|
|
4389
4176
|
/**
|
|
4390
|
-
*
|
|
4177
|
+
* Show floating uploader manually
|
|
4178
|
+
* This can be called to show the floating uploader even when no files are selected
|
|
4391
4179
|
*/
|
|
4392
|
-
|
|
4393
|
-
this.
|
|
4394
|
-
|
|
4180
|
+
showUploader() {
|
|
4181
|
+
if (this.floatingUploader && this.showFloatingUploaderSignal()) {
|
|
4182
|
+
console.log('👁️ [FileInput] Manually showing floating uploader');
|
|
4183
|
+
this.floatingUploader.showUploader(this.floatingUploaderGroupIdSignal());
|
|
4184
|
+
}
|
|
4395
4185
|
}
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
this.
|
|
4186
|
+
// Drag and Drop Event Handlers
|
|
4187
|
+
onDragOver(event) {
|
|
4188
|
+
event.preventDefault();
|
|
4189
|
+
event.stopPropagation();
|
|
4190
|
+
if (!this.disabledSignal()) {
|
|
4191
|
+
this.isDragOver.set(true);
|
|
4192
|
+
console.log('🔄 [FileInput] Drag over detected');
|
|
4193
|
+
}
|
|
4401
4194
|
}
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4195
|
+
onDragLeave(event) {
|
|
4196
|
+
event.preventDefault();
|
|
4197
|
+
event.stopPropagation();
|
|
4198
|
+
this.isDragOver.set(false);
|
|
4199
|
+
console.log('🔄 [FileInput] Drag leave detected');
|
|
4407
4200
|
}
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4201
|
+
onDragEnter(event) {
|
|
4202
|
+
event.preventDefault();
|
|
4203
|
+
event.stopPropagation();
|
|
4204
|
+
if (!this.disabledSignal()) {
|
|
4205
|
+
this.isDragOver.set(true);
|
|
4206
|
+
console.log('🔄 [FileInput] Drag enter detected');
|
|
4207
|
+
}
|
|
4414
4208
|
}
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4209
|
+
onDrop(event) {
|
|
4210
|
+
event.preventDefault();
|
|
4211
|
+
event.stopPropagation();
|
|
4212
|
+
this.isDragOver.set(false);
|
|
4213
|
+
if (this.disabledSignal()) {
|
|
4214
|
+
console.log('⏸️ [FileInput] Drop ignored - component is disabled');
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
const files = event.dataTransfer?.files;
|
|
4218
|
+
if (files && files.length > 0) {
|
|
4219
|
+
console.log('📁 [FileInput] Files dropped:', Array.from(files).map(f => f.name));
|
|
4220
|
+
// Validate file types if accept is specified
|
|
4221
|
+
if (this.acceptSignal() && !this.validateFileTypes(files)) {
|
|
4222
|
+
console.log('❌ [FileInput] Invalid file types dropped');
|
|
4223
|
+
this.notificationService.error('❌ Invalid file type. Please select files of the correct type.', { duration: 0 });
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
// Handle single vs multiple files
|
|
4227
|
+
if (!this.multipleSignal() && files.length > 1) {
|
|
4228
|
+
console.log('⚠️ [FileInput] Multiple files dropped but multiple is disabled');
|
|
4229
|
+
this.notificationService.warning('⚠️ Only one file is allowed. Using the first file.', { duration: 0 });
|
|
4230
|
+
// Create a new FileList with only the first file
|
|
4231
|
+
const dt = new DataTransfer();
|
|
4232
|
+
dt.items.add(files[0]);
|
|
4233
|
+
this.handleFileSelection(dt.files);
|
|
4234
|
+
}
|
|
4235
|
+
else {
|
|
4236
|
+
this.handleFileSelection(files);
|
|
4237
|
+
}
|
|
4426
4238
|
}
|
|
4427
4239
|
}
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4240
|
+
validateFileTypes(files) {
|
|
4241
|
+
const acceptTypes = this.acceptSignal().split(',').map(type => type.trim());
|
|
4242
|
+
if (acceptTypes.length === 0 || acceptTypes[0] === '')
|
|
4243
|
+
return true;
|
|
4244
|
+
return Array.from(files).every(file => {
|
|
4245
|
+
return acceptTypes.some(acceptType => {
|
|
4246
|
+
if (acceptType.startsWith('.')) {
|
|
4247
|
+
// Extension-based validation
|
|
4248
|
+
return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
|
|
4249
|
+
}
|
|
4250
|
+
else if (acceptType.includes('/')) {
|
|
4251
|
+
// MIME type validation
|
|
4252
|
+
return file.type === acceptType || file.type.startsWith(acceptType.replace('*', ''));
|
|
4253
|
+
}
|
|
4254
|
+
return false;
|
|
4255
|
+
});
|
|
4256
|
+
});
|
|
4257
|
+
}
|
|
4258
|
+
handleFileSelection(files) {
|
|
4259
|
+
this.files.set(files);
|
|
4260
|
+
this.fileNames.set(Array.from(files).map(f => f.name));
|
|
4261
|
+
console.log('📁 [FileInput] Files selected via drag & drop:', this.fileNames());
|
|
4262
|
+
this.generatePreviews();
|
|
4263
|
+
// Reset upload status when new file is selected
|
|
4264
|
+
this.uploadStatus.set('idle');
|
|
4265
|
+
console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
|
|
4266
|
+
this.onChange(files);
|
|
4267
|
+
this.fileChange.emit(files);
|
|
4268
|
+
this.onTouched();
|
|
4269
|
+
// Show floating uploader if enabled and files are selected
|
|
4270
|
+
if (this.showFloatingUploaderSignal() && files.length > 0 && this.floatingUploader) {
|
|
4271
|
+
console.log('👁️ [FileInput] Showing floating uploader for dropped files:', files.length, 'files');
|
|
4272
|
+
this.floatingUploader.showUploader(this.floatingUploaderGroupIdSignal());
|
|
4273
|
+
}
|
|
4274
|
+
// Auto upload if enabled
|
|
4275
|
+
if (this.autoUploadSignal() && files.length > 0) {
|
|
4276
|
+
if (this.multipleSignal()) {
|
|
4277
|
+
console.log('🚀 [FileInput] Auto upload enabled for multiple files mode (drag & drop):', files.length, 'files');
|
|
4278
|
+
this.uploadMultipleFiles(Array.from(files));
|
|
4279
|
+
}
|
|
4280
|
+
else {
|
|
4281
|
+
console.log('🚀 [FileInput] Auto upload enabled for single file mode (drag & drop):', files[0].name);
|
|
4282
|
+
this.uploadFile(files[0]);
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
else {
|
|
4286
|
+
console.log('⏸️ [FileInput] Auto upload disabled or no files');
|
|
4439
4287
|
}
|
|
4440
4288
|
}
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
*/
|
|
4444
|
-
getFileSizeDisplay(file) {
|
|
4445
|
-
const bytes = file.size;
|
|
4446
|
-
if (bytes === 0)
|
|
4447
|
-
return '0 Bytes';
|
|
4448
|
-
const k = 1024;
|
|
4449
|
-
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
4450
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
4451
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
4289
|
+
isRequired() {
|
|
4290
|
+
return this.requiredSignal();
|
|
4452
4291
|
}
|
|
4453
4292
|
/**
|
|
4454
|
-
*
|
|
4455
|
-
*
|
|
4293
|
+
* Angular 20: Utility method to get upload data with proper typing
|
|
4294
|
+
* @returns Properly typed upload data
|
|
4456
4295
|
*/
|
|
4457
|
-
|
|
4458
|
-
return
|
|
4296
|
+
getUploadData() {
|
|
4297
|
+
return this.uploadDataSignal();
|
|
4459
4298
|
}
|
|
4460
4299
|
/**
|
|
4461
|
-
*
|
|
4300
|
+
* Angular 20: Utility method to update upload data with type safety
|
|
4301
|
+
* @param data Partial upload data to merge with existing data
|
|
4462
4302
|
*/
|
|
4463
|
-
|
|
4303
|
+
updateUploadData(data) {
|
|
4304
|
+
const currentData = this.uploadDataSignal();
|
|
4305
|
+
const updatedData = { ...currentData, ...data };
|
|
4306
|
+
// Note: This would require the uploadData to be a writable signal
|
|
4307
|
+
// For now, this method serves as a type-safe way to work with upload data
|
|
4308
|
+
console.log('📝 [FileInput] Upload data updated:', updatedData);
|
|
4309
|
+
}
|
|
4310
|
+
getCurrentState() {
|
|
4464
4311
|
return {
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4312
|
+
id: this.id(),
|
|
4313
|
+
label: this.labelSignal(),
|
|
4314
|
+
required: this.requiredSignal(),
|
|
4315
|
+
disabled: this.disabledSignal(),
|
|
4316
|
+
accept: this.acceptSignal(),
|
|
4317
|
+
multiple: this.multipleSignal(),
|
|
4318
|
+
showPreview: this.showPreviewSignal(),
|
|
4319
|
+
autoUpload: this.autoUploadSignal(),
|
|
4320
|
+
uploadStatus: this.uploadStatus(),
|
|
4321
|
+
isUploading: this.isUploading(),
|
|
4322
|
+
uploadProgress: this.uploadProgress(),
|
|
4323
|
+
files: this.files() ? Array.from(this.files()).map(f => ({ name: f.name, size: f.size, type: f.type })) : null,
|
|
4324
|
+
fileNames: this.fileNames(),
|
|
4325
|
+
previewUrls: this.previewUrls().length,
|
|
4326
|
+
helperText: this.helperTextSignal(),
|
|
4327
|
+
errorText: this.errorTextSignal(),
|
|
4328
|
+
placeholderText: this.placeholderTextSignal(),
|
|
4329
|
+
placeholderIcon: this.placeholderIconSignal(),
|
|
4330
|
+
previewWidth: this.previewWidthSignal(),
|
|
4331
|
+
previewHeight: this.previewHeightSignal(),
|
|
4332
|
+
previewBoxMode: this.previewBoxModeSignal(),
|
|
4333
|
+
showFileName: this.showFileNameSignal(),
|
|
4334
|
+
uploadData: this.uploadDataSignal()
|
|
4470
4335
|
};
|
|
4471
4336
|
}
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4337
|
+
async getControlData() {
|
|
4338
|
+
console.log('🔍 [FileInput] getControlData called');
|
|
4339
|
+
const cide_element_data = await this.elementService?.getElementData({ sype_key: this.id() });
|
|
4340
|
+
if (cide_element_data) {
|
|
4341
|
+
console.log('📋 [FileInput] Element data loaded:', cide_element_data);
|
|
4342
|
+
// Note: Since we're using input signals, we can't directly set their values
|
|
4343
|
+
// This method would need to be refactored to work with the new signal-based approach
|
|
4344
|
+
// For now, we'll log the data and trigger validation
|
|
4345
|
+
console.log('✅ [FileInput] Control data received from element service');
|
|
4346
|
+
console.log('⚠️ [FileInput] Note: Input signals cannot be modified after component initialization');
|
|
4347
|
+
// Trigger validation update
|
|
4348
|
+
this.onValidatorChange();
|
|
4349
|
+
}
|
|
4350
|
+
else {
|
|
4351
|
+
console.log('⚠️ [FileInput] No element data found for key:', this.id());
|
|
4479
4352
|
}
|
|
4480
4353
|
}
|
|
4481
|
-
|
|
4482
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: CideEleGlobalFileUploaderComponent, isStandalone: true, selector: "cide-ele-global-file-uploader", inputs: { userId: "userId", multiple: "multiple", accept: "accept", maxFileSize: "maxFileSize", allowedTypes: "allowedTypes" }, outputs: { uploadComplete: "uploadComplete", uploadError: "uploadError", uploadCancelled: "uploadCancelled", allUploadsComplete: "allUploadsComplete" }, ngImport: i0, template: "<!-- Global File Uploader Component -->\r\n<div class=\"global-file-uploader\">\r\n\r\n <!-- Hidden file input -->\r\n <input id=\"global-file-input\" type=\"file\" [multiple]=\"multiple\" [accept]=\"accept\" \r\n (change)=\"onFileInputChange($event)\" style=\"display: none;\">\r\n\r\n <!-- Drag and Drop Zone -->\r\n <div class=\"upload-zone\" [class.drag-over]=\"isDragOver()\" (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\" (drop)=\"onDrop($event)\" (click)=\"triggerFileInput()\">\r\n\r\n <div class=\"upload-zone-content\">\r\n <cide-ele-icon name=\"cloud_upload\" class=\"upload-icon\" size=\"lg\">\r\n </cide-ele-icon>\r\n\r\n <h3 class=\"upload-title\">\r\n {{ isDragOver() ? 'Drop files here' : 'Upload Files' }}\r\n </h3>\r\n\r\n <p class=\"upload-subtitle\">\r\n {{ isDragOver() ? 'Release to upload' : 'Drag and drop files here or click to browse' }}\r\n </p>\r\n\r\n <button cideEleButton cideEleButton variant=\"outline\" size=\"sm\"\r\n (click)=\"triggerFileInput(); $event.stopPropagation()\">\r\n <cide-ele-icon name=\"add\" size=\"sm\"></cide-ele-icon>\r\n Choose Files\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Upload Queue -->\r\n @if (hasUploads()) {\r\n <div class=\"upload-queue\">\r\n <div class=\"queue-header\">\r\n <h4 class=\"queue-title\">Upload Queue</h4>\r\n <div class=\"queue-actions\">\r\n @if (completedUploads().length > 0) {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearCompleted()\">\r\n Clear Completed\r\n </button>\r\n }\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearAll()\">\r\n Clear All\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"queue-items\">\r\n @for (upload of uploadQueue(); track upload.fileId) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(upload.status)\">\r\n\r\n <!-- File Info -->\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"sm\">\r\n {{getStatusIcon(upload.status)}}\r\n </cide-ele-icon>\r\n\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ upload.fileName }}</div>\r\n @if (upload.file) {\r\n <div class=\"file-size\">{{ getFileSizeDisplay(upload.file) }}</div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar -->\r\n @if (upload.status === 'uploading') {\r\n <div class=\"progress-container\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"upload.progress\">\r\n </div>\r\n </div>\r\n <span class=\"progress-text\">{{ upload.progress }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Status Text -->\r\n @if (upload.status !== 'uploading') {\r\n <div class=\"status-text\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n }\r\n @case ('completed') {\r\n <span class=\"text-green-600\">Uploaded successfully</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">{{ upload.error || 'Upload failed' }}</span>\r\n }\r\n @case ('cancelled') {\r\n <span class=\"text-gray-600\">Cancelled</span>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('completed') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"retryUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"refresh\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('cancelled') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Queue Summary -->\r\n <div class=\"queue-summary\">\r\n <div class=\"summary-stats\">\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"schedule\" size=\"xs\"></cide-ele-icon>\r\n {{ pendingUploads().length }} pending\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"cloud_upload\" size=\"xs\"></cide-ele-icon>\r\n {{ activeUploads().length }} uploading\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"check_circle\" size=\"xs\"></cide-ele-icon>\r\n {{ completedUploads().length }} completed\r\n </span>\r\n @if (failedUploads().length > 0) {\r\n <span class=\"stat error\">\r\n <cide-ele-icon name=\"error\" size=\"xs\"></cide-ele-icon>\r\n {{ failedUploads().length }} failed\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n\r\n", styles: [".global-file-uploader{@apply tw-w-full tw-max-w-2xl tw-mx-auto tw-p-4;}.global-file-uploader .upload-zone{@apply tw-border-2 tw-border-dashed tw-border-gray-300 tw-rounded-lg tw-p-8 tw-text-center tw-cursor-pointer tw-transition-all tw-duration-200 tw-ease-in-out;@apply hover:tw-border-blue-400 hover:tw-bg-blue-50;}.global-file-uploader .upload-zone.drag-over{@apply tw-border-blue-500 tw-bg-blue-100 tw-scale-105;}.global-file-uploader .upload-zone .upload-zone-content{@apply tw-space-y-4;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-400 tw-mx-auto;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-xl tw-font-semibold tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-sm tw-text-gray-500 tw-m-0;}.global-file-uploader .upload-queue{@apply tw-mt-6 tw-bg-white tw-border tw-border-gray-200 tw-rounded-lg tw-shadow-sm;}.global-file-uploader .upload-queue .queue-header{@apply tw-flex tw-justify-between tw-items-center tw-p-4 tw-border-b tw-border-gray-200;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-lg tw-font-medium tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-flex tw-space-x-2;}.global-file-uploader .upload-queue .queue-items{@apply tw-max-h-96 tw-overflow-y-auto;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-flex tw-items-center tw-justify-between tw-p-4 tw-border-b tw-border-gray-100 tw-transition-colors tw-duration-200;}.global-file-uploader .upload-queue .queue-items .upload-item:last-child{@apply tw-border-b-0;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-50;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-flex tw-items-center tw-flex-1 tw-min-w-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .status-icon{@apply tw-mr-3 tw-flex-shrink-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details{@apply tw-min-w-0 tw-flex-1;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-sm tw-font-medium tw-text-gray-700 tw-truncate;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-xs tw-text-gray-500 tw-mt-1;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-flex tw-items-center tw-space-x-3 tw-flex-1 tw-max-w-xs tw-mx-4;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-flex-1 tw-h-2 tw-bg-gray-200 tw-rounded-full tw-overflow-hidden;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar .progress-fill{@apply tw-h-full tw-bg-blue-500 tw-transition-all tw-duration-300 tw-ease-out;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-xs tw-font-medium tw-text-gray-600 tw-min-w-12 tw-text-right;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-flex-1 tw-text-sm tw-text-center;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text span{@apply tw-font-medium;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-flex tw-space-x-1 tw-ml-4;}.global-file-uploader .upload-queue .queue-summary{@apply tw-p-4 tw-bg-gray-50 tw-border-t tw-border-gray-200;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-flex tw-flex-wrap tw-gap-4 tw-text-sm tw-text-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat{@apply tw-flex tw-items-center tw-space-x-1;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-600;}@media (max-width: 640px){.global-file-uploader{@apply tw-p-2;}.global-file-uploader .upload-zone{@apply tw-p-6;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-lg;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-xs;}.global-file-uploader .upload-queue .queue-header{@apply tw-p-3 tw-flex-col tw-space-y-2 tw-items-start;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-w-full tw-justify-end;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-p-3 tw-flex-col tw-space-y-3 tw-items-start;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-w-full;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-w-full tw-mx-0;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-w-full tw-text-left;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-w-full tw-justify-end tw-ml-0;}}@media (prefers-color-scheme: dark){.global-file-uploader .upload-zone{@apply tw-border-gray-600 hover:tw-border-blue-400 hover:tw-bg-gray-800;}.global-file-uploader .upload-zone.drag-over{@apply tw-bg-blue-900;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-500;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-gray-200;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-gray-400;}.global-file-uploader .upload-queue{@apply tw-bg-gray-800 tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-gray-400;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-bg-gray-600;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary{@apply tw-bg-gray-700 tw-border-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-400;}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CideEleButtonComponent, selector: "button[cideEleButton], a[cideEleButton]", inputs: ["label", "variant", "size", "type", "shape", "elevation", "disabled", "id", "loading", "fullWidth", "leftIcon", "rightIcon", "customClass", "tooltip", "ariaLabel", "testId", "routerLink", "routerExtras", "preventDoubleClick", "animated"], outputs: ["btnClick", "doubleClick"] }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
|
|
4354
|
+
// Validator implementation
|
|
4355
|
+
validate(control) {
|
|
4356
|
+
console.log('🔍 [FileInput] validate() called - uploadStatus:', this.uploadStatus(), 'required:', this.requiredSignal(), 'files:', !!this.files(), 'control.value:', control.value);
|
|
4357
|
+
// If upload is in progress (start or uploading status), return validation error
|
|
4358
|
+
if (this.uploadStatus() === 'start' || this.uploadStatus() === 'uploading') {
|
|
4359
|
+
console.log('⚠️ [FileInput] Validation ERROR: Upload in progress');
|
|
4360
|
+
return { 'uploadInProgress': { message: 'File upload in progress. Please wait...' } };
|
|
4361
|
+
}
|
|
4362
|
+
// If required and no file is selected and no control value (uploaded file ID), return validation error
|
|
4363
|
+
if (this.requiredSignal() && !this.files() && !control.value) {
|
|
4364
|
+
console.log('⚠️ [FileInput] Validation ERROR: File required');
|
|
4365
|
+
return { 'required': { message: 'Please select a file to upload.' } };
|
|
4366
|
+
}
|
|
4367
|
+
console.log('✅ [FileInput] Validation PASSED: No errors');
|
|
4368
|
+
return null; // No validation errors
|
|
4369
|
+
}
|
|
4370
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4371
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: CideEleFileInputComponent, isStandalone: true, selector: "cide-ele-file-input", inputs: { label: "label", accept: "accept", multiple: "multiple", disabled: "disabled", required: "required", helperText: "helperText", errorText: "errorText", showPreview: "showPreview", previewWidth: "previewWidth", previewHeight: "previewHeight", previewBoxMode: "previewBoxMode", showFileName: "showFileName", placeholderText: "placeholderText", placeholderIcon: "placeholderIcon", autoUpload: "autoUpload", uploadData: "uploadData", showFloatingUploader: "showFloatingUploader", floatingUploaderGroupId: "floatingUploaderGroupId" }, outputs: { fileChange: "fileChange", uploadSuccess: "uploadSuccess", uploadError: "uploadError", uploadProgressChange: "uploadProgressChange" }, providers: [
|
|
4372
|
+
{
|
|
4373
|
+
provide: NG_VALUE_ACCESSOR,
|
|
4374
|
+
useExisting: CideEleFileInputComponent,
|
|
4375
|
+
multi: true
|
|
4376
|
+
},
|
|
4377
|
+
{
|
|
4378
|
+
provide: NG_VALIDATORS,
|
|
4379
|
+
useExisting: CideEleFileInputComponent,
|
|
4380
|
+
multi: true
|
|
4381
|
+
}
|
|
4382
|
+
], usesOnChanges: true, ngImport: i0, template: "<div class=\"cide-file-input\">\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\n @if (labelSignal() && !isPreviewBoxMode()) {\n <label class=\"cide-file-input-label\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"cide-file-input-required\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview-box-container\">\n <!-- Hidden file input -->\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"cide-file-input-preview-box\"\n [class.cide-file-input-preview-box-disabled]=\"disabledSignal()\"\n [class.cide-file-input-preview-box-has-image]=\"hasImages()\"\n [class.cide-file-input-preview-box-drag-over]=\"isDragOver()\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n [attr.title]=\"disabledSignal() ? 'File selection disabled' : placeholderTextSignal()\">\n \n <!-- No Image State -->\n @if (!hasImages()) {\n <div class=\"cide-file-input-preview-box-placeholder\">\n <div class=\"cide-file-input-preview-box-icon\">\n <cide-ele-icon>{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\n </div>\n <div class=\"cide-file-input-preview-box-text\">\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"cide-file-input-preview-box-content\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"cide-file-input-preview-box-image\">\n <div class=\"cide-file-input-preview-box-overlay\">\n <div class=\"cide-file-input-preview-box-overlay-text\">Click to change</div>\n </div>\n @if (!disabledSignal()) {\n <button \n type=\"button\" \n class=\"cide-file-input-preview-box-remove\"\n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Remove image\">\n \u00D7\n </button>\n }\n </div>\n }\n </div>\n \n <!-- File name display for preview box mode -->\n @if (hasImages() && fileNames().length && showFileNameSignal()) {\n <div class=\"cide-file-input-preview-box-filename\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\n <div \n class=\"cide-file-input-wrapper\"\n [class.cide-file-input-drag-over]=\"isDragOver()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-element\"\n />\n @if (hasFiles()) {\n <button type=\"button\" class=\"cide-file-input-clear\" (click)=\"clearFiles()\">\n Clear\n </button>\n }\n </div>\n @if (hasFiles() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-files\">\n @if (multipleSignal() && fileNames().length > 1) {\n <!-- Show file count for multiple files -->\n <div class=\"cide-file-input-multiple-count\">\n {{ fileNames().length }} files selected\n </div>\n } @else {\n <!-- Show individual file names for single file or when only one file selected -->\n @for (name of fileNames(); track name) {\n <span>{{ name }}</span>\n }\n }\n <!-- Angular 20: Display file size using new computed values -->\n @if (totalFileSize() > 0) {\n <div class=\"cide-file-input-size\">\n Total size: {{ fileSizeInMB() }} MB\n </div>\n }\n </div>\n }\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview\">\n <div class=\"cide-file-input-preview-label\">Preview:</div>\n <div class=\"cide-file-input-preview-container\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"cide-file-input-preview-item\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\">\n <button \n type=\"button\" \n class=\"cide-file-input-preview-remove\"\n (click)=\"removePreview(i)\"\n title=\"Remove image\">\n \u00D7\n </button>\n <img \n [src]=\"previewUrl\" \n [alt]=\"fileNames()[i] || 'Preview image'\"\n class=\"cide-file-input-preview-image\"\n loading=\"lazy\">\n <div class=\"cide-file-input-preview-filename\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n @if (errorTextSignal()) {\n <div class=\"cide-file-input-error\">{{ errorTextSignal() }}</div>\n }\n @if (helperTextSignal() && !errorTextSignal()) {\n <div class=\"cide-file-input-helper\">{{ helperTextSignal() }}</div>\n }\n</div> ", styles: [".cide-file-input{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-label{font-weight:500;margin-bottom:.25rem}.cide-file-input-required{color:#d32f2f;font-weight:700}.cide-file-input-wrapper{display:flex;align-items:center;gap:.5rem;border:2px dashed transparent;border-radius:.5rem;padding:.5rem;transition:all .2s ease-in-out}.cide-file-input-wrapper.cide-file-input-drag-over{border-color:#3b82f6;background-color:#eff6ff;transform:scale(1.02)}.cide-file-input-element{flex:1}.cide-file-input-clear{background:none;border:none;color:#d32f2f;cursor:pointer;font-size:.9rem}.cide-file-input-files{font-size:.95rem;color:#333;margin-top:.25rem}.cide-file-input-multiple-count{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem 1rem;background-color:#f0f9ff;border:1px solid #0ea5e9;border-radius:.5rem;color:#0369a1;font-weight:500;font-size:.9rem}.cide-file-input-size{margin-top:.5rem;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;font-size:.75rem;color:#4b5563;font-weight:500}.cide-file-input-error{color:#d32f2f;font-size:.9rem}.cide-file-input-helper{color:#666;font-size:.9rem}.cide-file-input-preview{margin-top:.75rem;padding:.75rem;background-color:#f8f9fa;border:1px solid #e1e5e9;border-radius:.375rem}.cide-file-input-preview-label{font-weight:500;margin-bottom:.5rem;color:#374151;font-size:.875rem}.cide-file-input-preview-container{display:flex;flex-wrap:wrap;gap:.75rem}.cide-file-input-preview-item{position:relative;display:flex;flex-direction:column;border:1px solid #d1d5db;border-radius:.5rem;overflow:hidden;background-color:#fff;box-shadow:0 1px 3px #0000001a;transition:box-shadow .2s ease-in-out}.cide-file-input-preview-item:hover{box-shadow:0 4px 6px -1px #0000001a}.cide-file-input-preview-image{width:100%;height:calc(100% - 2rem);object-fit:cover;object-position:center;background-color:#f3f4f6}.cide-file-input-preview-filename{padding:.375rem .5rem;background-color:#f9fafbf2;border-top:1px solid #e5e7eb;font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:2rem;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-remove{position:absolute;top:.25rem;right:.25rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;transition:all .2s ease-in-out}.cide-file-input-preview-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-hidden{display:none}.cide-file-input-preview-box-container{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-preview-box{position:relative;border:2px dashed #d1d5db;border-radius:.5rem;cursor:pointer;background-color:#f9fafb;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:all .2s ease-in-out}.cide-file-input-preview-box:hover{border-color:#3b82f6;background-color:#eff6ff}.cide-file-input-preview-box.cide-file-input-preview-box-disabled{cursor:not-allowed;opacity:.6;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-disabled:hover{border-color:#d1d5db;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image{border-style:solid;border-color:#e5e7eb;padding:0}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover{border-color:#3b82f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover .cide-file-input-preview-box-overlay{opacity:1}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over{border-color:#3b82f6!important;background-color:#eff6ff!important;transform:scale(1.02);box-shadow:0 0 0 4px #3b82f61a}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-icon{color:#3b82f6;transform:scale(1.1)}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-text{color:#3b82f6;font-weight:600}.cide-file-input-preview-box-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1rem;text-align:center}.cide-file-input-preview-box-icon{font-size:2rem;color:#6b7280}.cide-file-input-preview-box-text{font-size:.875rem;color:#6b7280;font-weight:500}.cide-file-input-preview-box-content{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-box-image{width:100%;height:100%;object-fit:cover;object-position:center}.cide-file-input-preview-box-overlay{position:absolute;inset:0;background-color:#0009;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s ease-in-out}.cide-file-input-preview-box-overlay-text{color:#fff;font-size:.875rem;font-weight:500;text-align:center}.cide-file-input-preview-box-remove{position:absolute;top:.375rem;right:.375rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:20;transition:all .2s ease-in-out}.cide-file-input-preview-box-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-box-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-preview-box-filename{font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;margin-top:.25rem}@media (max-width: 640px){.cide-file-input-preview-container{justify-content:center}.cide-file-input-preview-item{min-width:120px;max-width:150px}.cide-file-input-preview-box-icon{font-size:1.5rem}.cide-file-input-preview-box-text{font-size:.75rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
|
|
4483
4383
|
}
|
|
4484
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type:
|
|
4384
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, decorators: [{
|
|
4485
4385
|
type: Component,
|
|
4486
|
-
args: [{ selector: 'cide-ele-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
], template: "<!-- Global File Uploader Component -->\r\n<div class=\"global-file-uploader\">\r\n\r\n <!-- Hidden file input -->\r\n <input id=\"global-file-input\" type=\"file\" [multiple]=\"multiple\" [accept]=\"accept\" \r\n (change)=\"onFileInputChange($event)\" style=\"display: none;\">\r\n\r\n <!-- Drag and Drop Zone -->\r\n <div class=\"upload-zone\" [class.drag-over]=\"isDragOver()\" (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\" (drop)=\"onDrop($event)\" (click)=\"triggerFileInput()\">\r\n\r\n <div class=\"upload-zone-content\">\r\n <cide-ele-icon name=\"cloud_upload\" class=\"upload-icon\" size=\"lg\">\r\n </cide-ele-icon>\r\n\r\n <h3 class=\"upload-title\">\r\n {{ isDragOver() ? 'Drop files here' : 'Upload Files' }}\r\n </h3>\r\n\r\n <p class=\"upload-subtitle\">\r\n {{ isDragOver() ? 'Release to upload' : 'Drag and drop files here or click to browse' }}\r\n </p>\r\n\r\n <button cideEleButton cideEleButton variant=\"outline\" size=\"sm\"\r\n (click)=\"triggerFileInput(); $event.stopPropagation()\">\r\n <cide-ele-icon name=\"add\" size=\"sm\"></cide-ele-icon>\r\n Choose Files\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Upload Queue -->\r\n @if (hasUploads()) {\r\n <div class=\"upload-queue\">\r\n <div class=\"queue-header\">\r\n <h4 class=\"queue-title\">Upload Queue</h4>\r\n <div class=\"queue-actions\">\r\n @if (completedUploads().length > 0) {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearCompleted()\">\r\n Clear Completed\r\n </button>\r\n }\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearAll()\">\r\n Clear All\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"queue-items\">\r\n @for (upload of uploadQueue(); track upload.fileId) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(upload.status)\">\r\n\r\n <!-- File Info -->\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"sm\">\r\n {{getStatusIcon(upload.status)}}\r\n </cide-ele-icon>\r\n\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ upload.fileName }}</div>\r\n @if (upload.file) {\r\n <div class=\"file-size\">{{ getFileSizeDisplay(upload.file) }}</div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar -->\r\n @if (upload.status === 'uploading') {\r\n <div class=\"progress-container\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"upload.progress\">\r\n </div>\r\n </div>\r\n <span class=\"progress-text\">{{ upload.progress }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Status Text -->\r\n @if (upload.status !== 'uploading') {\r\n <div class=\"status-text\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n }\r\n @case ('completed') {\r\n <span class=\"text-green-600\">Uploaded successfully</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">{{ upload.error || 'Upload failed' }}</span>\r\n }\r\n @case ('cancelled') {\r\n <span class=\"text-gray-600\">Cancelled</span>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('completed') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"retryUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"refresh\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('cancelled') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Queue Summary -->\r\n <div class=\"queue-summary\">\r\n <div class=\"summary-stats\">\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"schedule\" size=\"xs\"></cide-ele-icon>\r\n {{ pendingUploads().length }} pending\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"cloud_upload\" size=\"xs\"></cide-ele-icon>\r\n {{ activeUploads().length }} uploading\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"check_circle\" size=\"xs\"></cide-ele-icon>\r\n {{ completedUploads().length }} completed\r\n </span>\r\n @if (failedUploads().length > 0) {\r\n <span class=\"stat error\">\r\n <cide-ele-icon name=\"error\" size=\"xs\"></cide-ele-icon>\r\n {{ failedUploads().length }} failed\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n\r\n", styles: [".global-file-uploader{@apply tw-w-full tw-max-w-2xl tw-mx-auto tw-p-4;}.global-file-uploader .upload-zone{@apply tw-border-2 tw-border-dashed tw-border-gray-300 tw-rounded-lg tw-p-8 tw-text-center tw-cursor-pointer tw-transition-all tw-duration-200 tw-ease-in-out;@apply hover:tw-border-blue-400 hover:tw-bg-blue-50;}.global-file-uploader .upload-zone.drag-over{@apply tw-border-blue-500 tw-bg-blue-100 tw-scale-105;}.global-file-uploader .upload-zone .upload-zone-content{@apply tw-space-y-4;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-400 tw-mx-auto;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-xl tw-font-semibold tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-sm tw-text-gray-500 tw-m-0;}.global-file-uploader .upload-queue{@apply tw-mt-6 tw-bg-white tw-border tw-border-gray-200 tw-rounded-lg tw-shadow-sm;}.global-file-uploader .upload-queue .queue-header{@apply tw-flex tw-justify-between tw-items-center tw-p-4 tw-border-b tw-border-gray-200;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-lg tw-font-medium tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-flex tw-space-x-2;}.global-file-uploader .upload-queue .queue-items{@apply tw-max-h-96 tw-overflow-y-auto;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-flex tw-items-center tw-justify-between tw-p-4 tw-border-b tw-border-gray-100 tw-transition-colors tw-duration-200;}.global-file-uploader .upload-queue .queue-items .upload-item:last-child{@apply tw-border-b-0;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-50;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-flex tw-items-center tw-flex-1 tw-min-w-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .status-icon{@apply tw-mr-3 tw-flex-shrink-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details{@apply tw-min-w-0 tw-flex-1;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-sm tw-font-medium tw-text-gray-700 tw-truncate;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-xs tw-text-gray-500 tw-mt-1;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-flex tw-items-center tw-space-x-3 tw-flex-1 tw-max-w-xs tw-mx-4;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-flex-1 tw-h-2 tw-bg-gray-200 tw-rounded-full tw-overflow-hidden;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar .progress-fill{@apply tw-h-full tw-bg-blue-500 tw-transition-all tw-duration-300 tw-ease-out;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-xs tw-font-medium tw-text-gray-600 tw-min-w-12 tw-text-right;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-flex-1 tw-text-sm tw-text-center;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text span{@apply tw-font-medium;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-flex tw-space-x-1 tw-ml-4;}.global-file-uploader .upload-queue .queue-summary{@apply tw-p-4 tw-bg-gray-50 tw-border-t tw-border-gray-200;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-flex tw-flex-wrap tw-gap-4 tw-text-sm tw-text-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat{@apply tw-flex tw-items-center tw-space-x-1;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-600;}@media (max-width: 640px){.global-file-uploader{@apply tw-p-2;}.global-file-uploader .upload-zone{@apply tw-p-6;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-lg;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-xs;}.global-file-uploader .upload-queue .queue-header{@apply tw-p-3 tw-flex-col tw-space-y-2 tw-items-start;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-w-full tw-justify-end;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-p-3 tw-flex-col tw-space-y-3 tw-items-start;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-w-full;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-w-full tw-mx-0;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-w-full tw-text-left;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-w-full tw-justify-end tw-ml-0;}}@media (prefers-color-scheme: dark){.global-file-uploader .upload-zone{@apply tw-border-gray-600 hover:tw-border-blue-400 hover:tw-bg-gray-800;}.global-file-uploader .upload-zone.drag-over{@apply tw-bg-blue-900;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-500;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-gray-200;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-gray-400;}.global-file-uploader .upload-queue{@apply tw-bg-gray-800 tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-gray-400;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-bg-gray-600;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary{@apply tw-bg-gray-700 tw-border-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-400;}}\n"] }]
|
|
4491
|
-
|
|
4386
|
+
args: [{ selector: 'cide-ele-file-input', standalone: true, imports: [CommonModule, FormsModule, CideIconComponent], providers: [
|
|
4387
|
+
{
|
|
4388
|
+
provide: NG_VALUE_ACCESSOR,
|
|
4389
|
+
useExisting: CideEleFileInputComponent,
|
|
4390
|
+
multi: true
|
|
4391
|
+
},
|
|
4392
|
+
{
|
|
4393
|
+
provide: NG_VALIDATORS,
|
|
4394
|
+
useExisting: CideEleFileInputComponent,
|
|
4395
|
+
multi: true
|
|
4396
|
+
}
|
|
4397
|
+
], template: "<div class=\"cide-file-input\">\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\n @if (labelSignal() && !isPreviewBoxMode()) {\n <label class=\"cide-file-input-label\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"cide-file-input-required\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview-box-container\">\n <!-- Hidden file input -->\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"cide-file-input-preview-box\"\n [class.cide-file-input-preview-box-disabled]=\"disabledSignal()\"\n [class.cide-file-input-preview-box-has-image]=\"hasImages()\"\n [class.cide-file-input-preview-box-drag-over]=\"isDragOver()\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n [attr.title]=\"disabledSignal() ? 'File selection disabled' : placeholderTextSignal()\">\n \n <!-- No Image State -->\n @if (!hasImages()) {\n <div class=\"cide-file-input-preview-box-placeholder\">\n <div class=\"cide-file-input-preview-box-icon\">\n <cide-ele-icon>{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\n </div>\n <div class=\"cide-file-input-preview-box-text\">\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"cide-file-input-preview-box-content\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"cide-file-input-preview-box-image\">\n <div class=\"cide-file-input-preview-box-overlay\">\n <div class=\"cide-file-input-preview-box-overlay-text\">Click to change</div>\n </div>\n @if (!disabledSignal()) {\n <button \n type=\"button\" \n class=\"cide-file-input-preview-box-remove\"\n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Remove image\">\n \u00D7\n </button>\n }\n </div>\n }\n </div>\n \n <!-- File name display for preview box mode -->\n @if (hasImages() && fileNames().length && showFileNameSignal()) {\n <div class=\"cide-file-input-preview-box-filename\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\n <div \n class=\"cide-file-input-wrapper\"\n [class.cide-file-input-drag-over]=\"isDragOver()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-element\"\n />\n @if (hasFiles()) {\n <button type=\"button\" class=\"cide-file-input-clear\" (click)=\"clearFiles()\">\n Clear\n </button>\n }\n </div>\n @if (hasFiles() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-files\">\n @if (multipleSignal() && fileNames().length > 1) {\n <!-- Show file count for multiple files -->\n <div class=\"cide-file-input-multiple-count\">\n {{ fileNames().length }} files selected\n </div>\n } @else {\n <!-- Show individual file names for single file or when only one file selected -->\n @for (name of fileNames(); track name) {\n <span>{{ name }}</span>\n }\n }\n <!-- Angular 20: Display file size using new computed values -->\n @if (totalFileSize() > 0) {\n <div class=\"cide-file-input-size\">\n Total size: {{ fileSizeInMB() }} MB\n </div>\n }\n </div>\n }\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview\">\n <div class=\"cide-file-input-preview-label\">Preview:</div>\n <div class=\"cide-file-input-preview-container\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"cide-file-input-preview-item\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\">\n <button \n type=\"button\" \n class=\"cide-file-input-preview-remove\"\n (click)=\"removePreview(i)\"\n title=\"Remove image\">\n \u00D7\n </button>\n <img \n [src]=\"previewUrl\" \n [alt]=\"fileNames()[i] || 'Preview image'\"\n class=\"cide-file-input-preview-image\"\n loading=\"lazy\">\n <div class=\"cide-file-input-preview-filename\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n @if (errorTextSignal()) {\n <div class=\"cide-file-input-error\">{{ errorTextSignal() }}</div>\n }\n @if (helperTextSignal() && !errorTextSignal()) {\n <div class=\"cide-file-input-helper\">{{ helperTextSignal() }}</div>\n }\n</div> ", styles: [".cide-file-input{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-label{font-weight:500;margin-bottom:.25rem}.cide-file-input-required{color:#d32f2f;font-weight:700}.cide-file-input-wrapper{display:flex;align-items:center;gap:.5rem;border:2px dashed transparent;border-radius:.5rem;padding:.5rem;transition:all .2s ease-in-out}.cide-file-input-wrapper.cide-file-input-drag-over{border-color:#3b82f6;background-color:#eff6ff;transform:scale(1.02)}.cide-file-input-element{flex:1}.cide-file-input-clear{background:none;border:none;color:#d32f2f;cursor:pointer;font-size:.9rem}.cide-file-input-files{font-size:.95rem;color:#333;margin-top:.25rem}.cide-file-input-multiple-count{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem 1rem;background-color:#f0f9ff;border:1px solid #0ea5e9;border-radius:.5rem;color:#0369a1;font-weight:500;font-size:.9rem}.cide-file-input-size{margin-top:.5rem;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;font-size:.75rem;color:#4b5563;font-weight:500}.cide-file-input-error{color:#d32f2f;font-size:.9rem}.cide-file-input-helper{color:#666;font-size:.9rem}.cide-file-input-preview{margin-top:.75rem;padding:.75rem;background-color:#f8f9fa;border:1px solid #e1e5e9;border-radius:.375rem}.cide-file-input-preview-label{font-weight:500;margin-bottom:.5rem;color:#374151;font-size:.875rem}.cide-file-input-preview-container{display:flex;flex-wrap:wrap;gap:.75rem}.cide-file-input-preview-item{position:relative;display:flex;flex-direction:column;border:1px solid #d1d5db;border-radius:.5rem;overflow:hidden;background-color:#fff;box-shadow:0 1px 3px #0000001a;transition:box-shadow .2s ease-in-out}.cide-file-input-preview-item:hover{box-shadow:0 4px 6px -1px #0000001a}.cide-file-input-preview-image{width:100%;height:calc(100% - 2rem);object-fit:cover;object-position:center;background-color:#f3f4f6}.cide-file-input-preview-filename{padding:.375rem .5rem;background-color:#f9fafbf2;border-top:1px solid #e5e7eb;font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:2rem;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-remove{position:absolute;top:.25rem;right:.25rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;transition:all .2s ease-in-out}.cide-file-input-preview-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-hidden{display:none}.cide-file-input-preview-box-container{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-preview-box{position:relative;border:2px dashed #d1d5db;border-radius:.5rem;cursor:pointer;background-color:#f9fafb;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:all .2s ease-in-out}.cide-file-input-preview-box:hover{border-color:#3b82f6;background-color:#eff6ff}.cide-file-input-preview-box.cide-file-input-preview-box-disabled{cursor:not-allowed;opacity:.6;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-disabled:hover{border-color:#d1d5db;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image{border-style:solid;border-color:#e5e7eb;padding:0}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover{border-color:#3b82f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover .cide-file-input-preview-box-overlay{opacity:1}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over{border-color:#3b82f6!important;background-color:#eff6ff!important;transform:scale(1.02);box-shadow:0 0 0 4px #3b82f61a}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-icon{color:#3b82f6;transform:scale(1.1)}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-text{color:#3b82f6;font-weight:600}.cide-file-input-preview-box-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1rem;text-align:center}.cide-file-input-preview-box-icon{font-size:2rem;color:#6b7280}.cide-file-input-preview-box-text{font-size:.875rem;color:#6b7280;font-weight:500}.cide-file-input-preview-box-content{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-box-image{width:100%;height:100%;object-fit:cover;object-position:center}.cide-file-input-preview-box-overlay{position:absolute;inset:0;background-color:#0009;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s ease-in-out}.cide-file-input-preview-box-overlay-text{color:#fff;font-size:.875rem;font-weight:500;text-align:center}.cide-file-input-preview-box-remove{position:absolute;top:.375rem;right:.375rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:20;transition:all .2s ease-in-out}.cide-file-input-preview-box-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-box-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-preview-box-filename{font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;margin-top:.25rem}@media (max-width: 640px){.cide-file-input-preview-container{justify-content:center}.cide-file-input-preview-item{min-width:120px;max-width:150px}.cide-file-input-preview-box-icon{font-size:1.5rem}.cide-file-input-preview-box-text{font-size:.75rem}}\n"] }]
|
|
4398
|
+
}], ctorParameters: () => [], propDecorators: { label: [{
|
|
4399
|
+
type: Input
|
|
4400
|
+
}], accept: [{
|
|
4492
4401
|
type: Input
|
|
4493
4402
|
}], multiple: [{
|
|
4494
4403
|
type: Input
|
|
4495
|
-
}],
|
|
4404
|
+
}], disabled: [{
|
|
4496
4405
|
type: Input
|
|
4497
|
-
}],
|
|
4406
|
+
}], required: [{
|
|
4498
4407
|
type: Input
|
|
4499
|
-
}],
|
|
4408
|
+
}], helperText: [{
|
|
4500
4409
|
type: Input
|
|
4501
|
-
}],
|
|
4410
|
+
}], errorText: [{
|
|
4411
|
+
type: Input
|
|
4412
|
+
}], showPreview: [{
|
|
4413
|
+
type: Input
|
|
4414
|
+
}], previewWidth: [{
|
|
4415
|
+
type: Input
|
|
4416
|
+
}], previewHeight: [{
|
|
4417
|
+
type: Input
|
|
4418
|
+
}], previewBoxMode: [{
|
|
4419
|
+
type: Input
|
|
4420
|
+
}], showFileName: [{
|
|
4421
|
+
type: Input
|
|
4422
|
+
}], placeholderText: [{
|
|
4423
|
+
type: Input
|
|
4424
|
+
}], placeholderIcon: [{
|
|
4425
|
+
type: Input
|
|
4426
|
+
}], autoUpload: [{
|
|
4427
|
+
type: Input
|
|
4428
|
+
}], uploadData: [{
|
|
4429
|
+
type: Input
|
|
4430
|
+
}], showFloatingUploader: [{
|
|
4431
|
+
type: Input
|
|
4432
|
+
}], floatingUploaderGroupId: [{
|
|
4433
|
+
type: Input
|
|
4434
|
+
}], fileChange: [{
|
|
4502
4435
|
type: Output
|
|
4503
|
-
}],
|
|
4436
|
+
}], uploadSuccess: [{
|
|
4504
4437
|
type: Output
|
|
4505
|
-
}],
|
|
4438
|
+
}], uploadError: [{
|
|
4506
4439
|
type: Output
|
|
4507
|
-
}],
|
|
4440
|
+
}], uploadProgressChange: [{
|
|
4508
4441
|
type: Output
|
|
4509
4442
|
}] } });
|
|
4510
4443
|
|
|
4511
|
-
class
|
|
4444
|
+
class CideEleGlobalFileUploaderComponent {
|
|
4445
|
+
// Dependency injection
|
|
4512
4446
|
destroyRef = inject(DestroyRef);
|
|
4513
|
-
|
|
4514
|
-
//
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4447
|
+
fileService = inject(CideEleFileManagerService);
|
|
4448
|
+
// Input properties
|
|
4449
|
+
userId = '';
|
|
4450
|
+
multiple = true;
|
|
4451
|
+
accept = '';
|
|
4452
|
+
maxFileSize = 10; // MB
|
|
4453
|
+
allowedTypes = [];
|
|
4454
|
+
// Output events
|
|
4455
|
+
uploadComplete = new EventEmitter();
|
|
4456
|
+
uploadError = new EventEmitter();
|
|
4457
|
+
uploadCancelled = new EventEmitter();
|
|
4458
|
+
allUploadsComplete = new EventEmitter();
|
|
4459
|
+
// Signals for reactive state management
|
|
4460
|
+
isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
|
|
4461
|
+
isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
|
|
4462
|
+
uploadQueue = signal([], ...(ngDevMode ? [{ debugName: "uploadQueue" }] : []));
|
|
4463
|
+
currentGroupId = signal(null, ...(ngDevMode ? [{ debugName: "currentGroupId" }] : []));
|
|
4464
|
+
// Computed values
|
|
4522
4465
|
hasUploads = computed(() => this.uploadQueue().length > 0, ...(ngDevMode ? [{ debugName: "hasUploads" }] : []));
|
|
4523
|
-
pendingUploads = computed(() => this.uploadQueue().
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
}, ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
|
|
4528
|
-
failedUploads = computed(() => {
|
|
4529
|
-
const active = this.activeUploads();
|
|
4530
|
-
return Array.from(active.values()).filter(upload => upload.stage === 'error');
|
|
4531
|
-
}, ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
|
|
4532
|
-
// Animation states
|
|
4533
|
-
isAnimating = signal(false, ...(ngDevMode ? [{ debugName: "isAnimating" }] : []));
|
|
4466
|
+
pendingUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'pending'), ...(ngDevMode ? [{ debugName: "pendingUploads" }] : []));
|
|
4467
|
+
activeUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'uploading'), ...(ngDevMode ? [{ debugName: "activeUploads" }] : []));
|
|
4468
|
+
completedUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'completed'), ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
|
|
4469
|
+
failedUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'error'), ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
|
|
4534
4470
|
constructor() {
|
|
4535
|
-
console.log('🚀 [
|
|
4536
|
-
// Set up effect to monitor file manager service's upload queue
|
|
4537
|
-
effect(() => {
|
|
4538
|
-
const uploadQueue = this.fileManagerService.uploadQueue();
|
|
4539
|
-
const hasUploads = uploadQueue.length > 0;
|
|
4540
|
-
console.log('🔄 [FloatingFileUploader] File manager service queue changed:', {
|
|
4541
|
-
queueLength: uploadQueue.length,
|
|
4542
|
-
hasUploads,
|
|
4543
|
-
currentVisible: this.isVisible(),
|
|
4544
|
-
queueFiles: uploadQueue
|
|
4545
|
-
});
|
|
4546
|
-
// Show floating uploader when files are added to the file manager service queue
|
|
4547
|
-
if (hasUploads && !this.isVisible()) {
|
|
4548
|
-
console.log('👁️ [FloatingFileUploader] Showing floating uploader due to file manager queue');
|
|
4549
|
-
this.showWithAnimation();
|
|
4550
|
-
}
|
|
4551
|
-
else if (!hasUploads && this.isVisible()) {
|
|
4552
|
-
console.log('👁️ [FloatingFileUploader] Hiding floating uploader - no files in file manager queue');
|
|
4553
|
-
this.hideWithAnimation();
|
|
4554
|
-
}
|
|
4555
|
-
});
|
|
4471
|
+
console.log('🚀 [GlobalFileUploader] Component initialized');
|
|
4556
4472
|
}
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4473
|
+
/**
|
|
4474
|
+
* Handle drag over event
|
|
4475
|
+
*/
|
|
4476
|
+
onDragOver(event) {
|
|
4477
|
+
event.preventDefault();
|
|
4478
|
+
event.stopPropagation();
|
|
4479
|
+
this.isDragOver.set(true);
|
|
4562
4480
|
}
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4481
|
+
/**
|
|
4482
|
+
* Handle drag leave event
|
|
4483
|
+
*/
|
|
4484
|
+
onDragLeave(event) {
|
|
4485
|
+
event.preventDefault();
|
|
4486
|
+
event.stopPropagation();
|
|
4487
|
+
this.isDragOver.set(false);
|
|
4567
4488
|
}
|
|
4568
4489
|
/**
|
|
4569
|
-
*
|
|
4490
|
+
* Handle drop event
|
|
4570
4491
|
*/
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4492
|
+
onDrop(event) {
|
|
4493
|
+
event.preventDefault();
|
|
4494
|
+
event.stopPropagation();
|
|
4495
|
+
this.isDragOver.set(false);
|
|
4496
|
+
const files = event.dataTransfer?.files;
|
|
4497
|
+
if (files && files.length > 0) {
|
|
4498
|
+
this.addFilesToQueue(Array.from(files));
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
/**
|
|
4502
|
+
* Handle file input change
|
|
4503
|
+
*/
|
|
4504
|
+
onFileInputChange(event) {
|
|
4505
|
+
const target = event.target;
|
|
4506
|
+
const files = target.files;
|
|
4507
|
+
if (files && files.length > 0) {
|
|
4508
|
+
this.addFilesToQueue(Array.from(files));
|
|
4509
|
+
}
|
|
4510
|
+
// Reset input value to allow selecting the same file again
|
|
4511
|
+
target.value = '';
|
|
4512
|
+
}
|
|
4513
|
+
/**
|
|
4514
|
+
* Add files to upload queue
|
|
4515
|
+
*/
|
|
4516
|
+
addFilesToQueue(files) {
|
|
4517
|
+
// Validate files
|
|
4518
|
+
const validFiles = files.filter(file => this.validateFile(file));
|
|
4519
|
+
if (validFiles.length === 0) {
|
|
4520
|
+
console.warn('⚠️ [GlobalFileUploader] No valid files to upload');
|
|
4521
|
+
return;
|
|
4522
|
+
}
|
|
4523
|
+
const newUploads = validFiles.map(file => ({
|
|
4524
|
+
fileId: this.generateFileId(file),
|
|
4525
|
+
fileName: file.name,
|
|
4526
|
+
progress: 0,
|
|
4527
|
+
status: 'pending',
|
|
4528
|
+
file: file
|
|
4529
|
+
}));
|
|
4530
|
+
this.uploadQueue.update(queue => [...queue, ...newUploads]);
|
|
4531
|
+
this.processUploadQueue();
|
|
4575
4532
|
}
|
|
4576
4533
|
/**
|
|
4577
|
-
*
|
|
4534
|
+
* Validate file
|
|
4578
4535
|
*/
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4536
|
+
validateFile(file) {
|
|
4537
|
+
// Check file size
|
|
4538
|
+
const maxSizeBytes = this.maxFileSize * 1024 * 1024;
|
|
4539
|
+
if (file.size > maxSizeBytes) {
|
|
4540
|
+
console.warn(`⚠️ [GlobalFileUploader] File ${file.name} exceeds size limit`);
|
|
4541
|
+
return false;
|
|
4542
|
+
}
|
|
4543
|
+
// Check file type
|
|
4544
|
+
if (this.allowedTypes.length > 0 && !this.allowedTypes.includes(file.type)) {
|
|
4545
|
+
console.warn(`⚠️ [GlobalFileUploader] File type ${file.type} not allowed`);
|
|
4546
|
+
return false;
|
|
4547
|
+
}
|
|
4548
|
+
return true;
|
|
4583
4549
|
}
|
|
4584
4550
|
/**
|
|
4585
|
-
*
|
|
4551
|
+
* Process upload queue
|
|
4586
4552
|
*/
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4553
|
+
processUploadQueue() {
|
|
4554
|
+
const pendingUploads = this.uploadQueue().filter(upload => upload.status === 'pending');
|
|
4555
|
+
if (pendingUploads.length > 0 && !this.isUploading()) {
|
|
4556
|
+
this.isUploading.set(true);
|
|
4557
|
+
if (this.multiple && pendingUploads.length > 1) {
|
|
4558
|
+
// Multiple file upload with group ID
|
|
4559
|
+
this.uploadMultipleFiles(pendingUploads);
|
|
4560
|
+
}
|
|
4561
|
+
else {
|
|
4562
|
+
// Single file upload
|
|
4563
|
+
this.uploadNextFile();
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4590
4566
|
}
|
|
4591
4567
|
/**
|
|
4592
|
-
*
|
|
4568
|
+
* Upload multiple files with group ID
|
|
4593
4569
|
*/
|
|
4594
|
-
|
|
4595
|
-
|
|
4570
|
+
uploadMultipleFiles(uploads) {
|
|
4571
|
+
console.log('📤 [GlobalFileUploader] Starting multiple file upload for:', uploads.length, 'files');
|
|
4572
|
+
// Generate group ID first
|
|
4573
|
+
this.fileService.generateObjectId()
|
|
4574
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
4575
|
+
.subscribe({
|
|
4576
|
+
next: (response) => {
|
|
4577
|
+
const groupId = response.data?.objectId;
|
|
4578
|
+
console.log('🆔 [GlobalFileUploader] Generated group ID:', groupId);
|
|
4579
|
+
this.currentGroupId.set(groupId || null);
|
|
4580
|
+
if (groupId) {
|
|
4581
|
+
// Upload all files with the same group ID
|
|
4582
|
+
this.uploadFilesWithGroupId(uploads, groupId);
|
|
4583
|
+
}
|
|
4584
|
+
else {
|
|
4585
|
+
console.error('❌ [GlobalFileUploader] No group ID received');
|
|
4586
|
+
this.handleUploadError('Failed to generate group ID', uploads[0]?.fileId);
|
|
4587
|
+
}
|
|
4588
|
+
},
|
|
4589
|
+
error: (error) => {
|
|
4590
|
+
console.error('❌ [GlobalFileUploader] Failed to generate group ID:', error);
|
|
4591
|
+
this.handleUploadError('Failed to generate group ID', uploads[0]?.fileId);
|
|
4592
|
+
}
|
|
4593
|
+
});
|
|
4596
4594
|
}
|
|
4597
4595
|
/**
|
|
4598
|
-
*
|
|
4596
|
+
* Upload files with group ID
|
|
4599
4597
|
*/
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4598
|
+
uploadFilesWithGroupId(uploads, groupId) {
|
|
4599
|
+
let completedCount = 0;
|
|
4600
|
+
let failedCount = 0;
|
|
4601
|
+
const totalFiles = uploads.length;
|
|
4602
|
+
const uploadedFiles = [];
|
|
4603
|
+
uploads.forEach((upload, index) => {
|
|
4604
|
+
// Update status to uploading
|
|
4605
|
+
this.updateUploadStatus(upload.fileId, 'uploading', 0);
|
|
4606
|
+
const uploadRequest = {
|
|
4607
|
+
file: upload.file,
|
|
4608
|
+
userId: this.userId,
|
|
4609
|
+
groupId: groupId,
|
|
4610
|
+
tags: [],
|
|
4611
|
+
permissions: ['read', 'write']
|
|
4612
|
+
};
|
|
4613
|
+
this.fileService.uploadFile(upload.file, this.convertToFileUploadOptions(uploadRequest), (progress) => {
|
|
4614
|
+
// Use the original fileId for progress updates during upload
|
|
4615
|
+
this.updateUploadProgress(upload.fileId, progress);
|
|
4616
|
+
})
|
|
4617
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
4618
|
+
.subscribe({
|
|
4619
|
+
next: (response) => {
|
|
4620
|
+
completedCount++;
|
|
4621
|
+
console.log(`✅ [GlobalFileUploader] File ${index + 1}/${totalFiles} uploaded successfully`);
|
|
4622
|
+
// Find the upload item by cyfm_temp_unique_id from response
|
|
4623
|
+
const responseFile = response?.data?.core_file_manager?.[0];
|
|
4624
|
+
const tempUniqueId = responseFile?.cyfm_temp_unique_id;
|
|
4625
|
+
console.log('🔍 [GlobalFileUploader] Response tempUniqueId:', tempUniqueId);
|
|
4626
|
+
console.log('🔍 [GlobalFileUploader] Original upload fileId:', upload.fileId);
|
|
4627
|
+
// Use the tempUniqueId to find and update the correct upload item
|
|
4628
|
+
if (tempUniqueId) {
|
|
4629
|
+
this.updateUploadStatusByTempId(tempUniqueId, 'completed', 100, undefined, response);
|
|
4630
|
+
}
|
|
4631
|
+
else {
|
|
4632
|
+
// Fallback to original fileId
|
|
4633
|
+
this.updateUploadStatus(upload.fileId, 'completed', 100, undefined, response);
|
|
4634
|
+
}
|
|
4635
|
+
this.uploadComplete.emit({
|
|
4636
|
+
fileId: tempUniqueId || upload.fileId,
|
|
4637
|
+
fileName: upload.fileName,
|
|
4638
|
+
progress: 100,
|
|
4639
|
+
status: 'completed',
|
|
4640
|
+
uploadedFile: response
|
|
4641
|
+
});
|
|
4642
|
+
if (responseFile) {
|
|
4643
|
+
uploadedFiles.push(responseFile);
|
|
4644
|
+
}
|
|
4645
|
+
console.log('🔍 [GlobalFileUploader] Upload response received:', response);
|
|
4646
|
+
console.log('🔍 [GlobalFileUploader] Response data structure:', response?.data);
|
|
4647
|
+
// Check if all files are completed
|
|
4648
|
+
if (completedCount + failedCount === totalFiles) {
|
|
4649
|
+
this.handleMultipleUploadComplete(completedCount, failedCount, groupId, uploadedFiles);
|
|
4650
|
+
}
|
|
4651
|
+
},
|
|
4652
|
+
error: (error) => {
|
|
4653
|
+
failedCount++;
|
|
4654
|
+
console.error(`❌ [GlobalFileUploader] File ${index + 1}/${totalFiles} upload failed:`, error);
|
|
4655
|
+
this.updateUploadStatus(upload.fileId, 'error', 0, error.message || 'Upload failed');
|
|
4656
|
+
this.uploadError.emit({
|
|
4657
|
+
fileId: upload.fileId,
|
|
4658
|
+
error: error.message || 'Upload failed'
|
|
4659
|
+
});
|
|
4660
|
+
// Check if all files are completed
|
|
4661
|
+
if (completedCount + failedCount === totalFiles) {
|
|
4662
|
+
this.handleMultipleUploadComplete(completedCount, failedCount, groupId, uploadedFiles);
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
});
|
|
4606
4666
|
});
|
|
4607
|
-
// Check if this is a file input with files
|
|
4608
|
-
if (target.type === 'file' && target.files && target.files.length > 0) {
|
|
4609
|
-
console.log('📁 [FloatingFileUploader] File input change detected:', target.files.length, 'files');
|
|
4610
|
-
// Check if the input has a data-user-id attribute for user context
|
|
4611
|
-
const userId = target.getAttribute('data-user-id');
|
|
4612
|
-
if (userId && userId !== this.currentUserId()) {
|
|
4613
|
-
this.setCurrentUserId(userId);
|
|
4614
|
-
}
|
|
4615
|
-
// Handle the files
|
|
4616
|
-
this.handleFiles(Array.from(target.files));
|
|
4617
|
-
// Reset the input to allow selecting the same files again
|
|
4618
|
-
target.value = '';
|
|
4619
|
-
}
|
|
4620
4667
|
}
|
|
4621
4668
|
/**
|
|
4622
|
-
* Handle
|
|
4669
|
+
* Handle multiple upload completion
|
|
4623
4670
|
*/
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4671
|
+
handleMultipleUploadComplete(completed, failed, groupId, uploadedFiles) {
|
|
4672
|
+
console.log(`📊 [GlobalFileUploader] Multiple upload complete: ${completed}/${completed + failed} successful`);
|
|
4673
|
+
this.isUploading.set(false);
|
|
4674
|
+
if (completed > 0) {
|
|
4675
|
+
this.allUploadsComplete.emit({
|
|
4676
|
+
groupId: groupId,
|
|
4677
|
+
uploadedFiles: uploadedFiles
|
|
4678
|
+
});
|
|
4630
4679
|
}
|
|
4631
4680
|
}
|
|
4632
4681
|
/**
|
|
4633
|
-
*
|
|
4682
|
+
* Upload next file in queue (single file mode)
|
|
4634
4683
|
*/
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
this.updateVisibility();
|
|
4684
|
+
uploadNextFile() {
|
|
4685
|
+
const pendingUpload = this.uploadQueue().find(upload => upload.status === 'pending');
|
|
4686
|
+
if (!pendingUpload) {
|
|
4687
|
+
this.isUploading.set(false);
|
|
4688
|
+
return;
|
|
4641
4689
|
}
|
|
4690
|
+
// Update status to uploading
|
|
4691
|
+
this.updateUploadStatus(pendingUpload.fileId, 'uploading', 0);
|
|
4692
|
+
const uploadRequest = {
|
|
4693
|
+
file: pendingUpload.file,
|
|
4694
|
+
userId: this.userId,
|
|
4695
|
+
tags: [],
|
|
4696
|
+
permissions: ['read', 'write']
|
|
4697
|
+
};
|
|
4698
|
+
this.fileService.uploadFile(pendingUpload.file, this.convertToFileUploadOptions(uploadRequest), (progress) => {
|
|
4699
|
+
this.updateUploadProgress(pendingUpload.fileId, progress);
|
|
4700
|
+
})
|
|
4701
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
4702
|
+
.subscribe({
|
|
4703
|
+
next: (response) => {
|
|
4704
|
+
console.log('✅ [GlobalFileUploader] Single file upload successful:', response);
|
|
4705
|
+
// Find the upload item by cyfm_temp_unique_id from response
|
|
4706
|
+
const responseFile = response?.data?.core_file_manager?.[0];
|
|
4707
|
+
const tempUniqueId = responseFile?.cyfm_temp_unique_id;
|
|
4708
|
+
console.log('🔍 [GlobalFileUploader] Single upload - Response tempUniqueId:', tempUniqueId);
|
|
4709
|
+
console.log('🔍 [GlobalFileUploader] Single upload - Original upload fileId:', pendingUpload.fileId);
|
|
4710
|
+
// Use the tempUniqueId to find and update the correct upload item
|
|
4711
|
+
if (tempUniqueId) {
|
|
4712
|
+
this.updateUploadStatusByTempId(tempUniqueId, 'completed', 100, undefined, response);
|
|
4713
|
+
}
|
|
4714
|
+
else {
|
|
4715
|
+
// Fallback to original fileId
|
|
4716
|
+
this.updateUploadStatus(pendingUpload.fileId, 'completed', 100, undefined, response);
|
|
4717
|
+
}
|
|
4718
|
+
this.uploadComplete.emit({
|
|
4719
|
+
fileId: tempUniqueId || pendingUpload.fileId,
|
|
4720
|
+
fileName: pendingUpload.fileName,
|
|
4721
|
+
progress: 100,
|
|
4722
|
+
status: 'completed',
|
|
4723
|
+
uploadedFile: response
|
|
4724
|
+
});
|
|
4725
|
+
// Process next file
|
|
4726
|
+
setTimeout(() => this.uploadNextFile(), 500);
|
|
4727
|
+
},
|
|
4728
|
+
error: (error) => {
|
|
4729
|
+
console.error('❌ [GlobalFileUploader] Upload error:', error);
|
|
4730
|
+
this.updateUploadStatus(pendingUpload.fileId, 'error', 0, error.message || 'Upload failed');
|
|
4731
|
+
this.uploadError.emit({
|
|
4732
|
+
fileId: pendingUpload.fileId,
|
|
4733
|
+
error: error.message || 'Upload failed'
|
|
4734
|
+
});
|
|
4735
|
+
// Process next file
|
|
4736
|
+
setTimeout(() => this.uploadNextFile(), 500);
|
|
4737
|
+
}
|
|
4738
|
+
});
|
|
4642
4739
|
}
|
|
4643
4740
|
/**
|
|
4644
|
-
*
|
|
4741
|
+
* Update upload progress
|
|
4645
4742
|
*/
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
if (files && files.length > 0) {
|
|
4651
|
-
this.handleFiles(Array.from(files));
|
|
4652
|
-
}
|
|
4743
|
+
updateUploadProgress(fileId, progress) {
|
|
4744
|
+
this.uploadQueue.update(queue => queue.map(upload => upload.fileId === fileId
|
|
4745
|
+
? { ...upload, progress }
|
|
4746
|
+
: upload));
|
|
4653
4747
|
}
|
|
4654
4748
|
/**
|
|
4655
|
-
*
|
|
4749
|
+
* Update upload status
|
|
4656
4750
|
*/
|
|
4657
|
-
|
|
4658
|
-
console.log('
|
|
4659
|
-
|
|
4660
|
-
|
|
4751
|
+
updateUploadStatus(fileId, status, progress, error, uploadedFile) {
|
|
4752
|
+
console.log('🔄 [GlobalFileUploader] Updating upload status:', { fileId, status, progress, error: !!error, uploadedFile: !!uploadedFile });
|
|
4753
|
+
this.uploadQueue.update(queue => {
|
|
4754
|
+
const updatedQueue = queue.map(upload => {
|
|
4755
|
+
if (upload.fileId === fileId) {
|
|
4756
|
+
console.log('🔄 [GlobalFileUploader] Found upload to update:', upload.fileName, 'from', upload.status, 'to', status);
|
|
4757
|
+
return { ...upload, status, progress, error, uploadedFile };
|
|
4758
|
+
}
|
|
4759
|
+
return upload;
|
|
4760
|
+
});
|
|
4761
|
+
console.log('🔄 [GlobalFileUploader] Updated queue:', updatedQueue.map(u => ({ fileName: u.fileName, status: u.status })));
|
|
4762
|
+
return updatedQueue;
|
|
4763
|
+
});
|
|
4661
4764
|
}
|
|
4662
4765
|
/**
|
|
4663
|
-
* Update
|
|
4766
|
+
* Update upload status by cyfm_temp_unique_id
|
|
4664
4767
|
*/
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4768
|
+
updateUploadStatusByTempId(tempUniqueId, status, progress, error, uploadedFile) {
|
|
4769
|
+
console.log('🔄 [GlobalFileUploader] Updating upload status by tempId:', { tempUniqueId, status, progress, error: !!error, uploadedFile: !!uploadedFile });
|
|
4770
|
+
this.uploadQueue.update(queue => {
|
|
4771
|
+
const updatedQueue = queue.map(upload => {
|
|
4772
|
+
// Check if this upload's fileId matches the tempUniqueId
|
|
4773
|
+
if (upload.fileId === tempUniqueId) {
|
|
4774
|
+
console.log('🔄 [GlobalFileUploader] Found upload by tempId to update:', upload.fileName, 'from', upload.status, 'to', status);
|
|
4775
|
+
return { ...upload, status, progress, error, uploadedFile };
|
|
4776
|
+
}
|
|
4777
|
+
return upload;
|
|
4778
|
+
});
|
|
4779
|
+
console.log('🔄 [GlobalFileUploader] Updated queue by tempId:', updatedQueue.map(u => ({ fileName: u.fileName, status: u.status, fileId: u.fileId })));
|
|
4780
|
+
return updatedQueue;
|
|
4781
|
+
});
|
|
4668
4782
|
}
|
|
4669
4783
|
/**
|
|
4670
|
-
*
|
|
4784
|
+
* Handle upload error
|
|
4671
4785
|
*/
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
this.isAnimating.set(false);
|
|
4679
|
-
console.log('🎬 [FloatingFileUploader] Animation completed, isVisible:', this.isVisible());
|
|
4680
|
-
}, 300);
|
|
4786
|
+
handleUploadError(error, fileId) {
|
|
4787
|
+
if (fileId) {
|
|
4788
|
+
this.updateUploadStatus(fileId, 'error', 0, error);
|
|
4789
|
+
this.uploadError.emit({ fileId, error });
|
|
4790
|
+
}
|
|
4791
|
+
this.isUploading.set(false);
|
|
4681
4792
|
}
|
|
4682
4793
|
/**
|
|
4683
|
-
*
|
|
4794
|
+
* Cancel upload
|
|
4684
4795
|
*/
|
|
4685
|
-
|
|
4686
|
-
this.
|
|
4687
|
-
|
|
4796
|
+
cancelUpload(fileId) {
|
|
4797
|
+
this.updateUploadStatus(fileId, 'cancelled', 0);
|
|
4798
|
+
this.uploadCancelled.emit(fileId);
|
|
4799
|
+
// Remove from queue after a delay
|
|
4688
4800
|
setTimeout(() => {
|
|
4689
|
-
this.
|
|
4690
|
-
|
|
4691
|
-
}, 300);
|
|
4801
|
+
this.uploadQueue.update(queue => queue.filter(upload => upload.fileId !== fileId));
|
|
4802
|
+
}, 1000);
|
|
4692
4803
|
}
|
|
4693
4804
|
/**
|
|
4694
|
-
*
|
|
4805
|
+
* Retry upload
|
|
4695
4806
|
*/
|
|
4696
|
-
|
|
4697
|
-
this.
|
|
4807
|
+
retryUpload(fileId) {
|
|
4808
|
+
this.updateUploadStatus(fileId, 'pending', 0);
|
|
4809
|
+
this.processUploadQueue();
|
|
4698
4810
|
}
|
|
4699
4811
|
/**
|
|
4700
|
-
*
|
|
4812
|
+
* Remove upload from queue
|
|
4701
4813
|
*/
|
|
4702
|
-
|
|
4703
|
-
this.
|
|
4814
|
+
removeUpload(fileId) {
|
|
4815
|
+
this.uploadQueue.update(queue => queue.filter(upload => upload.fileId !== fileId));
|
|
4704
4816
|
}
|
|
4705
4817
|
/**
|
|
4706
|
-
*
|
|
4818
|
+
* Clear completed uploads
|
|
4707
4819
|
*/
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
const activeCount = this.activeUploads().size;
|
|
4711
|
-
const completed = this.completedUploads().length;
|
|
4712
|
-
const failed = this.failedUploads().length;
|
|
4713
|
-
if (activeCount > 0) {
|
|
4714
|
-
return `${activeCount} uploading`;
|
|
4715
|
-
}
|
|
4716
|
-
else if (completed > 0 && failed === 0) {
|
|
4717
|
-
return `${completed} completed`;
|
|
4718
|
-
}
|
|
4719
|
-
else if (failed > 0) {
|
|
4720
|
-
return `${completed} completed, ${failed} failed`;
|
|
4721
|
-
}
|
|
4722
|
-
else if (queueLength > 0) {
|
|
4723
|
-
return `${queueLength} pending`;
|
|
4724
|
-
}
|
|
4725
|
-
return 'No uploads';
|
|
4820
|
+
clearCompleted() {
|
|
4821
|
+
this.uploadQueue.update(queue => queue.filter(upload => upload.status !== 'completed'));
|
|
4726
4822
|
}
|
|
4727
4823
|
/**
|
|
4728
|
-
*
|
|
4824
|
+
* Clear all uploads
|
|
4729
4825
|
*/
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
return 0;
|
|
4734
|
-
const totalProgress = Array.from(activeUploads.values()).reduce((sum, upload) => sum + upload.percentage, 0);
|
|
4735
|
-
return Math.round(totalProgress / activeUploads.size);
|
|
4826
|
+
clearAll() {
|
|
4827
|
+
this.uploadQueue.set([]);
|
|
4828
|
+
this.currentGroupId.set(null);
|
|
4736
4829
|
}
|
|
4737
4830
|
/**
|
|
4738
|
-
* Get status icon
|
|
4831
|
+
* Get status icon
|
|
4739
4832
|
*/
|
|
4740
|
-
getStatusIcon(
|
|
4741
|
-
switch (
|
|
4742
|
-
case '
|
|
4833
|
+
getStatusIcon(status) {
|
|
4834
|
+
switch (status) {
|
|
4835
|
+
case 'pending': return 'schedule';
|
|
4743
4836
|
case 'uploading': return 'cloud_upload';
|
|
4744
|
-
case '
|
|
4837
|
+
case 'completed': return 'check_circle';
|
|
4745
4838
|
case 'error': return 'error';
|
|
4839
|
+
case 'cancelled': return 'cancel';
|
|
4746
4840
|
default: return 'help';
|
|
4747
4841
|
}
|
|
4748
4842
|
}
|
|
4749
4843
|
/**
|
|
4750
|
-
* Get status class
|
|
4844
|
+
* Get status class
|
|
4751
4845
|
*/
|
|
4752
|
-
getStatusClass(
|
|
4753
|
-
switch (
|
|
4754
|
-
case '
|
|
4846
|
+
getStatusClass(status) {
|
|
4847
|
+
switch (status) {
|
|
4848
|
+
case 'pending': return 'status-pending';
|
|
4755
4849
|
case 'uploading': return 'status-uploading';
|
|
4756
|
-
case '
|
|
4850
|
+
case 'completed': return 'status-completed';
|
|
4757
4851
|
case 'error': return 'status-error';
|
|
4852
|
+
case 'cancelled': return 'status-cancelled';
|
|
4758
4853
|
default: return 'status-unknown';
|
|
4759
4854
|
}
|
|
4760
4855
|
}
|
|
4761
4856
|
/**
|
|
4762
|
-
*
|
|
4857
|
+
* Get file size display
|
|
4763
4858
|
*/
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4859
|
+
getFileSizeDisplay(file) {
|
|
4860
|
+
const bytes = file.size;
|
|
4861
|
+
if (bytes === 0)
|
|
4862
|
+
return '0 Bytes';
|
|
4863
|
+
const k = 1024;
|
|
4864
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
4865
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
4866
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
4767
4867
|
}
|
|
4768
4868
|
/**
|
|
4769
|
-
*
|
|
4869
|
+
* Generate unique file ID - this should match the service's generateFileId method
|
|
4870
|
+
* The service uses: ${file.name}_${file.size}_${Date.now()}
|
|
4770
4871
|
*/
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
const parts = fileId.split('_');
|
|
4774
|
-
if (parts.length >= 3) {
|
|
4775
|
-
// Remove the last two parts (size and timestamp) to get the filename
|
|
4776
|
-
return parts.slice(0, -2).join('_');
|
|
4777
|
-
}
|
|
4778
|
-
return fileId;
|
|
4872
|
+
generateFileId(file) {
|
|
4873
|
+
return `${file.name}_${file.size}_${Date.now()}`;
|
|
4779
4874
|
}
|
|
4780
4875
|
/**
|
|
4781
|
-
*
|
|
4876
|
+
* Convert IFileUploadRequest to FileUploadOptions
|
|
4782
4877
|
*/
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4878
|
+
convertToFileUploadOptions(request) {
|
|
4879
|
+
return {
|
|
4880
|
+
altText: request.altText,
|
|
4881
|
+
userId: request.userId,
|
|
4882
|
+
permissions: request.permissions,
|
|
4883
|
+
tags: request.tags,
|
|
4884
|
+
groupId: request.groupId
|
|
4885
|
+
};
|
|
4786
4886
|
}
|
|
4787
4887
|
/**
|
|
4788
|
-
*
|
|
4789
|
-
* This can be called by other components to trigger the floating uploader
|
|
4888
|
+
* Trigger file input click
|
|
4790
4889
|
*/
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
this.setCurrentUserId(userId);
|
|
4890
|
+
triggerFileInput() {
|
|
4891
|
+
const fileInput = document.getElementById('global-file-input');
|
|
4892
|
+
if (fileInput) {
|
|
4893
|
+
fileInput.click();
|
|
4796
4894
|
}
|
|
4797
|
-
// Upload files using file manager service
|
|
4798
|
-
// The file manager service will handle adding to its queue and the effect will show the floating uploader
|
|
4799
|
-
files.forEach((file, index) => {
|
|
4800
|
-
console.log(`📁 [FloatingFileUploader] Starting upload for file ${index + 1}/${files.length}:`, file.name);
|
|
4801
|
-
this.fileManagerService.uploadFile(file, {
|
|
4802
|
-
userId: this.currentUserId(),
|
|
4803
|
-
permissions: ['read', 'write'],
|
|
4804
|
-
tags: []
|
|
4805
|
-
})
|
|
4806
|
-
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
4807
|
-
.subscribe({
|
|
4808
|
-
next: (response) => {
|
|
4809
|
-
console.log('✅ [FloatingFileUploader] Upload completed:', response);
|
|
4810
|
-
},
|
|
4811
|
-
error: (error) => {
|
|
4812
|
-
console.error('❌ [FloatingFileUploader] Upload failed:', error);
|
|
4813
|
-
}
|
|
4814
|
-
});
|
|
4815
|
-
});
|
|
4816
4895
|
}
|
|
4817
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type:
|
|
4818
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: CideEleFloatingFileUploaderComponent, isStandalone: true, selector: "cide-ele-floating-file-uploader", ngImport: i0, template: "<!-- Floating File Uploader Container -->\r\n@if (isVisible()) {\r\n<div class=\"floating-uploader\" \r\n [class.minimized]=\"isMinimized()\" \r\n [class.animating]=\"isAnimating()\">\r\n\r\n <!-- Header -->\r\n <div class=\"uploader-header\">\r\n <div class=\"header-left\">\r\n <div class=\"upload-icon\">\r\n <cide-ele-icon size=\"sm\">cloud_upload</cide-ele-icon>\r\n </div>\r\n <div class=\"upload-info\">\r\n <div class=\"upload-title\">File Upload</div>\r\n <div class=\"upload-summary\">{{ getUploadSummary() }}</div>\r\n </div>\r\n </div>\r\n \r\n <div class=\"header-actions\">\r\n <button class=\"action-btn minimize-btn\" (click)=\"toggleMinimize()\" [title]=\"isMinimized() ? 'Expand' : 'Minimize'\">\r\n <cide-ele-icon size=\"xs\">{{ isMinimized() ? 'expand_more' : 'expand_less' }}</cide-ele-icon>\r\n </button>\r\n <button class=\"action-btn close-btn\" (click)=\"close()\" title=\"Close\">\r\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Content (hidden when minimized) -->\r\n @if (!isMinimized()) {\r\n <div class=\"uploader-content\">\r\n \r\n <!-- Overall Progress Bar -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0) {\r\n <div class=\"overall-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"getOverallProgress()\"></div>\r\n </div>\r\n <div class=\"progress-text\">{{ getOverallProgress() }}%</div>\r\n </div>\r\n }\r\n\r\n <!-- Upload Queue - Show files from file manager service -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0) {\r\n <div class=\"upload-queue\">\r\n <!-- Show files in queue (pending) -->\r\n @for (fileId of uploadQueue(); track fileId) {\r\n <div class=\"upload-item status-pending\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">schedule</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(fileId) }}</div>\r\n <div class=\"file-status\">\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"upload-actions\">\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(fileId)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Show active uploads -->\r\n @for (entry of activeUploads() | keyvalue; track entry.key) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(entry.value.stage)\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">{{ getStatusIcon(entry.value.stage) }}</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(entry.key) }}</div>\r\n <div class=\"file-status\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <span class=\"text-yellow-600\">Reading...</span>\r\n }\r\n @case ('uploading') {\r\n <span class=\"text-blue-600\">Uploading...</span>\r\n }\r\n @case ('complete') {\r\n <span class=\"text-green-600\">Completed</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">Failed</span>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar (for uploading files) -->\r\n @if (entry.value.stage === 'uploading') {\r\n <div class=\"file-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"entry.value.percentage\"></div>\r\n </div>\r\n <span class=\"progress-text\">{{ entry.value.percentage }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('complete') {\r\n <button class=\"action-btn success-btn\" title=\"Completed\">\r\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button class=\"action-btn retry-btn\" title=\"Retry\">\r\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n</div>\r\n}\r\n", styles: [".floating-uploader{position:fixed;bottom:20px;right:20px;width:320px;max-height:500px;background:#fff;border-radius:12px;box-shadow:0 8px 32px #0000001f;border:1px solid rgba(0,0,0,.08);z-index:1000;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1);transform:translateY(0);opacity:1}.floating-uploader.animating{transition:all .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.minimized .uploader-content{display:none}.floating-uploader.minimized .uploader-footer{border-top:none}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f626}.floating-uploader .uploader-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#f8fafc;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-header .header-left{display:flex;align-items:center;gap:8px}.floating-uploader .uploader-header .header-left .upload-icon{display:flex;align-items:center;justify-content:center;width:24px;height:24px;background:#3b82f6;border-radius:6px;color:#fff}.floating-uploader .uploader-header .header-left .upload-info .upload-title{font-size:14px;font-weight:600;color:#1e293b;margin:0}.floating-uploader .uploader-header .header-left .upload-info .upload-summary{font-size:12px;color:#64748b;margin:0}.floating-uploader .uploader-header .header-actions{display:flex;gap:4px}.floating-uploader .uploader-header .header-actions .action-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:background-color .2s;color:#64748b}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#e2e8f0;color:#1e293b}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content{max-height:400px;overflow-y:auto}.floating-uploader .uploader-content .overall-progress{padding:12px 16px;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-content .overall-progress .progress-bar{width:100%;height:4px;background:#e2e8f0;border-radius:2px;overflow:hidden;margin-bottom:4px}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .overall-progress .progress-text{font-size:12px;color:#64748b;text-align:center;display:block}.floating-uploader .uploader-content .upload-queue .upload-item{display:flex;align-items:center;padding:8px 16px;border-bottom:1px solid #f1f5f9;transition:background-color .2s}.floating-uploader .uploader-content .upload-queue .upload-item:last-child{border-bottom:none}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#f0f9ff}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#f0fdf4}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#fef2f2}.floating-uploader .uploader-content .upload-queue .upload-item .file-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .status-icon{flex-shrink:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details{min-width:0;flex:1}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{font-size:13px;font-weight:500;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status{font-size:11px;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status span{font-weight:500}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress{display:flex;align-items:center;gap:8px;margin:0 8px;min-width:80px}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{flex:1;height:3px;background:#e2e8f0;border-radius:2px;overflow:hidden}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text{font-size:10px;color:#64748b;min-width:24px;text-align:right}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions{display:flex;gap:4px}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:all .2s;color:#64748b}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#e2e8f0}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#f0f9ff;color:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#16a34a}.floating-uploader .uploader-content .hidden-uploader{display:none}.floating-uploader .uploader-footer{padding:8px 16px;background:#f8fafc;border-top:1px solid #e2e8f0}.floating-uploader .uploader-footer .footer-stats{display:flex;gap:12px;font-size:11px}.floating-uploader .uploader-footer .footer-stats .stat{display:flex;align-items:center;gap:4px;color:#64748b}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#3b82f6}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#16a34a}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#dc2626}@media (max-width: 640px){.floating-uploader{bottom:10px;right:10px;left:10px;width:auto;max-width:none}}@media (prefers-color-scheme: dark){.floating-uploader{background:#1e293b;border-color:#334155;box-shadow:0 8px 32px #0000004d}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f633}.floating-uploader .uploader-header{background:#334155;border-bottom-color:#475569}.floating-uploader .uploader-header .header-left .upload-icon{background:#3b82f6}.floating-uploader .uploader-header .header-left .upload-info .upload-title{color:#f1f5f9}.floating-uploader .uploader-header .header-left .upload-info .upload-summary,.floating-uploader .uploader-header .header-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#475569;color:#f1f5f9}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .overall-progress{border-bottom-color:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .overall-progress .progress-text{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item{border-bottom-color:#334155}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#1e3a8a}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#14532d}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#7f1d1d}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{color:#f1f5f9}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text,.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#1e3a8a;color:#60a5fa}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#4ade80}.floating-uploader .uploader-footer{background:#334155;border-top-color:#475569}.floating-uploader .uploader-footer .footer-stats .stat{color:#94a3b8}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#60a5fa}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#4ade80}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#fca5a5}}@keyframes slideInUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes slideOutDown{0%{transform:translateY(0);opacity:1}to{transform:translateY(100%);opacity:0}}.floating-uploader.animating{animation:slideInUp .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.animating.hiding{animation:slideOutDown .3s cubic-bezier(.4,0,.2,1)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
|
|
4896
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleGlobalFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4897
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: CideEleGlobalFileUploaderComponent, isStandalone: true, selector: "cide-ele-global-file-uploader", inputs: { userId: "userId", multiple: "multiple", accept: "accept", maxFileSize: "maxFileSize", allowedTypes: "allowedTypes" }, outputs: { uploadComplete: "uploadComplete", uploadError: "uploadError", uploadCancelled: "uploadCancelled", allUploadsComplete: "allUploadsComplete" }, ngImport: i0, template: "<!-- Global File Uploader Component -->\r\n<div class=\"global-file-uploader\">\r\n\r\n <!-- Hidden file input -->\r\n <input id=\"global-file-input\" type=\"file\" [multiple]=\"multiple\" [accept]=\"accept\" \r\n (change)=\"onFileInputChange($event)\" style=\"display: none;\">\r\n\r\n <!-- Drag and Drop Zone -->\r\n <div class=\"upload-zone\" [class.drag-over]=\"isDragOver()\" (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\" (drop)=\"onDrop($event)\" (click)=\"triggerFileInput()\">\r\n\r\n <div class=\"upload-zone-content\">\r\n <cide-ele-icon name=\"cloud_upload\" class=\"upload-icon\" size=\"lg\">\r\n </cide-ele-icon>\r\n\r\n <h3 class=\"upload-title\">\r\n {{ isDragOver() ? 'Drop files here' : 'Upload Files' }}\r\n </h3>\r\n\r\n <p class=\"upload-subtitle\">\r\n {{ isDragOver() ? 'Release to upload' : 'Drag and drop files here or click to browse' }}\r\n </p>\r\n\r\n <button cideEleButton cideEleButton variant=\"outline\" size=\"sm\"\r\n (click)=\"triggerFileInput(); $event.stopPropagation()\">\r\n <cide-ele-icon name=\"add\" size=\"sm\"></cide-ele-icon>\r\n Choose Files\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Upload Queue -->\r\n @if (hasUploads()) {\r\n <div class=\"upload-queue\">\r\n <div class=\"queue-header\">\r\n <h4 class=\"queue-title\">Upload Queue</h4>\r\n <div class=\"queue-actions\">\r\n @if (completedUploads().length > 0) {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearCompleted()\">\r\n Clear Completed\r\n </button>\r\n }\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearAll()\">\r\n Clear All\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"queue-items\">\r\n @for (upload of uploadQueue(); track upload.fileId) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(upload.status)\">\r\n\r\n <!-- File Info -->\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"sm\">\r\n {{getStatusIcon(upload.status)}}\r\n </cide-ele-icon>\r\n\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ upload.fileName }}</div>\r\n @if (upload.file) {\r\n <div class=\"file-size\">{{ getFileSizeDisplay(upload.file) }}</div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar -->\r\n @if (upload.status === 'uploading') {\r\n <div class=\"progress-container\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"upload.progress\">\r\n </div>\r\n </div>\r\n <span class=\"progress-text\">{{ upload.progress }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Status Text -->\r\n @if (upload.status !== 'uploading') {\r\n <div class=\"status-text\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n }\r\n @case ('completed') {\r\n <span class=\"text-green-600\">Uploaded successfully</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">{{ upload.error || 'Upload failed' }}</span>\r\n }\r\n @case ('cancelled') {\r\n <span class=\"text-gray-600\">Cancelled</span>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('completed') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"retryUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"refresh\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('cancelled') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Queue Summary -->\r\n <div class=\"queue-summary\">\r\n <div class=\"summary-stats\">\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"schedule\" size=\"xs\"></cide-ele-icon>\r\n {{ pendingUploads().length }} pending\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"cloud_upload\" size=\"xs\"></cide-ele-icon>\r\n {{ activeUploads().length }} uploading\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"check_circle\" size=\"xs\"></cide-ele-icon>\r\n {{ completedUploads().length }} completed\r\n </span>\r\n @if (failedUploads().length > 0) {\r\n <span class=\"stat error\">\r\n <cide-ele-icon name=\"error\" size=\"xs\"></cide-ele-icon>\r\n {{ failedUploads().length }} failed\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n\r\n", styles: [".global-file-uploader{@apply tw-w-full tw-max-w-2xl tw-mx-auto tw-p-4;}.global-file-uploader .upload-zone{@apply tw-border-2 tw-border-dashed tw-border-gray-300 tw-rounded-lg tw-p-8 tw-text-center tw-cursor-pointer tw-transition-all tw-duration-200 tw-ease-in-out;@apply hover:tw-border-blue-400 hover:tw-bg-blue-50;}.global-file-uploader .upload-zone.drag-over{@apply tw-border-blue-500 tw-bg-blue-100 tw-scale-105;}.global-file-uploader .upload-zone .upload-zone-content{@apply tw-space-y-4;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-400 tw-mx-auto;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-xl tw-font-semibold tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-sm tw-text-gray-500 tw-m-0;}.global-file-uploader .upload-queue{@apply tw-mt-6 tw-bg-white tw-border tw-border-gray-200 tw-rounded-lg tw-shadow-sm;}.global-file-uploader .upload-queue .queue-header{@apply tw-flex tw-justify-between tw-items-center tw-p-4 tw-border-b tw-border-gray-200;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-lg tw-font-medium tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-flex tw-space-x-2;}.global-file-uploader .upload-queue .queue-items{@apply tw-max-h-96 tw-overflow-y-auto;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-flex tw-items-center tw-justify-between tw-p-4 tw-border-b tw-border-gray-100 tw-transition-colors tw-duration-200;}.global-file-uploader .upload-queue .queue-items .upload-item:last-child{@apply tw-border-b-0;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-50;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-flex tw-items-center tw-flex-1 tw-min-w-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .status-icon{@apply tw-mr-3 tw-flex-shrink-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details{@apply tw-min-w-0 tw-flex-1;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-sm tw-font-medium tw-text-gray-700 tw-truncate;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-xs tw-text-gray-500 tw-mt-1;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-flex tw-items-center tw-space-x-3 tw-flex-1 tw-max-w-xs tw-mx-4;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-flex-1 tw-h-2 tw-bg-gray-200 tw-rounded-full tw-overflow-hidden;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar .progress-fill{@apply tw-h-full tw-bg-blue-500 tw-transition-all tw-duration-300 tw-ease-out;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-xs tw-font-medium tw-text-gray-600 tw-min-w-12 tw-text-right;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-flex-1 tw-text-sm tw-text-center;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text span{@apply tw-font-medium;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-flex tw-space-x-1 tw-ml-4;}.global-file-uploader .upload-queue .queue-summary{@apply tw-p-4 tw-bg-gray-50 tw-border-t tw-border-gray-200;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-flex tw-flex-wrap tw-gap-4 tw-text-sm tw-text-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat{@apply tw-flex tw-items-center tw-space-x-1;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-600;}@media (max-width: 640px){.global-file-uploader{@apply tw-p-2;}.global-file-uploader .upload-zone{@apply tw-p-6;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-lg;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-xs;}.global-file-uploader .upload-queue .queue-header{@apply tw-p-3 tw-flex-col tw-space-y-2 tw-items-start;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-w-full tw-justify-end;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-p-3 tw-flex-col tw-space-y-3 tw-items-start;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-w-full;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-w-full tw-mx-0;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-w-full tw-text-left;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-w-full tw-justify-end tw-ml-0;}}@media (prefers-color-scheme: dark){.global-file-uploader .upload-zone{@apply tw-border-gray-600 hover:tw-border-blue-400 hover:tw-bg-gray-800;}.global-file-uploader .upload-zone.drag-over{@apply tw-bg-blue-900;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-500;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-gray-200;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-gray-400;}.global-file-uploader .upload-queue{@apply tw-bg-gray-800 tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-gray-400;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-bg-gray-600;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary{@apply tw-bg-gray-700 tw-border-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-400;}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CideEleButtonComponent, selector: "button[cideEleButton], a[cideEleButton]", inputs: ["label", "variant", "size", "type", "shape", "elevation", "disabled", "id", "loading", "fullWidth", "leftIcon", "rightIcon", "customClass", "tooltip", "ariaLabel", "testId", "routerLink", "routerExtras", "preventDoubleClick", "animated"], outputs: ["btnClick", "doubleClick"] }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
|
|
4819
4898
|
}
|
|
4820
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type:
|
|
4899
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleGlobalFileUploaderComponent, decorators: [{
|
|
4821
4900
|
type: Component,
|
|
4822
|
-
args: [{ selector: 'cide-ele-
|
|
4901
|
+
args: [{ selector: 'cide-ele-global-file-uploader', standalone: true, imports: [
|
|
4823
4902
|
CommonModule,
|
|
4903
|
+
CideEleButtonComponent,
|
|
4824
4904
|
CideIconComponent
|
|
4825
|
-
], template: "<!-- Floating File Uploader Container -->\r\n@if (isVisible()) {\r\n<div class=\"floating-uploader\" \r\n [class.minimized]=\"isMinimized()\" \r\n [class.animating]=\"isAnimating()\">\r\n\r\n <!-- Header -->\r\n <div class=\"uploader-header\">\r\n <div class=\"header-left\">\r\n <div class=\"upload-icon\">\r\n <cide-ele-icon size=\"sm\">cloud_upload</cide-ele-icon>\r\n </div>\r\n <div class=\"upload-info\">\r\n <div class=\"upload-title\">File Upload</div>\r\n <div class=\"upload-summary\">{{ getUploadSummary() }}</div>\r\n </div>\r\n </div>\r\n \r\n <div class=\"header-actions\">\r\n <button class=\"action-btn minimize-btn\" (click)=\"toggleMinimize()\" [title]=\"isMinimized() ? 'Expand' : 'Minimize'\">\r\n <cide-ele-icon size=\"xs\">{{ isMinimized() ? 'expand_more' : 'expand_less' }}</cide-ele-icon>\r\n </button>\r\n <button class=\"action-btn close-btn\" (click)=\"close()\" title=\"Close\">\r\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Content (hidden when minimized) -->\r\n @if (!isMinimized()) {\r\n <div class=\"uploader-content\">\r\n \r\n <!-- Overall Progress Bar -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0) {\r\n <div class=\"overall-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"getOverallProgress()\"></div>\r\n </div>\r\n <div class=\"progress-text\">{{ getOverallProgress() }}%</div>\r\n </div>\r\n }\r\n\r\n <!-- Upload Queue - Show files from file manager service -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0) {\r\n <div class=\"upload-queue\">\r\n <!-- Show files in queue (pending) -->\r\n @for (fileId of uploadQueue(); track fileId) {\r\n <div class=\"upload-item status-pending\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">schedule</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(fileId) }}</div>\r\n <div class=\"file-status\">\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"upload-actions\">\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(fileId)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Show active uploads -->\r\n @for (entry of activeUploads() | keyvalue; track entry.key) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(entry.value.stage)\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">{{ getStatusIcon(entry.value.stage) }}</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(entry.key) }}</div>\r\n <div class=\"file-status\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <span class=\"text-yellow-600\">Reading...</span>\r\n }\r\n @case ('uploading') {\r\n <span class=\"text-blue-600\">Uploading...</span>\r\n }\r\n @case ('complete') {\r\n <span class=\"text-green-600\">Completed</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">Failed</span>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar (for uploading files) -->\r\n @if (entry.value.stage === 'uploading') {\r\n <div class=\"file-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"entry.value.percentage\"></div>\r\n </div>\r\n <span class=\"progress-text\">{{ entry.value.percentage }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('complete') {\r\n <button class=\"action-btn success-btn\" title=\"Completed\">\r\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button class=\"action-btn retry-btn\" title=\"Retry\">\r\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n</div>\r\n}\r\n", styles: [".floating-uploader{position:fixed;bottom:20px;right:20px;width:320px;max-height:500px;background:#fff;border-radius:12px;box-shadow:0 8px 32px #0000001f;border:1px solid rgba(0,0,0,.08);z-index:1000;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1);transform:translateY(0);opacity:1}.floating-uploader.animating{transition:all .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.minimized .uploader-content{display:none}.floating-uploader.minimized .uploader-footer{border-top:none}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f626}.floating-uploader .uploader-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#f8fafc;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-header .header-left{display:flex;align-items:center;gap:8px}.floating-uploader .uploader-header .header-left .upload-icon{display:flex;align-items:center;justify-content:center;width:24px;height:24px;background:#3b82f6;border-radius:6px;color:#fff}.floating-uploader .uploader-header .header-left .upload-info .upload-title{font-size:14px;font-weight:600;color:#1e293b;margin:0}.floating-uploader .uploader-header .header-left .upload-info .upload-summary{font-size:12px;color:#64748b;margin:0}.floating-uploader .uploader-header .header-actions{display:flex;gap:4px}.floating-uploader .uploader-header .header-actions .action-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:background-color .2s;color:#64748b}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#e2e8f0;color:#1e293b}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content{max-height:400px;overflow-y:auto}.floating-uploader .uploader-content .overall-progress{padding:12px 16px;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-content .overall-progress .progress-bar{width:100%;height:4px;background:#e2e8f0;border-radius:2px;overflow:hidden;margin-bottom:4px}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .overall-progress .progress-text{font-size:12px;color:#64748b;text-align:center;display:block}.floating-uploader .uploader-content .upload-queue .upload-item{display:flex;align-items:center;padding:8px 16px;border-bottom:1px solid #f1f5f9;transition:background-color .2s}.floating-uploader .uploader-content .upload-queue .upload-item:last-child{border-bottom:none}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#f0f9ff}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#f0fdf4}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#fef2f2}.floating-uploader .uploader-content .upload-queue .upload-item .file-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .status-icon{flex-shrink:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details{min-width:0;flex:1}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{font-size:13px;font-weight:500;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status{font-size:11px;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status span{font-weight:500}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress{display:flex;align-items:center;gap:8px;margin:0 8px;min-width:80px}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{flex:1;height:3px;background:#e2e8f0;border-radius:2px;overflow:hidden}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text{font-size:10px;color:#64748b;min-width:24px;text-align:right}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions{display:flex;gap:4px}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:all .2s;color:#64748b}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#e2e8f0}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#f0f9ff;color:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#16a34a}.floating-uploader .uploader-content .hidden-uploader{display:none}.floating-uploader .uploader-footer{padding:8px 16px;background:#f8fafc;border-top:1px solid #e2e8f0}.floating-uploader .uploader-footer .footer-stats{display:flex;gap:12px;font-size:11px}.floating-uploader .uploader-footer .footer-stats .stat{display:flex;align-items:center;gap:4px;color:#64748b}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#3b82f6}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#16a34a}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#dc2626}@media (max-width: 640px){.floating-uploader{bottom:10px;right:10px;left:10px;width:auto;max-width:none}}@media (prefers-color-scheme: dark){.floating-uploader{background:#1e293b;border-color:#334155;box-shadow:0 8px 32px #0000004d}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f633}.floating-uploader .uploader-header{background:#334155;border-bottom-color:#475569}.floating-uploader .uploader-header .header-left .upload-icon{background:#3b82f6}.floating-uploader .uploader-header .header-left .upload-info .upload-title{color:#f1f5f9}.floating-uploader .uploader-header .header-left .upload-info .upload-summary,.floating-uploader .uploader-header .header-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#475569;color:#f1f5f9}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .overall-progress{border-bottom-color:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .overall-progress .progress-text{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item{border-bottom-color:#334155}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#1e3a8a}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#14532d}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#7f1d1d}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{color:#f1f5f9}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text,.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#1e3a8a;color:#60a5fa}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#4ade80}.floating-uploader .uploader-footer{background:#334155;border-top-color:#475569}.floating-uploader .uploader-footer .footer-stats .stat{color:#94a3b8}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#60a5fa}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#4ade80}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#fca5a5}}@keyframes slideInUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes slideOutDown{0%{transform:translateY(0);opacity:1}to{transform:translateY(100%);opacity:0}}.floating-uploader.animating{animation:slideInUp .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.animating.hiding{animation:slideOutDown .3s cubic-bezier(.4,0,.2,1)}\n"] }]
|
|
4826
|
-
}], ctorParameters: () => []
|
|
4905
|
+
], template: "<!-- Global File Uploader Component -->\r\n<div class=\"global-file-uploader\">\r\n\r\n <!-- Hidden file input -->\r\n <input id=\"global-file-input\" type=\"file\" [multiple]=\"multiple\" [accept]=\"accept\" \r\n (change)=\"onFileInputChange($event)\" style=\"display: none;\">\r\n\r\n <!-- Drag and Drop Zone -->\r\n <div class=\"upload-zone\" [class.drag-over]=\"isDragOver()\" (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\" (drop)=\"onDrop($event)\" (click)=\"triggerFileInput()\">\r\n\r\n <div class=\"upload-zone-content\">\r\n <cide-ele-icon name=\"cloud_upload\" class=\"upload-icon\" size=\"lg\">\r\n </cide-ele-icon>\r\n\r\n <h3 class=\"upload-title\">\r\n {{ isDragOver() ? 'Drop files here' : 'Upload Files' }}\r\n </h3>\r\n\r\n <p class=\"upload-subtitle\">\r\n {{ isDragOver() ? 'Release to upload' : 'Drag and drop files here or click to browse' }}\r\n </p>\r\n\r\n <button cideEleButton cideEleButton variant=\"outline\" size=\"sm\"\r\n (click)=\"triggerFileInput(); $event.stopPropagation()\">\r\n <cide-ele-icon name=\"add\" size=\"sm\"></cide-ele-icon>\r\n Choose Files\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Upload Queue -->\r\n @if (hasUploads()) {\r\n <div class=\"upload-queue\">\r\n <div class=\"queue-header\">\r\n <h4 class=\"queue-title\">Upload Queue</h4>\r\n <div class=\"queue-actions\">\r\n @if (completedUploads().length > 0) {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearCompleted()\">\r\n Clear Completed\r\n </button>\r\n }\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearAll()\">\r\n Clear All\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"queue-items\">\r\n @for (upload of uploadQueue(); track upload.fileId) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(upload.status)\">\r\n\r\n <!-- File Info -->\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"sm\">\r\n {{getStatusIcon(upload.status)}}\r\n </cide-ele-icon>\r\n\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ upload.fileName }}</div>\r\n @if (upload.file) {\r\n <div class=\"file-size\">{{ getFileSizeDisplay(upload.file) }}</div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar -->\r\n @if (upload.status === 'uploading') {\r\n <div class=\"progress-container\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"upload.progress\">\r\n </div>\r\n </div>\r\n <span class=\"progress-text\">{{ upload.progress }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Status Text -->\r\n @if (upload.status !== 'uploading') {\r\n <div class=\"status-text\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n }\r\n @case ('completed') {\r\n <span class=\"text-green-600\">Uploaded successfully</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">{{ upload.error || 'Upload failed' }}</span>\r\n }\r\n @case ('cancelled') {\r\n <span class=\"text-gray-600\">Cancelled</span>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('completed') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"retryUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"refresh\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('cancelled') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Queue Summary -->\r\n <div class=\"queue-summary\">\r\n <div class=\"summary-stats\">\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"schedule\" size=\"xs\"></cide-ele-icon>\r\n {{ pendingUploads().length }} pending\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"cloud_upload\" size=\"xs\"></cide-ele-icon>\r\n {{ activeUploads().length }} uploading\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"check_circle\" size=\"xs\"></cide-ele-icon>\r\n {{ completedUploads().length }} completed\r\n </span>\r\n @if (failedUploads().length > 0) {\r\n <span class=\"stat error\">\r\n <cide-ele-icon name=\"error\" size=\"xs\"></cide-ele-icon>\r\n {{ failedUploads().length }} failed\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n\r\n", styles: [".global-file-uploader{@apply tw-w-full tw-max-w-2xl tw-mx-auto tw-p-4;}.global-file-uploader .upload-zone{@apply tw-border-2 tw-border-dashed tw-border-gray-300 tw-rounded-lg tw-p-8 tw-text-center tw-cursor-pointer tw-transition-all tw-duration-200 tw-ease-in-out;@apply hover:tw-border-blue-400 hover:tw-bg-blue-50;}.global-file-uploader .upload-zone.drag-over{@apply tw-border-blue-500 tw-bg-blue-100 tw-scale-105;}.global-file-uploader .upload-zone .upload-zone-content{@apply tw-space-y-4;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-400 tw-mx-auto;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-xl tw-font-semibold tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-sm tw-text-gray-500 tw-m-0;}.global-file-uploader .upload-queue{@apply tw-mt-6 tw-bg-white tw-border tw-border-gray-200 tw-rounded-lg tw-shadow-sm;}.global-file-uploader .upload-queue .queue-header{@apply tw-flex tw-justify-between tw-items-center tw-p-4 tw-border-b tw-border-gray-200;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-lg tw-font-medium tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-flex tw-space-x-2;}.global-file-uploader .upload-queue .queue-items{@apply tw-max-h-96 tw-overflow-y-auto;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-flex tw-items-center tw-justify-between tw-p-4 tw-border-b tw-border-gray-100 tw-transition-colors tw-duration-200;}.global-file-uploader .upload-queue .queue-items .upload-item:last-child{@apply tw-border-b-0;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-50;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-flex tw-items-center tw-flex-1 tw-min-w-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .status-icon{@apply tw-mr-3 tw-flex-shrink-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details{@apply tw-min-w-0 tw-flex-1;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-sm tw-font-medium tw-text-gray-700 tw-truncate;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-xs tw-text-gray-500 tw-mt-1;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-flex tw-items-center tw-space-x-3 tw-flex-1 tw-max-w-xs tw-mx-4;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-flex-1 tw-h-2 tw-bg-gray-200 tw-rounded-full tw-overflow-hidden;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar .progress-fill{@apply tw-h-full tw-bg-blue-500 tw-transition-all tw-duration-300 tw-ease-out;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-xs tw-font-medium tw-text-gray-600 tw-min-w-12 tw-text-right;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-flex-1 tw-text-sm tw-text-center;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text span{@apply tw-font-medium;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-flex tw-space-x-1 tw-ml-4;}.global-file-uploader .upload-queue .queue-summary{@apply tw-p-4 tw-bg-gray-50 tw-border-t tw-border-gray-200;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-flex tw-flex-wrap tw-gap-4 tw-text-sm tw-text-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat{@apply tw-flex tw-items-center tw-space-x-1;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-600;}@media (max-width: 640px){.global-file-uploader{@apply tw-p-2;}.global-file-uploader .upload-zone{@apply tw-p-6;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-lg;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-xs;}.global-file-uploader .upload-queue .queue-header{@apply tw-p-3 tw-flex-col tw-space-y-2 tw-items-start;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-w-full tw-justify-end;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-p-3 tw-flex-col tw-space-y-3 tw-items-start;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-w-full;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-w-full tw-mx-0;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-w-full tw-text-left;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-w-full tw-justify-end tw-ml-0;}}@media (prefers-color-scheme: dark){.global-file-uploader .upload-zone{@apply tw-border-gray-600 hover:tw-border-blue-400 hover:tw-bg-gray-800;}.global-file-uploader .upload-zone.drag-over{@apply tw-bg-blue-900;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-500;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-gray-200;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-gray-400;}.global-file-uploader .upload-queue{@apply tw-bg-gray-800 tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-gray-400;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-bg-gray-600;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary{@apply tw-bg-gray-700 tw-border-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-400;}}\n"] }]
|
|
4906
|
+
}], ctorParameters: () => [], propDecorators: { userId: [{
|
|
4907
|
+
type: Input
|
|
4908
|
+
}], multiple: [{
|
|
4909
|
+
type: Input
|
|
4910
|
+
}], accept: [{
|
|
4911
|
+
type: Input
|
|
4912
|
+
}], maxFileSize: [{
|
|
4913
|
+
type: Input
|
|
4914
|
+
}], allowedTypes: [{
|
|
4915
|
+
type: Input
|
|
4916
|
+
}], uploadComplete: [{
|
|
4917
|
+
type: Output
|
|
4918
|
+
}], uploadError: [{
|
|
4919
|
+
type: Output
|
|
4920
|
+
}], uploadCancelled: [{
|
|
4921
|
+
type: Output
|
|
4922
|
+
}], allUploadsComplete: [{
|
|
4923
|
+
type: Output
|
|
4924
|
+
}] } });
|
|
4925
|
+
|
|
4926
|
+
class CideFloatingUploadTriggerDirective {
|
|
4927
|
+
elementRef = inject(ElementRef);
|
|
4928
|
+
floatingUploader = inject(CideEleFloatingFileUploaderComponent);
|
|
4929
|
+
groupId;
|
|
4930
|
+
userId;
|
|
4931
|
+
showIcon = true;
|
|
4932
|
+
filesSelected = new EventEmitter();
|
|
4933
|
+
triggerIcon;
|
|
4934
|
+
ngOnInit() {
|
|
4935
|
+
this.setupTrigger();
|
|
4936
|
+
}
|
|
4937
|
+
ngOnDestroy() {
|
|
4938
|
+
this.removeTrigger();
|
|
4939
|
+
}
|
|
4940
|
+
setupTrigger() {
|
|
4941
|
+
const element = this.elementRef.nativeElement;
|
|
4942
|
+
if (element.type !== 'file') {
|
|
4943
|
+
console.warn('⚠️ [FloatingUploadTrigger] Directive should only be used on file input elements');
|
|
4944
|
+
return;
|
|
4945
|
+
}
|
|
4946
|
+
// Add change event listener
|
|
4947
|
+
element.addEventListener('change', this.onFileChange.bind(this));
|
|
4948
|
+
// Add floating uploader trigger icon if enabled
|
|
4949
|
+
if (this.showIcon) {
|
|
4950
|
+
this.addTriggerIcon();
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
removeTrigger() {
|
|
4954
|
+
const element = this.elementRef.nativeElement;
|
|
4955
|
+
element.removeEventListener('change', this.onFileChange.bind(this));
|
|
4956
|
+
if (this.triggerIcon) {
|
|
4957
|
+
this.triggerIcon.remove();
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
onFileChange(event) {
|
|
4961
|
+
const target = event.target;
|
|
4962
|
+
const files = target.files;
|
|
4963
|
+
if (files && files.length > 0) {
|
|
4964
|
+
console.log('📁 [FloatingUploadTrigger] Files selected:', files.length);
|
|
4965
|
+
// Show floating uploader
|
|
4966
|
+
this.floatingUploader.showUploader(this.groupId);
|
|
4967
|
+
// Handle files through floating uploader
|
|
4968
|
+
this.floatingUploader.handleExternalFiles(Array.from(files), this.userId, this.groupId);
|
|
4969
|
+
// Emit files selected event
|
|
4970
|
+
this.filesSelected.emit(Array.from(files));
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4973
|
+
addTriggerIcon() {
|
|
4974
|
+
const element = this.elementRef.nativeElement;
|
|
4975
|
+
const container = element.parentElement;
|
|
4976
|
+
if (!container)
|
|
4977
|
+
return;
|
|
4978
|
+
// Create trigger icon
|
|
4979
|
+
this.triggerIcon = document.createElement('button');
|
|
4980
|
+
this.triggerIcon.type = 'button';
|
|
4981
|
+
this.triggerIcon.className = 'floating-upload-trigger-icon';
|
|
4982
|
+
this.triggerIcon.innerHTML = `
|
|
4983
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
4984
|
+
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
|
|
4985
|
+
</svg>
|
|
4986
|
+
`;
|
|
4987
|
+
this.triggerIcon.title = 'View upload progress';
|
|
4988
|
+
this.triggerIcon.style.cssText = `
|
|
4989
|
+
position: absolute;
|
|
4990
|
+
right: 8px;
|
|
4991
|
+
top: 50%;
|
|
4992
|
+
transform: translateY(-50%);
|
|
4993
|
+
background: #3b82f6;
|
|
4994
|
+
color: white;
|
|
4995
|
+
border: none;
|
|
4996
|
+
border-radius: 4px;
|
|
4997
|
+
padding: 4px;
|
|
4998
|
+
cursor: pointer;
|
|
4999
|
+
display: flex;
|
|
5000
|
+
align-items: center;
|
|
5001
|
+
justify-content: center;
|
|
5002
|
+
z-index: 10;
|
|
5003
|
+
`;
|
|
5004
|
+
// Make container relative positioned
|
|
5005
|
+
container.style.position = 'relative';
|
|
5006
|
+
// Add click handler to show floating uploader
|
|
5007
|
+
this.triggerIcon.addEventListener('click', (e) => {
|
|
5008
|
+
e.preventDefault();
|
|
5009
|
+
e.stopPropagation();
|
|
5010
|
+
this.floatingUploader.showUploader(this.groupId);
|
|
5011
|
+
});
|
|
5012
|
+
// Insert icon after the input
|
|
5013
|
+
element.parentNode?.insertBefore(this.triggerIcon, element.nextSibling);
|
|
5014
|
+
}
|
|
5015
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideFloatingUploadTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
5016
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.1.7", type: CideFloatingUploadTriggerDirective, isStandalone: true, selector: "[cideFloatingUploadTrigger]", inputs: { groupId: "groupId", userId: "userId", showIcon: "showIcon" }, outputs: { filesSelected: "filesSelected" }, ngImport: i0 });
|
|
5017
|
+
}
|
|
5018
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideFloatingUploadTriggerDirective, decorators: [{
|
|
5019
|
+
type: Directive,
|
|
5020
|
+
args: [{
|
|
5021
|
+
selector: '[cideFloatingUploadTrigger]',
|
|
5022
|
+
standalone: true
|
|
5023
|
+
}]
|
|
5024
|
+
}], propDecorators: { groupId: [{
|
|
5025
|
+
type: Input
|
|
5026
|
+
}], userId: [{
|
|
5027
|
+
type: Input
|
|
5028
|
+
}], showIcon: [{
|
|
5029
|
+
type: Input
|
|
5030
|
+
}], filesSelected: [{
|
|
5031
|
+
type: Output
|
|
5032
|
+
}] } });
|
|
4827
5033
|
|
|
4828
5034
|
class CideTextareaComponent {
|
|
4829
5035
|
label = '';
|
|
@@ -9063,5 +9269,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
|
|
|
9063
9269
|
* Generated bundle index. Do not edit.
|
|
9064
9270
|
*/
|
|
9065
9271
|
|
|
9066
|
-
export { CideCoreFileManagerService, CideEleButtonComponent, CideEleConfirmationModalComponent, CideEleDataGridComponent, CideEleDropdownComponent, CideEleFileImageDirective, CideEleFileInputComponent, CideEleFileManagerService, CideEleFloatingFileUploaderComponent, CideEleGlobalFileUploaderComponent, CideEleGlobalNotificationsComponent, CideEleJsonEditorComponent, CideEleResizerDirective, CideEleSkeletonLoaderComponent, CideEleTabComponent, CideEleToastNotificationComponent, CideElementsService, CideIconComponent, CideInputComponent, CideSelectComponent, CideSelectOptionComponent, CideSpinnerComponent, CideTextareaComponent, ConfirmationService, CoreFileManagerInsertUpdatePayload, DEFAULT_GRID_CONFIG, DropdownManagerService, ICoreCyfmSave, MFileManager, NotificationService, TooltipDirective };
|
|
9272
|
+
export { CideCoreFileManagerService, CideEleButtonComponent, CideEleConfirmationModalComponent, CideEleDataGridComponent, CideEleDropdownComponent, CideEleFileImageDirective, CideEleFileInputComponent, CideEleFileManagerService, CideEleFloatingFileUploaderComponent, CideEleGlobalFileUploaderComponent, CideEleGlobalNotificationsComponent, CideEleJsonEditorComponent, CideEleResizerDirective, CideEleSkeletonLoaderComponent, CideEleTabComponent, CideEleToastNotificationComponent, CideElementsService, CideFloatingUploadTriggerDirective, CideIconComponent, CideInputComponent, CideSelectComponent, CideSelectOptionComponent, CideSpinnerComponent, CideTextareaComponent, ConfirmationService, CoreFileManagerInsertUpdatePayload, DEFAULT_GRID_CONFIG, DropdownManagerService, ICoreCyfmSave, MFileManager, NotificationService, TooltipDirective };
|
|
9067
9273
|
//# sourceMappingURL=cloud-ide-element.mjs.map
|