cloud-ide-element 1.0.106 → 1.0.109

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';
@@ -2650,6 +2650,21 @@ class CideEleFileManagerService {
2650
2650
  this._fetchedFiles().forEach(files => total += files.length);
2651
2651
  return total;
2652
2652
  }, ...(ngDevMode ? [{ debugName: "totalFetchedFiles" }] : []));
2653
+ // Optimized computed properties for file counting by group
2654
+ getFileCountByGroup = computed(() => {
2655
+ const countMap = new Map();
2656
+ // Count active uploads by group
2657
+ this._activeUploads().forEach((upload, fileId) => {
2658
+ if (upload.groupId) {
2659
+ countMap.set(upload.groupId, (countMap.get(upload.groupId) || 0) + 1);
2660
+ }
2661
+ });
2662
+ // Add fetched files by group
2663
+ this._fetchedFiles().forEach((files, groupId) => {
2664
+ countMap.set(groupId, (countMap.get(groupId) || 0) + files.length);
2665
+ });
2666
+ return countMap;
2667
+ }, ...(ngDevMode ? [{ debugName: "getFileCountByGroup" }] : []));
2653
2668
  serviceState = computed(() => ({
2654
2669
  isUploading: this._isUploading(),
2655
2670
  uploadQueue: this._uploadQueue(),
@@ -2981,16 +2996,21 @@ class CideEleFileManagerService {
2981
2996
  * Signal to trigger floating uploader visibility
2982
2997
  */
2983
2998
  _showFloatingUploader = signal(false, ...(ngDevMode ? [{ debugName: "_showFloatingUploader" }] : []));
2999
+ _triggerGroupId = signal(null, ...(ngDevMode ? [{ debugName: "_triggerGroupId" }] : []));
2984
3000
  showFloatingUploader = this._showFloatingUploader.asReadonly();
3001
+ getTriggerGroupId = this._triggerGroupId.asReadonly();
2985
3002
  /**
2986
- * Trigger floating uploader to show
3003
+ * Trigger floating uploader to show with group ID
3004
+ * This is the ONLY way to pass group ID to floating uploader
2987
3005
  */
2988
- triggerFloatingUploaderShow() {
2989
- console.log('🎬 [FileManagerService] Triggering floating uploader to show groupId');
3006
+ triggerFloatingUploaderShow(groupId) {
3007
+ console.log('🎬 [FileManagerService] Triggering floating uploader to show with groupId:', groupId);
3008
+ this._triggerGroupId.set(groupId || null);
2990
3009
  this._showFloatingUploader.set(true);
2991
3010
  // Reset after a short delay to allow components to react
2992
3011
  setTimeout(() => {
2993
3012
  this._showFloatingUploader.set(false);
3013
+ this._triggerGroupId.set(null);
2994
3014
  }, 100);
2995
3015
  }
2996
3016
  /**
@@ -3078,6 +3098,20 @@ class CideEleFileManagerService {
3078
3098
  console.log('🧹 [FileManagerService] Cleared all completed uploads');
3079
3099
  }
3080
3100
  }
3101
+ /**
3102
+ * Optimized method to get file count for a specific group
3103
+ * Uses computed property for better performance
3104
+ */
3105
+ getFileCountForGroup(groupId) {
3106
+ return this.getFileCountByGroup().get(groupId) || 0;
3107
+ }
3108
+ /**
3109
+ * Optimized method to check if group has active uploads
3110
+ */
3111
+ hasActiveUploadsForGroup(groupId) {
3112
+ return Array.from(this._activeUploads().values())
3113
+ .some(upload => upload.groupId === groupId && upload.stage !== 'complete');
3114
+ }
3081
3115
  /**
3082
3116
  * Angular 20: File validation utility
3083
3117
  */
@@ -3322,1877 +3356,1623 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
3322
3356
  }]
3323
3357
  }], ctorParameters: () => [] });
3324
3358
 
3325
- class CideEleFloatingFileUploaderComponent {
3326
- destroyRef = inject(DestroyRef);
3359
+ class CideEleFileInputComponent {
3327
3360
  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
3361
+ notificationService = inject(NotificationService);
3362
+ elementService = inject(CideElementsService);
3363
+ destroyRef = inject(DestroyRef);
3364
+ // private readonly pendingTasks = inject(PendingTasks); // TODO: Fix PendingTasks API usage
3365
+ // Traditional @Input() decorators
3366
+ label = 'Choose file';
3367
+ accept = '';
3368
+ multiple = false;
3369
+ disabled = false;
3370
+ required = false;
3371
+ helperText = '';
3372
+ errorText = '';
3373
+ showPreview = false;
3374
+ previewWidth = '200px';
3375
+ previewHeight = '200px';
3376
+ previewBoxMode = false;
3377
+ showFileName = true;
3378
+ placeholderText = 'Click to select image';
3379
+ placeholderIcon = '📷';
3380
+ autoUpload = false;
3381
+ uploadData = {};
3382
+ showFloatingUploader = true;
3383
+ floatingUploaderGroupId;
3384
+ // Traditional @Output() decorators
3385
+ fileChange = new EventEmitter();
3386
+ uploadSuccess = new EventEmitter();
3387
+ uploadError = new EventEmitter();
3388
+ uploadProgressChange = new EventEmitter();
3389
+ // Readable signals created from @Input() decorator values
3390
+ labelSignal = signal(this.label, ...(ngDevMode ? [{ debugName: "labelSignal" }] : []));
3391
+ acceptSignal = signal(this.accept, ...(ngDevMode ? [{ debugName: "acceptSignal" }] : []));
3392
+ multipleSignal = signal(this.multiple, ...(ngDevMode ? [{ debugName: "multipleSignal" }] : []));
3393
+ disabledSignal = signal(this.disabled, ...(ngDevMode ? [{ debugName: "disabledSignal" }] : []));
3394
+ requiredSignal = signal(this.required, ...(ngDevMode ? [{ debugName: "requiredSignal" }] : []));
3395
+ helperTextSignal = signal(this.helperText, ...(ngDevMode ? [{ debugName: "helperTextSignal" }] : []));
3396
+ errorTextSignal = signal(this.errorText, ...(ngDevMode ? [{ debugName: "errorTextSignal" }] : []));
3397
+ showPreviewSignal = signal(this.showPreview, ...(ngDevMode ? [{ debugName: "showPreviewSignal" }] : []));
3398
+ previewWidthSignal = signal(this.previewWidth, ...(ngDevMode ? [{ debugName: "previewWidthSignal" }] : []));
3399
+ previewHeightSignal = signal(this.previewHeight, ...(ngDevMode ? [{ debugName: "previewHeightSignal" }] : []));
3400
+ previewBoxModeSignal = signal(this.previewBoxMode, ...(ngDevMode ? [{ debugName: "previewBoxModeSignal" }] : []));
3401
+ showFileNameSignal = signal(this.showFileName, ...(ngDevMode ? [{ debugName: "showFileNameSignal" }] : []));
3402
+ placeholderTextSignal = signal(this.placeholderText, ...(ngDevMode ? [{ debugName: "placeholderTextSignal" }] : []));
3403
+ placeholderIconSignal = signal(this.placeholderIcon, ...(ngDevMode ? [{ debugName: "placeholderIconSignal" }] : []));
3404
+ autoUploadSignal = signal(this.autoUpload, ...(ngDevMode ? [{ debugName: "autoUploadSignal" }] : []));
3405
+ uploadDataSignal = signal(this.uploadData, ...(ngDevMode ? [{ debugName: "uploadDataSignal" }] : []));
3406
+ showFloatingUploaderSignal = signal(this.showFloatingUploader, ...(ngDevMode ? [{ debugName: "showFloatingUploaderSignal" }] : []));
3407
+ floatingUploaderGroupIdSignal = signal(this.floatingUploaderGroupId, ...(ngDevMode ? [{ debugName: "floatingUploaderGroupIdSignal" }] : []));
3408
+ // Reactive state with signals
3409
+ id = signal(Math.random().toString(36).substring(2, 10), ...(ngDevMode ? [{ debugName: "id" }] : []));
3410
+ isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
3411
+ uploadProgress = signal(0, ...(ngDevMode ? [{ debugName: "uploadProgress" }] : []));
3412
+ uploadStatus = signal('idle', ...(ngDevMode ? [{ debugName: "uploadStatus" }] : []));
3413
+ files = signal(null, ...(ngDevMode ? [{ debugName: "files" }] : []));
3414
+ fileNames = signal([], ...(ngDevMode ? [{ debugName: "fileNames" }] : []));
3415
+ previewUrls = signal([], ...(ngDevMode ? [{ debugName: "previewUrls" }] : []));
3416
+ uploadNotificationId = signal(null, ...(ngDevMode ? [{ debugName: "uploadNotificationId" }] : []));
3388
3417
  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;
3418
+ groupId = signal(null, ...(ngDevMode ? [{ debugName: "groupId" }] : [])); // Group ID for multiple file uploads
3419
+ isMultipleUploadMode = signal(false, ...(ngDevMode ? [{ debugName: "isMultipleUploadMode" }] : [])); // Flag to track if we're in multiple upload mode
3420
+ hasEverUploaded = signal(false, ...(ngDevMode ? [{ debugName: "hasEverUploaded" }] : [])); // Track if this component has ever uploaded files
3421
+ // Computed signals for better relationships
3422
+ hasFiles = computed(() => this.files() !== null && this.files().length > 0, ...(ngDevMode ? [{ debugName: "hasFiles" }] : []));
3423
+ canUpload = computed(() => this.hasFiles() && !this.isUploading() && !this.disabledSignal(), ...(ngDevMode ? [{ debugName: "canUpload" }] : []));
3424
+ isInErrorState = computed(() => this.uploadStatus() === 'error', ...(ngDevMode ? [{ debugName: "isInErrorState" }] : []));
3425
+ isInSuccessState = computed(() => this.uploadStatus() === 'success', ...(ngDevMode ? [{ debugName: "isInSuccessState" }] : []));
3426
+ // Optimized computed values - only calculate when needed
3427
+ totalFileSize = computed(() => {
3428
+ const files = this.files();
3429
+ return files ? Array.from(files).reduce((total, file) => total + file.size, 0) : 0;
3430
+ }, ...(ngDevMode ? [{ debugName: "totalFileSize" }] : []));
3431
+ fileSizeInMB = computed(() => (this.totalFileSize() / 1048576).toFixed(2), ...(ngDevMode ? [{ debugName: "fileSizeInMB" }] : [])); // 1024^2 = 1048576
3432
+ // ControlValueAccessor callbacks
3433
+ onChange = (value) => { };
3434
+ onTouched = () => { };
3435
+ onValidatorChange = () => { };
3436
+ // Computed values
3437
+ hasImages = computed(() => this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "hasImages" }] : []));
3438
+ isPreviewBoxMode = computed(() => this.previewBoxModeSignal() && this.showPreviewSignal(), ...(ngDevMode ? [{ debugName: "isPreviewBoxMode" }] : []));
3439
+ isImagePreviewAvailable = computed(() => this.showPreviewSignal() && this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "isImagePreviewAvailable" }] : []));
3394
3440
  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();
