ngx-om-material-file-input
Version:
File input management for Angular Material
447 lines (436 loc) • 17.7 kB
JavaScript
import * as i0 from '@angular/core';
import { Component, Optional, Self, Input, HostBinding, HostListener, InjectionToken, Pipe, Inject, NgModule } from '@angular/core';
import * as i1 from '@angular/cdk/a11y';
import { FocusMonitor } from '@angular/cdk/a11y';
import { MatFormFieldControl } from '@angular/material/form-field';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Subject } from 'rxjs';
import * as i2 from '@angular/material/core';
import * as i3 from '@angular/forms';
/**
* The files to be uploaded
*/
class FileInput {
_files;
delimiter;
_fileNames;
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 that tracks the error state of a component.
* @docs-private
*/
class ErrorStateTracker {
_defaultMatcher;
_parentFormGroup;
_parentForm;
_stateChanges;
ngControl;
errorState;
matcher;
constructor(_defaultMatcher, ngControl, _parentFormGroup, _parentForm, _stateChanges) {
this._defaultMatcher = _defaultMatcher;
this.ngControl = ngControl;
this._parentFormGroup = _parentFormGroup;
this._parentForm = _parentForm;
this._stateChanges = _stateChanges;
/** Whether the tracker is currently in an error state. */
this.errorState = false;
}
/** Updates the error state based on the provided error state matcher. */
updateErrorState() {
const oldState = this.errorState;
const parent = this._parentFormGroup || this._parentForm;
const matcher = this.matcher || this._defaultMatcher;
const control = this.ngControl ? this.ngControl.control : null;
const newState = matcher?.isErrorState(control, parent) ?? false;
if (newState !== oldState) {
this.errorState = newState;
this._stateChanges.next();
}
}
}
function mixinErrorState(base) {
return class extends base {
/** Whether the component is in an error state. */
get errorState() {
return this._getTracker().errorState;
}
set errorState(value) {
this._getTracker().errorState = value;
}
/** An object used to control the error state of the component. */
get errorStateMatcher() {
return this._getTracker().matcher;
}
set errorStateMatcher(value) {
this._getTracker().matcher = value;
}
/** Updates the error state based on the provided error state matcher. */
updateErrorState() {
this._getTracker().updateErrorState();
}
_getTracker() {
if (!this._tracker) {
this._tracker = new ErrorStateTracker(this._defaultErrorStateMatcher, this.ngControl, this._parentFormGroup, this._parentForm, this.stateChanges);
}
return this._tracker;
}
constructor(...args) {
super(...args);
}
};
}
// Boilerplate for applying mixins to FileInput
/** @docs-private */
class FileInputBase {
_defaultErrorStateMatcher;
_parentForm;
_parentFormGroup;
ngControl;
stateChanges;
constructor(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl, stateChanges) {
this._defaultErrorStateMatcher = _defaultErrorStateMatcher;
this._parentForm = _parentForm;
this._parentFormGroup = _parentFormGroup;
this.ngControl = ngControl;
this.stateChanges = stateChanges;
}
}
/**
* Allows to use a custom ErrorStateMatcher with the file-input component
*/
const FileInputMixinBase = mixinErrorState(FileInputBase);
class FileInputComponent extends FileInputMixinBase {
fm;
_elementRef;
_renderer;
_defaultErrorStateMatcher;
ngControl;
_parentForm;
_parentFormGroup;
static nextId = 0;
focused = false;
controlType = 'file-input';
autofilled = false;
_placeholder;
_required = false;
_multiple;
valuePlaceholder;
accept = null;
_errorStateMatcher;
get errorStateMatcher() {
return this._errorStateMatcher;
}
set errorStateMatcher(value) {
this._errorStateMatcher = value;
}
id = `ngx-mat-file-input-${FileInputComponent.nextId++}`;
describedBy = '';
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();
}
onContainerClick(event) {
if (event.target.tagName.toLowerCase() !== 'input' && !this.disabled) {
this._elementRef.nativeElement.querySelector('input').focus();
this.focused = true;
this.open();
}
}
/** Whether the component is in an error state. */
_errorState = false;
get errorState() {
return this._errorState;
}
set errorState(value) {
this._errorState = value;
}
/**
* @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;
if (this.ngControl != null) {
this.ngControl.valueAccessor = this;
}
fm.monitor(_elementRef.nativeElement, true).subscribe((origin) => {
this.focused = !!origin;
this.stateChanges.next();
});
}
_onChange = (_) => { };
_onTouched = () => { };
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._elementRef.nativeElement.querySelector('input').value = null;
this._onChange(this.value);
}
change(event) {
const fileList = event.target.files;
const fileArray = [];
if (fileList) {
for (let i = 0; i < fileList.length; i++) {
fileArray.push(fileList[i]);
}
}
this.value = new FileInput(fileArray);
this._onChange(this.value);
}
blur() {
this.focused = false;
this._onTouched();
}
setDisabledState(isDisabled) {
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}
ngOnInit() {
this.multiple = coerceBooleanProperty(this.multiple);
}
open() {
if (!this.disabled) {
this._elementRef.nativeElement.querySelector('input').click();
}
}
ngOnDestroy() {
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();
}
}
stateChanges = new Subject();
updateErrorState() {
const parentSubmitted = this._parentFormGroup?.submitted || this._parentForm?.submitted;
const touchedOrParentSubmitted = parentSubmitted;
const newState = this.ngControl?.invalid && touchedOrParentSubmitted;
if (this.errorState !== newState) {
this.errorState = newState;
this.stateChanges.next(); // Notify listeners of state changes.
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", 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 ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.0.5", type: FileInputComponent, isStandalone: false, selector: "ngx-mat-file-input", inputs: { autofilled: "autofilled", valuePlaceholder: "valuePlaceholder", accept: "accept", _errorStateMatcher: "_errorStateMatcher", 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 #input type=\"file\" [attr.multiple]=\"multiple? '' : null\" [attr.accept]=\"accept\">\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:0;height:0;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: "19.0.5", ngImport: i0, type: FileInputComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-mat-file-input', providers: [{ provide: MatFormFieldControl, useExisting: FileInputComponent }], standalone: false, template: "<input #input type=\"file\" [attr.multiple]=\"multiple? '' : null\" [attr.accept]=\"accept\">\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:0;height:0;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
}], 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 {
config;
unit;
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 ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ByteFormatPipe, deps: [{ token: NGX_MAT_FILE_INPUT_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Pipe });
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: ByteFormatPipe, isStandalone: false, name: "byteFormat" });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", 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 ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: MaterialFileInputModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: MaterialFileInputModule, declarations: [FileInputComponent, ByteFormatPipe], exports: [FileInputComponent, ByteFormatPipe] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: MaterialFileInputModule, providers: [FocusMonitor] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: MaterialFileInputModule, decorators: [{
type: NgModule,
args: [{
declarations: [FileInputComponent, ByteFormatPipe],
providers: [FocusMonitor],
exports: [FileInputComponent, ByteFormatPipe]
}]
}] });
class FileValidator {
/**
* Function to control content of files
*
* @param bytes max number of bytes allowed
*
* @returns
*/
static maxContentSize(bytes) {
return (control) => {
const size = control && control.value ? control.value.files.map(f => f.size).reduce((acc, i) => acc + i, 0) : 0;
const condition = bytes >= size;
return condition
? null
: {
maxContentSize: {
actualSize: size,
maxSize: bytes
}
};
};
}
}
// Module
/**
* Generated bundle index. Do not edit.
*/
export { ByteFormatPipe, FileInput, FileInputComponent, FileValidator, MaterialFileInputModule, NGX_MAT_FILE_INPUT_CONFIG };
//# sourceMappingURL=ngx-om-material-file-input.mjs.map