cloud-ide-element 1.0.80 → 1.0.82

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.
@@ -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, effect, Directive, ElementRef, viewChild } from '@angular/core';
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,1583 +3148,1644 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
3148
3148
  }]
3149
3149
  }], ctorParameters: () => [] });
3150
3150
 
3151
- class CideEleFileInputComponent {
3152
- fileManagerService = inject(CideEleFileManagerService);
3153
- notificationService = inject(NotificationService);
3154
- elementService = inject(CideElementsService);
3151
+ class CideEleFloatingFileUploaderComponent {
3155
3152
  destroyRef = inject(DestroyRef);
3156
- // private readonly pendingTasks = inject(PendingTasks); // TODO: Fix PendingTasks API usage
3157
- // Traditional @Input() decorators
3158
- label = 'Choose file';
3159
- accept = '';
3160
- multiple = false;
3161
- disabled = false;
3162
- required = false;
3163
- helperText = '';
3164
- errorText = '';
3165
- showPreview = false;
3166
- previewWidth = '200px';
3167
- previewHeight = '200px';
3168
- previewBoxMode = false;
3169
- showFileName = true;
3170
- placeholderText = 'Click to select image';
3171
- placeholderIcon = '📷';
3172
- autoUpload = false;
3173
- uploadData = {};
3174
- // Traditional @Output() decorators
3175
- fileChange = new EventEmitter();
3176
- uploadSuccess = new EventEmitter();
3177
- uploadError = new EventEmitter();
3178
- uploadProgressChange = new EventEmitter();
3179
- // Readable signals created from @Input() decorator values
3180
- labelSignal = signal(this.label, ...(ngDevMode ? [{ debugName: "labelSignal" }] : []));
3181
- acceptSignal = signal(this.accept, ...(ngDevMode ? [{ debugName: "acceptSignal" }] : []));
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
+ // Use file manager service's upload queue as the single source of truth
3160
+ uploadQueue = computed(() => this.fileManagerService.uploadQueue(), ...(ngDevMode ? [{ debugName: "uploadQueue" }] : []));
3161
+ activeUploads = computed(() => this.fileManagerService.activeUploads(), ...(ngDevMode ? [{ debugName: "activeUploads" }] : []));
3162
+ // Computed values based on file manager service state
3163
+ hasUploads = computed(() => this.uploadQueue().length > 0, ...(ngDevMode ? [{ debugName: "hasUploads" }] : []));
3164
+ pendingUploads = computed(() => this.uploadQueue().length > 0, ...(ngDevMode ? [{ debugName: "pendingUploads" }] : [])); // All files in queue are pending
3165
+ completedUploads = computed(() => {
3166
+ const active = this.activeUploads();
3167
+ return Array.from(active.values()).filter(upload => upload.stage === 'complete');
3168
+ }, ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
3169
+ failedUploads = computed(() => {
3170
+ const active = this.activeUploads();
3171
+ return Array.from(active.values()).filter(upload => upload.stage === 'error');
3172
+ }, ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
3173
+ // Animation states
3174
+ isAnimating = signal(false, ...(ngDevMode ? [{ debugName: "isAnimating" }] : []));
3235
3175
  constructor() {
3236
- // Angular 20: afterRenderEffect for DOM operations
3237
- afterRenderEffect(() => {
3238
- // Update file input element when files change
3239
- if (this.files()) {
3240
- const fileInput = document.getElementById('cide-file-input-' + this.id());
3241
- if (fileInput) {
3242
- // Ensure the input reflects the current state
3243
- fileInput.files = this.files();
3244
- }
3176
+ console.log('🚀 [FloatingFileUploader] Component initialized');
3177
+ // Set up effect to monitor file manager service's upload queue
3178
+ effect(() => {
3179
+ const uploadQueue = this.fileManagerService.uploadQueue();
3180
+ const activeUploads = this.fileManagerService.activeUploads();
3181
+ const hasUploads = uploadQueue.length > 0 || activeUploads.size > 0;
3182
+ console.log('🔄 [FloatingFileUploader] File manager service queue changed:', {
3183
+ queueLength: uploadQueue.length,
3184
+ activeUploadsCount: activeUploads.size,
3185
+ hasUploads,
3186
+ currentVisible: this.isVisible(),
3187
+ queueFiles: uploadQueue
3188
+ });
3189
+ // Show floating uploader when files are added to the file manager service queue
3190
+ // But don't auto-hide when uploads complete - let user manually close
3191
+ if (hasUploads && !this.isVisible()) {
3192
+ console.log('👁️ [FloatingFileUploader] Showing floating uploader due to file manager queue');
3193
+ this.showWithAnimation();
3245
3194
  }
3246
- });
3247
- // Angular 20: afterNextRender for one-time DOM operations
3248
- afterNextRender(() => {
3249
- console.log('🎯 [FileInput] Component rendered and DOM is ready');
3195
+ // Note: Removed auto-hide logic - floating uploader stays visible until manually closed
3250
3196
  });
3251
3197
  }
3252
3198
  ngOnInit() {
3253
- // Update signals with initial @Input() values
3254
- this.labelSignal.set(this.label);
3255
- this.acceptSignal.set(this.accept);
3256
- this.multipleSignal.set(this.multiple);
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);
3199
+ // Set up drag and drop listeners
3200
+ this.setupDragAndDrop();
3201
+ // Set up file input change listeners
3202
+ this.setupFileInputListeners();
3270
3203
  }
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);
3204
+ ngOnDestroy() {
3205
+ console.log('🧹 [FloatingFileUploader] Component destroyed');
3206
+ this.removeDragAndDropListeners();
3207
+ this.removeFileInputListeners();
3305
3208
  }
3306
- writeValue(value) {
3307
- console.log('📝 [FileInput] writeValue called with:', value);
3308
- if (typeof value === 'string') {
3309
- // Value is an uploaded file ID - fetch file details and set preview
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
- }
3209
+ /**
3210
+ * Set up drag and drop functionality
3211
+ */
3212
+ setupDragAndDrop() {
3213
+ document.addEventListener('dragover', this.handleDragOver.bind(this));
3214
+ document.addEventListener('dragleave', this.handleDragLeave.bind(this));
3215
+ document.addEventListener('drop', this.handleDrop.bind(this));
3331
3216
  }
3332
- registerOnChange(fn) {
3333
- this.onChange = fn;
3217
+ /**
3218
+ * Remove drag and drop listeners
3219
+ */
3220
+ removeDragAndDropListeners() {
3221
+ document.removeEventListener('dragover', this.handleDragOver.bind(this));
3222
+ document.removeEventListener('dragleave', this.handleDragLeave.bind(this));
3223
+ document.removeEventListener('drop', this.handleDrop.bind(this));
3334
3224
  }
3335
- registerOnTouched(fn) {
3336
- this.onTouched = fn;
3225
+ /**
3226
+ * Set up file input change listeners
3227
+ */
3228
+ setupFileInputListeners() {
3229
+ // Listen for file input change events globally
3230
+ document.addEventListener('change', this.handleFileInputChange.bind(this));
3337
3231
  }
3338
- registerOnValidatorChange(fn) {
3339
- this.onValidatorChange = fn;
3232
+ /**
3233
+ * Remove file input listeners
3234
+ */
3235
+ removeFileInputListeners() {
3236
+ document.removeEventListener('change', this.handleFileInputChange.bind(this));
3340
3237
  }
3341
- setDisabledState(isDisabled) {
3342
- // Note: With input signals, disabled state is controlled by the parent component
3343
- // This method is kept for ControlValueAccessor compatibility but doesn't modify the signal
3344
- console.log('🔧 [FileInput] setDisabledState called with:', isDisabled, '(controlled by parent component)');
3345
- }
3346
- onFileSelected(event) {
3347
- console.log('🔍 [FileInput] onFileSelected called');
3348
- const input = event.target;
3349
- const selectedFiles = input.files;
3350
- this.files.set(selectedFiles);
3351
- this.fileNames.set(selectedFiles ? Array.from(selectedFiles).map(f => f.name) : []);
3352
- console.log('📁 [FileInput] Files selected:', this.fileNames());
3353
- this.generatePreviews();
3354
- // Reset upload status when new file is selected
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
- }
3238
+ /**
3239
+ * Handle file input change events
3240
+ */
3241
+ handleFileInputChange(event) {
3242
+ const target = event.target;
3243
+ console.log('🔍 [FloatingFileUploader] File input change event detected:', {
3244
+ type: target.type,
3245
+ filesLength: target.files?.length || 0,
3246
+ element: target
3247
+ });
3248
+ // Check if this is a file input with files
3249
+ if (target.type === 'file' && target.files && target.files.length > 0) {
3250
+ console.log('📁 [FloatingFileUploader] File input change detected:', target.files.length, 'files');
3251
+ // Check if the input has a data-user-id attribute for user context
3252
+ const userId = target.getAttribute('data-user-id');
3253
+ if (userId && userId !== this.currentUserId()) {
3254
+ this.setCurrentUserId(userId);
3255
+ }
3256
+ // Handle the files
3257
+ this.handleFiles(Array.from(target.files));
3258
+ // Reset the input to allow selecting the same files again
3259
+ target.value = '';
3260
+ }
3558
3261
  }
3559
3262
  /**
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
3263
+ * Handle drag over event
3562
3264
  */
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
- }
3717
- });
3718
- }
3719
- isImageFileFromName(fileName) {
3720
- if (!fileName)
3721
- return false;
3722
- const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
3723
- const lowerFileName = fileName.toLowerCase();
3724
- return imageExtensions.some(ext => lowerFileName.endsWith(ext));
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]);
3748
- }
3749
- this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
3750
- this.onChange(newFiles);
3751
- this.fileChange.emit(newFiles);
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();
3778
- }
3779
- }
3780
- // Drag and Drop Event Handlers
3781
- onDragOver(event) {
3782
- event.preventDefault();
3783
- event.stopPropagation();
3784
- if (!this.disabledSignal()) {
3785
- this.isDragOver.set(true);
3786
- console.log('🔄 [FileInput] Drag over detected');
3787
- }
3788
- }
3789
- onDragLeave(event) {
3790
- event.preventDefault();
3791
- event.stopPropagation();
3792
- this.isDragOver.set(false);
3793
- console.log('🔄 [FileInput] Drag leave detected');
3794
- }
3795
- onDragEnter(event) {
3796
- event.preventDefault();
3797
- event.stopPropagation();
3798
- if (!this.disabledSignal()) {
3799
- this.isDragOver.set(true);
3800
- console.log('🔄 [FileInput] Drag enter detected');
3801
- }
3802
- }
3803
- onDrop(event) {
3265
+ handleDragOver(event) {
3804
3266
  event.preventDefault();
3805
- event.stopPropagation();
3806
- this.isDragOver.set(false);
3807
- if (this.disabledSignal()) {
3808
- console.log('⏸️ [FileInput] Drop ignored - component is disabled');
3809
- return;
3810
- }
3811
- const files = event.dataTransfer?.files;
3812
- if (files && files.length > 0) {
3813
- console.log('📁 [FileInput] Files dropped:', Array.from(files).map(f => f.name));
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
- }
3832
- }
3833
- }
3834
- validateFileTypes(files) {
3835
- const acceptTypes = this.acceptSignal().split(',').map(type => type.trim());
3836
- if (acceptTypes.length === 0 || acceptTypes[0] === '')
3837
- return true;
3838
- return Array.from(files).every(file => {
3839
- return acceptTypes.some(acceptType => {
3840
- if (acceptType.startsWith('.')) {
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
- });
3851
- }
3852
- handleFileSelection(files) {
3853
- this.files.set(files);
3854
- this.fileNames.set(Array.from(files).map(f => f.name));
3855
- console.log('📁 [FileInput] Files selected via drag & drop:', this.fileNames());
3856
- this.generatePreviews();
3857
- // Reset upload status when new file is selected
3858
- this.uploadStatus.set('idle');
3859
- console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
3860
- this.onChange(files);
3861
- this.fileChange.emit(files);
3862
- this.onTouched();
3863
- // Auto upload if enabled
3864
- if (this.autoUploadSignal() && files.length > 0) {
3865
- if (this.multipleSignal()) {
3866
- console.log('🚀 [FileInput] Auto upload enabled for multiple files mode (drag & drop):', files.length, 'files');
3867
- this.uploadMultipleFiles(Array.from(files));
3868
- }
3869
- else {
3870
- console.log('🚀 [FileInput] Auto upload enabled for single file mode (drag & drop):', files[0].name);
3871
- this.uploadFile(files[0]);
3872
- }
3267
+ event.stopPropagation();
3268
+ // Show floating uploader when files are dragged over
3269
+ if (event.dataTransfer?.types.includes('Files')) {
3270
+ this.showWithAnimation();
3873
3271
  }
3874
- else {
3875
- console.log('⏸️ [FileInput] Auto upload disabled or no files');
3272
+ }
3273
+ /**
3274
+ * Handle drag leave event
3275
+ */
3276
+ handleDragLeave(event) {
3277
+ event.preventDefault();
3278
+ event.stopPropagation();
3279
+ // Only hide if leaving the entire document
3280
+ if (!event.relatedTarget || event.relatedTarget === document.body) {
3281
+ this.updateVisibility();
3876
3282
  }
3877
3283
  }
3878
- isRequired() {
3879
- return this.requiredSignal();
3284
+ /**
3285
+ * Handle drop event
3286
+ */
3287
+ handleDrop(event) {
3288
+ event.preventDefault();
3289
+ event.stopPropagation();
3290
+ const files = event.dataTransfer?.files;
3291
+ if (files && files.length > 0) {
3292
+ this.handleFiles(Array.from(files));
3293
+ }
3880
3294
  }
3881
3295
  /**
3882
- * Angular 20: Utility method to get upload data with proper typing
3883
- * @returns Properly typed upload data
3296
+ * Handle files from drag and drop or file input
3884
3297
  */
3885
- getUploadData() {
3886
- return this.uploadDataSignal();
3298
+ handleFiles(files) {
3299
+ console.log('📁 [FloatingFileUploader] Handling files:', files.length);
3300
+ // Use handleExternalFiles to process the files
3301
+ this.handleExternalFiles(files, this.currentUserId());
3887
3302
  }
3888
3303
  /**
3889
- * Angular 20: Utility method to update upload data with type safety
3890
- * @param data Partial upload data to merge with existing data
3304
+ * Update visibility - simplified for notification only
3891
3305
  */
3892
- updateUploadData(data) {
3893
- const currentData = this.uploadDataSignal();
3894
- const updatedData = { ...currentData, ...data };
3895
- // Note: This would require the uploadData to be a writable signal
3896
- // For now, this method serves as a type-safe way to work with upload data
3897
- console.log('📝 [FileInput] Upload data updated:', updatedData);
3306
+ updateVisibility() {
3307
+ // This is just a notification component now
3308
+ // The actual uploads are handled by the global uploader
3898
3309
  }
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
- };
3310
+ /**
3311
+ * Show with animation
3312
+ */
3313
+ showWithAnimation() {
3314
+ console.log('🎬 [FloatingFileUploader] showWithAnimation called - setting isVisible to true');
3315
+ this.isAnimating.set(true);
3316
+ this.isVisible.set(true);
3317
+ // Remove animation class after animation completes
3318
+ setTimeout(() => {
3319
+ this.isAnimating.set(false);
3320
+ console.log('🎬 [FloatingFileUploader] Animation completed, isVisible:', this.isVisible());
3321
+ }, 300);
3925
3322
  }
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();
3323
+ /**
3324
+ * Hide with animation
3325
+ */
3326
+ hideWithAnimation() {
3327
+ this.isAnimating.set(true);
3328
+ // Wait for animation to complete before hiding
3329
+ setTimeout(() => {
3330
+ this.isVisible.set(false);
3331
+ this.isAnimating.set(false);
3332
+ }, 300);
3333
+ }
3334
+ /**
3335
+ * Toggle minimize state
3336
+ */
3337
+ toggleMinimize() {
3338
+ this.isMinimized.set(!this.isMinimized());
3339
+ }
3340
+ /**
3341
+ * Close the floating uploader
3342
+ */
3343
+ close() {
3344
+ this.hideWithAnimation();
3345
+ }
3346
+ /**
3347
+ * Get upload summary text
3348
+ */
3349
+ getUploadSummary() {
3350
+ const queueLength = this.uploadQueue().length;
3351
+ const activeCount = this.activeUploads().size;
3352
+ const completed = this.completedUploads().length;
3353
+ const failed = this.failedUploads().length;
3354
+ if (activeCount > 0) {
3355
+ return `${activeCount} uploading`;
3938
3356
  }
3939
- else {
3940
- console.log('⚠️ [FileInput] No element data found for key:', this.id());
3357
+ else if (completed > 0 && failed === 0) {
3358
+ return `${completed} completed`;
3941
3359
  }
3942
- }
3943
- // Validator implementation
3944
- validate(control) {
3945
- console.log('🔍 [FileInput] validate() called - uploadStatus:', this.uploadStatus(), 'required:', this.requiredSignal(), 'files:', !!this.files(), 'control.value:', control.value);
3946
- // If upload is in progress (start or uploading status), return validation error
3947
- if (this.uploadStatus() === 'start' || this.uploadStatus() === 'uploading') {
3948
- console.log('⚠️ [FileInput] Validation ERROR: Upload in progress');
3949
- return { 'uploadInProgress': { message: 'File upload in progress. Please wait...' } };
3360
+ else if (failed > 0) {
3361
+ return `${completed} completed, ${failed} failed`;
3950
3362
  }
3951
- // If required and no file is selected and no control value (uploaded file ID), return validation error
3952
- if (this.requiredSignal() && !this.files() && !control.value) {
3953
- console.log('⚠️ [FileInput] Validation ERROR: File required');
3954
- return { 'required': { message: 'Please select a file to upload.' } };
3363
+ else if (queueLength > 0) {
3364
+ return `${queueLength} pending`;
3955
3365
  }
3956
- console.log('✅ [FileInput] Validation PASSED: No errors');
3957
- return null; // No validation errors
3366
+ return 'No uploads';
3958
3367
  }
3959
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3960
- 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" }, outputs: { fileChange: "fileChange", uploadSuccess: "uploadSuccess", uploadError: "uploadError", uploadProgressChange: "uploadProgressChange" }, providers: [
3961
- {
3962
- provide: NG_VALUE_ACCESSOR,
3963
- useExisting: CideEleFileInputComponent,
3964
- multi: true
3965
- },
3966
- {
3967
- provide: NG_VALIDATORS,
3968
- useExisting: CideEleFileInputComponent,
3969
- multi: true
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');
3368
+ /**
3369
+ * Get overall progress percentage
3370
+ */
3371
+ getOverallProgress() {
3372
+ const activeUploads = this.activeUploads();
3373
+ if (activeUploads.size === 0)
3374
+ return 0;
3375
+ const totalProgress = Array.from(activeUploads.values()).reduce((sum, upload) => sum + upload.percentage, 0);
3376
+ return Math.round(totalProgress / activeUploads.size);
3377
+ }
3378
+ /**
3379
+ * Get status icon based on upload stage
3380
+ */
3381
+ getStatusIcon(stage) {
3382
+ switch (stage) {
3383
+ case 'reading': return 'schedule';
3384
+ case 'uploading': return 'cloud_upload';
3385
+ case 'complete': return 'check_circle';
3386
+ case 'error': return 'error';
3387
+ default: return 'help';
3388
+ }
4057
3389
  }
4058
3390
  /**
4059
- * Handle drag over event
3391
+ * Get status class based on upload stage
4060
3392
  */
4061
- onDragOver(event) {
4062
- event.preventDefault();
4063
- event.stopPropagation();
4064
- this.isDragOver.set(true);
3393
+ getStatusClass(stage) {
3394
+ switch (stage) {
3395
+ case 'reading': return 'status-pending';
3396
+ case 'uploading': return 'status-uploading';
3397
+ case 'complete': return 'status-completed';
3398
+ case 'error': return 'status-error';
3399
+ default: return 'status-unknown';
3400
+ }
4065
3401
  }
4066
3402
  /**
4067
- * Handle drag leave event
3403
+ * Cancel upload
4068
3404
  */
4069
- onDragLeave(event) {
4070
- event.preventDefault();
4071
- event.stopPropagation();
4072
- this.isDragOver.set(false);
3405
+ cancelUpload(fileId) {
3406
+ console.log('🚫 [FloatingFileUploader] Cancelling upload:', fileId);
3407
+ this.fileManagerService.cancelUpload(fileId);
4073
3408
  }
4074
3409
  /**
4075
- * Handle drop event
3410
+ * Get file name from file ID (extract from the ID format)
4076
3411
  */
4077
- onDrop(event) {
4078
- event.preventDefault();
4079
- event.stopPropagation();
4080
- this.isDragOver.set(false);
4081
- const files = event.dataTransfer?.files;
4082
- if (files && files.length > 0) {
4083
- this.addFilesToQueue(Array.from(files));
3412
+ getFileNameFromId(fileId) {
3413
+ // Extract filename from the fileId format: filename_size_timestamp
3414
+ const parts = fileId.split('_');
3415
+ if (parts.length >= 3) {
3416
+ // Remove the last two parts (size and timestamp) to get the filename
3417
+ return parts.slice(0, -2).join('_');
4084
3418
  }
3419
+ return fileId;
4085
3420
  }
4086
3421
  /**
4087
- * Handle file input change
3422
+ * Set current user ID
4088
3423
  */
4089
- onFileInputChange(event) {
4090
- const target = event.target;
4091
- const files = target.files;
4092
- if (files && files.length > 0) {
4093
- this.addFilesToQueue(Array.from(files));
3424
+ setCurrentUserId(userId) {
3425
+ this.currentUserId.set(userId);
3426
+ this.fileManagerService.setUserId(userId);
3427
+ }
3428
+ /**
3429
+ * Public method to handle files from external sources
3430
+ * This can be called by other components to trigger the floating uploader
3431
+ */
3432
+ handleExternalFiles(files, userId, groupId) {
3433
+ console.log('📁 [FloatingFileUploader] External files received:', files.length, 'files');
3434
+ // Set user ID if provided
3435
+ if (userId && userId !== this.currentUserId()) {
3436
+ this.setCurrentUserId(userId);
4094
3437
  }
4095
- // Reset input value to allow selecting the same file again
4096
- target.value = '';
3438
+ // Set group ID if provided
3439
+ if (groupId) {
3440
+ this.currentGroupId.set(groupId);
3441
+ }
3442
+ // Upload files using file manager service
3443
+ // The file manager service will handle adding to its queue and the effect will show the floating uploader
3444
+ files.forEach((file, index) => {
3445
+ console.log(`📁 [FloatingFileUploader] Starting upload for file ${index + 1}/${files.length}:`, file.name);
3446
+ this.fileManagerService.uploadFile(file, {
3447
+ userId: this.currentUserId(),
3448
+ groupId: groupId,
3449
+ permissions: ['read', 'write'],
3450
+ tags: []
3451
+ })
3452
+ .pipe(takeUntilDestroyed(this.destroyRef))
3453
+ .subscribe({
3454
+ next: (response) => {
3455
+ console.log('✅ [FloatingFileUploader] Upload completed:', response);
3456
+ },
3457
+ error: (error) => {
3458
+ console.error('❌ [FloatingFileUploader] Upload failed:', error);
3459
+ }
3460
+ });
3461
+ });
4097
3462
  }
4098
3463
  /**
4099
- * Add files to upload queue
3464
+ * Manually show the floating uploader
3465
+ * This can be called from file input components or other triggers
4100
3466
  */
4101
- addFilesToQueue(files) {
4102
- // Validate files
4103
- const validFiles = files.filter(file => this.validateFile(file));
4104
- if (validFiles.length === 0) {
4105
- console.warn('⚠️ [GlobalFileUploader] No valid files to upload');
4106
- return;
3467
+ showUploader(groupId) {
3468
+ console.log('👁️ [FloatingFileUploader] Manually showing uploader', groupId ? `for group: ${groupId}` : '');
3469
+ if (groupId) {
3470
+ this.currentGroupId.set(groupId);
4107
3471
  }
4108
- const newUploads = validFiles.map(file => ({
4109
- fileId: this.generateFileId(file),
4110
- fileName: file.name,
4111
- progress: 0,
4112
- status: 'pending',
4113
- file: file
4114
- }));
4115
- this.uploadQueue.update(queue => [...queue, ...newUploads]);
4116
- this.processUploadQueue();
3472
+ this.showWithAnimation();
4117
3473
  }
4118
3474
  /**
4119
- * Validate file
3475
+ * Check if there are any uploads for the current group
4120
3476
  */
4121
- validateFile(file) {
4122
- // Check file size
4123
- const maxSizeBytes = this.maxFileSize * 1024 * 1024;
4124
- if (file.size > maxSizeBytes) {
4125
- console.warn(`⚠️ [GlobalFileUploader] File ${file.name} exceeds size limit`);
4126
- return false;
3477
+ hasUploadsForCurrentGroup() {
3478
+ const groupId = this.currentGroupId();
3479
+ if (!groupId) {
3480
+ // If no group filter, show all uploads
3481
+ return this.hasUploads();
3482
+ }
3483
+ // Check if any uploads belong to the current group
3484
+ // Note: This would need to be enhanced based on how group IDs are stored in the file manager service
3485
+ return this.hasUploads();
3486
+ }
3487
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3488
+ 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 } @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" }] });
3489
+ }
3490
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, decorators: [{
3491
+ type: Component,
3492
+ args: [{ selector: 'cide-ele-floating-file-uploader', standalone: true, imports: [
3493
+ CommonModule,
3494
+ CideIconComponent
3495
+ ], 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 } @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"] }]
3496
+ }], ctorParameters: () => [] });
3497
+
3498
+ class CideEleFileInputComponent {
3499
+ fileManagerService = inject(CideEleFileManagerService);
3500
+ notificationService = inject(NotificationService);
3501
+ elementService = inject(CideElementsService);
3502
+ destroyRef = inject(DestroyRef);
3503
+ floatingUploader = inject(CideEleFloatingFileUploaderComponent, { optional: true });
3504
+ // private readonly pendingTasks = inject(PendingTasks); // TODO: Fix PendingTasks API usage
3505
+ // Traditional @Input() decorators
3506
+ label = 'Choose file';
3507
+ accept = '';
3508
+ multiple = false;
3509
+ disabled = false;
3510
+ required = false;
3511
+ helperText = '';
3512
+ errorText = '';
3513
+ showPreview = false;
3514
+ previewWidth = '200px';
3515
+ previewHeight = '200px';
3516
+ previewBoxMode = false;
3517
+ showFileName = true;
3518
+ placeholderText = 'Click to select image';
3519
+ placeholderIcon = '📷';
3520
+ autoUpload = false;
3521
+ uploadData = {};
3522
+ showFloatingUploader = true;
3523
+ floatingUploaderGroupId;
3524
+ // Traditional @Output() decorators
3525
+ fileChange = new EventEmitter();
3526
+ uploadSuccess = new EventEmitter();
3527
+ uploadError = new EventEmitter();
3528
+ uploadProgressChange = new EventEmitter();
3529
+ // Readable signals created from @Input() decorator values
3530
+ labelSignal = signal(this.label, ...(ngDevMode ? [{ debugName: "labelSignal" }] : []));
3531
+ acceptSignal = signal(this.accept, ...(ngDevMode ? [{ debugName: "acceptSignal" }] : []));
3532
+ multipleSignal = signal(this.multiple, ...(ngDevMode ? [{ debugName: "multipleSignal" }] : []));
3533
+ disabledSignal = signal(this.disabled, ...(ngDevMode ? [{ debugName: "disabledSignal" }] : []));
3534
+ requiredSignal = signal(this.required, ...(ngDevMode ? [{ debugName: "requiredSignal" }] : []));
3535
+ helperTextSignal = signal(this.helperText, ...(ngDevMode ? [{ debugName: "helperTextSignal" }] : []));
3536
+ errorTextSignal = signal(this.errorText, ...(ngDevMode ? [{ debugName: "errorTextSignal" }] : []));
3537
+ showPreviewSignal = signal(this.showPreview, ...(ngDevMode ? [{ debugName: "showPreviewSignal" }] : []));
3538
+ previewWidthSignal = signal(this.previewWidth, ...(ngDevMode ? [{ debugName: "previewWidthSignal" }] : []));
3539
+ previewHeightSignal = signal(this.previewHeight, ...(ngDevMode ? [{ debugName: "previewHeightSignal" }] : []));
3540
+ previewBoxModeSignal = signal(this.previewBoxMode, ...(ngDevMode ? [{ debugName: "previewBoxModeSignal" }] : []));
3541
+ showFileNameSignal = signal(this.showFileName, ...(ngDevMode ? [{ debugName: "showFileNameSignal" }] : []));
3542
+ placeholderTextSignal = signal(this.placeholderText, ...(ngDevMode ? [{ debugName: "placeholderTextSignal" }] : []));
3543
+ placeholderIconSignal = signal(this.placeholderIcon, ...(ngDevMode ? [{ debugName: "placeholderIconSignal" }] : []));
3544
+ autoUploadSignal = signal(this.autoUpload, ...(ngDevMode ? [{ debugName: "autoUploadSignal" }] : []));
3545
+ uploadDataSignal = signal(this.uploadData, ...(ngDevMode ? [{ debugName: "uploadDataSignal" }] : []));
3546
+ showFloatingUploaderSignal = signal(this.showFloatingUploader, ...(ngDevMode ? [{ debugName: "showFloatingUploaderSignal" }] : []));
3547
+ floatingUploaderGroupIdSignal = signal(this.floatingUploaderGroupId, ...(ngDevMode ? [{ debugName: "floatingUploaderGroupIdSignal" }] : []));
3548
+ // Reactive state with signals
3549
+ id = signal(Math.random().toString(36).substring(2, 10), ...(ngDevMode ? [{ debugName: "id" }] : []));
3550
+ isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
3551
+ uploadProgress = signal(0, ...(ngDevMode ? [{ debugName: "uploadProgress" }] : []));
3552
+ uploadStatus = signal('idle', ...(ngDevMode ? [{ debugName: "uploadStatus" }] : []));
3553
+ files = signal(null, ...(ngDevMode ? [{ debugName: "files" }] : []));
3554
+ fileNames = signal([], ...(ngDevMode ? [{ debugName: "fileNames" }] : []));
3555
+ previewUrls = signal([], ...(ngDevMode ? [{ debugName: "previewUrls" }] : []));
3556
+ uploadNotificationId = signal(null, ...(ngDevMode ? [{ debugName: "uploadNotificationId" }] : []));
3557
+ isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
3558
+ groupId = signal(null, ...(ngDevMode ? [{ debugName: "groupId" }] : [])); // Group ID for multiple file uploads
3559
+ isMultipleUploadMode = signal(false, ...(ngDevMode ? [{ debugName: "isMultipleUploadMode" }] : [])); // Flag to track if we're in multiple upload mode
3560
+ // Computed signals for better relationships
3561
+ hasFiles = computed(() => this.files() !== null && this.files().length > 0, ...(ngDevMode ? [{ debugName: "hasFiles" }] : []));
3562
+ canUpload = computed(() => this.hasFiles() && !this.isUploading() && !this.disabledSignal(), ...(ngDevMode ? [{ debugName: "canUpload" }] : []));
3563
+ isInErrorState = computed(() => this.uploadStatus() === 'error', ...(ngDevMode ? [{ debugName: "isInErrorState" }] : []));
3564
+ isInSuccessState = computed(() => this.uploadStatus() === 'success', ...(ngDevMode ? [{ debugName: "isInSuccessState" }] : []));
3565
+ // Angular 20: Computed values using new features
3566
+ totalFileSize = computed(() => {
3567
+ if (!this.files())
3568
+ return 0;
3569
+ return Array.from(this.files()).reduce((total, file) => total + file.size, 0);
3570
+ }, ...(ngDevMode ? [{ debugName: "totalFileSize" }] : []));
3571
+ fileSizeInMB = computed(() => {
3572
+ // Angular 20: Using ** operator for exponentiation
3573
+ return (this.totalFileSize() / (1024 ** 2)).toFixed(2);
3574
+ }, ...(ngDevMode ? [{ debugName: "fileSizeInMB" }] : []));
3575
+ uploadProgressPercentage = computed(() => {
3576
+ // Angular 20: Using ** operator for exponentiation
3577
+ return Math.round(this.uploadProgress() ** 1); // Simple power operation
3578
+ }, ...(ngDevMode ? [{ debugName: "uploadProgressPercentage" }] : []));
3579
+ // ControlValueAccessor callbacks
3580
+ onChange = (value) => { };
3581
+ onTouched = () => { };
3582
+ onValidatorChange = () => { };
3583
+ // Computed values
3584
+ hasImages = computed(() => this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "hasImages" }] : []));
3585
+ isPreviewBoxMode = computed(() => this.previewBoxModeSignal() && this.showPreviewSignal(), ...(ngDevMode ? [{ debugName: "isPreviewBoxMode" }] : []));
3586
+ isImagePreviewAvailable = computed(() => this.showPreviewSignal() && this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "isImagePreviewAvailable" }] : []));
3587
+ constructor() {
3588
+ // Angular 20: afterRenderEffect for DOM operations
3589
+ afterRenderEffect(() => {
3590
+ // Update file input element when files change
3591
+ if (this.files()) {
3592
+ const fileInput = document.getElementById('cide-file-input-' + this.id());
3593
+ if (fileInput) {
3594
+ // Ensure the input reflects the current state
3595
+ fileInput.files = this.files();
3596
+ }
3597
+ }
3598
+ });
3599
+ // Angular 20: afterNextRender for one-time DOM operations
3600
+ afterNextRender(() => {
3601
+ console.log('🎯 [FileInput] Component rendered and DOM is ready');
3602
+ });
3603
+ }
3604
+ ngOnInit() {
3605
+ // Update signals with initial @Input() values
3606
+ this.labelSignal.set(this.label);
3607
+ this.acceptSignal.set(this.accept);
3608
+ this.multipleSignal.set(this.multiple);
3609
+ this.disabledSignal.set(this.disabled);
3610
+ this.requiredSignal.set(this.required);
3611
+ this.helperTextSignal.set(this.helperText);
3612
+ this.errorTextSignal.set(this.errorText);
3613
+ this.showPreviewSignal.set(this.showPreview);
3614
+ this.previewWidthSignal.set(this.previewWidth);
3615
+ this.previewHeightSignal.set(this.previewHeight);
3616
+ this.previewBoxModeSignal.set(this.previewBoxMode);
3617
+ this.showFileNameSignal.set(this.showFileName);
3618
+ this.placeholderTextSignal.set(this.placeholderText);
3619
+ this.placeholderIconSignal.set(this.placeholderIcon);
3620
+ this.autoUploadSignal.set(this.autoUpload);
3621
+ this.uploadDataSignal.set(this.uploadData);
3622
+ }
3623
+ ngOnChanges(changes) {
3624
+ // Angular 20: Update signals when @Input() values change
3625
+ if (changes['label'])
3626
+ this.labelSignal.set(this.label);
3627
+ if (changes['accept'])
3628
+ this.acceptSignal.set(this.accept);
3629
+ if (changes['multiple'])
3630
+ this.multipleSignal.set(this.multiple);
3631
+ if (changes['disabled'])
3632
+ this.disabledSignal.set(this.disabled);
3633
+ if (changes['required'])
3634
+ this.requiredSignal.set(this.required);
3635
+ if (changes['helperText'])
3636
+ this.helperTextSignal.set(this.helperText);
3637
+ if (changes['errorText'])
3638
+ this.errorTextSignal.set(this.errorText);
3639
+ if (changes['showPreview'])
3640
+ this.showPreviewSignal.set(this.showPreview);
3641
+ if (changes['previewWidth'])
3642
+ this.previewWidthSignal.set(this.previewWidth);
3643
+ if (changes['previewHeight'])
3644
+ this.previewHeightSignal.set(this.previewHeight);
3645
+ if (changes['previewBoxMode'])
3646
+ this.previewBoxModeSignal.set(this.previewBoxMode);
3647
+ if (changes['showFileName'])
3648
+ this.showFileNameSignal.set(this.showFileName);
3649
+ if (changes['placeholderText'])
3650
+ this.placeholderTextSignal.set(this.placeholderText);
3651
+ if (changes['placeholderIcon'])
3652
+ this.placeholderIconSignal.set(this.placeholderIcon);
3653
+ if (changes['autoUpload'])
3654
+ this.autoUploadSignal.set(this.autoUpload);
3655
+ if (changes['uploadData'])
3656
+ this.uploadDataSignal.set(this.uploadData);
3657
+ }
3658
+ writeValue(value) {
3659
+ console.log('📝 [FileInput] writeValue called with:', value);
3660
+ if (typeof value === 'string') {
3661
+ // Value is an uploaded file ID - fetch file details and set preview
3662
+ console.log('📝 [FileInput] Value is uploaded file ID:', value);
3663
+ this.files.set(null);
3664
+ this.fileNames.set([]);
3665
+ this.clearPreviews();
3666
+ // Fetch file details to get base64 and set preview
3667
+ this.loadFileDetailsFromId(value);
4127
3668
  }
4128
- // Check file type
4129
- if (this.allowedTypes.length > 0 && !this.allowedTypes.includes(file.type)) {
4130
- console.warn(`⚠️ [GlobalFileUploader] File type ${file.type} not allowed`);
4131
- return false;
3669
+ else if (value instanceof FileList) {
3670
+ // Value is a FileList
3671
+ console.log('📝 [FileInput] Value is FileList:', Array.from(value).map(f => f.name));
3672
+ this.files.set(value);
3673
+ this.fileNames.set(Array.from(value).map(f => f.name));
3674
+ this.generatePreviews();
3675
+ }
3676
+ else {
3677
+ // Value is null
3678
+ console.log('📝 [FileInput] Value is null');
3679
+ this.files.set(null);
3680
+ this.fileNames.set([]);
3681
+ this.clearPreviews();
4132
3682
  }
4133
- return true;
4134
3683
  }
4135
- /**
4136
- * Process upload queue
4137
- */
4138
- processUploadQueue() {
4139
- const pendingUploads = this.uploadQueue().filter(upload => upload.status === 'pending');
4140
- if (pendingUploads.length > 0 && !this.isUploading()) {
4141
- this.isUploading.set(true);
4142
- if (this.multiple && pendingUploads.length > 1) {
4143
- // Multiple file upload with group ID
4144
- this.uploadMultipleFiles(pendingUploads);
3684
+ registerOnChange(fn) {
3685
+ this.onChange = fn;
3686
+ }
3687
+ registerOnTouched(fn) {
3688
+ this.onTouched = fn;
3689
+ }
3690
+ registerOnValidatorChange(fn) {
3691
+ this.onValidatorChange = fn;
3692
+ }
3693
+ setDisabledState(isDisabled) {
3694
+ // Note: With input signals, disabled state is controlled by the parent component
3695
+ // This method is kept for ControlValueAccessor compatibility but doesn't modify the signal
3696
+ console.log('🔧 [FileInput] setDisabledState called with:', isDisabled, '(controlled by parent component)');
3697
+ }
3698
+ onFileSelected(event) {
3699
+ console.log('🔍 [FileInput] onFileSelected called');
3700
+ const input = event.target;
3701
+ const selectedFiles = input.files;
3702
+ this.files.set(selectedFiles);
3703
+ this.fileNames.set(selectedFiles ? Array.from(selectedFiles).map(f => f.name) : []);
3704
+ console.log('📁 [FileInput] Files selected:', this.fileNames());
3705
+ this.generatePreviews();
3706
+ // Reset upload status when new file is selected
3707
+ this.uploadStatus.set('idle');
3708
+ console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
3709
+ this.onChange(selectedFiles);
3710
+ this.fileChange.emit(selectedFiles);
3711
+ this.onTouched();
3712
+ // Show floating uploader if enabled and files are selected
3713
+ if (this.showFloatingUploaderSignal() && selectedFiles && selectedFiles.length > 0 && this.floatingUploader) {
3714
+ console.log('👁️ [FileInput] Showing floating uploader for', selectedFiles.length, 'files');
3715
+ this.floatingUploader.showUploader(this.floatingUploaderGroupIdSignal());
3716
+ }
3717
+ // Auto upload if enabled
3718
+ if (this.autoUploadSignal() && selectedFiles && selectedFiles.length > 0) {
3719
+ if (this.multipleSignal()) {
3720
+ console.log('🚀 [FileInput] Auto upload enabled for multiple files mode:', selectedFiles.length, 'files');
3721
+ this.uploadMultipleFiles(Array.from(selectedFiles));
4145
3722
  }
4146
3723
  else {
4147
- // Single file upload
4148
- this.uploadNextFile();
3724
+ console.log('🚀 [FileInput] Auto upload enabled for single file mode:', selectedFiles[0].name);
3725
+ this.uploadFile(selectedFiles[0]);
4149
3726
  }
4150
3727
  }
3728
+ else {
3729
+ console.log('⏸️ [FileInput] Auto upload disabled or no files');
3730
+ }
4151
3731
  }
4152
- /**
4153
- * Upload multiple files with group ID
4154
- */
4155
- uploadMultipleFiles(uploads) {
4156
- console.log('📤 [GlobalFileUploader] Starting multiple file upload for:', uploads.length, 'files');
4157
- // Generate group ID first
4158
- this.fileService.generateObjectId()
4159
- .pipe(takeUntilDestroyed(this.destroyRef))
4160
- .subscribe({
3732
+ clearFiles() {
3733
+ console.log('🗑️ [FileInput] clearFiles called');
3734
+ this.files.set(null);
3735
+ this.fileNames.set([]);
3736
+ this.clearPreviews();
3737
+ this.uploadStatus.set('idle');
3738
+ console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
3739
+ this.onChange(null);
3740
+ this.fileChange.emit(null);
3741
+ }
3742
+ uploadFile(file) {
3743
+ console.log('📤 [FileInput] uploadFile called for:', file.name, 'Size:', file.size, 'bytes');
3744
+ // Angular 20: Use PendingTasks for better loading state management
3745
+ // const uploadTask = this.pendingTasks.add(); // TODO: Fix PendingTasks API usage
3746
+ // console.log('⏳ [FileInput] Pending task added for upload tracking');
3747
+ // Set upload status to 'start' before starting upload
3748
+ this.uploadStatus.set('start');
3749
+ console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3750
+ this.isUploading.set(true);
3751
+ this.uploadProgress.set(0);
3752
+ this.uploadProgressChange.emit(0);
3753
+ console.log('📊 [FileInput] Upload progress initialized to 0%');
3754
+ // Make form control invalid during upload - this prevents form submission
3755
+ this.onChange(null);
3756
+ console.log('🚫 [FileInput] Form control value set to null to prevent submission');
3757
+ // Show initial progress notification with spinner (persistent - no auto-dismiss)
3758
+ const notificationId = this.notificationService.showProgress('🔄 Preparing file upload...', 0, { duration: 0 });
3759
+ this.uploadNotificationId.set(notificationId);
3760
+ console.log('🔔 [FileInput] Progress notification started with ID:', notificationId);
3761
+ this.fileManagerService.uploadFile(file, this.uploadDataSignal(), (progress) => {
3762
+ // Real progress callback from file manager service
3763
+ this.uploadProgress.set(progress);
3764
+ this.uploadProgressChange.emit(progress);
3765
+ console.log('📈 [FileInput] Upload progress:', Math.round(progress) + '%');
3766
+ // Set upload status to 'uploading' when progress starts
3767
+ if (this.uploadStatus() === 'start') {
3768
+ this.uploadStatus.set('uploading');
3769
+ console.log('🔄 [FileInput] Upload status changed to:', this.uploadStatus());
3770
+ }
3771
+ // Update progress notification with spinner
3772
+ const notificationId = this.uploadNotificationId();
3773
+ if (notificationId) {
3774
+ let progressMessage = '';
3775
+ if (progress < 10) {
3776
+ progressMessage = '🔄 Starting upload...';
3777
+ }
3778
+ else if (progress < 25) {
3779
+ progressMessage = '🔄 Uploading file...';
3780
+ }
3781
+ else if (progress < 50) {
3782
+ progressMessage = '🔄 Upload in progress...';
3783
+ }
3784
+ else if (progress < 75) {
3785
+ progressMessage = '🔄 Almost done...';
3786
+ }
3787
+ else if (progress < 95) {
3788
+ progressMessage = '🔄 Finishing upload...';
3789
+ }
3790
+ else {
3791
+ progressMessage = '🔄 Finalizing...';
3792
+ }
3793
+ this.notificationService.updateProgress(notificationId, progress, progressMessage);
3794
+ }
3795
+ }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
4161
3796
  next: (response) => {
4162
- const groupId = response.data?.objectId;
4163
- console.log('🆔 [GlobalFileUploader] Generated group ID:', groupId);
4164
- this.currentGroupId.set(groupId || null);
4165
- if (groupId) {
4166
- // Upload all files with the same group ID
4167
- this.uploadFilesWithGroupId(uploads, groupId);
3797
+ console.log('🎉 [FileInput] Upload SUCCESS - Response received:', response);
3798
+ // Angular 20: Complete the pending task
3799
+ // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
3800
+ // console.log('✅ [FileInput] Pending task completed for successful upload');
3801
+ // Set upload status to 'success'
3802
+ this.uploadStatus.set('success');
3803
+ console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3804
+ // Complete the progress
3805
+ this.uploadProgress.set(100);
3806
+ this.uploadProgressChange.emit(100);
3807
+ console.log('📊 [FileInput] Upload progress completed: 100%');
3808
+ // Update progress notification to complete
3809
+ const notificationId = this.uploadNotificationId();
3810
+ if (notificationId) {
3811
+ this.notificationService.remove(notificationId);
3812
+ console.log('🔔 [FileInput] Progress notification removed');
3813
+ }
3814
+ // Success notification removed for cleaner UX
3815
+ this.uploadNotificationId.set(null);
3816
+ // Extract ID from CoreFileManagerInsertUpdateResponse
3817
+ const uploadedId = response?.data?.core_file_manager?.[0]?.cyfm_id;
3818
+ if (uploadedId) {
3819
+ console.log('✅ [FileInput] File uploaded successfully with ID:', uploadedId);
3820
+ // Set the uploaded ID as the form control value
3821
+ this.onChange(uploadedId);
3822
+ console.log('📝 [FileInput] Form control value set to uploaded ID:', uploadedId);
3823
+ // Only emit individual uploadSuccess if not in multiple upload mode
3824
+ if (!this.isMultipleUploadMode()) {
3825
+ this.uploadSuccess.emit(uploadedId);
3826
+ console.log('📝 [FileInput] Upload success event emitted with file ID:', uploadedId);
3827
+ }
3828
+ else {
3829
+ console.log('📝 [FileInput] Individual upload success suppressed (multiple upload mode) - file ID:', uploadedId);
3830
+ }
4168
3831
  }
4169
3832
  else {
4170
- console.error('❌ [GlobalFileUploader] No group ID received');
4171
- this.handleUploadError('Failed to generate group ID', uploads[0]?.fileId);
3833
+ console.error('❌ [FileInput] Upload successful but no ID returned:', response);
3834
+ this.uploadError.emit('Upload successful but no ID returned');
3835
+ }
3836
+ this.isUploading.set(false);
3837
+ console.log('🔄 [FileInput] isUploading set to false');
3838
+ },
3839
+ error: (error) => {
3840
+ console.error('💥 [FileInput] Upload FAILED:', error);
3841
+ // Angular 20: Complete the pending task even on error
3842
+ // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
3843
+ // console.log('❌ [FileInput] Pending task completed for failed upload');
3844
+ // Set upload status to 'error' and remove upload validation error
3845
+ this.uploadStatus.set('error');
3846
+ console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3847
+ // Remove progress notification and show error
3848
+ const notificationId = this.uploadNotificationId();
3849
+ if (notificationId) {
3850
+ this.notificationService.remove(notificationId);
3851
+ console.log('🔔 [FileInput] Progress notification removed due to error');
3852
+ }
3853
+ this.notificationService.error(`❌ File upload failed: ${error.message || error.error?.message || 'Unknown error occurred'}`, { duration: 0 });
3854
+ this.uploadNotificationId.set(null);
3855
+ this.uploadError.emit(error.message || error.error?.message || 'Upload failed');
3856
+ this.isUploading.set(false);
3857
+ this.uploadProgress.set(0);
3858
+ this.uploadProgressChange.emit(0);
3859
+ console.log('🔄 [FileInput] Upload state reset - isUploading: false, progress: 0%');
3860
+ }
3861
+ });
3862
+ }
3863
+ /**
3864
+ * Upload multiple files with group ID support
3865
+ * FLOW: 1) Generate group ID first, 2) Upload all files with same group ID, 3) Emit group ID on completion
3866
+ */
3867
+ uploadMultipleFiles(files) {
3868
+ console.log('📤 [FileInput] uploadMultipleFiles called for:', files.length, 'files');
3869
+ console.log('🔄 [FileInput] STEP 1: Generate group ID before starting any file uploads');
3870
+ // Set multiple upload mode flag
3871
+ this.isMultipleUploadMode.set(true);
3872
+ // Set upload status to 'start' before starting upload
3873
+ this.uploadStatus.set('start');
3874
+ this.isUploading.set(true);
3875
+ this.uploadProgress.set(0);
3876
+ this.uploadProgressChange.emit(0);
3877
+ // Make form control invalid during upload
3878
+ this.onChange(null);
3879
+ // Show initial progress notification
3880
+ const notificationId = this.notificationService.showProgress('🔄 Preparing multiple file upload...', 0, { duration: 0 });
3881
+ this.uploadNotificationId.set(notificationId);
3882
+ // STEP 1: Generate or get group ID BEFORE starting any file uploads
3883
+ const existingGroupId = this.uploadDataSignal().groupId;
3884
+ if (existingGroupId) {
3885
+ console.log('🆔 [FileInput] STEP 1 COMPLETE: Using existing group ID:', existingGroupId);
3886
+ console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', existingGroupId);
3887
+ this.groupId.set(existingGroupId);
3888
+ this.startMulti(files, existingGroupId);
3889
+ }
3890
+ else {
3891
+ console.log('🆔 [FileInput] No existing group ID, generating new one...');
3892
+ // Generate group ID BEFORE starting any file uploads
3893
+ this.fileManagerService.generateObjectId().subscribe({
3894
+ next: (response) => {
3895
+ const newGroupId = response.data?.objectId;
3896
+ console.log('🆔 [FileInput] STEP 1 COMPLETE: Generated new group ID:', newGroupId);
3897
+ console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', newGroupId);
3898
+ this.groupId.set(newGroupId);
3899
+ this.startMulti(files, newGroupId);
3900
+ },
3901
+ error: (error) => {
3902
+ console.error('❌ [FileInput] Failed to generate group ID:', error);
3903
+ this.uploadError.emit('Failed to generate group ID');
3904
+ this.isUploading.set(false);
3905
+ this.uploadStatus.set('error');
3906
+ const notificationId = this.uploadNotificationId();
3907
+ if (notificationId) {
3908
+ this.notificationService.remove(notificationId);
3909
+ }
3910
+ this.notificationService.error('❌ Failed to generate group ID for multiple file upload', { duration: 0 });
3911
+ this.uploadNotificationId.set(null);
4172
3912
  }
4173
- },
4174
- error: (error) => {
4175
- console.error('❌ [GlobalFileUploader] Failed to generate group ID:', error);
4176
- this.handleUploadError('Failed to generate group ID', uploads[0]?.fileId);
4177
- }
4178
- });
3913
+ });
3914
+ }
4179
3915
  }
4180
3916
  /**
4181
- * Upload files with group ID
3917
+ * Start uploading multiple files with the provided group ID
3918
+ * All files will be uploaded with the SAME group ID that was generated before this method
4182
3919
  */
4183
- uploadFilesWithGroupId(uploads, groupId) {
4184
- let completedCount = 0;
4185
- let failedCount = 0;
4186
- const totalFiles = uploads.length;
4187
- const uploadedFiles = [];
4188
- uploads.forEach((upload, index) => {
4189
- // Update status to uploading
4190
- this.updateUploadStatus(upload.fileId, 'uploading', 0);
4191
- const uploadRequest = {
4192
- file: upload.file,
4193
- userId: this.userId,
4194
- groupId: groupId,
4195
- tags: [],
4196
- permissions: ['read', 'write']
4197
- };
4198
- this.fileService.uploadFile(upload.file, this.convertToFileUploadOptions(uploadRequest), (progress) => {
4199
- // Use the original fileId for progress updates during upload
4200
- this.updateUploadProgress(upload.fileId, progress);
4201
- })
4202
- .pipe(takeUntilDestroyed(this.destroyRef))
4203
- .subscribe({
3920
+ startMulti(files, groupId) {
3921
+ console.log('🚀 [FileInput] STEP 2: Starting upload for', files.length, 'files with group ID:', groupId);
3922
+ console.log('📋 [FileInput] All files will use the same group ID:', groupId);
3923
+ let completedUploads = 0;
3924
+ let failedUploads = 0;
3925
+ const totalFiles = files.length;
3926
+ // IMPORTANT: All files use the SAME group ID that was generated before starting uploads
3927
+ const uploadDataWithGroupId = {
3928
+ ...this.uploadDataSignal(),
3929
+ groupId: groupId
3930
+ };
3931
+ files.forEach((file, index) => {
3932
+ console.log(`📤 [FileInput] Uploading file ${index + 1}/${totalFiles}: "${file.name}" with group ID: ${groupId}`);
3933
+ this.fileManagerService.uploadFile(file, uploadDataWithGroupId, (progress) => {
3934
+ // Calculate overall progress
3935
+ const fileProgress = progress / totalFiles;
3936
+ const overallProgress = ((completedUploads * 100) + fileProgress) / totalFiles;
3937
+ this.uploadProgress.set(overallProgress);
3938
+ this.uploadProgressChange.emit(overallProgress);
3939
+ // Update progress notification
3940
+ const notificationId = this.uploadNotificationId();
3941
+ if (notificationId) {
3942
+ const progressMessage = `🔄 Uploading file ${index + 1} of ${totalFiles}...`;
3943
+ this.notificationService.updateProgress(notificationId, overallProgress, progressMessage);
3944
+ }
3945
+ }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
4204
3946
  next: (response) => {
4205
- completedCount++;
4206
- console.log(`✅ [GlobalFileUploader] File ${index + 1}/${totalFiles} uploaded successfully`);
4207
- // Find the upload item by cyfm_temp_unique_id from response
4208
- const responseFile = response?.data?.core_file_manager?.[0];
4209
- const tempUniqueId = responseFile?.cyfm_temp_unique_id;
4210
- console.log('🔍 [GlobalFileUploader] Response tempUniqueId:', tempUniqueId);
4211
- console.log('🔍 [GlobalFileUploader] Original upload fileId:', upload.fileId);
4212
- // Use the tempUniqueId to find and update the correct upload item
4213
- if (tempUniqueId) {
4214
- this.updateUploadStatusByTempId(tempUniqueId, 'completed', 100, undefined, response);
4215
- }
4216
- else {
4217
- // Fallback to original fileId
4218
- this.updateUploadStatus(upload.fileId, 'completed', 100, undefined, response);
4219
- }
4220
- this.uploadComplete.emit({
4221
- fileId: tempUniqueId || upload.fileId,
4222
- fileName: upload.fileName,
4223
- progress: 100,
4224
- status: 'completed',
4225
- uploadedFile: response
4226
- });
4227
- if (responseFile) {
4228
- uploadedFiles.push(responseFile);
4229
- }
4230
- console.log('🔍 [GlobalFileUploader] Upload response received:', response);
4231
- console.log('🔍 [GlobalFileUploader] Response data structure:', response?.data);
3947
+ completedUploads++;
3948
+ console.log(`✅ [FileInput] File ${index + 1}/${totalFiles} uploaded`);
4232
3949
  // Check if all files are completed
4233
- if (completedCount + failedCount === totalFiles) {
4234
- this.handleMultipleUploadComplete(completedCount, failedCount, groupId, uploadedFiles);
3950
+ if (completedUploads + failedUploads === totalFiles) {
3951
+ this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
4235
3952
  }
4236
3953
  },
4237
3954
  error: (error) => {
4238
- failedCount++;
4239
- console.error(`❌ [GlobalFileUploader] File ${index + 1}/${totalFiles} upload failed:`, 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
- });
3955
+ failedUploads++;
3956
+ console.error(`❌ [FileInput] File ${index + 1}/${totalFiles} upload failed:`, error);
4245
3957
  // Check if all files are completed
4246
- if (completedCount + failedCount === totalFiles) {
4247
- this.handleMultipleUploadComplete(completedCount, failedCount, groupId, uploadedFiles);
3958
+ if (completedUploads + failedUploads === totalFiles) {
3959
+ this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
4248
3960
  }
4249
3961
  }
4250
3962
  });
4251
3963
  });
4252
3964
  }
4253
3965
  /**
4254
- * Handle multiple upload completion
3966
+ * Handle completion of multiple file upload
4255
3967
  */
4256
- handleMultipleUploadComplete(completed, failed, groupId, uploadedFiles) {
4257
- console.log(`📊 [GlobalFileUploader] Multiple upload complete: ${completed}/${completed + failed} successful`);
3968
+ handleMultipleUploadComplete(completed, failed, total, groupId) {
3969
+ console.log(`📊 [FileInput] Multiple upload complete: ${completed}/${total} successful, ${failed} failed`);
4258
3970
  this.isUploading.set(false);
4259
- if (completed > 0) {
4260
- this.allUploadsComplete.emit({
4261
- groupId: groupId,
4262
- uploadedFiles: uploadedFiles
4263
- });
3971
+ this.uploadProgress.set(100);
3972
+ this.uploadProgressChange.emit(100);
3973
+ // Remove progress notification
3974
+ const notificationId = this.uploadNotificationId();
3975
+ if (notificationId) {
3976
+ this.notificationService.remove(notificationId);
3977
+ }
3978
+ this.uploadNotificationId.set(null);
3979
+ if (failed === 0) {
3980
+ // All files uploaded successfully
3981
+ this.uploadStatus.set('success');
3982
+ // Success notification removed for cleaner UX
3983
+ // STEP 3: For multiple file upload, emit the group ID (not individual file IDs)
3984
+ this.onChange(groupId);
3985
+ this.uploadSuccess.emit(groupId);
3986
+ console.log('📝 [FileInput] Multiple upload completed with group ID:', groupId);
3987
+ }
3988
+ else if (completed > 0) {
3989
+ // Some files uploaded successfully
3990
+ this.uploadStatus.set('error');
3991
+ this.notificationService.warning(`⚠️ ${completed}/${total} files uploaded. ${failed} failed.`, { duration: 0 });
3992
+ this.uploadError.emit(`${failed} out of ${total} files failed to upload`);
3993
+ }
3994
+ else {
3995
+ // All files failed
3996
+ this.uploadStatus.set('error');
3997
+ this.notificationService.error(`❌ All ${total} files failed to upload.`, { duration: 0 });
3998
+ this.uploadError.emit('All files failed to upload');
4264
3999
  }
4000
+ // Reset multiple upload mode flag
4001
+ this.isMultipleUploadMode.set(false);
4265
4002
  }
4266
- /**
4267
- * Upload next file in queue (single file mode)
4268
- */
4269
- uploadNextFile() {
4270
- const pendingUpload = this.uploadQueue().find(upload => upload.status === 'pending');
4271
- if (!pendingUpload) {
4272
- this.isUploading.set(false);
4003
+ generatePreviews() {
4004
+ // Clear existing previews
4005
+ this.clearPreviews();
4006
+ if (!this.showPreviewSignal() || !this.files()) {
4273
4007
  return;
4274
4008
  }
4275
- // Update status to uploading
4276
- this.updateUploadStatus(pendingUpload.fileId, 'uploading', 0);
4277
- const uploadRequest = {
4278
- file: pendingUpload.file,
4279
- userId: this.userId,
4280
- tags: [],
4281
- permissions: ['read', 'write']
4282
- };
4283
- this.fileService.uploadFile(pendingUpload.file, this.convertToFileUploadOptions(uploadRequest), (progress) => {
4284
- this.updateUploadProgress(pendingUpload.fileId, progress);
4285
- })
4286
- .pipe(takeUntilDestroyed(this.destroyRef))
4287
- .subscribe({
4288
- next: (response) => {
4289
- console.log('✅ [GlobalFileUploader] Single file upload successful:', response);
4290
- // Find the upload item by cyfm_temp_unique_id from response
4291
- const responseFile = response?.data?.core_file_manager?.[0];
4292
- const tempUniqueId = responseFile?.cyfm_temp_unique_id;
4293
- console.log('🔍 [GlobalFileUploader] Single upload - Response tempUniqueId:', tempUniqueId);
4294
- console.log('🔍 [GlobalFileUploader] Single upload - Original upload fileId:', pendingUpload.fileId);
4295
- // Use the tempUniqueId to find and update the correct upload item
4296
- if (tempUniqueId) {
4297
- this.updateUploadStatusByTempId(tempUniqueId, 'completed', 100, undefined, response);
4298
- }
4299
- else {
4300
- // Fallback to original fileId
4301
- this.updateUploadStatus(pendingUpload.fileId, 'completed', 100, undefined, response);
4302
- }
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);
4009
+ Array.from(this.files()).forEach(file => {
4010
+ if (this.isImageFile(file)) {
4011
+ const reader = new FileReader();
4012
+ reader.onload = (e) => {
4013
+ if (e.target?.result) {
4014
+ this.previewUrls.update(urls => [...urls, e.target.result]);
4015
+ }
4016
+ };
4017
+ reader.readAsDataURL(file);
4018
+ }
4019
+ });
4020
+ }
4021
+ clearPreviews() {
4022
+ // Revoke object URLs to prevent memory leaks
4023
+ this.previewUrls().forEach(url => {
4024
+ if (url.startsWith('blob:')) {
4025
+ URL.revokeObjectURL(url);
4026
+ }
4027
+ });
4028
+ this.previewUrls.set([]);
4029
+ }
4030
+ isImageFile(file) {
4031
+ return file.type.startsWith('image/');
4032
+ }
4033
+ loadFileDetailsFromId(fileId) {
4034
+ console.log('🔍 [FileInput] Loading file details for ID:', fileId);
4035
+ if (!fileId)
4036
+ return;
4037
+ this.fileManagerService?.getFileDetails({ cyfm_id: fileId })?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
4038
+ next: (fileDetails) => {
4039
+ console.log('📋 [FileInput] File details received:', fileDetails);
4040
+ if (fileDetails?.data?.length) {
4041
+ const fileData = fileDetails.data[0];
4042
+ console.log('📁 [FileInput] File data:', fileData);
4043
+ // Set file name from the details
4044
+ if (fileData.cyfm_name) {
4045
+ this.fileNames.set([fileData.cyfm_name]);
4046
+ console.log('📝 [FileInput] File name set:', fileData.cyfm_name);
4047
+ }
4048
+ // If it's an image and we have base64 data, set preview
4049
+ if (this.showPreviewSignal() && fileData.cyfm_file_base64) {
4050
+ // Check if it's an image file based on file name or type
4051
+ const isImage = this.isImageFileFromName(fileData.cyfm_name || '') ||
4052
+ this.isImageFileFromType(fileData.cyfm_type || '');
4053
+ if (isImage) {
4054
+ // Add data URL prefix if not already present
4055
+ let base64Data = fileData.cyfm_file_base64;
4056
+ if (!base64Data.startsWith('data:')) {
4057
+ const mimeType = fileData.cyfm_type || 'image/jpeg';
4058
+ base64Data = `data:${mimeType};base64,${base64Data}`;
4059
+ }
4060
+ this.previewUrls.set([base64Data]);
4061
+ console.log('🖼️ [FileInput] Preview set from base64 data');
4062
+ }
4063
+ }
4064
+ }
4065
+ else {
4066
+ console.warn('⚠️ [FileInput] No file data found for ID:', fileId);
4067
+ }
4312
4068
  },
4313
4069
  error: (error) => {
4314
- console.error('❌ [GlobalFileUploader] Upload error:', error);
4315
- this.updateUploadStatus(pendingUpload.fileId, 'error', 0, error.message || 'Upload failed');
4316
- this.uploadError.emit({
4317
- fileId: pendingUpload.fileId,
4318
- error: error.message || 'Upload failed'
4319
- });
4320
- // Process next file
4321
- setTimeout(() => this.uploadNextFile(), 500);
4070
+ console.error('❌ [FileInput] Error loading file details:', error);
4071
+ this.notificationService.error(`Failed to load file details: ${error.message || 'Unknown error'}`, { duration: 0 });
4322
4072
  }
4323
4073
  });
4324
4074
  }
4325
- /**
4326
- * Update upload progress
4327
- */
4328
- updateUploadProgress(fileId, progress) {
4329
- this.uploadQueue.update(queue => queue.map(upload => upload.fileId === fileId
4330
- ? { ...upload, progress }
4331
- : upload));
4075
+ isImageFileFromName(fileName) {
4076
+ if (!fileName)
4077
+ return false;
4078
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
4079
+ const lowerFileName = fileName.toLowerCase();
4080
+ return imageExtensions.some(ext => lowerFileName.endsWith(ext));
4332
4081
  }
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
- });
4082
+ isImageFileFromType(fileType) {
4083
+ if (!fileType)
4084
+ return false;
4085
+ return fileType.startsWith('image/');
4349
4086
  }
4350
- /**
4351
- * Update upload status by cyfm_temp_unique_id
4352
- */
4353
- updateUploadStatusByTempId(tempUniqueId, status, progress, error, uploadedFile) {
4354
- console.log('🔄 [GlobalFileUploader] Updating upload status by tempId:', { tempUniqueId, status, progress, error: !!error, uploadedFile: !!uploadedFile });
4355
- this.uploadQueue.update(queue => {
4356
- const updatedQueue = queue.map(upload => {
4357
- // Check if this upload's fileId matches the tempUniqueId
4358
- if (upload.fileId === tempUniqueId) {
4359
- console.log('🔄 [GlobalFileUploader] Found upload by tempId to update:', upload.fileName, 'from', upload.status, 'to', status);
4360
- return { ...upload, status, progress, error, uploadedFile };
4087
+ removePreview(index) {
4088
+ const currentFiles = this.files();
4089
+ const currentUrls = this.previewUrls();
4090
+ if (currentFiles && currentFiles.length > index) {
4091
+ // Handle FileList case - remove file from FileList
4092
+ const dt = new DataTransfer();
4093
+ Array.from(currentFiles).forEach((file, i) => {
4094
+ if (i !== index) {
4095
+ dt.items.add(file);
4361
4096
  }
4362
- return upload;
4363
4097
  });
4364
- console.log('🔄 [GlobalFileUploader] Updated queue by tempId:', updatedQueue.map(u => ({ fileName: u.fileName, status: u.status, fileId: u.fileId })));
4365
- return updatedQueue;
4366
- });
4098
+ const newFiles = dt.files;
4099
+ this.files.set(newFiles);
4100
+ this.fileNames.set(Array.from(newFiles).map(f => f.name));
4101
+ // Remove the preview URL
4102
+ if (currentUrls[index] && currentUrls[index].startsWith('blob:')) {
4103
+ URL.revokeObjectURL(currentUrls[index]);
4104
+ }
4105
+ this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
4106
+ this.onChange(newFiles);
4107
+ this.fileChange.emit(newFiles);
4108
+ }
4109
+ else if (currentUrls.length > index) {
4110
+ // Handle uploaded file ID case - clear the preview and set control value to null
4111
+ console.log('🗑️ [FileInput] Removing preview for uploaded file ID');
4112
+ // Clear preview
4113
+ this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
4114
+ this.fileNames.set([]);
4115
+ // Set control value to null since we're removing the uploaded file
4116
+ this.onChange(null);
4117
+ this.fileChange.emit(null);
4118
+ }
4367
4119
  }
4368
- /**
4369
- * Handle upload error
4370
- */
4371
- handleUploadError(error, fileId) {
4372
- if (fileId) {
4373
- this.updateUploadStatus(fileId, 'error', 0, error);
4374
- this.uploadError.emit({ fileId, error });
4120
+ ngOnDestroy() {
4121
+ // Clean up preview URLs to prevent memory leaks
4122
+ this.clearPreviews();
4123
+ // Clean up any active upload notification
4124
+ const notificationId = this.uploadNotificationId();
4125
+ if (notificationId) {
4126
+ this.notificationService.remove(notificationId);
4127
+ this.uploadNotificationId.set(null);
4375
4128
  }
4376
- this.isUploading.set(false);
4377
4129
  }
4378
- /**
4379
- * Cancel upload
4380
- */
4381
- cancelUpload(fileId) {
4382
- this.updateUploadStatus(fileId, 'cancelled', 0);
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);
4130
+ triggerFileSelect() {
4131
+ const fileInput = document.getElementById('cide-file-input-' + this.id());
4132
+ if (fileInput && !this.disabledSignal()) {
4133
+ fileInput.click();
4134
+ }
4388
4135
  }
4389
4136
  /**
4390
- * Retry upload
4137
+ * Show floating uploader manually
4138
+ * This can be called to show the floating uploader even when no files are selected
4391
4139
  */
4392
- retryUpload(fileId) {
4393
- this.updateUploadStatus(fileId, 'pending', 0);
4394
- this.processUploadQueue();
4140
+ showUploader() {
4141
+ if (this.floatingUploader && this.showFloatingUploaderSignal()) {
4142
+ console.log('👁️ [FileInput] Manually showing floating uploader');
4143
+ this.floatingUploader.showUploader(this.floatingUploaderGroupIdSignal());
4144
+ }
4395
4145
  }
4396
- /**
4397
- * Remove upload from queue
4398
- */
4399
- removeUpload(fileId) {
4400
- this.uploadQueue.update(queue => queue.filter(upload => upload.fileId !== fileId));
4146
+ // Drag and Drop Event Handlers
4147
+ onDragOver(event) {
4148
+ event.preventDefault();
4149
+ event.stopPropagation();
4150
+ if (!this.disabledSignal()) {
4151
+ this.isDragOver.set(true);
4152
+ console.log('🔄 [FileInput] Drag over detected');
4153
+ }
4401
4154
  }
4402
- /**
4403
- * Clear completed uploads
4404
- */
4405
- clearCompleted() {
4406
- this.uploadQueue.update(queue => queue.filter(upload => upload.status !== 'completed'));
4155
+ onDragLeave(event) {
4156
+ event.preventDefault();
4157
+ event.stopPropagation();
4158
+ this.isDragOver.set(false);
4159
+ console.log('🔄 [FileInput] Drag leave detected');
4407
4160
  }
4408
- /**
4409
- * Clear all uploads
4410
- */
4411
- clearAll() {
4412
- this.uploadQueue.set([]);
4413
- this.currentGroupId.set(null);
4161
+ onDragEnter(event) {
4162
+ event.preventDefault();
4163
+ event.stopPropagation();
4164
+ if (!this.disabledSignal()) {
4165
+ this.isDragOver.set(true);
4166
+ console.log('🔄 [FileInput] Drag enter detected');
4167
+ }
4414
4168
  }
4415
- /**
4416
- * Get status icon
4417
- */
4418
- getStatusIcon(status) {
4419
- switch (status) {
4420
- case 'pending': return 'schedule';
4421
- case 'uploading': return 'cloud_upload';
4422
- case 'completed': return 'check_circle';
4423
- case 'error': return 'error';
4424
- case 'cancelled': return 'cancel';
4425
- default: return 'help';
4169
+ onDrop(event) {
4170
+ event.preventDefault();
4171
+ event.stopPropagation();
4172
+ this.isDragOver.set(false);
4173
+ if (this.disabledSignal()) {
4174
+ console.log('⏸️ [FileInput] Drop ignored - component is disabled');
4175
+ return;
4176
+ }
4177
+ const files = event.dataTransfer?.files;
4178
+ if (files && files.length > 0) {
4179
+ console.log('📁 [FileInput] Files dropped:', Array.from(files).map(f => f.name));
4180
+ // Validate file types if accept is specified
4181
+ if (this.acceptSignal() && !this.validateFileTypes(files)) {
4182
+ console.log('❌ [FileInput] Invalid file types dropped');
4183
+ this.notificationService.error('❌ Invalid file type. Please select files of the correct type.', { duration: 0 });
4184
+ return;
4185
+ }
4186
+ // Handle single vs multiple files
4187
+ if (!this.multipleSignal() && files.length > 1) {
4188
+ console.log('⚠️ [FileInput] Multiple files dropped but multiple is disabled');
4189
+ this.notificationService.warning('⚠️ Only one file is allowed. Using the first file.', { duration: 0 });
4190
+ // Create a new FileList with only the first file
4191
+ const dt = new DataTransfer();
4192
+ dt.items.add(files[0]);
4193
+ this.handleFileSelection(dt.files);
4194
+ }
4195
+ else {
4196
+ this.handleFileSelection(files);
4197
+ }
4426
4198
  }
4427
4199
  }
4428
- /**
4429
- * Get status class
4430
- */
4431
- getStatusClass(status) {
4432
- switch (status) {
4433
- case 'pending': return 'status-pending';
4434
- case 'uploading': return 'status-uploading';
4435
- case 'completed': return 'status-completed';
4436
- case 'error': return 'status-error';
4437
- case 'cancelled': return 'status-cancelled';
4438
- default: return 'status-unknown';
4200
+ validateFileTypes(files) {
4201
+ const acceptTypes = this.acceptSignal().split(',').map(type => type.trim());
4202
+ if (acceptTypes.length === 0 || acceptTypes[0] === '')
4203
+ return true;
4204
+ return Array.from(files).every(file => {
4205
+ return acceptTypes.some(acceptType => {
4206
+ if (acceptType.startsWith('.')) {
4207
+ // Extension-based validation
4208
+ return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
4209
+ }
4210
+ else if (acceptType.includes('/')) {
4211
+ // MIME type validation
4212
+ return file.type === acceptType || file.type.startsWith(acceptType.replace('*', ''));
4213
+ }
4214
+ return false;
4215
+ });
4216
+ });
4217
+ }
4218
+ handleFileSelection(files) {
4219
+ this.files.set(files);
4220
+ this.fileNames.set(Array.from(files).map(f => f.name));
4221
+ console.log('📁 [FileInput] Files selected via drag & drop:', this.fileNames());
4222
+ this.generatePreviews();
4223
+ // Reset upload status when new file is selected
4224
+ this.uploadStatus.set('idle');
4225
+ console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
4226
+ this.onChange(files);
4227
+ this.fileChange.emit(files);
4228
+ this.onTouched();
4229
+ // Show floating uploader if enabled and files are selected
4230
+ if (this.showFloatingUploaderSignal() && files.length > 0 && this.floatingUploader) {
4231
+ console.log('👁️ [FileInput] Showing floating uploader for dropped files:', files.length, 'files');
4232
+ this.floatingUploader.showUploader(this.floatingUploaderGroupIdSignal());
4233
+ }
4234
+ // Auto upload if enabled
4235
+ if (this.autoUploadSignal() && files.length > 0) {
4236
+ if (this.multipleSignal()) {
4237
+ console.log('🚀 [FileInput] Auto upload enabled for multiple files mode (drag & drop):', files.length, 'files');
4238
+ this.uploadMultipleFiles(Array.from(files));
4239
+ }
4240
+ else {
4241
+ console.log('🚀 [FileInput] Auto upload enabled for single file mode (drag & drop):', files[0].name);
4242
+ this.uploadFile(files[0]);
4243
+ }
4244
+ }
4245
+ else {
4246
+ console.log('⏸️ [FileInput] Auto upload disabled or no files');
4439
4247
  }
4440
4248
  }
4441
- /**
4442
- * Get file size display
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];
4249
+ isRequired() {
4250
+ return this.requiredSignal();
4452
4251
  }
4453
4252
  /**
4454
- * Generate unique file ID - this should match the service's generateFileId method
4455
- * The service uses: ${file.name}_${file.size}_${Date.now()}
4253
+ * Angular 20: Utility method to get upload data with proper typing
4254
+ * @returns Properly typed upload data
4456
4255
  */
4457
- generateFileId(file) {
4458
- return `${file.name}_${file.size}_${Date.now()}`;
4256
+ getUploadData() {
4257
+ return this.uploadDataSignal();
4459
4258
  }
4460
4259
  /**
4461
- * Convert IFileUploadRequest to FileUploadOptions
4260
+ * Angular 20: Utility method to update upload data with type safety
4261
+ * @param data Partial upload data to merge with existing data
4462
4262
  */
4463
- convertToFileUploadOptions(request) {
4263
+ updateUploadData(data) {
4264
+ const currentData = this.uploadDataSignal();
4265
+ const updatedData = { ...currentData, ...data };
4266
+ // Note: This would require the uploadData to be a writable signal
4267
+ // For now, this method serves as a type-safe way to work with upload data
4268
+ console.log('📝 [FileInput] Upload data updated:', updatedData);
4269
+ }
4270
+ getCurrentState() {
4464
4271
  return {
4465
- altText: request.altText,
4466
- userId: request.userId,
4467
- permissions: request.permissions,
4468
- tags: request.tags,
4469
- groupId: request.groupId
4272
+ id: this.id(),
4273
+ label: this.labelSignal(),
4274
+ required: this.requiredSignal(),
4275
+ disabled: this.disabledSignal(),
4276
+ accept: this.acceptSignal(),
4277
+ multiple: this.multipleSignal(),
4278
+ showPreview: this.showPreviewSignal(),
4279
+ autoUpload: this.autoUploadSignal(),
4280
+ uploadStatus: this.uploadStatus(),
4281
+ isUploading: this.isUploading(),
4282
+ uploadProgress: this.uploadProgress(),
4283
+ files: this.files() ? Array.from(this.files()).map(f => ({ name: f.name, size: f.size, type: f.type })) : null,
4284
+ fileNames: this.fileNames(),
4285
+ previewUrls: this.previewUrls().length,
4286
+ helperText: this.helperTextSignal(),
4287
+ errorText: this.errorTextSignal(),
4288
+ placeholderText: this.placeholderTextSignal(),
4289
+ placeholderIcon: this.placeholderIconSignal(),
4290
+ previewWidth: this.previewWidthSignal(),
4291
+ previewHeight: this.previewHeightSignal(),
4292
+ previewBoxMode: this.previewBoxModeSignal(),
4293
+ showFileName: this.showFileNameSignal(),
4294
+ uploadData: this.uploadDataSignal()
4470
4295
  };
4471
4296
  }
4472
- /**
4473
- * Trigger file input click
4474
- */
4475
- triggerFileInput() {
4476
- const fileInput = document.getElementById('global-file-input');
4477
- if (fileInput) {
4478
- fileInput.click();
4297
+ async getControlData() {
4298
+ console.log('🔍 [FileInput] getControlData called');
4299
+ const cide_element_data = await this.elementService?.getElementData({ sype_key: this.id() });
4300
+ if (cide_element_data) {
4301
+ console.log('📋 [FileInput] Element data loaded:', cide_element_data);
4302
+ // Note: Since we're using input signals, we can't directly set their values
4303
+ // This method would need to be refactored to work with the new signal-based approach
4304
+ // For now, we'll log the data and trigger validation
4305
+ console.log('✅ [FileInput] Control data received from element service');
4306
+ console.log('⚠️ [FileInput] Note: Input signals cannot be modified after component initialization');
4307
+ // Trigger validation update
4308
+ this.onValidatorChange();
4309
+ }
4310
+ else {
4311
+ console.log('⚠️ [FileInput] No element data found for key:', this.id());
4479
4312
  }
4480
4313
  }
4481
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleGlobalFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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"] }] });
4314
+ // Validator implementation
4315
+ validate(control) {
4316
+ console.log('🔍 [FileInput] validate() called - uploadStatus:', this.uploadStatus(), 'required:', this.requiredSignal(), 'files:', !!this.files(), 'control.value:', control.value);
4317
+ // If upload is in progress (start or uploading status), return validation error
4318
+ if (this.uploadStatus() === 'start' || this.uploadStatus() === 'uploading') {
4319
+ console.log('⚠️ [FileInput] Validation ERROR: Upload in progress');
4320
+ return { 'uploadInProgress': { message: 'File upload in progress. Please wait...' } };
4321
+ }
4322
+ // If required and no file is selected and no control value (uploaded file ID), return validation error
4323
+ if (this.requiredSignal() && !this.files() && !control.value) {
4324
+ console.log('⚠️ [FileInput] Validation ERROR: File required');
4325
+ return { 'required': { message: 'Please select a file to upload.' } };
4326
+ }
4327
+ console.log('✅ [FileInput] Validation PASSED: No errors');
4328
+ return null; // No validation errors
4329
+ }
4330
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4331
+ 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: [
4332
+ {
4333
+ provide: NG_VALUE_ACCESSOR,
4334
+ useExisting: CideEleFileInputComponent,
4335
+ multi: true
4336
+ },
4337
+ {
4338
+ provide: NG_VALIDATORS,
4339
+ useExisting: CideEleFileInputComponent,
4340
+ multi: true
4341
+ }
4342
+ ], 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
4343
  }
4484
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleGlobalFileUploaderComponent, decorators: [{
4344
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, decorators: [{
4485
4345
  type: Component,
4486
- args: [{ selector: 'cide-ele-global-file-uploader', standalone: true, imports: [
4487
- CommonModule,
4488
- CideEleButtonComponent,
4489
- CideIconComponent
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
- }], ctorParameters: () => [], propDecorators: { userId: [{
4346
+ args: [{ selector: 'cide-ele-file-input', standalone: true, imports: [CommonModule, FormsModule, CideIconComponent], providers: [
4347
+ {
4348
+ provide: NG_VALUE_ACCESSOR,
4349
+ useExisting: CideEleFileInputComponent,
4350
+ multi: true
4351
+ },
4352
+ {
4353
+ provide: NG_VALIDATORS,
4354
+ useExisting: CideEleFileInputComponent,
4355
+ multi: true
4356
+ }
4357
+ ], 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"] }]
4358
+ }], ctorParameters: () => [], propDecorators: { label: [{
4359
+ type: Input
4360
+ }], accept: [{
4492
4361
  type: Input
4493
4362
  }], multiple: [{
4494
4363
  type: Input
4495
- }], accept: [{
4364
+ }], disabled: [{
4496
4365
  type: Input
4497
- }], maxFileSize: [{
4366
+ }], required: [{
4498
4367
  type: Input
4499
- }], allowedTypes: [{
4368
+ }], helperText: [{
4500
4369
  type: Input
4501
- }], uploadComplete: [{
4370
+ }], errorText: [{
4371
+ type: Input
4372
+ }], showPreview: [{
4373
+ type: Input
4374
+ }], previewWidth: [{
4375
+ type: Input
4376
+ }], previewHeight: [{
4377
+ type: Input
4378
+ }], previewBoxMode: [{
4379
+ type: Input
4380
+ }], showFileName: [{
4381
+ type: Input
4382
+ }], placeholderText: [{
4383
+ type: Input
4384
+ }], placeholderIcon: [{
4385
+ type: Input
4386
+ }], autoUpload: [{
4387
+ type: Input
4388
+ }], uploadData: [{
4389
+ type: Input
4390
+ }], showFloatingUploader: [{
4391
+ type: Input
4392
+ }], floatingUploaderGroupId: [{
4393
+ type: Input
4394
+ }], fileChange: [{
4502
4395
  type: Output
4503
- }], uploadError: [{
4396
+ }], uploadSuccess: [{
4504
4397
  type: Output
4505
- }], uploadCancelled: [{
4398
+ }], uploadError: [{
4506
4399
  type: Output
4507
- }], allUploadsComplete: [{
4400
+ }], uploadProgressChange: [{
4508
4401
  type: Output
4509
4402
  }] } });
4510
4403
 
4511
- class CideEleFloatingFileUploaderComponent {
4404
+ class CideEleGlobalFileUploaderComponent {
4405
+ // Dependency injection
4512
4406
  destroyRef = inject(DestroyRef);
4513
- fileManagerService = inject(CideEleFileManagerService);
4514
- // Signals for reactive state
4515
- isVisible = signal(false, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
4516
- isMinimized = signal(false, ...(ngDevMode ? [{ debugName: "isMinimized" }] : []));
4517
- currentUserId = signal('', ...(ngDevMode ? [{ debugName: "currentUserId" }] : []));
4407
+ fileService = inject(CideEleFileManagerService);
4408
+ // Input properties
4409
+ userId = '';
4410
+ multiple = true;
4411
+ accept = '';
4412
+ maxFileSize = 10; // MB
4413
+ allowedTypes = [];
4414
+ // Output events
4415
+ uploadComplete = new EventEmitter();
4416
+ uploadError = new EventEmitter();
4417
+ uploadCancelled = new EventEmitter();
4418
+ allUploadsComplete = new EventEmitter();
4419
+ // Signals for reactive state management
4420
+ isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
4421
+ isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
4518
4422
  uploadQueue = signal([], ...(ngDevMode ? [{ debugName: "uploadQueue" }] : []));
4423
+ currentGroupId = signal(null, ...(ngDevMode ? [{ debugName: "currentGroupId" }] : []));
4519
4424
  // Computed values
4520
4425
  hasUploads = computed(() => this.uploadQueue().length > 0, ...(ngDevMode ? [{ debugName: "hasUploads" }] : []));
4521
4426
  pendingUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'pending'), ...(ngDevMode ? [{ debugName: "pendingUploads" }] : []));
4522
4427
  activeUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'uploading'), ...(ngDevMode ? [{ debugName: "activeUploads" }] : []));
4523
4428
  completedUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'completed'), ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
4524
4429
  failedUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'error'), ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
4525
- // Animation states
4526
- isAnimating = signal(false, ...(ngDevMode ? [{ debugName: "isAnimating" }] : []));
4527
4430
  constructor() {
4528
- console.log('🚀 [FloatingFileUploader] Component initialized');
4529
- // Set up effect to monitor local upload queue
4530
- effect(() => {
4531
- const uploadQueue = this.uploadQueue();
4532
- const hasUploads = uploadQueue.length > 0;
4533
- console.log('🔄 [FloatingFileUploader] Upload queue changed:', {
4534
- queueLength: uploadQueue.length,
4535
- hasUploads,
4536
- currentVisible: this.isVisible()
4537
- });
4538
- // Show floating uploader when files are added to the queue
4539
- if (hasUploads && !this.isVisible()) {
4540
- console.log('👁️ [FloatingFileUploader] Showing floating uploader due to upload queue');
4541
- this.showWithAnimation();
4542
- }
4543
- else if (!hasUploads && this.isVisible()) {
4544
- console.log('👁️ [FloatingFileUploader] Hiding floating uploader - no files in queue');
4545
- this.hideWithAnimation();
4546
- }
4547
- });
4431
+ console.log('🚀 [GlobalFileUploader] Component initialized');
4548
4432
  }
4549
- ngOnInit() {
4550
- // Set up drag and drop listeners
4551
- this.setupDragAndDrop();
4552
- // Set up file input change listeners
4553
- this.setupFileInputListeners();
4433
+ /**
4434
+ * Handle drag over event
4435
+ */
4436
+ onDragOver(event) {
4437
+ event.preventDefault();
4438
+ event.stopPropagation();
4439
+ this.isDragOver.set(true);
4554
4440
  }
4555
- ngOnDestroy() {
4556
- console.log('🧹 [FloatingFileUploader] Component destroyed');
4557
- this.removeDragAndDropListeners();
4558
- this.removeFileInputListeners();
4441
+ /**
4442
+ * Handle drag leave event
4443
+ */
4444
+ onDragLeave(event) {
4445
+ event.preventDefault();
4446
+ event.stopPropagation();
4447
+ this.isDragOver.set(false);
4448
+ }
4449
+ /**
4450
+ * Handle drop event
4451
+ */
4452
+ onDrop(event) {
4453
+ event.preventDefault();
4454
+ event.stopPropagation();
4455
+ this.isDragOver.set(false);
4456
+ const files = event.dataTransfer?.files;
4457
+ if (files && files.length > 0) {
4458
+ this.addFilesToQueue(Array.from(files));
4459
+ }
4460
+ }
4461
+ /**
4462
+ * Handle file input change
4463
+ */
4464
+ onFileInputChange(event) {
4465
+ const target = event.target;
4466
+ const files = target.files;
4467
+ if (files && files.length > 0) {
4468
+ this.addFilesToQueue(Array.from(files));
4469
+ }
4470
+ // Reset input value to allow selecting the same file again
4471
+ target.value = '';
4559
4472
  }
4560
4473
  /**
4561
- * Set up drag and drop functionality
4474
+ * Add files to upload queue
4562
4475
  */
4563
- setupDragAndDrop() {
4564
- document.addEventListener('dragover', this.handleDragOver.bind(this));
4565
- document.addEventListener('dragleave', this.handleDragLeave.bind(this));
4566
- document.addEventListener('drop', this.handleDrop.bind(this));
4476
+ addFilesToQueue(files) {
4477
+ // Validate files
4478
+ const validFiles = files.filter(file => this.validateFile(file));
4479
+ if (validFiles.length === 0) {
4480
+ console.warn('⚠️ [GlobalFileUploader] No valid files to upload');
4481
+ return;
4482
+ }
4483
+ const newUploads = validFiles.map(file => ({
4484
+ fileId: this.generateFileId(file),
4485
+ fileName: file.name,
4486
+ progress: 0,
4487
+ status: 'pending',
4488
+ file: file
4489
+ }));
4490
+ this.uploadQueue.update(queue => [...queue, ...newUploads]);
4491
+ this.processUploadQueue();
4567
4492
  }
4568
4493
  /**
4569
- * Remove drag and drop listeners
4494
+ * Validate file
4570
4495
  */
4571
- removeDragAndDropListeners() {
4572
- document.removeEventListener('dragover', this.handleDragOver.bind(this));
4573
- document.removeEventListener('dragleave', this.handleDragLeave.bind(this));
4574
- document.removeEventListener('drop', this.handleDrop.bind(this));
4496
+ validateFile(file) {
4497
+ // Check file size
4498
+ const maxSizeBytes = this.maxFileSize * 1024 * 1024;
4499
+ if (file.size > maxSizeBytes) {
4500
+ console.warn(`⚠️ [GlobalFileUploader] File ${file.name} exceeds size limit`);
4501
+ return false;
4502
+ }
4503
+ // Check file type
4504
+ if (this.allowedTypes.length > 0 && !this.allowedTypes.includes(file.type)) {
4505
+ console.warn(`⚠️ [GlobalFileUploader] File type ${file.type} not allowed`);
4506
+ return false;
4507
+ }
4508
+ return true;
4575
4509
  }
4576
4510
  /**
4577
- * Set up file input change listeners
4511
+ * Process upload queue
4578
4512
  */
4579
- setupFileInputListeners() {
4580
- // Listen for file input change events globally
4581
- document.addEventListener('change', this.handleFileInputChange.bind(this));
4513
+ processUploadQueue() {
4514
+ const pendingUploads = this.uploadQueue().filter(upload => upload.status === 'pending');
4515
+ if (pendingUploads.length > 0 && !this.isUploading()) {
4516
+ this.isUploading.set(true);
4517
+ if (this.multiple && pendingUploads.length > 1) {
4518
+ // Multiple file upload with group ID
4519
+ this.uploadMultipleFiles(pendingUploads);
4520
+ }
4521
+ else {
4522
+ // Single file upload
4523
+ this.uploadNextFile();
4524
+ }
4525
+ }
4582
4526
  }
4583
4527
  /**
4584
- * Remove file input listeners
4528
+ * Upload multiple files with group ID
4585
4529
  */
4586
- removeFileInputListeners() {
4587
- document.removeEventListener('change', this.handleFileInputChange.bind(this));
4530
+ uploadMultipleFiles(uploads) {
4531
+ console.log('📤 [GlobalFileUploader] Starting multiple file upload for:', uploads.length, 'files');
4532
+ // Generate group ID first
4533
+ this.fileService.generateObjectId()
4534
+ .pipe(takeUntilDestroyed(this.destroyRef))
4535
+ .subscribe({
4536
+ next: (response) => {
4537
+ const groupId = response.data?.objectId;
4538
+ console.log('🆔 [GlobalFileUploader] Generated group ID:', groupId);
4539
+ this.currentGroupId.set(groupId || null);
4540
+ if (groupId) {
4541
+ // Upload all files with the same group ID
4542
+ this.uploadFilesWithGroupId(uploads, groupId);
4543
+ }
4544
+ else {
4545
+ console.error('❌ [GlobalFileUploader] No group ID received');
4546
+ this.handleUploadError('Failed to generate group ID', uploads[0]?.fileId);
4547
+ }
4548
+ },
4549
+ error: (error) => {
4550
+ console.error('❌ [GlobalFileUploader] Failed to generate group ID:', error);
4551
+ this.handleUploadError('Failed to generate group ID', uploads[0]?.fileId);
4552
+ }
4553
+ });
4588
4554
  }
4589
4555
  /**
4590
- * Handle file input change events
4556
+ * Upload files with group ID
4591
4557
  */
4592
- handleFileInputChange(event) {
4593
- const target = event.target;
4594
- console.log('🔍 [FloatingFileUploader] File input change event detected:', {
4595
- type: target.type,
4596
- filesLength: target.files?.length || 0,
4597
- element: target
4558
+ uploadFilesWithGroupId(uploads, groupId) {
4559
+ let completedCount = 0;
4560
+ let failedCount = 0;
4561
+ const totalFiles = uploads.length;
4562
+ const uploadedFiles = [];
4563
+ uploads.forEach((upload, index) => {
4564
+ // Update status to uploading
4565
+ this.updateUploadStatus(upload.fileId, 'uploading', 0);
4566
+ const uploadRequest = {
4567
+ file: upload.file,
4568
+ userId: this.userId,
4569
+ groupId: groupId,
4570
+ tags: [],
4571
+ permissions: ['read', 'write']
4572
+ };
4573
+ this.fileService.uploadFile(upload.file, this.convertToFileUploadOptions(uploadRequest), (progress) => {
4574
+ // Use the original fileId for progress updates during upload
4575
+ this.updateUploadProgress(upload.fileId, progress);
4576
+ })
4577
+ .pipe(takeUntilDestroyed(this.destroyRef))
4578
+ .subscribe({
4579
+ next: (response) => {
4580
+ completedCount++;
4581
+ console.log(`✅ [GlobalFileUploader] File ${index + 1}/${totalFiles} uploaded successfully`);
4582
+ // Find the upload item by cyfm_temp_unique_id from response
4583
+ const responseFile = response?.data?.core_file_manager?.[0];
4584
+ const tempUniqueId = responseFile?.cyfm_temp_unique_id;
4585
+ console.log('🔍 [GlobalFileUploader] Response tempUniqueId:', tempUniqueId);
4586
+ console.log('🔍 [GlobalFileUploader] Original upload fileId:', upload.fileId);
4587
+ // Use the tempUniqueId to find and update the correct upload item
4588
+ if (tempUniqueId) {
4589
+ this.updateUploadStatusByTempId(tempUniqueId, 'completed', 100, undefined, response);
4590
+ }
4591
+ else {
4592
+ // Fallback to original fileId
4593
+ this.updateUploadStatus(upload.fileId, 'completed', 100, undefined, response);
4594
+ }
4595
+ this.uploadComplete.emit({
4596
+ fileId: tempUniqueId || upload.fileId,
4597
+ fileName: upload.fileName,
4598
+ progress: 100,
4599
+ status: 'completed',
4600
+ uploadedFile: response
4601
+ });
4602
+ if (responseFile) {
4603
+ uploadedFiles.push(responseFile);
4604
+ }
4605
+ console.log('🔍 [GlobalFileUploader] Upload response received:', response);
4606
+ console.log('🔍 [GlobalFileUploader] Response data structure:', response?.data);
4607
+ // Check if all files are completed
4608
+ if (completedCount + failedCount === totalFiles) {
4609
+ this.handleMultipleUploadComplete(completedCount, failedCount, groupId, uploadedFiles);
4610
+ }
4611
+ },
4612
+ error: (error) => {
4613
+ failedCount++;
4614
+ console.error(`❌ [GlobalFileUploader] File ${index + 1}/${totalFiles} upload failed:`, error);
4615
+ this.updateUploadStatus(upload.fileId, 'error', 0, error.message || 'Upload failed');
4616
+ this.uploadError.emit({
4617
+ fileId: upload.fileId,
4618
+ error: error.message || 'Upload failed'
4619
+ });
4620
+ // Check if all files are completed
4621
+ if (completedCount + failedCount === totalFiles) {
4622
+ this.handleMultipleUploadComplete(completedCount, failedCount, groupId, uploadedFiles);
4623
+ }
4624
+ }
4625
+ });
4598
4626
  });
4599
- // Check if this is a file input with files
4600
- if (target.type === 'file' && target.files && target.files.length > 0) {
4601
- console.log('📁 [FloatingFileUploader] File input change detected:', target.files.length, 'files');
4602
- // Check if the input has a data-user-id attribute for user context
4603
- const userId = target.getAttribute('data-user-id');
4604
- if (userId && userId !== this.currentUserId()) {
4605
- this.setCurrentUserId(userId);
4606
- }
4607
- // Handle the files
4608
- this.handleFiles(Array.from(target.files));
4609
- // Reset the input to allow selecting the same files again
4610
- target.value = '';
4611
- }
4612
4627
  }
4613
4628
  /**
4614
- * Handle drag over event
4629
+ * Handle multiple upload completion
4615
4630
  */
4616
- handleDragOver(event) {
4617
- event.preventDefault();
4618
- event.stopPropagation();
4619
- // Show floating uploader when files are dragged over
4620
- if (event.dataTransfer?.types.includes('Files')) {
4621
- this.showWithAnimation();
4631
+ handleMultipleUploadComplete(completed, failed, groupId, uploadedFiles) {
4632
+ console.log(`📊 [GlobalFileUploader] Multiple upload complete: ${completed}/${completed + failed} successful`);
4633
+ this.isUploading.set(false);
4634
+ if (completed > 0) {
4635
+ this.allUploadsComplete.emit({
4636
+ groupId: groupId,
4637
+ uploadedFiles: uploadedFiles
4638
+ });
4622
4639
  }
4623
4640
  }
4624
4641
  /**
4625
- * Handle drag leave event
4642
+ * Upload next file in queue (single file mode)
4626
4643
  */
4627
- handleDragLeave(event) {
4628
- event.preventDefault();
4629
- event.stopPropagation();
4630
- // Only hide if leaving the entire document
4631
- if (!event.relatedTarget || event.relatedTarget === document.body) {
4632
- this.updateVisibility();
4644
+ uploadNextFile() {
4645
+ const pendingUpload = this.uploadQueue().find(upload => upload.status === 'pending');
4646
+ if (!pendingUpload) {
4647
+ this.isUploading.set(false);
4648
+ return;
4633
4649
  }
4650
+ // Update status to uploading
4651
+ this.updateUploadStatus(pendingUpload.fileId, 'uploading', 0);
4652
+ const uploadRequest = {
4653
+ file: pendingUpload.file,
4654
+ userId: this.userId,
4655
+ tags: [],
4656
+ permissions: ['read', 'write']
4657
+ };
4658
+ this.fileService.uploadFile(pendingUpload.file, this.convertToFileUploadOptions(uploadRequest), (progress) => {
4659
+ this.updateUploadProgress(pendingUpload.fileId, progress);
4660
+ })
4661
+ .pipe(takeUntilDestroyed(this.destroyRef))
4662
+ .subscribe({
4663
+ next: (response) => {
4664
+ console.log('✅ [GlobalFileUploader] Single file upload successful:', response);
4665
+ // Find the upload item by cyfm_temp_unique_id from response
4666
+ const responseFile = response?.data?.core_file_manager?.[0];
4667
+ const tempUniqueId = responseFile?.cyfm_temp_unique_id;
4668
+ console.log('🔍 [GlobalFileUploader] Single upload - Response tempUniqueId:', tempUniqueId);
4669
+ console.log('🔍 [GlobalFileUploader] Single upload - Original upload fileId:', pendingUpload.fileId);
4670
+ // Use the tempUniqueId to find and update the correct upload item
4671
+ if (tempUniqueId) {
4672
+ this.updateUploadStatusByTempId(tempUniqueId, 'completed', 100, undefined, response);
4673
+ }
4674
+ else {
4675
+ // Fallback to original fileId
4676
+ this.updateUploadStatus(pendingUpload.fileId, 'completed', 100, undefined, response);
4677
+ }
4678
+ this.uploadComplete.emit({
4679
+ fileId: tempUniqueId || pendingUpload.fileId,
4680
+ fileName: pendingUpload.fileName,
4681
+ progress: 100,
4682
+ status: 'completed',
4683
+ uploadedFile: response
4684
+ });
4685
+ // Process next file
4686
+ setTimeout(() => this.uploadNextFile(), 500);
4687
+ },
4688
+ error: (error) => {
4689
+ console.error('❌ [GlobalFileUploader] Upload error:', error);
4690
+ this.updateUploadStatus(pendingUpload.fileId, 'error', 0, error.message || 'Upload failed');
4691
+ this.uploadError.emit({
4692
+ fileId: pendingUpload.fileId,
4693
+ error: error.message || 'Upload failed'
4694
+ });
4695
+ // Process next file
4696
+ setTimeout(() => this.uploadNextFile(), 500);
4697
+ }
4698
+ });
4634
4699
  }
4635
4700
  /**
4636
- * Handle drop event
4701
+ * Update upload progress
4637
4702
  */
4638
- handleDrop(event) {
4639
- event.preventDefault();
4640
- event.stopPropagation();
4641
- const files = event.dataTransfer?.files;
4642
- if (files && files.length > 0) {
4643
- this.handleFiles(Array.from(files));
4644
- }
4703
+ updateUploadProgress(fileId, progress) {
4704
+ this.uploadQueue.update(queue => queue.map(upload => upload.fileId === fileId
4705
+ ? { ...upload, progress }
4706
+ : upload));
4645
4707
  }
4646
4708
  /**
4647
- * Handle files from drag and drop or file input
4709
+ * Update upload status
4648
4710
  */
4649
- handleFiles(files) {
4650
- console.log('📁 [FloatingFileUploader] Handling files:', files.length);
4651
- // Use handleExternalFiles to process the files
4652
- this.handleExternalFiles(files, this.currentUserId());
4711
+ updateUploadStatus(fileId, status, progress, error, uploadedFile) {
4712
+ console.log('🔄 [GlobalFileUploader] Updating upload status:', { fileId, status, progress, error: !!error, uploadedFile: !!uploadedFile });
4713
+ this.uploadQueue.update(queue => {
4714
+ const updatedQueue = queue.map(upload => {
4715
+ if (upload.fileId === fileId) {
4716
+ console.log('🔄 [GlobalFileUploader] Found upload to update:', upload.fileName, 'from', upload.status, 'to', status);
4717
+ return { ...upload, status, progress, error, uploadedFile };
4718
+ }
4719
+ return upload;
4720
+ });
4721
+ console.log('🔄 [GlobalFileUploader] Updated queue:', updatedQueue.map(u => ({ fileName: u.fileName, status: u.status })));
4722
+ return updatedQueue;
4723
+ });
4653
4724
  }
4654
4725
  /**
4655
- * Update visibility - simplified for notification only
4726
+ * Update upload status by cyfm_temp_unique_id
4656
4727
  */
4657
- updateVisibility() {
4658
- // This is just a notification component now
4659
- // The actual uploads are handled by the global uploader
4728
+ updateUploadStatusByTempId(tempUniqueId, status, progress, error, uploadedFile) {
4729
+ console.log('🔄 [GlobalFileUploader] Updating upload status by tempId:', { tempUniqueId, status, progress, error: !!error, uploadedFile: !!uploadedFile });
4730
+ this.uploadQueue.update(queue => {
4731
+ const updatedQueue = queue.map(upload => {
4732
+ // Check if this upload's fileId matches the tempUniqueId
4733
+ if (upload.fileId === tempUniqueId) {
4734
+ console.log('🔄 [GlobalFileUploader] Found upload by tempId to update:', upload.fileName, 'from', upload.status, 'to', status);
4735
+ return { ...upload, status, progress, error, uploadedFile };
4736
+ }
4737
+ return upload;
4738
+ });
4739
+ console.log('🔄 [GlobalFileUploader] Updated queue by tempId:', updatedQueue.map(u => ({ fileName: u.fileName, status: u.status, fileId: u.fileId })));
4740
+ return updatedQueue;
4741
+ });
4660
4742
  }
4661
4743
  /**
4662
- * Show with animation
4744
+ * Handle upload error
4663
4745
  */
4664
- showWithAnimation() {
4665
- console.log('🎬 [FloatingFileUploader] showWithAnimation called - setting isVisible to true');
4666
- this.isAnimating.set(true);
4667
- this.isVisible.set(true);
4668
- // Remove animation class after animation completes
4669
- setTimeout(() => {
4670
- this.isAnimating.set(false);
4671
- console.log('🎬 [FloatingFileUploader] Animation completed, isVisible:', this.isVisible());
4672
- }, 300);
4746
+ handleUploadError(error, fileId) {
4747
+ if (fileId) {
4748
+ this.updateUploadStatus(fileId, 'error', 0, error);
4749
+ this.uploadError.emit({ fileId, error });
4750
+ }
4751
+ this.isUploading.set(false);
4673
4752
  }
4674
4753
  /**
4675
- * Hide with animation
4754
+ * Cancel upload
4676
4755
  */
4677
- hideWithAnimation() {
4678
- this.isAnimating.set(true);
4679
- // Wait for animation to complete before hiding
4756
+ cancelUpload(fileId) {
4757
+ this.updateUploadStatus(fileId, 'cancelled', 0);
4758
+ this.uploadCancelled.emit(fileId);
4759
+ // Remove from queue after a delay
4680
4760
  setTimeout(() => {
4681
- this.isVisible.set(false);
4682
- this.isAnimating.set(false);
4683
- }, 300);
4761
+ this.uploadQueue.update(queue => queue.filter(upload => upload.fileId !== fileId));
4762
+ }, 1000);
4684
4763
  }
4685
4764
  /**
4686
- * Toggle minimize state
4765
+ * Retry upload
4687
4766
  */
4688
- toggleMinimize() {
4689
- this.isMinimized.set(!this.isMinimized());
4767
+ retryUpload(fileId) {
4768
+ this.updateUploadStatus(fileId, 'pending', 0);
4769
+ this.processUploadQueue();
4690
4770
  }
4691
4771
  /**
4692
- * Close the floating uploader
4772
+ * Remove upload from queue
4693
4773
  */
4694
- close() {
4695
- this.hideWithAnimation();
4774
+ removeUpload(fileId) {
4775
+ this.uploadQueue.update(queue => queue.filter(upload => upload.fileId !== fileId));
4696
4776
  }
4697
4777
  /**
4698
- * Get upload summary text
4778
+ * Clear completed uploads
4699
4779
  */
4700
- getUploadSummary() {
4701
- const active = this.activeUploads().length;
4702
- const completed = this.completedUploads().length;
4703
- const failed = this.failedUploads().length;
4704
- const pending = this.pendingUploads().length;
4705
- if (active > 0) {
4706
- return `${active} uploading`;
4707
- }
4708
- else if (completed > 0 && failed === 0) {
4709
- return `${completed} completed`;
4710
- }
4711
- else if (failed > 0) {
4712
- return `${completed} completed, ${failed} failed`;
4713
- }
4714
- else if (pending > 0) {
4715
- return `${pending} pending`;
4716
- }
4717
- return 'No uploads';
4780
+ clearCompleted() {
4781
+ this.uploadQueue.update(queue => queue.filter(upload => upload.status !== 'completed'));
4718
4782
  }
4719
4783
  /**
4720
- * Get overall progress percentage
4784
+ * Clear all uploads
4721
4785
  */
4722
- getOverallProgress() {
4723
- const uploads = this.uploadQueue();
4724
- if (uploads.length === 0)
4725
- return 0;
4726
- const totalProgress = uploads.reduce((sum, upload) => sum + upload.progress, 0);
4727
- return Math.round(totalProgress / uploads.length);
4786
+ clearAll() {
4787
+ this.uploadQueue.set([]);
4788
+ this.currentGroupId.set(null);
4728
4789
  }
4729
4790
  /**
4730
4791
  * Get status icon
@@ -4753,110 +4814,182 @@ class CideEleFloatingFileUploaderComponent {
4753
4814
  }
4754
4815
  }
4755
4816
  /**
4756
- * Cancel upload
4757
- */
4758
- cancelUpload(fileId) {
4759
- console.log('🚫 [FloatingFileUploader] Cancelling upload:', fileId);
4760
- this.fileManagerService.cancelUpload(fileId);
4761
- this.updateUploadStatus(fileId, 'cancelled', 0);
4762
- }
4763
- /**
4764
- * Retry upload
4765
- */
4766
- retryUpload(fileId) {
4767
- console.log('🔄 [FloatingFileUploader] Retrying upload:', fileId);
4768
- this.updateUploadStatus(fileId, 'pending', 0);
4769
- // Note: Actual retry logic would need to be implemented based on stored file data
4770
- }
4771
- /**
4772
- * Set current user ID
4773
- */
4774
- setCurrentUserId(userId) {
4775
- this.currentUserId.set(userId);
4776
- this.fileManagerService.setUserId(userId);
4777
- }
4778
- /**
4779
- * Public method to handle files from external sources
4780
- * This can be called by other components to trigger the floating uploader
4817
+ * Get file size display
4781
4818
  */
4782
- handleExternalFiles(files, userId) {
4783
- console.log('📁 [FloatingFileUploader] External files received:', files.length, 'files');
4784
- // Set user ID if provided
4785
- if (userId && userId !== this.currentUserId()) {
4786
- this.setCurrentUserId(userId);
4787
- }
4788
- // Add files to the local upload queue
4789
- // This will trigger the effect in the constructor to show the floating uploader
4790
- files.forEach((file, index) => {
4791
- const fileId = this.generateFileId(file);
4792
- // Create upload queue item
4793
- const uploadItem = {
4794
- fileId,
4795
- fileName: file.name,
4796
- progress: 0,
4797
- status: 'pending',
4798
- file: file
4799
- };
4800
- console.log(`📁 [FloatingFileUploader] Adding file ${index + 1}/${files.length} to upload queue:`, {
4801
- fileId,
4802
- fileName: file.name,
4803
- status: 'pending'
4804
- });
4805
- // Add to local upload queue
4806
- this.uploadQueue.update(queue => [...queue, uploadItem]);
4807
- // Start upload using file manager service
4808
- this.fileManagerService.uploadFile(file, {
4809
- userId: this.currentUserId(),
4810
- permissions: ['read', 'write'],
4811
- tags: []
4812
- }, (progress) => {
4813
- this.updateUploadProgress(fileId, progress);
4814
- })
4815
- .pipe(takeUntilDestroyed(this.destroyRef))
4816
- .subscribe({
4817
- next: (response) => {
4818
- console.log('✅ [FloatingFileUploader] Upload completed:', response);
4819
- this.updateUploadStatus(fileId, 'completed', 100, undefined, response);
4820
- },
4821
- error: (error) => {
4822
- console.error('❌ [FloatingFileUploader] Upload failed:', error);
4823
- this.updateUploadStatus(fileId, 'error', 0, error.message || 'Upload failed');
4824
- }
4825
- });
4826
- });
4819
+ getFileSizeDisplay(file) {
4820
+ const bytes = file.size;
4821
+ if (bytes === 0)
4822
+ return '0 Bytes';
4823
+ const k = 1024;
4824
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
4825
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
4826
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
4827
4827
  }
4828
4828
  /**
4829
- * Generate unique file ID
4829
+ * Generate unique file ID - this should match the service's generateFileId method
4830
+ * The service uses: ${file.name}_${file.size}_${Date.now()}
4830
4831
  */
4831
4832
  generateFileId(file) {
4832
4833
  return `${file.name}_${file.size}_${Date.now()}`;
4833
4834
  }
4834
4835
  /**
4835
- * Update upload progress
4836
+ * Convert IFileUploadRequest to FileUploadOptions
4836
4837
  */
4837
- updateUploadProgress(fileId, progress) {
4838
- this.uploadQueue.update(queue => queue.map(upload => upload.fileId === fileId
4839
- ? { ...upload, progress, status: 'uploading' }
4840
- : upload));
4838
+ convertToFileUploadOptions(request) {
4839
+ return {
4840
+ altText: request.altText,
4841
+ userId: request.userId,
4842
+ permissions: request.permissions,
4843
+ tags: request.tags,
4844
+ groupId: request.groupId
4845
+ };
4841
4846
  }
4842
4847
  /**
4843
- * Update upload status
4848
+ * Trigger file input click
4844
4849
  */
4845
- updateUploadStatus(fileId, status, progress, error, uploadedFile) {
4846
- this.uploadQueue.update(queue => queue.map(upload => upload.fileId === fileId
4847
- ? { ...upload, status, progress, error, uploadedFile }
4848
- : upload));
4850
+ triggerFileInput() {
4851
+ const fileInput = document.getElementById('global-file-input');
4852
+ if (fileInput) {
4853
+ fileInput.click();
4854
+ }
4849
4855
  }
4850
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4851
- 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) {\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 -->\r\n @if (uploadQueue().length > 0) {\r\n <div class=\"upload-queue\">\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=\"xs\">{{ getStatusIcon(upload.status) }}</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ upload.fileName }}</div>\r\n <div class=\"file-status\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n }\r\n @case ('uploading') {\r\n <span class=\"text-blue-600\">Uploading...</span>\r\n }\r\n @case ('completed') {\r\n <span class=\"text-green-600\">Completed</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">{{ upload.error || '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 </div>\r\n </div>\r\n\r\n <!-- Progress Bar (for uploading files) -->\r\n @if (upload.status === 'uploading') {\r\n <div class=\"file-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"upload.progress\"></div>\r\n </div>\r\n <span class=\"progress-text\">{{ upload.progress }}%</span>\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 class=\"action-btn cancel-btn\" (click)=\"cancelUpload(upload.fileId)\" 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(upload.fileId)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('completed') {\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\" (click)=\"retryUpload(upload.fileId)\" 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"] }] });
4856
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleGlobalFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4857
+ 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"] }] });
4852
4858
  }
4853
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, decorators: [{
4859
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleGlobalFileUploaderComponent, decorators: [{
4854
4860
  type: Component,
4855
- args: [{ selector: 'cide-ele-floating-file-uploader', standalone: true, imports: [
4861
+ args: [{ selector: 'cide-ele-global-file-uploader', standalone: true, imports: [
4856
4862
  CommonModule,
4863
+ CideEleButtonComponent,
4857
4864
  CideIconComponent
4858
- ], 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) {\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 -->\r\n @if (uploadQueue().length > 0) {\r\n <div class=\"upload-queue\">\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=\"xs\">{{ getStatusIcon(upload.status) }}</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ upload.fileName }}</div>\r\n <div class=\"file-status\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n }\r\n @case ('uploading') {\r\n <span class=\"text-blue-600\">Uploading...</span>\r\n }\r\n @case ('completed') {\r\n <span class=\"text-green-600\">Completed</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">{{ upload.error || '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 </div>\r\n </div>\r\n\r\n <!-- Progress Bar (for uploading files) -->\r\n @if (upload.status === 'uploading') {\r\n <div class=\"file-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"upload.progress\"></div>\r\n </div>\r\n <span class=\"progress-text\">{{ upload.progress }}%</span>\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 class=\"action-btn cancel-btn\" (click)=\"cancelUpload(upload.fileId)\" 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(upload.fileId)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('completed') {\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\" (click)=\"retryUpload(upload.fileId)\" 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"] }]
4859
- }], ctorParameters: () => [] });
4865
+ ], 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"] }]
4866
+ }], ctorParameters: () => [], propDecorators: { userId: [{
4867
+ type: Input
4868
+ }], multiple: [{
4869
+ type: Input
4870
+ }], accept: [{
4871
+ type: Input
4872
+ }], maxFileSize: [{
4873
+ type: Input
4874
+ }], allowedTypes: [{
4875
+ type: Input
4876
+ }], uploadComplete: [{
4877
+ type: Output
4878
+ }], uploadError: [{
4879
+ type: Output
4880
+ }], uploadCancelled: [{
4881
+ type: Output
4882
+ }], allUploadsComplete: [{
4883
+ type: Output
4884
+ }] } });
4885
+
4886
+ class CideFloatingUploadTriggerDirective {
4887
+ elementRef = inject(ElementRef);
4888
+ floatingUploader = inject(CideEleFloatingFileUploaderComponent);
4889
+ groupId;
4890
+ userId;
4891
+ showIcon = true;
4892
+ filesSelected = new EventEmitter();
4893
+ triggerIcon;
4894
+ ngOnInit() {
4895
+ this.setupTrigger();
4896
+ }
4897
+ ngOnDestroy() {
4898
+ this.removeTrigger();
4899
+ }
4900
+ setupTrigger() {
4901
+ const element = this.elementRef.nativeElement;
4902
+ if (element.type !== 'file') {
4903
+ console.warn('⚠️ [FloatingUploadTrigger] Directive should only be used on file input elements');
4904
+ return;
4905
+ }
4906
+ // Add change event listener
4907
+ element.addEventListener('change', this.onFileChange.bind(this));
4908
+ // Add floating uploader trigger icon if enabled
4909
+ if (this.showIcon) {
4910
+ this.addTriggerIcon();
4911
+ }
4912
+ }
4913
+ removeTrigger() {
4914
+ const element = this.elementRef.nativeElement;
4915
+ element.removeEventListener('change', this.onFileChange.bind(this));
4916
+ if (this.triggerIcon) {
4917
+ this.triggerIcon.remove();
4918
+ }
4919
+ }
4920
+ onFileChange(event) {
4921
+ const target = event.target;
4922
+ const files = target.files;
4923
+ if (files && files.length > 0) {
4924
+ console.log('📁 [FloatingUploadTrigger] Files selected:', files.length);
4925
+ // Show floating uploader
4926
+ this.floatingUploader.showUploader(this.groupId);
4927
+ // Handle files through floating uploader
4928
+ this.floatingUploader.handleExternalFiles(Array.from(files), this.userId, this.groupId);
4929
+ // Emit files selected event
4930
+ this.filesSelected.emit(Array.from(files));
4931
+ }
4932
+ }
4933
+ addTriggerIcon() {
4934
+ const element = this.elementRef.nativeElement;
4935
+ const container = element.parentElement;
4936
+ if (!container)
4937
+ return;
4938
+ // Create trigger icon
4939
+ this.triggerIcon = document.createElement('button');
4940
+ this.triggerIcon.type = 'button';
4941
+ this.triggerIcon.className = 'floating-upload-trigger-icon';
4942
+ this.triggerIcon.innerHTML = `
4943
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
4944
+ <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
4945
+ </svg>
4946
+ `;
4947
+ this.triggerIcon.title = 'View upload progress';
4948
+ this.triggerIcon.style.cssText = `
4949
+ position: absolute;
4950
+ right: 8px;
4951
+ top: 50%;
4952
+ transform: translateY(-50%);
4953
+ background: #3b82f6;
4954
+ color: white;
4955
+ border: none;
4956
+ border-radius: 4px;
4957
+ padding: 4px;
4958
+ cursor: pointer;
4959
+ display: flex;
4960
+ align-items: center;
4961
+ justify-content: center;
4962
+ z-index: 10;
4963
+ `;
4964
+ // Make container relative positioned
4965
+ container.style.position = 'relative';
4966
+ // Add click handler to show floating uploader
4967
+ this.triggerIcon.addEventListener('click', (e) => {
4968
+ e.preventDefault();
4969
+ e.stopPropagation();
4970
+ this.floatingUploader.showUploader(this.groupId);
4971
+ });
4972
+ // Insert icon after the input
4973
+ element.parentNode?.insertBefore(this.triggerIcon, element.nextSibling);
4974
+ }
4975
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideFloatingUploadTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4976
+ 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 });
4977
+ }
4978
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideFloatingUploadTriggerDirective, decorators: [{
4979
+ type: Directive,
4980
+ args: [{
4981
+ selector: '[cideFloatingUploadTrigger]',
4982
+ standalone: true
4983
+ }]
4984
+ }], propDecorators: { groupId: [{
4985
+ type: Input
4986
+ }], userId: [{
4987
+ type: Input
4988
+ }], showIcon: [{
4989
+ type: Input
4990
+ }], filesSelected: [{
4991
+ type: Output
4992
+ }] } });
4860
4993
 
4861
4994
  class CideTextareaComponent {
4862
4995
  label = '';
@@ -9096,5 +9229,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
9096
9229
  * Generated bundle index. Do not edit.
9097
9230
  */
9098
9231
 
9099
- 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 };
9232
+ 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 };
9100
9233
  //# sourceMappingURL=cloud-ide-element.mjs.map