UNPKG

ngx-uploader

Version:

Angular 2+ File Uploader

463 lines (454 loc) 19.8 kB
import * as i0 from '@angular/core'; import { EventEmitter, Directive, Input, Output, HostListener, NgModule } from '@angular/core'; import { Subject, mergeMap, finalize, Observable } from 'rxjs'; var UploadStatus; (function (UploadStatus) { UploadStatus[UploadStatus["Queue"] = 0] = "Queue"; UploadStatus[UploadStatus["Uploading"] = 1] = "Uploading"; UploadStatus[UploadStatus["Done"] = 2] = "Done"; UploadStatus[UploadStatus["Cancelled"] = 3] = "Cancelled"; })(UploadStatus || (UploadStatus = {})); function humanizeBytes(bytes) { if (bytes === 0) { return '0 Byte'; } const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } class NgUploaderService { constructor(concurrency = Number.POSITIVE_INFINITY, contentTypes = ['*'], maxUploads = Number.POSITIVE_INFINITY, maxFileSize = Number.POSITIVE_INFINITY) { this.queue = []; this.serviceEvents = new EventEmitter(); this.uploadScheduler = new Subject(); this.subs = []; this.contentTypes = contentTypes; this.maxUploads = maxUploads; this.maxFileSize = maxFileSize; this.uploadScheduler .pipe(mergeMap(upload => this.startUpload(upload), concurrency)) .subscribe(uploadOutput => this.serviceEvents.emit(uploadOutput)); } handleFiles(incomingFiles) { const allowedIncomingFiles = [].reduce.call(incomingFiles, (acc, checkFile, i) => { const futureQueueLength = acc.length + this.queue.length + 1; if (this.isContentTypeAllowed(checkFile.type) && futureQueueLength <= this.maxUploads && this.isFileSizeAllowed(checkFile.size)) { acc = acc.concat(checkFile); } else { const rejectedFile = this.makeUploadFile(checkFile, i); this.serviceEvents.emit({ type: 'rejected', file: rejectedFile }); } return acc; }, []); this.queue.push(...allowedIncomingFiles.map((file, i) => { const uploadFile = this.makeUploadFile(file, i); this.serviceEvents.emit({ type: 'addedToQueue', file: uploadFile }); return uploadFile; })); this.serviceEvents.emit({ type: 'allAddedToQueue' }); } initInputEvents(input) { return input.subscribe((event) => { switch (event.type) { case 'uploadFile': const uploadFileIndex = this.queue.findIndex(file => file === event.file); if (uploadFileIndex !== -1 && event.file) { this.uploadScheduler.next({ file: this.queue[uploadFileIndex], event: event }); } break; case 'uploadAll': const files = this.queue.filter(file => file.progress.status === UploadStatus.Queue); files.forEach(file => this.uploadScheduler.next({ file: file, event: event })); break; case 'cancel': const id = event.id || null; if (!id) { return; } const subs = this.subs.filter(sub => sub.id === id); subs.forEach(sub => { if (sub.sub) { sub.sub.unsubscribe(); const fileIndex = this.queue.findIndex(file => file.id === id); if (fileIndex !== -1) { this.queue[fileIndex].progress.status = UploadStatus.Cancelled; this.serviceEvents.emit({ type: 'cancelled', file: this.queue[fileIndex] }); } } }); break; case 'cancelAll': this.subs.forEach(sub => { if (sub.sub) { sub.sub.unsubscribe(); } const file = this.queue.find(uploadFile => uploadFile.id === sub.id); if (file) { file.progress.status = UploadStatus.Cancelled; this.serviceEvents.emit({ type: 'cancelled', file: file }); } }); break; case 'remove': if (!event.id) { return; } const i = this.queue.findIndex(file => file.id === event.id); if (i !== -1) { const file = this.queue[i]; this.queue.splice(i, 1); this.serviceEvents.emit({ type: 'removed', file: file }); } break; case 'removeAll': if (this.queue.length) { this.queue = []; this.serviceEvents.emit({ type: 'removedAll' }); } break; } }); } startUpload(upload) { return new Observable(observer => { const sub = this.uploadFile(upload.file, upload.event) .pipe(finalize(() => { if (!observer.closed) { observer.complete(); } })) .subscribe({ next: (output) => { observer.next(output); }, error: (err) => { observer.error(err); observer.complete(); }, complete: () => { observer.complete(); } }); this.subs.push({ id: upload.file.id, sub: sub }); }); } uploadFile(file, event) { return new Observable(observer => { const url = event.url || ''; const method = event.method || 'POST'; const data = event.data || {}; const headers = event.headers || {}; const xhr = new XMLHttpRequest(); const time = new Date().getTime(); let progressStartTime = (file.progress.data && file.progress.data.startTime) || time; let speed = 0; let eta = null; xhr.open(method, url, true); xhr.withCredentials = event.withCredentials ? true : false; xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const percentage = Math.round((e.loaded * 100) / e.total); const diff = new Date().getTime() - time; speed = Math.round((e.loaded / diff) * 1000); progressStartTime = (file.progress.data && file.progress.data.startTime) || new Date().getTime(); eta = Math.ceil((e.total - e.loaded) / speed); file.progress = { status: UploadStatus.Uploading, data: { percentage: percentage, speed: speed, speedHuman: `${humanizeBytes(speed)}/s`, startTime: progressStartTime, endTime: null, eta: eta, etaHuman: this.secondsToHuman(eta) } }; observer.next({ type: 'uploading', file: file }); } }; xhr.upload.ontimeout = (e) => { observer.error(e); observer.complete(); }; xhr.upload.onerror = (e) => { observer.error(e); observer.complete(); }; xhr.upload.onabort = () => { observer.complete(); }; xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { const speedAverage = Math.round((file.size / (new Date().getTime() - progressStartTime)) * 1000); file.progress = { status: UploadStatus.Done, data: { percentage: 100, speed: speedAverage, speedHuman: `${humanizeBytes(speedAverage)}/s`, startTime: progressStartTime, endTime: new Date().getTime(), eta: eta, etaHuman: this.secondsToHuman(eta || 0) } }; file.responseStatus = xhr.status; try { file.response = JSON.parse(xhr.response); } catch (e) { file.response = xhr.response; } file.responseHeaders = this.parseResponseHeaders(xhr.getAllResponseHeaders()); observer.next({ type: 'done', file: file }); observer.complete(); } }; try { const uploadFile = file.nativeFile; const uploadIndex = this.queue.findIndex(outFile => outFile.nativeFile === uploadFile); if (this.queue[uploadIndex].progress.status === UploadStatus.Cancelled) { observer.complete(); } Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key])); let bodyToSend; if (event.includeWebKitFormBoundary !== false) { Object.keys(data).forEach(key => file.form.append(key, data[key])); file.form.append(event.fieldName || 'file', uploadFile, uploadFile.name); bodyToSend = file.form; } else { bodyToSend = uploadFile; } this.serviceEvents.emit({ type: 'start', file: file }); xhr.send(bodyToSend); } catch (e) { observer.complete(); } return () => { xhr.abort(); }; }); } secondsToHuman(sec) { return new Date(sec * 1000).toISOString().substr(11, 8); } generateId() { return Math.random().toString(36).substring(7); } setContentTypes(contentTypes) { if (typeof contentTypes !== 'undefined' && contentTypes instanceof Array) { if (contentTypes.find((type) => type === '*') !== undefined) { this.contentTypes = ['*']; } else { this.contentTypes = contentTypes; } return; } this.contentTypes = ['*']; } allContentTypesAllowed() { return this.contentTypes.find((type) => type === '*') !== undefined; } isContentTypeAllowed(mimetype) { if (this.allContentTypesAllowed()) { return true; } return this.contentTypes.find((type) => type === mimetype) !== undefined; } isFileSizeAllowed(fileSize) { if (!this.maxFileSize) { return true; } return fileSize <= this.maxFileSize; } makeUploadFile(file, index) { return { fileIndex: index, id: this.generateId(), name: file.name, size: file.size, type: file.type, form: new FormData(), progress: { status: UploadStatus.Queue, data: { percentage: 0, speed: 0, speedHuman: `${humanizeBytes(0)}/s`, startTime: null, endTime: null, eta: null, etaHuman: null } }, lastModifiedDate: new Date(file.lastModified), sub: undefined, nativeFile: file }; } parseResponseHeaders(httpHeaders) { if (!httpHeaders) { return {}; } return httpHeaders .split('\n') .map((x) => x.split(/: */, 2)) .filter((x) => x[0]) .reduce((acc, x) => { acc[x[0]] = x[1]; return acc; }, {}); } } class NgFileDropDirective { constructor(elementRef) { this.elementRef = elementRef; this.stopEvent = (e) => { e.stopPropagation(); e.preventDefault(); }; this.uploadOutput = new EventEmitter(); } ngOnInit() { this._sub = []; const concurrency = (this.options && this.options.concurrency) || Number.POSITIVE_INFINITY; const allowedContentTypes = (this.options && this.options.allowedContentTypes) || ['*']; const maxUploads = (this.options && this.options.maxUploads) || Number.POSITIVE_INFINITY; const maxFileSize = (this.options && this.options.maxFileSize) || Number.POSITIVE_INFINITY; this.upload = new NgUploaderService(concurrency, allowedContentTypes, maxUploads, maxFileSize); this.el = this.elementRef.nativeElement; this._sub.push(this.upload.serviceEvents.subscribe((event) => { this.uploadOutput.emit(event); })); if (this.uploadInput instanceof EventEmitter) { this._sub.push(this.upload.initInputEvents(this.uploadInput)); } this.el.addEventListener('drop', this.stopEvent, false); this.el.addEventListener('dragenter', this.stopEvent, false); this.el.addEventListener('dragover', this.stopEvent, false); } ngOnDestroy() { if (this._sub) { this._sub.forEach(sub => sub.unsubscribe()); } } onDrop(e) { e.stopPropagation(); e.preventDefault(); const event = { type: 'drop' }; this.uploadOutput.emit(event); this.upload.handleFiles(e.dataTransfer.files); } onDragOver(e) { if (!e) { return; } const event = { type: 'dragOver' }; this.uploadOutput.emit(event); } onDragLeave(e) { if (!e) { return; } const event = { type: 'dragOut' }; this.uploadOutput.emit(event); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.5", ngImport: i0, type: NgFileDropDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.0.5", type: NgFileDropDirective, selector: "[ngFileDrop]", inputs: { options: "options", uploadInput: "uploadInput" }, outputs: { uploadOutput: "uploadOutput" }, host: { listeners: { "drop": "onDrop($event)", "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)" } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.5", ngImport: i0, type: NgFileDropDirective, decorators: [{ type: Directive, args: [{ selector: '[ngFileDrop]' }] }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { options: [{ type: Input }], uploadInput: [{ type: Input }], uploadOutput: [{ type: Output }], onDrop: [{ type: HostListener, args: ['drop', ['$event']] }], onDragOver: [{ type: HostListener, args: ['dragover', ['$event']] }], onDragLeave: [{ type: HostListener, args: ['dragleave', ['$event']] }] } }); class NgFileSelectDirective { constructor(elementRef) { this.elementRef = elementRef; this.fileListener = () => { if (this.el.files) { this.upload.handleFiles(this.el.files); } }; this.uploadOutput = new EventEmitter(); } ngOnInit() { this._sub = []; const concurrency = (this.options && this.options.concurrency) || Number.POSITIVE_INFINITY; const allowedContentTypes = (this.options && this.options.allowedContentTypes) || ['*']; const maxUploads = (this.options && this.options.maxUploads) || Number.POSITIVE_INFINITY; const maxFileSize = (this.options && this.options.maxFileSize) || Number.POSITIVE_INFINITY; this.upload = new NgUploaderService(concurrency, allowedContentTypes, maxUploads, maxFileSize); this.el = this.elementRef.nativeElement; this.el.addEventListener('change', this.fileListener, false); this._sub.push(this.upload.serviceEvents.subscribe((event) => { this.uploadOutput.emit(event); })); if (this.uploadInput instanceof EventEmitter) { this._sub.push(this.upload.initInputEvents(this.uploadInput)); } } ngOnDestroy() { if (this.el) { this.el.removeEventListener('change', this.fileListener, false); this._sub.forEach(sub => sub.unsubscribe()); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.5", ngImport: i0, type: NgFileSelectDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.0.5", type: NgFileSelectDirective, selector: "[ngFileSelect]", inputs: { options: "options", uploadInput: "uploadInput" }, outputs: { uploadOutput: "uploadOutput" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.5", ngImport: i0, type: NgFileSelectDirective, decorators: [{ type: Directive, args: [{ selector: '[ngFileSelect]' }] }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { options: [{ type: Input }], uploadInput: [{ type: Input }], uploadOutput: [{ type: Output }] } }); class NgxUploaderModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.5", ngImport: i0, type: NgxUploaderModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.0.5", ngImport: i0, type: NgxUploaderModule, declarations: [NgFileDropDirective, NgFileSelectDirective], exports: [NgFileDropDirective, NgFileSelectDirective] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.0.5", ngImport: i0, type: NgxUploaderModule }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.5", ngImport: i0, type: NgxUploaderModule, decorators: [{ type: NgModule, args: [{ declarations: [NgFileDropDirective, NgFileSelectDirective], exports: [NgFileDropDirective, NgFileSelectDirective] }] }] }); /* * Public API Surface of ngx-uploader */ /** * Generated bundle index. Do not edit. */ export { NgFileDropDirective, NgFileSelectDirective, NgUploaderService, NgxUploaderModule, UploadStatus, humanizeBytes }; //# sourceMappingURL=ngx-uploader.mjs.map