3441
+ // Minimal DOM operations - only when necessary
3442
+ }
3443
+ ngOnInit() {
3444
+ // Update signals with initial @Input() values
3445
+ this.labelSignal.set(this.label);
3446
+ this.acceptSignal.set(this.accept);
3447
+ this.multipleSignal.set(this.multiple);
3448
+ this.disabledSignal.set(this.disabled);
3449
+ this.requiredSignal.set(this.required);
3450
+ this.helperTextSignal.set(this.helperText);
3451
+ this.errorTextSignal.set(this.errorText);
3452
+ this.showPreviewSignal.set(this.showPreview);
3453
+ this.previewWidthSignal.set(this.previewWidth);
3454
+ this.previewHeightSignal.set(this.previewHeight);
3455
+ this.previewBoxModeSignal.set(this.previewBoxMode);
3456
+ this.showFileNameSignal.set(this.showFileName);
3457
+ this.placeholderTextSignal.set(this.placeholderText);
3458
+ this.placeholderIconSignal.set(this.placeholderIcon);
3459
+ this.autoUploadSignal.set(this.autoUpload);
3460
+ this.uploadDataSignal.set(this.uploadData);
3461
+ }
3462
+ ngOnChanges(changes) {
3463
+ // Angular 20: Update signals when @Input() values change
3464
+ if (changes['label'])
3465
+ this.labelSignal.set(this.label);
3466
+ if (changes['accept'])
3467
+ this.acceptSignal.set(this.accept);
3468
+ if (changes['multiple'])
3469
+ this.multipleSignal.set(this.multiple);
3470
+ if (changes['disabled'])
3471
+ this.disabledSignal.set(this.disabled);
3472
+ if (changes['required'])
3473
+ this.requiredSignal.set(this.required);
3474
+ if (changes['helperText'])
3475
+ this.helperTextSignal.set(this.helperText);
3476
+ if (changes['errorText'])
3477
+ this.errorTextSignal.set(this.errorText);
3478
+ if (changes['showPreview'])
3479
+ this.showPreviewSignal.set(this.showPreview);
3480
+ if (changes['previewWidth'])
3481
+ this.previewWidthSignal.set(this.previewWidth);
3482
+ if (changes['previewHeight'])
3483
+ this.previewHeightSignal.set(this.previewHeight);
3484
+ if (changes['previewBoxMode'])
3485
+ this.previewBoxModeSignal.set(this.previewBoxMode);
3486
+ if (changes['showFileName'])
3487
+ this.showFileNameSignal.set(this.showFileName);
3488
+ if (changes['placeholderText'])
3489
+ this.placeholderTextSignal.set(this.placeholderText);
3490
+ if (changes['placeholderIcon'])
3491
+ this.placeholderIconSignal.set(this.placeholderIcon);
3492
+ if (changes['autoUpload'])
3493
+ this.autoUploadSignal.set(this.autoUpload);
3494
+ if (changes['uploadData'])
3495
+ this.uploadDataSignal.set(this.uploadData);
3496
+ }
3497
+ writeValue(value) {
3498
+ console.log('📝 [FileInput] writeValue called with:', value);
3499
+ if (typeof value === 'string') {
3500
+ // Check if this is a group ID for multiple files or single file ID
3501
+ if (this.isMultipleFileMode()) {
3502
+ // Multiple file mode - value is group ID
3503
+ console.log('📁 [FileInput] Value is group ID for multiple files:', value);
3504
+ this.groupId.set(value);
3505
+ this.loadFilesFromGroupId(value);
3415
3506
  }
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();
3507
+ else {
3508
+ // Single file mode - value is file ID
3509
+ console.log('📝 [FileInput] Value is single file ID:', value);
3510
+ this.files.set(null);
3511
+ this.fileNames.set([]);
3512
+ this.clearPreviews();
3513
+ // Fetch file details to get base64 and set preview
3514
+ this.loadFileDetailsFromId(value);
3424
3515
  }
3425
- else if (shouldShow && hasFilesToShow && !this.isVisible()) {
3426
- console.log('👁️ [FloatingFileUploader] Showing floating uploader due to files available');
3427
- this.showWithAnimation();
3516
+ }
3517
+ else if (value instanceof FileList) {
3518
+ // Value is a FileList
3519
+ console.log('📝 [FileInput] Value is FileList:', Array.from(value).map(f => f.name));
3520
+ this.files.set(value);
3521
+ this.fileNames.set(Array.from(value).map(f => f.name));
3522
+ this.generatePreviews();
3523
+ // For multiple files, use group ID API to fetch files
3524
+ if (value.length > 1) {
3525
+ const groupId = this.groupId();
3526
+ if (groupId) {
3527
+ console.log('📁 [FileInput] Multiple files detected, fetching files for group:', groupId);
3528
+ this.loadFilesFromGroupId(groupId);
3529
+ }
3428
3530
  }
3429
- });
3531
+ }
3532
+ else {
3533
+ // Value is null
3534
+ console.log('📝 [FileInput] Value is null');
3535
+ this.files.set(null);
3536
+ this.fileNames.set([]);
3537
+ this.clearPreviews();
3538
+ }
3430
3539
  }
3431
- 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();
3540
+ registerOnChange(fn) {
3541
+ this.onChange = fn;
3438
3542
  }
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
- }
3543
+ registerOnTouched(fn) {
3544
+ this.onTouched = fn;
3447
3545
  }
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));
3546
+ registerOnValidatorChange(fn) {
3547
+ this.onValidatorChange = fn;
3470
3548
  }
3471
- /**
3472
- * Remove file input listeners
3473
- */
3474
- removeFileInputListeners() {
3475
- document.removeEventListener('change', this.handleFileInputChange.bind(this));
3549
+ setDisabledState(isDisabled) {
3550
+ // Note: With input signals, disabled state is controlled by the parent component
3551
+ // This method is kept for ControlValueAccessor compatibility but doesn't modify the signal
3552
+ console.log('🔧 [FileInput] setDisabledState called with:', isDisabled, '(controlled by parent component)');
3476
3553
  }
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);
3554
+ onFileSelected(event) {
3555
+ console.log('🔍 [FileInput] onFileSelected called');
3556
+ const input = event.target;
3557
+ const selectedFiles = input.files;
3558
+ this.files.set(selectedFiles);
3559
+ this.fileNames.set(selectedFiles ? Array.from(selectedFiles).map(f => f.name) : []);
3560
+ console.log('📁 [FileInput] Files selected:', this.fileNames());
3561
+ this.generatePreviews();
3562
+ // Reset upload status when new file is selected
3563
+ this.uploadStatus.set('idle');
3564
+ console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
3565
+ this.onChange(selectedFiles);
3566
+ this.fileChange.emit(selectedFiles);
3567
+ this.onTouched();
3568
+ // Note: Floating uploader is now triggered via service in upload methods
3569
+ // Auto upload if enabled
3570
+ if (this.autoUploadSignal() && selectedFiles && selectedFiles.length > 0) {
3571
+ if (this.multipleSignal()) {
3572
+ console.log('🚀 [FileInput] Auto upload enabled for multiple files mode:', selectedFiles.length, 'files');
3573
+ this.uploadMultipleFiles(Array.from(selectedFiles));
3574
+ }
3575
+ else {
3576
+ console.log('🚀 [FileInput] Auto upload enabled for single file mode:', selectedFiles[0].name);
3577
+ this.uploadFile(selectedFiles[0]);
3494
3578
  }
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
- }
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();
3521
3579
  }
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));
3580
+ else {
3581
+ console.log('⏸️ [FileInput] Auto upload disabled or no files');
3532
3582
  }
3533
3583
  }
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());
3584
+ clearFiles() {
3585
+ console.log('🗑️ [FileInput] clearFiles called');
3586
+ this.files.set(null);
3587
+ this.fileNames.set([]);
3588
+ this.clearPreviews();
3589
+ this.uploadStatus.set('idle');
3590
+ console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
3591
+ this.onChange(null);
3592
+ this.fileChange.emit(null);
3541
3593
  }
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
3594
+ uploadFile(file) {
3595
+ console.log('📤 [FileInput] uploadFile called for:', file.name, 'Size:', file.size, 'bytes');
3596
+ // Angular 20: Use PendingTasks for better loading state management
3597
+ // const uploadTask = this.pendingTasks.add(); // TODO: Fix PendingTasks API usage
3598
+ // console.log('⏳ [FileInput] Pending task added for upload tracking');
3599
+ // Set upload status to 'start' before starting upload
3600
+ this.uploadStatus.set('start');
3601
+ console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3602
+ this.isUploading.set(true);
3603
+ this.uploadProgress.set(0);
3604
+ this.uploadProgressChange.emit(0);
3605
+ console.log('📊 [FileInput] Upload progress initialized to 0%');
3606
+ // Make form control invalid during upload - this prevents form submission
3607
+ this.onChange(null);
3608
+ console.log('🚫 [FileInput] Form control value set to null to prevent submission');
3609
+ // Show initial progress notification with spinner (persistent - no auto-dismiss)
3610
+ const notificationId = this.notificationService.showProgress('🔄 Preparing file upload...', 0, { duration: 0 });
3611
+ this.uploadNotificationId.set(notificationId);
3612
+ console.log('🔔 [FileInput] Progress notification started with ID:', notificationId);
3613
+ this.fileManagerService.uploadFile(file, this.uploadDataSignal(), (progress) => {
3614
+ // Real progress callback from file manager service
3615
+ this.uploadProgress.set(progress);
3616
+ this.uploadProgressChange.emit(progress);
3617
+ console.log('📈 [FileInput] Upload progress:', Math.round(progress) + '%');
3618
+ // Set upload status to 'uploading' when progress starts
3619
+ if (this.uploadStatus() === 'start') {
3620
+ this.uploadStatus.set('uploading');
3621
+ console.log('🔄 [FileInput] Upload status changed to:', this.uploadStatus());
3622
+ }
3623
+ // Update progress notification with spinner
3624
+ const notificationId = this.uploadNotificationId();
3625
+ if (notificationId) {
3626
+ let progressMessage = '';
3627
+ if (progress < 10) {
3628
+ progressMessage = '🔄 Starting upload...';
3629
+ }
3630
+ else if (progress < 25) {
3631
+ progressMessage = '🔄 Uploading file...';
3632
+ }
3633
+ else if (progress < 50) {
3634
+ progressMessage = '🔄 Upload in progress...';
3635
+ }
3636
+ else if (progress < 75) {
3637
+ progressMessage = '🔄 Almost done...';
3638
+ }
3639
+ else if (progress < 95) {
3640
+ progressMessage = '🔄 Finishing upload...';
3641
+ }
3642
+ else {
3643
+ progressMessage = '🔄 Finalizing...';
3644
+ }
3645
+ this.notificationService.updateProgress(notificationId, progress, progressMessage);
3646
+ }
3647
+ }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3648
+ next: (response) => {
3649
+ console.log('🎉 [FileInput] Upload SUCCESS - Response received:', response);
3650
+ // Angular 20: Complete the pending task
3651
+ // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
3652
+ // console.log('✅ [FileInput] Pending task completed for successful upload');
3653
+ // Set upload status to 'success'
3654
+ this.uploadStatus.set('success');
3655
+ console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3656
+ // Complete the progress
3657
+ this.uploadProgress.set(100);
3658
+ this.uploadProgressChange.emit(100);
3659
+ console.log('📊 [FileInput] Upload progress completed: 100%');
3660
+ // Update progress notification to complete
3661
+ const notificationId = this.uploadNotificationId();
3662
+ if (notificationId) {
3663
+ this.notificationService.remove(notificationId);
3664
+ console.log('🔔 [FileInput] Progress notification removed');
3665
+ }
3666
+ // Success notification removed for cleaner UX
3667
+ this.uploadNotificationId.set(null);
3668
+ // Extract ID from CoreFileManagerInsertUpdateResponse
3669
+ const uploadedId = response?.data?.core_file_manager?.[0]?.cyfm_id;
3670
+ if (uploadedId) {
3671
+ console.log('✅ [FileInput] File uploaded successfully with ID:', uploadedId);
3672
+ // Set the uploaded ID as the form control value
3673
+ this.onChange(uploadedId);
3674
+ console.log('📝 [FileInput] Form control value set to uploaded ID:', uploadedId);
3675
+ // Only emit individual uploadSuccess if not in multiple upload mode
3676
+ if (!this.isMultipleUploadMode()) {
3677
+ this.uploadSuccess.emit(uploadedId);
3678
+ console.log('📝 [FileInput] Upload success event emitted with file ID:', uploadedId);
3679
+ }
3680
+ else {
3681
+ console.log('📝 [FileInput] Individual upload success suppressed (multiple upload mode) - file ID:', uploadedId);
3682
+ }
3683
+ }
3684
+ else {
3685
+ console.error('❌ [FileInput] Upload successful but no ID returned:', response);
3686
+ this.uploadError.emit('Upload successful but no ID returned');
3687
+ }
3688
+ this.isUploading.set(false);
3689
+ console.log('🔄 [FileInput] isUploading set to false');
3690
+ },
3691
+ error: (error) => {
3692
+ console.error('💥 [FileInput] Upload FAILED:', error);
3693
+ // Angular 20: Complete the pending task even on error
3694
+ // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
3695
+ // console.log('❌ [FileInput] Pending task completed for failed upload');
3696
+ // Set upload status to 'error' and remove upload validation error
3697
+ this.uploadStatus.set('error');
3698
+ console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3699
+ // Remove progress notification and show error
3700
+ const notificationId = this.uploadNotificationId();
3701
+ if (notificationId) {
3702
+ this.notificationService.remove(notificationId);
3703
+ console.log('🔔 [FileInput] Progress notification removed due to error');
3704
+ }
3705
+ this.notificationService.error(`❌ File upload failed: ${error.message || error.error?.message || 'Unknown error occurred'}`, { duration: 0 });
3706
+ this.uploadNotificationId.set(null);
3707
+ this.uploadError.emit(error.message || error.error?.message || 'Upload failed');
3708
+ this.isUploading.set(false);
3709
+ this.uploadProgress.set(0);
3710
+ this.uploadProgressChange.emit(0);
3711
+ console.log('🔄 [FileInput] Upload state reset - isUploading: false, progress: 0%');
3712
+ }
3713
+ });
3548
3714
  }
