ngx-custom-material-file-input
Version:
File input management for Angular Material
483 lines (472 loc) • 20 kB
JavaScript
import * as i0 from '@angular/core';
import { HostListener, Input, HostBinding, Optional, Self, Component, InjectionToken, Inject, Pipe, NgModule } from '@angular/core';
import { MatFormFieldControl } from '@angular/material/form-field';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Subject } from 'rxjs/internal/Subject';
import * as i1 from '@angular/cdk/a11y';
import { FocusMonitor } from '@angular/cdk/a11y';
import * as i2 from '@angular/material/core';
import { ErrorStateMatcher } from '@angular/material/core';
import * as i3 from '@angular/forms';
/** Base class for error state management */
class FileInputBase {
constructor(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl, stateChanges) {
this._defaultErrorStateMatcher = _defaultErrorStateMatcher;
this._parentForm = _parentForm;
this._parentFormGroup = _parentFormGroup;
this.ngControl = ngControl;
this.stateChanges = stateChanges;
this._errorState = false;
}
/** Determines whether the control is in an error state */
get errorState() {
const control = this.ngControl?.control || null;
const form = this._parentForm || this._parentFormGroup || null;
return this._defaultErrorStateMatcher.isErrorState(control, form);
}
/** Triggers error state update */
updateErrorState() {
const previousState = this._errorState;
this._errorState = this.errorState;
if (previousState !== this._errorState) {
this.stateChanges.next();
}
}
}
/**
* The files to be uploaded
*/
class FileInput {
constructor(_files, delimiter = ', ') {
this._files = _files;
this.delimiter = delimiter;
this._fileNames = (this._files || []).map((f) => f.name).join(delimiter);
}
get files() {
return this._files || [];
}
get fileNames() {
return this._fileNames;
}
}
class FileInputComponent extends FileInputBase {
static { this.nextId = 0; }
get errorState() {
const control = this.ngControl?.control || null;
const form = this._parentForm || this._parentFormGroup || null;
const matcher = this.errorStateMatcher || this._defaultErrorStateMatcher;
return matcher.isErrorState(control, form);
}
setDescribedByIds(ids) {
this.describedBy = ids.join(' ');
}
get value() {
return this.empty ? null : new FileInput(this._elementRef.nativeElement.value || []);
}
set value(fileInput) {
if (fileInput) {
this.writeValue(fileInput);
this.stateChanges.next();
}
}
get multiple() {
return this._multiple;
}
set multiple(value) {
this._multiple = coerceBooleanProperty(value);
this.stateChanges.next();
}
get placeholder() {
return this._placeholder;
}
set placeholder(plh) {
this._placeholder = plh;
this.stateChanges.next();
}
/**
* Whether the current input has files
*/
get empty() {
return !this._elementRef.nativeElement.value || this._elementRef.nativeElement.value.length === 0;
}
get shouldLabelFloat() {
return this.focused || !this.empty || this.valuePlaceholder !== undefined;
}
get required() {
return this._required;
}
set required(req) {
this._required = coerceBooleanProperty(req);
this.stateChanges.next();
}
get isDisabled() {
return this.disabled;
}
get disabled() {
return this._elementRef.nativeElement.disabled;
}
set disabled(dis) {
this.setDisabledState(coerceBooleanProperty(dis));
this.stateChanges.next();
}
get previewUrls() {
return this._previewUrls;
}
onContainerClick(event) {
if (event.target.tagName.toLowerCase() !== 'input' && !this.disabled) {
this._elementRef.nativeElement.querySelector('input').focus();
this.focused = true;
this.open();
}
}
/**
* @see https://angular.io/api/forms/ControlValueAccessor
*/
constructor(fm, _elementRef, _renderer, _defaultErrorStateMatcher, ngControl, _parentForm, _parentFormGroup) {
super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl, new Subject());
this.fm = fm;
this._elementRef = _elementRef;
this._renderer = _renderer;
this._defaultErrorStateMatcher = _defaultErrorStateMatcher;
this.ngControl = ngControl;
this._parentForm = _parentForm;
this._parentFormGroup = _parentFormGroup;
this.focused = false;
this.controlType = 'file-input';
this.autofilled = false;
this._required = false;
this._multiple = false;
this._previewUrls = [];
this._objectURLs = [];
this.accept = null;
this.defaultIconBase64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0Ij48cGF0aCBkPSJNMCAwaDI0djI0SDBWMHoiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJNMTUgMkg2Yy0xLjEgMC0yIC45LTIgMnYxNmMwIDEuMS45IDIgMiAyaDEyYzEuMSAwIDItLjkgMi0yVjdsLTUtNXpNNiAyMFY0aDh2NGg0djEySDZ6bTEwLTEwdjVjMCAyLjIxLTEuNzkgNC00IDRzLTQtMS43OS00LTRWOC41YzAtMS40NyAxLjI2LTIuNjQgMi43Ni0yLjQ5IDEuMy4xMyAyLjI0IDEuMzIgMi4yNCAyLjYzVjE1aC0yVjguNWMwLS4yOC0uMjItLjUtLjUtLjVzLS41LjIyLS41LjVWMTVjMCAxLjEuOSAyIDIgMnMyLS45IDItMnYtNWgyeiIvPjwvc3ZnPg==';
this.id = `ngx-mat-file-input-${FileInputComponent.nextId++}`;
this.describedBy = '';
this._onChange = (_) => { };
this._onTouched = () => { };
if (this.ngControl != null) {
this.ngControl.valueAccessor = this;
}
fm.monitor(_elementRef.nativeElement, true).subscribe(origin => {
this.focused = !!origin;
this.stateChanges.next();
});
}
get fileNames() {
return this.value ? this.value.fileNames : this.valuePlaceholder;
}
writeValue(obj) {
this._renderer.setProperty(this._elementRef.nativeElement, 'value', obj instanceof FileInput ? obj.files : null);
}
registerOnChange(fn) {
this._onChange = fn;
}
registerOnTouched(fn) {
this._onTouched = fn;
}
/**
* Remove all files from the file input component
* @param [event] optional event that may have triggered the clear action
*/
clear(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
this.value = new FileInput([]);
this._previewUrls = [];
this._elementRef.nativeElement.querySelector('input').value = null;
this._onChange(this.value);
}
change(event) {
const fileList = event.target.files;
if (!fileList)
return;
if (this.multiple) {
const existingFiles = this.value?.files || [];
const newFiles = [];
for (let i = 0; i < fileList.length; i++) {
newFiles.push(fileList[i]);
}
const updatedFiles = [...existingFiles, ...newFiles];
this.value = new FileInput(updatedFiles);
}
else {
this.value = new FileInput(Array.from(fileList));
}
this._onChange(this.value);
this.updatePreviewUrls();
}
updatePreviewUrls() {
this._objectURLs = [];
if (this.value?.files?.length) {
this._previewUrls = this.value.files.map((file) => {
const isImage = file.type.startsWith('image/');
if (isImage) {
const url = URL.createObjectURL(file);
this._objectURLs.push(url);
return url;
}
else {
return this.defaultIconBase64;
}
});
}
else {
this._previewUrls = [];
}
}
revokeObjectURLs() {
this._objectURLs.forEach((url) => URL.revokeObjectURL(url));
this._objectURLs = [];
}
removeFile(index) {
if (!this.value?.files?.length)
return;
const updatedFiles = [...this.value.files];
updatedFiles.splice(index, 1);
this.value = new FileInput(updatedFiles);
this._onChange(this.value);
this.updatePreviewUrls();
}
blur() {
this.focused = false;
this._onTouched();
}
setDisabledState(isDisabled) {
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}
open() {
if (!this.disabled) {
this._elementRef.nativeElement.querySelector('input').click();
}
}
ngOnInit() {
this.multiple = coerceBooleanProperty(this.multiple);
}
ngOnDestroy() {
this.revokeObjectURLs();
this.stateChanges.complete();
this.fm.stopMonitoring(this._elementRef.nativeElement);
}
ngDoCheck() {
if (this.ngControl) {
// We need to re-evaluate this on every change detection cycle, because there are some
// error triggers that we can't subscribe to (e.g. parent form submissions). This means
// that whatever logic is in here has to be super lean or we risk destroying the performance.
this.updateErrorState();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: FileInputComponent, deps: [{ token: i1.FocusMonitor }, { token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i2.ErrorStateMatcher }, { token: i3.NgControl, optional: true, self: true }, { token: i3.NgForm, optional: true }, { token: i3.FormGroupDirective, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.0", type: FileInputComponent, isStandalone: false, selector: "ngx-mat-file-input", inputs: { autofilled: "autofilled", valuePlaceholder: "valuePlaceholder", accept: "accept", errorStateMatcher: "errorStateMatcher", defaultIconBase64: "defaultIconBase64", value: "value", multiple: "multiple", placeholder: "placeholder", required: "required", disabled: "disabled" }, host: { listeners: { "change": "change($event)", "focusout": "blur()" }, properties: { "id": "this.id", "attr.aria-describedby": "this.describedBy", "class.mat-form-field-should-float": "this.shouldLabelFloat", "class.file-input-disabled": "this.isDisabled" } }, providers: [{ provide: MatFormFieldControl, useExisting: FileInputComponent }], usesInheritance: true, ngImport: i0, template: "<input\r\n #input\r\n [id]=\"id\"\r\n type=\"file\"\r\n [attr.multiple]=\"multiple ? '' : null\"\r\n [attr.accept]=\"accept\"\r\n/>\r\n<span class=\"filename\" [title]=\"fileNames\">{{ fileNames }}</span>\r\n", styles: [":host{display:inline-block;width:100%}:host:not(.file-input-disabled){cursor:pointer}input{width:0px;height:0px;opacity:0;overflow:hidden;position:absolute;z-index:-1}.filename{display:inline-block;text-overflow:ellipsis;overflow:hidden;width:100%}\n"] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: FileInputComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-mat-file-input', providers: [{ provide: MatFormFieldControl, useExisting: FileInputComponent }], standalone: false, template: "<input\r\n #input\r\n [id]=\"id\"\r\n type=\"file\"\r\n [attr.multiple]=\"multiple ? '' : null\"\r\n [attr.accept]=\"accept\"\r\n/>\r\n<span class=\"filename\" [title]=\"fileNames\">{{ fileNames }}</span>\r\n", styles: [":host{display:inline-block;width:100%}:host:not(.file-input-disabled){cursor:pointer}input{width:0px;height:0px;opacity:0;overflow:hidden;position:absolute;z-index:-1}.filename{display:inline-block;text-overflow:ellipsis;overflow:hidden;width:100%}\n"] }]
}], ctorParameters: () => [{ type: i1.FocusMonitor }, { type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i2.ErrorStateMatcher }, { type: i3.NgControl, decorators: [{
type: Optional
}, {
type: Self
}] }, { type: i3.NgForm, decorators: [{
type: Optional
}] }, { type: i3.FormGroupDirective, decorators: [{
type: Optional
}] }], propDecorators: { autofilled: [{
type: Input
}], valuePlaceholder: [{
type: Input
}], accept: [{
type: Input
}], errorStateMatcher: [{
type: Input
}], defaultIconBase64: [{
type: Input
}], id: [{
type: HostBinding
}], describedBy: [{
type: HostBinding,
args: ['attr.aria-describedby']
}], value: [{
type: Input
}], multiple: [{
type: Input
}], placeholder: [{
type: Input
}], shouldLabelFloat: [{
type: HostBinding,
args: ['class.mat-form-field-should-float']
}], required: [{
type: Input
}], isDisabled: [{
type: HostBinding,
args: ['class.file-input-disabled']
}], disabled: [{
type: Input
}], change: [{
type: HostListener,
args: ['change', ['$event']]
}], blur: [{
type: HostListener,
args: ['focusout']
}] } });
/**
* Optional token to provide custom configuration to the module
*/
const NGX_MAT_FILE_INPUT_CONFIG = new InjectionToken('ngx-mat-file-input.config');
class ByteFormatPipe {
constructor(config) {
this.config = config;
this.unit = config ? config.sizeUnit : 'Byte';
}
transform(value, args) {
if (parseInt(value, 10) >= 0) {
value = this.formatBytes(+value, +args);
}
return value;
}
formatBytes(bytes, decimals) {
if (bytes === 0) {
return '0 ' + this.unit;
}
const B = this.unit.charAt(0);
const k = 1024;
const dm = decimals || 2;
const sizes = [this.unit, 'K' + B, 'M' + B, 'G' + B, 'T' + B, 'P' + B, 'E' + B, 'Z' + B, 'Y' + B];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ByteFormatPipe, deps: [{ token: NGX_MAT_FILE_INPUT_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.0.0", ngImport: i0, type: ByteFormatPipe, isStandalone: false, name: "byteFormat" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ByteFormatPipe, decorators: [{
type: Pipe,
args: [{
name: 'byteFormat',
standalone: false
}]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [NGX_MAT_FILE_INPUT_CONFIG]
}] }] });
class MaterialFileInputModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: MaterialFileInputModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.0", ngImport: i0, type: MaterialFileInputModule, declarations: [FileInputComponent, ByteFormatPipe], exports: [FileInputComponent, ByteFormatPipe] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: MaterialFileInputModule, providers: [FocusMonitor, ErrorStateMatcher] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: MaterialFileInputModule, decorators: [{
type: NgModule,
args: [{
declarations: [FileInputComponent, ByteFormatPipe],
providers: [FocusMonitor, ErrorStateMatcher],
exports: [FileInputComponent, ByteFormatPipe],
}]
}] });
class FileValidator {
/**
* Function to control content of files
*
* @param bytes max number of bytes allowed
*
* @returns Validator function
*/
static maxContentSize(bytes) {
return (control) => {
const size = control && control.value && control.value.files
? control.value.files
.map((file) => file.size)
.reduce((acc, fileSize) => acc + fileSize, 0)
: 0;
const isValid = bytes >= size;
return isValid
? null
: {
maxContentSize: {
actualSize: size,
maxSize: bytes,
},
};
};
}
/**
* Validator function to validate accepted file formats
*
* @param acceptedMimeTypes Array of accepted MIME types (e.g., ['image/jpeg', 'application/pdf'])
* @returns Validator function
*/
static acceptedMimeTypes(acceptedMimeTypes) {
return (control) => {
const files = control.value ? control.value.files : [];
const invalidFiles = files.filter(file => !acceptedMimeTypes.includes(file.type));
if (invalidFiles.length > 0) {
return {
acceptedMimeTypes: {
validTypes: acceptedMimeTypes
}
};
}
return null;
};
}
/**
* Validator function to validate the min number of uploaded files
*
* @param minCount Number of minimum files to upload
* @returns Validator function
*/
static minFileCount(minCount) {
return (control) => {
if (!control.value) {
return {
minFileCount: {
minCount: minCount,
actualCount: 0
}
};
}
const files = control.value.files;
if (files && files.length < minCount) {
return {
minFileCount: {
minCount: minCount,
actualCount: files.length
}
};
}
return null;
};
}
/**
* Validator function to validate the max number of uploaded files
*
* @param maxCount Number of maximum files to upload
* @returns Validator function
*/
static maxFileCount(maxCount) {
return (control) => {
if (!control.value) {
return null;
}
const files = control.value ? control.value.files : [];
if (files && files.length > maxCount) {
return {
maxFileCount: {
maxCount: maxCount,
actualCount: files.length
}
};
}
return null;
};
}
}
/*
* Public API Surface of material-file-input
*/
// Module
/**
* Generated bundle index. Do not edit.
*/
export { ByteFormatPipe, FileInput, FileInputComponent, FileValidator, MaterialFileInputModule, NGX_MAT_FILE_INPUT_CONFIG };
//# sourceMappingURL=ngx-custom-material-file-input.mjs.map