import {NgModule,Component,OnDestroy,Input,Output,EventEmitter,TemplateRef,AfterViewInit,AfterContentInit,
ContentChildren,QueryList,ViewChild,ElementRef,NgZone,ChangeDetectionStrategy, ViewEncapsulation, ChangeDetectorRef} from '@angular/core';
import {CommonModule} from '@angular/common';
import {DomSanitizer} from '@angular/platform-browser';
import {ButtonModule} from 'mirra-ui/button';
import {MessagesModule} from 'mirra-ui/messages';
import {ProgressBarModule} from 'mirra-ui/progressbar';
import {DomHandler} from 'mirra-ui/dom';
import {Message} from 'mirra-ui/api';
import {PrimeTemplate,SharedModule} from 'mirra-ui/api';
import {BlockableUI} from 'mirra-ui/api';
import {RippleModule} from 'mirra-ui/ripple';
import {HttpClient, HttpEvent, HttpEventType, HttpHeaders} from "@angular/common/http";
@Component({
selector: 'p-fileUpload',
template: `
{{file.name}}
{{formatSize(file.size)}}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
styleUrls: ['./fileupload.scss']
})
export class FileUpload implements AfterViewInit,AfterContentInit,OnDestroy,BlockableUI {
@Input() name: string;
@Input() url: string;
@Input() method: string = 'POST';
@Input() multiple: boolean;
@Input() accept: string;
@Input() disabled: boolean;
@Input() auto: boolean;
@Input() withCredentials: boolean;
@Input() maxFileSize: number;
@Input() invalidFileSizeMessageSummary: string = '{0}: Invalid file size, ';
@Input() invalidFileSizeMessageDetail: string = 'maximum upload size is {0}.';
@Input() invalidFileTypeMessageSummary: string = '{0}: Invalid file type, ';
@Input() invalidFileTypeMessageDetail: string = 'allowed file types: {0}.';
@Input() invalidFileLimitMessageDetail: string = 'limit is {0} at most.';
@Input() invalidFileLimitMessageSummary: string = 'Maximum number of files exceeded, ';
@Input() style: any;
@Input() styleClass: string;
@Input() previewWidth: number = 50;
@Input() chooseLabel: string = 'Choose';
@Input() uploadLabel: string = 'Upload';
@Input() cancelLabel: string = 'Cancel';
@Input() chooseIcon: string = 'far fa-plus';
@Input() uploadIcon: string = 'far fa-upload';
@Input() cancelIcon: string = 'far fa-times';
@Input() showUploadButton: boolean = true;
@Input() showCancelButton: boolean = true;
@Input() mode: string = 'advanced';
@Input() headers: HttpHeaders;
@Input() customUpload: boolean;
@Input() fileLimit: number;
@Output() onBeforeUpload: EventEmitter = new EventEmitter();
@Output() onSend: EventEmitter = new EventEmitter();
@Output() onUpload: EventEmitter = new EventEmitter();
@Output() onError: EventEmitter = new EventEmitter();
@Output() onClear: EventEmitter = new EventEmitter();
@Output() onRemove: EventEmitter = new EventEmitter();
@Output() onSelect: EventEmitter = new EventEmitter();
@Output() onProgress: EventEmitter = new EventEmitter();
@Output() uploadHandler: EventEmitter = new EventEmitter();
@ContentChildren(PrimeTemplate) templates: QueryList;
@ViewChild('advancedfileinput') advancedFileInput: ElementRef;
@ViewChild('basicfileinput') basicFileInput: ElementRef;
@ViewChild('content') content: ElementRef;
@Input() set files(files) {
this._files = [];
for(let i = 0; i < files.length; i++) {
let file = files[i];
if (this.validate(file)) {
if (this.isImage(file)) {
(file).objectURL = this.sanitizer.bypassSecurityTrustUrl((window.URL.createObjectURL(files[i])));
}
this._files.push(files[i]);
}
}
}
get files(): File[] {
return this._files;
}
public _files: File[] = [];
public progress: number = 0;
public dragHighlight: boolean;
public msgs: Message[];
public fileTemplate: TemplateRef;
public contentTemplate: TemplateRef;
public toolbarTemplate: TemplateRef;
public uploadedFileCount: number = 0;
focus: boolean;
uploading: boolean;
duplicateIEEvent: boolean; // flag to recognize duplicate onchange event for file input
constructor(private el: ElementRef, public sanitizer: DomSanitizer, public zone: NgZone, private http: HttpClient, public cd: ChangeDetectorRef){}
ngAfterContentInit() {
this.templates.forEach((item) => {
switch(item.getType()) {
case 'file':
this.fileTemplate = item.template;
break;
case 'content':
this.contentTemplate = item.template;
break;
case 'toolbar':
this.toolbarTemplate = item.template;
break;
default:
this.fileTemplate = item.template;
break;
}
});
}
ngAfterViewInit() {
if (this.mode === 'advanced') {
this.zone.runOutsideAngular(() => {
if (this.content)
this.content.nativeElement.addEventListener('dragover', this.onDragOver.bind(this));
});
}
}
choose() {
this.advancedFileInput.nativeElement.click();
}
onFileSelect(event) {
if (event.type !== 'drop' && this.isIE11() && this.duplicateIEEvent) {
this.duplicateIEEvent = false;
return;
}
this.msgs = [];
if (!this.multiple) {
this.files = [];
}
let files = event.dataTransfer ? event.dataTransfer.files : event.target.files;
for(let i = 0; i < files.length; i++) {
let file = files[i];
if (!this.isFileSelected(file)){
if (this.validate(file)) {
if (this.isImage(file)) {
file.objectURL = this.sanitizer.bypassSecurityTrustUrl((window.URL.createObjectURL(files[i])));
}
this.files.push(files[i]);
}
}
}
this.onSelect.emit({originalEvent: event, files: files, currentFiles: this.files});
if (this.fileLimit && this.mode == "advanced") {
this.checkFileLimit();
}
if (this.hasFiles() && this.auto && (!(this.mode === "advanced") || !this.isFileLimitExceeded())) {
this.upload();
}
if (event.type !== 'drop' && this.isIE11()) {
this.clearIEInput();
} else {
this.clearInputElement();
}
}
isFileSelected(file: File): boolean{
for(let sFile of this.files){
if ((sFile.name + sFile.type + sFile.size) === (file.name + file.type+file.size)) {
return true;
}
}
return false;
}
isIE11() {
return !!window['MSInputMethodContext'] && !!document['documentMode'];
}
validate(file: File): boolean {
if (this.accept && !this.isFileTypeValid(file)) {
this.msgs.push({
severity: 'error',
summary: this.invalidFileTypeMessageSummary.replace('{0}', file.name),
detail: this.invalidFileTypeMessageDetail.replace('{0}', this.accept)
});
return false;
}
if (this.maxFileSize && file.size > this.maxFileSize) {
this.msgs.push({
severity: 'error',
summary: this.invalidFileSizeMessageSummary.replace('{0}', file.name),
detail: this.invalidFileSizeMessageDetail.replace('{0}', this.formatSize(this.maxFileSize))
});
return false;
}
return true;
}
private isFileTypeValid(file: File): boolean {
let acceptableTypes = this.accept.split(',').map(type => type.trim());
for(let type of acceptableTypes) {
let acceptable = this.isWildcard(type) ? this.getTypeClass(file.type) === this.getTypeClass(type)
: file.type == type || this.getFileExtension(file).toLowerCase() === type.toLowerCase();
if (acceptable) {
return true;
}
}
return false;
}
getTypeClass(fileType: string): string {
return fileType.substring(0, fileType.indexOf('/'));
}
isWildcard(fileType: string): boolean {
return fileType.indexOf('*') !== -1;
}
getFileExtension(file: File): string {
return '.' + file.name.split('.').pop();
}
isImage(file: File): boolean {
return /^image\//.test(file.type);
}
onImageLoad(img: any) {
window.URL.revokeObjectURL(img.src);
}
upload() {
if (this.customUpload) {
if (this.fileLimit) {
this.uploadedFileCount += this.files.length;
}
this.uploadHandler.emit({
files: this.files
});
this.cd.markForCheck();
}
else {
this.uploading = true;
this.msgs = [];
let formData = new FormData();
this.onBeforeUpload.emit({
'formData': formData
});
for (let i = 0; i < this.files.length; i++) {
formData.append(this.name, this.files[i], this.files[i].name);
}
this.http.post(this.url, formData, {
headers: this.headers, reportProgress: true, observe: 'events', withCredentials: this.withCredentials
}).subscribe( (event: HttpEvent) => {
switch (event.type) {
case HttpEventType.Sent:
this.onSend.emit({
originalEvent: event,
'formData': formData
});
break;
case HttpEventType.Response:
this.uploading = false;
this.progress = 0;
if (event['status'] >= 200 && event['status'] < 300) {
if (this.fileLimit) {
this.uploadedFileCount += this.files.length;
}
this.onUpload.emit({originalEvent: event, files: this.files});
} else {
this.onError.emit({files: this.files});
}
this.clear();
break;
case HttpEventType.UploadProgress: {
if (event['loaded']) {
this.progress = Math.round((event['loaded'] * 100) / event['total']);
}
this.onProgress.emit({originalEvent: event, progress: this.progress});
break;
}
}
this.cd.markForCheck();
},
error => {
this.uploading = false;
this.onError.emit({files: this.files, error: error});
});
}
}
clear() {
this.files = [];
this.onClear.emit();
this.clearInputElement();
this.cd.markForCheck();
}
remove(event: Event, index: number) {
this.clearInputElement();
this.onRemove.emit({originalEvent: event, file: this.files[index]});
this.files.splice(index, 1);
}
isFileLimitExceeded() {
if (this.fileLimit && this.fileLimit <= this.files.length + this.uploadedFileCount && this.focus) {
this.focus = false;
}
return this.fileLimit && this.fileLimit < this.files.length + this.uploadedFileCount;
}
isChooseDisabled() {
return this.fileLimit && this.fileLimit <= this.files.length + this.uploadedFileCount;
}
checkFileLimit() {
if (this.isFileLimitExceeded()) {
this.msgs.push({
severity: 'error',
summary: this.invalidFileLimitMessageSummary.replace('{0}', this.fileLimit.toString()),
detail: this.invalidFileLimitMessageDetail.replace('{0}', this.fileLimit.toString())
});
}
}
clearInputElement() {
if (this.advancedFileInput && this.advancedFileInput.nativeElement) {
this.advancedFileInput.nativeElement.value = '';
}
if (this.basicFileInput && this.basicFileInput.nativeElement) {
this.basicFileInput.nativeElement.value = '';
}
}
clearIEInput() {
if (this.advancedFileInput && this.advancedFileInput.nativeElement) {
this.duplicateIEEvent = true; //IE11 fix to prevent onFileChange trigger again
this.advancedFileInput.nativeElement.value = '';
}
}
hasFiles(): boolean {
return this.files && this.files.length > 0;
}
onDragEnter(e) {
if (!this.disabled) {
e.stopPropagation();
e.preventDefault();
}
}
onDragOver(e) {
if (!this.disabled) {
DomHandler.addClass(this.content.nativeElement, 'p-fileupload-highlight');
this.dragHighlight = true;
e.stopPropagation();
e.preventDefault();
}
}
onDragLeave(event) {
if (!this.disabled) {
DomHandler.removeClass(this.content.nativeElement, 'p-fileupload-highlight');
}
}
onDrop(event) {
if (!this.disabled) {
DomHandler.removeClass(this.content.nativeElement, 'p-fileupload-highlight');
event.stopPropagation();
event.preventDefault();
let files = event.dataTransfer ? event.dataTransfer.files : event.target.files;
let allowDrop = this.multiple||(files && files.length === 1);
if (allowDrop) {
this.onFileSelect(event);
}
}
}
onFocus() {
this.focus = true;
}
onBlur() {
this.focus = false;
}
formatSize(bytes) {
if (bytes == 0) {
return '0 B';
}
let k = 1024,
dm = 3,
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
onBasicUploaderClick(event: Event) {
if (this.hasFiles())
this.upload();
else
this.basicFileInput.nativeElement.click();
}
getBlockableElement(): HTMLElement {
return this.el.nativeElement.children[0];
}
ngOnDestroy() {
if (this.content && this.content.nativeElement) {
this.content.nativeElement.removeEventListener('dragover', this.onDragOver);
}
}
}
@NgModule({
imports: [CommonModule,SharedModule,ButtonModule,ProgressBarModule,MessagesModule,RippleModule],
exports: [FileUpload,SharedModule,ButtonModule,ProgressBarModule,MessagesModule],
declarations: [FileUpload]
})
export class FileUploadModule { }