3549
3715
  /**
3550
- * Show with animation
3716
+ * Upload multiple files with group ID support
3717
+ * FLOW: 1) Generate group ID first, 2) Upload all files with same group ID, 3) Emit group ID on completion
3551
3718
  */
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());
3578
- }
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();
3586
- }
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';
3608
- }
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);
3618
- }
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';
3629
- }
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';
3719
+ uploadMultipleFiles(files) {
3720
+ console.log('📤 [FileInput] uploadMultipleFiles called for:', files.length, 'files');
3721
+ console.log('🔄 [FileInput] STEP 1: Generate group ID before starting any file uploads');
3722
+ // Set multiple upload mode flag
3723
+ this.isMultipleUploadMode.set(true);
3724
+ // Set upload status to 'start' before starting upload
3725
+ this.uploadStatus.set('start');
3726
+ this.isUploading.set(true);
3727
+ this.uploadProgress.set(0);
3728
+ this.uploadProgressChange.emit(0);
3729
+ // Make form control invalid during upload
3730
+ this.onChange(null);
3731
+ // Show initial progress notification
3732
+ const notificationId = this.notificationService.showProgress('🔄 Preparing multiple file upload...', 0, { duration: 0 });
3733
+ this.uploadNotificationId.set(notificationId);
3734
+ // STEP 1: Generate or get group ID BEFORE starting any file uploads
3735
+ const existingGroupId = this.uploadDataSignal().groupId;
3736
+ if (existingGroupId) {
3737
+ console.log('🆔 [FileInput] STEP 1 COMPLETE: Using existing group ID:', existingGroupId);
3738
+ console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', existingGroupId);
3739
+ this.groupId.set(existingGroupId);
3740
+ this.startMulti(files, existingGroupId);
3641
3741
  }
3642
- }
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('_');
3742
+ else {
3743
+ console.log('🆔 [FileInput] No existing group ID, generating new one...');
3744
+ // Generate group ID BEFORE starting any file uploads
3745
+ this.fileManagerService.generateObjectId().subscribe({
3746
+ next: (response) => {
3747
+ const newGroupId = response.data?.objectId;
3748
+ console.log('🆔 [FileInput] STEP 1 COMPLETE: Generated new group ID:', newGroupId);
3749
+ console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', newGroupId);
3750
+ this.groupId.set(newGroupId);
3751
+ this.startMulti(files, newGroupId);
3752
+ },
3753
+ error: (error) => {
3754
+ console.error('❌ [FileInput] Failed to generate group ID:', error);
3755
+ this.uploadError.emit('Failed to generate group ID');
3756
+ this.isUploading.set(false);
3757
+ this.uploadStatus.set('error');
3758
+ const notificationId = this.uploadNotificationId();
3759
+ if (notificationId) {
3760
+ this.notificationService.remove(notificationId);
3761
+ }
3762
+ this.notificationService.error('❌ Failed to generate group ID for multiple file upload', { duration: 0 });
3763
+ this.uploadNotificationId.set(null);
3764
+ }
3765
+ });
3659
3766
  }
3660
- return fileId;
3661
- }
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();
3668
- }
3669
- /**
3670
- * Set current user ID
3671
- */
3672
- setCurrentUserId(userId) {
3673
- this.currentUserId.set(userId);
3674
- this.fileManagerService.setUserId(userId);
3675
3767
  }
3676
3768
  /**
3677
- * Public method to handle files from external sources
3678
- * This can be called by other components to trigger the floating uploader
3769
+ * Start uploading multiple files with the provided group ID
3770
+ * All files will be uploaded with the SAME group ID that was generated before this method
3679
3771
  */
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
3772
+ startMulti(files, groupId) {
3773
+ console.log('🚀 [FileInput] STEP 2: Starting upload for', files.length, 'files with group ID:', groupId);
3774
+ console.log('📋 [FileInput] All files will use the same group ID:', groupId);
3775
+ // Mark that this component has ever uploaded files
3776
+ this.hasEverUploaded.set(true);
3777
+ let completedUploads = 0;
3778
+ let failedUploads = 0;
3779
+ const totalFiles = files.length;
3780
+ // IMPORTANT: All files use the SAME group ID that was generated before starting uploads
3781
+ const uploadDataWithGroupId = {
3782
+ ...this.uploadDataSignal(),
3783
+ groupId: groupId,
3784
+ isMultiple: true
3785
+ };
3692
3786
  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({
3787
+ const componentId = this.id();
3788
+ console.log(`📤 [FileInput-${componentId}] Uploading file ${index + 1}/${totalFiles}: "${file.name}" with group ID: ${groupId}`);
3789
+ console.log(`📤 [FileInput-${componentId}] Upload data:`, uploadDataWithGroupId);
3790
+ this.fileManagerService.uploadFile(file, uploadDataWithGroupId, (progress) => {
3791
+ // Calculate overall progress
3792
+ const fileProgress = progress / totalFiles;
3793
+ const overallProgress = ((completedUploads * 100) + fileProgress) / totalFiles;
3794
+ this.uploadProgress.set(overallProgress);
3795
+ this.uploadProgressChange.emit(overallProgress);
3796
+ // Update progress notification
3797
+ const notificationId = this.uploadNotificationId();
3798
+ if (notificationId) {
3799
+ const progressMessage = `🔄 Uploading file ${index + 1} of ${totalFiles}...`;
3800
+ this.notificationService.updateProgress(notificationId, overallProgress, progressMessage);
3801
+ }
3802
+ }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3702
3803
  next: (response) => {
3703
- console.log('✅ [FloatingFileUploader] Upload completed:', response);
3804
+ completedUploads++;
3805
+ console.log(`✅ [FileInput] File ${index + 1}/${totalFiles} uploaded`);
3806
+ // Check if all files are completed
3807
+ if (completedUploads + failedUploads === totalFiles) {
3808
+ this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
3809
+ }
3704
3810
  },
3705
3811
  error: (error) => {
3706
- console.error('❌ [FloatingFileUploader] Upload failed:', error);
3812
+ failedUploads++;
3813
+ console.error(`❌ [FileInput] File ${index + 1}/${totalFiles} upload failed:`, error);
3814
+ // Check if all files are completed
3815
+ if (completedUploads + failedUploads === totalFiles) {
3816
+ this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
3817
+ }
3707
3818
  }
3708
3819
  });
3709
3820
  });
3710
3821
  }
