cloud-ide-element 1.0.55 → 1.0.57
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 +612 -261
- package/fesm2022/cloud-ide-element.mjs.map +1 -1
- package/index.d.ts +267 -151
- package/package.json +1 -1
|
@@ -1,12 +1,13 @@
|
|
|
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, computed,
|
|
4
|
+
import { Pipe, Injectable, inject, EventEmitter, ViewContainerRef, forwardRef, ViewChild, Output, Input, Component, HostListener, ContentChildren, signal, DestroyRef, computed, input, output, linkedSignal, afterRenderEffect, afterNextRender, Directive, viewChild, effect } from '@angular/core';
|
|
5
5
|
import * as i2 from '@angular/forms';
|
|
6
6
|
import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
|
|
7
|
-
import { BehaviorSubject, Subject, debounceTime, takeUntil, distinctUntilChanged, Observable } from 'rxjs';
|
|
7
|
+
import { BehaviorSubject, Subject, debounceTime, takeUntil, distinctUntilChanged, Observable, retry, catchError, finalize, throwError } from 'rxjs';
|
|
8
8
|
import * as i2$1 from '@angular/router';
|
|
9
|
-
import
|
|
9
|
+
import { HttpClient, HttpEventType } from '@angular/common/http';
|
|
10
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
10
11
|
|
|
11
12
|
class CapitalizePipe {
|
|
12
13
|
transform(value, capitalizationMethod) {
|
|
@@ -2563,88 +2564,307 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
|
|
|
2563
2564
|
type: Output
|
|
2564
2565
|
}] } });
|
|
2565
2566
|
|
|
2567
|
+
class ICoreCyfm {
|
|
2568
|
+
_id;
|
|
2569
|
+
cyfm_name;
|
|
2570
|
+
cyfm_alt_text;
|
|
2571
|
+
cyfm_path;
|
|
2572
|
+
cyfm_size_in_byte;
|
|
2573
|
+
cyfm_type;
|
|
2574
|
+
cyfm_creation_dt;
|
|
2575
|
+
cyfm_id_user;
|
|
2576
|
+
cyfm_permissions;
|
|
2577
|
+
cyfm_tags;
|
|
2578
|
+
cyfm_version;
|
|
2579
|
+
cyfm_file_status_sygmt;
|
|
2580
|
+
cyfm_isactive;
|
|
2581
|
+
}
|
|
2582
|
+
/* INTERFACE END */
|
|
2583
|
+
/* MODEL START */
|
|
2584
|
+
class MFileManager {
|
|
2585
|
+
cyfm_id = "";
|
|
2586
|
+
constructor(init) {
|
|
2587
|
+
Object.assign(this, init);
|
|
2588
|
+
}
|
|
2589
|
+
Validate() {
|
|
2590
|
+
let errorLogger = {};
|
|
2591
|
+
// Log errors here
|
|
2592
|
+
if (!this.cyfm_id) {
|
|
2593
|
+
errorLogger.cyfm_id = "File ID is Required!";
|
|
2594
|
+
}
|
|
2595
|
+
return errorLogger;
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
class ICoreCyfmSave extends ICoreCyfm {
|
|
2599
|
+
cyfm_file_base64 = "";
|
|
2600
|
+
cyfm_temp_unique_id = "";
|
|
2601
|
+
constructor(init) {
|
|
2602
|
+
super();
|
|
2603
|
+
Object.assign(this, init);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
class CoreFileManagerInsertUpdatePayload {
|
|
2607
|
+
core_file_manager_new = [];
|
|
2608
|
+
core_file_manager_update = [];
|
|
2609
|
+
core_file_manager_delete = [];
|
|
2610
|
+
constructor(init) {
|
|
2611
|
+
Object.assign(this, init);
|
|
2612
|
+
}
|
|
2613
|
+
Validate() {
|
|
2614
|
+
let errorLogger = {};
|
|
2615
|
+
// Log errors here
|
|
2616
|
+
return errorLogger;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2566
2620
|
class CideEleFileManagerService {
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2621
|
+
// Angular 20: Modern dependency injection
|
|
2622
|
+
http = inject(HttpClient);
|
|
2623
|
+
destroyRef = inject(DestroyRef);
|
|
2624
|
+
// Angular 20: Signal-based state management
|
|
2625
|
+
_baseUrl = signal('/api', ...(ngDevMode ? [{ debugName: "_baseUrl" }] : []));
|
|
2626
|
+
_isUploading = signal(false, ...(ngDevMode ? [{ debugName: "_isUploading" }] : []));
|
|
2627
|
+
_uploadQueue = signal([], ...(ngDevMode ? [{ debugName: "_uploadQueue" }] : []));
|
|
2628
|
+
_activeUploads = signal(new Map(), ...(ngDevMode ? [{ debugName: "_activeUploads" }] : []));
|
|
2629
|
+
_error = signal(null, ...(ngDevMode ? [{ debugName: "_error" }] : []));
|
|
2630
|
+
// Angular 20: Computed values
|
|
2631
|
+
baseUrl = this._baseUrl.asReadonly();
|
|
2632
|
+
isUploading = this._isUploading.asReadonly();
|
|
2633
|
+
uploadQueue = this._uploadQueue.asReadonly();
|
|
2634
|
+
activeUploads = this._activeUploads.asReadonly();
|
|
2635
|
+
error = this._error.asReadonly();
|
|
2636
|
+
hasActiveUploads = computed(() => this._activeUploads().size > 0, ...(ngDevMode ? [{ debugName: "hasActiveUploads" }] : []));
|
|
2637
|
+
queueLength = computed(() => this._uploadQueue().length, ...(ngDevMode ? [{ debugName: "queueLength" }] : []));
|
|
2638
|
+
serviceState = computed(() => ({
|
|
2639
|
+
isUploading: this._isUploading(),
|
|
2640
|
+
uploadQueue: this._uploadQueue(),
|
|
2641
|
+
activeUploads: this._activeUploads(),
|
|
2642
|
+
error: this._error()
|
|
2643
|
+
}), ...(ngDevMode ? [{ debugName: "serviceState" }] : []));
|
|
2644
|
+
constructor() {
|
|
2645
|
+
console.log('🚀 [FileManagerService] Angular 20 service initialized');
|
|
2571
2646
|
}
|
|
2572
2647
|
/**
|
|
2573
2648
|
* Upload a file with base64 encoding and progress tracking
|
|
2649
|
+
* Angular 20: Enhanced with better error handling, retry logic, and signal-based state
|
|
2574
2650
|
* @param file The file to upload
|
|
2575
|
-
* @param
|
|
2651
|
+
* @param options Upload options and additional data
|
|
2576
2652
|
* @param progressCallback Optional callback for progress updates
|
|
2577
2653
|
* @returns Observable with the upload response
|
|
2578
2654
|
*/
|
|
2579
|
-
uploadFile(file,
|
|
2580
|
-
|
|
2655
|
+
uploadFile(file, options, progressCallback) {
|
|
2656
|
+
const fileId = this.generateFileId(file);
|
|
2657
|
+
const uploadOptions = { retryAttempts: 3, timeout: 30000, ...options };
|
|
2658
|
+
// Angular 20: Update service state
|
|
2659
|
+
this._isUploading.set(true);
|
|
2660
|
+
this._error.set(null);
|
|
2661
|
+
this.addToUploadQueue(fileId);
|
|
2662
|
+
console.log('📤 [FileManagerService] Starting upload for:', file.name, 'Size:', file.size, 'bytes');
|
|
2581
2663
|
return new Observable(observer => {
|
|
2582
2664
|
const reader = new FileReader();
|
|
2583
|
-
//
|
|
2665
|
+
// Angular 20: Enhanced progress tracking with signals
|
|
2584
2666
|
reader.onprogress = (event) => {
|
|
2585
2667
|
if (event.lengthComputable && progressCallback) {
|
|
2586
2668
|
const progress = (event.loaded / event.total) * 50; // File reading is 50% of total progress
|
|
2587
2669
|
progressCallback(progress);
|
|
2670
|
+
this.updateUploadProgress(fileId, {
|
|
2671
|
+
loaded: event.loaded,
|
|
2672
|
+
total: event.total,
|
|
2673
|
+
percentage: progress,
|
|
2674
|
+
stage: 'reading'
|
|
2675
|
+
});
|
|
2588
2676
|
}
|
|
2589
2677
|
};
|
|
2590
2678
|
reader.onload = () => {
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
progressCallback
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2679
|
+
try {
|
|
2680
|
+
const base64String = reader.result;
|
|
2681
|
+
const base64Data = base64String.split(',')[1]; // Remove data:image/jpeg;base64, prefix
|
|
2682
|
+
// Update progress to 50% (file reading complete)
|
|
2683
|
+
if (progressCallback) {
|
|
2684
|
+
progressCallback(50);
|
|
2685
|
+
}
|
|
2686
|
+
this.updateUploadProgress(fileId, {
|
|
2687
|
+
loaded: 50,
|
|
2688
|
+
total: 100,
|
|
2689
|
+
percentage: 50,
|
|
2690
|
+
stage: 'uploading'
|
|
2691
|
+
});
|
|
2692
|
+
// Create the file data object with proper typing
|
|
2693
|
+
const fileData = {
|
|
2694
|
+
cyfm_name: file.name,
|
|
2695
|
+
cyfm_alt_text: uploadOptions?.altText || file.name,
|
|
2696
|
+
cyfm_path: '', // Will be set by backend
|
|
2697
|
+
cyfm_size_in_byte: file.size,
|
|
2698
|
+
cyfm_type: file.type,
|
|
2699
|
+
cyfm_creation_dt: new Date().toISOString(),
|
|
2700
|
+
cyfm_id_user: uploadOptions?.userId || '',
|
|
2701
|
+
cyfm_permissions: [], // Match the interface type
|
|
2702
|
+
cyfm_tags: uploadOptions?.tags || [],
|
|
2703
|
+
cyfm_version: 1,
|
|
2704
|
+
cyfm_file_status_sygmt: uploadOptions?.fileStatus || 'file_manager_file_status_active',
|
|
2705
|
+
cyfm_isactive: true,
|
|
2706
|
+
cyfm_file_base64: base64Data,
|
|
2707
|
+
cyfm_temp_unique_id: fileId
|
|
2708
|
+
};
|
|
2709
|
+
// Create the payload with proper typing
|
|
2710
|
+
const payload = new CoreFileManagerInsertUpdatePayload({
|
|
2711
|
+
core_file_manager_new: [fileData],
|
|
2712
|
+
core_file_manager_update: [],
|
|
2713
|
+
core_file_manager_delete: []
|
|
2714
|
+
});
|
|
2715
|
+
// Angular 20: Enhanced HTTP request with retry logic and better error handling
|
|
2716
|
+
// Use proper API endpoint from cloud-ide-lms-model
|
|
2717
|
+
this.http.post(`${this._baseUrl()}`, payload, {
|
|
2718
|
+
reportProgress: true,
|
|
2719
|
+
observe: 'events'
|
|
2720
|
+
}).pipe(retry(uploadOptions.retryAttempts || 3), catchError(this.handleError.bind(this)), finalize(() => {
|
|
2721
|
+
this.removeFromUploadQueue(fileId);
|
|
2722
|
+
this.removeActiveUpload(fileId);
|
|
2723
|
+
if (this._uploadQueue().length === 0) {
|
|
2724
|
+
this._isUploading.set(false);
|
|
2623
2725
|
}
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2726
|
+
}), takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
2727
|
+
next: (event) => {
|
|
2728
|
+
if (event.type === HttpEventType.UploadProgress && progressCallback) {
|
|
2729
|
+
const uploadProgress = (event.loaded / event.total) * 50; // Upload is remaining 50%
|
|
2730
|
+
const totalProgress = 50 + uploadProgress; // Total progress (50% file reading + upload progress)
|
|
2731
|
+
progressCallback(totalProgress);
|
|
2732
|
+
this.updateUploadProgress(fileId, {
|
|
2733
|
+
loaded: event.loaded,
|
|
2734
|
+
total: event.total,
|
|
2735
|
+
percentage: totalProgress,
|
|
2736
|
+
stage: 'uploading'
|
|
2737
|
+
});
|
|
2627
2738
|
}
|
|
2628
|
-
|
|
2739
|
+
else if (event.type === HttpEventType.Response) {
|
|
2740
|
+
if (progressCallback) {
|
|
2741
|
+
progressCallback(100);
|
|
2742
|
+
}
|
|
2743
|
+
this.updateUploadProgress(fileId, {
|
|
2744
|
+
loaded: 100,
|
|
2745
|
+
total: 100,
|
|
2746
|
+
percentage: 100,
|
|
2747
|
+
stage: 'complete'
|
|
2748
|
+
});
|
|
2749
|
+
console.log('✅ [FileManagerService] Upload successful:', event.body);
|
|
2750
|
+
// Handle the CoreFileManagerInsertUpdateResponse
|
|
2751
|
+
const response = event.body;
|
|
2752
|
+
if (response.success) {
|
|
2753
|
+
observer.next(response);
|
|
2754
|
+
observer.complete();
|
|
2755
|
+
}
|
|
2756
|
+
else {
|
|
2757
|
+
observer.error(new Error(response.message || 'Upload failed'));
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
},
|
|
2761
|
+
error: (error) => {
|
|
2762
|
+
console.error('❌ [FileManagerService] Upload failed:', error);
|
|
2763
|
+
this._error.set(error.message || 'Upload failed');
|
|
2764
|
+
this.updateUploadProgress(fileId, {
|
|
2765
|
+
loaded: 0,
|
|
2766
|
+
total: 100,
|
|
2767
|
+
percentage: 0,
|
|
2768
|
+
stage: 'error'
|
|
2769
|
+
});
|
|
2770
|
+
observer.error(error);
|
|
2629
2771
|
}
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
catch (error) {
|
|
2775
|
+
console.error('❌ [FileManagerService] File processing error:', error);
|
|
2776
|
+
this._error.set('Failed to process file');
|
|
2777
|
+
observer.error(error);
|
|
2778
|
+
}
|
|
2779
|
+
};
|
|
2780
|
+
reader.onerror = () => {
|
|
2781
|
+
console.error('❌ [FileManagerService] File reading failed');
|
|
2782
|
+
this._error.set('Failed to read file');
|
|
2783
|
+
observer.error(new Error('Failed to read file'));
|
|
2633
2784
|
};
|
|
2634
|
-
reader.onerror = () => observer.error('Failed to read file');
|
|
2635
2785
|
reader.readAsDataURL(file);
|
|
2636
2786
|
});
|
|
2637
2787
|
}
|
|
2638
2788
|
/**
|
|
2639
2789
|
* Set the base URL for API calls
|
|
2640
|
-
*
|
|
2790
|
+
* Angular 20: Using signal-based state management
|
|
2641
2791
|
* @param url The base URL for the API
|
|
2642
2792
|
*/
|
|
2643
2793
|
setBaseUrl(url) {
|
|
2644
2794
|
console.log('🔍 [FileManagerService] Setting base URL:', url);
|
|
2645
|
-
this.
|
|
2795
|
+
this._baseUrl.set(url);
|
|
2646
2796
|
}
|
|
2647
|
-
|
|
2797
|
+
/**
|
|
2798
|
+
* Angular 20: Helper methods for state management
|
|
2799
|
+
*/
|
|
2800
|
+
generateFileId(file) {
|
|
2801
|
+
return `${file.name}_${file.size}_${Date.now()}`;
|
|
2802
|
+
}
|
|
2803
|
+
addToUploadQueue(fileId) {
|
|
2804
|
+
const currentQueue = this._uploadQueue();
|
|
2805
|
+
this._uploadQueue.set([...currentQueue, fileId]);
|
|
2806
|
+
}
|
|
2807
|
+
removeFromUploadQueue(fileId) {
|
|
2808
|
+
const currentQueue = this._uploadQueue();
|
|
2809
|
+
this._uploadQueue.set(currentQueue.filter(id => id !== fileId));
|
|
2810
|
+
}
|
|
2811
|
+
updateUploadProgress(fileId, progress) {
|
|
2812
|
+
const currentUploads = new Map(this._activeUploads());
|
|
2813
|
+
currentUploads.set(fileId, progress);
|
|
2814
|
+
this._activeUploads.set(currentUploads);
|
|
2815
|
+
}
|
|
2816
|
+
removeActiveUpload(fileId) {
|
|
2817
|
+
const currentUploads = new Map(this._activeUploads());
|
|
2818
|
+
currentUploads.delete(fileId);
|
|
2819
|
+
this._activeUploads.set(currentUploads);
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* Angular 20: Enhanced error handling
|
|
2823
|
+
*/
|
|
2824
|
+
handleError(error) {
|
|
2825
|
+
let errorMessage = 'An unknown error occurred';
|
|
2826
|
+
if (error.error instanceof ErrorEvent) {
|
|
2827
|
+
// Client-side error
|
|
2828
|
+
errorMessage = `Client Error: ${error.error.message}`;
|
|
2829
|
+
}
|
|
2830
|
+
else {
|
|
2831
|
+
// Server-side error
|
|
2832
|
+
errorMessage = `Server Error: ${error.status} - ${error.message}`;
|
|
2833
|
+
}
|
|
2834
|
+
console.error('❌ [FileManagerService] HTTP Error:', errorMessage);
|
|
2835
|
+
this._error.set(errorMessage);
|
|
2836
|
+
return throwError(() => new Error(errorMessage));
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Angular 20: Service utility methods
|
|
2840
|
+
*/
|
|
2841
|
+
clearError() {
|
|
2842
|
+
this._error.set(null);
|
|
2843
|
+
}
|
|
2844
|
+
getUploadProgress(fileId) {
|
|
2845
|
+
return this._activeUploads().get(fileId);
|
|
2846
|
+
}
|
|
2847
|
+
cancelUpload(fileId) {
|
|
2848
|
+
this.removeFromUploadQueue(fileId);
|
|
2849
|
+
this.removeActiveUpload(fileId);
|
|
2850
|
+
console.log('🚫 [FileManagerService] Upload cancelled for:', fileId);
|
|
2851
|
+
}
|
|
2852
|
+
/**
|
|
2853
|
+
* Angular 20: File validation utility
|
|
2854
|
+
*/
|
|
2855
|
+
validateFile(file, maxSizeMB = 10, allowedTypes = []) {
|
|
2856
|
+
// Check file size
|
|
2857
|
+
const maxSizeBytes = maxSizeMB * 1024 * 1024; // Convert MB to bytes
|
|
2858
|
+
if (file.size > maxSizeBytes) {
|
|
2859
|
+
return { valid: false, error: `File size exceeds ${maxSizeMB}MB limit` };
|
|
2860
|
+
}
|
|
2861
|
+
// Check file type
|
|
2862
|
+
if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
|
|
2863
|
+
return { valid: false, error: `File type ${file.type} is not allowed` };
|
|
2864
|
+
}
|
|
2865
|
+
return { valid: true };
|
|
2866
|
+
}
|
|
2867
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2648
2868
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileManagerService, providedIn: 'root' });
|
|
2649
2869
|
}
|
|
2650
2870
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileManagerService, decorators: [{
|
|
@@ -2652,7 +2872,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
|
|
|
2652
2872
|
args: [{
|
|
2653
2873
|
providedIn: 'root'
|
|
2654
2874
|
}]
|
|
2655
|
-
}], ctorParameters: () => [
|
|
2875
|
+
}], ctorParameters: () => [] });
|
|
2656
2876
|
|
|
2657
2877
|
class NotificationService {
|
|
2658
2878
|
notifications = signal([], ...(ngDevMode ? [{ debugName: "notifications" }] : []));
|
|
@@ -2877,43 +3097,88 @@ class CideEleFileInputComponent {
|
|
|
2877
3097
|
fileManagerService = inject(CideEleFileManagerService);
|
|
2878
3098
|
notificationService = inject(NotificationService);
|
|
2879
3099
|
elementService = inject(CideElementsService);
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
3100
|
+
destroyRef = inject(DestroyRef);
|
|
3101
|
+
// private readonly pendingTasks = inject(PendingTasks); // TODO: Fix PendingTasks API usage
|
|
3102
|
+
// Modern input signals
|
|
3103
|
+
label = input('Choose file', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
3104
|
+
accept = input('', ...(ngDevMode ? [{ debugName: "accept" }] : []));
|
|
3105
|
+
multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
|
|
3106
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
3107
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
3108
|
+
helperText = input('', ...(ngDevMode ? [{ debugName: "helperText" }] : []));
|
|
3109
|
+
errorText = input('', ...(ngDevMode ? [{ debugName: "errorText" }] : []));
|
|
3110
|
+
showPreview = input(false, ...(ngDevMode ? [{ debugName: "showPreview" }] : []));
|
|
3111
|
+
previewWidth = input('200px', ...(ngDevMode ? [{ debugName: "previewWidth" }] : []));
|
|
3112
|
+
previewHeight = input('200px', ...(ngDevMode ? [{ debugName: "previewHeight" }] : []));
|
|
3113
|
+
previewBoxMode = input(false, ...(ngDevMode ? [{ debugName: "previewBoxMode" }] : []));
|
|
3114
|
+
showFileName = input(true, ...(ngDevMode ? [{ debugName: "showFileName" }] : []));
|
|
3115
|
+
placeholderText = input('Click to select image', ...(ngDevMode ? [{ debugName: "placeholderText" }] : []));
|
|
3116
|
+
placeholderIcon = input('📷', ...(ngDevMode ? [{ debugName: "placeholderIcon" }] : []));
|
|
3117
|
+
autoUpload = input(false, ...(ngDevMode ? [{ debugName: "autoUpload" }] : []));
|
|
3118
|
+
uploadData = input({}, ...(ngDevMode ? [{ debugName: "uploadData" }] : []));
|
|
3119
|
+
// Modern output signals
|
|
3120
|
+
fileChange = output();
|
|
3121
|
+
uploadSuccess = output();
|
|
3122
|
+
uploadError = output();
|
|
3123
|
+
uploadProgressChange = output();
|
|
3124
|
+
// Reactive state with signals
|
|
3125
|
+
id = signal(Math.random().toString(36).substring(2, 10), ...(ngDevMode ? [{ debugName: "id" }] : []));
|
|
3126
|
+
isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
|
|
3127
|
+
uploadProgress = signal(0, ...(ngDevMode ? [{ debugName: "uploadProgress" }] : []));
|
|
3128
|
+
uploadStatus = signal('idle', ...(ngDevMode ? [{ debugName: "uploadStatus" }] : []));
|
|
3129
|
+
files = signal(null, ...(ngDevMode ? [{ debugName: "files" }] : []));
|
|
3130
|
+
fileNames = signal([], ...(ngDevMode ? [{ debugName: "fileNames" }] : []));
|
|
3131
|
+
previewUrls = signal([], ...(ngDevMode ? [{ debugName: "previewUrls" }] : []));
|
|
3132
|
+
uploadNotificationId = signal(null, ...(ngDevMode ? [{ debugName: "uploadNotificationId" }] : []));
|
|
3133
|
+
isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
|
|
3134
|
+
// Angular 20: Linked signals for better relationships
|
|
3135
|
+
hasFiles = linkedSignal(() => this.files() !== null && this.files().length > 0);
|
|
3136
|
+
canUpload = linkedSignal(() => this.hasFiles() && !this.isUploading() && !this.disabled());
|
|
3137
|
+
isInErrorState = linkedSignal(() => this.uploadStatus() === 'error');
|
|
3138
|
+
isInSuccessState = linkedSignal(() => this.uploadStatus() === 'success');
|
|
3139
|
+
// Angular 20: Computed values using new features
|
|
3140
|
+
totalFileSize = computed(() => {
|
|
3141
|
+
if (!this.files())
|
|
3142
|
+
return 0;
|
|
3143
|
+
return Array.from(this.files()).reduce((total, file) => total + file.size, 0);
|
|
3144
|
+
}, ...(ngDevMode ? [{ debugName: "totalFileSize" }] : []));
|
|
3145
|
+
fileSizeInMB = computed(() => {
|
|
3146
|
+
// Angular 20: Using ** operator for exponentiation
|
|
3147
|
+
return (this.totalFileSize() / (1024 ** 2)).toFixed(2);
|
|
3148
|
+
}, ...(ngDevMode ? [{ debugName: "fileSizeInMB" }] : []));
|
|
3149
|
+
uploadProgressPercentage = computed(() => {
|
|
3150
|
+
// Angular 20: Using ** operator for exponentiation
|
|
3151
|
+
return Math.round(this.uploadProgress() ** 1); // Simple power operation
|
|
3152
|
+
}, ...(ngDevMode ? [{ debugName: "uploadProgressPercentage" }] : []));
|
|
3153
|
+
// ControlValueAccessor callbacks
|
|
2910
3154
|
onChange = (files) => { };
|
|
2911
3155
|
onTouched = () => { };
|
|
2912
3156
|
onValidatorChange = () => { };
|
|
3157
|
+
// Computed values
|
|
3158
|
+
hasImages = computed(() => this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "hasImages" }] : []));
|
|
3159
|
+
isPreviewBoxMode = computed(() => this.previewBoxMode() && this.showPreview(), ...(ngDevMode ? [{ debugName: "isPreviewBoxMode" }] : []));
|
|
3160
|
+
isImagePreviewAvailable = computed(() => this.showPreview() && this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "isImagePreviewAvailable" }] : []));
|
|
3161
|
+
constructor() {
|
|
3162
|
+
// Angular 20: afterRenderEffect for DOM operations
|
|
3163
|
+
afterRenderEffect(() => {
|
|
3164
|
+
// Update file input element when files change
|
|
3165
|
+
if (this.files()) {
|
|
3166
|
+
const fileInput = document.getElementById('cide-file-input-' + this.id());
|
|
3167
|
+
if (fileInput) {
|
|
3168
|
+
// Ensure the input reflects the current state
|
|
3169
|
+
fileInput.files = this.files();
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
});
|
|
3173
|
+
// Angular 20: afterNextRender for one-time DOM operations
|
|
3174
|
+
afterNextRender(() => {
|
|
3175
|
+
console.log('🎯 [FileInput] Component rendered and DOM is ready');
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
2913
3178
|
writeValue(files) {
|
|
2914
3179
|
console.log('📝 [FileInput] writeValue called with:', files ? Array.from(files).map(f => f.name) : 'null');
|
|
2915
|
-
this.files
|
|
2916
|
-
this.fileNames
|
|
3180
|
+
this.files.set(files);
|
|
3181
|
+
this.fileNames.set(files ? Array.from(files).map(f => f.name) : []);
|
|
2917
3182
|
this.generatePreviews();
|
|
2918
3183
|
}
|
|
2919
3184
|
registerOnChange(fn) {
|
|
@@ -2926,26 +3191,29 @@ class CideEleFileInputComponent {
|
|
|
2926
3191
|
this.onValidatorChange = fn;
|
|
2927
3192
|
}
|
|
2928
3193
|
setDisabledState(isDisabled) {
|
|
2929
|
-
|
|
3194
|
+
// Note: With input signals, disabled state is controlled by the parent component
|
|
3195
|
+
// This method is kept for ControlValueAccessor compatibility but doesn't modify the signal
|
|
3196
|
+
console.log('🔧 [FileInput] setDisabledState called with:', isDisabled, '(controlled by parent component)');
|
|
2930
3197
|
}
|
|
2931
3198
|
onFileSelected(event) {
|
|
2932
3199
|
console.log('🔍 [FileInput] onFileSelected called');
|
|
2933
3200
|
const input = event.target;
|
|
2934
|
-
|
|
2935
|
-
this.
|
|
2936
|
-
|
|
3201
|
+
const selectedFiles = input.files;
|
|
3202
|
+
this.files.set(selectedFiles);
|
|
3203
|
+
this.fileNames.set(selectedFiles ? Array.from(selectedFiles).map(f => f.name) : []);
|
|
3204
|
+
console.log('📁 [FileInput] Files selected:', this.fileNames());
|
|
2937
3205
|
this.generatePreviews();
|
|
2938
3206
|
// Reset upload status when new file is selected
|
|
2939
|
-
this.uploadStatus
|
|
2940
|
-
console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus);
|
|
2941
|
-
this.onChange(
|
|
2942
|
-
this.fileChange.emit(
|
|
3207
|
+
this.uploadStatus.set('idle');
|
|
3208
|
+
console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
|
|
3209
|
+
this.onChange(selectedFiles);
|
|
3210
|
+
this.fileChange.emit(selectedFiles);
|
|
2943
3211
|
this.onTouched();
|
|
2944
3212
|
this.onValidatorChange();
|
|
2945
3213
|
// Auto upload if enabled
|
|
2946
|
-
if (this.autoUpload &&
|
|
2947
|
-
console.log('🚀 [FileInput] Auto upload enabled, starting upload for:',
|
|
2948
|
-
this.uploadFile(
|
|
3214
|
+
if (this.autoUpload() && selectedFiles && selectedFiles.length > 0) {
|
|
3215
|
+
console.log('🚀 [FileInput] Auto upload enabled, starting upload for:', selectedFiles[0].name);
|
|
3216
|
+
this.uploadFile(selectedFiles[0]);
|
|
2949
3217
|
}
|
|
2950
3218
|
else {
|
|
2951
3219
|
console.log('⏸️ [FileInput] Auto upload disabled or no files');
|
|
@@ -2953,80 +3221,107 @@ class CideEleFileInputComponent {
|
|
|
2953
3221
|
}
|
|
2954
3222
|
clearFiles() {
|
|
2955
3223
|
console.log('🗑️ [FileInput] clearFiles called');
|
|
2956
|
-
this.files
|
|
2957
|
-
this.fileNames
|
|
3224
|
+
this.files.set(null);
|
|
3225
|
+
this.fileNames.set([]);
|
|
2958
3226
|
this.clearPreviews();
|
|
2959
|
-
this.uploadStatus
|
|
2960
|
-
console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus);
|
|
3227
|
+
this.uploadStatus.set('idle');
|
|
3228
|
+
console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
|
|
2961
3229
|
this.onChange(null);
|
|
2962
3230
|
this.fileChange.emit(null);
|
|
2963
3231
|
this.onValidatorChange();
|
|
2964
3232
|
}
|
|
2965
3233
|
uploadFile(file) {
|
|
2966
3234
|
console.log('📤 [FileInput] uploadFile called for:', file.name, 'Size:', file.size, 'bytes');
|
|
3235
|
+
// Angular 20: Use PendingTasks for better loading state management
|
|
3236
|
+
// const uploadTask = this.pendingTasks.add(); // TODO: Fix PendingTasks API usage
|
|
3237
|
+
// console.log('⏳ [FileInput] Pending task added for upload tracking');
|
|
2967
3238
|
// Set upload status to 'start' before starting upload
|
|
2968
|
-
this.uploadStatus
|
|
2969
|
-
console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus);
|
|
3239
|
+
this.uploadStatus.set('start');
|
|
3240
|
+
console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
|
|
2970
3241
|
// Trigger validation update to show upload in progress error
|
|
2971
3242
|
this.onValidatorChange();
|
|
2972
3243
|
console.log('✅ [FileInput] Validation triggered - should show upload in progress error');
|
|
2973
|
-
this.isUploading
|
|
2974
|
-
this.uploadProgress
|
|
3244
|
+
this.isUploading.set(true);
|
|
3245
|
+
this.uploadProgress.set(0);
|
|
2975
3246
|
this.uploadProgressChange.emit(0);
|
|
2976
3247
|
console.log('📊 [FileInput] Upload progress initialized to 0%');
|
|
2977
3248
|
// Make form control invalid during upload - this prevents form submission
|
|
2978
3249
|
this.onChange(null);
|
|
2979
3250
|
console.log('🚫 [FileInput] Form control value set to null to prevent submission');
|
|
2980
|
-
// Show initial progress notification
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
3251
|
+
// Show initial progress notification with spinner (persistent - no auto-dismiss)
|
|
3252
|
+
const notificationId = this.notificationService.showProgress('🔄 Preparing file upload...', 0, { duration: 0 });
|
|
3253
|
+
this.uploadNotificationId.set(notificationId);
|
|
3254
|
+
console.log('🔔 [FileInput] Progress notification started with ID:', notificationId);
|
|
3255
|
+
this.fileManagerService.uploadFile(file, this.uploadData(), (progress) => {
|
|
2984
3256
|
// Real progress callback from file manager service
|
|
2985
|
-
this.uploadProgress
|
|
3257
|
+
this.uploadProgress.set(progress);
|
|
2986
3258
|
this.uploadProgressChange.emit(progress);
|
|
2987
3259
|
console.log('📈 [FileInput] Upload progress:', Math.round(progress) + '%');
|
|
2988
3260
|
// Set upload status to 'uploading' when progress starts
|
|
2989
|
-
if (this.uploadStatus === 'start') {
|
|
2990
|
-
this.uploadStatus
|
|
2991
|
-
console.log('🔄 [FileInput] Upload status changed to:', this.uploadStatus);
|
|
3261
|
+
if (this.uploadStatus() === 'start') {
|
|
3262
|
+
this.uploadStatus.set('uploading');
|
|
3263
|
+
console.log('🔄 [FileInput] Upload status changed to:', this.uploadStatus());
|
|
2992
3264
|
this.onValidatorChange();
|
|
2993
3265
|
console.log('✅ [FileInput] Validation triggered after status change to uploading');
|
|
2994
3266
|
}
|
|
2995
|
-
// Update progress notification
|
|
2996
|
-
|
|
2997
|
-
|
|
3267
|
+
// Update progress notification with spinner
|
|
3268
|
+
const notificationId = this.uploadNotificationId();
|
|
3269
|
+
if (notificationId) {
|
|
3270
|
+
let progressMessage = '';
|
|
3271
|
+
if (progress < 10) {
|
|
3272
|
+
progressMessage = '🔄 Starting upload...';
|
|
3273
|
+
}
|
|
3274
|
+
else if (progress < 25) {
|
|
3275
|
+
progressMessage = '🔄 Uploading file...';
|
|
3276
|
+
}
|
|
3277
|
+
else if (progress < 50) {
|
|
3278
|
+
progressMessage = '🔄 Upload in progress...';
|
|
3279
|
+
}
|
|
3280
|
+
else if (progress < 75) {
|
|
3281
|
+
progressMessage = '🔄 Almost done...';
|
|
3282
|
+
}
|
|
3283
|
+
else if (progress < 95) {
|
|
3284
|
+
progressMessage = '🔄 Finishing upload...';
|
|
3285
|
+
}
|
|
3286
|
+
else {
|
|
3287
|
+
progressMessage = '🔄 Finalizing...';
|
|
3288
|
+
}
|
|
3289
|
+
this.notificationService.updateProgress(notificationId, progress, progressMessage);
|
|
2998
3290
|
}
|
|
2999
|
-
}).subscribe({
|
|
3291
|
+
}).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
3000
3292
|
next: (response) => {
|
|
3001
3293
|
console.log('🎉 [FileInput] Upload SUCCESS - Response received:', response);
|
|
3294
|
+
// Angular 20: Complete the pending task
|
|
3295
|
+
// this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
|
|
3296
|
+
// console.log('✅ [FileInput] Pending task completed for successful upload');
|
|
3002
3297
|
// Set upload status to 'success'
|
|
3003
|
-
this.uploadStatus
|
|
3004
|
-
console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus);
|
|
3298
|
+
this.uploadStatus.set('success');
|
|
3299
|
+
console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
|
|
3005
3300
|
// Complete the progress
|
|
3006
|
-
this.uploadProgress
|
|
3301
|
+
this.uploadProgress.set(100);
|
|
3007
3302
|
this.uploadProgressChange.emit(100);
|
|
3008
3303
|
console.log('📊 [FileInput] Upload progress completed: 100%');
|
|
3009
3304
|
// Update progress notification to complete
|
|
3010
|
-
|
|
3011
|
-
|
|
3305
|
+
const notificationId = this.uploadNotificationId();
|
|
3306
|
+
if (notificationId) {
|
|
3307
|
+
this.notificationService.remove(notificationId);
|
|
3012
3308
|
console.log('🔔 [FileInput] Progress notification removed');
|
|
3013
3309
|
}
|
|
3014
|
-
this.notificationService.success('File
|
|
3015
|
-
this.uploadNotificationId
|
|
3016
|
-
// Extract ID from
|
|
3017
|
-
const uploadedId = response?.
|
|
3310
|
+
this.notificationService.success('✅ File uploaded successfully!', { duration: 0 });
|
|
3311
|
+
this.uploadNotificationId.set(null);
|
|
3312
|
+
// Extract ID from CoreFileManagerInsertUpdateResponse
|
|
3313
|
+
const uploadedId = response?.data?.core_file_manager?.[0]?.cyfm_id;
|
|
3018
3314
|
if (uploadedId) {
|
|
3019
3315
|
console.log('✅ [FileInput] File uploaded successfully with ID:', uploadedId);
|
|
3020
3316
|
this.uploadSuccess.emit(uploadedId);
|
|
3021
|
-
//
|
|
3022
|
-
|
|
3023
|
-
console.log('📝 [FileInput] Form control value updated with uploaded file ID');
|
|
3317
|
+
// Note: Form control value remains as FileList - uploaded ID is emitted separately
|
|
3318
|
+
console.log('📝 [FileInput] Upload success event emitted with file ID:', uploadedId);
|
|
3024
3319
|
}
|
|
3025
3320
|
else {
|
|
3026
3321
|
console.error('❌ [FileInput] Upload successful but no ID returned:', response);
|
|
3027
3322
|
this.uploadError.emit('Upload successful but no ID returned');
|
|
3028
3323
|
}
|
|
3029
|
-
this.isUploading
|
|
3324
|
+
this.isUploading.set(false);
|
|
3030
3325
|
console.log('🔄 [FileInput] isUploading set to false');
|
|
3031
3326
|
// Trigger validation update to clear upload in progress error
|
|
3032
3327
|
this.onValidatorChange();
|
|
@@ -3034,19 +3329,23 @@ class CideEleFileInputComponent {
|
|
|
3034
3329
|
},
|
|
3035
3330
|
error: (error) => {
|
|
3036
3331
|
console.error('💥 [FileInput] Upload FAILED:', error);
|
|
3332
|
+
// Angular 20: Complete the pending task even on error
|
|
3333
|
+
// this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
|
|
3334
|
+
// console.log('❌ [FileInput] Pending task completed for failed upload');
|
|
3037
3335
|
// Set upload status to 'error' and remove upload validation error
|
|
3038
|
-
this.uploadStatus
|
|
3039
|
-
console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus);
|
|
3336
|
+
this.uploadStatus.set('error');
|
|
3337
|
+
console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
|
|
3040
3338
|
// Remove progress notification and show error
|
|
3041
|
-
|
|
3042
|
-
|
|
3339
|
+
const notificationId = this.uploadNotificationId();
|
|
3340
|
+
if (notificationId) {
|
|
3341
|
+
this.notificationService.remove(notificationId);
|
|
3043
3342
|
console.log('🔔 [FileInput] Progress notification removed due to error');
|
|
3044
3343
|
}
|
|
3045
|
-
this.notificationService.error(
|
|
3046
|
-
this.uploadNotificationId
|
|
3344
|
+
this.notificationService.error(`❌ File upload failed: ${error.message || error.error?.message || 'Unknown error occurred'}`, { duration: 0 });
|
|
3345
|
+
this.uploadNotificationId.set(null);
|
|
3047
3346
|
this.uploadError.emit(error.message || error.error?.message || 'Upload failed');
|
|
3048
|
-
this.isUploading
|
|
3049
|
-
this.uploadProgress
|
|
3347
|
+
this.isUploading.set(false);
|
|
3348
|
+
this.uploadProgress.set(0);
|
|
3050
3349
|
this.uploadProgressChange.emit(0);
|
|
3051
3350
|
console.log('🔄 [FileInput] Upload state reset - isUploading: false, progress: 0%');
|
|
3052
3351
|
// Trigger validation update to clear upload in progress error
|
|
@@ -3058,15 +3357,15 @@ class CideEleFileInputComponent {
|
|
|
3058
3357
|
generatePreviews() {
|
|
3059
3358
|
// Clear existing previews
|
|
3060
3359
|
this.clearPreviews();
|
|
3061
|
-
if (!this.showPreview || !this.files) {
|
|
3360
|
+
if (!this.showPreview() || !this.files()) {
|
|
3062
3361
|
return;
|
|
3063
3362
|
}
|
|
3064
|
-
Array.from(this.files).forEach(file => {
|
|
3363
|
+
Array.from(this.files()).forEach(file => {
|
|
3065
3364
|
if (this.isImageFile(file)) {
|
|
3066
3365
|
const reader = new FileReader();
|
|
3067
3366
|
reader.onload = (e) => {
|
|
3068
3367
|
if (e.target?.result) {
|
|
3069
|
-
this.previewUrls.
|
|
3368
|
+
this.previewUrls.update(urls => [...urls, e.target.result]);
|
|
3070
3369
|
}
|
|
3071
3370
|
};
|
|
3072
3371
|
reader.readAsDataURL(file);
|
|
@@ -3075,131 +3374,223 @@ class CideEleFileInputComponent {
|
|
|
3075
3374
|
}
|
|
3076
3375
|
clearPreviews() {
|
|
3077
3376
|
// Revoke object URLs to prevent memory leaks
|
|
3078
|
-
this.previewUrls.forEach(url => {
|
|
3377
|
+
this.previewUrls().forEach(url => {
|
|
3079
3378
|
if (url.startsWith('blob:')) {
|
|
3080
3379
|
URL.revokeObjectURL(url);
|
|
3081
3380
|
}
|
|
3082
3381
|
});
|
|
3083
|
-
this.previewUrls
|
|
3382
|
+
this.previewUrls.set([]);
|
|
3084
3383
|
}
|
|
3085
3384
|
isImageFile(file) {
|
|
3086
3385
|
return file.type.startsWith('image/');
|
|
3087
3386
|
}
|
|
3088
|
-
isImagePreviewAvailable() {
|
|
3089
|
-
return this.showPreview && this.previewUrls.length > 0;
|
|
3090
|
-
}
|
|
3091
3387
|
removePreview(index) {
|
|
3092
|
-
|
|
3388
|
+
const currentFiles = this.files();
|
|
3389
|
+
if (currentFiles && currentFiles.length > index) {
|
|
3093
3390
|
// Create new FileList without the removed file
|
|
3094
3391
|
const dt = new DataTransfer();
|
|
3095
|
-
Array.from(
|
|
3392
|
+
Array.from(currentFiles).forEach((file, i) => {
|
|
3096
3393
|
if (i !== index) {
|
|
3097
3394
|
dt.items.add(file);
|
|
3098
3395
|
}
|
|
3099
3396
|
});
|
|
3100
|
-
|
|
3101
|
-
this.
|
|
3397
|
+
const newFiles = dt.files;
|
|
3398
|
+
this.files.set(newFiles);
|
|
3399
|
+
this.fileNames.set(Array.from(newFiles).map(f => f.name));
|
|
3102
3400
|
// Remove the preview URL
|
|
3103
|
-
|
|
3104
|
-
|
|
3401
|
+
const currentUrls = this.previewUrls();
|
|
3402
|
+
if (currentUrls[index] && currentUrls[index].startsWith('blob:')) {
|
|
3403
|
+
URL.revokeObjectURL(currentUrls[index]);
|
|
3105
3404
|
}
|
|
3106
|
-
this.previewUrls.
|
|
3107
|
-
this.onChange(
|
|
3108
|
-
this.fileChange.emit(
|
|
3405
|
+
this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
|
|
3406
|
+
this.onChange(newFiles);
|
|
3407
|
+
this.fileChange.emit(newFiles);
|
|
3109
3408
|
}
|
|
3110
3409
|
}
|
|
3111
3410
|
ngOnDestroy() {
|
|
3112
3411
|
// Clean up preview URLs to prevent memory leaks
|
|
3113
3412
|
this.clearPreviews();
|
|
3114
3413
|
// Clean up any active upload notification
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
this.
|
|
3414
|
+
const notificationId = this.uploadNotificationId();
|
|
3415
|
+
if (notificationId) {
|
|
3416
|
+
this.notificationService.remove(notificationId);
|
|
3417
|
+
this.uploadNotificationId.set(null);
|
|
3118
3418
|
}
|
|
3119
3419
|
}
|
|
3120
3420
|
triggerFileSelect() {
|
|
3121
|
-
const fileInput = document.getElementById('cide-file-input-' + this.id);
|
|
3122
|
-
if (fileInput && !this.disabled) {
|
|
3421
|
+
const fileInput = document.getElementById('cide-file-input-' + this.id());
|
|
3422
|
+
if (fileInput && !this.disabled()) {
|
|
3123
3423
|
fileInput.click();
|
|
3124
3424
|
}
|
|
3125
3425
|
}
|
|
3126
|
-
|
|
3127
|
-
|
|
3426
|
+
// Drag and Drop Event Handlers
|
|
3427
|
+
onDragOver(event) {
|
|
3428
|
+
event.preventDefault();
|
|
3429
|
+
event.stopPropagation();
|
|
3430
|
+
if (!this.disabled()) {
|
|
3431
|
+
this.isDragOver.set(true);
|
|
3432
|
+
console.log('🔄 [FileInput] Drag over detected');
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
onDragLeave(event) {
|
|
3436
|
+
event.preventDefault();
|
|
3437
|
+
event.stopPropagation();
|
|
3438
|
+
this.isDragOver.set(false);
|
|
3439
|
+
console.log('🔄 [FileInput] Drag leave detected');
|
|
3128
3440
|
}
|
|
3129
|
-
|
|
3130
|
-
|
|
3441
|
+
onDragEnter(event) {
|
|
3442
|
+
event.preventDefault();
|
|
3443
|
+
event.stopPropagation();
|
|
3444
|
+
if (!this.disabled()) {
|
|
3445
|
+
this.isDragOver.set(true);
|
|
3446
|
+
console.log('🔄 [FileInput] Drag enter detected');
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
onDrop(event) {
|
|
3450
|
+
event.preventDefault();
|
|
3451
|
+
event.stopPropagation();
|
|
3452
|
+
this.isDragOver.set(false);
|
|
3453
|
+
if (this.disabled()) {
|
|
3454
|
+
console.log('⏸️ [FileInput] Drop ignored - component is disabled');
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
const files = event.dataTransfer?.files;
|
|
3458
|
+
if (files && files.length > 0) {
|
|
3459
|
+
console.log('📁 [FileInput] Files dropped:', Array.from(files).map(f => f.name));
|
|
3460
|
+
// Validate file types if accept is specified
|
|
3461
|
+
if (this.accept() && !this.validateFileTypes(files)) {
|
|
3462
|
+
console.log('❌ [FileInput] Invalid file types dropped');
|
|
3463
|
+
this.notificationService.error('❌ Invalid file type. Please select files of the correct type.', { duration: 0 });
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
// Handle single vs multiple files
|
|
3467
|
+
if (!this.multiple() && files.length > 1) {
|
|
3468
|
+
console.log('⚠️ [FileInput] Multiple files dropped but multiple is disabled');
|
|
3469
|
+
this.notificationService.warning('⚠️ Only one file is allowed. Using the first file.', { duration: 0 });
|
|
3470
|
+
// Create a new FileList with only the first file
|
|
3471
|
+
const dt = new DataTransfer();
|
|
3472
|
+
dt.items.add(files[0]);
|
|
3473
|
+
this.handleFileSelection(dt.files);
|
|
3474
|
+
}
|
|
3475
|
+
else {
|
|
3476
|
+
this.handleFileSelection(files);
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
validateFileTypes(files) {
|
|
3481
|
+
const acceptTypes = this.accept().split(',').map(type => type.trim());
|
|
3482
|
+
if (acceptTypes.length === 0 || acceptTypes[0] === '')
|
|
3483
|
+
return true;
|
|
3484
|
+
return Array.from(files).every(file => {
|
|
3485
|
+
return acceptTypes.some(acceptType => {
|
|
3486
|
+
if (acceptType.startsWith('.')) {
|
|
3487
|
+
// Extension-based validation
|
|
3488
|
+
return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
|
|
3489
|
+
}
|
|
3490
|
+
else if (acceptType.includes('/')) {
|
|
3491
|
+
// MIME type validation
|
|
3492
|
+
return file.type === acceptType || file.type.startsWith(acceptType.replace('*', ''));
|
|
3493
|
+
}
|
|
3494
|
+
return false;
|
|
3495
|
+
});
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
handleFileSelection(files) {
|
|
3499
|
+
this.files.set(files);
|
|
3500
|
+
this.fileNames.set(Array.from(files).map(f => f.name));
|
|
3501
|
+
console.log('📁 [FileInput] Files selected via drag & drop:', this.fileNames());
|
|
3502
|
+
this.generatePreviews();
|
|
3503
|
+
// Reset upload status when new file is selected
|
|
3504
|
+
this.uploadStatus.set('idle');
|
|
3505
|
+
console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
|
|
3506
|
+
this.onChange(files);
|
|
3507
|
+
this.fileChange.emit(files);
|
|
3508
|
+
this.onTouched();
|
|
3509
|
+
this.onValidatorChange();
|
|
3510
|
+
// Auto upload if enabled
|
|
3511
|
+
if (this.autoUpload() && files.length > 0) {
|
|
3512
|
+
console.log('🚀 [FileInput] Auto upload enabled, starting upload for:', files[0].name);
|
|
3513
|
+
this.uploadFile(files[0]);
|
|
3514
|
+
}
|
|
3515
|
+
else {
|
|
3516
|
+
console.log('⏸️ [FileInput] Auto upload disabled or no files');
|
|
3517
|
+
}
|
|
3131
3518
|
}
|
|
3132
3519
|
isRequired() {
|
|
3133
|
-
return this.required;
|
|
3520
|
+
return this.required();
|
|
3521
|
+
}
|
|
3522
|
+
/**
|
|
3523
|
+
* Angular 20: Utility method to get upload data with proper typing
|
|
3524
|
+
* @returns Properly typed upload data
|
|
3525
|
+
*/
|
|
3526
|
+
getUploadData() {
|
|
3527
|
+
return this.uploadData();
|
|
3528
|
+
}
|
|
3529
|
+
/**
|
|
3530
|
+
* Angular 20: Utility method to update upload data with type safety
|
|
3531
|
+
* @param data Partial upload data to merge with existing data
|
|
3532
|
+
*/
|
|
3533
|
+
updateUploadData(data) {
|
|
3534
|
+
const currentData = this.uploadData();
|
|
3535
|
+
const updatedData = { ...currentData, ...data };
|
|
3536
|
+
// Note: This would require the uploadData to be a writable signal
|
|
3537
|
+
// For now, this method serves as a type-safe way to work with upload data
|
|
3538
|
+
console.log('📝 [FileInput] Upload data updated:', updatedData);
|
|
3134
3539
|
}
|
|
3135
3540
|
getCurrentState() {
|
|
3136
3541
|
return {
|
|
3137
|
-
id: this.id,
|
|
3138
|
-
label: this.label,
|
|
3139
|
-
required: this.required,
|
|
3140
|
-
disabled: this.disabled,
|
|
3141
|
-
accept: this.accept,
|
|
3142
|
-
multiple: this.multiple,
|
|
3143
|
-
showPreview: this.showPreview,
|
|
3144
|
-
autoUpload: this.autoUpload,
|
|
3145
|
-
uploadStatus: this.uploadStatus,
|
|
3146
|
-
isUploading: this.isUploading,
|
|
3147
|
-
uploadProgress: this.uploadProgress,
|
|
3148
|
-
files: this.files ? Array.from(this.files).map(f => ({ name: f.name, size: f.size, type: f.type })) : null,
|
|
3149
|
-
fileNames: this.fileNames,
|
|
3150
|
-
previewUrls: this.previewUrls.length,
|
|
3151
|
-
helperText: this.helperText,
|
|
3152
|
-
errorText: this.errorText,
|
|
3153
|
-
placeholderText: this.placeholderText,
|
|
3154
|
-
placeholderIcon: this.placeholderIcon,
|
|
3155
|
-
previewWidth: this.previewWidth,
|
|
3156
|
-
previewHeight: this.previewHeight,
|
|
3157
|
-
previewBoxMode: this.previewBoxMode,
|
|
3158
|
-
showFileName: this.showFileName,
|
|
3159
|
-
uploadData: this.uploadData
|
|
3542
|
+
id: this.id(),
|
|
3543
|
+
label: this.label(),
|
|
3544
|
+
required: this.required(),
|
|
3545
|
+
disabled: this.disabled(),
|
|
3546
|
+
accept: this.accept(),
|
|
3547
|
+
multiple: this.multiple(),
|
|
3548
|
+
showPreview: this.showPreview(),
|
|
3549
|
+
autoUpload: this.autoUpload(),
|
|
3550
|
+
uploadStatus: this.uploadStatus(),
|
|
3551
|
+
isUploading: this.isUploading(),
|
|
3552
|
+
uploadProgress: this.uploadProgress(),
|
|
3553
|
+
files: this.files() ? Array.from(this.files()).map(f => ({ name: f.name, size: f.size, type: f.type })) : null,
|
|
3554
|
+
fileNames: this.fileNames(),
|
|
3555
|
+
previewUrls: this.previewUrls().length,
|
|
3556
|
+
helperText: this.helperText(),
|
|
3557
|
+
errorText: this.errorText(),
|
|
3558
|
+
placeholderText: this.placeholderText(),
|
|
3559
|
+
placeholderIcon: this.placeholderIcon(),
|
|
3560
|
+
previewWidth: this.previewWidth(),
|
|
3561
|
+
previewHeight: this.previewHeight(),
|
|
3562
|
+
previewBoxMode: this.previewBoxMode(),
|
|
3563
|
+
showFileName: this.showFileName(),
|
|
3564
|
+
uploadData: this.uploadData()
|
|
3160
3565
|
};
|
|
3161
3566
|
}
|
|
3162
3567
|
async getControlData() {
|
|
3163
3568
|
console.log('🔍 [FileInput] getControlData called');
|
|
3164
|
-
const cide_element_data = await this.elementService?.getElementData({ sype_key: this.id });
|
|
3569
|
+
const cide_element_data = await this.elementService?.getElementData({ sype_key: this.id() });
|
|
3165
3570
|
if (cide_element_data) {
|
|
3166
3571
|
console.log('📋 [FileInput] Element data loaded:', cide_element_data);
|
|
3167
|
-
//
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
const data = cide_element_data?.data;
|
|
3173
|
-
if (data) {
|
|
3174
|
-
this.accept = data.accept || '';
|
|
3175
|
-
this.multiple = data.multiple || false;
|
|
3176
|
-
this.showPreview = data.showPreview || false;
|
|
3177
|
-
this.autoUpload = data.autoUpload || false;
|
|
3178
|
-
this.placeholderIcon = data.placeholderIcon || '📷';
|
|
3179
|
-
this.previewWidth = data.previewWidth || '200px';
|
|
3180
|
-
this.previewHeight = data.previewHeight || '200px';
|
|
3181
|
-
this.previewBoxMode = data.previewBoxMode || false;
|
|
3182
|
-
this.showFileName = data.showFileName || true;
|
|
3183
|
-
this.uploadData = data.uploadData || {};
|
|
3184
|
-
}
|
|
3185
|
-
console.log('✅ [FileInput] Control data updated from element service');
|
|
3572
|
+
// Note: Since we're using input signals, we can't directly set their values
|
|
3573
|
+
// This method would need to be refactored to work with the new signal-based approach
|
|
3574
|
+
// For now, we'll log the data and trigger validation
|
|
3575
|
+
console.log('✅ [FileInput] Control data received from element service');
|
|
3576
|
+
console.log('⚠️ [FileInput] Note: Input signals cannot be modified after component initialization');
|
|
3186
3577
|
// Trigger validation update
|
|
3187
3578
|
this.onValidatorChange();
|
|
3188
3579
|
}
|
|
3189
3580
|
else {
|
|
3190
|
-
console.log('⚠️ [FileInput] No element data found for key:', this.id);
|
|
3581
|
+
console.log('⚠️ [FileInput] No element data found for key:', this.id());
|
|
3191
3582
|
}
|
|
3192
3583
|
}
|
|
3193
3584
|
// Validator implementation
|
|
3194
3585
|
validate(control) {
|
|
3195
|
-
console.log('🔍 [FileInput] validate() called - uploadStatus:', this.uploadStatus, 'required:', this.required, 'files:', !!this.files, 'control.value:', control.value);
|
|
3586
|
+
console.log('🔍 [FileInput] validate() called - uploadStatus:', this.uploadStatus(), 'required:', this.required(), 'files:', !!this.files(), 'control.value:', control.value);
|
|
3196
3587
|
// If upload is in progress (start or uploading status), return validation error
|
|
3197
|
-
if (this.uploadStatus === 'start' || this.uploadStatus === 'uploading') {
|
|
3588
|
+
if (this.uploadStatus() === 'start' || this.uploadStatus() === 'uploading') {
|
|
3198
3589
|
console.log('⚠️ [FileInput] Validation ERROR: Upload in progress');
|
|
3199
3590
|
return { 'uploadInProgress': { message: 'File upload in progress. Please wait...' } };
|
|
3200
3591
|
}
|
|
3201
3592
|
// If required and no file is selected and no control value (uploaded file ID), return validation error
|
|
3202
|
-
if (this.required && !this.files && !control.value) {
|
|
3593
|
+
if (this.required() && !this.files() && !control.value) {
|
|
3203
3594
|
console.log('⚠️ [FileInput] Validation ERROR: File required');
|
|
3204
3595
|
return { 'required': { message: 'Please select a file to upload.' } };
|
|
3205
3596
|
}
|
|
@@ -3207,74 +3598,34 @@ class CideEleFileInputComponent {
|
|
|
3207
3598
|
return null; // No validation errors
|
|
3208
3599
|
}
|
|
3209
3600
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3210
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "
|
|
3601
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: CideEleFileInputComponent, isStandalone: true, selector: "cide-ele-file-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, errorText: { classPropertyName: "errorText", publicName: "errorText", isSignal: true, isRequired: false, transformFunction: null }, showPreview: { classPropertyName: "showPreview", publicName: "showPreview", isSignal: true, isRequired: false, transformFunction: null }, previewWidth: { classPropertyName: "previewWidth", publicName: "previewWidth", isSignal: true, isRequired: false, transformFunction: null }, previewHeight: { classPropertyName: "previewHeight", publicName: "previewHeight", isSignal: true, isRequired: false, transformFunction: null }, previewBoxMode: { classPropertyName: "previewBoxMode", publicName: "previewBoxMode", isSignal: true, isRequired: false, transformFunction: null }, showFileName: { classPropertyName: "showFileName", publicName: "showFileName", isSignal: true, isRequired: false, transformFunction: null }, placeholderText: { classPropertyName: "placeholderText", publicName: "placeholderText", isSignal: true, isRequired: false, transformFunction: null }, placeholderIcon: { classPropertyName: "placeholderIcon", publicName: "placeholderIcon", isSignal: true, isRequired: false, transformFunction: null }, autoUpload: { classPropertyName: "autoUpload", publicName: "autoUpload", isSignal: true, isRequired: false, transformFunction: null }, uploadData: { classPropertyName: "uploadData", publicName: "uploadData", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileChange: "fileChange", uploadSuccess: "uploadSuccess", uploadError: "uploadError", uploadProgressChange: "uploadProgressChange" }, providers: [
|
|
3211
3602
|
{
|
|
3212
3603
|
provide: NG_VALUE_ACCESSOR,
|
|
3213
|
-
useExisting:
|
|
3604
|
+
useExisting: CideEleFileInputComponent,
|
|
3214
3605
|
multi: true
|
|
3215
3606
|
},
|
|
3216
3607
|
{
|
|
3217
3608
|
provide: NG_VALIDATORS,
|
|
3218
|
-
useExisting:
|
|
3609
|
+
useExisting: CideEleFileInputComponent,
|
|
3219
3610
|
multi: true
|
|
3220
3611
|
}
|
|
3221
|
-
], ngImport: i0, template: "<div class=\"cide-file-input\">\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\n <label *ngIf=\"label && !isPreviewBoxMode()\" class=\"cide-file-input-label\" [attr.for]=\"'cide-file-input-' + id\">\n {{ label }}<span *ngIf=\"required\" class=\"cide-file-input-required\"> *</span>\n </label>\n \n <!-- Preview Box Mode -->\n <div *ngIf=\"isPreviewBoxMode()\" class=\"cide-file-input-preview-box-container\">\n <!-- Hidden file input -->\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id\"\n [attr.accept]=\"accept\"\n [attr.multiple]=\"multiple ? true : null\"\n [disabled]=\"disabled\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"cide-file-input-preview-box\"\n [class.cide-file-input-preview-box-disabled]=\"disabled\"\n [class.cide-file-input-preview-box-has-image]=\"hasImages()\"\n [style.width]=\"previewWidth\"\n [style.height]=\"previewHeight\"\n (click)=\"triggerFileSelect()\"\n [attr.title]=\"disabled ? 'File selection disabled' : placeholderText\">\n \n <!-- No Image State -->\n <div *ngIf=\"!hasImages()\" class=\"cide-file-input-preview-box-placeholder\">\n <div class=\"cide-file-input-preview-box-icon\">\n <cide-ele-icon>{{ placeholderIcon }}</cide-ele-icon>\n </div>\n <div class=\"cide-file-input-preview-box-text\">{{ placeholderText }}</div>\n </div>\n \n <!-- Image Preview State -->\n <div *ngIf=\"hasImages()\" class=\"cide-file-input-preview-box-content\">\n <img \n [src]=\"previewUrls[0]\" \n [alt]=\"fileNames[0] || 'Preview image'\"\n class=\"cide-file-input-preview-box-image\">\n <div class=\"cide-file-input-preview-box-overlay\">\n <div class=\"cide-file-input-preview-box-overlay-text\">Click to change</div>\n </div>\n <button \n *ngIf=\"!disabled\"\n type=\"button\" \n class=\"cide-file-input-preview-box-remove\"\n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Remove image\">\n \u00D7\n </button>\n </div>\n </div>\n \n <!-- File name display for preview box mode -->\n <div *ngIf=\"hasImages() && fileNames.length && showFileName\" class=\"cide-file-input-preview-box-filename\">\n {{ fileNames[0] }}\n </div>\n </div>\n\n <!-- Standard Mode -->\n <div *ngIf=\"!isPreviewBoxMode()\" class=\"cide-file-input-wrapper\">\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id\"\n [attr.accept]=\"accept\"\n [attr.multiple]=\"multiple ? true : null\"\n [disabled]=\"disabled\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-element\"\n />\n <button *ngIf=\"fileNames.length\" type=\"button\" class=\"cide-file-input-clear\" (click)=\"clearFiles()\">\n Clear\n </button>\n </div>\n <div *ngIf=\"fileNames.length && !isPreviewBoxMode()\" class=\"cide-file-input-files\">\n <span *ngFor=\"let name of fileNames\">{{ name }}</span>\n </div>\n \n <!-- Image Preview Section (only for standard mode) -->\n <div *ngIf=\"isImagePreviewAvailable() && !isPreviewBoxMode()\" class=\"cide-file-input-preview\">\n <div class=\"cide-file-input-preview-label\">Preview:</div>\n <div class=\"cide-file-input-preview-container\">\n <div \n *ngFor=\"let previewUrl of previewUrls; let i = index\" \n class=\"cide-file-input-preview-item\"\n [style.width]=\"previewWidth\"\n [style.height]=\"previewHeight\">\n <button \n type=\"button\" \n class=\"cide-file-input-preview-remove\"\n (click)=\"removePreview(i)\"\n title=\"Remove image\">\n \u00D7\n </button>\n <img \n [src]=\"previewUrl\" \n [alt]=\"fileNames[i] || 'Preview image'\"\n class=\"cide-file-input-preview-image\"\n loading=\"lazy\">\n <div class=\"cide-file-input-preview-filename\">{{ fileNames[i] }}</div>\n </div>\n </div>\n </div>\n \n <div *ngIf=\"errorText\" class=\"cide-file-input-error\">{{ errorText }}</div>\n <div *ngIf=\"helperText && !errorText\" class=\"cide-file-input-helper\">{{ helperText }}</div>\n</div> ", styles: [".cide-file-input{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-label{font-weight:500;margin-bottom:.25rem}.cide-file-input-required{color:#d32f2f;font-weight:700}.cide-file-input-wrapper{display:flex;align-items:center;gap:.5rem}.cide-file-input-element{flex:1}.cide-file-input-clear{background:none;border:none;color:#d32f2f;cursor:pointer;font-size:.9rem}.cide-file-input-files{font-size:.95rem;color:#333;margin-top:.25rem}.cide-file-input-error{color:#d32f2f;font-size:.9rem}.cide-file-input-helper{color:#666;font-size:.9rem}.cide-file-input-preview{margin-top:.75rem;padding:.75rem;background-color:#f8f9fa;border:1px solid #e1e5e9;border-radius:.375rem}.cide-file-input-preview-label{font-weight:500;margin-bottom:.5rem;color:#374151;font-size:.875rem}.cide-file-input-preview-container{display:flex;flex-wrap:wrap;gap:.75rem}.cide-file-input-preview-item{position:relative;display:flex;flex-direction:column;border:1px solid #d1d5db;border-radius:.5rem;overflow:hidden;background-color:#fff;box-shadow:0 1px 3px #0000001a;transition:box-shadow .2s ease-in-out}.cide-file-input-preview-item:hover{box-shadow:0 4px 6px -1px #0000001a}.cide-file-input-preview-image{width:100%;height:calc(100% - 2rem);object-fit:cover;object-position:center;background-color:#f3f4f6}.cide-file-input-preview-filename{padding:.375rem .5rem;background-color:#f9fafbf2;border-top:1px solid #e5e7eb;font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:2rem;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-remove{position:absolute;top:.25rem;right:.25rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;transition:all .2s ease-in-out}.cide-file-input-preview-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-hidden{display:none}.cide-file-input-preview-box-container{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-preview-box{position:relative;border:2px dashed #d1d5db;border-radius:.5rem;cursor:pointer;background-color:#f9fafb;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:all .2s ease-in-out}.cide-file-input-preview-box:hover{border-color:#3b82f6;background-color:#eff6ff}.cide-file-input-preview-box.cide-file-input-preview-box-disabled{cursor:not-allowed;opacity:.6;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-disabled:hover{border-color:#d1d5db;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image{border-style:solid;border-color:#e5e7eb;padding:0}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover{border-color:#3b82f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover .cide-file-input-preview-box-overlay{opacity:1}.cide-file-input-preview-box-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1rem;text-align:center}.cide-file-input-preview-box-icon{font-size:2rem;color:#6b7280}.cide-file-input-preview-box-text{font-size:.875rem;color:#6b7280;font-weight:500}.cide-file-input-preview-box-content{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-box-image{width:100%;height:100%;object-fit:cover;object-position:center}.cide-file-input-preview-box-overlay{position:absolute;inset:0;background-color:#0009;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s ease-in-out}.cide-file-input-preview-box-overlay-text{color:#fff;font-size:.875rem;font-weight:500;text-align:center}.cide-file-input-preview-box-remove{position:absolute;top:.375rem;right:.375rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:20;transition:all .2s ease-in-out}.cide-file-input-preview-box-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-box-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-preview-box-filename{font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;margin-top:.25rem}@media (max-width: 640px){.cide-file-input-preview-container{justify-content:center}.cide-file-input-preview-item{min-width:120px;max-width:150px}.cide-file-input-preview-box-icon{font-size:1.5rem}.cide-file-input-preview-box-text{font-size:.75rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
|
|
3612
|
+
], ngImport: i0, template: "<div class=\"cide-file-input\">\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\n @if (label() && !isPreviewBoxMode()) {\n <label class=\"cide-file-input-label\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ label() }}@if (required()) {<span class=\"cide-file-input-required\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview-box-container\">\n <!-- Hidden file input -->\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"accept()\"\n [attr.multiple]=\"multiple() ? true : null\"\n [disabled]=\"disabled()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"cide-file-input-preview-box\"\n [class.cide-file-input-preview-box-disabled]=\"disabled()\"\n [class.cide-file-input-preview-box-has-image]=\"hasImages()\"\n [class.cide-file-input-preview-box-drag-over]=\"isDragOver()\"\n [style.width]=\"previewWidth()\"\n [style.height]=\"previewHeight()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n [attr.title]=\"disabled() ? 'File selection disabled' : placeholderText()\">\n \n <!-- No Image State -->\n @if (!hasImages()) {\n <div class=\"cide-file-input-preview-box-placeholder\">\n <div class=\"cide-file-input-preview-box-icon\">\n <cide-ele-icon>{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIcon() }}</cide-ele-icon>\n </div>\n <div class=\"cide-file-input-preview-box-text\">\n {{ isDragOver() ? 'Drop files here...' : placeholderText() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"cide-file-input-preview-box-content\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"cide-file-input-preview-box-image\">\n <div class=\"cide-file-input-preview-box-overlay\">\n <div class=\"cide-file-input-preview-box-overlay-text\">Click to change</div>\n </div>\n @if (!disabled()) {\n <button \n type=\"button\" \n class=\"cide-file-input-preview-box-remove\"\n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Remove image\">\n \u00D7\n </button>\n }\n </div>\n }\n </div>\n \n <!-- File name display for preview box mode -->\n @if (hasImages() && fileNames().length && showFileName()) {\n <div class=\"cide-file-input-preview-box-filename\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\n <div \n class=\"cide-file-input-wrapper\"\n [class.cide-file-input-drag-over]=\"isDragOver()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"accept()\"\n [attr.multiple]=\"multiple() ? true : null\"\n [disabled]=\"disabled()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-element\"\n />\n @if (hasFiles()) {\n <button type=\"button\" class=\"cide-file-input-clear\" (click)=\"clearFiles()\">\n Clear\n </button>\n }\n </div>\n @if (hasFiles() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-files\">\n @for (name of fileNames(); track name) {\n <span>{{ name }}</span>\n }\n <!-- Angular 20: Display file size using new computed values -->\n @if (totalFileSize() > 0) {\n <div class=\"cide-file-input-size\">\n Total size: {{ fileSizeInMB() }} MB\n </div>\n }\n </div>\n }\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview\">\n <div class=\"cide-file-input-preview-label\">Preview:</div>\n <div class=\"cide-file-input-preview-container\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"cide-file-input-preview-item\"\n [style.width]=\"previewWidth()\"\n [style.height]=\"previewHeight()\">\n <button \n type=\"button\" \n class=\"cide-file-input-preview-remove\"\n (click)=\"removePreview(i)\"\n title=\"Remove image\">\n \u00D7\n </button>\n <img \n [src]=\"previewUrl\" \n [alt]=\"fileNames()[i] || 'Preview image'\"\n class=\"cide-file-input-preview-image\"\n loading=\"lazy\">\n <div class=\"cide-file-input-preview-filename\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n @if (errorText()) {\n <div class=\"cide-file-input-error\">{{ errorText() }}</div>\n }\n @if (helperText() && !errorText()) {\n <div class=\"cide-file-input-helper\">{{ helperText() }}</div>\n }\n</div> ", styles: [".cide-file-input{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-label{font-weight:500;margin-bottom:.25rem}.cide-file-input-required{color:#d32f2f;font-weight:700}.cide-file-input-wrapper{display:flex;align-items:center;gap:.5rem;border:2px dashed transparent;border-radius:.5rem;padding:.5rem;transition:all .2s ease-in-out}.cide-file-input-wrapper.cide-file-input-drag-over{border-color:#3b82f6;background-color:#eff6ff;transform:scale(1.02)}.cide-file-input-element{flex:1}.cide-file-input-clear{background:none;border:none;color:#d32f2f;cursor:pointer;font-size:.9rem}.cide-file-input-files{font-size:.95rem;color:#333;margin-top:.25rem}.cide-file-input-size{margin-top:.5rem;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;font-size:.75rem;color:#4b5563;font-weight:500}.cide-file-input-error{color:#d32f2f;font-size:.9rem}.cide-file-input-helper{color:#666;font-size:.9rem}.cide-file-input-preview{margin-top:.75rem;padding:.75rem;background-color:#f8f9fa;border:1px solid #e1e5e9;border-radius:.375rem}.cide-file-input-preview-label{font-weight:500;margin-bottom:.5rem;color:#374151;font-size:.875rem}.cide-file-input-preview-container{display:flex;flex-wrap:wrap;gap:.75rem}.cide-file-input-preview-item{position:relative;display:flex;flex-direction:column;border:1px solid #d1d5db;border-radius:.5rem;overflow:hidden;background-color:#fff;box-shadow:0 1px 3px #0000001a;transition:box-shadow .2s ease-in-out}.cide-file-input-preview-item:hover{box-shadow:0 4px 6px -1px #0000001a}.cide-file-input-preview-image{width:100%;height:calc(100% - 2rem);object-fit:cover;object-position:center;background-color:#f3f4f6}.cide-file-input-preview-filename{padding:.375rem .5rem;background-color:#f9fafbf2;border-top:1px solid #e5e7eb;font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:2rem;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-remove{position:absolute;top:.25rem;right:.25rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;transition:all .2s ease-in-out}.cide-file-input-preview-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-hidden{display:none}.cide-file-input-preview-box-container{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-preview-box{position:relative;border:2px dashed #d1d5db;border-radius:.5rem;cursor:pointer;background-color:#f9fafb;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:all .2s ease-in-out}.cide-file-input-preview-box:hover{border-color:#3b82f6;background-color:#eff6ff}.cide-file-input-preview-box.cide-file-input-preview-box-disabled{cursor:not-allowed;opacity:.6;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-disabled:hover{border-color:#d1d5db;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image{border-style:solid;border-color:#e5e7eb;padding:0}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover{border-color:#3b82f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover .cide-file-input-preview-box-overlay{opacity:1}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over{border-color:#3b82f6!important;background-color:#eff6ff!important;transform:scale(1.02);box-shadow:0 0 0 4px #3b82f61a}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-icon{color:#3b82f6;transform:scale(1.1)}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-text{color:#3b82f6;font-weight:600}.cide-file-input-preview-box-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1rem;text-align:center}.cide-file-input-preview-box-icon{font-size:2rem;color:#6b7280}.cide-file-input-preview-box-text{font-size:.875rem;color:#6b7280;font-weight:500}.cide-file-input-preview-box-content{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-box-image{width:100%;height:100%;object-fit:cover;object-position:center}.cide-file-input-preview-box-overlay{position:absolute;inset:0;background-color:#0009;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s ease-in-out}.cide-file-input-preview-box-overlay-text{color:#fff;font-size:.875rem;font-weight:500;text-align:center}.cide-file-input-preview-box-remove{position:absolute;top:.375rem;right:.375rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:20;transition:all .2s ease-in-out}.cide-file-input-preview-box-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-box-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-preview-box-filename{font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;margin-top:.25rem}@media (max-width: 640px){.cide-file-input-preview-container{justify-content:center}.cide-file-input-preview-item{min-width:120px;max-width:150px}.cide-file-input-preview-box-icon{font-size:1.5rem}.cide-file-input-preview-box-text{font-size:.75rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
|
|
3222
3613
|
}
|
|
3223
3614
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, decorators: [{
|
|
3224
3615
|
type: Component,
|
|
3225
3616
|
args: [{ selector: 'cide-ele-file-input', standalone: true, imports: [CommonModule, FormsModule, CideIconComponent], providers: [
|
|
3226
3617
|
{
|
|
3227
3618
|
provide: NG_VALUE_ACCESSOR,
|
|
3228
|
-
useExisting:
|
|
3619
|
+
useExisting: CideEleFileInputComponent,
|
|
3229
3620
|
multi: true
|
|
3230
3621
|
},
|
|
3231
3622
|
{
|
|
3232
3623
|
provide: NG_VALIDATORS,
|
|
3233
|
-
useExisting:
|
|
3624
|
+
useExisting: CideEleFileInputComponent,
|
|
3234
3625
|
multi: true
|
|
3235
3626
|
}
|
|
3236
|
-
], template: "<div class=\"cide-file-input\">\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\n <label *ngIf=\"label && !isPreviewBoxMode()\" class=\"cide-file-input-label\" [attr.for]=\"'cide-file-input-' + id\">\n {{ label }}<span *ngIf=\"required\" class=\"cide-file-input-required\"> *</span>\n </label>\n \n <!-- Preview Box Mode -->\n <div *ngIf=\"isPreviewBoxMode()\" class=\"cide-file-input-preview-box-container\">\n <!-- Hidden file input -->\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id\"\n [attr.accept]=\"accept\"\n [attr.multiple]=\"multiple ? true : null\"\n [disabled]=\"disabled\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"cide-file-input-preview-box\"\n [class.cide-file-input-preview-box-disabled]=\"disabled\"\n [class.cide-file-input-preview-box-has-image]=\"hasImages()\"\n [style.width]=\"previewWidth\"\n [style.height]=\"previewHeight\"\n (click)=\"triggerFileSelect()\"\n [attr.title]=\"disabled ? 'File selection disabled' : placeholderText\">\n \n <!-- No Image State -->\n <div *ngIf=\"!hasImages()\" class=\"cide-file-input-preview-box-placeholder\">\n <div class=\"cide-file-input-preview-box-icon\">\n <cide-ele-icon>{{ placeholderIcon }}</cide-ele-icon>\n </div>\n <div class=\"cide-file-input-preview-box-text\">{{ placeholderText }}</div>\n </div>\n \n <!-- Image Preview State -->\n <div *ngIf=\"hasImages()\" class=\"cide-file-input-preview-box-content\">\n <img \n [src]=\"previewUrls[0]\" \n [alt]=\"fileNames[0] || 'Preview image'\"\n class=\"cide-file-input-preview-box-image\">\n <div class=\"cide-file-input-preview-box-overlay\">\n <div class=\"cide-file-input-preview-box-overlay-text\">Click to change</div>\n </div>\n <button \n *ngIf=\"!disabled\"\n type=\"button\" \n class=\"cide-file-input-preview-box-remove\"\n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Remove image\">\n \u00D7\n </button>\n </div>\n </div>\n \n <!-- File name display for preview box mode -->\n <div *ngIf=\"hasImages() && fileNames.length && showFileName\" class=\"cide-file-input-preview-box-filename\">\n {{ fileNames[0] }}\n </div>\n </div>\n\n <!-- Standard Mode -->\n <div *ngIf=\"!isPreviewBoxMode()\" class=\"cide-file-input-wrapper\">\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id\"\n [attr.accept]=\"accept\"\n [attr.multiple]=\"multiple ? true : null\"\n [disabled]=\"disabled\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-element\"\n />\n <button *ngIf=\"fileNames.length\" type=\"button\" class=\"cide-file-input-clear\" (click)=\"clearFiles()\">\n Clear\n </button>\n </div>\n <div *ngIf=\"fileNames.length && !isPreviewBoxMode()\" class=\"cide-file-input-files\">\n <span *ngFor=\"let name of fileNames\">{{ name }}</span>\n </div>\n \n <!-- Image Preview Section (only for standard mode) -->\n <div *ngIf=\"isImagePreviewAvailable() && !isPreviewBoxMode()\" class=\"cide-file-input-preview\">\n <div class=\"cide-file-input-preview-label\">Preview:</div>\n <div class=\"cide-file-input-preview-container\">\n <div \n *ngFor=\"let previewUrl of previewUrls; let i = index\" \n class=\"cide-file-input-preview-item\"\n [style.width]=\"previewWidth\"\n [style.height]=\"previewHeight\">\n <button \n type=\"button\" \n class=\"cide-file-input-preview-remove\"\n (click)=\"removePreview(i)\"\n title=\"Remove image\">\n \u00D7\n </button>\n <img \n [src]=\"previewUrl\" \n [alt]=\"fileNames[i] || 'Preview image'\"\n class=\"cide-file-input-preview-image\"\n loading=\"lazy\">\n <div class=\"cide-file-input-preview-filename\">{{ fileNames[i] }}</div>\n </div>\n </div>\n </div>\n \n <div *ngIf=\"errorText\" class=\"cide-file-input-error\">{{ errorText }}</div>\n <div *ngIf=\"helperText && !errorText\" class=\"cide-file-input-helper\">{{ helperText }}</div>\n</div> ", styles: [".cide-file-input{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-label{font-weight:500;margin-bottom:.25rem}.cide-file-input-required{color:#d32f2f;font-weight:700}.cide-file-input-wrapper{display:flex;align-items:center;gap:.5rem}.cide-file-input-element{flex:1}.cide-file-input-clear{background:none;border:none;color:#d32f2f;cursor:pointer;font-size:.9rem}.cide-file-input-files{font-size:.95rem;color:#333;margin-top:.25rem}.cide-file-input-error{color:#d32f2f;font-size:.9rem}.cide-file-input-helper{color:#666;font-size:.9rem}.cide-file-input-preview{margin-top:.75rem;padding:.75rem;background-color:#f8f9fa;border:1px solid #e1e5e9;border-radius:.375rem}.cide-file-input-preview-label{font-weight:500;margin-bottom:.5rem;color:#374151;font-size:.875rem}.cide-file-input-preview-container{display:flex;flex-wrap:wrap;gap:.75rem}.cide-file-input-preview-item{position:relative;display:flex;flex-direction:column;border:1px solid #d1d5db;border-radius:.5rem;overflow:hidden;background-color:#fff;box-shadow:0 1px 3px #0000001a;transition:box-shadow .2s ease-in-out}.cide-file-input-preview-item:hover{box-shadow:0 4px 6px -1px #0000001a}.cide-file-input-preview-image{width:100%;height:calc(100% - 2rem);object-fit:cover;object-position:center;background-color:#f3f4f6}.cide-file-input-preview-filename{padding:.375rem .5rem;background-color:#f9fafbf2;border-top:1px solid #e5e7eb;font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:2rem;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-remove{position:absolute;top:.25rem;right:.25rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;transition:all .2s ease-in-out}.cide-file-input-preview-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-hidden{display:none}.cide-file-input-preview-box-container{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-preview-box{position:relative;border:2px dashed #d1d5db;border-radius:.5rem;cursor:pointer;background-color:#f9fafb;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:all .2s ease-in-out}.cide-file-input-preview-box:hover{border-color:#3b82f6;background-color:#eff6ff}.cide-file-input-preview-box.cide-file-input-preview-box-disabled{cursor:not-allowed;opacity:.6;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-disabled:hover{border-color:#d1d5db;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image{border-style:solid;border-color:#e5e7eb;padding:0}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover{border-color:#3b82f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover .cide-file-input-preview-box-overlay{opacity:1}.cide-file-input-preview-box-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1rem;text-align:center}.cide-file-input-preview-box-icon{font-size:2rem;color:#6b7280}.cide-file-input-preview-box-text{font-size:.875rem;color:#6b7280;font-weight:500}.cide-file-input-preview-box-content{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-box-image{width:100%;height:100%;object-fit:cover;object-position:center}.cide-file-input-preview-box-overlay{position:absolute;inset:0;background-color:#0009;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s ease-in-out}.cide-file-input-preview-box-overlay-text{color:#fff;font-size:.875rem;font-weight:500;text-align:center}.cide-file-input-preview-box-remove{position:absolute;top:.375rem;right:.375rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:20;transition:all .2s ease-in-out}.cide-file-input-preview-box-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-box-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-preview-box-filename{font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;margin-top:.25rem}@media (max-width: 640px){.cide-file-input-preview-container{justify-content:center}.cide-file-input-preview-item{min-width:120px;max-width:150px}.cide-file-input-preview-box-icon{font-size:1.5rem}.cide-file-input-preview-box-text{font-size:.75rem}}\n"] }]
|
|
3237
|
-
}],
|
|
3238
|
-
type: Input
|
|
3239
|
-
}], accept: [{
|
|
3240
|
-
type: Input
|
|
3241
|
-
}], multiple: [{
|
|
3242
|
-
type: Input
|
|
3243
|
-
}], disabled: [{
|
|
3244
|
-
type: Input
|
|
3245
|
-
}], required: [{
|
|
3246
|
-
type: Input
|
|
3247
|
-
}], helperText: [{
|
|
3248
|
-
type: Input
|
|
3249
|
-
}], errorText: [{
|
|
3250
|
-
type: Input
|
|
3251
|
-
}], showPreview: [{
|
|
3252
|
-
type: Input
|
|
3253
|
-
}], previewWidth: [{
|
|
3254
|
-
type: Input
|
|
3255
|
-
}], previewHeight: [{
|
|
3256
|
-
type: Input
|
|
3257
|
-
}], previewBoxMode: [{
|
|
3258
|
-
type: Input
|
|
3259
|
-
}], showFileName: [{
|
|
3260
|
-
type: Input
|
|
3261
|
-
}], placeholderText: [{
|
|
3262
|
-
type: Input
|
|
3263
|
-
}], placeholderIcon: [{
|
|
3264
|
-
type: Input
|
|
3265
|
-
}], autoUpload: [{
|
|
3266
|
-
type: Input
|
|
3267
|
-
}], uploadData: [{
|
|
3268
|
-
type: Input
|
|
3269
|
-
}], fileChange: [{
|
|
3270
|
-
type: Output
|
|
3271
|
-
}], uploadSuccess: [{
|
|
3272
|
-
type: Output
|
|
3273
|
-
}], uploadError: [{
|
|
3274
|
-
type: Output
|
|
3275
|
-
}], uploadProgressChange: [{
|
|
3276
|
-
type: Output
|
|
3277
|
-
}] } });
|
|
3627
|
+
], template: "<div class=\"cide-file-input\">\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\n @if (label() && !isPreviewBoxMode()) {\n <label class=\"cide-file-input-label\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ label() }}@if (required()) {<span class=\"cide-file-input-required\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview-box-container\">\n <!-- Hidden file input -->\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"accept()\"\n [attr.multiple]=\"multiple() ? true : null\"\n [disabled]=\"disabled()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"cide-file-input-preview-box\"\n [class.cide-file-input-preview-box-disabled]=\"disabled()\"\n [class.cide-file-input-preview-box-has-image]=\"hasImages()\"\n [class.cide-file-input-preview-box-drag-over]=\"isDragOver()\"\n [style.width]=\"previewWidth()\"\n [style.height]=\"previewHeight()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n [attr.title]=\"disabled() ? 'File selection disabled' : placeholderText()\">\n \n <!-- No Image State -->\n @if (!hasImages()) {\n <div class=\"cide-file-input-preview-box-placeholder\">\n <div class=\"cide-file-input-preview-box-icon\">\n <cide-ele-icon>{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIcon() }}</cide-ele-icon>\n </div>\n <div class=\"cide-file-input-preview-box-text\">\n {{ isDragOver() ? 'Drop files here...' : placeholderText() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"cide-file-input-preview-box-content\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"cide-file-input-preview-box-image\">\n <div class=\"cide-file-input-preview-box-overlay\">\n <div class=\"cide-file-input-preview-box-overlay-text\">Click to change</div>\n </div>\n @if (!disabled()) {\n <button \n type=\"button\" \n class=\"cide-file-input-preview-box-remove\"\n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Remove image\">\n \u00D7\n </button>\n }\n </div>\n }\n </div>\n \n <!-- File name display for preview box mode -->\n @if (hasImages() && fileNames().length && showFileName()) {\n <div class=\"cide-file-input-preview-box-filename\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\n <div \n class=\"cide-file-input-wrapper\"\n [class.cide-file-input-drag-over]=\"isDragOver()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"accept()\"\n [attr.multiple]=\"multiple() ? true : null\"\n [disabled]=\"disabled()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-element\"\n />\n @if (hasFiles()) {\n <button type=\"button\" class=\"cide-file-input-clear\" (click)=\"clearFiles()\">\n Clear\n </button>\n }\n </div>\n @if (hasFiles() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-files\">\n @for (name of fileNames(); track name) {\n <span>{{ name }}</span>\n }\n <!-- Angular 20: Display file size using new computed values -->\n @if (totalFileSize() > 0) {\n <div class=\"cide-file-input-size\">\n Total size: {{ fileSizeInMB() }} MB\n </div>\n }\n </div>\n }\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview\">\n <div class=\"cide-file-input-preview-label\">Preview:</div>\n <div class=\"cide-file-input-preview-container\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"cide-file-input-preview-item\"\n [style.width]=\"previewWidth()\"\n [style.height]=\"previewHeight()\">\n <button \n type=\"button\" \n class=\"cide-file-input-preview-remove\"\n (click)=\"removePreview(i)\"\n title=\"Remove image\">\n \u00D7\n </button>\n <img \n [src]=\"previewUrl\" \n [alt]=\"fileNames()[i] || 'Preview image'\"\n class=\"cide-file-input-preview-image\"\n loading=\"lazy\">\n <div class=\"cide-file-input-preview-filename\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n @if (errorText()) {\n <div class=\"cide-file-input-error\">{{ errorText() }}</div>\n }\n @if (helperText() && !errorText()) {\n <div class=\"cide-file-input-helper\">{{ helperText() }}</div>\n }\n</div> ", styles: [".cide-file-input{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-label{font-weight:500;margin-bottom:.25rem}.cide-file-input-required{color:#d32f2f;font-weight:700}.cide-file-input-wrapper{display:flex;align-items:center;gap:.5rem;border:2px dashed transparent;border-radius:.5rem;padding:.5rem;transition:all .2s ease-in-out}.cide-file-input-wrapper.cide-file-input-drag-over{border-color:#3b82f6;background-color:#eff6ff;transform:scale(1.02)}.cide-file-input-element{flex:1}.cide-file-input-clear{background:none;border:none;color:#d32f2f;cursor:pointer;font-size:.9rem}.cide-file-input-files{font-size:.95rem;color:#333;margin-top:.25rem}.cide-file-input-size{margin-top:.5rem;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;font-size:.75rem;color:#4b5563;font-weight:500}.cide-file-input-error{color:#d32f2f;font-size:.9rem}.cide-file-input-helper{color:#666;font-size:.9rem}.cide-file-input-preview{margin-top:.75rem;padding:.75rem;background-color:#f8f9fa;border:1px solid #e1e5e9;border-radius:.375rem}.cide-file-input-preview-label{font-weight:500;margin-bottom:.5rem;color:#374151;font-size:.875rem}.cide-file-input-preview-container{display:flex;flex-wrap:wrap;gap:.75rem}.cide-file-input-preview-item{position:relative;display:flex;flex-direction:column;border:1px solid #d1d5db;border-radius:.5rem;overflow:hidden;background-color:#fff;box-shadow:0 1px 3px #0000001a;transition:box-shadow .2s ease-in-out}.cide-file-input-preview-item:hover{box-shadow:0 4px 6px -1px #0000001a}.cide-file-input-preview-image{width:100%;height:calc(100% - 2rem);object-fit:cover;object-position:center;background-color:#f3f4f6}.cide-file-input-preview-filename{padding:.375rem .5rem;background-color:#f9fafbf2;border-top:1px solid #e5e7eb;font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:2rem;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-remove{position:absolute;top:.25rem;right:.25rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;transition:all .2s ease-in-out}.cide-file-input-preview-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-hidden{display:none}.cide-file-input-preview-box-container{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-preview-box{position:relative;border:2px dashed #d1d5db;border-radius:.5rem;cursor:pointer;background-color:#f9fafb;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:all .2s ease-in-out}.cide-file-input-preview-box:hover{border-color:#3b82f6;background-color:#eff6ff}.cide-file-input-preview-box.cide-file-input-preview-box-disabled{cursor:not-allowed;opacity:.6;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-disabled:hover{border-color:#d1d5db;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image{border-style:solid;border-color:#e5e7eb;padding:0}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover{border-color:#3b82f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover .cide-file-input-preview-box-overlay{opacity:1}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over{border-color:#3b82f6!important;background-color:#eff6ff!important;transform:scale(1.02);box-shadow:0 0 0 4px #3b82f61a}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-icon{color:#3b82f6;transform:scale(1.1)}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-text{color:#3b82f6;font-weight:600}.cide-file-input-preview-box-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1rem;text-align:center}.cide-file-input-preview-box-icon{font-size:2rem;color:#6b7280}.cide-file-input-preview-box-text{font-size:.875rem;color:#6b7280;font-weight:500}.cide-file-input-preview-box-content{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-box-image{width:100%;height:100%;object-fit:cover;object-position:center}.cide-file-input-preview-box-overlay{position:absolute;inset:0;background-color:#0009;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s ease-in-out}.cide-file-input-preview-box-overlay-text{color:#fff;font-size:.875rem;font-weight:500;text-align:center}.cide-file-input-preview-box-remove{position:absolute;top:.375rem;right:.375rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:20;transition:all .2s ease-in-out}.cide-file-input-preview-box-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-box-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-preview-box-filename{font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;margin-top:.25rem}@media (max-width: 640px){.cide-file-input-preview-container{justify-content:center}.cide-file-input-preview-item{min-width:120px;max-width:150px}.cide-file-input-preview-box-icon{font-size:1.5rem}.cide-file-input-preview-box-text{font-size:.75rem}}\n"] }]
|
|
3628
|
+
}], ctorParameters: () => [] });
|
|
3278
3629
|
|
|
3279
3630
|
class CideTextareaComponent {
|
|
3280
3631
|
label = '';
|