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.
- package/fesm2022/cloud-ide-element.mjs +1468 -1688
- package/fesm2022/cloud-ide-element.mjs.map +1 -1
- package/index.d.ts +18 -28
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i1 from '@angular/common';
|
|
2
2
|
import { CommonModule, NgTemplateOutlet } from '@angular/common';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { Pipe, Injectable, inject, EventEmitter, ViewContainerRef, forwardRef, ViewChild, Output, Input, Component, HostListener, ContentChildren, signal, DestroyRef, computed, effect,
|
|
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
|
|
3326
|
-
destroyRef = inject(DestroyRef);
|
|
3359
|
+
class CideEleFileInputComponent {
|
|
3327
3360
|
fileManagerService = inject(CideEleFileManagerService);
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
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
|
-
|
|
3390
|
-
|
|
3391
|
-
//
|
|
3392
|
-
|
|
3393
|
-
|
|
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
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
//
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
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
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
this.
|
|
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
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
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
|
-
|
|
3432
|
-
|
|
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
|
-
|
|
3440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
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
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
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
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
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
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
//
|
|
3547
|
-
//
|
|
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
|
-
*
|
|
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
|
-
|
|
3553
|
-
console.log('
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
this.
|
|
3567
|
-
//
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
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
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
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
|
-
*
|
|
3678
|
-
*
|
|
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
|
-
|
|
3681
|
-
console.log('
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
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
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
.
|
|
3701
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
3716
|
-
console.log(
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
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.
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
.
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
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('❌ [
|
|
3750
|
-
|
|
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
|
|
3933
|
+
* Check if the component is in multiple file mode
|
|
3766
3934
|
*/
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
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
|
-
*
|
|
3940
|
+
* Load files from group ID using the group API
|
|
3779
3941
|
*/
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
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
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
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
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
this.
|
|
3800
|
-
const
|
|
3801
|
-
if (
|
|
3802
|
-
|
|
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
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
const
|
|
3810
|
-
if (
|
|
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
|
-
*
|
|
4030
|
+
* Show floating uploader manually
|
|
4031
|
+
* This can be called to show the floating uploader even when no files are selected
|
|
3816
4032
|
*/
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
if (
|
|
3820
|
-
|
|
3821
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
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
|
-
|
|
3833
|
-
return;
|
|
4080
|
+
return Array.from(this.fileManagerService.activeUploads().values()).some(upload => upload.stage !== 'complete');
|
|
3834
4081
|
}
|
|
3835
|
-
|
|
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
|
-
*
|
|
4085
|
+
* Get count of active (non-completed) uploads for this component's group ID
|
|
3848
4086
|
*/
|
|
3849
|
-
|
|
3850
|
-
const
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
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
|
-
*
|
|
4097
|
+
* Show floating uploader (alias for showUploader for template)
|
|
3866
4098
|
*/
|
|
3867
|
-
|
|
3868
|
-
|
|
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
|
-
*
|
|
4103
|
+
* Get dynamic classes for drag and drop zone
|
|
3891
4104
|
*/
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
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
|
-
*
|
|
4119
|
+
* Get dynamic classes for icon
|
|
3913
4120
|
*/
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
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
|
-
*
|
|
4132
|
+
* Get dynamic classes for preview box
|
|
3926
4133
|
*/
|
|
3927
|
-
|
|
3928
|
-
const
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
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
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
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
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
4215
|
-
this.fileChange.emit(
|
|
4227
|
+
this.onChange(files);
|
|
4228
|
+
this.fileChange.emit(files);
|
|
4216
4229
|
this.onTouched();
|
|
4217
|
-
//
|
|
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() &&
|
|
4232
|
+
if (this.autoUploadSignal() && files.length > 0) {
|
|
4224
4233
|
if (this.multipleSignal()) {
|
|
4225
|
-
console.log('🚀 [FileInput] Auto upload enabled for multiple files mode:',
|
|
4226
|
-
this.uploadMultipleFiles(Array.from(
|
|
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:',
|
|
4230
|
-
this.uploadFile(
|
|
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
|
-
|
|
4238
|
-
|
|
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
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
//
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
this.
|
|
4269
|
-
this.
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
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
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
}
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
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
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
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
|
-
*
|
|
4510
|
+
* Set up file input change listeners
|
|
4477
4511
|
*/
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
this.
|
|
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
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
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
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
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
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
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
|
-
*
|
|
4558
|
+
* Handle drag leave event
|
|
4587
4559
|
*/
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
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
|
-
*
|
|
4569
|
+
* Handle drop event
|
|
4594
4570
|
*/
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
.
|
|
4601
|
-
|
|
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
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
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
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
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
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
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
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
this.
|
|
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
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4618
|
+
/**
|
|
4619
|
+
* Toggle minimize state
|
|
4620
|
+
*/
|
|
4621
|
+
toggleMinimize() {
|
|
4622
|
+
this.isMinimized.set(!this.isMinimized());
|
|
4681
4623
|
}
|
|
4682
4624
|
/**
|
|
4683
|
-
*
|
|
4684
|
-
* This can be called to show the floating uploader even when no files are selected
|
|
4625
|
+
* Close the floating uploader
|
|
4685
4626
|
*/
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
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
|
-
|
|
4693
|
-
|
|
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
|
-
|
|
4713
|
-
this.fileManagerService.triggerFloatingUploaderShow();
|
|
4646
|
+
else if (completed.length > 0 && failed.length === 0) {
|
|
4647
|
+
return `${completed.length} completed`;
|
|
4714
4648
|
}
|
|
4715
|
-
|
|
4716
|
-
|
|
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
|
|
4655
|
+
* Get overall progress percentage
|
|
4723
4656
|
*/
|
|
4724
|
-
|
|
4725
|
-
const
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
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
|
-
*
|
|
4677
|
+
* Get status class based on upload stage
|
|
4742
4678
|
*/
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
return
|
|
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
|
-
*
|
|
4689
|
+
* Cancel upload
|
|
4762
4690
|
*/
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
//
|
|
4772
|
-
const
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
4784
|
-
this.
|
|
4711
|
+
getAllFiles() {
|
|
4712
|
+
return this.allFilesForGroup();
|
|
4785
4713
|
}
|
|
4786
4714
|
/**
|
|
4787
|
-
*
|
|
4715
|
+
* Set current user ID
|
|
4788
4716
|
*/
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
4806
|
-
|
|
4807
|
-
if
|
|
4808
|
-
|
|
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
|
-
|
|
4811
|
-
|
|
4731
|
+
// Set group ID if provided
|
|
4732
|
+
if (groupId) {
|
|
4733
|
+
this.currentGroupId.set(groupId);
|
|
4812
4734
|
}
|
|
4813
|
-
|
|
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
|
-
*
|
|
4757
|
+
* Check if there are any uploads for the current group
|
|
4817
4758
|
*/
|
|
4818
|
-
|
|
4819
|
-
const
|
|
4820
|
-
if (
|
|
4821
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4769
|
+
/**
|
|
4770
|
+
* Handle drag over event for file drop
|
|
4771
|
+
*/
|
|
4832
4772
|
onDragOver(event) {
|
|
4833
4773
|
event.preventDefault();
|
|
4834
4774
|
event.stopPropagation();
|
|
4835
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
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
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
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
|
-
|
|
5101
|
-
|
|
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
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
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
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
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
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
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
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
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
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
this.
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
this.
|
|
5151
|
-
this.
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
//
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
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
|
-
//
|
|
5176
|
-
|
|
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:
|
|
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:
|
|
5182
|
-
type:
|
|
5183
|
-
args: [{
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
}]
|
|
5187
|
-
}],
|
|
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,
|
|
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
|