3711
3822
  /**
3712
- * Manually show the floating uploader
3713
- * This should always be called with a group ID from the file input component
3823
+ * Handle completion of multiple file upload
3714
3824
  */
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;
3825
+ handleMultipleUploadComplete(completed, failed, total, groupId) {
3826
+ console.log(`📊 [FileInput] Multiple upload complete: ${completed}/${total} successful, ${failed} failed`);
3827
+ this.isUploading.set(false);
3828
+ this.uploadProgress.set(100);
3829
+ this.uploadProgressChange.emit(100);
3830
+ // Remove progress notification
3831
+ const notificationId = this.uploadNotificationId();
3832
+ if (notificationId) {
3833
+ this.notificationService.remove(notificationId);
3720
3834
  }
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
3731
- this.fileManagerService.fetchAndStoreFilesByGroupId(groupId)
3732
- .pipe(takeUntilDestroyed(this.destroyRef))
3733
- .subscribe({
3734
- 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);
3747
- },
3835
+ this.uploadNotificationId.set(null);
3836
+ if (failed === 0) {
3837
+ // All files uploaded successfully
3838
+ this.uploadStatus.set('success');
3839
+ // Success notification removed for cleaner UX
3840
+ // STEP 3: For multiple file upload, emit the group ID (not individual file IDs)
3841
+ this.onChange(groupId);
3842
+ this.uploadSuccess.emit(groupId);
3843
+ console.log('📝 [FileInput] Multiple upload completed with group ID:', groupId);
3844
+ }
3845
+ else if (completed > 0) {
3846
+ // Some files uploaded successfully
3847
+ this.uploadStatus.set('error');
3848
+ this.notificationService.warning(`⚠️ ${completed}/${total} files uploaded. ${failed} failed.`, { duration: 0 });
3849
+ this.uploadError.emit(`${failed} out of ${total} files failed to upload`);
3850
+ }
3851
+ else {
3852
+ // All files failed
3853
+ this.uploadStatus.set('error');
3854
+ this.notificationService.error(`❌ All ${total} files failed to upload.`, { duration: 0 });
3855
+ this.uploadError.emit('All files failed to upload');
3856
+ }
3857
+ // Reset multiple upload mode flag
3858
+ this.isMultipleUploadMode.set(false);
3859
+ }
3860
+ generatePreviews() {
3861
+ // Clear existing previews
3862
+ this.clearPreviews();
3863
+ if (!this.showPreviewSignal() || !this.files()) {
3864
+ return;
3865
+ }
3866
+ Array.from(this.files()).forEach(file => {
3867
+ if (this.isImageFile(file)) {
3868
+ const reader = new FileReader();
3869
+ reader.onload = (e) => {
3870
+ if (e.target?.result) {
3871
+ this.previewUrls.update(urls => [...urls, e.target.result]);
3872
+ }
3873
+ };
3874
+ reader.readAsDataURL(file);
3875
+ }
3876
+ });
3877
+ }
3878
+ clearPreviews() {
3879
+ // Revoke object URLs to prevent memory leaks
3880
+ this.previewUrls().forEach(url => {
3881
+ if (url.startsWith('blob:')) {
3882
+ URL.revokeObjectURL(url);
3883
+ }
3884
+ });
3885
+ this.previewUrls.set([]);
3886
+ }
3887
+ isImageFile(file) {
3888
+ return file.type.startsWith('image/');
3889
+ }
3890
+ loadFileDetailsFromId(fileId) {
3891
+ console.log('🔍 [FileInput] Loading file details for ID:', fileId);
3892
+ if (!fileId)
3893
+ return;
3894
+ this.fileManagerService?.getFileDetails({ cyfm_id: fileId })?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3895
+ next: (fileDetails) => {
3896
+ console.log('📋 [FileInput] File details received:', fileDetails);
3897
+ if (fileDetails?.data?.length) {
3898
+ const fileData = fileDetails.data[0];
3899
+ console.log('📁 [FileInput] File data:', fileData);
3900
+ // Set file name from the details
3901
+ if (fileData.cyfm_name) {
3902
+ this.fileNames.set([fileData.cyfm_name]);
3903
+ console.log('📝 [FileInput] File name set:', fileData.cyfm_name);
3904
+ }
3905
+ // If it's an image and we have base64 data, set preview
3906
+ if (this.showPreviewSignal() && fileData.cyfm_file_base64) {
3907
+ // Check if it's an image file based on file name or type
3908
+ const isImage = this.isImageFileFromName(fileData.cyfm_name || '') ||
3909
+ this.isImageFileFromType(fileData.cyfm_type || '');
3910
+ if (isImage) {
3911
+ // Add data URL prefix if not already present
3912
+ let base64Data = fileData.cyfm_file_base64;
3913
+ if (!base64Data.startsWith('data:')) {
3914
+ const mimeType = fileData.cyfm_type || 'image/jpeg';
3915
+ base64Data = `data:${mimeType};base64,${base64Data}`;
3916
+ }
3917
+ this.previewUrls.set([base64Data]);
3918
+ console.log('🖼️ [FileInput] Preview set from base64 data');
3919
+ }
3920
+ }
3921
+ }
3922
+ else {
3923
+ console.warn('⚠️ [FileInput] No file data found for ID:', fileId);
3924
+ }
3925
+ },
3748
3926
  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);
3927
+ console.error('❌ [FileInput] Error loading file details:', error);
3928
+ this.notificationService.error(`Failed to load file details: ${error.message || 'Unknown error'}`, { duration: 0 });
3761
3929
  }
3762
3930
  });
3763
3931
  }
3764
3932
  /**
3765
- * Check if there are any uploads for the current group
3933
+ * Check if the component is in multiple file mode
3766
3934
  */
3767
- hasUploadsForCurrentGroup() {
3768
- const groupId = this.currentGroupId();
3769
- if (!groupId) {
3770
- // If no group filter, show all uploads
3771
- return this.hasUploads();
3772
- }
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();
3935
+ isMultipleFileMode() {
3936
+ // Check if multiple attribute is set or if we have a group ID
3937
+ return this.multiple || this.groupId() !== null;
3776
3938
  }
3777
3939
  /**
3778
- * Handle drag over event for file drop
3940
+ * Load files from group ID using the group API
3779
3941
  */
3780
- onDragOver(event) {
3781
- event.preventDefault();
3782
- event.stopPropagation();
3783
- this.isDragOver.set(true);
3942
+ loadFilesFromGroupId(groupId) {
3943
+ console.log('🔍 [FileInput] Loading files for group ID:', groupId);
3944
+ if (!groupId)
3945
+ return;
3946
+ this.fileManagerService.fetchAndStoreFilesByGroupId(groupId)
3947
+ .pipe(takeUntilDestroyed(this.destroyRef))
3948
+ .subscribe({
3949
+ next: (files) => {
3950
+ console.log('📋 [FileInput] Files loaded for group:', files.length);
3951
+ // Set file names to show count in input
3952
+ if (files && files.length > 0) {
3953
+ const fileNames = files.map(file => file.file_name || file.name || 'Unknown file');
3954
+ this.fileNames.set(fileNames);
3955
+ console.log('📝 [FileInput] File names set for display:', fileNames);
3956
+ }
3957
+ else {
3958
+ this.fileNames.set([]);
3959
+ }
3960
+ // Files are now stored in service state and will be displayed by floating uploader
3961
+ },
3962
+ error: (error) => {
3963
+ console.error('❌ [FileInput] Failed to load files for group:', error);
3964
+ this.fileNames.set([]);
3965
+ }
3966
+ });
3784
3967
  }
3785
- /**
3786
- * Handle drag leave event for file drop
3787
- */
3788
- onDragLeave(event) {
3789
- event.preventDefault();
3790
- event.stopPropagation();
3791
- this.isDragOver.set(false);
3968
+ isImageFileFromName(fileName) {
3969
+ if (!fileName)
3970
+ return false;
3971
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
3972
+ const lowerFileName = fileName.toLowerCase();
3973
+ return imageExtensions.some(ext => lowerFileName.endsWith(ext));
3792
3974
  }
3793
- /**
3794
- * Handle drop event for file drop
3795
- */
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));
3975
+ isImageFileFromType(fileType) {
3976
+ if (!fileType)
3977
+ return false;
3978
+ return fileType.startsWith('image/');
3979
+ }
3980
+ removePreview(index) {
3981
+ const currentFiles = this.files();
3982
+ const currentUrls = this.previewUrls();
3983
+ if (currentFiles && currentFiles.length > index) {
3984
+ // Handle FileList case - remove file from FileList
3985
+ const dt = new DataTransfer();
3986
+ Array.from(currentFiles).forEach((file, i) => {
3987
+ if (i !== index) {
3988
+ dt.items.add(file);
3989
+ }
3990
+ });
3991
+ const newFiles = dt.files;
3992
+ this.files.set(newFiles);
3993
+ this.fileNames.set(Array.from(newFiles).map(f => f.name));
3994
+ // Remove the preview URL
3995
+ if (currentUrls[index] && currentUrls[index].startsWith('blob:')) {
3996
+ URL.revokeObjectURL(currentUrls[index]);
3997
+ }
3998
+ this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
3999
+ this.onChange(newFiles);
4000
+ this.fileChange.emit(newFiles);
4001
+ }
4002
+ else if (currentUrls.length > index) {
4003
+ // Handle uploaded file ID case - clear the preview and set control value to null
4004
+ console.log('🗑️ [FileInput] Removing preview for uploaded file ID');
4005
+ // Clear preview
4006
+ this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
4007
+ this.fileNames.set([]);
4008
+ // Set control value to null since we're removing the uploaded file
4009
+ this.onChange(null);
4010
+ this.fileChange.emit(null);
3803
4011
  }
3804
4012
  }
3805
- /**
3806
- * Trigger file input click
3807
- */
3808
- triggerFileInput() {
3809
- const fileInput = document.querySelector('input[type="file"]');
3810
- if (fileInput) {
4013
+ ngOnDestroy() {
4014
+ // Clean up preview URLs to prevent memory leaks
4015
+ this.clearPreviews();
4016
+ // Clean up any active upload notification
4017
+ const notificationId = this.uploadNotificationId();
4018
+ if (notificationId) {
4019
+ this.notificationService.remove(notificationId);
4020
+ this.uploadNotificationId.set(null);
4021
+ }
4022
+ }
4023
+ triggerFileSelect() {
4024
+ const fileInput = document.getElementById('cide-file-input-' + this.id());
4025
+ if (fileInput && !this.disabledSignal()) {
3811
4026
  fileInput.click();
3812
4027
  }
3813
4028
  }
3814
4029
  /**
3815
- * Handle file input change
4030
+ * Show floating uploader manually
4031
+ * This can be called to show the floating uploader even when no files are selected
3816
4032
  */
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
4033
+ showUploader() {
4034
+ console.log('👁️ [FileInput] Manually showing floating uploader');
4035
+ if (!this.showFloatingUploaderSignal()) {
4036
+ console.log('⚠️ [FileInput] Floating uploader is disabled');
4037
+ return;
4038
+ }
4039
+ const groupId = this.groupId();
4040
+ if (groupId) {
4041
+ console.log("groupId groupId", groupId);
4042
+ // Fetch files for the group and trigger floating uploader to show
4043
+ this.fileManagerService.fetchAndStoreFilesByGroupId(groupId)
4044
+ .pipe(takeUntilDestroyed(this.destroyRef))
4045
+ .subscribe({
4046
+ next: (files) => {
4047
+ console.log('✅ [FileInput] Files fetched for floating uploader: groupId', files.length);
4048
+ // Trigger the floating uploader to show via service with group ID
4049
+ this.fileManagerService.triggerFloatingUploaderShow(groupId);
4050
+ },
4051
+ error: (error) => {
4052
+ console.error('❌ [FileInput] Failed to fetch files for floating uploader:', error);
4053
+ // Still trigger show even if fetch fails, with group ID
4054
+ this.fileManagerService.triggerFloatingUploaderShow(groupId);
4055
+ }
4056
+ });
4057
+ }
4058
+ else {
4059
+ // No group ID, just trigger show
4060
+ this.fileManagerService.triggerFloatingUploaderShow();
3822
4061
  }
3823
4062
  }
3824
4063
  /**
3825
- * Handle file selection from drag/drop or file input
4064
+ * Get total upload count from file manager service for this component's group ID
4065
+ * Uses optimized service method for better performance
3826
4066
  */
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
4067
+ getUploadCount() {
4068
+ const groupId = this.groupId();
4069
+ if (!groupId)
4070
+ return this.fileManagerService.activeUploads().size;
4071
+ return this.fileManagerService.getFileCountForGroup(groupId);
4072
+ }
4073
+ /**
4074
+ * Check if there are active uploads for this component's group ID
4075
+ * Uses optimized service method for better performance
4076
+ */
4077
+ hasActiveUploads() {
4078
+ const groupId = this.groupId();
3831
4079
  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;
4080
+ return Array.from(this.fileManagerService.activeUploads().values()).some(upload => upload.stage !== 'complete');
3834
4081
  }
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
- });
4082
+ return this.fileManagerService.hasActiveUploadsForGroup(groupId);
3845
4083
  }
