ngx-custom-material-file-input
Version:
File input management for Angular Material
497 lines (487 loc) • 19.4 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, HostListener, Input, HostBinding, Optional, Self, Component, Inject, Pipe } 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 * as i2 from '@angular/material/core';
import * as i3 from '@angular/forms';
/**
* Optional token to provide custom configuration to the module
*/
const NGX_MAT_FILE_INPUT_CONFIG = new InjectionToken('ngx-mat-file-input.config');
/**
* 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;
}
}
/** 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();
}
}
}
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 : this._internalValue;
}
set value(fileInput) {
this._internalValue = fileInput;
this.empty = !this._internalValue || !this._internalValue.files.length;
this.stateChanges.next();
this._onChange(this._internalValue);
this.updatePreviewUrls();
}
get multiple() {
return this._multiple;
}
set multiple(value) {
this._multiple = coerceBooleanProperty(value);
this.stateChanges.next();
}
get checkDuplicates() {
return this._checkDuplicates;
}
set checkDuplicates(value) {
this._checkDuplicates = coerceBooleanProperty(value);
}
get placeholder() {
return this._placeholder;
}
set placeholder(plh) {
this._placeholder = plh;
this.stateChanges.next();
}
get required() {
return this._required;
}
set required(req) {
this._required = coerceBooleanProperty(req);
this.stateChanges.next();
}
get shouldLabelFloat() {
return this.focused || !this.empty || this.valuePlaceholder !== undefined;
}
get isDisabled() {
return this.disabled;
}
get disabled() {
return this._elementRef.nativeElement.disabled;
}
set disabled(dis) {
const isDisabled = coerceBooleanProperty(dis);
this.setDisabledState(isDisabled);
this.stateChanges.next();
}
get previewUrls() {
return this._previewUrls;
}
get fileNames() {
return this._internalValue
? this._internalValue.fileNames
: this.valuePlaceholder;
}
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._checkDuplicates = false;
this._previewUrls = [];
this._objectURLs = [];
this._internalValue = null;
this.accept = null;
this.defaultIconBase64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0Ij48cGF0aCBkPSJNMCAwaDI0djI0SDBWMHoiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJNMTUgMkg2Yy0xLjEgMC0yIC45LTIgMnYxNmMwIDEuMS45IDIgMiAyaDEyYzEuMSAwIDItLjkgMi0yVjdsLTUtNXpNNiAyMFY0aDh2NGg0djEySDZ6bTEwLTEwdjVjMCAyLjIxLTEuNzkgNC00IDRzLTQtMS43OS00LTRWOC41YzAtMS40NyAxLjI2LTIuNjQgMi43Ni0yLjQ5IDEuMy4xMyAyLjI0IDEuMzIgMi4yNCAyLjYzVjE1aC0yVjguNWMwLS4yOC0uMjItLjUtLjUtLjVzLS41LjIyLS41LjVWMTVjMCAxLjEuOSAyIDIgMnMyLS45IDItMnYtNWgyeiIvPjwvc3ZnPg==';
this.empty = true;
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();
});
}
onContainerClick(event) {
if (event.target.tagName.toLowerCase() !== 'input' &&
!this.disabled) {
this._elementRef.nativeElement.querySelector('input').focus();
this.focused = true;
this.open();
}
}
writeValue(obj) {
this.value = obj;
if (!obj || obj.files.length === 0) {
const inputElement = this._elementRef.nativeElement.querySelector('input');
if (inputElement) {
inputElement.value = null;
}
}
}
registerOnChange(fn) {
this._onChange = fn;
}
registerOnTouched(fn) {
this._onTouched = fn;
}
clear(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
this.value = new FileInput([]);
const inputElement = this._elementRef.nativeElement.querySelector('input');
if (inputElement) {
inputElement.value = null;
}
}
change(event) {
const fileList = event.target.files;
if (!fileList)
return;
let newFiles = Array.from(fileList);
let currentFiles = this._internalValue?.files || [];
let filesToSet = [];
if (this.multiple) {
filesToSet = [...currentFiles];
for (const newFile of newFiles) {
let isDuplicate = false;
if (this.checkDuplicates) {
isDuplicate = currentFiles.some((existingFile) => existingFile.name === newFile.name &&
existingFile.size === newFile.size &&
existingFile.lastModified === newFile.lastModified);
}
if (!isDuplicate) {
filesToSet.push(newFile);
}
else {
console.warn(`Duplicate file skipped: ${newFile.name}`);
}
}
}
else {
filesToSet = newFiles;
}
this.value = new FileInput(filesToSet);
event.target.value = '';
}
updatePreviewUrls() {
this._objectURLs.forEach((url) => URL.revokeObjectURL(url));
this._objectURLs = [];
if (this._internalValue?.files?.length) {
this._previewUrls = this._internalValue.files.map((file) => {
if (file.type.startsWith('image/')) {
const url = URL.createObjectURL(file);
this._objectURLs.push(url);
return url;
}
else {
return this.defaultIconBase64;
}
});
}
else {
this._previewUrls = [];
}
}
removeFile(index) {
if (!this._internalValue?.files?.length)
return;
const updatedFiles = [...this._internalValue.files];
updatedFiles.splice(index, 1);
this.value = new FileInput(updatedFiles);
}
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._objectURLs.forEach((url) => URL.revokeObjectURL(url));
this._objectURLs = [];
this.stateChanges.complete();
this.fm.stopMonitoring(this._elementRef.nativeElement);
}
ngDoCheck() {
if (this.ngControl) {
this.updateErrorState();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.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: "21.0.0", type: FileInputComponent, isStandalone: true, selector: "ngx-mat-file-input", inputs: { autofilled: "autofilled", valuePlaceholder: "valuePlaceholder", accept: "accept", errorStateMatcher: "errorStateMatcher", defaultIconBase64: "defaultIconBase64", value: "value", multiple: "multiple", checkDuplicates: "checkDuplicates", 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: "21.0.0", ngImport: i0, type: FileInputComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-mat-file-input', providers: [
{ provide: MatFormFieldControl, useExisting: FileInputComponent },
], standalone: true, 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
}], checkDuplicates: [{
type: Input
}], placeholder: [{
type: Input
}], required: [{
type: Input
}], shouldLabelFloat: [{
type: HostBinding,
args: ['class.mat-form-field-should-float']
}], isDisabled: [{
type: HostBinding,
args: ['class.file-input-disabled']
}], disabled: [{
type: Input
}], change: [{
type: HostListener,
args: ['change', ['$event']]
}], blur: [{
type: HostListener,
args: ['focusout']
}] } });
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: "21.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: "21.0.0", ngImport: i0, type: ByteFormatPipe, isStandalone: true, name: "byteFormat" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: ByteFormatPipe, decorators: [{
type: Pipe,
args: [{ name: 'byteFormat', standalone: true }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [NGX_MAT_FILE_INPUT_CONFIG]
}] }] });
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
*/
// Model & Constant
/**
* Generated bundle index. Do not edit.
*/
export { ByteFormatPipe, FileInput, FileInputComponent, FileValidator, NGX_MAT_FILE_INPUT_CONFIG };
//# sourceMappingURL=ngx-custom-material-file-input.mjs.map