cloud-ide-element 1.0.107 → 1.0.110

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, effect, afterRenderEffect, afterNextRender, ElementRef, Directive, viewChild } from '@angular/core';
4
+ import { Pipe, Injectable, inject, EventEmitter, ViewContainerRef, forwardRef, ViewChild, Output, Input, Component, HostListener, ContentChildren, signal, DestroyRef, computed, effect, Directive, ElementRef, 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, map, of } from 'rxjs';
@@ -2633,6 +2633,7 @@ class CideEleFileManagerService {
2633
2633
  _uploadQueue = signal([], ...(ngDevMode ? [{ debugName: "_uploadQueue" }] : []));
2634
2634
  _activeUploads = signal(new Map(), ...(ngDevMode ? [{ debugName: "_activeUploads" }] : []));
2635
2635
  _fetchedFiles = signal(new Map(), ...(ngDevMode ? [{ debugName: "_fetchedFiles" }] : [])); // Group ID -> Files mapping
2636
+ _fileIdMapping = signal(new Map(), ...(ngDevMode ? [{ debugName: "_fileIdMapping" }] : [])); // Temp ID -> Actual ID mapping
2636
2637
  _error = signal(null, ...(ngDevMode ? [{ debugName: "_error" }] : []));
2637
2638
  // Angular 20: Computed values
2638
2639
  baseUrl = this._baseUrl.asReadonly();
@@ -2650,6 +2651,21 @@ class CideEleFileManagerService {
2650
2651
  this._fetchedFiles().forEach(files => total += files.length);
2651
2652
  return total;
2652
2653
  }, ...(ngDevMode ? [{ debugName: "totalFetchedFiles" }] : []));
2654
+ // Optimized computed properties for file counting by group
2655
+ getFileCountByGroup = computed(() => {
2656
+ const countMap = new Map();
2657
+ // Count active uploads by group
2658
+ this._activeUploads().forEach((upload, fileId) => {
2659
+ if (upload.groupId) {
2660
+ countMap.set(upload.groupId, (countMap.get(upload.groupId) || 0) + 1);
2661
+ }
2662
+ });
2663
+ // Add fetched files by group
2664
+ this._fetchedFiles().forEach((files, groupId) => {
2665
+ countMap.set(groupId, (countMap.get(groupId) || 0) + files.length);
2666
+ });
2667
+ return countMap;
2668
+ }, ...(ngDevMode ? [{ debugName: "getFileCountByGroup" }] : []));
2653
2669
  serviceState = computed(() => ({
2654
2670
  isUploading: this._isUploading(),
2655
2671
  uploadQueue: this._uploadQueue(),
@@ -2658,7 +2674,7 @@ class CideEleFileManagerService {
2658
2674
  error: this._error()
2659
2675
  }), ...(ngDevMode ? [{ debugName: "serviceState" }] : []));
2660
2676
  constructor() {
2661
- console.log('🚀 [FileManagerService] Angular 20 service initialized');
2677
+ // Service initialized
2662
2678
  }
2663
2679
  /**
2664
2680
  * Upload a file with base64 encoding and progress tracking
@@ -2675,9 +2691,7 @@ class CideEleFileManagerService {
2675
2691
  this._isUploading.set(true);
2676
2692
  this._error.set(null);
2677
2693
  this.addToUploadQueue(fileId);
2678
- console.log('📤 [FileManagerService] Starting upload for:', file.name, 'Size:', file.size, 'bytes');
2679
- console.log('📤 [FileManagerService] Upload options:', uploadOptions);
2680
- console.log('📤 [FileManagerService] File ID:', fileId, 'Group ID:', uploadOptions?.groupId);
2694
+ // Upload started for file: ${file.name}
2681
2695
  return new Observable(observer => {
2682
2696
  const reader = new FileReader();
2683
2697
  // Angular 20: Enhanced progress tracking with signals
@@ -2767,13 +2781,18 @@ class CideEleFileManagerService {
2767
2781
  percentage: 100,
2768
2782
  stage: 'complete'
2769
2783
  }, uploadOptions?.groupId);
2770
- console.log('✅ [FileManagerService] Upload successful:', event.body);
2771
2784
  // Handle the CoreFileManagerInsertUpdateResponse
2772
2785
  const response = event.body;
2773
- console.log('🔍 [FileManagerService] Upload response received:', response);
2774
- console.log('🔍 [FileManagerService] Response success:', response.success);
2775
- console.log('🔍 [FileManagerService] Response data:', response.data);
2776
2786
  if (response.success) {
2787
+ // Map temp file ID to actual file ID from response
2788
+ const fileData = response.data?.core_file_manager?.[0];
2789
+ const actualFileId = fileData?.cyfm_id;
2790
+ if (actualFileId && actualFileId !== fileId) {
2791
+ // Store the mapping
2792
+ const currentMapping = new Map(this._fileIdMapping());
2793
+ currentMapping.set(fileId, actualFileId);
2794
+ this._fileIdMapping.set(currentMapping);
2795
+ }
2777
2796
  observer.next(response);
2778
2797
  observer.complete();
2779
2798
  }
@@ -2870,21 +2889,8 @@ class CideEleFileManagerService {
2870
2889
  updateUploadProgress(fileId, progress, groupId) {
2871
2890
  const currentUploads = new Map(this._activeUploads());
2872
2891
  const progressWithGroupId = { ...progress, groupId };
2873
- console.log('📊 [FileManagerService] Updating upload progress:', {
2874
- fileId,
2875
- stage: progress.stage,
2876
- percentage: progress.percentage,
2877
- groupId: groupId,
2878
- progressWithGroupId
2879
- });
2880
2892
  currentUploads.set(fileId, progressWithGroupId);
2881
2893
  this._activeUploads.set(currentUploads);
2882
- // Debug: Show current active uploads with their group IDs
2883
- console.log('📊 [FileManagerService] Current active uploads:', Array.from(currentUploads.entries()).map(([id, upload]) => ({
2884
- fileId: id,
2885
- stage: upload.stage,
2886
- groupId: upload.groupId
2887
- })));
2888
2894
  }
2889
2895
  removeActiveUpload(fileId) {
2890
2896
  const currentUploads = new Map(this._activeUploads());
@@ -2981,16 +2987,21 @@ class CideEleFileManagerService {
2981
2987
  * Signal to trigger floating uploader visibility
2982
2988
  */
2983
2989
  _showFloatingUploader = signal(false, ...(ngDevMode ? [{ debugName: "_showFloatingUploader" }] : []));
2990
+ _triggerGroupId = signal(null, ...(ngDevMode ? [{ debugName: "_triggerGroupId" }] : []));
2984
2991
  showFloatingUploader = this._showFloatingUploader.asReadonly();
2992
+ getTriggerGroupId = this._triggerGroupId.asReadonly();
2985
2993
  /**
2986
- * Trigger floating uploader to show
2994
+ * Trigger floating uploader to show with group ID
2995
+ * This is the ONLY way to pass group ID to floating uploader
2987
2996
  */
2988
- triggerFloatingUploaderShow() {
2989
- console.log('🎬 [FileManagerService] Triggering floating uploader to show groupId');
2997
+ triggerFloatingUploaderShow(groupId) {
2998
+ console.log('🎬 [FileManagerService] Triggering floating uploader to show with groupId:', groupId);
2999
+ this._triggerGroupId.set(groupId || null);
2990
3000
  this._showFloatingUploader.set(true);
2991
3001
  // Reset after a short delay to allow components to react
2992
3002
  setTimeout(() => {
2993
3003
  this._showFloatingUploader.set(false);
3004
+ this._triggerGroupId.set(null);
2994
3005
  }, 100);
2995
3006
  }
2996
3007
  /**
@@ -3000,39 +3011,46 @@ class CideEleFileManagerService {
3000
3011
  */
3001
3012
  getAllFilesForGroup(groupId) {
3002
3013
  const files = [];
3003
- const fileIds = new Set(); // Track file IDs to prevent duplicates
3014
+ const processedIds = new Set(); // Track all processed IDs (temp and actual)
3015
+ const fileNames = new Set(); // Track file names to prevent duplicates by name
3016
+ const fileIdMapping = this._fileIdMapping();
3004
3017
  // Add active uploads for this group
3005
- this._activeUploads().forEach((upload, fileId) => {
3006
- if (upload.groupId === groupId && !fileIds.has(fileId)) {
3007
- fileIds.add(fileId);
3008
- files.push({
3009
- fileId,
3010
- fileName: this.getFileNameFromId(fileId),
3011
- stage: upload.stage,
3012
- percentage: upload.percentage
3013
- });
3018
+ this._activeUploads().forEach((upload, tempFileId) => {
3019
+ if (upload.groupId === groupId) {
3020
+ const fileName = this.getFileNameFromId(tempFileId);
3021
+ const actualFileId = fileIdMapping.get(tempFileId) || tempFileId;
3022
+ // Check if we've already processed this file (by actual ID or name)
3023
+ if (!processedIds.has(actualFileId) && !fileNames.has(fileName)) {
3024
+ processedIds.add(tempFileId); // Track temp ID
3025
+ processedIds.add(actualFileId); // Track actual ID
3026
+ fileNames.add(fileName);
3027
+ files.push({
3028
+ fileId: tempFileId, // Keep temp ID for UI consistency
3029
+ fileName,
3030
+ stage: upload.stage,
3031
+ percentage: upload.percentage
3032
+ });
3033
+ }
3014
3034
  }
3015
3035
  });
3016
- // Add fetched files for this group (only if not already in active uploads)
3036
+ // Add fetched files for this group (only if not already processed)
3017
3037
  const fetchedFiles = this.getFetchedFilesByGroupId(groupId);
3018
3038
  fetchedFiles.forEach(file => {
3019
3039
  const fetchedFileId = file._id;
3020
- if (!fileIds.has(fetchedFileId)) {
3021
- fileIds.add(fetchedFileId);
3040
+ const fetchedFileName = file.cyfm_name || 'Unknown file';
3041
+ // Check if this file was already processed (either as temp or actual ID)
3042
+ const isAlreadyProcessed = processedIds.has(fetchedFileId) || fileNames.has(fetchedFileName);
3043
+ if (!isAlreadyProcessed) {
3044
+ processedIds.add(fetchedFileId);
3045
+ fileNames.add(fetchedFileName);
3022
3046
  files.push({
3023
3047
  fileId: fetchedFileId,
3024
- fileName: file.cyfm_name || 'Unknown file',
3048
+ fileName: fetchedFileName,
3025
3049
  stage: 'complete' // Fetched files are already completed
3026
3050
  });
3027
3051
  }
3028
3052
  });
3029
- console.log('📊 [FileManagerService] getAllFilesForGroup result:', {
3030
- groupId,
3031
- totalFiles: files.length,
3032
- activeUploads: Array.from(this._activeUploads().entries()).filter(([_, upload]) => upload.groupId === groupId).length,
3033
- fetchedFiles: fetchedFiles.length,
3034
- uniqueFileIds: Array.from(fileIds)
3035
- });
3053
+ // Smart deduplication completed for group: ${groupId}
3036
3054
  return files;
3037
3055
  }
3038
3056
  /**
@@ -3070,14 +3088,47 @@ class CideEleFileManagerService {
3070
3088
  if (upload.stage === 'complete') {
3071
3089
  currentUploads.delete(fileId);
3072
3090
  hasChanges = true;
3073
- console.log('🧹 [FileManagerService] Clearing completed upload:', fileId);
3091
+ // Cleared completed upload
3074
3092
  }
3075
3093
  });
3076
3094
  if (hasChanges) {
3077
3095
  this._activeUploads.set(currentUploads);
3078
- console.log('🧹 [FileManagerService] Cleared all completed uploads');
3096
+ // Cleared all completed uploads
3097
+ }
3098
+ }
3099
+ /**
3100
+ * Remove a specific completed upload from active uploads
3101
+ */
3102
+ removeCompletedUpload(fileId) {
3103
+ const currentUploads = new Map(this._activeUploads());
3104
+ const upload = currentUploads.get(fileId);
3105
+ if (upload && upload.stage === 'complete') {
3106
+ currentUploads.delete(fileId);
3107
+ this._activeUploads.set(currentUploads);
3108
+ // Also clean up the file ID mapping for this temp ID
3109
+ const currentMapping = new Map(this._fileIdMapping());
3110
+ if (currentMapping.has(fileId)) {
3111
+ currentMapping.delete(fileId);
3112
+ this._fileIdMapping.set(currentMapping);
3113
+ // Cleaned up mapping
3114
+ }
3115
+ // Removed completed upload
3079
3116
  }
3080
3117
  }
3118
+ /**
3119
+ * Optimized method to get file count for a specific group
3120
+ * Uses computed property for better performance
3121
+ */
3122
+ getFileCountForGroup(groupId) {
3123
+ return this.getFileCountByGroup().get(groupId) || 0;
3124
+ }
3125
+ /**
3126
+ * Optimized method to check if group has active uploads
3127
+ */
3128
+ hasActiveUploadsForGroup(groupId) {
3129
+ return Array.from(this._activeUploads().values())
3130
+ .some(upload => upload.groupId === groupId && upload.stage !== 'complete');
3131
+ }
3081
3132
  /**
3082
3133
  * Angular 20: File validation utility
3083
3134
  */
@@ -3322,1877 +3373,1620 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
3322
3373
  }]
3323
3374
  }], ctorParameters: () => [] });
3324
3375
 
3325
- class CideEleFloatingFileUploaderComponent {
3326
- destroyRef = inject(DestroyRef);
3376
+ class CideEleFileInputComponent {
3327
3377
  fileManagerService = inject(CideEleFileManagerService);
3328
- // Signals for reactive state
3329
- isVisible = signal(false, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
3330
- isMinimized = signal(false, ...(ngDevMode ? [{ debugName: "isMinimized" }] : []));
3331
- currentUserId = signal('', ...(ngDevMode ? [{ debugName: "currentUserId" }] : []));
3332
- currentGroupId = signal(null, ...(ngDevMode ? [{ debugName: "currentGroupId" }] : []));
3333
- // Use file manager service as the single source of truth
3334
- uploadQueue = computed(() => this.fileManagerService.uploadQueue(), ...(ngDevMode ? [{ debugName: "uploadQueue" }] : []));
3335
- activeUploads = computed(() => this.fileManagerService.activeUploads(), ...(ngDevMode ? [{ debugName: "activeUploads" }] : []));
3336
- // Computed values based on service state
3337
- hasUploads = computed(() => this.uploadQueue().length > 0 || this.activeUploads().size > 0, ...(ngDevMode ? [{ debugName: "hasUploads" }] : []));
3338
- hasActiveUploads = computed(() => this.uploadQueue().length > 0 || Array.from(this.activeUploads().values()).some(upload => upload.stage !== 'complete'), ...(ngDevMode ? [{ debugName: "hasActiveUploads" }] : []));
3339
- pendingUploads = computed(() => this.uploadQueue().filter(fileId => !this.activeUploads().has(fileId)), ...(ngDevMode ? [{ debugName: "pendingUploads" }] : []));
3340
- activeUploadsLocal = computed(() => Array.from(this.activeUploads().values()).filter(upload => upload.stage === 'reading' || upload.stage === 'uploading'), ...(ngDevMode ? [{ debugName: "activeUploadsLocal" }] : []));
3341
- completedUploads = computed(() => Array.from(this.activeUploads().values()).filter(upload => upload.stage === 'complete'), ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
3342
- failedUploads = computed(() => Array.from(this.activeUploads().values()).filter(upload => upload.stage === 'error'), ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
3343
- // Get all files for the current group (computed property for reactivity)
3344
- allFilesForGroup = computed(() => {
3345
- const groupId = this.currentGroupId();
3346
- if (!groupId) {
3347
- console.log('🔍 [FloatingFileUploader] No group ID set, returning empty array');
3348
- return [];
3349
- }
3350
- console.log("groupId groupId", groupId);
3351
- const files = this.fileManagerService.getAllFilesForGroup(groupId);
3352
- const fetchedFiles = this.fileManagerService.getFetchedFilesByGroupId(groupId);
3353
- console.log("fetchedFiles fetchedFiles", fetchedFiles);
3354
- const activeUploads = Array.from(this.fileManagerService.activeUploads().entries()).filter(([_, upload]) => upload.groupId === groupId);
3355
- console.log('🔍 [FloatingFileUploader] allFilesForGroup computed - DETAILED:', {
3356
- groupId,
3357
- filesCount: files.length,
3358
- fetchedFilesCount: fetchedFiles.length,
3359
- activeUploadsCount: activeUploads.length,
3360
- files: files.map(f => ({ id: f.fileId, name: f.fileName, stage: f.stage })),
3361
- fetchedFiles: fetchedFiles.map(f => ({ id: f._id, name: f.cyfm_name })),
3362
- activeUploads: activeUploads.map(([id, upload]) => ({ id, stage: upload.stage, groupId: upload.groupId }))
3363
- });
3364
- return files;
3365
- }, ...(ngDevMode ? [{ debugName: "allFilesForGroup" }] : []));
3366
- // Check if there are any files to display (active uploads OR fetched files for current group)
3367
- hasFilesToShow = computed(() => {
3368
- const hasActiveUploads = this.hasActiveUploads();
3369
- const allFiles = this.allFilesForGroup();
3370
- const hasFiles = allFiles.length > 0;
3371
- const currentGroupId = this.currentGroupId();
3372
- console.log('🔍 [FloatingFileUploader] hasFilesToShow check:', {
3373
- hasActiveUploads,
3374
- allFilesCount: allFiles.length,
3375
- hasFiles,
3376
- currentGroupId,
3377
- result: hasActiveUploads || hasFiles
3378
- });
3379
- return hasActiveUploads || hasFiles;
3380
- }, ...(ngDevMode ? [{ debugName: "hasFilesToShow" }] : []));
3381
- // Animation states
3382
- isAnimating = signal(false, ...(ngDevMode ? [{ debugName: "isAnimating" }] : []));
3383
- // Drag functionality
3384
- isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
3385
- position = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "position" }] : []));
3386
- dragOffset = { x: 0, y: 0 };
3387
- // File drag and drop functionality
3378
+ notificationService = inject(NotificationService);
3379
+ elementService = inject(CideElementsService);
3380
+ destroyRef = inject(DestroyRef);
3381
+ // private readonly pendingTasks = inject(PendingTasks); // TODO: Fix PendingTasks API usage
3382
+ // Traditional @Input() decorators
3383
+ label = 'Choose file';
3384
+ accept = '';
3385
+ multiple = false;
3386
+ disabled = false;
3387
+ required = false;
3388
+ helperText = '';
3389
+ errorText = '';
3390
+ showPreview = false;
3391
+ previewWidth = '200px';
3392
+ previewHeight = '200px';
3393
+ previewBoxMode = false;
3394
+ showFileName = true;
3395
+ placeholderText = 'Click to select image';
3396
+ placeholderIcon = '📷';
3397
+ autoUpload = false;
3398
+ uploadData = {};
3399
+ showFloatingUploader = true;
3400
+ floatingUploaderGroupId;
3401
+ // Traditional @Output() decorators
3402
+ fileChange = new EventEmitter();
3403
+ uploadSuccess = new EventEmitter();
3404
+ uploadError = new EventEmitter();
3405
+ uploadProgressChange = new EventEmitter();
3406
+ // Readable signals created from @Input() decorator values
3407
+ labelSignal = signal(this.label, ...(ngDevMode ? [{ debugName: "labelSignal" }] : []));
3408
+ acceptSignal = signal(this.accept, ...(ngDevMode ? [{ debugName: "acceptSignal" }] : []));
3409
+ multipleSignal = signal(this.multiple, ...(ngDevMode ? [{ debugName: "multipleSignal" }] : []));
3410
+ disabledSignal = signal(this.disabled, ...(ngDevMode ? [{ debugName: "disabledSignal" }] : []));
3411
+ requiredSignal = signal(this.required, ...(ngDevMode ? [{ debugName: "requiredSignal" }] : []));
3412
+ helperTextSignal = signal(this.helperText, ...(ngDevMode ? [{ debugName: "helperTextSignal" }] : []));
3413
+ errorTextSignal = signal(this.errorText, ...(ngDevMode ? [{ debugName: "errorTextSignal" }] : []));
3414
+ showPreviewSignal = signal(this.showPreview, ...(ngDevMode ? [{ debugName: "showPreviewSignal" }] : []));
3415
+ previewWidthSignal = signal(this.previewWidth, ...(ngDevMode ? [{ debugName: "previewWidthSignal" }] : []));
3416
+ previewHeightSignal = signal(this.previewHeight, ...(ngDevMode ? [{ debugName: "previewHeightSignal" }] : []));
3417
+ previewBoxModeSignal = signal(this.previewBoxMode, ...(ngDevMode ? [{ debugName: "previewBoxModeSignal" }] : []));
3418
+ showFileNameSignal = signal(this.showFileName, ...(ngDevMode ? [{ debugName: "showFileNameSignal" }] : []));
3419
+ placeholderTextSignal = signal(this.placeholderText, ...(ngDevMode ? [{ debugName: "placeholderTextSignal" }] : []));
3420
+ placeholderIconSignal = signal(this.placeholderIcon, ...(ngDevMode ? [{ debugName: "placeholderIconSignal" }] : []));
3421
+ autoUploadSignal = signal(this.autoUpload, ...(ngDevMode ? [{ debugName: "autoUploadSignal" }] : []));
3422
+ uploadDataSignal = signal(this.uploadData, ...(ngDevMode ? [{ debugName: "uploadDataSignal" }] : []));
3423
+ showFloatingUploaderSignal = signal(this.showFloatingUploader, ...(ngDevMode ? [{ debugName: "showFloatingUploaderSignal" }] : []));
3424
+ floatingUploaderGroupIdSignal = signal(this.floatingUploaderGroupId, ...(ngDevMode ? [{ debugName: "floatingUploaderGroupIdSignal" }] : []));
3425
+ // Reactive state with signals
3426
+ id = signal(Math.random().toString(36).substring(2, 10), ...(ngDevMode ? [{ debugName: "id" }] : []));
3427
+ isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
3428
+ uploadProgress = signal(0, ...(ngDevMode ? [{ debugName: "uploadProgress" }] : []));
3429
+ uploadStatus = signal('idle', ...(ngDevMode ? [{ debugName: "uploadStatus" }] : []));
3430
+ files = signal(null, ...(ngDevMode ? [{ debugName: "files" }] : []));
3431
+ fileNames = signal([], ...(ngDevMode ? [{ debugName: "fileNames" }] : []));
3432
+ previewUrls = signal([], ...(ngDevMode ? [{ debugName: "previewUrls" }] : []));
3433
+ uploadNotificationId = signal(null, ...(ngDevMode ? [{ debugName: "uploadNotificationId" }] : []));
3388
3434
  isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
3389
- // Window resize handler reference for cleanup
3390
- windowResizeHandler;
3391
- // Cached dimensions for performance
3392
- cachedDimensions = { width: 320, height: 200 };
3393
- lastDimensionUpdate = 0;
3435
+ groupId = signal(null, ...(ngDevMode ? [{ debugName: "groupId" }] : [])); // Group ID for multiple file uploads
3436
+ isMultipleUploadMode = signal(false, ...(ngDevMode ? [{ debugName: "isMultipleUploadMode" }] : [])); // Flag to track if we're in multiple upload mode
3437
+ hasEverUploaded = signal(false, ...(ngDevMode ? [{ debugName: "hasEverUploaded" }] : [])); // Track if this component has ever uploaded files
3438
+ // Computed signals for better relationships
3439
+ hasFiles = computed(() => this.files() !== null && this.files().length > 0, ...(ngDevMode ? [{ debugName: "hasFiles" }] : []));
3440
+ canUpload = computed(() => this.hasFiles() && !this.isUploading() && !this.disabledSignal(), ...(ngDevMode ? [{ debugName: "canUpload" }] : []));
3441
+ isInErrorState = computed(() => this.uploadStatus() === 'error', ...(ngDevMode ? [{ debugName: "isInErrorState" }] : []));
3442
+ isInSuccessState = computed(() => this.uploadStatus() === 'success', ...(ngDevMode ? [{ debugName: "isInSuccessState" }] : []));
3443
+ // Optimized computed values - only calculate when needed
3444
+ totalFileSize = computed(() => {
3445
+ const files = this.files();
3446
+ return files ? Array.from(files).reduce((total, file) => total + file.size, 0) : 0;
3447
+ }, ...(ngDevMode ? [{ debugName: "totalFileSize" }] : []));
3448
+ fileSizeInMB = computed(() => (this.totalFileSize() / 1048576).toFixed(2), ...(ngDevMode ? [{ debugName: "fileSizeInMB" }] : [])); // 1024^2 = 1048576
3449
+ // ControlValueAccessor callbacks
3450
+ onChange = (value) => { };
3451
+ onTouched = () => { };
3452
+ onValidatorChange = () => { };
3453
+ // Computed values
3454
+ hasImages = computed(() => this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "hasImages" }] : []));
3455
+ isPreviewBoxMode = computed(() => this.previewBoxModeSignal() && this.showPreviewSignal(), ...(ngDevMode ? [{ debugName: "isPreviewBoxMode" }] : []));
3456
+ isImagePreviewAvailable = computed(() => this.showPreviewSignal() && this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "isImagePreviewAvailable" }] : []));
3394
3457
  constructor() {
3395
- console.log('🚀 [FloatingFileUploader] Component initialized');
3396
- // Initialize default position
3397
- this.initializePosition();
3398
- // Set up effect to show/hide floating uploader based on service state
3399
- effect(() => {
3400
- const hasActiveUploads = this.hasActiveUploads();
3401
- const hasUploads = this.hasUploads();
3402
- const hasFilesToShow = this.hasFilesToShow();
3403
- console.log('🔄 [FloatingFileUploader] Service state changed:', {
3404
- hasActiveUploads,
3405
- hasUploads,
3406
- hasFilesToShow,
3407
- queueLength: this.uploadQueue().length,
3408
- activeUploadsCount: this.activeUploads().size,
3409
- activeUploads: Array.from(this.activeUploads().entries())
3410
- });
3411
- // Show floating uploader when there are active uploads OR files to show
3412
- if (hasActiveUploads && !this.isVisible()) {
3413
- console.log('👁️ [FloatingFileUploader] Showing floating uploader due to active uploads');
3414
- this.showWithAnimation();
3415
- }
3416
- });
3417
- // Set up effect to listen for manual show trigger from service
3418
- effect(() => {
3419
- const shouldShow = this.fileManagerService.showFloatingUploader();
3420
- const hasFilesToShow = this.hasFilesToShow();
3421
- if (shouldShow && !this.isVisible()) {
3422
- console.log('👁️ [FloatingFileUploader] Showing floating uploader due to service trigger');
3423
- this.showWithAnimation();
3424
- }
3425
- else if (shouldShow && hasFilesToShow && !this.isVisible()) {
3426
- console.log('👁️ [FloatingFileUploader] Showing floating uploader due to files available');
3427
- this.showWithAnimation();
3428
- }
3429
- });
3458
+ // Minimal DOM operations - only when necessary
3430
3459
  }
3431
3460
  ngOnInit() {
3432
- // Set up drag and drop listeners
3433
- this.setupDragAndDrop();
3434
- // Set up file input change listeners
3435
- this.setupFileInputListeners();
3436
- // Set up window resize listener
3437
- this.setupWindowResize();
3438
- }
3439
- ngOnDestroy() {
3440
- console.log('🧹 [FloatingFileUploader] Component destroyed');
3441
- this.removeDragAndDropListeners();
3442
- this.removeFileInputListeners();
3443
- // Clean up window resize listener
3444
- if (this.windowResizeHandler) {
3445
- window.removeEventListener('resize', this.windowResizeHandler);
3446
- }
3447
- }
3448
- /**
3449
- * Set up drag and drop functionality
3450
- */
3451
- setupDragAndDrop() {
3452
- document.addEventListener('dragover', this.handleDragOver.bind(this));
3453
- document.addEventListener('dragleave', this.handleDragLeave.bind(this));
3454
- document.addEventListener('drop', this.handleDrop.bind(this));
3455
- }
3456
- /**
3457
- * Remove drag and drop listeners
3458
- */
3459
- removeDragAndDropListeners() {
3460
- document.removeEventListener('dragover', this.handleDragOver.bind(this));
3461
- document.removeEventListener('dragleave', this.handleDragLeave.bind(this));
3462
- document.removeEventListener('drop', this.handleDrop.bind(this));
3463
- }
3464
- /**
3465
- * Set up file input change listeners
3466
- */
3467
- setupFileInputListeners() {
3468
- // Listen for file input change events globally
3469
- document.addEventListener('change', this.handleFileInputChange.bind(this));
3461
+ // Update signals with initial @Input() values
3462
+ this.labelSignal.set(this.label);
3463
+ this.acceptSignal.set(this.accept);
3464
+ this.multipleSignal.set(this.multiple);
3465
+ this.disabledSignal.set(this.disabled);
3466
+ this.requiredSignal.set(this.required);
3467
+ this.helperTextSignal.set(this.helperText);
3468
+ this.errorTextSignal.set(this.errorText);
3469
+ this.showPreviewSignal.set(this.showPreview);
3470
+ this.previewWidthSignal.set(this.previewWidth);
3471
+ this.previewHeightSignal.set(this.previewHeight);
3472
+ this.previewBoxModeSignal.set(this.previewBoxMode);
3473
+ this.showFileNameSignal.set(this.showFileName);
3474
+ this.placeholderTextSignal.set(this.placeholderText);
3475
+ this.placeholderIconSignal.set(this.placeholderIcon);
3476
+ this.autoUploadSignal.set(this.autoUpload);
3477
+ this.uploadDataSignal.set(this.uploadData);
3470
3478
  }
