UNPKG

@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
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