@sixbell-telco/sdk
Version:
A collection of reusable components designed for use in Sixbell Telco Angular projects
707 lines (701 loc) • 41.1 kB
JavaScript
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