3846
4084
  /**
3847
- * Update cached dimensions (throttled for performance)
4085
+ * Get count of active (non-completed) uploads for this component's group ID
3848
4086
  */
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;
3854
- }
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;
4087
+ getActiveUploadCount() {
4088
+ const groupId = this.groupId();
4089
+ if (!groupId) {
4090
+ return Array.from(this.fileManagerService.activeUploads().values())
4091
+ .filter(upload => upload.stage !== 'complete').length;
3862
4092
  }
4093
+ return this.fileManagerService.getAllFilesForGroup(groupId)
4094
+ .filter(file => file.stage !== 'complete').length;
3863
4095
  }
3864
4096
  /**
3865
- * Start dragging the uploader
4097
+ * Show floating uploader (alias for showUploader for template)
3866
4098
  */
3867
- startDrag(event) {
3868
- 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';
4099
+ showFloatingUploaderDialog() {
4100
+ this.showUploader();
3888
4101
  }
3889
4102
  /**
3890
- * Handle dragging movement
4103
+ * Get dynamic classes for drag and drop zone
3891
4104
  */
3892
- onDrag(event) {
3893
- if (!this.isDragging())
3894
- return;
3895
- 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 });
4105
+ getDragDropZoneClasses() {
4106
+ const classes = [];
4107
+ if (this.isDragOver()) {
4108
+ classes.push('!tw-border-blue-500', '!tw-bg-blue-100', 'dark:!tw-bg-blue-900/30', 'tw-scale-[1.01]');
4109
+ }
4110
+ if (this.disabledSignal()) {
4111
+ 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');
4112
+ }
4113
+ if (this.hasFiles()) {
4114
+ 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');
4115
+ }
4116
+ return classes.join(' ');
3910
4117
  }
3911
4118
  /**
3912
- * End dragging
4119
+ * Get dynamic classes for icon
3913
4120
  */
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 = '';
4121
+ getIconClasses() {
4122
+ const classes = ['tw-text-gray-500', 'dark:tw-text-gray-400'];
4123
+ if (this.isDragOver()) {
4124
+ classes.push('!tw-text-blue-500', 'dark:!tw-text-blue-400');
4125
+ }
4126
+ else if (this.hasFiles()) {
4127
+ classes.push('!tw-text-emerald-500', 'dark:!tw-text-emerald-400');
4128
+ }
4129
+ return classes.join(' ');
3923
4130
  }
3924
4131
  /**
3925
- * Set up window resize listener to keep uploader within bounds
4132
+ * Get dynamic classes for preview box
3926
4133
  */
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 });
4134
+ getPreviewBoxClasses() {
4135
+ const classes = [];
4136
+ if (this.isDragOver()) {
4137
+ classes.push('!tw-border-blue-500', '!tw-bg-blue-100', 'dark:!tw-bg-blue-900/30');
4138
+ }
4139
+ if (this.disabledSignal()) {
4140
+ 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');
4141
+ }
4142
+ if (this.hasImages()) {
4143
+ classes.push('!tw-border-emerald-500', '!tw-bg-emerald-50', 'dark:!tw-bg-emerald-900/20');
4144
+ }
4145
+ return classes.join(' ');
4146
+ }
4147
+ // Drag and Drop Event Handlers
4148
+ onDragOver(event) {
4149
+ event.preventDefault();
4150
+ event.stopPropagation();
4151
+ if (!this.disabledSignal()) {
4152
+ this.isDragOver.set(true);
4153
+ console.log('🔄 [FileInput] Drag over detected');
4154
+ }
4155
+ }
4156
+ onDragLeave(event) {
4157
+ event.preventDefault();
4158
+ event.stopPropagation();
4159
+ this.isDragOver.set(false);
4160
+ console.log('🔄 [FileInput] Drag leave detected');
4161
+ }
4162
+ onDragEnter(event) {
4163
+ event.preventDefault();
4164
+ event.stopPropagation();
4165
+ if (!this.disabledSignal()) {
4166
+ this.isDragOver.set(true);
4167
+ console.log('🔄 [FileInput] Drag enter detected');
4168
+ }
4169
+ }
4170
+ onDrop(event) {
4171
+ event.preventDefault();
4172
+ event.stopPropagation();
4173
+ this.isDragOver.set(false);
4174
+ if (this.disabledSignal()) {
4175
+ console.log('⏸️ [FileInput] Drop ignored - component is disabled');
4176
+ return;
4177
+ }
4178
+ const files = event.dataTransfer?.files;
4179
+ if (files && files.length > 0) {
4180
+ console.log('📁 [FileInput] Files dropped:', Array.from(files).map(f => f.name));
4181
+ // Validate file types if accept is specified
4182
+ if (this.acceptSignal() && !this.validateFileTypes(files)) {
4183
+ console.log('❌ [FileInput] Invalid file types dropped');
4184
+ this.notificationService.error('❌ Invalid file type. Please select files of the correct type.', { duration: 0 });
4185
+ return;
3944
4186
  }
3945
- };
3946
- window.addEventListener('resize', handleResize);
3947
- // Store reference for cleanup
3948
- this.windowResizeHandler = handleResize;
4187
+ // Handle single vs multiple files
4188
+ if (!this.multipleSignal() && files.length > 1) {
4189
+ console.log('⚠️ [FileInput] Multiple files dropped but multiple is disabled');
4190
+ this.notificationService.warning('⚠️ Only one file is allowed. Using the first file.', { duration: 0 });
4191
+ // Create a new FileList with only the first file
4192
+ const dt = new DataTransfer();
4193
+ dt.items.add(files[0]);
4194
+ this.handleFileSelection(dt.files);
4195
+ }
4196
+ else {
4197
+ this.handleFileSelection(files);
4198
+ }
4199
+ }
3949
4200
  }
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
4201
+ validateFileTypes(files) {
4202
+ const acceptTypes = this.acceptSignal().split(',').map(type => type.trim());
4203
+ if (acceptTypes.length === 0 || acceptTypes[0] === '')
4204
+ return true;
4205
+ return Array.from(files).every(file => {
4206
+ return acceptTypes.some(acceptType => {
4207
+ if (acceptType.startsWith('.')) {
4208
+ // Extension-based validation
4209
+ return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
4210
+ }
4211
+ else if (acceptType.includes('/')) {
4212
+ // MIME type validation
4213
+ return file.type === acceptType || file.type.startsWith(acceptType.replace('*', ''));
4214
+ }
4215
+ return false;
4216
+ });
3968
4217
  });
3969
- // Update dimensions after a short delay to get actual rendered size
3970
- setTimeout(() => {
3971
- this.updateCachedDimensions();
3972
- }, 100);
3973
4218
  }
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\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\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());
4219
+ handleFileSelection(files) {
4220
+ this.files.set(files);
4221
+ this.fileNames.set(Array.from(files).map(f => f.name));
4222
+ console.log('📁 [FileInput] Files selected via drag & drop:', this.fileNames());
4210
4223
  this.generatePreviews();
4211
4224
  // Reset upload status when new file is selected
4212
4225
  this.uploadStatus.set('idle');
4213
4226
  console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
4214
- this.onChange(selectedFiles);
4215
- this.fileChange.emit(selectedFiles);
4227
+ this.onChange(files);
4228
+ this.fileChange.emit(files);
4216
4229
  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
- }
4230
+ // Note: Floating uploader is now triggered via service in upload methods
4222
4231
  // Auto upload if enabled
