UNPKG

ngx-custom-material-file-input

Version:
483 lines (472 loc) 20 kB
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