ngx-uploader
Version:
Angular 2+ File Uploader
463 lines (454 loc) • 19.8 kB
JavaScript
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