UNPKG

angular-file-upload

Version:

Angular File Upload is a module for the AngularJS framework

780 lines (718 loc) 24.8 kB
'use strict'; import CONFIG from './../config.json'; let { bind, copy, extend, forEach, isObject, isNumber, isDefined, isArray, isUndefined, element } = angular; export default function __identity(fileUploaderOptions, $rootScope, $http, $window, $timeout, FileLikeObject, FileItem, Pipeline) { let { File, FormData } = $window; class FileUploader { /********************** * PUBLIC **********************/ /** * Creates an instance of FileUploader * @param {Object} [options] * @constructor */ constructor(options) { var settings = copy(fileUploaderOptions); extend(this, settings, options, { isUploading: false, _nextIndex: 0, _directives: {select: [], drop: [], over: []} }); // add default filters this.filters.unshift({name: 'queueLimit', fn: this._queueLimitFilter}); this.filters.unshift({name: 'folder', fn: this._folderFilter}); } /** * Adds items to the queue * @param {File|HTMLInputElement|Object|FileList|Array<Object>} files * @param {Object} [options] * @param {Array<Function>|String} filters */ addToQueue(files, options, filters) { let incomingQueue = this.isArrayLikeObject(files) ? Array.prototype.slice.call(files): [files]; var arrayOfFilters = this._getFilters(filters); var count = this.queue.length; var addedFileItems = []; let next = () => { let something = incomingQueue.shift(); if (isUndefined(something)) { return done(); } let fileLikeObject = this.isFile(something) ? something : new FileLikeObject(something); let pipes = this._convertFiltersToPipes(arrayOfFilters); let pipeline = new Pipeline(pipes); let onThrown = (err) => { let {originalFilter} = err.pipe; let [fileLikeObject, options] = err.args; this._onWhenAddingFileFailed(fileLikeObject, originalFilter, options); next(); }; let onSuccessful = (fileLikeObject, options) => { let fileItem = new FileItem(this, fileLikeObject, options); addedFileItems.push(fileItem); this.queue.push(fileItem); this._onAfterAddingFile(fileItem); next(); }; pipeline.onThrown = onThrown; pipeline.onSuccessful = onSuccessful; pipeline.exec(fileLikeObject, options); }; let done = () => { if(this.queue.length !== count) { this._onAfterAddingAll(addedFileItems); this.progress = this._getTotalProgress(); } this._render(); if (this.autoUpload) this.uploadAll(); }; next(); } /** * Remove items from the queue. Remove last: index = -1 * @param {FileItem|Number} value */ removeFromQueue(value) { var index = this.getIndexOfItem(value); var item = this.queue[index]; if(item.isUploading) item.cancel(); this.queue.splice(index, 1); item._destroy(); this.progress = this._getTotalProgress(); } /** * Clears the queue */ clearQueue() { while(this.queue.length) { this.queue[0].remove(); } this.progress = 0; } /** * Uploads a item from the queue * @param {FileItem|Number} value */ uploadItem(value) { var index = this.getIndexOfItem(value); var item = this.queue[index]; var transport = this.isHTML5 ? '_xhrTransport' : '_iframeTransport'; item._prepareToUploading(); if(this.isUploading) return; this._onBeforeUploadItem(item); if (item.isCancel) return; item.isUploading = true; this.isUploading = true; this[transport](item); this._render(); } /** * Cancels uploading of item from the queue * @param {FileItem|Number} value */ cancelItem(value) { var index = this.getIndexOfItem(value); var item = this.queue[index]; var prop = this.isHTML5 ? '_xhr' : '_form'; if (!item) return; item.isCancel = true; if(item.isUploading) { // It will call this._onCancelItem() & this._onCompleteItem() asynchronously item[prop].abort(); } else { let dummy = [undefined, 0, {}]; let onNextTick = () => { this._onCancelItem(item, ...dummy); this._onCompleteItem(item, ...dummy); }; $timeout(onNextTick); // Trigger callbacks asynchronously (setImmediate emulation) } } /** * Uploads all not uploaded items of queue */ uploadAll() { var items = this.getNotUploadedItems().filter(item => !item.isUploading); if(!items.length) return; forEach(items, item => item._prepareToUploading()); items[0].upload(); } /** * Cancels all uploads */ cancelAll() { var items = this.getNotUploadedItems(); forEach(items, item => item.cancel()); } /** * Returns "true" if value an instance of File * @param {*} value * @returns {Boolean} * @private */ isFile(value) { return this.constructor.isFile(value); } /** * Returns "true" if value an instance of FileLikeObject * @param {*} value * @returns {Boolean} * @private */ isFileLikeObject(value) { return this.constructor.isFileLikeObject(value); } /** * Returns "true" if value is array like object * @param {*} value * @returns {Boolean} */ isArrayLikeObject(value) { return this.constructor.isArrayLikeObject(value); } /** * Returns a index of item from the queue * @param {Item|Number} value * @returns {Number} */ getIndexOfItem(value) { return isNumber(value) ? value : this.queue.indexOf(value); } /** * Returns not uploaded items * @returns {Array} */ getNotUploadedItems() { return this.queue.filter(item => !item.isUploaded); } /** * Returns items ready for upload * @returns {Array} */ getReadyItems() { return this.queue .filter(item => (item.isReady && !item.isUploading)) .sort((item1, item2) => item1.index - item2.index); } /** * Destroys instance of FileUploader */ destroy() { forEach(this._directives, (key) => { forEach(this._directives[key], (object) => { object.destroy(); }); }); } /** * Callback * @param {Array} fileItems */ onAfterAddingAll(fileItems) { } /** * Callback * @param {FileItem} fileItem */ onAfterAddingFile(fileItem) { } /** * Callback * @param {File|Object} item * @param {Object} filter * @param {Object} options */ onWhenAddingFileFailed(item, filter, options) { } /** * Callback * @param {FileItem} fileItem */ onBeforeUploadItem(fileItem) { } /** * Callback * @param {FileItem} fileItem * @param {Number} progress */ onProgressItem(fileItem, progress) { } /** * Callback * @param {Number} progress */ onProgressAll(progress) { } /** * Callback * @param {FileItem} item * @param {*} response * @param {Number} status * @param {Object} headers */ onSuccessItem(item, response, status, headers) { } /** * Callback * @param {FileItem} item * @param {*} response * @param {Number} status * @param {Object} headers */ onErrorItem(item, response, status, headers) { } /** * Callback * @param {FileItem} item * @param {*} response * @param {Number} status * @param {Object} headers */ onCancelItem(item, response, status, headers) { } /** * Callback * @param {FileItem} item * @param {*} response * @param {Number} status * @param {Object} headers */ onCompleteItem(item, response, status, headers) { } /** * Callback */ onCompleteAll() { } /********************** * PRIVATE **********************/ /** * Returns the total progress * @param {Number} [value] * @returns {Number} * @private */ _getTotalProgress(value) { if(this.removeAfterUpload) return value || 0; var notUploaded = this.getNotUploadedItems().length; var uploaded = notUploaded ? this.queue.length - notUploaded : this.queue.length; var ratio = 100 / this.queue.length; var current = (value || 0) * ratio / 100; return Math.round(uploaded * ratio + current); } /** * Returns array of filters * @param {Array<Function>|String} filters * @returns {Array<Function>} * @private */ _getFilters(filters) { if(!filters) return this.filters; if(isArray(filters)) return filters; var names = filters.match(/[^\s,]+/g); return this.filters .filter(filter => names.indexOf(filter.name) !== -1); } /** * @param {Array<Function>} filters * @returns {Array<Function>} * @private */ _convertFiltersToPipes(filters) { return filters .map(filter => { let fn = bind(this, filter.fn); fn.isAsync = filter.fn.length === 3; fn.originalFilter = filter; return fn; }); } /** * Updates html * @private */ _render() { if(!$rootScope.$$phase) $rootScope.$apply(); } /** * Returns "true" if item is a file (not folder) * @param {File|FileLikeObject} item * @returns {Boolean} * @private */ _folderFilter(item) { return !!(item.size || item.type); } /** * Returns "true" if the limit has not been reached * @returns {Boolean} * @private */ _queueLimitFilter() { return this.queue.length < this.queueLimit; } /** * Checks whether upload successful * @param {Number} status * @returns {Boolean} * @private */ _isSuccessCode(status) { return (status >= 200 && status < 300) || status === 304; } /** * Transforms the server response * @param {*} response * @param {Object} headers * @returns {*} * @private */ _transformResponse(response, headers) { var headersGetter = this._headersGetter(headers); forEach($http.defaults.transformResponse, (transformFn) => { response = transformFn(response, headersGetter); }); return response; } /** * Parsed response headers * @param headers * @returns {Object} * @see https://github.com/angular/angular.js/blob/master/src/ng/http.js * @private */ _parseHeaders(headers) { var parsed = {}, key, val, i; if(!headers) return parsed; forEach(headers.split('\n'), (line) => { i = line.indexOf(':'); key = line.slice(0, i).trim().toLowerCase(); val = line.slice(i + 1).trim(); if(key) { parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; } }); return parsed; } /** * Returns function that returns headers * @param {Object} parsedHeaders * @returns {Function} * @private */ _headersGetter(parsedHeaders) { return (name) => { if(name) { return parsedHeaders[name.toLowerCase()] || null; } return parsedHeaders; }; } /** * The XMLHttpRequest transport * @param {FileItem} item * @private */ _xhrTransport(item) { var xhr = item._xhr = new XMLHttpRequest(); var sendable; if (!item.disableMultipart) { sendable = new FormData(); forEach(item.formData, (obj) => { forEach(obj, (value, key) => { sendable.append(key, value); }); }); sendable.append(item.alias, item._file, item.file.name); } else { sendable = item._file; } if(typeof(item._file.size) != 'number') { throw new TypeError('The file specified is no longer valid'); } xhr.upload.onprogress = (event) => { var progress = Math.round(event.lengthComputable ? event.loaded * 100 / event.total : 0); this._onProgressItem(item, progress); }; xhr.onload = () => { var headers = this._parseHeaders(xhr.getAllResponseHeaders()); var response = this._transformResponse(xhr.response, headers); var gist = this._isSuccessCode(xhr.status) ? 'Success' : 'Error'; var method = '_on' + gist + 'Item'; this[method](item, response, xhr.status, headers); this._onCompleteItem(item, response, xhr.status, headers); }; xhr.onerror = () => { var headers = this._parseHeaders(xhr.getAllResponseHeaders()); var response = this._transformResponse(xhr.response, headers); this._onErrorItem(item, response, xhr.status, headers); this._onCompleteItem(item, response, xhr.status, headers); }; xhr.onabort = () => { var headers = this._parseHeaders(xhr.getAllResponseHeaders()); var response = this._transformResponse(xhr.response, headers); this._onCancelItem(item, response, xhr.status, headers); this._onCompleteItem(item, response, xhr.status, headers); }; xhr.open(item.method, item.url, true); xhr.withCredentials = item.withCredentials; forEach(item.headers, (value, name) => { xhr.setRequestHeader(name, value); }); xhr.send(sendable); } /** * The IFrame transport * @param {FileItem} item * @private */ _iframeTransport(item) { var form = element('<form style="display: none;" />'); var iframe = element('<iframe name="iframeTransport' + Date.now() + '">'); var input = item._input; if(item._form) item._form.replaceWith(input); // remove old form item._form = form; // save link to new form input.prop('name', item.alias); forEach(item.formData, (obj) => { forEach(obj, (value, key) => { var element_ = element('<input type="hidden" name="' + key + '" />'); element_.val(value); form.append(element_); }); }); form.prop({ action: item.url, method: 'POST', target: iframe.prop('name'), enctype: 'multipart/form-data', encoding: 'multipart/form-data' // old IE }); iframe.bind('load', () => { var html = ''; var status = 200; try { // Fix for legacy IE browsers that loads internal error page // when failed WS response received. In consequence iframe // content access denied error is thrown becouse trying to // access cross domain page. When such thing occurs notifying // with empty response object. See more info at: // http://stackoverflow.com/questions/151362/access-is-denied-error-on-accessing-iframe-document-object // Note that if non standard 4xx or 5xx error code returned // from WS then response content can be accessed without error // but 'XHR' status becomes 200. In order to avoid confusion // returning response via same 'success' event handler. // fixed angular.contents() for iframes html = iframe[0].contentDocument.body.innerHTML; } catch(e) { // in case we run into the access-is-denied error or we have another error on the server side // (intentional 500,40... errors), we at least say 'something went wrong' -> 500 status = 500; } var xhr = {response: html, status: status, dummy: true}; var headers = {}; var response = this._transformResponse(xhr.response, headers); this._onSuccessItem(item, response, xhr.status, headers); this._onCompleteItem(item, response, xhr.status, headers); }); form.abort = () => { var xhr = {status: 0, dummy: true}; var headers = {}; var response; iframe.unbind('load').prop('src', 'javascript:false;'); form.replaceWith(input); this._onCancelItem(item, response, xhr.status, headers); this._onCompleteItem(item, response, xhr.status, headers); }; input.after(form); form.append(input).append(iframe); form[0].submit(); } /** * Inner callback * @param {File|Object} item * @param {Object} filter * @param {Object} options * @private */ _onWhenAddingFileFailed(item, filter, options) { this.onWhenAddingFileFailed(item, filter, options); } /** * Inner callback * @param {FileItem} item */ _onAfterAddingFile(item) { this.onAfterAddingFile(item); } /** * Inner callback * @param {Array<FileItem>} items */ _onAfterAddingAll(items) { this.onAfterAddingAll(items); } /** * Inner callback * @param {FileItem} item * @private */ _onBeforeUploadItem(item) { item._onBeforeUpload(); this.onBeforeUploadItem(item); } /** * Inner callback * @param {FileItem} item * @param {Number} progress * @private */ _onProgressItem(item, progress) { var total = this._getTotalProgress(progress); this.progress = total; item._onProgress(progress); this.onProgressItem(item, progress); this.onProgressAll(total); this._render(); } /** * Inner callback * @param {FileItem} item * @param {*} response * @param {Number} status * @param {Object} headers * @private */ _onSuccessItem(item, response, status, headers) { item._onSuccess(response, status, headers); this.onSuccessItem(item, response, status, headers); } /** * Inner callback * @param {FileItem} item * @param {*} response * @param {Number} status * @param {Object} headers * @private */ _onErrorItem(item, response, status, headers) { item._onError(response, status, headers); this.onErrorItem(item, response, status, headers); } /** * Inner callback * @param {FileItem} item * @param {*} response * @param {Number} status * @param {Object} headers * @private */ _onCancelItem(item, response, status, headers) { item._onCancel(response, status, headers); this.onCancelItem(item, response, status, headers); } /** * Inner callback * @param {FileItem} item * @param {*} response * @param {Number} status * @param {Object} headers * @private */ _onCompleteItem(item, response, status, headers) { item._onComplete(response, status, headers); this.onCompleteItem(item, response, status, headers); var nextItem = this.getReadyItems()[0]; this.isUploading = false; if(isDefined(nextItem)) { nextItem.upload(); return; } this.onCompleteAll(); this.progress = this._getTotalProgress(); this._render(); } /********************** * STATIC **********************/ /** * Returns "true" if value an instance of File * @param {*} value * @returns {Boolean} * @private */ static isFile(value) { return (File && value instanceof File); } /** * Returns "true" if value an instance of FileLikeObject * @param {*} value * @returns {Boolean} * @private */ static isFileLikeObject(value) { return value instanceof FileLikeObject; } /** * Returns "true" if value is array like object * @param {*} value * @returns {Boolean} */ static isArrayLikeObject(value) { return (isObject(value) && 'length' in value); } /** * Inherits a target (Class_1) by a source (Class_2) * @param {Function} target * @param {Function} source */ static inherit(target, source) { target.prototype = Object.create(source.prototype); target.prototype.constructor = target; target.super_ = source; } } /********************** * PUBLIC **********************/ /** * Checks a support the html5 uploader * @returns {Boolean} * @readonly */ FileUploader.prototype.isHTML5 = !!(File && FormData); /********************** * STATIC **********************/ /** * @borrows FileUploader.prototype.isHTML5 */ FileUploader.isHTML5 = FileUploader.prototype.isHTML5; return FileUploader; } __identity.$inject = [ 'fileUploaderOptions', '$rootScope', '$http', '$window', '$timeout', 'FileLikeObject', 'FileItem', 'Pipeline' ];