From dc145edc28b5b22c6e42669aaf94f830c959accc Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 21 Mar 2014 17:28:57 -0700 Subject: [PATCH] Add a HTML5/Angular based file upload demo --- example/app.js | 7 +- example/public/angular-file-upload.js | 685 +++++++++++++++++++++ example/public/angular-file-upload.min.js | 6 + example/public/angular-file-upload.min.map | 1 + example/public/controllers.js | 97 +++ example/public/index.html | 202 ++++++ 6 files changed, 996 insertions(+), 2 deletions(-) create mode 100644 example/public/angular-file-upload.js create mode 100644 example/public/angular-file-upload.min.js create mode 100644 example/public/angular-file-upload.min.map create mode 100644 example/public/controllers.js create mode 100644 example/public/index.html diff --git a/example/app.js b/example/app.js index 56c91df..7fce5f8 100644 --- a/example/app.js +++ b/example/app.js @@ -6,7 +6,10 @@ var path = require('path'); app.use(app.router); // expose a rest api -app.use(loopback.rest()); +app.use('/api', loopback.rest()); + +app.use(loopback.static(path.join(__dirname, 'public'))); + app.configure(function () { app.set('port', process.env.PORT || 3000); @@ -25,7 +28,7 @@ app.model(container); app.get('/', function (req, res, next) { res.setHeader('Content-Type', 'text/html'); var form = "

Storage Service Demo

" + - "List all containers

" + + "List all containers

" + "Upload to container c1:

" + "

" + "File to upload:
" diff --git a/example/public/angular-file-upload.js b/example/public/angular-file-upload.js new file mode 100644 index 0000000..a73fd4c --- /dev/null +++ b/example/public/angular-file-upload.js @@ -0,0 +1,685 @@ +/* + 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('