@trendster-io/ng-uploader
Version:
A fully featured Angular file uploader that utilizes Angular's HttpClient internally, allows you to customize outgoing HTTP requests using the "Angular Way" (Interceptors), provides customizable concurrent queuing of uploads and a simple way to select fil
1,044 lines (1,029 loc) • 27.9 kB
JavaScript
import { Observable, Subject, of } from 'rxjs';
import { mergeMap, tap, filter, map, catchError, finalize } from 'rxjs/operators';
import { HttpRequest, HttpEventType, HttpClient } from '@angular/common/http';
import { Injectable, NgModule, Directive, EventEmitter, HostBinding, HostListener, Input, Output, defineInjectable, inject } from '@angular/core';
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class UploadItem {
/**
* @param {?} listener
* @param {?} options
*/
constructor(listener, options) {
this.listener = listener;
this.onResponseSubject = new Subject();
this.onProgressSubject = new Subject();
this.onErrorSubject = new Subject();
this.onResponse = this.onResponseSubject.asObservable();
this.onProgress = this.onProgressSubject.asObservable();
this.onError = this.onErrorSubject.asObservable();
this._progress = 0;
this.url = options.url;
this.method = options.method;
this.file = options.file;
this.fileAlias = options.fileAlias || 'file';
this.additionalParams = options.additionalParams;
this._req = this.buildRequest();
/** @type {?} */
const fileCategory = this.file.type.split('/')[0];
if (fileCategory === 'image' || fileCategory === 'video') {
this.preview = this.readMediaFile();
}
}
/**
* @return {?}
*/
get req() {
return this._req;
}
/**
* @return {?}
*/
get progress() {
return this._progress;
}
/**
* @return {?}
*/
get fileName() {
return this.file.name;
}
/**
* @return {?}
*/
get isReady() {
return this.ready;
}
/**
* @return {?}
*/
get isUploading() {
return this.uploading;
}
/**
* @return {?}
*/
get isUploaded() {
return this.uploaded;
}
/**
* @return {?}
*/
get isCancelled() {
return this.cancelled;
}
/**
* @return {?}
*/
get isError() {
return this.error;
}
/**
* @private
* @return {?}
*/
buildFormData() {
/** @type {?} */
const formData = new FormData();
if (this.additionalParams) {
Object.entries(this.additionalParams).forEach((/**
* @param {?} __0
* @return {?}
*/
([key, value]) => formData.set(key, value)));
}
formData.set(this.fileAlias, this.file, this.file.name);
return formData;
}
/**
* @private
* @return {?}
*/
buildRequest() {
/** @type {?} */
const body = this.buildFormData();
return new HttpRequest(this.method, this.url, body, {
reportProgress: true
});
}
/**
* @private
* @return {?}
*/
readMediaFile() {
return new Observable((/**
* @param {?} observer
* @return {?}
*/
observer => {
/** @type {?} */
const reader = new FileReader();
reader.onload = (/**
* @return {?}
*/
() => {
observer.next((/** @type {?} */ (reader.result)));
observer.complete();
});
reader.onerror = (/**
* @return {?}
*/
() => {
reader.abort();
observer.error(reader.error);
});
reader.readAsDataURL(this.file);
}));
}
/**
* @return {?}
*/
cleanup() {
this.onResponseSubject.complete();
this.onProgressSubject.complete();
this.onErrorSubject.complete();
}
/**
* @return {?}
*/
onBeforeUpload() {
this.ready = true;
this.uploading = false;
this.uploaded = false;
this.cancelled = false;
this.error = false;
this._progress = 0;
}
/**
* @return {?}
*/
onUploadStart() {
this.ready = true;
this.uploading = true;
this.uploaded = false;
this.cancelled = false;
this.error = false;
}
/**
* @param {?} res
* @return {?}
*/
onUploadSuccess(res) {
this.ready = false;
this.uploading = false;
this.uploaded = true;
this.cancelled = false;
this.error = false;
this._progress = 100;
this.onResponseSubject.next(res);
this.cleanup();
}
/**
* @param {?} progress
* @return {?}
*/
onUploadProgress(progress) {
this._progress = progress;
this.onProgressSubject.next(this._progress);
}
/**
* @return {?}
*/
onUploadCancelled() {
this.ready = false;
this.uploading = false;
this.uploaded = false;
this.cancelled = true;
this.error = false;
this._progress = 0;
}
/**
* @param {?} err
* @return {?}
*/
onUploadError(err) {
this.ready = false;
this.uploading = false;
this.uploaded = false;
this.cancelled = false;
this.error = true;
this._progress = 0;
this.onErrorSubject.next(err);
}
/**
* @param {?=} additionalParams
* @return {?}
*/
upload(additionalParams) {
if (additionalParams) {
this.additionalParams = additionalParams;
this._req = this.buildRequest();
}
this.listener.uploadItem(this);
}
/**
* @return {?}
*/
cancel() {
this.listener.cancelItem(this);
}
/**
* @return {?}
*/
remove() {
this.listener.removeItem(this);
}
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class Uploader {
/**
* @param {?} httpClient
* @param {?=} options
*/
constructor(httpClient, options = {}) {
this.httpClient = httpClient;
this.onItemSuccessSubject = new Subject();
this.onItemProgressSubject = new Subject();
this.onItemErrorSubject = new Subject();
this.onItemSuccess = this.onItemSuccessSubject.asObservable();
this.onItemProgress = this.onItemProgressSubject.asObservable();
this.onItemError = this.onItemErrorSubject.asObservable();
this._totalProgress = 0;
this.url = options.url;
this.method = options.method;
this.autoUpload = options.autoUpload;
this.removeAfterUpload = options.removeAfterUpload;
this.queueLimit = options.queueLimit;
this.concurrency = options.concurrency || 1;
this.maxFileSize = options.maxFileSize;
this.allowedMimeTypes = options.allowedMimeTypes;
this.setupQueue();
}
/**
* @return {?}
*/
get itemsCount() {
return this.queue.length;
}
/**
* @return {?}
*/
get items() {
return this.queue.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item));
}
/**
* @return {?}
*/
get readyItemsCount() {
return this.queue
.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item))
.filter((/**
* @param {?} item
* @return {?}
*/
item => item.isReady && !item.isUploading)).length;
}
/**
* @private
* @return {?}
*/
get readyItems() {
return this.queue
.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item))
.filter((/**
* @param {?} item
* @return {?}
*/
item => item.isReady && !item.isUploading));
}
/**
* @return {?}
*/
get uploadingItemsCount() {
return this.queue
.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item))
.filter((/**
* @param {?} item
* @return {?}
*/
item => item.isUploading)).length;
}
/**
* @private
* @return {?}
*/
get uploadingItems() {
return this.queue
.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item))
.filter((/**
* @param {?} item
* @return {?}
*/
item => item.isUploading));
}
/**
* @return {?}
*/
get uploadedItemsCount() {
return this.queue
.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item))
.filter((/**
* @param {?} item
* @return {?}
*/
item => item.isUploaded)).length;
}
/**
* @private
* @return {?}
*/
get uploadedItems() {
return this.queue
.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item))
.filter((/**
* @param {?} item
* @return {?}
*/
item => item.isUploaded));
}
/**
* @return {?}
*/
get notUploadedItemsCount() {
return this.queue
.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item))
.filter((/**
* @param {?} item
* @return {?}
*/
item => !item.isUploaded)).length;
}
/**
* @private
* @return {?}
*/
get notUploadedItems() {
return this.queue
.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item))
.filter((/**
* @param {?} item
* @return {?}
*/
item => !item.isUploaded));
}
/**
* @return {?}
*/
get cancelledItemsCount() {
return this.queue
.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item))
.filter((/**
* @param {?} item
* @return {?}
*/
item => item.isCancelled)).length;
}
/**
* @private
* @return {?}
*/
get cancelledItems() {
return this.queue
.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item))
.filter((/**
* @param {?} item
* @return {?}
*/
item => item.isCancelled));
}
/**
* @return {?}
*/
get errorItemsCount() {
return this.queue
.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item))
.filter((/**
* @param {?} item
* @return {?}
*/
item => item.isError)).length;
}
/**
* @private
* @return {?}
*/
get errorItems() {
return this.queue
.map((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => queueItem.item))
.filter((/**
* @param {?} item
* @return {?}
*/
item => item.isError));
}
/**
* @return {?}
*/
get totalProgress() {
return this._totalProgress;
}
/**
* @return {?}
*/
get isUploading() {
return this.uploadingItemsCount > 0;
}
/**
* @private
* @param {?} __0
* @return {?}
*/
onQueueItemSuccess({ item, res }) {
item.onUploadSuccess(res);
this.onItemSuccessSubject.next({ item, res });
if (this.removeAfterUpload) {
item.remove();
}
}
/**
* @private
* @param {?} __0
* @return {?}
*/
onQueueItemError({ item, err }) {
item.onUploadError(err);
this.onItemErrorSubject.next({ item, err });
}
/**
* @private
* @param {?} req
* @return {?}
*/
request(req) {
return this.httpClient.request(req).pipe(filter((/**
* @param {?} event
* @return {?}
*/
event => event.type === HttpEventType.Sent ||
event.type === HttpEventType.UploadProgress ||
event.type === HttpEventType.Response)), map((/**
* @param {?} event
* @return {?}
*/
event => {
switch (event.type) {
case HttpEventType.Sent:
return { type: 'start', value: null };
case HttpEventType.UploadProgress:
/** @type {?} */
const progress = Math.round((100 * event.loaded) / event.total);
return { type: 'progress', value: progress };
case HttpEventType.Response:
return { type: 'response', value: event.body };
}
})));
}
/**
* @private
* @return {?}
*/
calculateTotalProgress() {
/** @type {?} */
const value = this.uploadingItems.reduce((/**
* @param {?} acc
* @param {?} item
* @return {?}
*/
(acc, item) => item.progress + acc), 0) /
(this.uploadingItemsCount || 1);
if (this.removeAfterUpload) {
this._totalProgress = value;
}
else {
/** @type {?} */
const ratio = 100 / this.itemsCount;
/** @type {?} */
const current = (value * ratio) / 100;
this._totalProgress = Math.round(this.uploadedItemsCount * ratio + current);
}
}
/**
* @private
* @param {?} queueItem
* @return {?}
*/
prepareQueueItem(queueItem) {
return new Observable((/**
* @param {?} observer
* @return {?}
*/
observer => {
queueItem.subscription = this.request(queueItem.item.req)
.pipe(finalize((/**
* @return {?}
*/
() => {
if (!observer.closed) {
observer.complete();
}
})), tap((/**
* @param {?} event
* @return {?}
*/
event => {
switch (event.type) {
case 'start':
queueItem.item.onUploadStart();
break;
case 'progress':
/** @type {?} */
const progress = event.value;
/** @type {?} */
const item = queueItem.item;
item.onUploadProgress(progress);
this.onItemProgressSubject.next({ item, progress });
this.calculateTotalProgress();
break;
default:
break;
}
})), filter((/**
* @param {?} event
* @return {?}
*/
event => event.type === 'response')), map((/**
* @param {?} event
* @return {?}
*/
event => event.value)))
.subscribe({
next: (/**
* @param {?} res
* @return {?}
*/
res => {
queueItem.subscription = null;
observer.next({ item: queueItem.item, res });
observer.complete();
}),
error: (/**
* @param {?} err
* @return {?}
*/
(err) => {
queueItem.subscription = null;
observer.error({ item: queueItem.item, err });
}),
complete: (/**
* @return {?}
*/
() => observer.complete())
});
}));
}
/**
* @private
* @return {?}
*/
setupQueue() {
this.queue = [];
this.scheduler = new Subject();
this.scheduler
.pipe(mergeMap((/**
* @param {?} queueItem
* @return {?}
*/
queueItem => this.prepareQueueItem(queueItem).pipe(catchError((/**
* @param {?} uploadEvent
* @return {?}
*/
(uploadEvent) => {
this.onQueueItemError(uploadEvent);
return of(null);
})))), this.concurrency), filter((/**
* @param {?} uploadEvent
* @return {?}
*/
(uploadEvent) => Boolean(uploadEvent))))
.subscribe((/**
* @param {?} uploadEvent
* @return {?}
*/
uploadEvent => this.onQueueItemSuccess(uploadEvent)));
}
/**
* @private
* @param {?} __0
* @return {?}
*/
isValidRequestParams({ url, method }) {
return Boolean(url || this.url) && Boolean(method || this.method);
}
/**
* @private
* @param {?} file
* @return {?}
*/
isValidFileType(file) {
return this.allowedMimeTypes && this.allowedMimeTypes.length
? this.allowedMimeTypes.includes(file.type)
: true;
}
/**
* @private
* @param {?} file
* @return {?}
*/
isValidFileSize(file) {
return this.maxFileSize ? file.size <= this.maxFileSize : true;
}
/**
* @private
* @param {?} file
* @return {?}
*/
isValidFile(file) {
return this.isValidFileType(file) && this.isValidFileSize(file);
}
/**
* @private
* @return {?}
*/
canAddToQueue() {
return this.queueLimit ? this.itemsCount < this.queueLimit : true;
}
/**
* @private
* @param {?} options
* @return {?}
*/
checkItem(options) {
if (!this.isValidRequestParams(options)) {
throw {
name: 'RequestParamsMissing',
message: 'You have not supplied a general url or method to the uploader and also didn\'t provide them in your item.'
};
}
if (!this.isValidFileType(options.file)) {
throw {
name: 'NotAllowedFileType',
message: 'The file type is not included in the allowedMimeTypes.'
};
}
if (!this.isValidFileSize(options.file)) {
throw {
name: 'FileSizeExceeded',
message: 'The file size exceeded the maximumFileSize.'
};
}
if (!this.canAddToQueue()) {
throw {
name: 'QueueLimitReached',
message: 'The specified queueLimit was reached, cannot add more items at the moment.'
};
}
}
/**
* @param {?} options
* @return {?}
*/
addItem(options) {
this.checkItem(options);
Object.assign(options, {
url: options.url || this.url,
method: options.method || this.method
});
/** @type {?} */
const item = new UploadItem(this, options);
this.queue.push({ item, subscription: null });
this.calculateTotalProgress();
if (this.autoUpload) {
item.upload();
}
return item;
}
/**
* @param {?} item
* @return {?}
*/
uploadItem(item) {
/** @type {?} */
const queueItem = this.queue.find((/**
* @param {?} current
* @return {?}
*/
current => current.item === item &&
!(current.item.isReady ||
current.item.isUploading ||
current.item.isUploaded)));
if (queueItem) {
item.onBeforeUpload();
this.scheduler.next(queueItem);
}
}
/**
* @return {?}
*/
uploadAll() {
this.items
.filter((/**
* @param {?} item
* @return {?}
*/
item => !(item.isReady || item.isUploading || item.isUploaded)))
.forEach((/**
* @param {?} item
* @return {?}
*/
item => item.upload()));
}
/**
* @param {?} item
* @return {?}
*/
cancelItem(item) {
/** @type {?} */
const queueItem = this.queue.find((/**
* @param {?} current
* @return {?}
*/
current => current.item === item && item.isReady));
if (queueItem) {
if (queueItem.subscription) {
queueItem.subscription.unsubscribe();
queueItem.subscription = null;
}
queueItem.item.onUploadCancelled();
this.calculateTotalProgress();
}
}
/**
* @return {?}
*/
cancelAll() {
this.notUploadedItems.forEach((/**
* @param {?} item
* @return {?}
*/
item => item.cancel()));
}
/**
* @param {?} item
* @return {?}
*/
removeItem(item) {
if (item.isUploading) {
item.cancel();
}
item.cleanup();
this.queue = this.queue.filter((/**
* @param {?} current
* @return {?}
*/
current => current.item !== item));
this.calculateTotalProgress();
}
/**
* @return {?}
*/
clearQueue() {
while (this.itemsCount) {
this.queue[0].item.remove();
}
this._totalProgress = 0;
}
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class UploaderService {
/**
* @param {?} httpClient
*/
constructor(httpClient) {
this.httpClient = httpClient;
}
/**
* @param {?} options
* @return {?}
*/
getUploader(options) {
return new Uploader(this.httpClient, options);
}
}
UploaderService.decorators = [
{ type: Injectable, args: [{
providedIn: 'root'
},] }
];
/** @nocollapse */
UploaderService.ctorParameters = () => [
{ type: HttpClient }
];
/** @nocollapse */ UploaderService.ngInjectableDef = defineInjectable({ factory: function UploaderService_Factory() { return new UploaderService(inject(HttpClient)); }, token: UploaderService, providedIn: "root" });
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class DropzoneDirective {
constructor() {
// Disable dropping on the body of the document.
// This prevents the browser from loading the dropped files
// using it's default behaviour if the user misses the drop zone.
// Set this input to false if you want the browser default behaviour.
this.preventBodyDrop = true;
this.disabled = false;
this.fileDrop = new EventEmitter();
// The `dropzone-active` class is applied to the host
// element when a drag is currently over the target, to allow styling it
this.active = false;
}
/**
* @param {?} event
* @return {?}
*/
onDrop(event) {
event.preventDefault();
if (!this.disabled) {
this.active = false;
const { dataTransfer } = event;
if (dataTransfer.items) {
/** @type {?} */
const files = [];
for (let i = 0; i < dataTransfer.items.length; i++) {
// If dropped items aren't files, reject them
if (dataTransfer.items[i].kind === 'file') {
files.push(dataTransfer.items[i].getAsFile());
}
}
dataTransfer.items.clear();
this.fileDrop.emit(files);
}
else {
/** @type {?} */
const files = dataTransfer.files;
dataTransfer.clearData();
this.fileDrop.emit(Array.from(files));
}
}
}
/**
* @param {?} event
* @return {?}
*/
onDragOver(event) {
event.stopPropagation();
event.preventDefault();
if (!this.disabled) {
this.active = true;
}
}
/**
* @param {?} event
* @return {?}
*/
onDragLeave(event) {
if (!this.disabled) {
this.active = false;
}
}
/**
* @param {?} event
* @return {?}
*/
onBodyDragOver(event) {
if (this.preventBodyDrop) {
event.preventDefault();
event.stopPropagation();
}
}
/**
* @param {?} event
* @return {?}
*/
onBodyDrop(event) {
if (this.preventBodyDrop) {
event.preventDefault();
}
}
}
DropzoneDirective.decorators = [
{ type: Directive, args: [{
selector: '[dropzone]'
},] }
];
DropzoneDirective.propDecorators = {
preventBodyDrop: [{ type: Input }],
disabled: [{ type: Input }],
fileDrop: [{ type: Output }],
active: [{ type: HostBinding, args: ['class.dropzone-active',] }],
onDrop: [{ type: HostListener, args: ['drop', ['$event'],] }],
onDragOver: [{ type: HostListener, args: ['dragover', ['$event'],] }],
onDragLeave: [{ type: HostListener, args: ['dragleave', ['$event'],] }],
onBodyDragOver: [{ type: HostListener, args: ['body:dragover', ['$event'],] }],
onBodyDrop: [{ type: HostListener, args: ['body:drop', ['$event'],] }]
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class UploaderModule {
}
UploaderModule.decorators = [
{ type: NgModule, args: [{
declarations: [DropzoneDirective],
exports: [DropzoneDirective]
},] }
];
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
export { UploadItem, Uploader, UploaderService, DropzoneDirective, UploaderModule };
//# sourceMappingURL=trendster-io-ng-uploader.js.map