4223
- if (this.autoUploadSignal() && selectedFiles && selectedFiles.length > 0) {
4232
+ if (this.autoUploadSignal() && files.length > 0) {
4224
4233
  if (this.multipleSignal()) {
4225
- console.log('🚀 [FileInput] Auto upload enabled for multiple files mode:', selectedFiles.length, 'files');
4226
- this.uploadMultipleFiles(Array.from(selectedFiles));
4234
+ console.log('🚀 [FileInput] Auto upload enabled for multiple files mode (drag & drop):', files.length, 'files');
4235
+ this.uploadMultipleFiles(Array.from(files));
4227
4236
  }
4228
4237
  else {
4229
- console.log('🚀 [FileInput] Auto upload enabled for single file mode:', selectedFiles[0].name);
4230
- this.uploadFile(selectedFiles[0]);
4238
+ console.log('🚀 [FileInput] Auto upload enabled for single file mode (drag & drop):', files[0].name);
4239
+ this.uploadFile(files[0]);
4231
4240
  }
4232
4241
  }
4233
4242
  else {
4234
4243
  console.log('⏸️ [FileInput] Auto upload disabled or no files');
4235
4244
  }
4236
4245
  }
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);
4246
+ isRequired() {
4247
+ return this.requiredSignal();
4246
4248
  }
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());
4249
+ /**
4250
+ * Angular 20: Utility method to get upload data with proper typing
4251
+ * @returns Properly typed upload data
4252
+ */
4253
+ getUploadData() {
4254
+ return this.uploadDataSignal();
4255
+ }
4256
+ /**
4257
+ * Angular 20: Utility method to update upload data with type safety
4258
+ * @param data Partial upload data to merge with existing data
4259
+ */
4260
+ updateUploadData(data) {
4261
+ const currentData = this.uploadDataSignal();
4262
+ const updatedData = { ...currentData, ...data };
4263
+ // Note: This would require the uploadData to be a writable signal
4264
+ // For now, this method serves as a type-safe way to work with upload data
4265
+ console.log('📝 [FileInput] Upload data updated:', updatedData);
4266
+ }
4267
+ getCurrentState() {
4268
+ return {
4269
+ id: this.id(),
4270
+ label: this.labelSignal(),
4271
+ required: this.requiredSignal(),
4272
+ disabled: this.disabledSignal(),
4273
+ accept: this.acceptSignal(),
4274
+ multiple: this.multipleSignal(),
4275
+ showPreview: this.showPreviewSignal(),
4276
+ autoUpload: this.autoUploadSignal(),
4277
+ uploadStatus: this.uploadStatus(),
4278
+ isUploading: this.isUploading(),
4279
+ uploadProgress: this.uploadProgress(),
4280
+ files: this.files() ? Array.from(this.files()).map(f => ({ name: f.name, size: f.size, type: f.type })) : null,
4281
+ fileNames: this.fileNames(),
4282
+ previewUrls: this.previewUrls().length,
4283
+ helperText: this.helperTextSignal(),
4284
+ errorText: this.errorTextSignal(),
4285
+ placeholderText: this.placeholderTextSignal(),
4286
+ placeholderIcon: this.placeholderIconSignal(),
4287
+ previewWidth: this.previewWidthSignal(),
4288
+ previewHeight: this.previewHeightSignal(),
4289
+ previewBoxMode: this.previewBoxModeSignal(),
4290
+ showFileName: this.showFileNameSignal(),
4291
+ uploadData: this.uploadDataSignal()
4292
+ };
4293
+ }
4294
+ async getControlData() {
4295
+ console.log('🔍 [FileInput] getControlData called');
4296
+ const cide_element_data = await this.elementService?.getElementData({ sype_key: this.id() });
4297
+ if (cide_element_data) {
4298
+ console.log('📋 [FileInput] Element data loaded:', cide_element_data);
4299
+ // Note: Since we're using input signals, we can't directly set their values
4300
+ // This method would need to be refactored to work with the new signal-based approach
4301
+ // For now, we'll log the data and trigger validation
4302
+ console.log('✅ [FileInput] Control data received from element service');
4303
+ console.log('⚠️ [FileInput] Note: Input signals cannot be modified after component initialization');
4304
+ // Trigger validation update
4305
+ this.onValidatorChange();
4306
+ }
4307
+ else {
4308
+ console.log('⚠️ [FileInput] No element data found for key:', this.id());
4309
+ }
4310
+ }
4311
+ // Validator implementation
4312
+ validate(control) {
4313
+ console.log('🔍 [FileInput] validate() called - uploadStatus:', this.uploadStatus(), 'required:', this.requiredSignal(), 'files:', !!this.files(), 'control.value:', control.value);
4314
+ // If upload is in progress (start or uploading status), return validation error
4315
+ if (this.uploadStatus() === 'start' || this.uploadStatus() === 'uploading') {
4316
+ console.log('⚠️ [FileInput] Validation ERROR: Upload in progress');
4317
+ return { 'uploadInProgress': { message: 'File upload in progress. Please wait...' } };
4318
+ }
4319
+ // If required and no file is selected and no control value (uploaded file ID), return validation error
4320
+ if (this.requiredSignal() && !this.files() && !control.value) {
4321
+ console.log('⚠️ [FileInput] Validation ERROR: File required');
4322
+ return { 'required': { message: 'Please select a file to upload.' } };
4323
+ }
4324
+ console.log('✅ [FileInput] Validation PASSED: No errors');
4325
+ return null; // No validation errors
4326
+ }
4327
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4328
+ 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: [
4329
+ {
4330
+ provide: NG_VALUE_ACCESSOR,
4331
+ useExisting: CideEleFileInputComponent,
4332
+ multi: true
4333
+ },
4334
+ {
4335
+ provide: NG_VALIDATORS,
4336
+ useExisting: CideEleFileInputComponent,
4337
+ multi: true
4275
4338
  }
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);
4339
+ ], 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"] }] });
4340
+ }
4341
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, decorators: [{
4342
+ type: Component,
4343
+ args: [{ selector: 'cide-ele-file-input', standalone: true, imports: [CommonModule, FormsModule, CideIconComponent], providers: [
4344
+ {
4345
+ provide: NG_VALUE_ACCESSOR,
4346
+ useExisting: CideEleFileInputComponent,
4347
+ multi: true
4348
+ },
4349
+ {
4350
+ provide: NG_VALIDATORS,
4351
+ useExisting: CideEleFileInputComponent,
4352
+ multi: true
4353
+ }
4354
+ ], 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> " }]
4355
+ }], ctorParameters: () => [], propDecorators: { label: [{
4356
+ type: Input
4357
+ }], accept: [{
4358
+ type: Input
4359
+ }], multiple: [{
4360
+ type: Input
4361
+ }], disabled: [{
4362
+ type: Input
4363
+ }], required: [{
4364
+ type: Input
4365
+ }], helperText: [{
4366
+ type: Input
4367
+ }], errorText: [{
4368
+ type: Input
4369
+ }], showPreview: [{
4370
+ type: Input
4371
+ }], previewWidth: [{
4372
+ type: Input
4373
+ }], previewHeight: [{
4374
+ type: Input
4375
+ }], previewBoxMode: [{
4376
+ type: Input
4377
+ }], showFileName: [{
4378
+ type: Input
4379
+ }], placeholderText: [{
4380
+ type: Input
4381
+ }], placeholderIcon: [{
4382
+ type: Input
4383
+ }], autoUpload: [{
4384
+ type: Input
4385
+ }], uploadData: [{
4386
+ type: Input
4387
+ }], showFloatingUploader: [{
4388
+ type: Input
4389
+ }], floatingUploaderGroupId: [{
4390
+ type: Input
4391
+ }], fileChange: [{
4392
+ type: Output
4393
+ }], uploadSuccess: [{
4394
+ type: Output
4395
+ }], uploadError: [{
4396
+ type: Output
4397
+ }], uploadProgressChange: [{
4398
+ type: Output
4399
+ }] } });
4400
+
4401
+ class CideEleFloatingFileUploaderComponent {
4402
+ destroyRef = inject(DestroyRef);
4403
+ fileManagerService = inject(CideEleFileManagerService);
4404
+ // Signals for reactive state
4405
+ isVisible = signal(false, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
4406
+ isMinimized = signal(false, ...(ngDevMode ? [{ debugName: "isMinimized" }] : []));
4407
+ currentUserId = signal('', ...(ngDevMode ? [{ debugName: "currentUserId" }] : []));
4408
+ currentGroupId = signal(null, ...(ngDevMode ? [{ debugName: "currentGroupId" }] : []));
4409
+ // Use file manager service as the single source of truth
4410
+ uploadQueue = computed(() => this.fileManagerService.uploadQueue(), ...(ngDevMode ? [{ debugName: "uploadQueue" }] : []));
4411
+ activeUploads = computed(() => this.fileManagerService.activeUploads(), ...(ngDevMode ? [{ debugName: "activeUploads" }] : []));
4412
+ // Computed values based on service state
4413
+ hasUploads = computed(() => this.uploadQueue().length > 0 || this.activeUploads().size > 0, ...(ngDevMode ? [{ debugName: "hasUploads" }] : []));
4414
+ hasActiveUploads = computed(() => this.uploadQueue().length > 0 || Array.from(this.activeUploads().values()).some(upload => upload.stage !== 'complete'), ...(ngDevMode ? [{ debugName: "hasActiveUploads" }] : []));
4415
+ pendingUploads = computed(() => this.uploadQueue().filter(fileId => !this.activeUploads().has(fileId)), ...(ngDevMode ? [{ debugName: "pendingUploads" }] : []));
4416
+ activeUploadsLocal = computed(() => Array.from(this.activeUploads().values()).filter(upload => upload.stage === 'reading' || upload.stage === 'uploading'), ...(ngDevMode ? [{ debugName: "activeUploadsLocal" }] : []));
4417
+ completedUploads = computed(() => Array.from(this.activeUploads().values()).filter(upload => upload.stage === 'complete'), ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
4418
+ failedUploads = computed(() => Array.from(this.activeUploads().values()).filter(upload => upload.stage === 'error'), ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
4419
+ // Get all files for the current group (computed property for reactivity)
4420
+ allFilesForGroup = computed(() => {
4421
+ const groupId = this.currentGroupId();
4422
+ if (!groupId)
4423
+ return [];
4424
+ return this.fileManagerService.getAllFilesForGroup(groupId);
4425
+ }, ...(ngDevMode ? [{ debugName: "allFilesForGroup" }] : []));
4426
+ // Check if there are any files to display (active uploads OR fetched files for current group)
4427
+ hasFilesToShow = computed(() => {
4428
+ return this.hasActiveUploads() || this.allFilesForGroup().length > 0;
4429
+ }, ...(ngDevMode ? [{ debugName: "hasFilesToShow" }] : []));
4430
+ // Animation states
4431
+ isAnimating = signal(false, ...(ngDevMode ? [{ debugName: "isAnimating" }] : []));
4432
+ // Drag functionality
4433
+ isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
4434
+ position = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "position" }] : []));
4435
+ dragOffset = { x: 0, y: 0 };
4436
+ // File drag and drop functionality
4437
+ isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
4438
+ // Window resize handler reference for cleanup
4439
+ windowResizeHandler;
4440
+ // Cached dimensions for performance
4441
+ cachedDimensions = { width: 320, height: 200 };
4442
+ lastDimensionUpdate = 0;
4443
+ constructor() {
4444
+ console.log('🚀 [FloatingFileUploader] Component initialized');
4445
+ // Initialize default position
4446
+ this.initializePosition();
4447
+ // Consolidated effect for all visibility logic - SINGLE SOURCE OF TRUTH
4448
+ effect(() => {
4449
+ const hasActiveUploads = this.hasActiveUploads();
4450
+ const shouldShow = this.fileManagerService.showFloatingUploader();
4451
+ const triggerGroupId = this.fileManagerService.getTriggerGroupId();
4452
+ const isCurrentlyVisible = this.isVisible();
4453
+ // Show due to active uploads
4454
+ if (hasActiveUploads && !isCurrentlyVisible) {
4455
+ this.showWithAnimation();
4456
+ return;
4299
4457
  }
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
- }
4458
+ // Show due to manual trigger
4459
+ if (shouldShow && !isCurrentlyVisible) {
4460
+ if (triggerGroupId) {
4461
+ this.currentGroupId.set(triggerGroupId);
4462
+ // Fetch files for this group
4463
+ this.fileManagerService.fetchAndStoreFilesByGroupId(triggerGroupId)
4464
+ .pipe(takeUntilDestroyed(this.destroyRef))
4465
+ .subscribe({
4466
+ next: () => this.showWithAnimation(),
4467
+ error: () => this.showWithAnimation() // Show anyway
4468
+ });
4336
4469
  }
4337
4470
  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');
4471
+ this.showWithAnimation();
4357
4472
  }
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
4473
  }
4366
4474
  });
4367
4475
  }
4476
+ ngOnInit() {
4477
+ // Set up drag and drop listeners
4478
+ this.setupDragAndDrop();
4479
+ // Set up file input change listeners
4480
+ this.setupFileInputListeners();
4481
+ // Set up window resize listener
4482
+ this.setupWindowResize();
4483
+ }
4484
+ ngOnDestroy() {
4485
+ console.log('🧹 [FloatingFileUploader] Component destroyed');
4486
+ this.removeDragAndDropListeners();
4487
+ this.removeFileInputListeners();
4488
+ // Clean up window resize listener
4489
+ if (this.windowResizeHandler) {
4490
+ window.removeEventListener('resize', this.windowResizeHandler);
4491
+ }
4492
+ }
4368
4493
  /**
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
4494
+ * Set up drag and drop functionality
4371
4495
  */
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
- }
4496
+ setupDragAndDrop() {
4497
+ document.addEventListener('dragover', this.handleDragOver.bind(this));
4498
+ document.addEventListener('dragleave', this.handleDragLeave.bind(this));
4499
+ document.addEventListener('drop', this.handleDrop.bind(this));
4420
4500
  }
4421
4501
  /**
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
4502
+ * Remove drag and drop listeners
4424
4503
  */
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
- });
4504
+ removeDragAndDropListeners() {
4505
+ document.removeEventListener('dragover', this.handleDragOver.bind(this));
4506
+ document.removeEventListener('dragleave', this.handleDragLeave.bind(this));
4507
+ document.removeEventListener('drop', this.handleDrop.bind(this));
4474
4508
  }
