UNPKG

@sixbell-telco/sdk

Version:

A collection of reusable components designed for use in Sixbell Telco Angular projects

707 lines (701 loc) 41.1 kB
import { NgClass } from '@angular/common'; import * as i0 from '@angular/core'; import { inject, input, model, computed, signal, output, viewChild, forwardRef, Component, ChangeDetectionStrategy } from '@angular/core'; import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; import { TranslateService, _, TranslatePipe } from '@ngx-translate/core'; import { AudioPlayerService, ButtonPlayComponent, AudioPlayerComponent } from '@sixbell-telco/sdk/components/audio-player'; import { ButtonComponent } from '@sixbell-telco/sdk/components/button'; import { IconComponent } from '@sixbell-telco/sdk/components/icon'; import { matDelete, matDownload, matInsertDriveFile, matCloudUpload, matError, matVisibility, matVisibilityOff } from '@sixbell-telco/sdk/components/icon/material/baseline'; import { TooltipComponent } from '@sixbell-telco/sdk/components/tooltip'; import { TypographyDirective } from '@sixbell-telco/sdk/directives/typography'; import { cn } from '@sixbell-telco/sdk/utils/cn'; import { ExtractExtensionPipe } from '@sixbell-telco/sdk/utils/pipes/extract-extension'; import { ToBytesPipe } from '@sixbell-telco/sdk/utils/pipes/to-bytes-extesion'; import { switchMap, startWith } from 'rxjs'; import { InputComponent } from '@sixbell-telco/sdk/components/forms/input'; const AUDIO_EXTENSIONS = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.wma']; const VIDEO_EXTENSIONS = ['.mp4', '.avi', '.mov', '.wmv', '.mkv', '.flv', '.webm']; const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp']; const DOCUMENT_EXTENSIONS = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.csv']; const ARCHIVE_EXTENSIONS = ['.zip', '.rar', '.7z']; const CODE_EXTENSIONS = ['.html', '.xml', '.json', '.js', '.ts', '.css']; /** * A customizable drag-and-drop file upload zone with audio playback capabilities * * @remarks * Supports file validation, multi-file uploads, and audio file preview/playback. * Implements ControlValueAccessor for Angular form integration. * * @example * ```html * <st-file-dropzone * [extensions]="['.mp3', '.wav']" * [maxFiles]="3" * (fileDropped)="handleUpload($event)" * ></st-file-dropzone> * ``` * * @example * ```html * <!-- Form integration --> * <st-file-dropzone * [parentForm]="mediaForm" * formControlName="attachments" * [multiple]="true" * ></st-file-dropzone> * ``` */ class DropzoneComponent { /** @internal Translation service instance */ translateService = inject(TranslateService); /** @internal Audio player service instance */ audioService = inject(AudioPlayerService); // Input properties /** * Maximum number of allowed files * @defaultValue 1 */ maxFiles = input(1); /** * Maximum file size in bytes (0 for unlimited) * @defaultValue 0 */ maxFileSize = input(0); /** * Whether to display uploaded files list * @defaultValue true */ showFiles = input(true); /** * Allowed file extensions (e.g., ['.pdf', '.mp3']) */ extensions = input.required(); /** * Whether upload is in progress * @defaultValue false */ uploading = input(false); /** * Whether the component is disabled * @defaultValue false */ disabled = model(false); /** * Whether multiple files can be uploaded * @defaultValue false */ multiple = input(false); /** * HTML name attribute for the input * @defaultValue 'file' */ name = input('file'); /** * Computed accept attribute for file input * @internal */ accept = computed(() => this.extensions().join(',')); /** @internal Tracks audio player open state */ opened = signal(false); /** @internal Tracks blur events for validation */ blurTrigger = signal(0); // Form integration /** * Parent form group for reactive forms */ parentForm = input(null); /** * Form control name for reactive forms */ formControlName = input(''); // Output events /** * Emitted when valid files are dropped/selected */ fileDropped = output(); /** * Emitted when a file is removed */ fileRemoved = output(); // Internal state /** @internal Current list of uploaded files */ files = signal([]); /** @internal Drag state tracker */ isDragging = signal(false); /** @internal Validation error message */ errorMessage = signal(''); // ControlValueAccessor implementation /** @internal Change callback function */ onControlChange = () => { }; /** @internal Touch callback function */ onControlTouch = () => { }; /** @internal Reference to file input element */ fileInput = viewChild('fileInput'); // Icon references /** @internal Material icon references */ iconDelete = matDelete; iconDownload = matDownload; iconInsertDriveFile = matInsertDriveFile; iconCloudUpload = matCloudUpload; iconError = matError; iconVisibility = matVisibility; iconVisibilityOff = matVisibilityOff; /** * Computed audio tracks from uploaded files * @internal */ tracks = computed(() => { return this.files() .filter((audio) => this.checkIfIsAudioFile(audio)) .map((audio) => ({ title: audio.name, description: new ToBytesPipe().transform(audio.size), url: URL.createObjectURL(audio), })); }); /** * Whether to show audio player controls * @internal */ addAudioPlayer = computed(() => this.extensions().some((ext) => AUDIO_EXTENSIONS.includes(ext))); /** * Gets track index for audio files * @param file - File to check * @returns Index in audio tracks array * @internal */ getIndexForTracks(file) { return this.files() .filter((audio) => this.checkIfIsAudioFile(audio)) .indexOf(file); } // Drag and drop handlers /** * Handles drag over event * @param event - Drag event * @internal */ handleDragOver(event) { event.preventDefault(); if (this.disabled()) return; this.isDragging.set(true); } /** * Handles drag leave event * @param event - Drag event * @internal */ handleDragLeave(event) { event.preventDefault(); this.isDragging.set(false); } /** * Handles file drop event * @param event - Drag event * @internal */ handleDrop(event) { event.preventDefault(); this.isDragging.set(false); if (this.disabled()) return; const files = Array.from(event.dataTransfer?.files || []); this.processFiles(files); } /** * Handles file input selection * @param event - Input change event * @internal */ handleFileSelect(event) { const input = event.target; const files = Array.from(input.files || []); this.processFiles(files); input.value = ''; // Reset input } /** @internal Processes and validates uploaded files */ processFiles(files) { this.errorMessage.set(''); // Validate max files const totalFiles = this.files().length + files.length; if (this.maxFiles() > 0 && totalFiles > this.maxFiles()) { this.translateService.get(_('sdk.fileUpload.dropzone.maxFilesExceeded'), { max: this.maxFiles() }).subscribe((res) => { this.errorMessage.set(res); }); return; } // File validation const validFiles = files.filter((file) => { const isValidSize = this.validateFileSize(file); const isValidType = this.validateFileType(file); if (!isValidSize) { console.warn(`File ${file.name} exceeds size limit`); return false; } if (!isValidType) { console.warn(`File ${file.name} has invalid type`); return false; } return true; }); if (validFiles.length > 0) { this.files.update((current) => [...current, ...validFiles]); this.onControlChange(this.files()); this.onControlTouch(); this.fileDropped.emit(validFiles); } else { this.translateService.get(_('sdk.fileUpload.dropzone.noValidFiles')).subscribe((res) => { this.errorMessage.set(res); }); } } /** * Validates file size against maxFileSize * @param file - File to validate * @returns True if file size is valid * @internal */ validateFileSize(file) { return this.maxFileSize() === 0 || file.size <= this.maxFileSize(); } /** * Validates file type against allowed extensions * @param file - File to validate * @returns True if file type is valid * @internal */ validateFileType(file) { if (this.extensions().length === 0) return true; const extension = file.name.match(/\.([0-9a-z]+)$/i)?.[0]?.toLowerCase() || ''; return this.extensions().some((ext) => ext.toLowerCase() === extension); } /** * Handles file download * @param file - File to download * @internal */ handlefileDownload(file) { const url = URL.createObjectURL(file); const a = document.createElement('a'); a.href = url; a.download = file.name; document.body.appendChild(a); a.click(); URL.revokeObjectURL(url); document.body.removeChild(a); } /** * Removes a file from the list * @param file - File to remove * @internal */ handleRemove(file) { this.files.update((current) => current.filter((f) => f !== file)); this.onControlChange(this.files()); this.onControlTouch(); this.fileRemoved.emit(file); } /** * Checks if a file is an audio file * @param file - File to check * @returns True if audio file * @internal */ checkIfIsAudioFile(file) { return AUDIO_EXTENSIONS.some((ext) => file.name.endsWith(ext)); } /** * Toggles audio player visibility * @internal */ handleAudioPlayerOpen() { this.opened.set(!this.opened()); } /** @internal Cleans up object URLs */ ngDestroy() { this.tracks().forEach((track) => URL.revokeObjectURL(track.url)); } // ControlValueAccessor implementation /** * Writes form value to component * @param files - Array of files from form control */ writeValue(files) { this.files.set(files || []); } /** * Registers change callback * @param fn - Callback function for value changes */ registerOnChange(fn) { this.onControlChange = fn; } /** * Registers touch callback * @param fn - Callback function for touch events */ registerOnTouched(fn) { this.onControlTouch = fn; } /** * Sets disabled state * @param isDisabled - Whether component is disabled */ setDisabledState(isDisabled) { this.disabled.set(isDisabled); } /** * Triggers file input dialog * @internal */ triggerFileInput() { if (!this.disabled()) { this.fileInput()?.nativeElement.click(); } } // Form control integration /** @internal Form control reference */ formControl = computed(() => this.parentForm()?.get(this.formControlName())); /** @internal Form control observable */ formControl$ = toObservable(this.formControl); /** @internal Status changes observable */ statusChanges$ = this.formControl$.pipe(switchMap((control) => control?.statusChanges.pipe(startWith(control.status)) || [])); /** @internal Value changes observable */ stateChanges$ = this.formControl$.pipe(switchMap((control) => control?.valueChanges.pipe(startWith(control.value)) || [])); /** @internal Status signal */ statusSignal = toSignal(this.statusChanges$); /** @internal State signal */ stateSignal = toSignal(this.stateChanges$); /** * Handles blur events for validation * @internal */ handleBlur() { this.onControlTouch(); this.blurTrigger.update((v) => v + 1); } /** * Computes CSS classes for dropzone based on state * @internal */ dropzoneClass = computed(() => { const control = this.formControl(); const trigger = this.blurTrigger(); const baseClasses = 'cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors duration-300 border-secondary hover:bg-secondary/10'; if (!control) return baseClasses; const isTouched = control.touched || trigger > 0; this.statusSignal(); this.stateSignal(); const isFormError = control.invalid && (control.touched || isTouched); const isComponentError = !!this.errorMessage(); return cn(baseClasses, { 'border-secondary bg-secondary/30': this.isDragging() && !this.disabled(), 'cursor-not-allowed bg-base-100/40 text-base-content/40': this.disabled(), 'border-error bg-error/15 hover:border-error hover:bg-error/10': isComponentError || isFormError, }); }); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropzoneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: DropzoneComponent, isStandalone: true, selector: "st-file-dropzone", inputs: { maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, maxFileSize: { classPropertyName: "maxFileSize", publicName: "maxFileSize", isSignal: true, isRequired: false, transformFunction: null }, showFiles: { classPropertyName: "showFiles", publicName: "showFiles", isSignal: true, isRequired: false, transformFunction: null }, extensions: { classPropertyName: "extensions", publicName: "extensions", isSignal: true, isRequired: true, transformFunction: null }, uploading: { classPropertyName: "uploading", publicName: "uploading", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, parentForm: { classPropertyName: "parentForm", publicName: "parentForm", isSignal: true, isRequired: false, transformFunction: null }, formControlName: { classPropertyName: "formControlName", publicName: "formControlName", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", fileDropped: "fileDropped", fileRemoved: "fileRemoved" }, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropzoneComponent), multi: true, }, ], viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\n<div class=\"w-full\">\n\t<div\n\t\t[class]=\"dropzoneClass()\"\n\t\t(dragover)=\"handleDragOver($event)\"\n\t\t(dragleave)=\"handleDragLeave($event)\"\n\t\t(drop)=\"handleDrop($event)\"\n\t\t(click)=\"triggerFileInput()\"\n\t\t(blur)=\"handleBlur()\"\n\t\ttabindex=\"0\"\n\t>\n\t\t@if (parentForm()) {\n\t\t\t<input\n\t\t\t\t#fileInput\n\t\t\t\ttype=\"file\"\n\t\t\t\t[multiple]=\"multiple()\"\n\t\t\t\t[accept]=\"accept()\"\n\t\t\t\t(change)=\"handleFileSelect($event)\"\n\t\t\t\t[name]=\"name()\"\n\t\t\t\t(blur)=\"handleBlur()\"\n\t\t\t\trequired\n\t\t\t\thidden\n\t\t\t/>\n\t\t} @else {\n\t\t\t<input #fileInput type=\"file\" [multiple]=\"multiple()\" [accept]=\"accept()\" (change)=\"handleFileSelect($event)\" [name]=\"name()\" required hidden />\n\t\t}\n\n\t\t<div class=\"grid h-48 place-content-center\">\n\t\t\t@let errorMessageVar = this.errorMessage();\n\t\t\t@if (errorMessageVar) {\n\t\t\t\t<div>\n\t\t\t\t\t<st-icon [icon]=\"iconError\" size=\"xl\" class=\"text-error\" />\n\t\t\t\t\t<div typography [tyVariant]=\"'body'\">\n\t\t\t\t\t\t{{ errorMessageVar }}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t} @else if (!uploading()) {\n\t\t\t\t<div>\n\t\t\t\t\t<st-icon [icon]=\"iconCloudUpload\" size=\"xl\" class=\"text-accent\" />\n\t\t\t\t\t<div typography [tyVariant]=\"'body'\">\n\t\t\t\t\t\t{{ 'sdk.fileUpload.dropzone.selectPrompt' | translate }}\n\t\t\t\t\t</div>\n\n\t\t\t\t\t@if (maxFileSize() > 0) {\n\t\t\t\t\t\t<div class=\"mt-1\">\n\t\t\t\t\t\t\t<span typography [tyVariant]=\"'body-sm'\" tyFontWeight=\"medium\">\n\t\t\t\t\t\t\t\t{{ 'sdk.fileUpload.dropzone.maxSizeLabel' | translate }} <span class=\"text-accent\">{{ maxFileSize() | toBytes }}</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\n\t\t\t\t\t@if (extensions().length > 0) {\n\t\t\t\t\t\t<div class=\"mt-1\">\n\t\t\t\t\t\t\t<span typography [tyVariant]=\"'body-sm'\" tyFontWeight=\"medium\">\n\t\t\t\t\t\t\t\t{{ 'sdk.fileUpload.dropzone.allowedTypesLabel' | translate }} <span class=\"text-accent\">{{ extensions().join(', ') }}</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\t</div>\n\n\t@if (maxFiles() > 0) {\n\t\t<div class=\"mt-4 flex items-center justify-between\">\n\t\t\t<ng-content></ng-content>\n\t\t\t<span typography [tyVariant]=\"'body-sm'\" [ngClass]=\"{ 'text-error': files().length > maxFiles() }\">\n\t\t\t\t{{ 'sdk.fileUpload.dropzone.fileCounter' | translate: { current: files().length, max: maxFiles() } }}\n\t\t\t</span>\n\t\t</div>\n\t}\n\n\t@if (showFiles() && files().length > 0) {\n\t\t<div class=\"mt-4 space-y-2\">\n\t\t\t@for (file of files(); track $index) {\n\t\t\t\t<div class=\"bg-neutral/30 flex items-center justify-between rounded-md py-3 pr-5 pl-3\">\n\t\t\t\t\t<div class=\"flex items-center justify-start\">\n\t\t\t\t\t\t<div class=\"text-primary relative\">\n\t\t\t\t\t\t\t<st-icon [icon]=\"iconInsertDriveFile\" size=\"xl\" />\n\t\t\t\t\t\t\t<div class=\"text-primary-content absolute bottom-2 left-1/2 -translate-x-1/2\">\n\t\t\t\t\t\t\t\t<span typography [tyVariant]=\"'body-xxs'\" tyFontWeight=\"medium\">\n\t\t\t\t\t\t\t\t\t{{ file.name | extractExtension }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"flex flex-col items-start justify-between\">\n\t\t\t\t\t\t\t<span typography [tyVariant]=\"'body'\">\n\t\t\t\t\t\t\t\t{{ file.name }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<div class=\"text-tertiary\">\n\t\t\t\t\t\t\t\t<span typography [tyVariant]=\"'body-xs'\"> {{ file.size | toBytes }} </span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"inline-flex items-center justify-end gap-1 align-top\">\n\t\t\t\t\t\t@if (checkIfIsAudioFile(file)) {\n\t\t\t\t\t\t\t<st-tooltip\n\t\t\t\t\t\t\t\t[label]=\"\n\t\t\t\t\t\t\t\t\t!this.audioService.isPlayingATrack()\n\t\t\t\t\t\t\t\t\t\t? ('sdk.fileUpload.dropzone.fileActions.play' | translate)\n\t\t\t\t\t\t\t\t\t\t: ('sdk.fileUpload.dropzone.fileActions.pause' | translate)\n\t\t\t\t\t\t\t\t\"\n\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<st-button-play\n\t\t\t\t\t\t\t\t\t[outline]=\"true\"\n\t\t\t\t\t\t\t\t\t[square]=\"true\"\n\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t[asButton]=\"true\"\n\t\t\t\t\t\t\t\t\t[trackIndex]=\"getIndexForTracks(file)\"\n\t\t\t\t\t\t\t\t\t(togglePlay)=\"handleAudioPlayerOpen()\"\n\t\t\t\t\t\t\t\t></st-button-play>\n\t\t\t\t\t\t\t</st-tooltip>\n\t\t\t\t\t\t}\n\t\t\t\t\t\t<div></div>\n\t\t\t\t\t\t<st-tooltip [label]=\"'sdk.fileUpload.dropzone.fileActions.download' | translate\" variant=\"secondary\">\n\t\t\t\t\t\t\t<st-button variant=\"info\" [outline]=\"true\" [square]=\"true\" size=\"sm\" (click)=\"handlefileDownload(file)\">\n\t\t\t\t\t\t\t\t<st-icon class=\"text-lg\" [icon]=\"iconDownload\"></st-icon>\n\t\t\t\t\t\t\t</st-button>\n\t\t\t\t\t\t</st-tooltip>\n\t\t\t\t\t\t<st-tooltip [label]=\"'sdk.fileUpload.dropzone.fileActions.remove' | translate\" variant=\"secondary\">\n\t\t\t\t\t\t\t<st-button variant=\"error\" [outline]=\"true\" [square]=\"true\" size=\"sm\" (click)=\"handleRemove(file)\">\n\t\t\t\t\t\t\t\t<st-icon class=\"text-lg\" [icon]=\"iconDelete\"></st-icon>\n\t\t\t\t\t\t\t</st-button>\n\t\t\t\t\t\t</st-tooltip>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\t}\n</div>\n@if (addAudioPlayer()) {\n\t<st-audio-player [tracks]=\"tracks()\" [showTrackList]=\"false\" variant=\"fixed\" [(opened)]=\"opened\"></st-audio-player>\n}\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: ButtonComponent, selector: "st-button", inputs: ["variant", "ghost", "outline", "link", "soft", "dash", "wide", "circle", "square", "glass", "block", "loader", "size", "shadow", "focusable", "icon", "iconPosition", "type", "disabled"] }, { kind: "directive", type: TypographyDirective, selector: "[typography]", inputs: ["tyVariant", "tyColor", "tyFontWeight", "tyTextOverflow"] }, { kind: "pipe", type: ToBytesPipe, name: "toBytes" }, { kind: "pipe", type: ExtractExtensionPipe, name: "extractExtension" }, { kind: "component", type: IconComponent, selector: "st-icon", inputs: ["color", "size", "icon"] }, { kind: "component", type: ButtonPlayComponent, selector: "st-button-play", inputs: ["variant", "ghost", "outline", "circle", "square", "glass", "size", "shadow", "asButton", "trackIndex"], outputs: ["togglePlay"] }, { kind: "component", type: AudioPlayerComponent, selector: "st-audio-player", inputs: ["variant", "shadow", "opened", "tracks", "showTrackList", "currentTrackIndex"], outputs: ["openedChange", "currentTrackIndexChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "component", type: TooltipComponent, selector: "st-tooltip", inputs: ["variant", "position", "label"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: DropzoneComponent, decorators: [{ type: Component, args: [{ selector: 'st-file-dropzone', imports: [ NgClass, ButtonComponent, TypographyDirective, ToBytesPipe, ExtractExtensionPipe, IconComponent, ButtonPlayComponent, AudioPlayerComponent, FormsModule, TranslatePipe, TooltipComponent, ], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropzoneComponent), multi: true, }, ], template: "<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\n<div class=\"w-full\">\n\t<div\n\t\t[class]=\"dropzoneClass()\"\n\t\t(dragover)=\"handleDragOver($event)\"\n\t\t(dragleave)=\"handleDragLeave($event)\"\n\t\t(drop)=\"handleDrop($event)\"\n\t\t(click)=\"triggerFileInput()\"\n\t\t(blur)=\"handleBlur()\"\n\t\ttabindex=\"0\"\n\t>\n\t\t@if (parentForm()) {\n\t\t\t<input\n\t\t\t\t#fileInput\n\t\t\t\ttype=\"file\"\n\t\t\t\t[multiple]=\"multiple()\"\n\t\t\t\t[accept]=\"accept()\"\n\t\t\t\t(change)=\"handleFileSelect($event)\"\n\t\t\t\t[name]=\"name()\"\n\t\t\t\t(blur)=\"handleBlur()\"\n\t\t\t\trequired\n\t\t\t\thidden\n\t\t\t/>\n\t\t} @else {\n\t\t\t<input #fileInput type=\"file\" [multiple]=\"multiple()\" [accept]=\"accept()\" (change)=\"handleFileSelect($event)\" [name]=\"name()\" required hidden />\n\t\t}\n\n\t\t<div class=\"grid h-48 place-content-center\">\n\t\t\t@let errorMessageVar = this.errorMessage();\n\t\t\t@if (errorMessageVar) {\n\t\t\t\t<div>\n\t\t\t\t\t<st-icon [icon]=\"iconError\" size=\"xl\" class=\"text-error\" />\n\t\t\t\t\t<div typography [tyVariant]=\"'body'\">\n\t\t\t\t\t\t{{ errorMessageVar }}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t} @else if (!uploading()) {\n\t\t\t\t<div>\n\t\t\t\t\t<st-icon [icon]=\"iconCloudUpload\" size=\"xl\" class=\"text-accent\" />\n\t\t\t\t\t<div typography [tyVariant]=\"'body'\">\n\t\t\t\t\t\t{{ 'sdk.fileUpload.dropzone.selectPrompt' | translate }}\n\t\t\t\t\t</div>\n\n\t\t\t\t\t@if (maxFileSize() > 0) {\n\t\t\t\t\t\t<div class=\"mt-1\">\n\t\t\t\t\t\t\t<span typography [tyVariant]=\"'body-sm'\" tyFontWeight=\"medium\">\n\t\t\t\t\t\t\t\t{{ 'sdk.fileUpload.dropzone.maxSizeLabel' | translate }} <span class=\"text-accent\">{{ maxFileSize() | toBytes }}</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\n\t\t\t\t\t@if (extensions().length > 0) {\n\t\t\t\t\t\t<div class=\"mt-1\">\n\t\t\t\t\t\t\t<span typography [tyVariant]=\"'body-sm'\" tyFontWeight=\"medium\">\n\t\t\t\t\t\t\t\t{{ 'sdk.fileUpload.dropzone.allowedTypesLabel' | translate }} <span class=\"text-accent\">{{ extensions().join(', ') }}</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\t</div>\n\n\t@if (maxFiles() > 0) {\n\t\t<div class=\"mt-4 flex items-center justify-between\">\n\t\t\t<ng-content></ng-content>\n\t\t\t<span typography [tyVariant]=\"'body-sm'\" [ngClass]=\"{ 'text-error': files().length > maxFiles() }\">\n\t\t\t\t{{ 'sdk.fileUpload.dropzone.fileCounter' | translate: { current: files().length, max: maxFiles() } }}\n\t\t\t</span>\n\t\t</div>\n\t}\n\n\t@if (showFiles() && files().length > 0) {\n\t\t<div class=\"mt-4 space-y-2\">\n\t\t\t@for (file of files(); track $index) {\n\t\t\t\t<div class=\"bg-neutral/30 flex items-center justify-between rounded-md py-3 pr-5 pl-3\">\n\t\t\t\t\t<div class=\"flex items-center justify-start\">\n\t\t\t\t\t\t<div class=\"text-primary relative\">\n\t\t\t\t\t\t\t<st-icon [icon]=\"iconInsertDriveFile\" size=\"xl\" />\n\t\t\t\t\t\t\t<div class=\"text-primary-content absolute bottom-2 left-1/2 -translate-x-1/2\">\n\t\t\t\t\t\t\t\t<span typography [tyVariant]=\"'body-xxs'\" tyFontWeight=\"medium\">\n\t\t\t\t\t\t\t\t\t{{ file.name | extractExtension }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"flex flex-col items-start justify-between\">\n\t\t\t\t\t\t\t<span typography [tyVariant]=\"'body'\">\n\t\t\t\t\t\t\t\t{{ file.name }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<div class=\"text-tertiary\">\n\t\t\t\t\t\t\t\t<span typography [tyVariant]=\"'body-xs'\"> {{ file.size | toBytes }} </span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"inline-flex items-center justify-end gap-1 align-top\">\n\t\t\t\t\t\t@if (checkIfIsAudioFile(file)) {\n\t\t\t\t\t\t\t<st-tooltip\n\t\t\t\t\t\t\t\t[label]=\"\n\t\t\t\t\t\t\t\t\t!this.audioService.isPlayingATrack()\n\t\t\t\t\t\t\t\t\t\t? ('sdk.fileUpload.dropzone.fileActions.play' | translate)\n\t\t\t\t\t\t\t\t\t\t: ('sdk.fileUpload.dropzone.fileActions.pause' | translate)\n\t\t\t\t\t\t\t\t\"\n\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<st-button-play\n\t\t\t\t\t\t\t\t\t[outline]=\"true\"\n\t\t\t\t\t\t\t\t\t[square]=\"true\"\n\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t[asButton]=\"true\"\n\t\t\t\t\t\t\t\t\t[trackIndex]=\"getIndexForTracks(file)\"\n\t\t\t\t\t\t\t\t\t(togglePlay)=\"handleAudioPlayerOpen()\"\n\t\t\t\t\t\t\t\t></st-button-play>\n\t\t\t\t\t\t\t</st-tooltip>\n\t\t\t\t\t\t}\n\t\t\t\t\t\t<div></div>\n\t\t\t\t\t\t<st-tooltip [label]=\"'sdk.fileUpload.dropzone.fileActions.download' | translate\" variant=\"secondary\">\n\t\t\t\t\t\t\t<st-button variant=\"info\" [outline]=\"true\" [square]=\"true\" size=\"sm\" (click)=\"handlefileDownload(file)\">\n\t\t\t\t\t\t\t\t<st-icon class=\"text-lg\" [icon]=\"iconDownload\"></st-icon>\n\t\t\t\t\t\t\t</st-button>\n\t\t\t\t\t\t</st-tooltip>\n\t\t\t\t\t\t<st-tooltip [label]=\"'sdk.fileUpload.dropzone.fileActions.remove' | translate\" variant=\"secondary\">\n\t\t\t\t\t\t\t<st-button variant=\"error\" [outline]=\"true\" [square]=\"true\" size=\"sm\" (click)=\"handleRemove(file)\">\n\t\t\t\t\t\t\t\t<st-icon class=\"text-lg\" [icon]=\"iconDelete\"></st-icon>\n\t\t\t\t\t\t\t</st-button>\n\t\t\t\t\t\t</st-tooltip>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\t}\n</div>\n@if (addAudioPlayer()) {\n\t<st-audio-player [tracks]=\"tracks()\" [showTrackList]=\"false\" variant=\"fixed\" [(opened)]=\"opened\"></st-audio-player>\n}\n" }] }] }); /* eslint-disable @typescript-eslint/no-explicit-any */ /** * A customizable file upload component with form integration and validation support * * @remarks * Supports file type restrictions, form control integration, and visual validation states. * Implements ControlValueAccessor for Angular form compatibility. * * @example * ```html * <!-- Basic file upload --> * <st-file-uploader * [extensions]="['.pdf', '.docx']" * label="Upload Resume" * ></st-file-uploader> * ``` * * @example * ```html * <!-- Reactive form integration --> * <st-file-uploader * [parentForm]="documentForm" * formControlName="attachment" * variant="primary" * ></st-file-uploader> * ``` */ class FileUploaderComponent { /** * Visual style variant * @defaultValue 'secondary' */ variant = input('secondary'); /** * Component size variant * @defaultValue 'md' */ size = input('md'); /** * Label displayed above the uploader */ label = input(''); /** * HTML name attribute for the input */ name = input(null); /** * Placeholder text when no file is selected */ placeholder = input(''); /** * Whether to show allowed file extensions * @defaultValue true */ showExtensions = input(true); /** * Allowed file extensions (e.g., ['.pdf', '.docx']) */ extensions = input.required(); /** * Computed accept attribute for native file input * @internal */ accept = computed(() => this.extensions().join(',')); /** * Event emitted when a valid file is selected */ fileUpdated = output(); /** * Current selected filename * @internal */ filename = ''; /** * Current file value * @internal */ fileValue = null; /** * Formatted extensions string for display * @internal */ formattedExtensions = computed(() => this.extensions().join(', ')); /** * Reference to the hidden file input * @internal */ fileInput = viewChild('uploader'); /** * Event emitted on component blur */ blurred = output(); /** * Whether the component is disabled * @defaultValue false */ disabled = model(false); /** @internal Track blur events for validation */ blurTrigger = signal(0); /** * Parent form group for reactive forms */ parentForm = input(null); /** * Form control name for reactive forms */ formControlName = input(''); /** * Two-way bindable file value */ value = model(null); // ControlValueAccessor implementation /** @internal */ onControlChange = () => { }; /** @internal */ onControlTouch = () => { }; /** * Writes form value to component * @param obj - The incoming form value */ writeValue(obj) { this.value.set(obj); } /** * Registers change callback * @param fn - Callback function for value changes */ registerOnChange(fn) { this.onControlChange = fn; } /** * Registers touch callback * @param fn - Callback function for touch events */ registerOnTouched(fn) { this.onControlTouch = fn; } /** * Sets disabled state * @param isDisabled - Whether the component should be disabled */ setDisabledState(isDisabled) { this.disabled.set(isDisabled); } /** * Handles click event on the upload area * @internal */ handleClick() { if (!this.fileInput()) return; this.fileInput()?.nativeElement.click(); this.handleBlur(); } /** * Notifies form control of value changes * @internal */ handleChange() { this.onControlChange(this.value()); } /** * Handles file selection from input * @param event - File input change event * @internal */ handleFileChange(event) { const file = event.target.files?.[0]; if (file instanceof File) { this.filename = file.name; this.value.set(file); this.fileUpdated.emit(file); this.handleChange(); } } /** * Clears selected file * @internal */ removeFile() { this.filename = ''; this.value.set(null); this.handleChange(); } /** * @internal * Returns base component classes */ componentClass() { return { variant: this.variant(), size: this.size(), }; } /** * @internal * Returns error state classes */ errorClass() { return { variant: 'error', size: this.size(), }; } /** * @internal * Returns success state classes */ successClass() { return { variant: 'success', size: this.size(), }; } /** * Handles blur events and validation * @internal */ handleBlur() { this.blurred.emit(this.value()); this.onControlTouch(); this.blurTrigger.update((v) => v + 1); } // Form control integration /** @internal */ formControl = computed(() => this.parentForm()?.get(this.formControlName())); /** @internal */ formControl$ = toObservable(this.formControl); /** @internal */ statusChanges$ = this.formControl$.pipe(switchMap((control) => control?.statusChanges.pipe(startWith(control.status)) || [])); /** @internal */ stateChanges$ = this.formControl$.pipe(switchMap((control) => control?.valueChanges.pipe(startWith(control.value)) || [])); /** @internal */ statusSignal = toSignal(this.statusChanges$); /** @internal */ stateSignal = toSignal(this.stateChanges$); /** * Computes validation classes based on form state * @internal */ validationClass = computed(() => { const control = this.formControl(); const trigger = this.blurTrigger(); if (!control) return this.componentClass(); const isTouched = control.touched || trigger > 0; this.statusSignal(); this.stateSignal(); if (control.dirty || isTouched) { if (control.invalid) return this.errorClass(); if (control.valid) return this.successClass(); } return this.componentClass(); }); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: FileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.0", type: FileUploaderComponent, isStandalone: true, selector: "st-file-uploader", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, showExtensions: { classPropertyName: "showExtensions", publicName: "showExtensions", isSignal: true, isRequired: false, transformFunction: null }, extensions: { classPropertyName: "extensions", publicName: "extensions", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, parentForm: { classPropertyName: "parentForm", publicName: "parentForm", isSignal: true, isRequired: false, transformFunction: null }, formControlName: { classPropertyName: "formControlName", publicName: "formControlName", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileUpdated: "fileUpdated", blurred: "blurred", disabled: "disabledChange", value: "valueChange" }, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FileUploaderComponent), multi: true }], viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["uploader"], descendants: true, isSignal: true }], ngImport: i0, template: "<st-input\n\t[label]=\"label()\"\n\t[placeholder]=\"placeholder()\"\n\t[readonly]=\"true\"\n\t(click)=\"handleClick()\"\n\t[(value)]=\"filename\"\n\t[name]=\"name()\"\n\t[variant]=\"validationClass().variant\"\n\t[size]=\"validationClass().size\"\n\t(blur)=\"handleBlur()\"\n\t[disabled]=\"disabled()\"\n\t(valueChange)=\"handleChange()\"\n>\n\t<div class=\"flex justify-between\">\n\t\t<div>\n\t\t\t<ng-content></ng-content>\n\t\t</div>\n\t\t@if (extensions().length > 0 && showExtensions()) {\n\t\t\t<div class=\"mt-1\">\n\t\t\t\t<span typography [tyVariant]=\"'body-xs'\" tyFontWeight=\"normal\">\n\t\t\t\t\t{{ 'sdk.fileUpload.fileUploader.allowedTypesLabel' | translate }} <span class=\"text-accent\">{{ formattedExtensions() }}</span>\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t}\n\t</div>\n</st-input>\n\n<input type=\"file\" #uploader hidden (change)=\"handleFileChange($event)\" (blur)=\"handleBlur()\" [accept]=\"accept()\" required />\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "component", type: InputComponent, selector: "st-input", inputs: ["variant", "size", "label", "type", "name", "placeholder", "readonly", "focus", "onlyNumbers", "ghost", "value", "parentForm", "formControlName", "disabled", "debounceTime"], outputs: ["valueChange", "disabledChange", "enterPressed", "blurred", "valueDebounced"] }, { kind: "directive", type: TypographyDirective, selector: "[typography]", inputs: ["tyVariant", "tyColor", "tyFontWeight", "tyTextOverflow"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.Default }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: FileUploaderComponent, decorators: [{ type: Component, args: [{ selector: 'st-file-uploader', imports: [FormsModule, InputComponent, TypographyDirective, TranslatePipe], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FileUploaderComponent), multi: true }], changeDetection: ChangeDetectionStrategy.Default, template: "<st-input\n\t[label]=\"label()\"\n\t[placeholder]=\"placeholder()\"\n\t[readonly]=\"true\"\n\t(click)=\"handleClick()\"\n\t[(value)]=\"filename\"\n\t[name]=\"name()\"\n\t[variant]=\"validationClass().variant\"\n\t[size]=\"validationClass().size\"\n\t(blur)=\"handleBlur()\"\n\t[disabled]=\"disabled()\"\n\t(valueChange)=\"handleChange()\"\n>\n\t<div class=\"flex justify-between\">\n\t\t<div>\n\t\t\t<ng-content></ng-content>\n\t\t</div>\n\t\t@if (extensions().length > 0 && showExtensions()) {\n\t\t\t<div class=\"mt-1\">\n\t\t\t\t<span typography [tyVariant]=\"'body-xs'\" tyFontWeight=\"normal\">\n\t\t\t\t\t{{ 'sdk.fileUpload.fileUploader.allowedTypesLabel' | translate }} <span class=\"text-accent\">{{ formattedExtensions() }}</span>\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t}\n\t</div>\n</st-input>\n\n<input type=\"file\" #uploader hidden (change)=\"handleFileChange($event)\" (blur)=\"handleBlur()\" [accept]=\"accept()\" required />\n" }] }] }); /** * Generated bundle index. Do not edit. */ export { ARCHIVE_EXTENSIONS, AUDIO_EXTENSIONS, CODE_EXTENSIONS, DOCUMENT_EXTENSIONS, DropzoneComponent, FileUploaderComponent, IMAGE_EXTENSIONS, VIDEO_EXTENSIONS }; //# sourceMappingURL=sixbell-telco-sdk-components-file-upload.mjs.map