/* Angular File Upload v0.3.3.1 https://github.com/nervgh/angular-file-upload */ (function(angular, factory) { if (typeof define === 'function' && define.amd) { define('angular-file-upload', ['angular'], function(angular) { return factory(angular); }); } else { return factory(angular); } }(angular || null, function(angular) { var app = angular.module('angularFileUpload', []); // It is attached to an element that catches the event drop file app.directive('ngFileDrop', [ '$fileUploader', function ($fileUploader) { 'use strict'; return { // don't use drag-n-drop files in IE9, because not File API support link: !$fileUploader.isHTML5 ? angular.noop : function (scope, element, attributes) { element .bind('drop', function (event) { var dataTransfer = event.dataTransfer ? event.dataTransfer : event.originalEvent.dataTransfer; // jQuery fix; if (!dataTransfer) return; event.preventDefault(); event.stopPropagation(); scope.$broadcast('file:removeoverclass'); scope.$emit('file:add', dataTransfer.files, scope.$eval(attributes.ngFileDrop)); }) .bind('dragover', function (event) { var dataTransfer = event.dataTransfer ? event.dataTransfer : event.originalEvent.dataTransfer; // jQuery fix; event.preventDefault(); event.stopPropagation(); dataTransfer.dropEffect = 'copy'; scope.$broadcast('file:addoverclass'); }) .bind('dragleave', function () { scope.$broadcast('file:removeoverclass'); }); } }; }]) // It is attached to an element which will be assigned to a class "ng-file-over" or ng-file-over="className" app.directive('ngFileOver', function () { 'use strict'; return { link: function (scope, element, attributes) { scope.$on('file:addoverclass', function () { element.addClass(attributes.ngFileOver || 'ng-file-over'); }); scope.$on('file:removeoverclass', function () { element.removeClass(attributes.ngFileOver || 'ng-file-over'); }); } }; }); // It is attached to element like app.directive('ngFileSelect', [ '$fileUploader', function ($fileUploader) { 'use strict'; return { link: function (scope, element, attributes) { $fileUploader.isHTML5 || element.removeAttr('multiple'); element.bind('change', function () { scope.$emit('file:add', $fileUploader.isHTML5 ? this.files : this, scope.$eval(attributes.ngFileSelect)); ($fileUploader.isHTML5 && element.attr('multiple')) && element.prop('value', null); }); element.prop('value', null); // FF fix } }; }]); app.factory('$fileUploader', [ '$compile', '$rootScope', '$http', '$window', function ($compile, $rootScope, $http, $window) { 'use strict'; /** * Creates a uploader * @param {Object} params * @constructor */ function Uploader(params) { angular.extend(this, { scope: $rootScope, url: '/', alias: 'file', queue: [], headers: {}, progress: null, autoUpload: false, removeAfterUpload: false, method: 'POST', filters: [], formData: [], isUploading: false, _nextIndex: 0, _timestamp: Date.now() }, params); // add the base filter this.filters.unshift(this._filter); this.scope.$on('file:add', function (event, items, options) { event.stopPropagation(); this.addToQueue(items, options); }.bind(this)); this.bind('beforeupload', Item.prototype._beforeupload); this.bind('in:progress', Item.prototype._progress); this.bind('in:success', Item.prototype._success); this.bind('in:cancel', Item.prototype._cancel); this.bind('in:error', Item.prototype._error); this.bind('in:complete', Item.prototype._complete); this.bind('in:progress', this._progress); this.bind('in:complete', this._complete); } Uploader.prototype = { /** * Link to the constructor */ constructor: Uploader, /** * The base filter. If returns "true" an item will be added to the queue * @param {File|Input} item * @returns {boolean} * @private */ _filter: function (item) { return angular.isElement(item) ? true : !!item.size; }, /** * Registers a event handler * @param {String} event * @param {Function} handler * @return {Function} unsubscribe function */ bind: function (event, handler) { return this.scope.$on(this._timestamp + ':' + event, handler.bind(this)); }, /** * Triggers events * @param {String} event * @param {...*} [some] */ trigger: function (event, some) { arguments[ 0 ] = this._timestamp + ':' + event; this.scope.$broadcast.apply(this.scope, arguments); }, /** * Checks a support the html5 uploader * @returns {Boolean} * @readonly */ isHTML5: !!($window.File && $window.FormData), /** * Adds items to the queue * @param {FileList|File|HTMLInputElement} items * @param {Object} [options] */ addToQueue: function (items, options) { var length = this.queue.length; var list = 'length' in items ? items : [items]; angular.forEach(list, function (file) { // check a [File|HTMLInputElement] var isValid = !this.filters.length ? true : this.filters.every(function (filter) { return filter.call(this, file); }, this); // create new item var item = new Item(angular.extend({ url: this.url, alias: this.alias, headers: angular.copy(this.headers), formData: angular.copy(this.formData), removeAfterUpload: this.removeAfterUpload, method: this.method, uploader: this, file: file }, options)); if (isValid) { this.queue.push(item); this.trigger('afteraddingfile', item); } else { this.trigger('whenaddingfilefailed', item); } }, this); if (this.queue.length !== length) { this.trigger('afteraddingall', this.queue); this.progress = this._getTotalProgress(); } this._render(); this.autoUpload && this.uploadAll(); }, /** * Remove items from the queue. Remove last: index = -1 * @param {Item|Number} value */ removeFromQueue: function (value) { var index = this.getIndexOfItem(value); var item = this.queue[ index ]; item.isUploading && item.cancel(); this.queue.splice(index, 1); item._destroy(); this.progress = this._getTotalProgress(); }, /** * Clears the queue */ clearQueue: function () { this.queue.forEach(function (item) { item.isUploading && item.cancel(); item._destroy(); }, this); this.queue.length = 0; this.progress = 0; }, /** * Returns a index of item from the queue * @param {Item|Number} value * @returns {Number} */ getIndexOfItem: function (value) { return angular.isObject(value) ? this.queue.indexOf(value) : value; }, /** * Returns not uploaded items * @returns {Array} */ getNotUploadedItems: function () { return this.queue.filter(function (item) { return !item.isUploaded; }); }, /** * Returns items ready for upload * @returns {Array} */ getReadyItems: function() { return this.queue .filter(function(item) { return item.isReady && !item.isUploading; }) .sort(function(item1, item2) { return item1.index - item2.index; }); }, /** * Uploads a item from the queue * @param {Item|Number} value */ uploadItem: function (value) { var index = this.getIndexOfItem(value); var item = this.queue[ index ]; var transport = this.isHTML5 ? '_xhrTransport' : '_iframeTransport'; item.index = item.index || this._nextIndex++; item.isReady = true; if (this.isUploading) { return; } this.isUploading = true; this[ transport ](item); }, /** * Cancels uploading of item from the queue * @param {Item|Number} value */ cancelItem: function(value) { var index = this.getIndexOfItem(value); var item = this.queue[ index ]; var prop = this.isHTML5 ? '_xhr' : '_form'; item[prop] && item[prop].abort(); }, /** * Uploads all not uploaded items of queue */ uploadAll: function () { var items = this.getNotUploadedItems().filter(function(item) { return !item.isUploading; }); items.forEach(function(item) { item.index = item.index || this._nextIndex++; item.isReady = true; }, this); items.length && this.uploadItem(items[ 0 ]); }, /** * Cancels all uploads */ cancelAll: function() { this.getNotUploadedItems().forEach(function(item) { this.cancelItem(item); }, this); }, /** * Updates angular scope * @private */ _render: function() { this.scope.$$phase || this.scope.$digest(); }, /** * Returns the total progress * @param {Number} [value] * @returns {Number} * @private */ _getTotalProgress: function (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); }, /** * The 'in:progress' handler * @private */ _progress: function (event, item, progress) { var result = this._getTotalProgress(progress); this.trigger('progressall', result); this.progress = result; this._render(); }, /** * The 'in:complete' handler * @private */ _complete: function () { var item = this.getReadyItems()[ 0 ]; this.isUploading = false; if (angular.isDefined(item)) { this.uploadItem(item); return; } this.trigger('completeall', this.queue); this.progress = this._getTotalProgress(); this._render(); }, /** * The XMLHttpRequest transport * @private */ _xhrTransport: function (item) { var xhr = item._xhr = new XMLHttpRequest(); var form = new FormData(); var that = this; this.trigger('beforeupload', item); item.formData.forEach(function(obj) { angular.forEach(obj, function(value, key) { form.append(key, value); }); }); form.append(item.alias, item.file); xhr.upload.onprogress = function (event) { var progress = event.lengthComputable ? event.loaded * 100 / event.total : 0; that.trigger('in:progress', item, Math.round(progress)); }; xhr.onload = function () { var response = that._transformResponse(xhr.response); var event = that._isSuccessCode(xhr.status) ? 'success' : 'error'; that.trigger('in:' + event, xhr, item, response); that.trigger('in:complete', xhr, item, response); }; xhr.onerror = function () { that.trigger('in:error', xhr, item); that.trigger('in:complete', xhr, item); }; xhr.onabort = function () { that.trigger('in:cancel', xhr, item); that.trigger('in:complete', xhr, item); }; xhr.open(item.method, item.url, true); angular.forEach(item.headers, function (value, name) { xhr.setRequestHeader(name, value); }); xhr.send(form); }, /** * The IFrame transport * @private */ _iframeTransport: function (item) { var form = angular.element('
'); var iframe = angular.element('