4475
4509
  /**
4476
- * Handle completion of multiple file upload
4510
+ * Set up file input change listeners
4477
4511
  */
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);
4512
+ setupFileInputListeners() {
4513
+ // Listen for file input change events globally
4514
+ document.addEventListener('change', this.handleFileInputChange.bind(this));
4512
4515
  }
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
- });
4516
+ /**
4517
+ * Remove file input listeners
4518
+ */
4519
+ removeFileInputListeners() {
4520
+ document.removeEventListener('change', this.handleFileInputChange.bind(this));
4530
4521
  }
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
- }
4522
+ /**
4523
+ * Handle file input change events
4524
+ */
4525
+ handleFileInputChange(event) {
4526
+ const target = event.target;
4527
+ console.log('🔍 [FloatingFileUploader] File input change event detected:', {
4528
+ type: target.type,
4529
+ filesLength: target.files?.length || 0,
4530
+ element: target
4537
4531
  });
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 });
4532
+ // Check if this is a file input with files
4533
+ if (target.type === 'file' && target.files && target.files.length > 0) {
4534
+ console.log('📁 [FloatingFileUploader] File input change detected:', target.files.length, 'files');
4535
+ // Check if the input has a data-user-id attribute for user context
4536
+ const userId = target.getAttribute('data-user-id');
4537
+ if (userId && userId !== this.currentUserId()) {
4538
+ this.setCurrentUserId(userId);
4582
4539
  }
4583
- });
4540
+ // Handle the files
4541
+ this.handleFiles(Array.from(target.files));
4542
+ // Reset the input to allow selecting the same files again
4543
+ target.value = '';
4544
+ }
4545
+ }
4546
+ /**
4547
+ * Handle drag over event
4548
+ */
4549
+ handleDragOver(event) {
4550
+ event.preventDefault();
4551
+ event.stopPropagation();
4552
+ // Show floating uploader when files are dragged over
4553
+ if (event.dataTransfer?.types.includes('Files')) {
4554
+ this.showWithAnimation();
4555
+ }
4584
4556
  }
4585
4557
  /**
4586
- * Check if the component is in multiple file mode
4558
+ * Handle drag leave event
4587
4559
  */
4588
- isMultipleFileMode() {
4589
- // Check if multiple attribute is set or if we have a group ID
4590
- return this.multiple || this.groupId() !== null;
4560
+ handleDragLeave(event) {
4561
+ event.preventDefault();
4562
+ event.stopPropagation();
4563
+ // Only hide if leaving the entire document
4564
+ if (!event.relatedTarget || event.relatedTarget === document.body) {
4565
+ this.updateVisibility();
4566
+ }
4591
4567
  }
4592
4568
  /**
4593
- * Load files from group ID using the group API
4569
+ * Handle drop event
4594
4570
  */
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
- });
4571
+ handleDrop(event) {
4572
+ event.preventDefault();
4573
+ event.stopPropagation();
4574
+ const files = event.dataTransfer?.files;
4575
+ if (files && files.length > 0) {
4576
+ this.handleFiles(Array.from(files));
4577
+ }
4620
4578
  }
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));
4579
+ /**
4580
+ * Handle files from drag and drop or file input
4581
+ */
4582
+ handleFiles(files) {
4583
+ console.log('📁 [FloatingFileUploader] Handling files:', files.length);
4584
+ // Use handleExternalFiles to process the files
4585
+ this.handleExternalFiles(files, this.currentUserId());
4627
4586
  }
4628
- isImageFileFromType(fileType) {
4629
- if (!fileType)
4630
- return false;
4631
- return fileType.startsWith('image/');
4587
+ /**
4588
+ * Update visibility - simplified for notification only
4589
+ */
4590
+ updateVisibility() {
4591
+ // This is just a notification component now
4592
+ // The actual uploads are handled by the global uploader
4632
4593
  }
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
- }
4594
+ /**
4595
+ * Show with animation
4596
+ */
4597
+ showWithAnimation() {
4598
+ console.log('🎬 [FloatingFileUploader] showWithAnimation called - setting isVisible to true');
4599
+ this.isAnimating.set(true);
4600
+ this.isVisible.set(true);
4601
+ // Remove animation class after animation completes
4602
+ setTimeout(() => {
4603
+ this.isAnimating.set(false);
4604
+ console.log('🎬 [FloatingFileUploader] Animation completed, isVisible:', this.isVisible());
4605
+ }, 300);
4665
4606
  }
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
- }
4607
+ /**
4608
+ * Hide with animation
4609
+ */
4610
+ hideWithAnimation() {
4611
+ this.isAnimating.set(true);
4612
+ // Wait for animation to complete before hiding
4613
+ setTimeout(() => {
4614
+ this.isVisible.set(false);
4615
+ this.isAnimating.set(false);
4616
+ }, 300);
4675
4617
  }
4676
- triggerFileSelect() {
4677
- const fileInput = document.getElementById('cide-file-input-' + this.id());
4678
- if (fileInput && !this.disabledSignal()) {
4679
- fileInput.click();
4680
- }
4618
+ /**
4619
+ * Toggle minimize state
4620
+ */
4621
+ toggleMinimize() {
4622
+ this.isMinimized.set(!this.isMinimized());
4681
4623
  }
4682
4624
  /**
4683
- * Show floating uploader manually
4684
- * This can be called to show the floating uploader even when no files are selected
4625
+ * Close the floating uploader
4685
4626
  */
4686
- showUploader() {
4687
- console.log('👁️ [FileInput] Manually showing floating uploader');
4688
- if (!this.showFloatingUploaderSignal()) {
4689
- console.log('⚠️ [FileInput] Floating uploader is disabled');
4690
- return;
4627
+ close() {
4628
+ // Don't clear files from service - just hide the uploader
4629
+ // Files will be fetched from API when "Show Files" is clicked
4630
+ this.hideWithAnimation();
4631
+ }
4632
+ /**
4633
+ * Get upload summary text
4634
+ */
4635
+ getUploadSummary() {
4636
+ const pending = this.pendingUploads();
4637
+ const active = this.activeUploadsLocal();
4638
+ const completed = this.completedUploads();
4639
+ const failed = this.failedUploads();
4640
+ if (active.length > 0) {
4641
+ return `${active.length} uploading`;
4691
4642
  }
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
- });
4643
+ else if (pending.length > 0) {
4644
+ return `${pending.length} pending`;
4710
4645
  }
4711
- else {
4712
- // No group ID, just trigger show
4713
- this.fileManagerService.triggerFloatingUploaderShow();
4646
+ else if (completed.length > 0 && failed.length === 0) {
4647
+ return `${completed.length} completed`;
4714
4648
  }
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);
4649
+ else if (failed.length > 0) {
4650
+ return `${completed.length} completed, ${failed.length} failed`;
4719
4651
  }
4652
+ return 'No uploads';
4720
4653
  }
4721
4654
  /**
4722
- * Get total upload count from file manager service for this component's group ID
4655
+ * Get overall progress percentage
4723
4656
  */
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;
4657
+ getOverallProgress() {
4658
+ const allUploads = Array.from(this.activeUploads().values());
4659
+ if (allUploads.length === 0)
4660
+ return 0;
4661
+ const totalProgress = allUploads.reduce((sum, upload) => sum + (upload.percentage || 0), 0);
4662
+ return Math.round(totalProgress / allUploads.length);
4663
+ }
4664
+ /**
4665
+ * Get status icon based on upload stage
4666
+ */
4667
+ getStatusIcon(stage) {
4668
+ switch (stage) {
4669
+ case 'reading': return 'schedule';
4670
+ case 'uploading': return 'cloud_upload';
4671
+ case 'complete': return 'check_circle';
4672
+ case 'error': return 'error';
4673
+ default: return 'help';
4731
4674
  }
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
4675
  }
4740
4676
  /**
4741
- * Check if there are active uploads for this component's group ID
4677
+ * Get status class based on upload stage
4742
4678
  */
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;
4679
+ getStatusClass(stage) {
4680
+ switch (stage) {
4681
+ case 'reading': return 'status-pending';
4682
+ case 'uploading': return 'status-uploading';
4683
+ case 'complete': return 'status-completed';
4684
+ case 'error': return 'status-error';
4685
+ default: return 'status-unknown';
4686
+ }
4759
4687
  }
4760
4688
  /**
4761
- * Get count of active (non-completed) uploads for this component's group ID
4689
+ * Cancel upload
4762
4690
  */
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;
4691
+ cancelUpload(fileId) {
4692
+ console.log('🚫 [FloatingFileUploader] Cancelling upload:', fileId);
4693
+ this.fileManagerService.cancelUpload(fileId);
4694
+ }
4695
+ /**
4696
+ * Get file name from file ID (extract from the ID format)
4697
+ */
4698
+ getFileNameFromId(fileId) {
4699
+ // Extract filename from the fileId format: filename_size_timestamp
4700
+ const parts = fileId.split('_');
4701
+ if (parts.length >= 3) {
4702
+ // Remove the last two parts (size and timestamp) to get the filename
4703
+ return parts.slice(0, -2).join('_');
4704
+ }
4705
+ return fileId;
4779
4706
  }
4780
4707
  /**
4781
- * Show floating uploader (alias for showUploader for template)
4708
+ * Get all files from service state (pending + active uploads + fetched files)
4709
+ * This method now uses the computed property for consistency
4782
4710
  */
4783
- showFloatingUploaderDialog() {
4784
- this.showUploader();
4711
+ getAllFiles() {
4712
+ return this.allFilesForGroup();
4785
4713
  }
4786
4714
  /**
4787
- * Get dynamic classes for drag and drop zone
4715
+ * Set current user ID
4788
4716
  */
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');
4796
- }
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');
4799
- }
4800
- return classes.join(' ');
4717
+ setCurrentUserId(userId) {
4718
+ this.currentUserId.set(userId);
4719
+ this.fileManagerService.setUserId(userId);
4801
4720
  }
