UNPKG

ngx-custom-material-file-input

Version:
497 lines (487 loc) 19.4 kB
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