3471
- /**
3472
- * Remove file input listeners
3473
- */
3474
- removeFileInputListeners() {
3475
- document.removeEventListener('change', this.handleFileInputChange.bind(this));
3479
+ ngOnChanges(changes) {
3480
+ // Angular 20: Update signals when @Input() values change
3481
+ if (changes['label'])
3482
+ this.labelSignal.set(this.label);
3483
+ if (changes['accept'])
3484
+ this.acceptSignal.set(this.accept);
3485
+ if (changes['multiple'])
3486
+ this.multipleSignal.set(this.multiple);
3487
+ if (changes['disabled'])
3488
+ this.disabledSignal.set(this.disabled);
3489
+ if (changes['required'])
3490
+ this.requiredSignal.set(this.required);
3491
+ if (changes['helperText'])
3492
+ this.helperTextSignal.set(this.helperText);
3493
+ if (changes['errorText'])
3494
+ this.errorTextSignal.set(this.errorText);
3495
+ if (changes['showPreview'])
3496
+ this.showPreviewSignal.set(this.showPreview);
3497
+ if (changes['previewWidth'])
3498
+ this.previewWidthSignal.set(this.previewWidth);
3499
+ if (changes['previewHeight'])
3500
+ this.previewHeightSignal.set(this.previewHeight);
3501
+ if (changes['previewBoxMode'])
3502
+ this.previewBoxModeSignal.set(this.previewBoxMode);
3503
+ if (changes['showFileName'])
3504
+ this.showFileNameSignal.set(this.showFileName);
3505
+ if (changes['placeholderText'])
3506
+ this.placeholderTextSignal.set(this.placeholderText);
3507
+ if (changes['placeholderIcon'])
3508
+ this.placeholderIconSignal.set(this.placeholderIcon);
3509
+ if (changes['autoUpload'])
3510
+ this.autoUploadSignal.set(this.autoUpload);
3511
+ if (changes['uploadData'])
3512
+ this.uploadDataSignal.set(this.uploadData);
3476
3513
  }
3477
- /**
3478
- * Handle file input change events
3479
- */
3480
- handleFileInputChange(event) {
3481
- const target = event.target;
3482
- console.log('🔍 [FloatingFileUploader] File input change event detected:', {
3483
- type: target.type,
3484
- filesLength: target.files?.length || 0,
3485
- element: target
3486
- });
3487
- // Check if this is a file input with files
3488
- if (target.type === 'file' && target.files && target.files.length > 0) {
3489
- console.log('📁 [FloatingFileUploader] File input change detected:', target.files.length, 'files');
3490
- // Check if the input has a data-user-id attribute for user context
3491
- const userId = target.getAttribute('data-user-id');
3492
- if (userId && userId !== this.currentUserId()) {
3493
- this.setCurrentUserId(userId);
3514
+ writeValue(value) {
3515
+ console.log('📝 [FileInput] writeValue called with:', value);
3516
+ if (typeof value === 'string') {
3517
+ // Check if this is a group ID for multiple files or single file ID
3518
+ if (this.isMultipleFileMode()) {
3519
+ // Multiple file mode - value is group ID
3520
+ console.log('📁 [FileInput] Value is group ID for multiple files:', value);
3521
+ this.groupId.set(value);
3522
+ this.loadFilesFromGroupId(value);
3523
+ }
3524
+ else {
3525
+ // Single file mode - value is file ID
3526
+ console.log('📝 [FileInput] Value is single file ID:', value);
3527
+ this.files.set(null);
3528
+ this.fileNames.set([]);
3529
+ this.clearPreviews();
3530
+ // Fetch file details to get base64 and set preview
3531
+ this.loadFileDetailsFromId(value);
3494
3532
  }
3495
- // Handle the files
3496
- this.handleFiles(Array.from(target.files));
3497
- // Reset the input to allow selecting the same files again
3498
- target.value = '';
3499
- }
3500
- }
3501
- /**
3502
- * Handle drag over event
3503
- */
3504
- handleDragOver(event) {
3505
- event.preventDefault();
3506
- event.stopPropagation();
3507
- // Show floating uploader when files are dragged over
3508
- if (event.dataTransfer?.types.includes('Files')) {
3509
- this.showWithAnimation();
3510
3533
  }
3511
- }
3512
- /**
3513
- * Handle drag leave event
3514
- */
3515
- handleDragLeave(event) {
3516
- event.preventDefault();
3517
- event.stopPropagation();
3518
- // Only hide if leaving the entire document
3519
- if (!event.relatedTarget || event.relatedTarget === document.body) {
3520
- this.updateVisibility();
3534
+ else if (value instanceof FileList) {
3535
+ // Value is a FileList
3536
+ console.log('📝 [FileInput] Value is FileList:', Array.from(value).map(f => f.name));
3537
+ this.files.set(value);
3538
+ this.fileNames.set(Array.from(value).map(f => f.name));
3539
+ this.generatePreviews();
3540
+ // For multiple files, use group ID API to fetch files
3541
+ if (value.length > 1) {
3542
+ const groupId = this.groupId();
3543
+ if (groupId) {
3544
+ console.log('📁 [FileInput] Multiple files detected, fetching files for group:', groupId);
3545
+ this.loadFilesFromGroupId(groupId);
3546
+ }
3547
+ }
3521
3548
  }
3522
- }
3523
- /**
3524
- * Handle drop event
3525
- */
3526
- handleDrop(event) {
3527
- event.preventDefault();
3528
- event.stopPropagation();
3529
- const files = event.dataTransfer?.files;
3530
- if (files && files.length > 0) {
3531
- this.handleFiles(Array.from(files));
3549
+ else {
3550
+ // Value is null
3551
+ console.log('📝 [FileInput] Value is null');
3552
+ this.files.set(null);
3553
+ this.fileNames.set([]);
3554
+ this.clearPreviews();
3532
3555
  }
3533
3556
  }
3534
- /**
3535
- * Handle files from drag and drop or file input
3536
- */
3537
- handleFiles(files) {
3538
- console.log('📁 [FloatingFileUploader] Handling files:', files.length);
3539
- // Use handleExternalFiles to process the files
3540
- this.handleExternalFiles(files, this.currentUserId());
3541
- }
3542
- /**
3543
- * Update visibility - simplified for notification only
3544
- */
3545
- updateVisibility() {
3546
- // This is just a notification component now
3547
- // The actual uploads are handled by the global uploader
3548
- }
3549
- /**
3550
- * Show with animation
3551
- */
3552
- showWithAnimation() {
3553
- console.log('🎬 [FloatingFileUploader] showWithAnimation called - setting isVisible to true');
3554
- this.isAnimating.set(true);
3555
- this.isVisible.set(true);
3556
- // Remove animation class after animation completes
3557
- setTimeout(() => {
3558
- this.isAnimating.set(false);
3559
- console.log('🎬 [FloatingFileUploader] Animation completed, isVisible:', this.isVisible());
3560
- }, 300);
3561
- }
3562
- /**
3563
- * Hide with animation
3564
- */
3565
- hideWithAnimation() {
3566
- this.isAnimating.set(true);
3567
- // Wait for animation to complete before hiding
3568
- setTimeout(() => {
3569
- this.isVisible.set(false);
3570
- this.isAnimating.set(false);
3571
- }, 300);
3572
- }
3573
- /**
3574
- * Toggle minimize state
3575
- */
3576
- toggleMinimize() {
3577
- this.isMinimized.set(!this.isMinimized());
3557
+ registerOnChange(fn) {
3558
+ this.onChange = fn;
3578
3559
  }
3579
- /**
3580
- * Close the floating uploader
3581
- */
3582
- close() {
3583
- // Don't clear files from service - just hide the uploader
3584
- // Files will be fetched from API when "Show Files" is clicked
3585
- this.hideWithAnimation();
3560
+ registerOnTouched(fn) {
3561
+ this.onTouched = fn;
3586
3562
  }
3587
- /**
3588
- * Get upload summary text
3589
- */
3590
- getUploadSummary() {
3591
- const pending = this.pendingUploads();
3592
- const active = this.activeUploadsLocal();
3593
- const completed = this.completedUploads();
3594
- const failed = this.failedUploads();
3595
- if (active.length > 0) {
3596
- return `${active.length} uploading`;
3597
- }
3598
- else if (pending.length > 0) {
3599
- return `${pending.length} pending`;
3600
- }
3601
- else if (completed.length > 0 && failed.length === 0) {
3602
- return `${completed.length} completed`;
3603
- }
3604
- else if (failed.length > 0) {
3605
- return `${completed.length} completed, ${failed.length} failed`;
3606
- }
3607
- return 'No uploads';
3563
+ registerOnValidatorChange(fn) {
3564
+ this.onValidatorChange = fn;
3608
3565
  }
3609
- /**
3610
- * Get overall progress percentage
3611
- */
3612
- getOverallProgress() {
3613
- const allUploads = Array.from(this.activeUploads().values());
3614
- if (allUploads.length === 0)
3615
- return 0;
3616
- const totalProgress = allUploads.reduce((sum, upload) => sum + (upload.percentage || 0), 0);
3617
- return Math.round(totalProgress / allUploads.length);
3566
+ setDisabledState(isDisabled) {
3567
+ // Note: With input signals, disabled state is controlled by the parent component
3568
+ // This method is kept for ControlValueAccessor compatibility but doesn't modify the signal
3569
+ console.log('🔧 [FileInput] setDisabledState called with:', isDisabled, '(controlled by parent component)');
3618
3570
  }
3619
- /**
3620
- * Get status icon based on upload stage
3621
- */
3622
- getStatusIcon(stage) {
3623
- switch (stage) {
3624
- case 'reading': return 'schedule';
3625
- case 'uploading': return 'cloud_upload';
3626
- case 'complete': return 'check_circle';
3627
- case 'error': return 'error';
3628
- default: return 'help';
3571
+ onFileSelected(event) {
3572
+ console.log('🔍 [FileInput] onFileSelected called');
3573
+ const input = event.target;
3574
+ const selectedFiles = input.files;
3575
+ this.files.set(selectedFiles);
3576
+ this.fileNames.set(selectedFiles ? Array.from(selectedFiles).map(f => f.name) : []);
3577
+ console.log('📁 [FileInput] Files selected:', this.fileNames());
3578
+ this.generatePreviews();
3579
+ // Reset upload status when new file is selected
3580
+ this.uploadStatus.set('idle');
3581
+ console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
3582
+ this.onChange(selectedFiles);
3583
+ this.fileChange.emit(selectedFiles);
3584
+ this.onTouched();
3585
+ // Note: Floating uploader is now triggered via service in upload methods
3586
+ // Auto upload if enabled
3587
+ if (this.autoUploadSignal() && selectedFiles && selectedFiles.length > 0) {
3588
+ if (this.multipleSignal()) {
3589
+ console.log('🚀 [FileInput] Auto upload enabled for multiple files mode:', selectedFiles.length, 'files');
3590
+ this.uploadMultipleFiles(Array.from(selectedFiles));
3591
+ }
3592
+ else {
3593
+ console.log('🚀 [FileInput] Auto upload enabled for single file mode:', selectedFiles[0].name);
3594
+ this.uploadFile(selectedFiles[0]);
3595
+ }
3629
3596
  }
3630
- }
3631
- /**
3632
- * Get status class based on upload stage
3633
- */
3634
- getStatusClass(stage) {
3635
- switch (stage) {
3636
- case 'reading': return 'status-pending';
3637
- case 'uploading': return 'status-uploading';
3638
- case 'complete': return 'status-completed';
3639
- case 'error': return 'status-error';
3640
- default: return 'status-unknown';
3597
+ else {
3598
+ console.log('⏸️ [FileInput] Auto upload disabled or no files');
3641
3599
  }
3642
3600
  }
3643
- /**
3644
- * Cancel upload
3645
- */
3646
- cancelUpload(fileId) {
3647
- console.log('🚫 [FloatingFileUploader] Cancelling upload:', fileId);
3648
- this.fileManagerService.cancelUpload(fileId);
3649
- }
3650
- /**
3651
- * Get file name from file ID (extract from the ID format)
3652
- */
3653
- getFileNameFromId(fileId) {
3654
- // Extract filename from the fileId format: filename_size_timestamp
3655
- const parts = fileId.split('_');
3656
- if (parts.length >= 3) {
3657
- // Remove the last two parts (size and timestamp) to get the filename
3658
- return parts.slice(0, -2).join('_');
3659
- }
3660
- return fileId;
3601
+ clearFiles() {
3602
+ console.log('🗑️ [FileInput] clearFiles called');
3603
+ this.files.set(null);
3604
+ this.fileNames.set([]);
3605
+ this.clearPreviews();
3606
+ this.uploadStatus.set('idle');
3607
+ console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
3608
+ this.onChange(null);
3609
+ this.fileChange.emit(null);
3661
3610
  }
3662
- /**
3663
- * Get all files from service state (pending + active uploads + fetched files)
3664
- * This method now uses the computed property for consistency
3665
- */
3666
- getAllFiles() {
3667
- return this.allFilesForGroup();
3611
+ uploadFile(file) {
3612
+ console.log('📤 [FileInput] uploadFile called for:', file.name, 'Size:', file.size, 'bytes');
3613
+ // Angular 20: Use PendingTasks for better loading state management
3614
+ // const uploadTask = this.pendingTasks.add(); // TODO: Fix PendingTasks API usage
3615
+ // console.log('⏳ [FileInput] Pending task added for upload tracking');
3616
+ // Set upload status to 'start' before starting upload
3617
+ this.uploadStatus.set('start');
3618
+ console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3619
+ this.isUploading.set(true);
3620
+ this.uploadProgress.set(0);
3621
+ this.uploadProgressChange.emit(0);
3622
+ console.log('📊 [FileInput] Upload progress initialized to 0%');
3623
+ // Make form control invalid during upload - this prevents form submission
3624
+ this.onChange(null);
3625
+ console.log('🚫 [FileInput] Form control value set to null to prevent submission');
3626
+ // Show initial progress notification with spinner (persistent - no auto-dismiss)
3627
+ const notificationId = this.notificationService.showProgress('🔄 Preparing file upload...', 0, { duration: 0 });
3628
+ this.uploadNotificationId.set(notificationId);
3629
+ console.log('🔔 [FileInput] Progress notification started with ID:', notificationId);
3630
+ this.fileManagerService.uploadFile(file, this.uploadDataSignal(), (progress) => {
3631
+ // Real progress callback from file manager service
3632
+ this.uploadProgress.set(progress);
3633
+ this.uploadProgressChange.emit(progress);
3634
+ // Upload progress updated
3635
+ // Set upload status to 'uploading' when progress starts
3636
+ if (this.uploadStatus() === 'start') {
3637
+ this.uploadStatus.set('uploading');
3638
+ console.log('🔄 [FileInput] Upload status changed to:', this.uploadStatus());
3639
+ }
3640
+ // Update progress notification with spinner
3641
+ const notificationId = this.uploadNotificationId();
3642
+ if (notificationId) {
3643
+ let progressMessage = '';
3644
+ if (progress < 10) {
3645
+ progressMessage = '🔄 Starting upload...';
3646
+ }
3647
+ else if (progress < 25) {
3648
+ progressMessage = '🔄 Uploading file...';
3649
+ }
3650
+ else if (progress < 50) {
3651
+ progressMessage = '🔄 Upload in progress...';
3652
+ }
3653
+ else if (progress < 75) {
3654
+ progressMessage = '🔄 Almost done...';
3655
+ }
3656
+ else if (progress < 95) {
3657
+ progressMessage = '🔄 Finishing upload...';
3658
+ }
3659
+ else {
3660
+ progressMessage = '🔄 Finalizing...';
3661
+ }
3662
+ this.notificationService.updateProgress(notificationId, progress, progressMessage);
3663
+ }
3664
+ }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3665
+ next: (response) => {
3666
+ console.log('🎉 [FileInput] Upload SUCCESS - Response received:', response);
3667
+ // Angular 20: Complete the pending task
3668
+ // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
3669
+ // console.log('✅ [FileInput] Pending task completed for successful upload');
3670
+ // Set upload status to 'success'
3671
+ this.uploadStatus.set('success');
3672
+ console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3673
+ // Complete the progress
3674
+ this.uploadProgress.set(100);
3675
+ this.uploadProgressChange.emit(100);
3676
+ console.log('📊 [FileInput] Upload progress completed: 100%');
3677
+ // Update progress notification to complete
3678
+ const notificationId = this.uploadNotificationId();
3679
+ if (notificationId) {
3680
+ this.notificationService.remove(notificationId);
3681
+ console.log('🔔 [FileInput] Progress notification removed');
3682
+ }
3683
+ // Success notification removed for cleaner UX
3684
+ this.uploadNotificationId.set(null);
3685
+ // Extract ID from CoreFileManagerInsertUpdateResponse
3686
+ const uploadedId = response?.data?.core_file_manager?.[0]?.cyfm_id;
3687
+ if (uploadedId) {
3688
+ console.log('✅ [FileInput] File uploaded successfully with ID:', uploadedId);
3689
+ // Set the uploaded ID as the form control value
3690
+ this.onChange(uploadedId);
3691
+ console.log('📝 [FileInput] Form control value set to uploaded ID:', uploadedId);
3692
+ // Only emit individual uploadSuccess if not in multiple upload mode
3693
+ if (!this.isMultipleUploadMode()) {
3694
+ this.uploadSuccess.emit(uploadedId);
3695
+ console.log('📝 [FileInput] Upload success event emitted with file ID:', uploadedId);
3696
+ }
3697
+ else {
3698
+ console.log('📝 [FileInput] Individual upload success suppressed (multiple upload mode) - file ID:', uploadedId);
3699
+ }
3700
+ }
3701
+ else {
3702
+ console.error('❌ [FileInput] Upload successful but no ID returned:', response);
3703
+ this.uploadError.emit('Upload successful but no ID returned');
3704
+ }
3705
+ this.isUploading.set(false);
3706
+ console.log('🔄 [FileInput] isUploading set to false');
3707
+ },
3708
+ error: (error) => {
3709
+ console.error('💥 [FileInput] Upload FAILED:', error);
3710
+ // Angular 20: Complete the pending task even on error
3711
+ // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
3712
+ // console.log('❌ [FileInput] Pending task completed for failed upload');
3713
+ // Set upload status to 'error' and remove upload validation error
3714
+ this.uploadStatus.set('error');
3715
+ console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3716
+ // Remove progress notification and show error
3717
+ const notificationId = this.uploadNotificationId();
3718
+ if (notificationId) {
3719
+ this.notificationService.remove(notificationId);
3720
+ console.log('🔔 [FileInput] Progress notification removed due to error');
3721
+ }
3722
+ this.notificationService.error(`❌ File upload failed: ${error.message || error.error?.message || 'Unknown error occurred'}`, { duration: 0 });
3723
+ this.uploadNotificationId.set(null);
3724
+ this.uploadError.emit(error.message || error.error?.message || 'Upload failed');
3725
+ this.isUploading.set(false);
3726
+ this.uploadProgress.set(0);
3727
+ this.uploadProgressChange.emit(0);
3728
+ console.log('🔄 [FileInput] Upload state reset - isUploading: false, progress: 0%');
3729
+ }
3730
+ });
3668
3731
  }
3669
3732
  /**
3670
- * Set current user ID
3733
+ * Upload multiple files with group ID support
3734
+ * FLOW: 1) Generate group ID first, 2) Upload all files with same group ID, 3) Emit group ID on completion
3671
3735
  */
3672
- setCurrentUserId(userId) {
3673
- this.currentUserId.set(userId);
3674
- this.fileManagerService.setUserId(userId);
3736
+ uploadMultipleFiles(files) {
3737
+ console.log('📤 [FileInput] uploadMultipleFiles called for:', files.length, 'files');
3738
+ console.log('🔄 [FileInput] STEP 1: Generate group ID before starting any file uploads');
3739
+ // Set multiple upload mode flag
3740
+ this.isMultipleUploadMode.set(true);
3741
+ // Set upload status to 'start' before starting upload
3742
+ this.uploadStatus.set('start');
3743
+ this.isUploading.set(true);
3744
+ this.uploadProgress.set(0);
3745
+ this.uploadProgressChange.emit(0);
3746
+ // Make form control invalid during upload
3747
+ this.onChange(null);
3748
+ // Show initial progress notification
3749
+ const notificationId = this.notificationService.showProgress('🔄 Preparing multiple file upload...', 0, { duration: 0 });
3750
+ this.uploadNotificationId.set(notificationId);
3751
+ // STEP 1: Generate or get group ID BEFORE starting any file uploads
3752
+ const existingGroupId = this.uploadDataSignal().groupId;
3753
+ if (existingGroupId) {
3754
+ console.log('🆔 [FileInput] STEP 1 COMPLETE: Using existing group ID:', existingGroupId);
3755
+ console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', existingGroupId);
3756
+ this.groupId.set(existingGroupId);
3757
+ this.startMulti(files, existingGroupId);
3758
+ }
3759
+ else {
3760
+ console.log('🆔 [FileInput] No existing group ID, generating new one...');
3761
+ // Generate group ID BEFORE starting any file uploads
3762
+ this.fileManagerService.generateObjectId().subscribe({
3763
+ next: (response) => {
3764
+ const newGroupId = response.data?.objectId;
3765
+ console.log('🆔 [FileInput] STEP 1 COMPLETE: Generated new group ID:', newGroupId);
3766
+ console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', newGroupId);
3767
+ this.groupId.set(newGroupId);
3768
+ this.startMulti(files, newGroupId);
3769
+ },
3770
+ error: (error) => {
3771
+ console.error('❌ [FileInput] Failed to generate group ID:', error);
3772
+ this.uploadError.emit('Failed to generate group ID');
3773
+ this.isUploading.set(false);
3774
+ this.uploadStatus.set('error');
3775
+ const notificationId = this.uploadNotificationId();
3776
+ if (notificationId) {
3777
+ this.notificationService.remove(notificationId);
3778
+ }
3779
+ this.notificationService.error('❌ Failed to generate group ID for multiple file upload', { duration: 0 });
3780
+ this.uploadNotificationId.set(null);
3781
+ }
3782
+ });
3783
+ }
3675
3784
  }
3676
3785
  /**
3677
- * Public method to handle files from external sources
3678
- * This can be called by other components to trigger the floating uploader
3786
+ * Start uploading multiple files with the provided group ID
3787
+ * All files will be uploaded with the SAME group ID that was generated before this method
3679
3788
  */
3680
- handleExternalFiles(files, userId, groupId) {
3681
- console.log('📁 [FloatingFileUploader] External files received:', files.length, 'files');
3682
- // Set user ID if provided
3683
- if (userId && userId !== this.currentUserId()) {
3684
- this.setCurrentUserId(userId);
3685
- }
3686
- // Set group ID if provided
3687
- if (groupId) {
3688
- this.currentGroupId.set(groupId);
3689
- }
3690
- // Upload files using file manager service
3691
- // The file manager service will handle adding to its queue and the effect will show the floating uploader
3789
+ startMulti(files, groupId) {
3790
+ console.log('🚀 [FileInput] STEP 2: Starting upload for', files.length, 'files with group ID:', groupId);
3791
+ console.log('📋 [FileInput] All files will use the same group ID:', groupId);
3792
+ // Mark that this component has ever uploaded files
3793
+ this.hasEverUploaded.set(true);
3794
+ let completedUploads = 0;
3795
+ let failedUploads = 0;
3796
+ const totalFiles = files.length;
3797
+ // IMPORTANT: All files use the SAME group ID that was generated before starting uploads
3798
+ const uploadDataWithGroupId = {
3799
+ ...this.uploadDataSignal(),
3800
+ groupId: groupId,
3801
+ isMultiple: true
3802
+ };
3692
3803
  files.forEach((file, index) => {
3693
- console.log(`📁 [FloatingFileUploader] Starting upload for file ${index + 1}/${files.length}:`, file.name);
3694
- this.fileManagerService.uploadFile(file, {
3695
- userId: this.currentUserId(),
3696
- groupId: groupId,
3697
- permissions: ['read', 'write'],
3698
- tags: []
3699
- })
3700
- .pipe(takeUntilDestroyed(this.destroyRef))
3701
- .subscribe({
3804
+ const componentId = this.id();
3805
+ console.log(`📤 [FileInput-${componentId}] Uploading file ${index + 1}/${totalFiles}: "${file.name}" with group ID: ${groupId}`);
3806
+ console.log(`📤 [FileInput-${componentId}] Upload data:`, uploadDataWithGroupId);
3807
+ this.fileManagerService.uploadFile(file, uploadDataWithGroupId, (progress) => {
3808
+ // Calculate overall progress
3809
+ const fileProgress = progress / totalFiles;
3810
+ const overallProgress = ((completedUploads * 100) + fileProgress) / totalFiles;
3811
+ this.uploadProgress.set(overallProgress);
3812
+ this.uploadProgressChange.emit(overallProgress);
3813
+ // Update progress notification
3814
+ const notificationId = this.uploadNotificationId();
3815
+ if (notificationId) {
3816
+ const progressMessage = `🔄 Uploading file ${index + 1} of ${totalFiles}...`;
3817
+ this.notificationService.updateProgress(notificationId, overallProgress, progressMessage);
3818
+ }
3819
+ }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3702
3820
  next: (response) => {
3703
- console.log('✅ [FloatingFileUploader] Upload completed:', response);
3821
+ completedUploads++;
3822
+ console.log(`✅ [FileInput] File ${index + 1}/${totalFiles} uploaded`);
3823
+ // Check if all files are completed
3824
+ if (completedUploads + failedUploads === totalFiles) {
3825
+ this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
3826
+ }
3704
3827
  },
3705
3828
  error: (error) => {
3706
- console.error('❌ [FloatingFileUploader] Upload failed:', error);
3829
+ failedUploads++;
3830
+ console.error(`❌ [FileInput] File ${index + 1}/${totalFiles} upload failed:`, error);
3831
+ // Check if all files are completed
3832
+ if (completedUploads + failedUploads === totalFiles) {
3833
+ this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
3834
+ }
3707
3835
  }
3708
3836
  });
3709
3837
  });
3710
3838
  }
3711
3839
  /**
3712
- * Manually show the floating uploader
3713
- * This should always be called with a group ID from the file input component
3840
+ * Handle completion of multiple file upload
3714
3841
  */
3715
- showUploader(groupId) {
3716
- console.log('👁️ [FloatingFileUploader] Manually showing uploader', groupId ? `for group: ${groupId}` : 'without group ID');
3717
- if (!groupId) {
3718
- console.error('❌ [FloatingFileUploader] No group ID provided. Floating uploader should always be opened with a group ID from the file input component.');
3719
- return;
3842
+ handleMultipleUploadComplete(completed, failed, total, groupId) {
3843
+ console.log(`📊 [FileInput] Multiple upload complete: ${completed}/${total} successful, ${failed} failed`);
3844
+ this.isUploading.set(false);
3845
+ this.uploadProgress.set(100);
3846
+ this.uploadProgressChange.emit(100);
3847
+ // Remove progress notification
3848
+ const notificationId = this.uploadNotificationId();
3849
+ if (notificationId) {
3850
+ this.notificationService.remove(notificationId);
3720
3851
  }
3721
- this.currentGroupId.set(groupId);
3722
- console.log('🆔 [FloatingFileUploader] Set group ID:', groupId);
3723
- // Check if we already have files for this group
3724
- const existingFiles = this.fileManagerService.getAllFilesForGroup(groupId);
3725
- console.log('🔍 [FloatingFileUploader] Existing files for group:', {
3726
- groupId,
3727
- existingFilesCount: existingFiles.length,
3728
- files: existingFiles.map(f => ({ id: f.fileId, name: f.fileName, stage: f.stage }))
3729
- });
3730
- // Always fetch fresh data from API to ensure we have the latest files
3852
+ this.uploadNotificationId.set(null);
3853
+ if (failed === 0) {
3854
+ // All files uploaded successfully
3855
+ this.uploadStatus.set('success');
3856
+ // Success notification removed for cleaner UX
3857
+ // STEP 3: For multiple file upload, emit the group ID (not individual file IDs)
3858
+ this.onChange(groupId);
3859
+ this.uploadSuccess.emit(groupId);
3860
+ console.log('📝 [FileInput] Multiple upload completed with group ID:', groupId);
3861
+ }
3862
+ else if (completed > 0) {
3863
+ // Some files uploaded successfully
3864
+ this.uploadStatus.set('error');
3865
+ this.notificationService.warning(`⚠️ ${completed}/${total} files uploaded. ${failed} failed.`, { duration: 0 });
3866
+ this.uploadError.emit(`${failed} out of ${total} files failed to upload`);
3867
+ }
3868
+ else {
3869
+ // All files failed
3870
+ this.uploadStatus.set('error');
3871
+ this.notificationService.error(`❌ All ${total} files failed to upload.`, { duration: 0 });
3872
+ this.uploadError.emit('All files failed to upload');
3873
+ }
3874
+ // Reset multiple upload mode flag
3875
+ this.isMultipleUploadMode.set(false);
3876
+ }
3877
+ generatePreviews() {
3878
+ // Clear existing previews
3879
+ this.clearPreviews();
3880
+ if (!this.showPreviewSignal() || !this.files()) {
3881
+ return;
3882
+ }
3883
+ Array.from(this.files()).forEach(file => {
3884
+ if (this.isImageFile(file)) {
3885
+ const reader = new FileReader();
3886
+ reader.onload = (e) => {
3887
+ if (e.target?.result) {
3888
+ this.previewUrls.update(urls => [...urls, e.target.result]);
3889
+ }
3890
+ };
3891
+ reader.readAsDataURL(file);
3892
+ }
3893
+ });
3894
+ }
3895
+ clearPreviews() {
3896
+ // Revoke object URLs to prevent memory leaks
3897
+ this.previewUrls().forEach(url => {
3898
+ if (url.startsWith('blob:')) {
3899
+ URL.revokeObjectURL(url);
3900
+ }
3901
+ });
3902
+ this.previewUrls.set([]);
3903
+ }
3904
+ isImageFile(file) {
3905
+ return file.type.startsWith('image/');
3906
+ }
3907
+ loadFileDetailsFromId(fileId) {
3908
+ console.log('🔍 [FileInput] Loading file details for ID:', fileId);
3909
+ if (!fileId)
3910
+ return;
3911
+ this.fileManagerService?.getFileDetails({ cyfm_id: fileId })?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3912
+ next: (fileDetails) => {
3913
+ console.log('📋 [FileInput] File details received:', fileDetails);
3914
+ if (fileDetails?.data?.length) {
3915
+ const fileData = fileDetails.data[0];
3916
+ console.log('📁 [FileInput] File data:', fileData);
3917
+ // Set file name from the details
3918
+ if (fileData.cyfm_name) {
3919
+ this.fileNames.set([fileData.cyfm_name]);
3920
+ console.log('📝 [FileInput] File name set:', fileData.cyfm_name);
3921
+ }
3922
+ // If it's an image and we have base64 data, set preview
3923
+ if (this.showPreviewSignal() && fileData.cyfm_file_base64) {
3924
+ // Check if it's an image file based on file name or type
3925
+ const isImage = this.isImageFileFromName(fileData.cyfm_name || '') ||
3926
+ this.isImageFileFromType(fileData.cyfm_type || '');
3927
+ if (isImage) {
3928
+ // Add data URL prefix if not already present
3929
+ let base64Data = fileData.cyfm_file_base64;
3930
+ if (!base64Data.startsWith('data:')) {
3931
+ const mimeType = fileData.cyfm_type || 'image/jpeg';
3932
+ base64Data = `data:${mimeType};base64,${base64Data}`;
3933
+ }
3934
+ this.previewUrls.set([base64Data]);
3935
+ console.log('🖼️ [FileInput] Preview set from base64 data');
3936
+ }
3937
+ }
3938
+ }
3939
+ else {
3940
+ console.warn('⚠️ [FileInput] No file data found for ID:', fileId);
3941
+ }
3942
+ },
3943
+ error: (error) => {
3944
+ console.error('❌ [FileInput] Error loading file details:', error);
3945
+ this.notificationService.error(`Failed to load file details: ${error.message || 'Unknown error'}`, { duration: 0 });
3946
+ }
3947
+ });
3948
+ }
3949
+ /**
3950
+ * Check if the component is in multiple file mode
3951
+ */
3952
+ isMultipleFileMode() {
3953
+ // Check if multiple attribute is set or if we have a group ID
3954
+ return this.multiple || this.groupId() !== null;
3955
+ }
3956
+ /**
3957
+ * Load files from group ID using the group API
3958
+ */
3959
+ loadFilesFromGroupId(groupId) {
3960
+ console.log('🔍 [FileInput] Loading files for group ID:', groupId);
3961
+ if (!groupId)
3962
+ return;
3731
3963
  this.fileManagerService.fetchAndStoreFilesByGroupId(groupId)
3732
3964
  .pipe(takeUntilDestroyed(this.destroyRef))
3733
3965
  .subscribe({
3734
3966
  next: (files) => {
3735
- console.log(' [FloatingFileUploader] Files fetched and stored:', files.length);
3736
- // Force show the uploader after files are loaded
3737
- this.showWithAnimation();
3738
- // Debug: Check what files are available now using computed property
3739
- setTimeout(() => {
3740
- const allFiles = this.allFilesForGroup();
3741
- console.log('🔍 [FloatingFileUploader] Files available after fetch (computed):', {
3742
- allFilesCount: allFiles.length,
3743
- files: allFiles.map(f => ({ id: f.fileId, name: f.fileName, stage: f.stage })),
3744
- hasFilesToShow: this.hasFilesToShow()
3745
- });
3746
- }, 100);
3967
+ console.log('📋 [FileInput] Files loaded for group:', files.length);
3968
+ // Set file names to show count in input
3969
+ if (files && files.length > 0) {
3970
+ const fileNames = files.map(file => file.file_name || file.name || 'Unknown file');
3971
+ this.fileNames.set(fileNames);
3972
+ console.log('📝 [FileInput] File names set for display:', fileNames);
3973
+ }
3974
+ else {
3975
+ this.fileNames.set([]);
3976
+ }
3977
+ // Files are now stored in service state and will be displayed by floating uploader
3747
3978
  },
3748
3979
  error: (error) => {
3749
- console.error('❌ [FloatingFileUploader] Failed to fetch files:', error);
3750
- // Still show the uploader even if fetch fails
3751
- this.showWithAnimation();
3752
- // Debug existing files even on error
3753
- setTimeout(() => {
3754
- const allFiles = this.allFilesForGroup();
3755
- console.log('🔍 [FloatingFileUploader] Files available after error (computed):', {
3756
- allFilesCount: allFiles.length,
3757
- files: allFiles.map(f => ({ id: f.fileId, name: f.fileName, stage: f.stage })),
3758
- hasFilesToShow: this.hasFilesToShow()
3759
- });
3760
- }, 100);
3980
+ console.error('❌ [FileInput] Failed to load files for group:', error);
3981
+ this.fileNames.set([]);
3761
3982
  }
3762
3983
  });
3763
3984
  }
3764
- /**
3765
- * Check if there are any uploads for the current group
3766
- */
3767
- hasUploadsForCurrentGroup() {
3768
- const groupId = this.currentGroupId();
3769
- if (!groupId) {
3770
- // If no group filter, show all uploads
3771
- return this.hasUploads();
3985
+ isImageFileFromName(fileName) {
3986
+ if (!fileName)
3987
+ return false;
3988
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
3989
+ const lowerFileName = fileName.toLowerCase();
3990
+ return imageExtensions.some(ext => lowerFileName.endsWith(ext));
3991
+ }
3992
+ isImageFileFromType(fileType) {
3993
+ if (!fileType)
3994
+ return false;
3995
+ return fileType.startsWith('image/');
3996
+ }
3997
+ removePreview(index) {
3998
+ const currentFiles = this.files();
3999
+ const currentUrls = this.previewUrls();
4000
+ if (currentFiles && currentFiles.length > index) {
4001
+ // Handle FileList case - remove file from FileList
4002
+ const dt = new DataTransfer();
4003
+ Array.from(currentFiles).forEach((file, i) => {
4004
+ if (i !== index) {
4005
+ dt.items.add(file);
4006
+ }
4007
+ });
4008
+ const newFiles = dt.files;
4009
+ this.files.set(newFiles);
4010
+ this.fileNames.set(Array.from(newFiles).map(f => f.name));
4011
+ // Remove the preview URL
4012
+ if (currentUrls[index] && currentUrls[index].startsWith('blob:')) {
4013
+ URL.revokeObjectURL(currentUrls[index]);
4014
+ }
4015
+ this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
4016
+ this.onChange(newFiles);
4017
+ this.fileChange.emit(newFiles);
4018
+ }
4019
+ else if (currentUrls.length > index) {
4020
+ // Handle uploaded file ID case - clear the preview and set control value to null
4021
+ console.log('🗑️ [FileInput] Removing preview for uploaded file ID');
4022
+ // Clear preview
4023
+ this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
4024
+ this.fileNames.set([]);
4025
+ // Set control value to null since we're removing the uploaded file
4026
+ this.onChange(null);
4027
+ this.fileChange.emit(null);
4028
+ }
4029
+ }
4030
+ ngOnDestroy() {
4031
+ // Clean up preview URLs to prevent memory leaks
4032
+ this.clearPreviews();
4033
+ // Clean up any active upload notification
4034
+ const notificationId = this.uploadNotificationId();
4035
+ if (notificationId) {
4036
+ this.notificationService.remove(notificationId);
4037
+ this.uploadNotificationId.set(null);
4038
+ }
4039
+ }
4040
+ triggerFileSelect() {
4041
+ const fileInput = document.getElementById('cide-file-input-' + this.id());
4042
+ if (fileInput && !this.disabledSignal()) {
4043
+ fileInput.click();
3772
4044
  }
3773
- // Check if any uploads belong to the current group
3774
- // Note: This would need to be enhanced based on how group IDs are stored in the file manager service
3775
- return this.hasUploads();
3776
4045
  }
3777
4046
  /**
3778
- * Handle drag over event for file drop
4047
+ * Show floating uploader manually
4048
+ * This can be called to show the floating uploader even when no files are selected
3779
4049
  */
3780
- onDragOver(event) {
3781
- event.preventDefault();
3782
- event.stopPropagation();
3783
- this.isDragOver.set(true);
4050
+ showUploader() {
4051
+ console.log('👁️ [FileInput] Manually showing floating uploader');
4052
+ if (!this.showFloatingUploaderSignal()) {
4053
+ console.log('⚠️ [FileInput] Floating uploader is disabled');
4054
+ return;
4055
+ }
4056
+ const groupId = this.groupId();
4057
+ if (groupId) {
4058
+ console.log("groupId groupId", groupId);
4059
+ // Fetch files for the group and trigger floating uploader to show
4060
+ this.fileManagerService.fetchAndStoreFilesByGroupId(groupId)
4061
+ .pipe(takeUntilDestroyed(this.destroyRef))
4062
+ .subscribe({
4063
+ next: (files) => {
4064
+ console.log('✅ [FileInput] Files fetched for floating uploader: groupId', files.length);
4065
+ // Trigger the floating uploader to show via service with group ID
4066
+ this.fileManagerService.triggerFloatingUploaderShow(groupId);
4067
+ },
4068
+ error: (error) => {
4069
+ console.error('❌ [FileInput] Failed to fetch files for floating uploader:', error);
4070
+ // Still trigger show even if fetch fails, with group ID
4071
+ this.fileManagerService.triggerFloatingUploaderShow(groupId);
4072
+ }
4073
+ });
4074
+ }
4075
+ else {
4076
+ // No group ID, just trigger show
4077
+ this.fileManagerService.triggerFloatingUploaderShow();
4078
+ }
3784
4079
  }
3785
4080
  /**
3786
- * Handle drag leave event for file drop
4081
+ * Get total upload count from file manager service for this component's group ID
4082
+ * Uses optimized service method for better performance
3787
4083
  */
3788
- onDragLeave(event) {
3789
- event.preventDefault();
3790
- event.stopPropagation();
3791
- this.isDragOver.set(false);
4084
+ getUploadCount() {
4085
+ const groupId = this.groupId();
4086
+ if (!groupId)
4087
+ return this.fileManagerService.activeUploads().size;
4088
+ return this.fileManagerService.getFileCountForGroup(groupId);
3792
4089
  }
3793
4090
  /**
3794
- * Handle drop event for file drop
4091
+ * Check if there are active uploads for this component's group ID
4092
+ * Uses optimized service method for better performance
3795
4093
  */
3796
- onDrop(event) {
3797
- event.preventDefault();
3798
- event.stopPropagation();
3799
- this.isDragOver.set(false);
3800
- const files = event.dataTransfer?.files;
3801
- if (files && files.length > 0) {
3802
- this.handleFileSelection(Array.from(files));
4094
+ hasActiveUploads() {
4095
+ const groupId = this.groupId();
4096
+ if (!groupId) {
4097
+ return Array.from(this.fileManagerService.activeUploads().values()).some(upload => upload.stage !== 'complete');
3803
4098
  }
4099
+ return this.fileManagerService.hasActiveUploadsForGroup(groupId);
3804
4100
  }
3805
4101
  /**
3806
- * Trigger file input click
4102
+ * Get count of active (non-completed) uploads for this component's group ID
3807
4103
  */
3808
- triggerFileInput() {
3809
- const fileInput = document.querySelector('input[type="file"]');
3810
- if (fileInput) {
3811
- fileInput.click();
4104
+ getActiveUploadCount() {
4105
+ const groupId = this.groupId();
4106
+ if (!groupId) {
4107
+ return Array.from(this.fileManagerService.activeUploads().values())
4108
+ .filter(upload => upload.stage !== 'complete').length;
3812
4109
  }
4110
+ return this.fileManagerService.getAllFilesForGroup(groupId)
4111
+ .filter(file => file.stage !== 'complete').length;
3813
4112
  }
3814
4113
  /**
3815
- * Handle file input change
4114
+ * Show floating uploader (alias for showUploader for template)
3816
4115
  */
3817
- onFileInputChange(event) {
3818
- const input = event.target;
3819
- if (input.files && input.files.length > 0) {
3820
- this.handleFileSelection(Array.from(input.files));
3821
- input.value = ''; // Reset input
3822
- }
4116
+ showFloatingUploaderDialog() {
4117
+ this.showUploader();
3823
4118
  }
3824
4119
  /**
3825
- * Handle file selection from drag/drop or file input
4120
+ * Get dynamic classes for drag and drop zone
3826
4121
  */
3827
- handleFileSelection(files) {
3828
- console.log('📁 [FloatingFileUploader] Files selected:', files.map(f => f.name));
3829
- const groupId = this.currentGroupId();
3830
- // Group ID must be provided by the file input component
3831
- if (!groupId) {
3832
- console.error('❌ [FloatingFileUploader] No group ID available. Files cannot be uploaded without a group ID from the file input component.');
3833
- return;
4122
+ getDragDropZoneClasses() {
4123
+ const classes = [];
4124
+ if (this.isDragOver()) {
4125
+ classes.push('!tw-border-blue-500', '!tw-bg-blue-100', 'dark:!tw-bg-blue-900/30', 'tw-scale-[1.01]');
3834
4126
  }
3835
- console.log('🆔 [FloatingFileUploader] Using group ID from file input:', groupId);
3836
- // Upload files using the file manager service
3837
- files.forEach((file, index) => {
3838
- console.log(`📤 [FloatingFileUploader] Uploading file ${index + 1}/${files.length}: ${file.name} to group: ${groupId}`);
3839
- this.fileManagerService.uploadFile(file, {
3840
- groupId: groupId,
3841
- isMultiple: true,
3842
- userId: this.currentUserId()
3843
- });
3844
- });
4127
+ if (this.disabledSignal()) {
4128
+ classes.push('tw-opacity-50', 'tw-cursor-not-allowed', '!hover:tw-border-gray-300', '!hover:tw-bg-gray-50', 'dark:!hover:tw-bg-gray-800');
4129
+ }
4130
+ if (this.hasFiles()) {
4131
+ classes.push('!tw-border-emerald-500', '!tw-bg-emerald-50', 'dark:!tw-bg-emerald-900/20', 'hover:!tw-border-emerald-600', 'hover:!tw-bg-emerald-100', 'dark:hover:!tw-bg-emerald-900/30');
4132
+ }
4133
+ return classes.join(' ');
3845
4134
  }
3846
4135
  /**
3847
- * Update cached dimensions (throttled for performance)
4136
+ * Get dynamic classes for icon
3848
4137
  */
3849
- updateCachedDimensions() {
3850
- const now = Date.now();
3851
- // Only update dimensions every 100ms to avoid excessive DOM queries
3852
- if (now - this.lastDimensionUpdate < 100) {
3853
- return;
4138
+ getIconClasses() {
4139
+ const classes = ['tw-text-gray-500', 'dark:tw-text-gray-400'];
4140
+ if (this.isDragOver()) {
4141
+ classes.push('!tw-text-blue-500', 'dark:!tw-text-blue-400');
3854
4142
  }
3855
- const uploaderElement = document.querySelector('.floating-uploader');
3856
- if (uploaderElement) {
3857
- this.cachedDimensions = {
3858
- width: uploaderElement.offsetWidth,
3859
- height: uploaderElement.offsetHeight
3860
- };
3861
- this.lastDimensionUpdate = now;
4143
+ else if (this.hasFiles()) {
4144
+ classes.push('!tw-text-emerald-500', 'dark:!tw-text-emerald-400');
3862
4145
  }
4146
+ return classes.join(' ');
3863
4147
  }
3864
4148
  /**
3865
- * Start dragging the uploader
4149
+ * Get dynamic classes for preview box
3866
4150
  */
3867
- startDrag(event) {
4151
+ getPreviewBoxClasses() {
4152
+ const classes = [];
4153
+ if (this.isDragOver()) {
4154
+ classes.push('!tw-border-blue-500', '!tw-bg-blue-100', 'dark:!tw-bg-blue-900/30');
4155
+ }
4156
+ if (this.disabledSignal()) {
4157
+ classes.push('tw-opacity-50', 'tw-cursor-not-allowed', '!hover:tw-border-gray-300', '!hover:tw-bg-gray-50', 'dark:!hover:tw-bg-gray-800');
4158
+ }
4159
+ if (this.hasImages()) {
4160
+ classes.push('!tw-border-emerald-500', '!tw-bg-emerald-50', 'dark:!tw-bg-emerald-900/20');
4161
+ }
4162
+ return classes.join(' ');
4163
+ }
4164
+ // Drag and Drop Event Handlers
4165
+ onDragOver(event) {
3868
4166
  event.preventDefault();
3869
- const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
3870
- const clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY;
3871
- const currentPos = this.position();
3872
- this.dragOffset = {
3873
- x: clientX - currentPos.x,
3874
- y: clientY - currentPos.y
3875
- };
3876
- this.isDragging.set(true);
3877
- // Update cached dimensions at the start of drag for better performance
3878
- this.updateCachedDimensions();
3879
- // Add event listeners for drag and end
3880
- const moveHandler = (e) => this.onDrag(e);
3881
- const endHandler = () => this.endDrag(moveHandler, endHandler);
3882
- document.addEventListener('mousemove', moveHandler, { passive: false });
3883
- document.addEventListener('mouseup', endHandler);
3884
- document.addEventListener('touchmove', moveHandler, { passive: false });
3885
- document.addEventListener('touchend', endHandler);
3886
- // Prevent text selection during drag
3887
- document.body.style.userSelect = 'none';
4167
+ event.stopPropagation();
4168
+ if (!this.disabledSignal()) {
4169
+ this.isDragOver.set(true);
4170
+ console.log('🔄 [FileInput] Drag over detected');
4171
+ }
3888
4172
  }
3889
- /**
3890
- * Handle dragging movement
3891
- */
3892
- onDrag(event) {
3893
- if (!this.isDragging())
3894
- return;
4173
+ onDragLeave(event) {
3895
4174
  event.preventDefault();
3896
- const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
3897
- const clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY;
3898
- const newX = clientX - this.dragOffset.x;
3899
- const newY = clientY - this.dragOffset.y;
3900
- // Constrain to viewport bounds using cached dimensions for performance
3901
- const viewportWidth = window.innerWidth;
3902
- const viewportHeight = window.innerHeight;
3903
- // Use cached dimensions instead of DOM queries for better performance
3904
- const uploaderWidth = this.cachedDimensions.width;
3905
- const uploaderHeight = this.cachedDimensions.height;
3906
- // Ensure uploader stays within viewport bounds
3907
- const constrainedX = Math.max(0, Math.min(newX, viewportWidth - uploaderWidth));
3908
- const constrainedY = Math.max(0, Math.min(newY, viewportHeight - uploaderHeight));
3909
- this.position.set({ x: constrainedX, y: constrainedY });
4175
+ event.stopPropagation();
4176
+ this.isDragOver.set(false);
4177
+ console.log('🔄 [FileInput] Drag leave detected');
3910
4178
  }
3911
- /**
3912
- * End dragging
3913
- */
3914
- endDrag(moveHandler, endHandler) {
3915
- this.isDragging.set(false);
3916
- // Remove event listeners
3917
- document.removeEventListener('mousemove', moveHandler);
3918
- document.removeEventListener('mouseup', endHandler);
3919
- document.removeEventListener('touchmove', moveHandler);
3920
- document.removeEventListener('touchend', endHandler);
3921
- // Restore text selection
3922
- document.body.style.userSelect = '';
4179
+ onDragEnter(event) {
4180
+ event.preventDefault();
4181
+ event.stopPropagation();
4182
+ if (!this.disabledSignal()) {
4183
+ this.isDragOver.set(true);
4184
+ console.log('🔄 [FileInput] Drag enter detected');
4185
+ }
3923
4186
  }
3924
- /**
3925
- * Set up window resize listener to keep uploader within bounds
3926
- */
3927
- setupWindowResize() {
3928
- const handleResize = () => {
3929
- const currentPos = this.position();
3930
- const viewportWidth = window.innerWidth;
3931
- const viewportHeight = window.innerHeight;
3932
- // Update cached dimensions on resize
3933
- this.updateCachedDimensions();
3934
- // Use cached dimensions for performance
3935
- const uploaderWidth = this.cachedDimensions.width;
3936
- const uploaderHeight = this.cachedDimensions.height;
3937
- // Constrain position to new viewport bounds
3938
- const constrainedX = Math.max(0, Math.min(currentPos.x, viewportWidth - uploaderWidth));
3939
- const constrainedY = Math.max(0, Math.min(currentPos.y, viewportHeight - uploaderHeight));
3940
- // Update position if it changed
3941
- if (constrainedX !== currentPos.x || constrainedY !== currentPos.y) {
3942
- this.position.set({ x: constrainedX, y: constrainedY });
3943
- console.log('📐 [FloatingFileUploader] Position adjusted for window resize:', { x: constrainedX, y: constrainedY });
4187
+ onDrop(event) {
4188
+ event.preventDefault();
4189
+ event.stopPropagation();
4190
+ this.isDragOver.set(false);
4191
+ if (this.disabledSignal()) {
4192
+ console.log('⏸️ [FileInput] Drop ignored - component is disabled');
4193
+ return;
4194
+ }
4195
+ const files = event.dataTransfer?.files;
4196
+ if (files && files.length > 0) {
4197
+ console.log('📁 [FileInput] Files dropped:', Array.from(files).map(f => f.name));
4198
+ // Validate file types if accept is specified
4199
+ if (this.acceptSignal() && !this.validateFileTypes(files)) {
4200
+ console.log('❌ [FileInput] Invalid file types dropped');
4201
+ this.notificationService.error('❌ Invalid file type. Please select files of the correct type.', { duration: 0 });
4202
+ return;
3944
4203
  }
3945
- };
3946
- window.addEventListener('resize', handleResize);
3947
- // Store reference for cleanup
3948
- this.windowResizeHandler = handleResize;
4204
+ // Handle single vs multiple files
4205
+ if (!this.multipleSignal() && files.length > 1) {
4206
+ console.log('⚠️ [FileInput] Multiple files dropped but multiple is disabled');
4207
+ this.notificationService.warning('⚠️ Only one file is allowed. Using the first file.', { duration: 0 });
4208
+ // Create a new FileList with only the first file
4209
+ const dt = new DataTransfer();
4210
+ dt.items.add(files[0]);
4211
+ this.handleFileSelection(dt.files);
4212
+ }
4213
+ else {
4214
+ this.handleFileSelection(files);
4215
+ }
4216
+ }
3949
4217
  }
3950
- /**
3951
- * Initialize default position
3952
- */
3953
- initializePosition() {
3954
- // Set initial position to bottom-right corner
3955
- const viewportWidth = window.innerWidth;
3956
- const viewportHeight = window.innerHeight;
3957
- // Initialize cached dimensions with defaults
3958
- this.cachedDimensions = { width: 320, height: 300 };
3959
- // Use cached dimensions for initial positioning
3960
- const uploaderWidth = this.cachedDimensions.width;
3961
- const uploaderHeight = this.cachedDimensions.height;
3962
- // Ensure initial position is within bounds
3963
- const initialX = Math.max(20, viewportWidth - uploaderWidth - 20);
3964
- const initialY = Math.max(20, viewportHeight - uploaderHeight - 20);
3965
- this.position.set({
3966
- x: initialX,
3967
- y: initialY
4218
+ validateFileTypes(files) {
4219
+ const acceptTypes = this.acceptSignal().split(',').map(type => type.trim());
4220
+ if (acceptTypes.length === 0 || acceptTypes[0] === '')
4221
+ return true;
4222
+ return Array.from(files).every(file => {
4223
+ return acceptTypes.some(acceptType => {
4224
+ if (acceptType.startsWith('.')) {
4225
+ // Extension-based validation
4226
+ return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
4227
+ }
4228
+ else if (acceptType.includes('/')) {
4229
+ // MIME type validation
4230
+ return file.type === acceptType || file.type.startsWith(acceptType.replace('*', ''));
4231
+ }
4232
+ return false;
4233
+ });
3968
4234
  });
3969
- // Update dimensions after a short delay to get actual rendered size
3970
- setTimeout(() => {
3971
- this.updateCachedDimensions();
3972
- }, 100);
3973
4235
  }
3974
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3975
- 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 -->\n@if (isVisible()) {\n<div class=\"floating-uploader\" \n [class.minimized]=\"isMinimized()\" \n [class.animating]=\"isAnimating()\"\n [style.left.px]=\"position().x\"\n [style.top.px]=\"position().y\">\n\n <!-- Header (Draggable) -->\n <div class=\"uploader-header draggable-header\"\n (mousedown)=\"startDrag($event)\"\n (touchstart)=\"startDrag($event)\">\n <div class=\"header-left\">\n <div class=\"upload-icon\">\n <cide-ele-icon size=\"sm\">cloud_upload</cide-ele-icon>\n </div>\n <div class=\"upload-info\">\n <div class=\"upload-title\">File Upload</div>\n <div class=\"upload-summary\">{{ getUploadSummary() }}</div>\n </div>\n </div>\n \n <div class=\"header-actions\">\n <button class=\"action-btn minimize-btn\" (click)=\"toggleMinimize()\" [title]=\"isMinimized() ? 'Expand' : 'Minimize'\">\n <cide-ele-icon size=\"xs\">{{ isMinimized() ? 'expand_more' : 'expand_less' }}</cide-ele-icon>\n </button>\n <button class=\"action-btn close-btn\" (click)=\"close()\" title=\"Close\">\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\n </button>\n </div>\n </div>\n {{currentGroupId()}}\n\n <!-- Content (hidden when minimized) -->\n @if (!isMinimized()) {\n <div class=\"uploader-content\">\n \n <!-- Drag and Drop Zone -->\n <div class=\"upload-zone\" \n [class.drag-over]=\"isDragOver()\" \n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\" \n (drop)=\"onDrop($event)\" \n (click)=\"triggerFileInput()\">\n \n <!-- Hidden file input -->\n <input #fileInput \n type=\"file\" \n [multiple]=\"true\" \n [accept]=\"'*/*'\" \n (change)=\"onFileInputChange($event)\" \n style=\"display: none;\">\n \n <div class=\"upload-zone-content\">\n <cide-ele-icon class=\"upload-icon\" size=\"sm\">cloud_upload</cide-ele-icon>\n \n <div class=\"upload-text\">\n <div class=\"upload-title\">\n {{ isDragOver() ? 'Drop files here' : 'Drag files here or click to browse' }}\n </div>\n </div>\n </div>\n </div>\n \n <!-- Upload Queue - Show files from service state -->\n @if (allFilesForGroup().length > 0) {\n <div class=\"upload-queue\">\n <!-- Show all files from service state -->\n @for (file of allFilesForGroup(); track file.fileId) {\n <div class=\"upload-item\" [class]=\"getStatusClass(file.stage)\">\n <div class=\"file-info\">\n <cide-ele-icon class=\"status-icon\" size=\"xs\">{{ getStatusIcon(file.stage) }}</cide-ele-icon>\n <div class=\"file-details\">\n <div class=\"file-name\">{{ file.fileName }}</div>\n <div class=\"file-status\">\n @switch (file.stage) {\n @case ('pending') {\n <span class=\"text-yellow-600\">Waiting...</span>\n }\n @case ('reading') {\n <span class=\"text-yellow-600\">Reading...</span>\n }\n @case ('uploading') {\n <span class=\"text-blue-600\">Uploading...</span>\n }\n @case ('complete') {\n <span class=\"text-green-600\">Completed</span>\n }\n @case ('error') {\n <span class=\"text-red-600\">Failed</span>\n }\n }\n </div>\n </div>\n </div>\n\n <!-- Progress Bar (only for uploading files) -->\n @if (file.stage === 'uploading' && file.percentage !== undefined) {\n <div class=\"file-progress\">\n <div class=\"progress-bar\">\n <div class=\"progress-fill\" [style.width.%]=\"file.percentage\"></div>\n </div>\n <span class=\"progress-text\">{{ file.percentage }}%</span>\n </div>\n }\n\n <!-- Actions -->\n <div class=\"upload-actions\">\n @switch (file.stage) {\n @case ('pending') {\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(file.fileId)\" title=\"Cancel\">\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\n </button>\n }\n @case ('reading') {\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(file.fileId)\" title=\"Cancel\">\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\n </button>\n }\n @case ('uploading') {\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(file.fileId)\" title=\"Cancel\">\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\n </button>\n }\n @case ('complete') {\n <button class=\"action-btn success-btn\" title=\"Completed\">\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\n </button>\n }\n @case ('error') {\n <button class=\"action-btn retry-btn\" title=\"Retry\">\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\n </button>\n }\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- No uploads message when manually opened -->\n <div class=\"no-uploads-message\">\n <div class=\"message-content\">\n <cide-ele-icon size=\"md\" class=\"message-icon\">cloud_upload</cide-ele-icon>\n <div class=\"message-text\">\n <h4>No active uploads</h4>\n <p>Upload files to see their progress here</p>\n </div>\n </div>\n </div>\n }\n </div>\n }\n</div>\n}\n", styles: [".floating-uploader{position:fixed;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.draggable-header{cursor:move;-webkit-user-select:none;user-select:none}.floating-uploader .uploader-header.draggable-header:hover{background:#f1f5f9}.floating-uploader .uploader-header.draggable-header:active{background:#e2e8f0;cursor:grabbing}.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 .upload-zone{margin:8px 16px;padding:12px;border:2px dashed #d1d5db;border-radius:6px;background:#f9fafb;cursor:pointer;transition:all .2s ease;text-align:center}.floating-uploader .uploader-content .upload-zone:hover{border-color:#3b82f6;background:#f0f9ff}.floating-uploader .uploader-content .upload-zone.drag-over{border-color:#3b82f6;background:#dbeafe;transform:scale(1.01)}.floating-uploader .uploader-content .upload-zone .upload-zone-content{display:flex;flex-direction:column;align-items:center;gap:6px}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-icon{color:#6b7280;transition:color .2s ease}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-text .upload-title{font-size:13px;font-weight:500;color:#374151;margin:0;line-height:1.2}.floating-uploader .uploader-content .upload-zone:hover .upload-zone-content .upload-icon{color:#3b82f6}.floating-uploader .uploader-content .upload-zone.drag-over .upload-zone-content .upload-icon{color:#1d4ed8}.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.draggable-header:hover{background:#475569}.floating-uploader .uploader-header.draggable-header:active{background:#64748b}.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 .upload-zone{border-color:#475569;background:#334155}.floating-uploader .uploader-content .upload-zone:hover{border-color:#3b82f6;background:#1e3a8a}.floating-uploader .uploader-content .upload-zone.drag-over{border-color:#60a5fa;background:#1e40af}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-icon{color:#94a3b8}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-text .upload-title{color:#f1f5f9}.floating-uploader .uploader-content .upload-zone:hover .upload-zone-content .upload-icon{color:#60a5fa}.floating-uploader .uploader-content .upload-zone.drag-over .upload-zone-content .upload-icon{color:#93c5fd}.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"] }] });
3976
- }
3977
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, decorators: [{
3978
- type: Component,
3979
- args: [{ selector: 'cide-ele-floating-file-uploader', standalone: true, imports: [
3980
- CommonModule,
3981
- CideIconComponent
3982
- ], template: "<!-- Floating File Uploader Container -->\n@if (isVisible()) {\n<div class=\"floating-uploader\" \n [class.minimized]=\"isMinimized()\" \n [class.animating]=\"isAnimating()\"\n [style.left.px]=\"position().x\"\n [style.top.px]=\"position().y\">\n\n <!-- Header (Draggable) -->\n <div class=\"uploader-header draggable-header\"\n (mousedown)=\"startDrag($event)\"\n (touchstart)=\"startDrag($event)\">\n <div class=\"header-left\">\n <div class=\"upload-icon\">\n <cide-ele-icon size=\"sm\">cloud_upload</cide-ele-icon>\n </div>\n <div class=\"upload-info\">\n <div class=\"upload-title\">File Upload</div>\n <div class=\"upload-summary\">{{ getUploadSummary() }}</div>\n </div>\n </div>\n \n <div class=\"header-actions\">\n <button class=\"action-btn minimize-btn\" (click)=\"toggleMinimize()\" [title]=\"isMinimized() ? 'Expand' : 'Minimize'\">\n <cide-ele-icon size=\"xs\">{{ isMinimized() ? 'expand_more' : 'expand_less' }}</cide-ele-icon>\n </button>\n <button class=\"action-btn close-btn\" (click)=\"close()\" title=\"Close\">\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\n </button>\n </div>\n </div>\n {{currentGroupId()}}\n\n <!-- Content (hidden when minimized) -->\n @if (!isMinimized()) {\n <div class=\"uploader-content\">\n \n <!-- Drag and Drop Zone -->\n <div class=\"upload-zone\" \n [class.drag-over]=\"isDragOver()\" \n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\" \n (drop)=\"onDrop($event)\" \n (click)=\"triggerFileInput()\">\n \n <!-- Hidden file input -->\n <input #fileInput \n type=\"file\" \n [multiple]=\"true\" \n [accept]=\"'*/*'\" \n (change)=\"onFileInputChange($event)\" \n style=\"display: none;\">\n \n <div class=\"upload-zone-content\">\n <cide-ele-icon class=\"upload-icon\" size=\"sm\">cloud_upload</cide-ele-icon>\n \n <div class=\"upload-text\">\n <div class=\"upload-title\">\n {{ isDragOver() ? 'Drop files here' : 'Drag files here or click to browse' }}\n </div>\n </div>\n </div>\n </div>\n \n <!-- Upload Queue - Show files from service state -->\n @if (allFilesForGroup().length > 0) {\n <div class=\"upload-queue\">\n <!-- Show all files from service state -->\n @for (file of allFilesForGroup(); track file.fileId) {\n <div class=\"upload-item\" [class]=\"getStatusClass(file.stage)\">\n <div class=\"file-info\">\n <cide-ele-icon class=\"status-icon\" size=\"xs\">{{ getStatusIcon(file.stage) }}</cide-ele-icon>\n <div class=\"file-details\">\n <div class=\"file-name\">{{ file.fileName }}</div>\n <div class=\"file-status\">\n @switch (file.stage) {\n @case ('pending') {\n <span class=\"text-yellow-600\">Waiting...</span>\n }\n @case ('reading') {\n <span class=\"text-yellow-600\">Reading...</span>\n }\n @case ('uploading') {\n <span class=\"text-blue-600\">Uploading...</span>\n }\n @case ('complete') {\n <span class=\"text-green-600\">Completed</span>\n }\n @case ('error') {\n <span class=\"text-red-600\">Failed</span>\n }\n }\n </div>\n </div>\n </div>\n\n <!-- Progress Bar (only for uploading files) -->\n @if (file.stage === 'uploading' && file.percentage !== undefined) {\n <div class=\"file-progress\">\n <div class=\"progress-bar\">\n <div class=\"progress-fill\" [style.width.%]=\"file.percentage\"></div>\n </div>\n <span class=\"progress-text\">{{ file.percentage }}%</span>\n </div>\n }\n\n <!-- Actions -->\n <div class=\"upload-actions\">\n @switch (file.stage) {\n @case ('pending') {\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(file.fileId)\" title=\"Cancel\">\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\n </button>\n }\n @case ('reading') {\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(file.fileId)\" title=\"Cancel\">\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\n </button>\n }\n @case ('uploading') {\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(file.fileId)\" title=\"Cancel\">\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\n </button>\n }\n @case ('complete') {\n <button class=\"action-btn success-btn\" title=\"Completed\">\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\n </button>\n }\n @case ('error') {\n <button class=\"action-btn retry-btn\" title=\"Retry\">\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\n </button>\n }\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- No uploads message when manually opened -->\n <div class=\"no-uploads-message\">\n <div class=\"message-content\">\n <cide-ele-icon size=\"md\" class=\"message-icon\">cloud_upload</cide-ele-icon>\n <div class=\"message-text\">\n <h4>No active uploads</h4>\n <p>Upload files to see their progress here</p>\n </div>\n </div>\n </div>\n }\n </div>\n }\n</div>\n}\n", styles: [".floating-uploader{position:fixed;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.draggable-header{cursor:move;-webkit-user-select:none;user-select:none}.floating-uploader .uploader-header.draggable-header:hover{background:#f1f5f9}.floating-uploader .uploader-header.draggable-header:active{background:#e2e8f0;cursor:grabbing}.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 .upload-zone{margin:8px 16px;padding:12px;border:2px dashed #d1d5db;border-radius:6px;background:#f9fafb;cursor:pointer;transition:all .2s ease;text-align:center}.floating-uploader .uploader-content .upload-zone:hover{border-color:#3b82f6;background:#f0f9ff}.floating-uploader .uploader-content .upload-zone.drag-over{border-color:#3b82f6;background:#dbeafe;transform:scale(1.01)}.floating-uploader .uploader-content .upload-zone .upload-zone-content{display:flex;flex-direction:column;align-items:center;gap:6px}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-icon{color:#6b7280;transition:color .2s ease}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-text .upload-title{font-size:13px;font-weight:500;color:#374151;margin:0;line-height:1.2}.floating-uploader .uploader-content .upload-zone:hover .upload-zone-content .upload-icon{color:#3b82f6}.floating-uploader .uploader-content .upload-zone.drag-over .upload-zone-content .upload-icon{color:#1d4ed8}.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.draggable-header:hover{background:#475569}.floating-uploader .uploader-header.draggable-header:active{background:#64748b}.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 .upload-zone{border-color:#475569;background:#334155}.floating-uploader .uploader-content .upload-zone:hover{border-color:#3b82f6;background:#1e3a8a}.floating-uploader .uploader-content .upload-zone.drag-over{border-color:#60a5fa;background:#1e40af}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-icon{color:#94a3b8}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-text .upload-title{color:#f1f5f9}.floating-uploader .uploader-content .upload-zone:hover .upload-zone-content .upload-icon{color:#60a5fa}.floating-uploader .uploader-content .upload-zone.drag-over .upload-zone-content .upload-icon{color:#93c5fd}.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"] }]
3983
- }], ctorParameters: () => [] });
3984
-
3985
- class CideEleFileInputComponent {
3986
- fileManagerService = inject(CideEleFileManagerService);
3987
- notificationService = inject(NotificationService);
3988
- elementService = inject(CideElementsService);
3989
- destroyRef = inject(DestroyRef);
3990
- floatingUploader = inject(CideEleFloatingFileUploaderComponent, { optional: true });
3991
- // private readonly pendingTasks = inject(PendingTasks); // TODO: Fix PendingTasks API usage
3992
- // Traditional @Input() decorators
3993
- label = 'Choose file';
3994
- accept = '';
3995
- multiple = false;
3996
- disabled = false;
3997
- required = false;
3998
- helperText = '';
3999
- errorText = '';
4000
- showPreview = false;
4001
- previewWidth = '200px';
4002
- previewHeight = '200px';
4003
- previewBoxMode = false;
4004
- showFileName = true;
4005
- placeholderText = 'Click to select image';
4006
- placeholderIcon = '📷';
4007
- autoUpload = false;
4008
- uploadData = {};
4009
- showFloatingUploader = true;
4010
- floatingUploaderGroupId;
4011
- // Traditional @Output() decorators
4012
- fileChange = new EventEmitter();
4013
- uploadSuccess = new EventEmitter();
4014
- uploadError = new EventEmitter();
4015
- uploadProgressChange = new EventEmitter();
4016
- // Readable signals created from @Input() decorator values
4017
- labelSignal = signal(this.label, ...(ngDevMode ? [{ debugName: "labelSignal" }] : []));
4018
- acceptSignal = signal(this.accept, ...(ngDevMode ? [{ debugName: "acceptSignal" }] : []));
4019
- multipleSignal = signal(this.multiple, ...(ngDevMode ? [{ debugName: "multipleSignal" }] : []));
4020
- disabledSignal = signal(this.disabled, ...(ngDevMode ? [{ debugName: "disabledSignal" }] : []));
4021
- requiredSignal = signal(this.required, ...(ngDevMode ? [{ debugName: "requiredSignal" }] : []));
4022
- helperTextSignal = signal(this.helperText, ...(ngDevMode ? [{ debugName: "helperTextSignal" }] : []));
4023
- errorTextSignal = signal(this.errorText, ...(ngDevMode ? [{ debugName: "errorTextSignal" }] : []));
4024
- showPreviewSignal = signal(this.showPreview, ...(ngDevMode ? [{ debugName: "showPreviewSignal" }] : []));
4025
- previewWidthSignal = signal(this.previewWidth, ...(ngDevMode ? [{ debugName: "previewWidthSignal" }] : []));
4026
- previewHeightSignal = signal(this.previewHeight, ...(ngDevMode ? [{ debugName: "previewHeightSignal" }] : []));
4027
- previewBoxModeSignal = signal(this.previewBoxMode, ...(ngDevMode ? [{ debugName: "previewBoxModeSignal" }] : []));
4028
- showFileNameSignal = signal(this.showFileName, ...(ngDevMode ? [{ debugName: "showFileNameSignal" }] : []));
4029
- placeholderTextSignal = signal(this.placeholderText, ...(ngDevMode ? [{ debugName: "placeholderTextSignal" }] : []));
4030
- placeholderIconSignal = signal(this.placeholderIcon, ...(ngDevMode ? [{ debugName: "placeholderIconSignal" }] : []));
4031
- autoUploadSignal = signal(this.autoUpload, ...(ngDevMode ? [{ debugName: "autoUploadSignal" }] : []));
4032
- uploadDataSignal = signal(this.uploadData, ...(ngDevMode ? [{ debugName: "uploadDataSignal" }] : []));
4033
- showFloatingUploaderSignal = signal(this.showFloatingUploader, ...(ngDevMode ? [{ debugName: "showFloatingUploaderSignal" }] : []));
4034
- floatingUploaderGroupIdSignal = signal(this.floatingUploaderGroupId, ...(ngDevMode ? [{ debugName: "floatingUploaderGroupIdSignal" }] : []));
4035
- // Reactive state with signals
4036
- id = signal(Math.random().toString(36).substring(2, 10), ...(ngDevMode ? [{ debugName: "id" }] : []));
4037
- isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
4038
- uploadProgress = signal(0, ...(ngDevMode ? [{ debugName: "uploadProgress" }] : []));
4039
- uploadStatus = signal('idle', ...(ngDevMode ? [{ debugName: "uploadStatus" }] : []));
4040
- files = signal(null, ...(ngDevMode ? [{ debugName: "files" }] : []));
4041
- fileNames = signal([], ...(ngDevMode ? [{ debugName: "fileNames" }] : []));
4042
- previewUrls = signal([], ...(ngDevMode ? [{ debugName: "previewUrls" }] : []));
4043
- uploadNotificationId = signal(null, ...(ngDevMode ? [{ debugName: "uploadNotificationId" }] : []));
4044
- isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
4045
- groupId = signal(null, ...(ngDevMode ? [{ debugName: "groupId" }] : [])); // Group ID for multiple file uploads
4046
- isMultipleUploadMode = signal(false, ...(ngDevMode ? [{ debugName: "isMultipleUploadMode" }] : [])); // Flag to track if we're in multiple upload mode
4047
- hasEverUploaded = signal(false, ...(ngDevMode ? [{ debugName: "hasEverUploaded" }] : [])); // Track if this component has ever uploaded files
4048
- // Computed signals for better relationships
4049
- hasFiles = computed(() => this.files() !== null && this.files().length > 0, ...(ngDevMode ? [{ debugName: "hasFiles" }] : []));
4050
- canUpload = computed(() => this.hasFiles() && !this.isUploading() && !this.disabledSignal(), ...(ngDevMode ? [{ debugName: "canUpload" }] : []));
4051
- isInErrorState = computed(() => this.uploadStatus() === 'error', ...(ngDevMode ? [{ debugName: "isInErrorState" }] : []));
4052
- isInSuccessState = computed(() => this.uploadStatus() === 'success', ...(ngDevMode ? [{ debugName: "isInSuccessState" }] : []));
4053
- // Angular 20: Computed values using new features
4054
- totalFileSize = computed(() => {
4055
- if (!this.files())
4056
- return 0;
4057
- return Array.from(this.files()).reduce((total, file) => total + file.size, 0);
4058
- }, ...(ngDevMode ? [{ debugName: "totalFileSize" }] : []));
4059
- fileSizeInMB = computed(() => {
4060
- // Angular 20: Using ** operator for exponentiation
4061
- return (this.totalFileSize() / (1024 ** 2)).toFixed(2);
4062
- }, ...(ngDevMode ? [{ debugName: "fileSizeInMB" }] : []));
4063
- uploadProgressPercentage = computed(() => {
4064
- // Angular 20: Using ** operator for exponentiation
4065
- return Math.round(this.uploadProgress() ** 1); // Simple power operation
4066
- }, ...(ngDevMode ? [{ debugName: "uploadProgressPercentage" }] : []));
4067
- // ControlValueAccessor callbacks
4068
- onChange = (value) => { };
4069
- onTouched = () => { };
4070
- onValidatorChange = () => { };
4071
- // Computed values
4072
- hasImages = computed(() => this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "hasImages" }] : []));
4073
- isPreviewBoxMode = computed(() => this.previewBoxModeSignal() && this.showPreviewSignal(), ...(ngDevMode ? [{ debugName: "isPreviewBoxMode" }] : []));
4074
- isImagePreviewAvailable = computed(() => this.showPreviewSignal() && this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "isImagePreviewAvailable" }] : []));
4075
- constructor() {
4076
- // Angular 20: afterRenderEffect for DOM operations
4077
- afterRenderEffect(() => {
4078
- // Update file input element when files change
4079
- if (this.files()) {
4080
- const fileInput = document.getElementById('cide-file-input-' + this.id());
4081
- if (fileInput) {
4082
- // Ensure the input reflects the current state
4083
- fileInput.files = this.files();
4084
- }
4085
- }
4086
- });
4087
- // Angular 20: afterNextRender for one-time DOM operations
4088
- afterNextRender(() => {
4089
- console.log('🎯 [FileInput] Component rendered and DOM is ready');
4090
- });
4091
- }
4092
- ngOnInit() {
4093
- // Update signals with initial @Input() values
4094
- this.labelSignal.set(this.label);
4095
- this.acceptSignal.set(this.accept);
4096
- this.multipleSignal.set(this.multiple);
4097
- this.disabledSignal.set(this.disabled);
4098
- this.requiredSignal.set(this.required);
4099
- this.helperTextSignal.set(this.helperText);
4100
- this.errorTextSignal.set(this.errorText);
4101
- this.showPreviewSignal.set(this.showPreview);
4102
- this.previewWidthSignal.set(this.previewWidth);
4103
- this.previewHeightSignal.set(this.previewHeight);
4104
- this.previewBoxModeSignal.set(this.previewBoxMode);
4105
- this.showFileNameSignal.set(this.showFileName);
4106
- this.placeholderTextSignal.set(this.placeholderText);
4107
- this.placeholderIconSignal.set(this.placeholderIcon);
4108
- this.autoUploadSignal.set(this.autoUpload);
4109
- this.uploadDataSignal.set(this.uploadData);
4110
- }
4111
- ngOnChanges(changes) {
4112
- // Angular 20: Update signals when @Input() values change
4113
- if (changes['label'])
4114
- this.labelSignal.set(this.label);
4115
- if (changes['accept'])
4116
- this.acceptSignal.set(this.accept);
4117
- if (changes['multiple'])
4118
- this.multipleSignal.set(this.multiple);
4119
- if (changes['disabled'])
4120
- this.disabledSignal.set(this.disabled);
4121
- if (changes['required'])
4122
- this.requiredSignal.set(this.required);
4123
- if (changes['helperText'])
4124
- this.helperTextSignal.set(this.helperText);
4125
- if (changes['errorText'])
4126
- this.errorTextSignal.set(this.errorText);
4127
- if (changes['showPreview'])
4128
- this.showPreviewSignal.set(this.showPreview);
4129
- if (changes['previewWidth'])
4130
- this.previewWidthSignal.set(this.previewWidth);
4131
- if (changes['previewHeight'])
4132
- this.previewHeightSignal.set(this.previewHeight);
4133
- if (changes['previewBoxMode'])
4134
- this.previewBoxModeSignal.set(this.previewBoxMode);
4135
- if (changes['showFileName'])
4136
- this.showFileNameSignal.set(this.showFileName);
4137
- if (changes['placeholderText'])
4138
- this.placeholderTextSignal.set(this.placeholderText);
4139
- if (changes['placeholderIcon'])
4140
- this.placeholderIconSignal.set(this.placeholderIcon);
4141
- if (changes['autoUpload'])
4142
- this.autoUploadSignal.set(this.autoUpload);
4143
- if (changes['uploadData'])
4144
- this.uploadDataSignal.set(this.uploadData);
4145
- }
4146
- writeValue(value) {
4147
- console.log('📝 [FileInput] writeValue called with:', value);
4148
- if (typeof value === 'string') {
4149
- // Check if this is a group ID for multiple files or single file ID
4150
- if (this.isMultipleFileMode()) {
4151
- // Multiple file mode - value is group ID
4152
- console.log('📁 [FileInput] Value is group ID for multiple files:', value);
4153
- this.groupId.set(value);
4154
- this.loadFilesFromGroupId(value);
4155
- }
4156
- else {
4157
- // Single file mode - value is file ID
4158
- console.log('📝 [FileInput] Value is single file ID:', value);
4159
- this.files.set(null);
4160
- this.fileNames.set([]);
4161
- this.clearPreviews();
4162
- // Fetch file details to get base64 and set preview
4163
- this.loadFileDetailsFromId(value);
4164
- }
4165
- }
4166
- else if (value instanceof FileList) {
4167
- // Value is a FileList
4168
- console.log('📝 [FileInput] Value is FileList:', Array.from(value).map(f => f.name));
4169
- this.files.set(value);
4170
- this.fileNames.set(Array.from(value).map(f => f.name));
4171
- this.generatePreviews();
4172
- // For multiple files, use group ID API to fetch files
4173
- if (value.length > 1) {
4174
- const groupId = this.groupId();
4175
- if (groupId) {
4176
- console.log('📁 [FileInput] Multiple files detected, fetching files for group:', groupId);
4177
- this.loadFilesFromGroupId(groupId);
4178
- }
4179
- }
4180
- }
4181
- else {
4182
- // Value is null
4183
- console.log('📝 [FileInput] Value is null');
4184
- this.files.set(null);
4185
- this.fileNames.set([]);
4186
- this.clearPreviews();
4187
- }
4188
- }
4189
- registerOnChange(fn) {
4190
- this.onChange = fn;
4191
- }
4192
- registerOnTouched(fn) {
4193
- this.onTouched = fn;
4194
- }
4195
- registerOnValidatorChange(fn) {
4196
- this.onValidatorChange = fn;
4197
- }
4198
- setDisabledState(isDisabled) {
4199
- // Note: With input signals, disabled state is controlled by the parent component
4200
- // This method is kept for ControlValueAccessor compatibility but doesn't modify the signal
4201
- console.log('🔧 [FileInput] setDisabledState called with:', isDisabled, '(controlled by parent component)');
4202
- }
4203
- onFileSelected(event) {
4204
- console.log('🔍 [FileInput] onFileSelected called');
4205
- const input = event.target;
4206
- const selectedFiles = input.files;
4207
- this.files.set(selectedFiles);
4208
- this.fileNames.set(selectedFiles ? Array.from(selectedFiles).map(f => f.name) : []);
4209
- console.log('📁 [FileInput] Files selected:', this.fileNames());
4236
+ handleFileSelection(files) {
4237
+ this.files.set(files);
4238
+ this.fileNames.set(Array.from(files).map(f => f.name));
4239
+ console.log('📁 [FileInput] Files selected via drag & drop:', this.fileNames());
4210
4240
  this.generatePreviews();
4211
4241
  // Reset upload status when new file is selected
4212
4242
  this.uploadStatus.set('idle');
4213
4243
  console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
4214
- this.onChange(selectedFiles);
4215
- this.fileChange.emit(selectedFiles);
4244
+ this.onChange(files);
4245
+ this.fileChange.emit(files);
4216
4246
  this.onTouched();
4217
- // Show floating uploader if enabled and files are selected
4218
- if (this.showFloatingUploaderSignal() && selectedFiles && selectedFiles.length > 0 && this.floatingUploader) {
4219
- console.log('👁️ [FileInput] Showing floating uploader for', selectedFiles.length, 'files');
4220
- this.floatingUploader.showUploader(this.floatingUploaderGroupIdSignal());
4221
- }
4247
+ // Note: Floating uploader is now triggered via service in upload methods
4222
4248
  // Auto upload if enabled
4223
- if (this.autoUploadSignal() && selectedFiles && selectedFiles.length > 0) {
4249
+ if (this.autoUploadSignal() && files.length > 0) {
4224
4250
  if (this.multipleSignal()) {
4225
- console.log('🚀 [FileInput] Auto upload enabled for multiple files mode:', selectedFiles.length, 'files');
4226
- this.uploadMultipleFiles(Array.from(selectedFiles));
4251
+ console.log('🚀 [FileInput] Auto upload enabled for multiple files mode (drag & drop):', files.length, 'files');
4252
+ this.uploadMultipleFiles(Array.from(files));
4227
4253
  }
4228
4254
  else {
4229
- console.log('🚀 [FileInput] Auto upload enabled for single file mode:', selectedFiles[0].name);
4230
- this.uploadFile(selectedFiles[0]);
4255
+ console.log('🚀 [FileInput] Auto upload enabled for single file mode (drag & drop):', files[0].name);
4256
+ this.uploadFile(files[0]);
4231
4257
  }
4232
4258
  }
4233
4259
  else {
4234
4260
  console.log('⏸️ [FileInput] Auto upload disabled or no files');
4235
4261
  }
4236
4262
  }
4237
- clearFiles() {
4238
- console.log('🗑️ [FileInput] clearFiles called');
4239
- this.files.set(null);
4240
- this.fileNames.set([]);
4241
- this.clearPreviews();
4242
- this.uploadStatus.set('idle');
4243
- console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
4244
- this.onChange(null);
4245
- this.fileChange.emit(null);
4263
+ isRequired() {
4264
+ return this.requiredSignal();
4246
4265
  }
4247
- uploadFile(file) {
4248
- console.log('📤 [FileInput] uploadFile called for:', file.name, 'Size:', file.size, 'bytes');
4249
- // Angular 20: Use PendingTasks for better loading state management
4250
- // const uploadTask = this.pendingTasks.add(); // TODO: Fix PendingTasks API usage
4251
- // console.log('⏳ [FileInput] Pending task added for upload tracking');
4252
- // Set upload status to 'start' before starting upload
4253
- this.uploadStatus.set('start');
4254
- console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
4255
- this.isUploading.set(true);
4256
- this.uploadProgress.set(0);
4257
- this.uploadProgressChange.emit(0);
4258
- console.log('📊 [FileInput] Upload progress initialized to 0%');
4259
- // Make form control invalid during upload - this prevents form submission
4260
- this.onChange(null);
4261
- console.log('🚫 [FileInput] Form control value set to null to prevent submission');
4262
- // Show initial progress notification with spinner (persistent - no auto-dismiss)
4263
- const notificationId = this.notificationService.showProgress('🔄 Preparing file upload...', 0, { duration: 0 });
4264
- this.uploadNotificationId.set(notificationId);
4265
- console.log('🔔 [FileInput] Progress notification started with ID:', notificationId);
4266
- this.fileManagerService.uploadFile(file, this.uploadDataSignal(), (progress) => {
4267
- // Real progress callback from file manager service
4268
- this.uploadProgress.set(progress);
4269
- this.uploadProgressChange.emit(progress);
4270
- console.log('📈 [FileInput] Upload progress:', Math.round(progress) + '%');
4271
- // Set upload status to 'uploading' when progress starts
4272
- if (this.uploadStatus() === 'start') {
4273
- this.uploadStatus.set('uploading');
4274
- console.log('🔄 [FileInput] Upload status changed to:', this.uploadStatus());
4275
- }
4276
- // Update progress notification with spinner
4277
- const notificationId = this.uploadNotificationId();
4278
- if (notificationId) {
4279
- let progressMessage = '';
4280
- if (progress < 10) {
4281
- progressMessage = '🔄 Starting upload...';
4282
- }
4283
- else if (progress < 25) {
4284
- progressMessage = '🔄 Uploading file...';
4285
- }
4286
- else if (progress < 50) {
4287
- progressMessage = '🔄 Upload in progress...';
4288
- }
4289
- else if (progress < 75) {
4290
- progressMessage = '🔄 Almost done...';
4291
- }
4292
- else if (progress < 95) {
4293
- progressMessage = '🔄 Finishing upload...';
4294
- }
4295
- else {
4296
- progressMessage = '🔄 Finalizing...';
4297
- }
4298
- this.notificationService.updateProgress(notificationId, progress, progressMessage);
4266
+ /**
4267
+ * Angular 20: Utility method to get upload data with proper typing
4268
+ * @returns Properly typed upload data
4269
+ */
4270
+ getUploadData() {
4271
+ return this.uploadDataSignal();
4272
+ }
4273
+ /**
4274
+ * Angular 20: Utility method to update upload data with type safety
4275
+ * @param data Partial upload data to merge with existing data
4276
+ */
4277
+ updateUploadData(data) {
4278
+ const currentData = this.uploadDataSignal();
4279
+ const updatedData = { ...currentData, ...data };
4280
+ // Note: This would require the uploadData to be a writable signal
4281
+ // For now, this method serves as a type-safe way to work with upload data
4282
+ console.log('📝 [FileInput] Upload data updated:', updatedData);
4283
+ }
4284
+ getCurrentState() {
4285
+ return {
4286
+ id: this.id(),
4287
+ label: this.labelSignal(),
4288
+ required: this.requiredSignal(),
4289
+ disabled: this.disabledSignal(),
4290
+ accept: this.acceptSignal(),
4291
+ multiple: this.multipleSignal(),
4292
+ showPreview: this.showPreviewSignal(),
4293
+ autoUpload: this.autoUploadSignal(),
4294
+ uploadStatus: this.uploadStatus(),
4295
+ isUploading: this.isUploading(),
4296
+ uploadProgress: this.uploadProgress(),
4297
+ files: this.files() ? Array.from(this.files()).map(f => ({ name: f.name, size: f.size, type: f.type })) : null,
4298
+ fileNames: this.fileNames(),
4299
+ previewUrls: this.previewUrls().length,
4300
+ helperText: this.helperTextSignal(),
4301
+ errorText: this.errorTextSignal(),
4302
+ placeholderText: this.placeholderTextSignal(),
4303
+ placeholderIcon: this.placeholderIconSignal(),
4304
+ previewWidth: this.previewWidthSignal(),
4305
+ previewHeight: this.previewHeightSignal(),
4306
+ previewBoxMode: this.previewBoxModeSignal(),
4307
+ showFileName: this.showFileNameSignal(),
4308
+ uploadData: this.uploadDataSignal()
4309
+ };
4310
+ }
4311
+ async getControlData() {
4312
+ console.log('🔍 [FileInput] getControlData called');
4313
+ const cide_element_data = await this.elementService?.getElementData({ sype_key: this.id() });
4314
+ if (cide_element_data) {
4315
+ console.log('📋 [FileInput] Element data loaded:', cide_element_data);
4316
+ // Note: Since we're using input signals, we can't directly set their values
4317
+ // This method would need to be refactored to work with the new signal-based approach
4318
+ // For now, we'll log the data and trigger validation
4319
+ console.log('✅ [FileInput] Control data received from element service');
4320
+ console.log('⚠️ [FileInput] Note: Input signals cannot be modified after component initialization');
4321
+ // Trigger validation update
4322
+ this.onValidatorChange();
4323
+ }
4324
+ else {
4325
+ console.log('⚠️ [FileInput] No element data found for key:', this.id());
4326
+ }
4327
+ }
4328
+ // Validator implementation
4329
+ validate(control) {
4330
+ console.log('🔍 [FileInput] validate() called - uploadStatus:', this.uploadStatus(), 'required:', this.requiredSignal(), 'files:', !!this.files(), 'control.value:', control.value);
4331
+ // If upload is in progress (start or uploading status), return validation error
4332
+ if (this.uploadStatus() === 'start' || this.uploadStatus() === 'uploading') {
4333
+ console.log('⚠️ [FileInput] Validation ERROR: Upload in progress');
4334
+ return { 'uploadInProgress': { message: 'File upload in progress. Please wait...' } };
4335
+ }
4336
+ // If required and no file is selected and no control value (uploaded file ID), return validation error
4337
+ if (this.requiredSignal() && !this.files() && !control.value) {
4338
+ console.log('⚠️ [FileInput] Validation ERROR: File required');
4339
+ return { 'required': { message: 'Please select a file to upload.' } };
4340
+ }
4341
+ console.log('✅ [FileInput] Validation PASSED: No errors');
4342
+ return null; // No validation errors
4343
+ }
4344
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4345
+ 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: [
4346
+ {
4347
+ provide: NG_VALUE_ACCESSOR,
4348
+ useExisting: CideEleFileInputComponent,
4349
+ multi: true
4350
+ },
4351
+ {
4352
+ provide: NG_VALIDATORS,
4353
+ useExisting: CideEleFileInputComponent,
4354
+ multi: true
4299
4355
  }
4300
- }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
4301
- next: (response) => {
4302
- console.log('🎉 [FileInput] Upload SUCCESS - Response received:', response);
4303
- // Angular 20: Complete the pending task
4304
- // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
4305
- // console.log('✅ [FileInput] Pending task completed for successful upload');
4306
- // Set upload status to 'success'
4307
- this.uploadStatus.set('success');
4308
- console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
4309
- // Complete the progress
4310
- this.uploadProgress.set(100);
4311
- this.uploadProgressChange.emit(100);
4312
- console.log('📊 [FileInput] Upload progress completed: 100%');
4313
- // Update progress notification to complete
4314
- const notificationId = this.uploadNotificationId();
4315
- if (notificationId) {
4316
- this.notificationService.remove(notificationId);
4317
- console.log('🔔 [FileInput] Progress notification removed');
4318
- }
4319
- // Success notification removed for cleaner UX
4320
- this.uploadNotificationId.set(null);
4321
- // Extract ID from CoreFileManagerInsertUpdateResponse
4322
- const uploadedId = response?.data?.core_file_manager?.[0]?.cyfm_id;
4323
- if (uploadedId) {
4324
- console.log('✅ [FileInput] File uploaded successfully with ID:', uploadedId);
4325
- // Set the uploaded ID as the form control value
4326
- this.onChange(uploadedId);
4327
- console.log('📝 [FileInput] Form control value set to uploaded ID:', uploadedId);
4328
- // Only emit individual uploadSuccess if not in multiple upload mode
4329
- if (!this.isMultipleUploadMode()) {
4330
- this.uploadSuccess.emit(uploadedId);
4331
- console.log('📝 [FileInput] Upload success event emitted with file ID:', uploadedId);
4332
- }
4333
- else {
4334
- console.log('📝 [FileInput] Individual upload success suppressed (multiple upload mode) - file ID:', uploadedId);
4335
- }
4356
+ ], usesOnChanges: true, ngImport: i0, template: "<div class=\"tw-flex tw-flex-col tw-gap-2\">\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=\"tw-block tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-1.5 tw-leading-5\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"tw-text-red-500 dark:tw-text-red-400\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"tw-relative\">\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=\"tw-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-relative tw-overflow-hidden hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\n [class]=\"getPreviewBoxClasses()\"\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=\"tw-flex tw-flex-col tw-items-center tw-justify-center tw-h-full tw-p-4\">\n <div class=\"tw-mb-2\">\n <cide-ele-icon class=\"tw-text-gray-400 dark:tw-text-gray-500\" size=\"lg\">{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\n </div>\n <div class=\"tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center\">\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"tw-relative tw-w-full tw-h-full\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"tw-w-full tw-h-full tw-object-cover tw-rounded-lg\">\n <div class=\"tw-absolute tw-inset-0 tw-bg-black tw-bg-opacity-0 hover:tw-bg-opacity-30 tw-transition-all tw-duration-200 tw-flex tw-items-center tw-justify-center tw-rounded-lg\">\n <div class=\"tw-text-white tw-text-sm tw-opacity-0 hover:tw-opacity-100 tw-transition-opacity tw-duration-200\">Click to change</div>\n </div>\n @if (!disabledSignal()) {\n <button \n type=\"button\" \n class=\"tw-absolute tw-top-2 tw-right-2 tw-w-6 tw-h-6 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-sm tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\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=\"tw-mt-2 tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center tw-truncate\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\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=\"tw-hidden\"\n />\n \n <!-- Modern Drag and Drop Zone -->\n <div \n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-min-h-[60px] hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\n [class]=\"getDragDropZoneClasses()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n \n <div class=\"tw-flex tw-items-center tw-justify-between tw-p-3 tw-gap-3\">\n <!-- Icon and Text -->\n <div class=\"tw-flex tw-items-center tw-gap-2.5 tw-flex-1 tw-min-w-0\">\n <cide-ele-icon class=\"tw-flex-shrink-0 tw-transition-colors tw-duration-200\" \n [class]=\"getIconClasses()\" \n size=\"sm\">\n {{ isDragOver() ? 'file_download' : (hasFiles() ? 'check_circle' : 'cloud_upload') }}\n </cide-ele-icon>\n \n <div class=\"tw-flex tw-flex-col tw-gap-0.5 tw-min-w-0\">\n @if (isDragOver()) {\n <span class=\"tw-text-sm tw-font-medium tw-text-blue-700 dark:tw-text-blue-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">Drop files here</span>\n } @else if (hasFiles()) {\n <span class=\"tw-text-sm tw-font-medium tw-text-emerald-700 dark:tw-text-emerald-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\n @if (multipleSignal() && fileNames().length > 1) {\n {{ fileNames().length }} files selected\n } @else {\n {{ fileNames()[0] }}\n }\n </span>\n @if (totalFileSize() > 0) {\n <span class=\"tw-text-xs tw-text-emerald-600 dark:tw-text-emerald-400\">{{ fileSizeInMB() }} MB</span>\n }\n } @else {\n <span class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\n {{ multipleSignal() ? 'Choose files or drag here' : 'Choose file or drag here' }}\n </span>\n }\n </div>\n </div>\n \n <!-- Action Buttons -->\n <div class=\"tw-flex tw-gap-1 tw-flex-shrink-0\">\n @if (hasFiles()) {\n <button type=\"button\" \n class=\"tw-flex tw-items-center tw-justify-center tw-w-6 tw-h-6 tw-border-none tw-rounded tw-bg-transparent tw-cursor-pointer tw-transition-all tw-duration-200 tw-text-red-600 hover:tw-bg-red-50 dark:hover:tw-bg-red-900/20 hover:tw-text-red-700\" \n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Clear files\">\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\n </button>\n }\n </div>\n </div>\n </div>\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"tw-mt-3\">\n <div class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-2\">Preview:</div>\n <div class=\"tw-flex tw-flex-wrap tw-gap-3\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"tw-relative tw-border tw-border-gray-200 dark:tw-border-gray-600 tw-rounded-lg tw-overflow-hidden tw-bg-white dark:tw-bg-gray-800\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\">\n <button \n type=\"button\" \n class=\"tw-absolute tw-top-1 tw-right-1 tw-w-5 tw-h-5 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-xs tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer tw-z-10\"\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=\"tw-w-full tw-h-full tw-object-cover\"\n loading=\"lazy\">\n <div class=\"tw-absolute tw-bottom-0 tw-left-0 tw-right-0 tw-bg-black tw-bg-opacity-75 tw-text-white tw-text-xs tw-p-1 tw-truncate\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n <!-- Upload Status and Show Files Button (only for multiple file inputs) -->\n @if (multiple && showFloatingUploaderSignal() && (getUploadCount() > 0 || hasActiveUploads() || hasEverUploaded())) {\n <div class=\"tw-flex tw-items-center tw-justify-between tw-py-1.5 tw-gap-2\">\n <div class=\"tw-flex tw-items-center tw-gap-2\">\n <cide-ele-icon class=\"tw-text-blue-600 dark:tw-text-blue-400\" size=\"sm\">cloud_upload</cide-ele-icon>\n <span class=\"tw-text-sm tw-text-gray-700 dark:tw-text-gray-300\">\n @if (hasActiveUploads()) {\n {{ getActiveUploadCount() }} uploading\n } @else if (getUploadCount() > 0) {\n {{ getUploadCount() }} completed\n } @else if (hasEverUploaded()) {\n View uploads\n }\n </span>\n </div>\n <button \n type=\"button\" \n class=\"tw-flex tw-items-center tw-justify-center tw-w-8 tw-h-8 tw-rounded-md tw-bg-gray-100 dark:tw-bg-gray-700 hover:tw-bg-gray-200 dark:hover:tw-bg-gray-600 tw-text-gray-600 dark:tw-text-gray-300 tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\n (click)=\"showFloatingUploaderDialog()\"\n title=\"View upload progress and history\">\n <cide-ele-icon size=\"sm\">visibility</cide-ele-icon>\n </button>\n </div>\n }\n \n @if (errorTextSignal()) {\n <div class=\"tw-text-sm tw-text-red-600 dark:tw-text-red-400 tw-mt-1\">{{ errorTextSignal() }}</div>\n }\n @if (helperTextSignal() && !errorTextSignal()) {\n <div class=\"tw-text-sm tw-text-gray-500 dark:tw-text-gray-400 tw-mt-1\">{{ helperTextSignal() }}</div>\n }\n</div> ", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
4357
+ }
4358
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, decorators: [{
4359
+ type: Component,
4360
+ args: [{ selector: 'cide-ele-file-input', standalone: true, imports: [CommonModule, FormsModule, CideIconComponent], providers: [
4361
+ {
4362
+ provide: NG_VALUE_ACCESSOR,
4363
+ useExisting: CideEleFileInputComponent,
4364
+ multi: true
4365
+ },
4366
+ {
4367
+ provide: NG_VALIDATORS,
4368
+ useExisting: CideEleFileInputComponent,
4369
+ multi: true
4370
+ }
4371
+ ], template: "<div class=\"tw-flex tw-flex-col tw-gap-2\">\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=\"tw-block tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-1.5 tw-leading-5\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"tw-text-red-500 dark:tw-text-red-400\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"tw-relative\">\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=\"tw-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-relative tw-overflow-hidden hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\n [class]=\"getPreviewBoxClasses()\"\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=\"tw-flex tw-flex-col tw-items-center tw-justify-center tw-h-full tw-p-4\">\n <div class=\"tw-mb-2\">\n <cide-ele-icon class=\"tw-text-gray-400 dark:tw-text-gray-500\" size=\"lg\">{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\n </div>\n <div class=\"tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center\">\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"tw-relative tw-w-full tw-h-full\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"tw-w-full tw-h-full tw-object-cover tw-rounded-lg\">\n <div class=\"tw-absolute tw-inset-0 tw-bg-black tw-bg-opacity-0 hover:tw-bg-opacity-30 tw-transition-all tw-duration-200 tw-flex tw-items-center tw-justify-center tw-rounded-lg\">\n <div class=\"tw-text-white tw-text-sm tw-opacity-0 hover:tw-opacity-100 tw-transition-opacity tw-duration-200\">Click to change</div>\n </div>\n @if (!disabledSignal()) {\n <button \n type=\"button\" \n class=\"tw-absolute tw-top-2 tw-right-2 tw-w-6 tw-h-6 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-sm tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\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=\"tw-mt-2 tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center tw-truncate\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\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=\"tw-hidden\"\n />\n \n <!-- Modern Drag and Drop Zone -->\n <div \n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-min-h-[60px] hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\n [class]=\"getDragDropZoneClasses()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n \n <div class=\"tw-flex tw-items-center tw-justify-between tw-p-3 tw-gap-3\">\n <!-- Icon and Text -->\n <div class=\"tw-flex tw-items-center tw-gap-2.5 tw-flex-1 tw-min-w-0\">\n <cide-ele-icon class=\"tw-flex-shrink-0 tw-transition-colors tw-duration-200\" \n [class]=\"getIconClasses()\" \n size=\"sm\">\n {{ isDragOver() ? 'file_download' : (hasFiles() ? 'check_circle' : 'cloud_upload') }}\n </cide-ele-icon>\n \n <div class=\"tw-flex tw-flex-col tw-gap-0.5 tw-min-w-0\">\n @if (isDragOver()) {\n <span class=\"tw-text-sm tw-font-medium tw-text-blue-700 dark:tw-text-blue-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">Drop files here</span>\n } @else if (hasFiles()) {\n <span class=\"tw-text-sm tw-font-medium tw-text-emerald-700 dark:tw-text-emerald-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\n @if (multipleSignal() && fileNames().length > 1) {\n {{ fileNames().length }} files selected\n } @else {\n {{ fileNames()[0] }}\n }\n </span>\n @if (totalFileSize() > 0) {\n <span class=\"tw-text-xs tw-text-emerald-600 dark:tw-text-emerald-400\">{{ fileSizeInMB() }} MB</span>\n }\n } @else {\n <span class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\n {{ multipleSignal() ? 'Choose files or drag here' : 'Choose file or drag here' }}\n </span>\n }\n </div>\n </div>\n \n <!-- Action Buttons -->\n <div class=\"tw-flex tw-gap-1 tw-flex-shrink-0\">\n @if (hasFiles()) {\n <button type=\"button\" \n class=\"tw-flex tw-items-center tw-justify-center tw-w-6 tw-h-6 tw-border-none tw-rounded tw-bg-transparent tw-cursor-pointer tw-transition-all tw-duration-200 tw-text-red-600 hover:tw-bg-red-50 dark:hover:tw-bg-red-900/20 hover:tw-text-red-700\" \n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Clear files\">\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\n </button>\n }\n </div>\n </div>\n </div>\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"tw-mt-3\">\n <div class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-2\">Preview:</div>\n <div class=\"tw-flex tw-flex-wrap tw-gap-3\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"tw-relative tw-border tw-border-gray-200 dark:tw-border-gray-600 tw-rounded-lg tw-overflow-hidden tw-bg-white dark:tw-bg-gray-800\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\">\n <button \n type=\"button\" \n class=\"tw-absolute tw-top-1 tw-right-1 tw-w-5 tw-h-5 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-xs tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer tw-z-10\"\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=\"tw-w-full tw-h-full tw-object-cover\"\n loading=\"lazy\">\n <div class=\"tw-absolute tw-bottom-0 tw-left-0 tw-right-0 tw-bg-black tw-bg-opacity-75 tw-text-white tw-text-xs tw-p-1 tw-truncate\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n <!-- Upload Status and Show Files Button (only for multiple file inputs) -->\n @if (multiple && showFloatingUploaderSignal() && (getUploadCount() > 0 || hasActiveUploads() || hasEverUploaded())) {\n <div class=\"tw-flex tw-items-center tw-justify-between tw-py-1.5 tw-gap-2\">\n <div class=\"tw-flex tw-items-center tw-gap-2\">\n <cide-ele-icon class=\"tw-text-blue-600 dark:tw-text-blue-400\" size=\"sm\">cloud_upload</cide-ele-icon>\n <span class=\"tw-text-sm tw-text-gray-700 dark:tw-text-gray-300\">\n @if (hasActiveUploads()) {\n {{ getActiveUploadCount() }} uploading\n } @else if (getUploadCount() > 0) {\n {{ getUploadCount() }} completed\n } @else if (hasEverUploaded()) {\n View uploads\n }\n </span>\n </div>\n <button \n type=\"button\" \n class=\"tw-flex tw-items-center tw-justify-center tw-w-8 tw-h-8 tw-rounded-md tw-bg-gray-100 dark:tw-bg-gray-700 hover:tw-bg-gray-200 dark:hover:tw-bg-gray-600 tw-text-gray-600 dark:tw-text-gray-300 tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\n (click)=\"showFloatingUploaderDialog()\"\n title=\"View upload progress and history\">\n <cide-ele-icon size=\"sm\">visibility</cide-ele-icon>\n </button>\n </div>\n }\n \n @if (errorTextSignal()) {\n <div class=\"tw-text-sm tw-text-red-600 dark:tw-text-red-400 tw-mt-1\">{{ errorTextSignal() }}</div>\n }\n @if (helperTextSignal() && !errorTextSignal()) {\n <div class=\"tw-text-sm tw-text-gray-500 dark:tw-text-gray-400 tw-mt-1\">{{ helperTextSignal() }}</div>\n }\n</div> " }]
4372
+ }], ctorParameters: () => [], propDecorators: { label: [{
4373
+ type: Input
4374
+ }], accept: [{
4375
+ type: Input
4376
+ }], multiple: [{
4377
+ type: Input
4378
+ }], disabled: [{
4379
+ type: Input
4380
+ }], required: [{
4381
+ type: Input
4382
+ }], helperText: [{
4383
+ type: Input
4384
+ }], errorText: [{
4385
+ type: Input
4386
+ }], showPreview: [{
4387
+ type: Input
4388
+ }], previewWidth: [{
4389
+ type: Input
4390
+ }], previewHeight: [{
4391
+ type: Input
4392
+ }], previewBoxMode: [{
4393
+ type: Input
4394
+ }], showFileName: [{
4395
+ type: Input
4396
+ }], placeholderText: [{
4397
+ type: Input
4398
+ }], placeholderIcon: [{
4399
+ type: Input
4400
+ }], autoUpload: [{
4401
+ type: Input
4402
+ }], uploadData: [{
4403
+ type: Input
4404
+ }], showFloatingUploader: [{
4405
+ type: Input
4406
+ }], floatingUploaderGroupId: [{
4407
+ type: Input
4408
+ }], fileChange: [{
4409
+ type: Output
4410
+ }], uploadSuccess: [{
4411
+ type: Output
4412
+ }], uploadError: [{
4413
+ type: Output
4414
+ }], uploadProgressChange: [{
4415
+ type: Output
4416
+ }] } });
4417
+
4418
+ class CideEleFloatingFileUploaderComponent {
4419
+ destroyRef = inject(DestroyRef);
4420
+ fileManagerService = inject(CideEleFileManagerService);
4421
+ // Signals for reactive state
4422
+ isVisible = signal(false, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
4423
+ isMinimized = signal(false, ...(ngDevMode ? [{ debugName: "isMinimized" }] : []));
4424
+ currentUserId = signal('', ...(ngDevMode ? [{ debugName: "currentUserId" }] : []));
4425
+ currentGroupId = signal(null, ...(ngDevMode ? [{ debugName: "currentGroupId" }] : []));
4426
+ // Use file manager service as the single source of truth
4427
+ uploadQueue = computed(() => this.fileManagerService.uploadQueue(), ...(ngDevMode ? [{ debugName: "uploadQueue" }] : []));
4428
+ activeUploads = computed(() => this.fileManagerService.activeUploads(), ...(ngDevMode ? [{ debugName: "activeUploads" }] : []));
4429
+ // Computed values based on service state
4430
+ hasUploads = computed(() => this.uploadQueue().length > 0 || this.activeUploads().size > 0, ...(ngDevMode ? [{ debugName: "hasUploads" }] : []));
4431
+ hasActiveUploads = computed(() => this.uploadQueue().length > 0 || Array.from(this.activeUploads().values()).some(upload => upload.stage !== 'complete'), ...(ngDevMode ? [{ debugName: "hasActiveUploads" }] : []));
4432
+ pendingUploads = computed(() => this.uploadQueue().filter(fileId => !this.activeUploads().has(fileId)), ...(ngDevMode ? [{ debugName: "pendingUploads" }] : []));
4433
+ activeUploadsLocal = computed(() => Array.from(this.activeUploads().values()).filter(upload => upload.stage === 'reading' || upload.stage === 'uploading'), ...(ngDevMode ? [{ debugName: "activeUploadsLocal" }] : []));
4434
+ completedUploads = computed(() => Array.from(this.activeUploads().values()).filter(upload => upload.stage === 'complete'), ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
4435
+ failedUploads = computed(() => Array.from(this.activeUploads().values()).filter(upload => upload.stage === 'error'), ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
4436
+ // Get all files for the current group (computed property for reactivity)
4437
+ allFilesForGroup = computed(() => {
4438
+ const groupId = this.currentGroupId();
4439
+ if (!groupId)
4440
+ return [];
4441
+ return this.fileManagerService.getAllFilesForGroup(groupId);
4442
+ }, ...(ngDevMode ? [{ debugName: "allFilesForGroup" }] : []));
4443
+ // Check if there are any files to display (active uploads OR fetched files for current group)
4444
+ hasFilesToShow = computed(() => {
4445
+ return this.hasActiveUploads() || this.allFilesForGroup().length > 0;
4446
+ }, ...(ngDevMode ? [{ debugName: "hasFilesToShow" }] : []));
4447
+ // Animation states
4448
+ isAnimating = signal(false, ...(ngDevMode ? [{ debugName: "isAnimating" }] : []));
4449
+ // Drag functionality
4450
+ isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
4451
+ position = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "position" }] : []));
4452
+ dragOffset = { x: 0, y: 0 };
4453
+ // File drag and drop functionality
4454
+ isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
4455
+ // Window resize handler reference for cleanup
4456
+ windowResizeHandler;
4457
+ // Cached dimensions for performance
4458
+ cachedDimensions = { width: 320, height: 200 };
4459
+ lastDimensionUpdate = 0;
4460
+ constructor() {
4461
+ console.log('🚀 [FloatingFileUploader] Component initialized');
4462
+ // Initialize default position
4463
+ this.initializePosition();
4464
+ // Consolidated effect for all visibility logic - SINGLE SOURCE OF TRUTH
4465
+ effect(() => {
4466
+ const hasActiveUploads = this.hasActiveUploads();
4467
+ const shouldShow = this.fileManagerService.showFloatingUploader();
4468
+ const triggerGroupId = this.fileManagerService.getTriggerGroupId();
4469
+ const isCurrentlyVisible = this.isVisible();
4470
+ // Show due to active uploads
4471
+ if (hasActiveUploads && !isCurrentlyVisible) {
4472
+ this.showWithAnimation();
4473
+ return;
4474
+ }
4475
+ // Show due to manual trigger
4476
+ if (shouldShow && !isCurrentlyVisible) {
4477
+ if (triggerGroupId) {
4478
+ this.currentGroupId.set(triggerGroupId);
4479
+ // Fetch files for this group
4480
+ this.fileManagerService.fetchAndStoreFilesByGroupId(triggerGroupId)
4481
+ .pipe(takeUntilDestroyed(this.destroyRef))
4482
+ .subscribe({
4483
+ next: () => this.showWithAnimation(),
4484
+ error: () => this.showWithAnimation() // Show anyway
4485
+ });
4336
4486
  }
4337
4487
  else {
4338
- console.error('❌ [FileInput] Upload successful but no ID returned:', response);
4339
- this.uploadError.emit('Upload successful but no ID returned');
4340
- }
4341
- this.isUploading.set(false);
4342
- console.log('🔄 [FileInput] isUploading set to false');
4343
- },
4344
- error: (error) => {
4345
- console.error('💥 [FileInput] Upload FAILED:', error);
4346
- // Angular 20: Complete the pending task even on error
4347
- // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
4348
- // console.log('❌ [FileInput] Pending task completed for failed upload');
4349
- // Set upload status to 'error' and remove upload validation error
4350
- this.uploadStatus.set('error');
4351
- console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
4352
- // Remove progress notification and show error
4353
- const notificationId = this.uploadNotificationId();
4354
- if (notificationId) {
4355
- this.notificationService.remove(notificationId);
4356
- console.log('🔔 [FileInput] Progress notification removed due to error');
4488
+ this.showWithAnimation();
4357
4489
  }
4358
- this.notificationService.error(`❌ File upload failed: ${error.message || error.error?.message || 'Unknown error occurred'}`, { duration: 0 });
4359
- this.uploadNotificationId.set(null);
4360
- this.uploadError.emit(error.message || error.error?.message || 'Upload failed');
4361
- this.isUploading.set(false);
4362
- this.uploadProgress.set(0);
4363
- this.uploadProgressChange.emit(0);
4364
- console.log('🔄 [FileInput] Upload state reset - isUploading: false, progress: 0%');
4365
4490
  }
4366
4491
  });
4367
4492
  }
4493
+ ngOnInit() {
4494
+ // Set up drag and drop listeners
4495
+ this.setupDragAndDrop();
4496
+ // Set up file input change listeners
4497
+ this.setupFileInputListeners();
4498
+ // Set up window resize listener
4499
+ this.setupWindowResize();
4500
+ }
4501
+ ngOnDestroy() {
4502
+ console.log('🧹 [FloatingFileUploader] Component destroyed');
4503
+ this.removeDragAndDropListeners();
4504
+ this.removeFileInputListeners();
4505
+ // Clean up window resize listener
4506
+ if (this.windowResizeHandler) {
4507
+ window.removeEventListener('resize', this.windowResizeHandler);
4508
+ }
4509
+ }
4368
4510
  /**
4369
- * Upload multiple files with group ID support
4370
- * FLOW: 1) Generate group ID first, 2) Upload all files with same group ID, 3) Emit group ID on completion
4511
+ * Set up drag and drop functionality
4371
4512
  */
4372
- uploadMultipleFiles(files) {
4373
- console.log('📤 [FileInput] uploadMultipleFiles called for:', files.length, 'files');
4374
- console.log('🔄 [FileInput] STEP 1: Generate group ID before starting any file uploads');
4375
- // Set multiple upload mode flag
4376
- this.isMultipleUploadMode.set(true);
4377
- // Set upload status to 'start' before starting upload
4378
- this.uploadStatus.set('start');
4379
- this.isUploading.set(true);
4380
- this.uploadProgress.set(0);
4381
- this.uploadProgressChange.emit(0);
4382
- // Make form control invalid during upload
4383
- this.onChange(null);
4384
- // Show initial progress notification
4385
- const notificationId = this.notificationService.showProgress('🔄 Preparing multiple file upload...', 0, { duration: 0 });
4386
- this.uploadNotificationId.set(notificationId);
4387
- // STEP 1: Generate or get group ID BEFORE starting any file uploads
4388
- const existingGroupId = this.uploadDataSignal().groupId;
4389
- if (existingGroupId) {
4390
- console.log('🆔 [FileInput] STEP 1 COMPLETE: Using existing group ID:', existingGroupId);
4391
- console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', existingGroupId);
4392
- this.groupId.set(existingGroupId);
4393
- this.startMulti(files, existingGroupId);
4394
- }
4395
- else {
4396
- console.log('🆔 [FileInput] No existing group ID, generating new one...');
4397
- // Generate group ID BEFORE starting any file uploads
4398
- this.fileManagerService.generateObjectId().subscribe({
4399
- next: (response) => {
4400
- const newGroupId = response.data?.objectId;
4401
- console.log('🆔 [FileInput] STEP 1 COMPLETE: Generated new group ID:', newGroupId);
4402
- console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', newGroupId);
4403
- this.groupId.set(newGroupId);
4404
- this.startMulti(files, newGroupId);
4405
- },
4406
- error: (error) => {
4407
- console.error('❌ [FileInput] Failed to generate group ID:', error);
4408
- this.uploadError.emit('Failed to generate group ID');
4409
- this.isUploading.set(false);
4410
- this.uploadStatus.set('error');
4411
- const notificationId = this.uploadNotificationId();
4412
- if (notificationId) {
4413
- this.notificationService.remove(notificationId);
4414
- }
4415
- this.notificationService.error('❌ Failed to generate group ID for multiple file upload', { duration: 0 });
4416
- this.uploadNotificationId.set(null);
4417
- }
4418
- });
4419
- }
4513
+ setupDragAndDrop() {
4514
+ document.addEventListener('dragover', this.handleDragOver.bind(this));
4515
+ document.addEventListener('dragleave', this.handleDragLeave.bind(this));
4516
+ document.addEventListener('drop', this.handleDrop.bind(this));
4420
4517
  }
4421
4518
  /**
4422
- * Start uploading multiple files with the provided group ID
4423
- * All files will be uploaded with the SAME group ID that was generated before this method
4519
+ * Remove drag and drop listeners
4424
4520
  */
4425
- startMulti(files, groupId) {
4426
- console.log('🚀 [FileInput] STEP 2: Starting upload for', files.length, 'files with group ID:', groupId);
4427
- console.log('📋 [FileInput] All files will use the same group ID:', groupId);
4428
- // Mark that this component has ever uploaded files
4429
- this.hasEverUploaded.set(true);
4430
- let completedUploads = 0;
4431
- let failedUploads = 0;
4432
- const totalFiles = files.length;
4433
- // IMPORTANT: All files use the SAME group ID that was generated before starting uploads
4434
- const uploadDataWithGroupId = {
4435
- ...this.uploadDataSignal(),
4436
- groupId: groupId,
4437
- isMultiple: true
4438
- };
4439
- files.forEach((file, index) => {
4440
- const componentId = this.id();
4441
- console.log(`📤 [FileInput-${componentId}] Uploading file ${index + 1}/${totalFiles}: "${file.name}" with group ID: ${groupId}`);
4442
- console.log(`📤 [FileInput-${componentId}] Upload data:`, uploadDataWithGroupId);
4443
- this.fileManagerService.uploadFile(file, uploadDataWithGroupId, (progress) => {
4444
- // Calculate overall progress
4445
- const fileProgress = progress / totalFiles;
4446
- const overallProgress = ((completedUploads * 100) + fileProgress) / totalFiles;
4447
- this.uploadProgress.set(overallProgress);
4448
- this.uploadProgressChange.emit(overallProgress);
4449
- // Update progress notification
4450
- const notificationId = this.uploadNotificationId();
4451
- if (notificationId) {
4452
- const progressMessage = `🔄 Uploading file ${index + 1} of ${totalFiles}...`;
4453
- this.notificationService.updateProgress(notificationId, overallProgress, progressMessage);
4454
- }
4455
- }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
4456
- next: (response) => {
4457
- completedUploads++;
4458
- console.log(`✅ [FileInput] File ${index + 1}/${totalFiles} uploaded`);
4459
- // Check if all files are completed
4460
- if (completedUploads + failedUploads === totalFiles) {
4461
- this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
4462
- }
4463
- },
4464
- error: (error) => {
4465
- failedUploads++;
4466
- console.error(`❌ [FileInput] File ${index + 1}/${totalFiles} upload failed:`, error);
4467
- // Check if all files are completed
4468
- if (completedUploads + failedUploads === totalFiles) {
4469
- this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
4470
- }
4471
- }
4472
- });
4473
- });
4521
+ removeDragAndDropListeners() {
4522
+ document.removeEventListener('dragover', this.handleDragOver.bind(this));
4523
+ document.removeEventListener('dragleave', this.handleDragLeave.bind(this));
4524
+ document.removeEventListener('drop', this.handleDrop.bind(this));
4474
4525
  }
4475
4526
  /**
4476
- * Handle completion of multiple file upload
4527
+ * Set up file input change listeners
4477
4528
  */
4478
- handleMultipleUploadComplete(completed, failed, total, groupId) {
4479
- console.log(`📊 [FileInput] Multiple upload complete: ${completed}/${total} successful, ${failed} failed`);
4480
- this.isUploading.set(false);
4481
- this.uploadProgress.set(100);
4482
- this.uploadProgressChange.emit(100);
4483
- // Remove progress notification
4484
- const notificationId = this.uploadNotificationId();
4485
- if (notificationId) {
4486
- this.notificationService.remove(notificationId);
4487
- }
4488
- this.uploadNotificationId.set(null);
4489
- if (failed === 0) {
4490
- // All files uploaded successfully
4491
- this.uploadStatus.set('success');
4492
- // Success notification removed for cleaner UX
4493
- // STEP 3: For multiple file upload, emit the group ID (not individual file IDs)
4494
- this.onChange(groupId);
4495
- this.uploadSuccess.emit(groupId);
4496
- console.log('📝 [FileInput] Multiple upload completed with group ID:', groupId);
4497
- }
4498
- else if (completed > 0) {
4499
- // Some files uploaded successfully
4500
- this.uploadStatus.set('error');
4501
- this.notificationService.warning(`⚠️ ${completed}/${total} files uploaded. ${failed} failed.`, { duration: 0 });
4502
- this.uploadError.emit(`${failed} out of ${total} files failed to upload`);
4503
- }
4504
- else {
4505
- // All files failed
4506
- this.uploadStatus.set('error');
4507
- this.notificationService.error(`❌ All ${total} files failed to upload.`, { duration: 0 });
4508
- this.uploadError.emit('All files failed to upload');
4509
- }
4510
- // Reset multiple upload mode flag
4511
- this.isMultipleUploadMode.set(false);
4529
+ setupFileInputListeners() {
4530
+ // Listen for file input change events globally
4531
+ document.addEventListener('change', this.handleFileInputChange.bind(this));
4512
4532
  }
4513
- generatePreviews() {
4514
- // Clear existing previews
4515
- this.clearPreviews();
4516
- if (!this.showPreviewSignal() || !this.files()) {
4517
- return;
4518
- }
4519
- Array.from(this.files()).forEach(file => {
4520
- if (this.isImageFile(file)) {
4521
- const reader = new FileReader();
4522
- reader.onload = (e) => {
4523
- if (e.target?.result) {
4524
- this.previewUrls.update(urls => [...urls, e.target.result]);
4525
- }
4526
- };
4527
- reader.readAsDataURL(file);
4528
- }
4529
- });
4533
+ /**
4534
+ * Remove file input listeners
4535
+ */
4536
+ removeFileInputListeners() {
4537
+ document.removeEventListener('change', this.handleFileInputChange.bind(this));
4530
4538
  }
4531
- clearPreviews() {
4532
- // Revoke object URLs to prevent memory leaks
4533
- this.previewUrls().forEach(url => {
4534
- if (url.startsWith('blob:')) {
4535
- URL.revokeObjectURL(url);
4536
- }
4539
+ /**
4540
+ * Handle file input change events
4541
+ */
4542
+ handleFileInputChange(event) {
4543
+ const target = event.target;
4544
+ console.log('🔍 [FloatingFileUploader] File input change event detected:', {
4545
+ type: target.type,
4546
+ filesLength: target.files?.length || 0,
4547
+ element: target
4537
4548
  });
4538
- this.previewUrls.set([]);
4539
- }
4540
- isImageFile(file) {
4541
- return file.type.startsWith('image/');
4542
- }
4543
- loadFileDetailsFromId(fileId) {
4544
- console.log('🔍 [FileInput] Loading file details for ID:', fileId);
4545
- if (!fileId)
4546
- return;
4547
- this.fileManagerService?.getFileDetails({ cyfm_id: fileId })?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
4548
- next: (fileDetails) => {
4549
- console.log('📋 [FileInput] File details received:', fileDetails);
4550
- if (fileDetails?.data?.length) {
4551
- const fileData = fileDetails.data[0];
4552
- console.log('📁 [FileInput] File data:', fileData);
4553
- // Set file name from the details
4554
- if (fileData.cyfm_name) {
4555
- this.fileNames.set([fileData.cyfm_name]);
4556
- console.log('📝 [FileInput] File name set:', fileData.cyfm_name);
4557
- }
4558
- // If it's an image and we have base64 data, set preview
4559
- if (this.showPreviewSignal() && fileData.cyfm_file_base64) {
4560
- // Check if it's an image file based on file name or type
4561
- const isImage = this.isImageFileFromName(fileData.cyfm_name || '') ||
4562
- this.isImageFileFromType(fileData.cyfm_type || '');
4563
- if (isImage) {
4564
- // Add data URL prefix if not already present
4565
- let base64Data = fileData.cyfm_file_base64;
4566
- if (!base64Data.startsWith('data:')) {
4567
- const mimeType = fileData.cyfm_type || 'image/jpeg';
4568
- base64Data = `data:${mimeType};base64,${base64Data}`;
4569
- }
4570
- this.previewUrls.set([base64Data]);
4571
- console.log('🖼️ [FileInput] Preview set from base64 data');
4572
- }
4573
- }
4574
- }
4575
- else {
4576
- console.warn('⚠️ [FileInput] No file data found for ID:', fileId);
4577
- }
4578
- },
4579
- error: (error) => {
4580
- console.error('❌ [FileInput] Error loading file details:', error);
4581
- this.notificationService.error(`Failed to load file details: ${error.message || 'Unknown error'}`, { duration: 0 });
4549
+ // Check if this is a file input with files
4550
+ if (target.type === 'file' && target.files && target.files.length > 0) {
4551
+ console.log('📁 [FloatingFileUploader] File input change detected:', target.files.length, 'files');
4552
+ // Check if the input has a data-user-id attribute for user context
4553
+ const userId = target.getAttribute('data-user-id');
4554
+ if (userId && userId !== this.currentUserId()) {
4555
+ this.setCurrentUserId(userId);
4582
4556
  }
4583
- });
4557
+ // Handle the files
4558
+ this.handleFiles(Array.from(target.files));
4559
+ // Reset the input to allow selecting the same files again
4560
+ target.value = '';
4561
+ }
4584
4562
  }
4585
4563
  /**
4586
- * Check if the component is in multiple file mode
4564
+ * Handle drag over event
4587
4565
  */
4588
- isMultipleFileMode() {
4589
- // Check if multiple attribute is set or if we have a group ID
4590
- return this.multiple || this.groupId() !== null;
4566
+ handleDragOver(event) {
4567
+ event.preventDefault();
4568
+ event.stopPropagation();
4569
+ // Show floating uploader when files are dragged over
4570
+ if (event.dataTransfer?.types.includes('Files')) {
4571
+ this.showWithAnimation();
4572
+ }
4591
4573
  }
4592
4574
  /**
4593
- * Load files from group ID using the group API
4575
+ * Handle drag leave event
4594
4576
  */
4595
- loadFilesFromGroupId(groupId) {
4596
- console.log('🔍 [FileInput] Loading files for group ID:', groupId);
4597
- if (!groupId)
4598
- return;
4599
- this.fileManagerService.fetchAndStoreFilesByGroupId(groupId)
4600
- .pipe(takeUntilDestroyed(this.destroyRef))
4601
- .subscribe({
4602
- next: (files) => {
4603
- console.log('📋 [FileInput] Files loaded for group:', files.length);
4604
- // Set file names to show count in input
4605
- if (files && files.length > 0) {
4606
- const fileNames = files.map(file => file.file_name || file.name || 'Unknown file');
4607
- this.fileNames.set(fileNames);
4608
- console.log('📝 [FileInput] File names set for display:', fileNames);
4609
- }
4610
- else {
4611
- this.fileNames.set([]);
4612
- }
4613
- // Files are now stored in service state and will be displayed by floating uploader
4614
- },
4615
- error: (error) => {
4616
- console.error('❌ [FileInput] Failed to load files for group:', error);
4617
- this.fileNames.set([]);
4618
- }
4619
- });
4577
+ handleDragLeave(event) {
4578
+ event.preventDefault();
4579
+ event.stopPropagation();
4580
+ // Only hide if leaving the entire document
4581
+ if (!event.relatedTarget || event.relatedTarget === document.body) {
4582
+ this.updateVisibility();
4583
+ }
4620
4584
  }
4621
- isImageFileFromName(fileName) {
4622
- if (!fileName)
4623
- return false;
4624
- const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
4625
- const lowerFileName = fileName.toLowerCase();
4626
- return imageExtensions.some(ext => lowerFileName.endsWith(ext));
4585
+ /**
4586
+ * Handle drop event
4587
+ */
4588
+ handleDrop(event) {
4589
+ event.preventDefault();
4590
+ event.stopPropagation();
4591
+ const files = event.dataTransfer?.files;
4592
+ if (files && files.length > 0) {
4593
+ this.handleFiles(Array.from(files));
4594
+ }
4627
4595
  }
4628
- isImageFileFromType(fileType) {
4629
- if (!fileType)
4630
- return false;
4631
- return fileType.startsWith('image/');
4596
+ /**
4597
+ * Handle files from drag and drop or file input
4598
+ */
4599
+ handleFiles(files) {
4600
+ console.log('📁 [FloatingFileUploader] Handling files:', files.length);
4601
+ // Use handleExternalFiles to process the files
4602
+ this.handleExternalFiles(files, this.currentUserId());
4632
4603
  }
4633
- removePreview(index) {
4634
- const currentFiles = this.files();
4635
- const currentUrls = this.previewUrls();
4636
- if (currentFiles && currentFiles.length > index) {
4637
- // Handle FileList case - remove file from FileList
4638
- const dt = new DataTransfer();
4639
- Array.from(currentFiles).forEach((file, i) => {
4640
- if (i !== index) {
4641
- dt.items.add(file);
4642
- }
4643
- });
4644
- const newFiles = dt.files;
4645
- this.files.set(newFiles);
4646
- this.fileNames.set(Array.from(newFiles).map(f => f.name));
4647
- // Remove the preview URL
4648
- if (currentUrls[index] && currentUrls[index].startsWith('blob:')) {
4649
- URL.revokeObjectURL(currentUrls[index]);
4650
- }
4651
- this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
4652
- this.onChange(newFiles);
4653
- this.fileChange.emit(newFiles);
4654
- }
4655
- else if (currentUrls.length > index) {
4656
- // Handle uploaded file ID case - clear the preview and set control value to null
4657
- console.log('🗑️ [FileInput] Removing preview for uploaded file ID');
4658
- // Clear preview
4659
- this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
4660
- this.fileNames.set([]);
4661
- // Set control value to null since we're removing the uploaded file
4662
- this.onChange(null);
4663
- this.fileChange.emit(null);
4664
- }
4604
+ /**
4605
+ * Update visibility - simplified for notification only
4606
+ */
4607
+ updateVisibility() {
4608
+ // This is just a notification component now
4609
+ // The actual uploads are handled by the global uploader
4665
4610
  }
4666
- ngOnDestroy() {
4667
- // Clean up preview URLs to prevent memory leaks
4668
- this.clearPreviews();
4669
- // Clean up any active upload notification
4670
- const notificationId = this.uploadNotificationId();
4671
- if (notificationId) {
4672
- this.notificationService.remove(notificationId);
4673
- this.uploadNotificationId.set(null);
4674
- }
4611
+ /**
4612
+ * Show with animation
4613
+ */
4614
+ showWithAnimation() {
4615
+ console.log('🎬 [FloatingFileUploader] showWithAnimation called - setting isVisible to true');
4616
+ this.isAnimating.set(true);
4617
+ this.isVisible.set(true);
4618
+ // Remove animation class after animation completes
4619
+ setTimeout(() => {
4620
+ this.isAnimating.set(false);
4621
+ console.log('🎬 [FloatingFileUploader] Animation completed, isVisible:', this.isVisible());
4622
+ }, 300);
4675
4623
  }
4676
- triggerFileSelect() {
4677
- const fileInput = document.getElementById('cide-file-input-' + this.id());
4678
- if (fileInput && !this.disabledSignal()) {
4679
- fileInput.click();
4680
- }
4624
+ /**
4625
+ * Hide with animation
4626
+ */
4627
+ hideWithAnimation() {
4628
+ this.isAnimating.set(true);
4629
+ // Wait for animation to complete before hiding
4630
+ setTimeout(() => {
4631
+ this.isVisible.set(false);
4632
+ this.isAnimating.set(false);
4633
+ }, 300);
4681
4634
  }
4682
4635
  /**
4683
- * Show floating uploader manually
4684
- * This can be called to show the floating uploader even when no files are selected
4636
+ * Toggle minimize state
4685
4637
  */
4686
- showUploader() {
4687
- console.log('👁️ [FileInput] Manually showing floating uploader');
4688
- if (!this.showFloatingUploaderSignal()) {
4689
- console.log('⚠️ [FileInput] Floating uploader is disabled');
4690
- return;
4638
+ toggleMinimize() {
4639
+ this.isMinimized.set(!this.isMinimized());
4640
+ }
4641
+ /**
4642
+ * Close the floating uploader
4643
+ */
4644
+ close() {
4645
+ // Don't clear files from service - just hide the uploader
4646
+ // Files will be fetched from API when "Show Files" is clicked
4647
+ this.hideWithAnimation();
4648
+ }
4649
+ /**
4650
+ * Get upload summary text
4651
+ */
4652
+ getUploadSummary() {
4653
+ const pending = this.pendingUploads();
4654
+ const active = this.activeUploadsLocal();
4655
+ const completed = this.completedUploads();
4656
+ const failed = this.failedUploads();
4657
+ if (active.length > 0) {
4658
+ return `${active.length} uploading`;
4691
4659
  }
4692
- const groupId = this.groupId();
4693
- if (groupId) {
4694
- console.log("groupId groupId", groupId);
4695
- // Fetch files for the group and trigger floating uploader to show
4696
- this.fileManagerService.fetchAndStoreFilesByGroupId(groupId)
4697
- .pipe(takeUntilDestroyed(this.destroyRef))
4698
- .subscribe({
4699
- next: (files) => {
4700
- console.log('✅ [FileInput] Files fetched for floating uploader: groupId', files.length);
4701
- // Trigger the floating uploader to show via service
4702
- this.fileManagerService.triggerFloatingUploaderShow();
4703
- },
4704
- error: (error) => {
4705
- console.error('❌ [FileInput] Failed to fetch files for floating uploader:', error);
4706
- // Still trigger show even if fetch fails
4707
- this.fileManagerService.triggerFloatingUploaderShow();
4708
- }
4709
- });
4660
+ else if (pending.length > 0) {
4661
+ return `${pending.length} pending`;
4710
4662
  }
4711
- else {
4712
- // No group ID, just trigger show
4713
- this.fileManagerService.triggerFloatingUploaderShow();
4663
+ else if (completed.length > 0 && failed.length === 0) {
4664
+ return `${completed.length} completed`;
4714
4665
  }
4715
- // Fallback to direct component access if available
4716
- if (this.floatingUploader) {
4717
- console.log('👁️ [FileInput] Using direct component access as fallback');
4718
- this.floatingUploader.showUploader(groupId || undefined);
4666
+ else if (failed.length > 0) {
4667
+ return `${completed.length} completed, ${failed.length} failed`;
4719
4668
  }
4669
+ return 'No uploads';
4720
4670
  }
4721
4671
  /**
4722
- * Get total upload count from file manager service for this component's group ID
4672
+ * Get overall progress percentage
4723
4673
  */
4724
- getUploadCount() {
4725
- const groupId = this.groupId();
4726
- const componentId = this.id();
4727
- if (!groupId) {
4728
- console.log(`📊 [FileInput-${componentId}] No group ID, returning all uploads count:`, this.fileManagerService.activeUploads().size);
4729
- // If no group ID, return all uploads
4730
- return this.fileManagerService.activeUploads().size;
4674
+ getOverallProgress() {
4675
+ const allUploads = Array.from(this.activeUploads().values());
4676
+ if (allUploads.length === 0)
4677
+ return 0;
4678
+ const totalProgress = allUploads.reduce((sum, upload) => sum + (upload.percentage || 0), 0);
4679
+ return Math.round(totalProgress / allUploads.length);
4680
+ }
4681
+ /**
4682
+ * Get status icon based on upload stage
4683
+ */
4684
+ getStatusIcon(stage) {
4685
+ switch (stage) {
4686
+ case 'reading': return 'schedule';
4687
+ case 'uploading': return 'cloud_upload';
4688
+ case 'complete': return 'check_circle';
4689
+ case 'error': return 'error';
4690
+ default: return 'help';
4731
4691
  }
4732
- // Use service method to get all files for this group
4733
- const allFiles = this.fileManagerService.getAllFilesForGroup(groupId);
4734
- console.log(`📊 [FileInput-${componentId}] Upload count for group ${groupId}:`, {
4735
- count: allFiles.length,
4736
- files: allFiles.map(f => ({ id: f.fileId, name: f.fileName, stage: f.stage }))
4737
- });
4738
- return allFiles.length;
4739
4692
  }
4740
4693
  /**
4741
- * Check if there are active uploads for this component's group ID
4694
+ * Get status class based on upload stage
4742
4695
  */
4743
- hasActiveUploads() {
4744
- const groupId = this.groupId();
4745
- const componentId = this.id();
4746
- if (!groupId) {
4747
- const hasActive = Array.from(this.fileManagerService.activeUploads().values()).some(upload => upload.stage !== 'complete');
4748
- console.log(`📊 [FileInput-${componentId}] No group ID, checking all active uploads:`, hasActive);
4749
- return hasActive;
4750
- }
4751
- // Use service method to get all files and check for active ones
4752
- const allFiles = this.fileManagerService.getAllFilesForGroup(groupId);
4753
- const hasActive = allFiles.some(file => file.stage !== 'complete');
4754
- console.log(`📊 [FileInput-${componentId}] Active uploads for group ${groupId}:`, {
4755
- hasActive,
4756
- activeFiles: allFiles.filter(f => f.stage !== 'complete').map(f => ({ id: f.fileId, name: f.fileName, stage: f.stage }))
4757
- });
4758
- return hasActive;
4696
+ getStatusClass(stage) {
4697
+ switch (stage) {
4698
+ case 'reading': return 'status-pending';
4699
+ case 'uploading': return 'status-uploading';
4700
+ case 'complete': return 'status-completed';
4701
+ case 'error': return 'status-error';
4702
+ default: return 'status-unknown';
4703
+ }
4704
+ }
4705
+ /**
4706
+ * Cancel upload
4707
+ */
4708
+ cancelUpload(fileId) {
4709
+ console.log('🚫 [FloatingFileUploader] Cancelling upload:', fileId);
4710
+ this.fileManagerService.cancelUpload(fileId);
4711
+ }
4712
+ /**
4713
+ * Get file name from file ID (extract from the ID format)
4714
+ */
4715
+ getFileNameFromId(fileId) {
4716
+ // Extract filename from the fileId format: filename_size_timestamp
4717
+ const parts = fileId.split('_');
4718
+ if (parts.length >= 3) {
4719
+ // Remove the last two parts (size and timestamp) to get the filename
4720
+ return parts.slice(0, -2).join('_');
4721
+ }
4722
+ return fileId;
4759
4723
  }
4760
4724
  /**
4761
- * Get count of active (non-completed) uploads for this component's group ID
4725
+ * Get all files from service state (pending + active uploads + fetched files)
4726
+ * This method now uses the computed property for consistency
4762
4727
  */
4763
- getActiveUploadCount() {
4764
- const groupId = this.groupId();
4765
- const componentId = this.id();
4766
- if (!groupId) {
4767
- const count = Array.from(this.fileManagerService.activeUploads().values()).filter(upload => upload.stage !== 'complete').length;
4768
- console.log(`📊 [FileInput-${componentId}] No group ID, active upload count:`, count);
4769
- return count;
4770
- }
4771
- // Use service method to get all files and filter active ones
4772
- const allFiles = this.fileManagerService.getAllFilesForGroup(groupId);
4773
- const activeCount = allFiles.filter(file => file.stage !== 'complete').length;
4774
- console.log(`📊 [FileInput-${componentId}] Active upload count for group ${groupId}:`, {
4775
- count: activeCount,
4776
- activeFiles: allFiles.filter(f => f.stage !== 'complete').map(f => ({ id: f.fileId, name: f.fileName, stage: f.stage }))
4777
- });
4778
- return activeCount;
4728
+ getAllFiles() {
4729
+ return this.allFilesForGroup();
4779
4730
  }
4780
4731
  /**
4781
- * Show floating uploader (alias for showUploader for template)
4732
+ * Set current user ID
4782
4733
  */
4783
- showFloatingUploaderDialog() {
4784
- this.showUploader();
4734
+ setCurrentUserId(userId) {
4735
+ this.currentUserId.set(userId);
4736
+ this.fileManagerService.setUserId(userId);
4785
4737
  }
4786
4738
  /**
4787
- * Get dynamic classes for drag and drop zone
4739
+ * Public method to handle files from external sources
4740
+ * This can be called by other components to trigger the floating uploader
4788
4741
  */
4789
- getDragDropZoneClasses() {
4790
- const classes = [];
4791
- if (this.isDragOver()) {
4792
- classes.push('!tw-border-blue-500', '!tw-bg-blue-100', 'dark:!tw-bg-blue-900/30', 'tw-scale-[1.01]');
4793
- }
4794
- if (this.disabledSignal()) {
4795
- classes.push('tw-opacity-50', 'tw-cursor-not-allowed', '!hover:tw-border-gray-300', '!hover:tw-bg-gray-50', 'dark:!hover:tw-bg-gray-800');
4742
+ handleExternalFiles(files, userId, groupId) {
4743
+ console.log('📁 [FloatingFileUploader] External files received:', files.length, 'files');
4744
+ // Set user ID if provided
4745
+ if (userId && userId !== this.currentUserId()) {
4746
+ this.setCurrentUserId(userId);
4796
4747
  }
4797
- if (this.hasFiles()) {
4798
- classes.push('!tw-border-emerald-500', '!tw-bg-emerald-50', 'dark:!tw-bg-emerald-900/20', 'hover:!tw-border-emerald-600', 'hover:!tw-bg-emerald-100', 'dark:hover:!tw-bg-emerald-900/30');
4748
+ // Set group ID if provided
4749
+ if (groupId) {
4750
+ this.currentGroupId.set(groupId);
4799
4751
  }
4800
- return classes.join(' ');
4752
+ // Upload files using file manager service
4753
+ // The file manager service will handle adding to its queue and the effect will show the floating uploader
4754
+ files.forEach((file, index) => {
4755
+ console.log(`📁 [FloatingFileUploader] Starting upload for file ${index + 1}/${files.length}:`, file.name);
4756
+ this.fileManagerService.uploadFile(file, {
4757
+ userId: this.currentUserId(),
4758
+ groupId: groupId,
4759
+ permissions: ['read', 'write'],
4760
+ tags: []
4761
+ })
4762
+ .pipe(takeUntilDestroyed(this.destroyRef))
4763
+ .subscribe({
4764
+ next: (response) => {
4765
+ console.log('✅ [FloatingFileUploader] Upload completed:', response);
4766
+ },
4767
+ error: (error) => {
4768
+ console.error('❌ [FloatingFileUploader] Upload failed:', error);
4769
+ }
4770
+ });
4771
+ });
4801
4772
  }
4802
4773
  /**
4803
- * Get dynamic classes for icon
4774
+ * Check if there are any uploads for the current group
4804
4775
  */
4805
- getIconClasses() {
4806
- const classes = ['tw-text-gray-500', 'dark:tw-text-gray-400'];
4807
- if (this.isDragOver()) {
4808
- classes.push('!tw-text-blue-500', 'dark:!tw-text-blue-400');
4809
- }
4810
- else if (this.hasFiles()) {
4811
- classes.push('!tw-text-emerald-500', 'dark:!tw-text-emerald-400');
4776
+ hasUploadsForCurrentGroup() {
4777
+ const groupId = this.currentGroupId();
4778
+ if (!groupId) {
4779
+ // If no group filter, show all uploads
4780
+ return this.hasUploads();
4812
4781
  }
4813
- return classes.join(' ');
4782
+ // Check if any uploads belong to the current group
4783
+ // Note: This would need to be enhanced based on how group IDs are stored in the file manager service
4784
+ return this.hasUploads();
4814
4785
  }
4815
4786
  /**
4816
- * Get dynamic classes for preview box
4787
+ * Handle drag over event for file drop
4817
4788
  */
4818
- getPreviewBoxClasses() {
4819
- const classes = [];
4820
- if (this.isDragOver()) {
4821
- classes.push('!tw-border-blue-500', '!tw-bg-blue-100', 'dark:!tw-bg-blue-900/30');
4822
- }
4823
- if (this.disabledSignal()) {
4824
- classes.push('tw-opacity-50', 'tw-cursor-not-allowed', '!hover:tw-border-gray-300', '!hover:tw-bg-gray-50', 'dark:!hover:tw-bg-gray-800');
4825
- }
4826
- if (this.hasImages()) {
4827
- classes.push('!tw-border-emerald-500', '!tw-bg-emerald-50', 'dark:!tw-bg-emerald-900/20');
4828
- }
4829
- return classes.join(' ');
4830
- }
4831
- // Drag and Drop Event Handlers
4832
4789
  onDragOver(event) {
4833
4790
  event.preventDefault();
4834
4791
  event.stopPropagation();
4835
- if (!this.disabledSignal()) {
4836
- this.isDragOver.set(true);
4837
- console.log('🔄 [FileInput] Drag over detected');
4838
- }
4792
+ this.isDragOver.set(true);
4839
4793
  }
4794
+ /**
4795
+ * Handle drag leave event for file drop
4796
+ */
4840
4797
  onDragLeave(event) {
4841
4798
  event.preventDefault();
4842
4799
  event.stopPropagation();
4843
4800
  this.isDragOver.set(false);
4844
- console.log('🔄 [FileInput] Drag leave detected');
4845
- }
4846
- onDragEnter(event) {
4847
- event.preventDefault();
4848
- event.stopPropagation();
4849
- if (!this.disabledSignal()) {
4850
- this.isDragOver.set(true);
4851
- console.log('🔄 [FileInput] Drag enter detected');
4852
- }
4853
4801
  }
4802
+ /**
4803
+ * Handle drop event for file drop
4804
+ */
4854
4805
  onDrop(event) {
4855
4806
  event.preventDefault();
4856
4807
  event.stopPropagation();
4857
4808
  this.isDragOver.set(false);
4858
- if (this.disabledSignal()) {
4859
- console.log('⏸️ [FileInput] Drop ignored - component is disabled');
4860
- return;
4861
- }
4862
4809
  const files = event.dataTransfer?.files;
4863
4810
  if (files && files.length > 0) {
4864
- console.log('📁 [FileInput] Files dropped:', Array.from(files).map(f => f.name));
4865
- // Validate file types if accept is specified
4866
- if (this.acceptSignal() && !this.validateFileTypes(files)) {
4867
- console.log('❌ [FileInput] Invalid file types dropped');
4868
- this.notificationService.error('❌ Invalid file type. Please select files of the correct type.', { duration: 0 });
4869
- return;
4870
- }
4871
- // Handle single vs multiple files
4872
- if (!this.multipleSignal() && files.length > 1) {
4873
- console.log('⚠️ [FileInput] Multiple files dropped but multiple is disabled');
4874
- this.notificationService.warning('⚠️ Only one file is allowed. Using the first file.', { duration: 0 });
4875
- // Create a new FileList with only the first file
4876
- const dt = new DataTransfer();
4877
- dt.items.add(files[0]);
4878
- this.handleFileSelection(dt.files);
4879
- }
4880
- else {
4881
- this.handleFileSelection(files);
4882
- }
4883
- }
4884
- }
4885
- validateFileTypes(files) {
4886
- const acceptTypes = this.acceptSignal().split(',').map(type => type.trim());
4887
- if (acceptTypes.length === 0 || acceptTypes[0] === '')
4888
- return true;
4889
- return Array.from(files).every(file => {
4890
- return acceptTypes.some(acceptType => {
4891
- if (acceptType.startsWith('.')) {
4892
- // Extension-based validation
4893
- return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
4894
- }
4895
- else if (acceptType.includes('/')) {
4896
- // MIME type validation
4897
- return file.type === acceptType || file.type.startsWith(acceptType.replace('*', ''));
4898
- }
4899
- return false;
4900
- });
4901
- });
4902
- }
4903
- handleFileSelection(files) {
4904
- this.files.set(files);
4905
- this.fileNames.set(Array.from(files).map(f => f.name));
4906
- console.log('📁 [FileInput] Files selected via drag & drop:', this.fileNames());
4907
- this.generatePreviews();
4908
- // Reset upload status when new file is selected
4909
- this.uploadStatus.set('idle');
4910
- console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
4911
- this.onChange(files);
4912
- this.fileChange.emit(files);
4913
- this.onTouched();
4914
- // Show floating uploader if enabled and files are selected
4915
- if (this.showFloatingUploaderSignal() && files.length > 0 && this.floatingUploader) {
4916
- console.log('👁️ [FileInput] Showing floating uploader for dropped files:', files.length, 'files');
4917
- this.floatingUploader.showUploader(this.floatingUploaderGroupIdSignal());
4918
- }
4919
- // Auto upload if enabled
4920
- if (this.autoUploadSignal() && files.length > 0) {
4921
- if (this.multipleSignal()) {
4922
- console.log('🚀 [FileInput] Auto upload enabled for multiple files mode (drag & drop):', files.length, 'files');
4923
- this.uploadMultipleFiles(Array.from(files));
4924
- }
4925
- else {
4926
- console.log('🚀 [FileInput] Auto upload enabled for single file mode (drag & drop):', files[0].name);
4927
- this.uploadFile(files[0]);
4928
- }
4929
- }
4930
- else {
4931
- console.log('⏸️ [FileInput] Auto upload disabled or no files');
4932
- }
4933
- }
4934
- isRequired() {
4935
- return this.requiredSignal();
4936
- }
4937
- /**
4938
- * Angular 20: Utility method to get upload data with proper typing
4939
- * @returns Properly typed upload data
4940
- */
4941
- getUploadData() {
4942
- return this.uploadDataSignal();
4943
- }
4944
- /**
4945
- * Angular 20: Utility method to update upload data with type safety
4946
- * @param data Partial upload data to merge with existing data
4947
- */
4948
- updateUploadData(data) {
4949
- const currentData = this.uploadDataSignal();
4950
- const updatedData = { ...currentData, ...data };
4951
- // Note: This would require the uploadData to be a writable signal
4952
- // For now, this method serves as a type-safe way to work with upload data
4953
- console.log('📝 [FileInput] Upload data updated:', updatedData);
4954
- }
4955
- getCurrentState() {
4956
- return {
4957
- id: this.id(),
4958
- label: this.labelSignal(),
4959
- required: this.requiredSignal(),
4960
- disabled: this.disabledSignal(),
4961
- accept: this.acceptSignal(),
4962
- multiple: this.multipleSignal(),
4963
- showPreview: this.showPreviewSignal(),
4964
- autoUpload: this.autoUploadSignal(),
4965
- uploadStatus: this.uploadStatus(),
4966
- isUploading: this.isUploading(),
4967
- uploadProgress: this.uploadProgress(),
4968
- files: this.files() ? Array.from(this.files()).map(f => ({ name: f.name, size: f.size, type: f.type })) : null,
4969
- fileNames: this.fileNames(),
4970
- previewUrls: this.previewUrls().length,
4971
- helperText: this.helperTextSignal(),
4972
- errorText: this.errorTextSignal(),
4973
- placeholderText: this.placeholderTextSignal(),
4974
- placeholderIcon: this.placeholderIconSignal(),
4975
- previewWidth: this.previewWidthSignal(),
4976
- previewHeight: this.previewHeightSignal(),
4977
- previewBoxMode: this.previewBoxModeSignal(),
4978
- showFileName: this.showFileNameSignal(),
4979
- uploadData: this.uploadDataSignal()
4980
- };
4981
- }
4982
- async getControlData() {
4983
- console.log('🔍 [FileInput] getControlData called');
4984
- const cide_element_data = await this.elementService?.getElementData({ sype_key: this.id() });
4985
- if (cide_element_data) {
4986
- console.log('📋 [FileInput] Element data loaded:', cide_element_data);
4987
- // Note: Since we're using input signals, we can't directly set their values
4988
- // This method would need to be refactored to work with the new signal-based approach
4989
- // For now, we'll log the data and trigger validation
4990
- console.log('✅ [FileInput] Control data received from element service');
4991
- console.log('⚠️ [FileInput] Note: Input signals cannot be modified after component initialization');
4992
- // Trigger validation update
4993
- this.onValidatorChange();
4994
- }
4995
- else {
4996
- console.log('⚠️ [FileInput] No element data found for key:', this.id());
4997
- }
4998
- }
4999
- // Validator implementation
5000
- validate(control) {
5001
- console.log('🔍 [FileInput] validate() called - uploadStatus:', this.uploadStatus(), 'required:', this.requiredSignal(), 'files:', !!this.files(), 'control.value:', control.value);
5002
- // If upload is in progress (start or uploading status), return validation error
5003
- if (this.uploadStatus() === 'start' || this.uploadStatus() === 'uploading') {
5004
- console.log('⚠️ [FileInput] Validation ERROR: Upload in progress');
5005
- return { 'uploadInProgress': { message: 'File upload in progress. Please wait...' } };
4811
+ this.handleFileSelection(Array.from(files));
5006
4812
  }
5007
- // If required and no file is selected and no control value (uploaded file ID), return validation error
5008
- if (this.requiredSignal() && !this.files() && !control.value) {
5009
- console.log('⚠️ [FileInput] Validation ERROR: File required');
5010
- return { 'required': { message: 'Please select a file to upload.' } };
4813
+ }
4814
+ /**
4815
+ * Trigger file input click
4816
+ */
4817
+ triggerFileInput() {
4818
+ const fileInput = document.querySelector('input[type="file"]');
4819
+ if (fileInput) {
4820
+ fileInput.click();
5011
4821
  }
5012
- console.log('✅ [FileInput] Validation PASSED: No errors');
5013
- return null; // No validation errors
5014
4822
  }
5015
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5016
- 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: [
5017
- {
5018
- provide: NG_VALUE_ACCESSOR,
5019
- useExisting: CideEleFileInputComponent,
5020
- multi: true
5021
- },
5022
- {
5023
- provide: NG_VALIDATORS,
5024
- useExisting: CideEleFileInputComponent,
5025
- multi: true
5026
- }
5027
- ], usesOnChanges: true, ngImport: i0, template: "<div class=\"tw-flex tw-flex-col tw-gap-2\">\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=\"tw-block tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-1.5 tw-leading-5\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"tw-text-red-500 dark:tw-text-red-400\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"tw-relative\">\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=\"tw-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-relative tw-overflow-hidden hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\n [class]=\"getPreviewBoxClasses()\"\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=\"tw-flex tw-flex-col tw-items-center tw-justify-center tw-h-full tw-p-4\">\n <div class=\"tw-mb-2\">\n <cide-ele-icon class=\"tw-text-gray-400 dark:tw-text-gray-500\" size=\"lg\">{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\n </div>\n <div class=\"tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center\">\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"tw-relative tw-w-full tw-h-full\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"tw-w-full tw-h-full tw-object-cover tw-rounded-lg\">\n <div class=\"tw-absolute tw-inset-0 tw-bg-black tw-bg-opacity-0 hover:tw-bg-opacity-30 tw-transition-all tw-duration-200 tw-flex tw-items-center tw-justify-center tw-rounded-lg\">\n <div class=\"tw-text-white tw-text-sm tw-opacity-0 hover:tw-opacity-100 tw-transition-opacity tw-duration-200\">Click to change</div>\n </div>\n @if (!disabledSignal()) {\n <button \n type=\"button\" \n class=\"tw-absolute tw-top-2 tw-right-2 tw-w-6 tw-h-6 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-sm tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\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=\"tw-mt-2 tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center tw-truncate\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\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=\"tw-hidden\"\n />\n \n <!-- Modern Drag and Drop Zone -->\n <div \n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-min-h-[60px] hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\n [class]=\"getDragDropZoneClasses()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n \n <div class=\"tw-flex tw-items-center tw-justify-between tw-p-3 tw-gap-3\">\n <!-- Icon and Text -->\n <div class=\"tw-flex tw-items-center tw-gap-2.5 tw-flex-1 tw-min-w-0\">\n <cide-ele-icon class=\"tw-flex-shrink-0 tw-transition-colors tw-duration-200\" \n [class]=\"getIconClasses()\" \n size=\"sm\">\n {{ isDragOver() ? 'file_download' : (hasFiles() ? 'check_circle' : 'cloud_upload') }}\n </cide-ele-icon>\n \n <div class=\"tw-flex tw-flex-col tw-gap-0.5 tw-min-w-0\">\n @if (isDragOver()) {\n <span class=\"tw-text-sm tw-font-medium tw-text-blue-700 dark:tw-text-blue-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">Drop files here</span>\n } @else if (hasFiles()) {\n <span class=\"tw-text-sm tw-font-medium tw-text-emerald-700 dark:tw-text-emerald-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\n @if (multipleSignal() && fileNames().length > 1) {\n {{ fileNames().length }} files selected\n } @else {\n {{ fileNames()[0] }}\n }\n </span>\n @if (totalFileSize() > 0) {\n <span class=\"tw-text-xs tw-text-emerald-600 dark:tw-text-emerald-400\">{{ fileSizeInMB() }} MB</span>\n }\n } @else {\n <span class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\n {{ multipleSignal() ? 'Choose files or drag here' : 'Choose file or drag here' }}\n </span>\n }\n </div>\n </div>\n \n <!-- Action Buttons -->\n <div class=\"tw-flex tw-gap-1 tw-flex-shrink-0\">\n @if (hasFiles()) {\n <button type=\"button\" \n class=\"tw-flex tw-items-center tw-justify-center tw-w-6 tw-h-6 tw-border-none tw-rounded tw-bg-transparent tw-cursor-pointer tw-transition-all tw-duration-200 tw-text-red-600 hover:tw-bg-red-50 dark:hover:tw-bg-red-900/20 hover:tw-text-red-700\" \n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Clear files\">\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\n </button>\n }\n </div>\n </div>\n </div>\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"tw-mt-3\">\n <div class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-2\">Preview:</div>\n <div class=\"tw-flex tw-flex-wrap tw-gap-3\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"tw-relative tw-border tw-border-gray-200 dark:tw-border-gray-600 tw-rounded-lg tw-overflow-hidden tw-bg-white dark:tw-bg-gray-800\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\">\n <button \n type=\"button\" \n class=\"tw-absolute tw-top-1 tw-right-1 tw-w-5 tw-h-5 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-xs tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer tw-z-10\"\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=\"tw-w-full tw-h-full tw-object-cover\"\n loading=\"lazy\">\n <div class=\"tw-absolute tw-bottom-0 tw-left-0 tw-right-0 tw-bg-black tw-bg-opacity-75 tw-text-white tw-text-xs tw-p-1 tw-truncate\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n <!-- Upload Status and Show Files Button (only for multiple file inputs) -->\n @if (multiple && showFloatingUploaderSignal() && (getUploadCount() > 0 || hasActiveUploads() || hasEverUploaded())) {\n <div class=\"tw-flex tw-items-center tw-justify-between tw-py-1.5 tw-gap-2\">\n <div class=\"tw-flex tw-items-center tw-gap-2\">\n <cide-ele-icon class=\"tw-text-blue-600 dark:tw-text-blue-400\" size=\"sm\">cloud_upload</cide-ele-icon>\n <span class=\"tw-text-sm tw-text-gray-700 dark:tw-text-gray-300\">\n @if (hasActiveUploads()) {\n {{ getActiveUploadCount() }} uploading\n } @else if (getUploadCount() > 0) {\n {{ getUploadCount() }} completed\n } @else if (hasEverUploaded()) {\n View uploads\n }\n </span>\n </div>\n <button \n type=\"button\" \n class=\"tw-flex tw-items-center tw-justify-center tw-w-8 tw-h-8 tw-rounded-md tw-bg-gray-100 dark:tw-bg-gray-700 hover:tw-bg-gray-200 dark:hover:tw-bg-gray-600 tw-text-gray-600 dark:tw-text-gray-300 tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\n (click)=\"showFloatingUploaderDialog()\"\n title=\"View upload progress and history\">\n <cide-ele-icon size=\"sm\">visibility</cide-ele-icon>\n </button>\n </div>\n }\n \n @if (errorTextSignal()) {\n <div class=\"tw-text-sm tw-text-red-600 dark:tw-text-red-400 tw-mt-1\">{{ errorTextSignal() }}</div>\n }\n @if (helperTextSignal() && !errorTextSignal()) {\n <div class=\"tw-text-sm tw-text-gray-500 dark:tw-text-gray-400 tw-mt-1\">{{ helperTextSignal() }}</div>\n }\n</div> ", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
5028
- }
5029
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, decorators: [{
5030
- type: Component,
5031
- args: [{ selector: 'cide-ele-file-input', standalone: true, imports: [CommonModule, FormsModule, CideIconComponent], providers: [
5032
- {
5033
- provide: NG_VALUE_ACCESSOR,
5034
- useExisting: CideEleFileInputComponent,
5035
- multi: true
5036
- },
5037
- {
5038
- provide: NG_VALIDATORS,
5039
- useExisting: CideEleFileInputComponent,
5040
- multi: true
5041
- }
5042
- ], template: "<div class=\"tw-flex tw-flex-col tw-gap-2\">\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=\"tw-block tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-1.5 tw-leading-5\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"tw-text-red-500 dark:tw-text-red-400\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"tw-relative\">\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=\"tw-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-relative tw-overflow-hidden hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\n [class]=\"getPreviewBoxClasses()\"\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=\"tw-flex tw-flex-col tw-items-center tw-justify-center tw-h-full tw-p-4\">\n <div class=\"tw-mb-2\">\n <cide-ele-icon class=\"tw-text-gray-400 dark:tw-text-gray-500\" size=\"lg\">{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\n </div>\n <div class=\"tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center\">\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"tw-relative tw-w-full tw-h-full\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"tw-w-full tw-h-full tw-object-cover tw-rounded-lg\">\n <div class=\"tw-absolute tw-inset-0 tw-bg-black tw-bg-opacity-0 hover:tw-bg-opacity-30 tw-transition-all tw-duration-200 tw-flex tw-items-center tw-justify-center tw-rounded-lg\">\n <div class=\"tw-text-white tw-text-sm tw-opacity-0 hover:tw-opacity-100 tw-transition-opacity tw-duration-200\">Click to change</div>\n </div>\n @if (!disabledSignal()) {\n <button \n type=\"button\" \n class=\"tw-absolute tw-top-2 tw-right-2 tw-w-6 tw-h-6 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-sm tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\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=\"tw-mt-2 tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-text-center tw-truncate\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\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=\"tw-hidden\"\n />\n \n <!-- Modern Drag and Drop Zone -->\n <div \n class=\"tw-border-2 tw-border-dashed tw-border-gray-300 dark:tw-border-gray-600 tw-rounded-lg tw-bg-gray-50 dark:tw-bg-gray-800 tw-cursor-pointer tw-transition-all tw-duration-200 tw-min-h-[60px] hover:tw-border-blue-500 hover:tw-bg-blue-50 dark:hover:tw-bg-blue-900/20\"\n [class]=\"getDragDropZoneClasses()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n \n <div class=\"tw-flex tw-items-center tw-justify-between tw-p-3 tw-gap-3\">\n <!-- Icon and Text -->\n <div class=\"tw-flex tw-items-center tw-gap-2.5 tw-flex-1 tw-min-w-0\">\n <cide-ele-icon class=\"tw-flex-shrink-0 tw-transition-colors tw-duration-200\" \n [class]=\"getIconClasses()\" \n size=\"sm\">\n {{ isDragOver() ? 'file_download' : (hasFiles() ? 'check_circle' : 'cloud_upload') }}\n </cide-ele-icon>\n \n <div class=\"tw-flex tw-flex-col tw-gap-0.5 tw-min-w-0\">\n @if (isDragOver()) {\n <span class=\"tw-text-sm tw-font-medium tw-text-blue-700 dark:tw-text-blue-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">Drop files here</span>\n } @else if (hasFiles()) {\n <span class=\"tw-text-sm tw-font-medium tw-text-emerald-700 dark:tw-text-emerald-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\n @if (multipleSignal() && fileNames().length > 1) {\n {{ fileNames().length }} files selected\n } @else {\n {{ fileNames()[0] }}\n }\n </span>\n @if (totalFileSize() > 0) {\n <span class=\"tw-text-xs tw-text-emerald-600 dark:tw-text-emerald-400\">{{ fileSizeInMB() }} MB</span>\n }\n } @else {\n <span class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-300 tw-whitespace-nowrap tw-overflow-hidden tw-text-ellipsis\">\n {{ multipleSignal() ? 'Choose files or drag here' : 'Choose file or drag here' }}\n </span>\n }\n </div>\n </div>\n \n <!-- Action Buttons -->\n <div class=\"tw-flex tw-gap-1 tw-flex-shrink-0\">\n @if (hasFiles()) {\n <button type=\"button\" \n class=\"tw-flex tw-items-center tw-justify-center tw-w-6 tw-h-6 tw-border-none tw-rounded tw-bg-transparent tw-cursor-pointer tw-transition-all tw-duration-200 tw-text-red-600 hover:tw-bg-red-50 dark:hover:tw-bg-red-900/20 hover:tw-text-red-700\" \n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Clear files\">\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\n </button>\n }\n </div>\n </div>\n </div>\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"tw-mt-3\">\n <div class=\"tw-text-sm tw-font-medium tw-text-gray-700 dark:tw-text-gray-200 tw-mb-2\">Preview:</div>\n <div class=\"tw-flex tw-flex-wrap tw-gap-3\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"tw-relative tw-border tw-border-gray-200 dark:tw-border-gray-600 tw-rounded-lg tw-overflow-hidden tw-bg-white dark:tw-bg-gray-800\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\">\n <button \n type=\"button\" \n class=\"tw-absolute tw-top-1 tw-right-1 tw-w-5 tw-h-5 tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-xs tw-font-bold tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer tw-z-10\"\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=\"tw-w-full tw-h-full tw-object-cover\"\n loading=\"lazy\">\n <div class=\"tw-absolute tw-bottom-0 tw-left-0 tw-right-0 tw-bg-black tw-bg-opacity-75 tw-text-white tw-text-xs tw-p-1 tw-truncate\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n <!-- Upload Status and Show Files Button (only for multiple file inputs) -->\n @if (multiple && showFloatingUploaderSignal() && (getUploadCount() > 0 || hasActiveUploads() || hasEverUploaded())) {\n <div class=\"tw-flex tw-items-center tw-justify-between tw-py-1.5 tw-gap-2\">\n <div class=\"tw-flex tw-items-center tw-gap-2\">\n <cide-ele-icon class=\"tw-text-blue-600 dark:tw-text-blue-400\" size=\"sm\">cloud_upload</cide-ele-icon>\n <span class=\"tw-text-sm tw-text-gray-700 dark:tw-text-gray-300\">\n @if (hasActiveUploads()) {\n {{ getActiveUploadCount() }} uploading\n } @else if (getUploadCount() > 0) {\n {{ getUploadCount() }} completed\n } @else if (hasEverUploaded()) {\n View uploads\n }\n </span>\n </div>\n <button \n type=\"button\" \n class=\"tw-flex tw-items-center tw-justify-center tw-w-8 tw-h-8 tw-rounded-md tw-bg-gray-100 dark:tw-bg-gray-700 hover:tw-bg-gray-200 dark:hover:tw-bg-gray-600 tw-text-gray-600 dark:tw-text-gray-300 tw-transition-colors tw-duration-200 tw-border-none tw-cursor-pointer\"\n (click)=\"showFloatingUploaderDialog()\"\n title=\"View upload progress and history\">\n <cide-ele-icon size=\"sm\">visibility</cide-ele-icon>\n </button>\n </div>\n }\n \n @if (errorTextSignal()) {\n <div class=\"tw-text-sm tw-text-red-600 dark:tw-text-red-400 tw-mt-1\">{{ errorTextSignal() }}</div>\n }\n @if (helperTextSignal() && !errorTextSignal()) {\n <div class=\"tw-text-sm tw-text-gray-500 dark:tw-text-gray-400 tw-mt-1\">{{ helperTextSignal() }}</div>\n }\n</div> " }]
5043
- }], ctorParameters: () => [], propDecorators: { label: [{
5044
- type: Input
5045
- }], accept: [{
5046
- type: Input
5047
- }], multiple: [{
5048
- type: Input
5049
- }], disabled: [{
5050
- type: Input
5051
- }], required: [{
5052
- type: Input
5053
- }], helperText: [{
5054
- type: Input
5055
- }], errorText: [{
5056
- type: Input
5057
- }], showPreview: [{
5058
- type: Input
5059
- }], previewWidth: [{
5060
- type: Input
5061
- }], previewHeight: [{
5062
- type: Input
5063
- }], previewBoxMode: [{
5064
- type: Input
5065
- }], showFileName: [{
5066
- type: Input
5067
- }], placeholderText: [{
5068
- type: Input
5069
- }], placeholderIcon: [{
5070
- type: Input
5071
- }], autoUpload: [{
5072
- type: Input
5073
- }], uploadData: [{
5074
- type: Input
5075
- }], showFloatingUploader: [{
5076
- type: Input
5077
- }], floatingUploaderGroupId: [{
5078
- type: Input
5079
- }], fileChange: [{
5080
- type: Output
5081
- }], uploadSuccess: [{
5082
- type: Output
5083
- }], uploadError: [{
5084
- type: Output
5085
- }], uploadProgressChange: [{
5086
- type: Output
5087
- }] } });
5088
-
5089
- class CideFloatingUploadTriggerDirective {
5090
- elementRef = inject(ElementRef);
5091
- floatingUploader = inject(CideEleFloatingFileUploaderComponent);
5092
- groupId;
5093
- userId;
5094
- showIcon = true;
5095
- filesSelected = new EventEmitter();
5096
- triggerIcon;
5097
- ngOnInit() {
5098
- this.setupTrigger();
4823
+ /**
4824
+ * Handle file input change
4825
+ */
4826
+ onFileInputChange(event) {
4827
+ const input = event.target;
4828
+ if (input.files && input.files.length > 0) {
4829
+ this.handleFileSelection(Array.from(input.files));
4830
+ input.value = ''; // Reset input
4831
+ }
5099
4832
  }
5100
- ngOnDestroy() {
5101
- this.removeTrigger();
4833
+ /**
4834
+ * Handle file selection from drag/drop or file input
4835
+ */
4836
+ handleFileSelection(files) {
4837
+ const groupId = this.currentGroupId();
4838
+ // Group ID must be provided by the file input component
4839
+ if (!groupId) {
4840
+ console.error('❌ [FloatingFileUploader] No group ID available. Files cannot be uploaded without a group ID from the file input component.');
4841
+ return;
4842
+ }
4843
+ // Upload files using the file manager service
4844
+ files.forEach((file) => {
4845
+ this.fileManagerService.uploadFile(file, {
4846
+ groupId: groupId,
4847
+ isMultiple: true,
4848
+ userId: this.currentUserId()
4849
+ });
4850
+ });
5102
4851
  }
5103
- setupTrigger() {
5104
- const element = this.elementRef.nativeElement;
5105
- if (element.type !== 'file') {
5106
- console.warn('⚠️ [FloatingUploadTrigger] Directive should only be used on file input elements');
4852
+ /**
4853
+ * Update cached dimensions (throttled for performance)
4854
+ */
4855
+ updateCachedDimensions() {
4856
+ const now = Date.now();
4857
+ // Only update dimensions every 100ms to avoid excessive DOM queries
4858
+ if (now - this.lastDimensionUpdate < 100) {
5107
4859
  return;
5108
4860
  }
5109
- // Add change event listener
5110
- element.addEventListener('change', this.onFileChange.bind(this));
5111
- // Add floating uploader trigger icon if enabled
5112
- if (this.showIcon) {
5113
- this.addTriggerIcon();
4861
+ const uploaderElement = document.querySelector('.floating-uploader');
4862
+ if (uploaderElement) {
4863
+ this.cachedDimensions = {
4864
+ width: uploaderElement.offsetWidth,
4865
+ height: uploaderElement.offsetHeight
4866
+ };
4867
+ this.lastDimensionUpdate = now;
5114
4868
  }
5115
4869
  }
5116
- removeTrigger() {
5117
- const element = this.elementRef.nativeElement;
5118
- element.removeEventListener('change', this.onFileChange.bind(this));
5119
- if (this.triggerIcon) {
5120
- this.triggerIcon.remove();
5121
- }
4870
+ /**
4871
+ * Start dragging the uploader
4872
+ */
4873
+ startDrag(event) {
4874
+ event.preventDefault();
4875
+ const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
4876
+ const clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY;
4877
+ const currentPos = this.position();
4878
+ this.dragOffset = {
4879
+ x: clientX - currentPos.x,
4880
+ y: clientY - currentPos.y
4881
+ };
4882
+ this.isDragging.set(true);
4883
+ // Update cached dimensions at the start of drag for better performance
4884
+ this.updateCachedDimensions();
4885
+ // Add event listeners for drag and end
4886
+ const moveHandler = (e) => this.onDrag(e);
4887
+ const endHandler = () => this.endDrag(moveHandler, endHandler);
4888
+ document.addEventListener('mousemove', moveHandler, { passive: false });
4889
+ document.addEventListener('mouseup', endHandler);
4890
+ document.addEventListener('touchmove', moveHandler, { passive: false });
4891
+ document.addEventListener('touchend', endHandler);
4892
+ // Prevent text selection during drag
4893
+ document.body.style.userSelect = 'none';
5122
4894
  }
5123
- onFileChange(event) {
5124
- const target = event.target;
5125
- const files = target.files;
5126
- if (files && files.length > 0) {
5127
- console.log('📁 [FloatingUploadTrigger] Files selected:', files.length);
5128
- // Show floating uploader
5129
- this.floatingUploader.showUploader(this.groupId);
5130
- // Handle files through floating uploader
5131
- this.floatingUploader.handleExternalFiles(Array.from(files), this.userId, this.groupId);
5132
- // Emit files selected event
5133
- this.filesSelected.emit(Array.from(files));
5134
- }
5135
- }
5136
- addTriggerIcon() {
5137
- const element = this.elementRef.nativeElement;
5138
- const container = element.parentElement;
5139
- if (!container)
4895
+ /**
4896
+ * Handle dragging movement
4897
+ */
4898
+ onDrag(event) {
4899
+ if (!this.isDragging())
5140
4900
  return;
5141
- // Create trigger icon
5142
- this.triggerIcon = document.createElement('button');
5143
- this.triggerIcon.type = 'button';
5144
- this.triggerIcon.className = 'floating-upload-trigger-icon';
5145
- this.triggerIcon.innerHTML = `
5146
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
5147
- <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
5148
- </svg>
5149
- `;
5150
- this.triggerIcon.title = 'View upload progress';
5151
- this.triggerIcon.style.cssText = `
5152
- position: absolute;
5153
- right: 8px;
5154
- top: 50%;
5155
- transform: translateY(-50%);
5156
- background: #3b82f6;
5157
- color: white;
5158
- border: none;
5159
- border-radius: 4px;
5160
- padding: 4px;
5161
- cursor: pointer;
5162
- display: flex;
5163
- align-items: center;
5164
- justify-content: center;
5165
- z-index: 10;
5166
- `;
5167
- // Make container relative positioned
5168
- container.style.position = 'relative';
5169
- // Add click handler to show floating uploader
5170
- this.triggerIcon.addEventListener('click', (e) => {
5171
- e.preventDefault();
5172
- e.stopPropagation();
5173
- this.floatingUploader.showUploader(this.groupId);
4901
+ event.preventDefault();
4902
+ const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
4903
+ const clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY;
4904
+ const newX = clientX - this.dragOffset.x;
4905
+ const newY = clientY - this.dragOffset.y;
4906
+ // Constrain to viewport bounds using cached dimensions for performance
4907
+ const viewportWidth = window.innerWidth;
4908
+ const viewportHeight = window.innerHeight;
4909
+ // Use cached dimensions instead of DOM queries for better performance
4910
+ const uploaderWidth = this.cachedDimensions.width;
4911
+ const uploaderHeight = this.cachedDimensions.height;
4912
+ // Ensure uploader stays within viewport bounds
4913
+ const constrainedX = Math.max(0, Math.min(newX, viewportWidth - uploaderWidth));
4914
+ const constrainedY = Math.max(0, Math.min(newY, viewportHeight - uploaderHeight));
4915
+ this.position.set({ x: constrainedX, y: constrainedY });
4916
+ }
4917
+ /**
4918
+ * End dragging
4919
+ */
4920
+ endDrag(moveHandler, endHandler) {
4921
+ this.isDragging.set(false);
4922
+ // Remove event listeners
4923
+ document.removeEventListener('mousemove', moveHandler);
4924
+ document.removeEventListener('mouseup', endHandler);
4925
+ document.removeEventListener('touchmove', moveHandler);
4926
+ document.removeEventListener('touchend', endHandler);
4927
+ // Restore text selection
4928
+ document.body.style.userSelect = '';
4929
+ }
4930
+ /**
4931
+ * Set up window resize listener to keep uploader within bounds
4932
+ */
4933
+ setupWindowResize() {
4934
+ const handleResize = () => {
4935
+ const currentPos = this.position();
4936
+ const viewportWidth = window.innerWidth;
4937
+ const viewportHeight = window.innerHeight;
4938
+ // Update cached dimensions on resize
4939
+ this.updateCachedDimensions();
4940
+ // Use cached dimensions for performance
4941
+ const uploaderWidth = this.cachedDimensions.width;
4942
+ const uploaderHeight = this.cachedDimensions.height;
4943
+ // Constrain position to new viewport bounds
4944
+ const constrainedX = Math.max(0, Math.min(currentPos.x, viewportWidth - uploaderWidth));
4945
+ const constrainedY = Math.max(0, Math.min(currentPos.y, viewportHeight - uploaderHeight));
4946
+ // Update position if it changed
4947
+ if (constrainedX !== currentPos.x || constrainedY !== currentPos.y) {
4948
+ this.position.set({ x: constrainedX, y: constrainedY });
4949
+ console.log('📐 [FloatingFileUploader] Position adjusted for window resize:', { x: constrainedX, y: constrainedY });
4950
+ }
4951
+ };
4952
+ window.addEventListener('resize', handleResize);
4953
+ // Store reference for cleanup
4954
+ this.windowResizeHandler = handleResize;
4955
+ }
4956
+ /**
4957
+ * Initialize default position
4958
+ */
4959
+ initializePosition() {
4960
+ // Set initial position to bottom-right corner
4961
+ const viewportWidth = window.innerWidth;
4962
+ const viewportHeight = window.innerHeight;
4963
+ // Initialize cached dimensions with defaults
4964
+ this.cachedDimensions = { width: 320, height: 300 };
4965
+ // Use cached dimensions for initial positioning
4966
+ const uploaderWidth = this.cachedDimensions.width;
4967
+ const uploaderHeight = this.cachedDimensions.height;
4968
+ // Ensure initial position is within bounds
4969
+ const initialX = Math.max(20, viewportWidth - uploaderWidth - 20);
4970
+ const initialY = Math.max(20, viewportHeight - uploaderHeight - 20);
4971
+ this.position.set({
4972
+ x: initialX,
4973
+ y: initialY
5174
4974
  });
5175
- // Insert icon after the input
5176
- element.parentNode?.insertBefore(this.triggerIcon, element.nextSibling);
4975
+ // Update dimensions after a short delay to get actual rendered size
4976
+ setTimeout(() => {
4977
+ this.updateCachedDimensions();
4978
+ }, 100);
5177
4979
  }
5178
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideFloatingUploadTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
5179
- 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 });
4980
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4981
+ 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 -->\n@if (isVisible()) {\n<div class=\"floating-uploader\" \n [class.minimized]=\"isMinimized()\" \n [class.animating]=\"isAnimating()\"\n [style.left.px]=\"position().x\"\n [style.top.px]=\"position().y\">\n\n <!-- Header (Draggable) -->\n <div class=\"uploader-header draggable-header\"\n (mousedown)=\"startDrag($event)\"\n (touchstart)=\"startDrag($event)\">\n <div class=\"header-left\">\n <div class=\"upload-icon\">\n <cide-ele-icon size=\"sm\">cloud_upload</cide-ele-icon>\n </div>\n <div class=\"upload-info\">\n <div class=\"upload-title\">File Upload</div>\n <div class=\"upload-summary\">{{ getUploadSummary() }}</div>\n </div>\n </div>\n \n <div class=\"header-actions\">\n <button class=\"action-btn minimize-btn\" (click)=\"toggleMinimize()\" [title]=\"isMinimized() ? 'Expand' : 'Minimize'\">\n <cide-ele-icon size=\"xs\">{{ isMinimized() ? 'expand_more' : 'expand_less' }}</cide-ele-icon>\n </button>\n <button class=\"action-btn close-btn\" (click)=\"close()\" title=\"Close\">\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\n </button>\n </div>\n </div>\n\n <!-- Content (hidden when minimized) -->\n @if (!isMinimized()) {\n <div class=\"uploader-content\">\n \n <!-- Drag and Drop Zone -->\n <div class=\"upload-zone\" \n [class.drag-over]=\"isDragOver()\" \n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\" \n (drop)=\"onDrop($event)\" \n (click)=\"triggerFileInput()\">\n \n <!-- Hidden file input -->\n <input #fileInput \n type=\"file\" \n [multiple]=\"true\" \n [accept]=\"'*/*'\" \n (change)=\"onFileInputChange($event)\" \n style=\"display: none;\">\n \n <div class=\"upload-zone-content\">\n <cide-ele-icon class=\"upload-icon\" size=\"sm\">cloud_upload</cide-ele-icon>\n \n <div class=\"upload-text\">\n <div class=\"upload-title\">\n {{ isDragOver() ? 'Drop files here' : 'Drag files here or click to browse' }}\n </div>\n </div>\n </div>\n </div>\n \n <!-- Upload Queue - Show files from service state -->\n @if (allFilesForGroup().length > 0) {\n <div class=\"upload-queue\">\n <!-- Show all files from service state -->\n @for (file of allFilesForGroup(); track file.fileId) {\n <div class=\"upload-item\" [class]=\"getStatusClass(file.stage)\">\n <div class=\"file-info\">\n <cide-ele-icon class=\"status-icon\" size=\"xs\">{{ getStatusIcon(file.stage) }}</cide-ele-icon>\n <div class=\"file-details\">\n <div class=\"file-name\">{{ file.fileName }}</div>\n <div class=\"file-status\">\n @switch (file.stage) {\n @case ('pending') {\n <span class=\"text-yellow-600\">Waiting...</span>\n }\n @case ('reading') {\n <span class=\"text-yellow-600\">Reading...</span>\n }\n @case ('uploading') {\n <span class=\"text-blue-600\">Uploading...</span>\n }\n @case ('complete') {\n <span class=\"text-green-600\">Completed</span>\n }\n @case ('error') {\n <span class=\"text-red-600\">Failed</span>\n }\n }\n </div>\n </div>\n </div>\n\n <!-- Progress Bar (only for uploading files) -->\n @if (file.stage === 'uploading' && file.percentage !== undefined) {\n <div class=\"file-progress\">\n <div class=\"progress-bar\">\n <div class=\"progress-fill\" [style.width.%]=\"file.percentage\"></div>\n </div>\n <span class=\"progress-text\">{{ file.percentage }}%</span>\n </div>\n }\n\n <!-- Actions -->\n <div class=\"upload-actions\">\n @switch (file.stage) {\n @case ('pending') {\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(file.fileId)\" title=\"Cancel\">\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\n </button>\n }\n @case ('reading') {\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(file.fileId)\" title=\"Cancel\">\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\n </button>\n }\n @case ('uploading') {\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(file.fileId)\" title=\"Cancel\">\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\n </button>\n }\n @case ('complete') {\n <button class=\"action-btn success-btn\" title=\"Completed\">\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\n </button>\n }\n @case ('error') {\n <button class=\"action-btn retry-btn\" title=\"Retry\">\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\n </button>\n }\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- No uploads message when manually opened -->\n <div class=\"no-uploads-message\">\n <div class=\"message-content\">\n <cide-ele-icon size=\"md\" class=\"message-icon\">cloud_upload</cide-ele-icon>\n <div class=\"message-text\">\n <h4>No active uploads</h4>\n <p>Upload files to see their progress here</p>\n </div>\n </div>\n </div>\n }\n </div>\n }\n</div>\n}\n", styles: [".floating-uploader{position:fixed;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.draggable-header{cursor:move;-webkit-user-select:none;user-select:none}.floating-uploader .uploader-header.draggable-header:hover{background:#f1f5f9}.floating-uploader .uploader-header.draggable-header:active{background:#e2e8f0;cursor:grabbing}.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 .upload-zone{margin:8px 16px;padding:12px;border:2px dashed #d1d5db;border-radius:6px;background:#f9fafb;cursor:pointer;transition:all .2s ease;text-align:center}.floating-uploader .uploader-content .upload-zone:hover{border-color:#3b82f6;background:#f0f9ff}.floating-uploader .uploader-content .upload-zone.drag-over{border-color:#3b82f6;background:#dbeafe;transform:scale(1.01)}.floating-uploader .uploader-content .upload-zone .upload-zone-content{display:flex;flex-direction:column;align-items:center;gap:6px}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-icon{color:#6b7280;transition:color .2s ease}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-text .upload-title{font-size:13px;font-weight:500;color:#374151;margin:0;line-height:1.2}.floating-uploader .uploader-content .upload-zone:hover .upload-zone-content .upload-icon{color:#3b82f6}.floating-uploader .uploader-content .upload-zone.drag-over .upload-zone-content .upload-icon{color:#1d4ed8}.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.draggable-header:hover{background:#475569}.floating-uploader .uploader-header.draggable-header:active{background:#64748b}.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 .upload-zone{border-color:#475569;background:#334155}.floating-uploader .uploader-content .upload-zone:hover{border-color:#3b82f6;background:#1e3a8a}.floating-uploader .uploader-content .upload-zone.drag-over{border-color:#60a5fa;background:#1e40af}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-icon{color:#94a3b8}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-text .upload-title{color:#f1f5f9}.floating-uploader .uploader-content .upload-zone:hover .upload-zone-content .upload-icon{color:#60a5fa}.floating-uploader .uploader-content .upload-zone.drag-over .upload-zone-content .upload-icon{color:#93c5fd}.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"] }] });
5180
4982
  }
5181
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideFloatingUploadTriggerDirective, decorators: [{
5182
- type: Directive,
5183
- args: [{
5184
- selector: '[cideFloatingUploadTrigger]',
5185
- standalone: true
5186
- }]
5187
- }], propDecorators: { groupId: [{
5188
- type: Input
5189
- }], userId: [{
5190
- type: Input
5191
- }], showIcon: [{
5192
- type: Input
5193
- }], filesSelected: [{
5194
- type: Output
5195
- }] } });
4983
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, decorators: [{
4984
+ type: Component,
4985
+ args: [{ selector: 'cide-ele-floating-file-uploader', standalone: true, imports: [
4986
+ CommonModule,
4987
+ CideIconComponent
4988
+ ], template: "<!-- Floating File Uploader Container -->\n@if (isVisible()) {\n<div class=\"floating-uploader\" \n [class.minimized]=\"isMinimized()\" \n [class.animating]=\"isAnimating()\"\n [style.left.px]=\"position().x\"\n [style.top.px]=\"position().y\">\n\n <!-- Header (Draggable) -->\n <div class=\"uploader-header draggable-header\"\n (mousedown)=\"startDrag($event)\"\n (touchstart)=\"startDrag($event)\">\n <div class=\"header-left\">\n <div class=\"upload-icon\">\n <cide-ele-icon size=\"sm\">cloud_upload</cide-ele-icon>\n </div>\n <div class=\"upload-info\">\n <div class=\"upload-title\">File Upload</div>\n <div class=\"upload-summary\">{{ getUploadSummary() }}</div>\n </div>\n </div>\n \n <div class=\"header-actions\">\n <button class=\"action-btn minimize-btn\" (click)=\"toggleMinimize()\" [title]=\"isMinimized() ? 'Expand' : 'Minimize'\">\n <cide-ele-icon size=\"xs\">{{ isMinimized() ? 'expand_more' : 'expand_less' }}</cide-ele-icon>\n </button>\n <button class=\"action-btn close-btn\" (click)=\"close()\" title=\"Close\">\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\n </button>\n </div>\n </div>\n\n <!-- Content (hidden when minimized) -->\n @if (!isMinimized()) {\n <div class=\"uploader-content\">\n \n <!-- Drag and Drop Zone -->\n <div class=\"upload-zone\" \n [class.drag-over]=\"isDragOver()\" \n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\" \n (drop)=\"onDrop($event)\" \n (click)=\"triggerFileInput()\">\n \n <!-- Hidden file input -->\n <input #fileInput \n type=\"file\" \n [multiple]=\"true\" \n [accept]=\"'*/*'\" \n (change)=\"onFileInputChange($event)\" \n style=\"display: none;\">\n \n <div class=\"upload-zone-content\">\n <cide-ele-icon class=\"upload-icon\" size=\"sm\">cloud_upload</cide-ele-icon>\n \n <div class=\"upload-text\">\n <div class=\"upload-title\">\n {{ isDragOver() ? 'Drop files here' : 'Drag files here or click to browse' }}\n </div>\n </div>\n </div>\n </div>\n \n <!-- Upload Queue - Show files from service state -->\n @if (allFilesForGroup().length > 0) {\n <div class=\"upload-queue\">\n <!-- Show all files from service state -->\n @for (file of allFilesForGroup(); track file.fileId) {\n <div class=\"upload-item\" [class]=\"getStatusClass(file.stage)\">\n <div class=\"file-info\">\n <cide-ele-icon class=\"status-icon\" size=\"xs\">{{ getStatusIcon(file.stage) }}</cide-ele-icon>\n <div class=\"file-details\">\n <div class=\"file-name\">{{ file.fileName }}</div>\n <div class=\"file-status\">\n @switch (file.stage) {\n @case ('pending') {\n <span class=\"text-yellow-600\">Waiting...</span>\n }\n @case ('reading') {\n <span class=\"text-yellow-600\">Reading...</span>\n }\n @case ('uploading') {\n <span class=\"text-blue-600\">Uploading...</span>\n }\n @case ('complete') {\n <span class=\"text-green-600\">Completed</span>\n }\n @case ('error') {\n <span class=\"text-red-600\">Failed</span>\n }\n }\n </div>\n </div>\n </div>\n\n <!-- Progress Bar (only for uploading files) -->\n @if (file.stage === 'uploading' && file.percentage !== undefined) {\n <div class=\"file-progress\">\n <div class=\"progress-bar\">\n <div class=\"progress-fill\" [style.width.%]=\"file.percentage\"></div>\n </div>\n <span class=\"progress-text\">{{ file.percentage }}%</span>\n </div>\n }\n\n <!-- Actions -->\n <div class=\"upload-actions\">\n @switch (file.stage) {\n @case ('pending') {\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(file.fileId)\" title=\"Cancel\">\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\n </button>\n }\n @case ('reading') {\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(file.fileId)\" title=\"Cancel\">\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\n </button>\n }\n @case ('uploading') {\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(file.fileId)\" title=\"Cancel\">\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\n </button>\n }\n @case ('complete') {\n <button class=\"action-btn success-btn\" title=\"Completed\">\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\n </button>\n }\n @case ('error') {\n <button class=\"action-btn retry-btn\" title=\"Retry\">\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\n </button>\n }\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- No uploads message when manually opened -->\n <div class=\"no-uploads-message\">\n <div class=\"message-content\">\n <cide-ele-icon size=\"md\" class=\"message-icon\">cloud_upload</cide-ele-icon>\n <div class=\"message-text\">\n <h4>No active uploads</h4>\n <p>Upload files to see their progress here</p>\n </div>\n </div>\n </div>\n }\n </div>\n }\n</div>\n}\n", styles: [".floating-uploader{position:fixed;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.draggable-header{cursor:move;-webkit-user-select:none;user-select:none}.floating-uploader .uploader-header.draggable-header:hover{background:#f1f5f9}.floating-uploader .uploader-header.draggable-header:active{background:#e2e8f0;cursor:grabbing}.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 .upload-zone{margin:8px 16px;padding:12px;border:2px dashed #d1d5db;border-radius:6px;background:#f9fafb;cursor:pointer;transition:all .2s ease;text-align:center}.floating-uploader .uploader-content .upload-zone:hover{border-color:#3b82f6;background:#f0f9ff}.floating-uploader .uploader-content .upload-zone.drag-over{border-color:#3b82f6;background:#dbeafe;transform:scale(1.01)}.floating-uploader .uploader-content .upload-zone .upload-zone-content{display:flex;flex-direction:column;align-items:center;gap:6px}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-icon{color:#6b7280;transition:color .2s ease}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-text .upload-title{font-size:13px;font-weight:500;color:#374151;margin:0;line-height:1.2}.floating-uploader .uploader-content .upload-zone:hover .upload-zone-content .upload-icon{color:#3b82f6}.floating-uploader .uploader-content .upload-zone.drag-over .upload-zone-content .upload-icon{color:#1d4ed8}.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.draggable-header:hover{background:#475569}.floating-uploader .uploader-header.draggable-header:active{background:#64748b}.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 .upload-zone{border-color:#475569;background:#334155}.floating-uploader .uploader-content .upload-zone:hover{border-color:#3b82f6;background:#1e3a8a}.floating-uploader .uploader-content .upload-zone.drag-over{border-color:#60a5fa;background:#1e40af}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-icon{color:#94a3b8}.floating-uploader .uploader-content .upload-zone .upload-zone-content .upload-text .upload-title{color:#f1f5f9}.floating-uploader .uploader-content .upload-zone:hover .upload-zone-content .upload-icon{color:#60a5fa}.floating-uploader .uploader-content .upload-zone.drag-over .upload-zone-content .upload-icon{color:#93c5fd}.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"] }]
4989
+ }], ctorParameters: () => [] });
5196
4990
 
5197
4991
  class CideTextareaComponent {
5198
4992
  label = '';
@@ -9432,5 +9226,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
9432
9226
  * Generated bundle index. Do not edit.
9433
9227
  */
9434
9228
 
9435
- export { CideCoreFileManagerService, CideEleButtonComponent, CideEleConfirmationModalComponent, CideEleDataGridComponent, CideEleDropdownComponent, CideEleFileImageDirective, CideEleFileInputComponent, CideEleFileManagerService, CideEleFloatingFileUploaderComponent, CideEleGlobalNotificationsComponent, CideEleJsonEditorComponent, CideEleResizerDirective, CideEleSkeletonLoaderComponent, CideEleTabComponent, CideEleToastNotificationComponent, CideElementsService, CideFloatingUploadTriggerDirective, CideIconComponent, CideInputComponent, CideSelectComponent, CideSelectOptionComponent, CideSpinnerComponent, CideTextareaComponent, ConfirmationService, CoreFileManagerInsertUpdatePayload, DEFAULT_GRID_CONFIG, DropdownManagerService, ICoreCyfmSave, MFileManager, NotificationService, TooltipDirective };
9229
+ export { CideCoreFileManagerService, CideEleButtonComponent, CideEleConfirmationModalComponent, CideEleDataGridComponent, CideEleDropdownComponent, CideEleFileImageDirective, CideEleFileInputComponent, CideEleFileManagerService, CideEleFloatingFileUploaderComponent, CideEleGlobalNotificationsComponent, CideEleJsonEditorComponent, CideEleResizerDirective, CideEleSkeletonLoaderComponent, CideEleTabComponent, CideEleToastNotificationComponent, CideElementsService, CideIconComponent, CideInputComponent, CideSelectComponent, CideSelectOptionComponent, CideSpinnerComponent, CideTextareaComponent, ConfirmationService, CoreFileManagerInsertUpdatePayload, DEFAULT_GRID_CONFIG, DropdownManagerService, ICoreCyfmSave, MFileManager, NotificationService, TooltipDirective };
9436
9230
  //# sourceMappingURL=cloud-ide-element.mjs.map