4802
4721
  /**
4803
- * Get dynamic classes for icon
4722
+ * Public method to handle files from external sources
4723
+ * This can be called by other components to trigger the floating uploader
4804
4724
  */
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');
4725
+ handleExternalFiles(files, userId, groupId) {
4726
+ console.log('📁 [FloatingFileUploader] External files received:', files.length, 'files');
4727
+ // Set user ID if provided
4728
+ if (userId && userId !== this.currentUserId()) {
4729
+ this.setCurrentUserId(userId);
4809
4730
  }
4810
- else if (this.hasFiles()) {
4811
- classes.push('!tw-text-emerald-500', 'dark:!tw-text-emerald-400');
4731
+ // Set group ID if provided
4732
+ if (groupId) {
4733
+ this.currentGroupId.set(groupId);
4812
4734
  }
4813
- return classes.join(' ');
4735
+ // Upload files using file manager service
4736
+ // The file manager service will handle adding to its queue and the effect will show the floating uploader
4737
+ files.forEach((file, index) => {
4738
+ console.log(`📁 [FloatingFileUploader] Starting upload for file ${index + 1}/${files.length}:`, file.name);
4739
+ this.fileManagerService.uploadFile(file, {
4740
+ userId: this.currentUserId(),
4741
+ groupId: groupId,
4742
+ permissions: ['read', 'write'],
4743
+ tags: []
4744
+ })
4745
+ .pipe(takeUntilDestroyed(this.destroyRef))
4746
+ .subscribe({
4747
+ next: (response) => {
4748
+ console.log('✅ [FloatingFileUploader] Upload completed:', response);
4749
+ },
4750
+ error: (error) => {
4751
+ console.error('❌ [FloatingFileUploader] Upload failed:', error);
4752
+ }
4753
+ });
4754
+ });
4814
4755
  }
4815
4756
  /**
4816
- * Get dynamic classes for preview box
4757
+ * Check if there are any uploads for the current group
4817
4758
  */
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');
4759
+ hasUploadsForCurrentGroup() {
4760
+ const groupId = this.currentGroupId();
4761
+ if (!groupId) {
4762
+ // If no group filter, show all uploads
4763
+ return this.hasUploads();
4828
4764
  }
4829
- return classes.join(' ');
4765
+ // Check if any uploads belong to the current group
4766
+ // Note: This would need to be enhanced based on how group IDs are stored in the file manager service
4767
+ return this.hasUploads();
4830
4768
  }
4831
- // Drag and Drop Event Handlers
4769
+ /**
4770
+ * Handle drag over event for file drop
4771
+ */
4832
4772
  onDragOver(event) {
4833
4773
  event.preventDefault();
4834
4774
  event.stopPropagation();
4835
- if (!this.disabledSignal()) {
4836
- this.isDragOver.set(true);
4837
- console.log('🔄 [FileInput] Drag over detected');
4838
- }
4775
+ this.isDragOver.set(true);
4839
4776
  }
4777
+ /**
4778
+ * Handle drag leave event for file drop
4779
+ */
4840
4780
  onDragLeave(event) {
4841
4781
  event.preventDefault();
4842
4782
  event.stopPropagation();
4843
4783
  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
4784
  }
4785
+ /**
4786
+ * Handle drop event for file drop
4787
+ */
4854
4788
  onDrop(event) {
4855
4789
  event.preventDefault();
4856
4790
  event.stopPropagation();
4857
4791
  this.isDragOver.set(false);
4858
- if (this.disabledSignal()) {
4859
- console.log('⏸️ [FileInput] Drop ignored - component is disabled');
4860
- return;
4861
- }
4862
4792
  const files = event.dataTransfer?.files;
4863
4793
  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...' } };
4794
+ this.handleFileSelection(Array.from(files));
5006
4795
  }
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.' } };
4796
+ }
4797
+ /**
4798
+ * Trigger file input click
4799
+ */
4800
+ triggerFileInput() {
4801
+ const fileInput = document.querySelector('input[type="file"]');
4802
+ if (fileInput) {
4803
+ fileInput.click();
5011
4804
  }
5012
- console.log('✅ [FileInput] Validation PASSED: No errors');
5013
- return null; // No validation errors
5014
4805
  }
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();
4806
+ /**
4807
+ * Handle file input change
4808
+ */
4809
+ onFileInputChange(event) {
4810
+ const input = event.target;
4811
+ if (input.files && input.files.length > 0) {
4812
+ this.handleFileSelection(Array.from(input.files));
4813
+ input.value = ''; // Reset input
4814
+ }
5099
4815
  }
5100
- ngOnDestroy() {
5101
- this.removeTrigger();
4816
+ /**
4817
+ * Handle file selection from drag/drop or file input
4818
+ */
4819
+ handleFileSelection(files) {
4820
+ console.log('📁 [FloatingFileUploader] Files selected:', files.map(f => f.name));
4821
+ const groupId = this.currentGroupId();
4822
+ // Group ID must be provided by the file input component
4823
+ if (!groupId) {
4824
+ console.error('❌ [FloatingFileUploader] No group ID available. Files cannot be uploaded without a group ID from the file input component.');
4825
+ return;
4826
+ }
4827
+ console.log('🆔 [FloatingFileUploader] Using group ID from file input:', groupId);
4828
+ // Upload files using the file manager service
4829
+ files.forEach((file, index) => {
4830
+ console.log(`📤 [FloatingFileUploader] Uploading file ${index + 1}/${files.length}: ${file.name} to group: ${groupId}`);
4831
+ this.fileManagerService.uploadFile(file, {
4832
+ groupId: groupId,
4833
+ isMultiple: true,
4834
+ userId: this.currentUserId()
4835
+ });
4836
+ });
5102
4837
  }
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');
4838
+ /**
4839
+ * Update cached dimensions (throttled for performance)
4840
+ */
4841
+ updateCachedDimensions() {
4842
+ const now = Date.now();
4843
+ // Only update dimensions every 100ms to avoid excessive DOM queries
4844
+ if (now - this.lastDimensionUpdate < 100) {
5107
4845
  return;
5108
4846
  }
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();
4847
+ const uploaderElement = document.querySelector('.floating-uploader');
4848
+ if (uploaderElement) {
4849
+ this.cachedDimensions = {
4850
+ width: uploaderElement.offsetWidth,
4851
+ height: uploaderElement.offsetHeight
4852
+ };
4853
+ this.lastDimensionUpdate = now;
5114
4854
  }
5115
4855
  }
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
- }
4856
+ /**
4857
+ * Start dragging the uploader
4858
+ */
4859
+ startDrag(event) {
4860
+ event.preventDefault();
4861
+ const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
4862
+ const clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY;
4863
+ const currentPos = this.position();
4864
+ this.dragOffset = {
4865
+ x: clientX - currentPos.x,
4866
+ y: clientY - currentPos.y
4867
+ };
4868
+ this.isDragging.set(true);
4869
+ // Update cached dimensions at the start of drag for better performance
4870
+ this.updateCachedDimensions();
4871
+ // Add event listeners for drag and end
4872
+ const moveHandler = (e) => this.onDrag(e);
4873
+ const endHandler = () => this.endDrag(moveHandler, endHandler);
4874
+ document.addEventListener('mousemove', moveHandler, { passive: false });
4875
+ document.addEventListener('mouseup', endHandler);
4876
+ document.addEventListener('touchmove', moveHandler, { passive: false });
4877
+ document.addEventListener('touchend', endHandler);
4878
+ // Prevent text selection during drag
4879
+ document.body.style.userSelect = 'none';
5122
4880
  }
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)
4881
+ /**
4882
+ * Handle dragging movement
4883
+ */
4884
+ onDrag(event) {
4885
+ if (!this.isDragging())
5140
4886
  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);
4887
+ event.preventDefault();
4888
+ const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
4889
+ const clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY;
4890
+ const newX = clientX - this.dragOffset.x;
4891
+ const newY = clientY - this.dragOffset.y;
4892
+ // Constrain to viewport bounds using cached dimensions for performance
4893
+ const viewportWidth = window.innerWidth;
4894
+ const viewportHeight = window.innerHeight;
4895
+ // Use cached dimensions instead of DOM queries for better performance
4896
+ const uploaderWidth = this.cachedDimensions.width;
4897
+ const uploaderHeight = this.cachedDimensions.height;
4898
+ // Ensure uploader stays within viewport bounds
4899
+ const constrainedX = Math.max(0, Math.min(newX, viewportWidth - uploaderWidth));
4900
+ const constrainedY = Math.max(0, Math.min(newY, viewportHeight - uploaderHeight));
4901
+ this.position.set({ x: constrainedX, y: constrainedY });
4902
+ }
4903
+ /**
4904
+ * End dragging
4905
+ */
4906
+ endDrag(moveHandler, endHandler) {
4907
+ this.isDragging.set(false);
4908
+ // Remove event listeners
4909
+ document.removeEventListener('mousemove', moveHandler);
4910
+ document.removeEventListener('mouseup', endHandler);
4911
+ document.removeEventListener('touchmove', moveHandler);
4912
+ document.removeEventListener('touchend', endHandler);
4913
+ // Restore text selection
4914
+ document.body.style.userSelect = '';
4915
+ }
4916
+ /**
4917
+ * Set up window resize listener to keep uploader within bounds
4918
+ */
4919
+ setupWindowResize() {
4920
+ const handleResize = () => {
4921
+ const currentPos = this.position();
4922
+ const viewportWidth = window.innerWidth;
4923
+ const viewportHeight = window.innerHeight;
4924
+ // Update cached dimensions on resize
4925
+ this.updateCachedDimensions();
4926
+ // Use cached dimensions for performance
4927
+ const uploaderWidth = this.cachedDimensions.width;
4928
+ const uploaderHeight = this.cachedDimensions.height;
4929
+ // Constrain position to new viewport bounds
4930
+ const constrainedX = Math.max(0, Math.min(currentPos.x, viewportWidth - uploaderWidth));
4931
+ const constrainedY = Math.max(0, Math.min(currentPos.y, viewportHeight - uploaderHeight));
4932
+ // Update position if it changed
4933
+ if (constrainedX !== currentPos.x || constrainedY !== currentPos.y) {
4934
+ this.position.set({ x: constrainedX, y: constrainedY });
4935
+ console.log('📐 [FloatingFileUploader] Position adjusted for window resize:', { x: constrainedX, y: constrainedY });
4936
+ }
4937
+ };
4938
+ window.addEventListener('resize', handleResize);
4939
+ // Store reference for cleanup
4940
+ this.windowResizeHandler = handleResize;
4941
+ }
4942
+ /**
4943
+ * Initialize default position
4944
+ */
4945
+ initializePosition() {
4946
+ // Set initial position to bottom-right corner
4947
+ const viewportWidth = window.innerWidth;
4948
+ const viewportHeight = window.innerHeight;
4949
+ // Initialize cached dimensions with defaults
4950
+ this.cachedDimensions = { width: 320, height: 300 };
4951
+ // Use cached dimensions for initial positioning
4952
+ const uploaderWidth = this.cachedDimensions.width;
4953
+ const uploaderHeight = this.cachedDimensions.height;
4954
+ // Ensure initial position is within bounds
4955
+ const initialX = Math.max(20, viewportWidth - uploaderWidth - 20);
4956
+ const initialY = Math.max(20, viewportHeight - uploaderHeight - 20);
4957
+ this.position.set({
4958
+ x: initialX,
4959
+ y: initialY
5174
4960
  });
5175
- // Insert icon after the input
5176
- element.parentNode?.insertBefore(this.triggerIcon, element.nextSibling);
4961
+ // Update dimensions after a short delay to get actual rendered size
4962
+ setTimeout(() => {
4963
+ this.updateCachedDimensions();
4964
+ }, 100);
5177
4965
  }
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 });
4966
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4967
+ 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"] }] });
5180
4968
  }
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
- }] } });
4969
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, decorators: [{
4970
+ type: Component,
4971
+ args: [{ selector: 'cide-ele-floating-file-uploader', standalone: true, imports: [
4972
+ CommonModule,
4973
+ CideIconComponent
4974
+ ], 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"] }]
4975
+ }], ctorParameters: () => [] });
5196
4976
 
5197
4977
  class CideTextareaComponent {
5198
4978
  label = '';
@@ -9432,5 +9212,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
9432
9212
  * Generated bundle index. Do not edit.
9433
9213
  */
9434
9214
 
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 };
9215
+ 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
9216
  //# sourceMappingURL=cloud-ide-element.mjs.map