Browse files

Refactored to be more flexible and generic.

* Removed translation requirements and status updates from the library. Allows for more flexible interfaces.
* Fixed name spacing issues with the old version
* gemified it a bit more
  • Loading branch information...
1 parent 07a6c2c commit 7e6242a562b4e12806cb8b9b50c4a84113a0524d @stakach committed Jan 27, 2012
View
3 .gitignore
@@ -3,4 +3,5 @@ log/*.log
pkg/
test/dummy/db/*.sqlite3
test/dummy/log/*.log
-test/dummy/tmp/
+test/dummy/tmp/
+/Gemfile.lock
View
95 Gemfile.lock
@@ -1,95 +0,0 @@
-PATH
- remote: .
- specs:
- resolute (0.0.1)
- rails (~> 3.1.0.rc6)
-
-GEM
- remote: http://rubygems.org/
- specs:
- actionmailer (3.1.0.rc6)
- actionpack (= 3.1.0.rc6)
- mail (~> 2.3.0)
- actionpack (3.1.0.rc6)
- activemodel (= 3.1.0.rc6)
- activesupport (= 3.1.0.rc6)
- builder (~> 3.0.0)
- erubis (~> 2.7.0)
- i18n (~> 0.6)
- rack (~> 1.3.2)
- rack-cache (~> 1.0.2)
- rack-mount (~> 0.8.1)
- rack-test (~> 0.6.0)
- sprockets (~> 2.0.0.beta.12)
- activemodel (3.1.0.rc6)
- activesupport (= 3.1.0.rc6)
- bcrypt-ruby (~> 2.1.4)
- builder (~> 3.0.0)
- i18n (~> 0.6)
- activerecord (3.1.0.rc6)
- activemodel (= 3.1.0.rc6)
- activesupport (= 3.1.0.rc6)
- arel (~> 2.2.1)
- tzinfo (~> 0.3.29)
- activeresource (3.1.0.rc6)
- activemodel (= 3.1.0.rc6)
- activesupport (= 3.1.0.rc6)
- activesupport (3.1.0.rc6)
- multi_json (~> 1.0)
- arel (2.2.1)
- bcrypt-ruby (2.1.4-x86-mingw32)
- builder (3.0.0)
- erubis (2.7.0)
- hike (1.2.1)
- i18n (0.6.0)
- mail (2.3.0)
- i18n (>= 0.4.0)
- mime-types (~> 1.16)
- treetop (~> 1.4.8)
- mime-types (1.16)
- multi_json (1.0.3)
- polyglot (0.3.2)
- rack (1.3.2)
- rack-cache (1.0.2)
- rack (>= 0.4)
- rack-mount (0.8.2)
- rack (>= 1.0.0)
- rack-ssl (1.3.2)
- rack
- rack-test (0.6.1)
- rack (>= 1.0)
- rails (3.1.0.rc6)
- actionmailer (= 3.1.0.rc6)
- actionpack (= 3.1.0.rc6)
- activerecord (= 3.1.0.rc6)
- activeresource (= 3.1.0.rc6)
- activesupport (= 3.1.0.rc6)
- bundler (~> 1.0)
- railties (= 3.1.0.rc6)
- railties (3.1.0.rc6)
- actionpack (= 3.1.0.rc6)
- activesupport (= 3.1.0.rc6)
- rack-ssl (~> 1.3.2)
- rake (>= 0.8.7)
- rdoc (~> 3.4)
- thor (~> 0.14.6)
- rake (0.9.2)
- rdoc (3.9.3)
- sprockets (2.0.0.beta.14)
- hike (~> 1.2)
- rack (~> 1.0)
- tilt (!= 1.3.0, ~> 1.1)
- sqlite3 (1.3.4-x86-mingw32)
- thor (0.14.6)
- tilt (1.3.2)
- treetop (1.4.10)
- polyglot
- polyglot (>= 0.3.1)
- tzinfo (0.3.29)
-
-PLATFORMS
- x86-mingw32
-
-DEPENDENCIES
- resolute!
- sqlite3
View
121 README.textile
@@ -7,6 +7,8 @@ Works in
* Firefox 7+
* IE 10+
+For ajax upload fallback in older browsers use: https://github.com/JangoSteve/remotipart
+
h2. How it works
@@ -22,6 +24,10 @@ h2. How it works
I've defined the part size to be a constant 1mb (1024 x 1024). Feel free to adjust this as you see fit.
+Other features include:
+* Appending more files while an upload is taking place
+* Pausing and resuming uploads (to the nearest mb) - when blobs are supported
+* Client side filtering of files that can be uploaded where desirable
h2. Using it with Rails 3.1
@@ -33,17 +39,14 @@ I've made the following assumptions
h3. Basic Usage
-# Copy folder into the vendor directory (not the plugins folder)
-# Create an entry in your gem file for it: gem 'resolute', :path => '/vendor/resolute'
+# Create an entry in your gem file for it: gem 'resolute', :git => 'git://github.com/stakach/Resumable-Uploads.git'
# Create an initializer for the configuration: config/initializers/uploads.rb (for example)
#* Enter config (outlined in the next section)
# Update your routes
# Run migrations
#* First need to copy over the migrations: rake railties:install:migrations
#* Then: rake db:migrate
-# In the head section of your document include: <%= javascript_include_tag *Resolute.js %>
-#* This provides client side internationalisation based on the current sessions locale
-#* Alternatively Include *aca-i18n.js*, *resumables-en.js* and *resumables.js* file in your app/assets/javascripts/application.js file
+# In the head section of your document include: <%= javascript_include_tag resumables.js %>
# Configure the client side jQuery as you wish (outlined below)
h3. Engine Config
@@ -122,24 +125,15 @@ Provides the hooks into your web application to provide upload hotspots (think g
$('#file_input, #drop_spot').resumable({
//
- // Event callbacks
+ // Event callbacks (can be set here or bound at any point in time)
//
- onStart: function(event, total) { return true; },
- onUploadStarted: function(event, name, number, total) { return true; },
- onUploadProgress: function(event, progress, name, number, total) { },
- onUploadFinish: function(event, response, name, number, total) { },
- onUploadError: function(event, name, error, messages) { },
- onFinish: function(event, total) { },
-
- startDragHover: function(){}, // Hovering over hotspot
- endDragHover: function(){}, // No longer hovering over the hotspot
-
- //
- // Informational callbacks
- //
- setName: function(text) { },
- setStatus: function(text) { },
- setProgress: function(value) { },
+ //onStart: return modified file list or undefined, // Passed: (event, file list)
+ //onAppendFiles: return modified file list or undefined, // Passed: (event, file list)
+ //onUploadStarted: returning false will skip the file, // Passed: (event, name, number, total)
+ //onUploadProgress: // Passed: (event, progress, name, number, total)
+ //onUploadFinish: // Passed: (event, response, name, number, total)
+ //onUploadError: // Passed: (event, name, error, messages)
+ //onFinish: // Passed: (event, total, failures)
//
// Configuration options
@@ -149,92 +143,11 @@ Provides the hooks into your web application to provide upload hotspots (think g
autostart: true, // On change if using input box
autoclear: true, // Clears the file upload input box once complete
+ disableInput: true, // Prevents an input field being used while uploads are occuring
retry_part_errors: false, // Applies only to resumable uploads
retry_limit: 3, // Number of part retries before giving up
halt_on_error: false // Stop uploading further files?
});
-
- //
- // A more realistic setup with jquery ui
- //
- if ($.support.filereader && $.support.formdata) { // Check HTML5 uploads are workable
- var diag = $('<div />').html('<div></div><p></p>');
- var progress = diag.children('div').progressbar({
- value: 0
- });
- var status = diag.children('p');
- var failures = 0;
-
- $('#drop_spot').resumable({
- onStart: function (event, total) {
- // Make sure we warn the user before exiting the page while uploading
- window.onbeforeunload = function() {return "You are attempting to leave this page while an upload is taking place. Are you sure you want to leave?";};
-
- failures = 0;
- progress.progressbar("value", 0);
-
- var thebuttons = {};
- thebuttons["Cancel"] = function () {
- $('#drop_spot').trigger('cancelAll');
- };
- diag.dialog({
- modal: true,
- buttons: thebuttons,
- close: function (event, ui) {
- if (window.onbeforeunload != null)
- draghotspot.trigger('cancelAll'); // This will call onFinish
- }
- });
- return true;
- },
- setName: function (text) {
- diag.dialog("option", "title", text);
- },
- setStatus: function (text) {
- status.text(text);
- },
- setProgress: function (val) {
- progress.progressbar("value", Math.ceil(val * 100));
- },
- onUploadError: function(event, name, error, messages) {
- failures = failures + 1;
- var inner = '<p>There were some issues uploading ' + name + ':</p><ul>';
-
- //
- // Display active record validation errors if avaliable
- //
- if(messages.error.constructor == Array) {
- $.each(messages.error, function (key, val) {
- inner = inner + '<li>' + key + '<ul>';
- $.each(val, function (index, inval) {
- inner = inner + '<li>' + inval + '</li>';
- });
- inner = inner + '</ul></li>';
- });
- } else if (messages.error != null) {
- inner = inner + '<li>' + messages.error + '</li>';
- } else {
- inner = inner + '<li>' + error + '</li>';
- }
- inner = inner + '</ul>';
- $.noticeAdd({ text: inner, stay: true });
- },
- onFinish: function (event, total) {
- window.onbeforeunload = null; // No more need for user warning
- diag.dialog("close").dialog("destroy");
- //
- // Update view here then inform users
- //
- $.noticeAdd({ text: (total - failures) + " items successfully uploaded", stayTime: 6000 });
- },
- startDragHover: function(){
- $('#drop_spot > #highlighter').stop(true).animate({opacity:1});
- },
- endDragHover: function(){
- $('#drop_spot > #highlighter').stop(true).animate({opacity:0.2});
- }
- });
- }
</code>
</pre>
View
58 app/assets/javascripts/resolute/aca-i18n.js
@@ -1,58 +0,0 @@
-// http://kallesaas.com/code/translate.js
-
-(function () {
- if (!window.translate) {
- /**
- * this global function is for string substitution
- * @property {string} string to translate.
- * @property {string} as much
- * return STRING
- */
- window.translate = function(){
- var html = [ ];
- var arguments = arguments;
- var format = arguments[0];
-
- var objIndex = 0;
- var reg = /\%s/;
- var parts = [ ];
-
- /**
- * analyze the string, extract the parts with the %s identifier.
- */
- for ( var m = reg.exec(format); m; m = reg.exec(format) ) {
- parts.push(format.substr(0, m[0][0] == "%" ? m.index : m.index));
- parts.push("%s");
- format = format.substr(m.index+m[0].length);
- }
-
- parts.push(format);
-
- /**
- * analyze the parts, replace the %s with the given arguments.
- * beware of undefined!
- */
- for (var i = 0; i < parts.length; ++i){
- var part = parts[i];
-
- if (part && part === "%s"){
- var object = arguments[++objIndex];
-
- if (object === undefined) {
- html.push("%s");
- }else{
- html.push(object);
- };
- }
- else{
- html.push(part);
- }
- }
-
- /**
- * Join the array and return as string.
- */
- return html.join('');
- }
- };
-})();
View
9 app/assets/javascripts/resolute/resumables-de.js
@@ -1,9 +0,0 @@
-var i18n_resumables = {
- status: {
- STARTED: 'gestartet',
- UPLOADING: 'Hochladen',
- UPLOADED: 'hochgeladen',
- FINISHED: 'komplett'
- },
- progress: '%s (%s von %s)' // File.ext (1 of 3) for example
-};
View
9 app/assets/javascripts/resolute/resumables-en.js
@@ -1,9 +0,0 @@
-var i18n_resumables = {
- status: {
- STARTED: 'Started',
- UPLOADING: 'Uploading',
- UPLOADED: 'Uploaded',
- FINISHED: 'Complete'
- },
- progress: '%s (%s of %s)' // File.ext (1 of 3) for example
-};
View
789 app/assets/javascripts/resolute/resumables.js
@@ -1,6 +1,3 @@
-//= require resolute/aca-i18n
-
-
/**
* jQuery.resumable()
* This provides HTML5 uploads with a resume function if it is avaliable.
@@ -9,429 +6,483 @@
*
* @author Stephen von Takach <steve@advancedcontrol.com.au>
* @copyright 2011 advancedcontrol.com.au
-* @version 1.0
+* @version 1.1
+*
**/
(function($) {
//
- // This allows us to check for drag and drop upload support
+ // Uploads files using HTML5 FileAPI
//
- $.extend($.support, {
- filereader: !!window.FileReader, // Tests for support for the HTML5 File API (http://dev.w3.org/2006/webapi/FileAPI/)
- formdata: !!window.FormData
- });
-
-
- //
- // HTML5 Resumable Upload code
- //
- jQuery.fn.resumable = function(options) {
- var events = ['onStart', 'onUploadStarted', 'onUploadProgress', 'onUploadFinish', 'onUploadError', 'onFinish'],
- options = jQuery.extend({
- //
- // Progress callbacks
- //
- onStart: function(event, total) {
- return true;
- },
- onUploadStarted: function(event, name, number, total) {
- return true;
- },
- onUploadProgress: function(event, progress, name, number, total) { },
- onUploadFinish: function(event, response, name, number, total) { },
- onUploadError: function(event, name, error, messages) { },
- onFinish: function(event, total, failures) { },
-
-
- //
- // Status feedback options
- //
- setName: function(text) { },
- setStatus: function(text) { },
- setProgress: function(value) { },
- genName: function(file, number, total) {
- return translate(i18n_resumables.progress, file, (number + 1), total);
- },
- genStatus: function(progress, finished) {
- if (finished) {
- return i18n_resumables.status.FINISHED;
- }
- if (progress == 0) {
- return i18n_resumables.status.STARTED;
- }
- else if (progress == 1) {
- return i18n_resumables.status.UPLOADED;
- }
- else {
- return i18n_resumables.status.UPLOADING;
- }
- },
- genProgress: function(loaded, total) {
- return loaded / total;
- },
-
-
- //
- // Application data required
- //
- additionalParameters: {},//JS Object or function(file)
- baseURL: '/uploads', // resumable_upload, regular_upload
-
-
-
- //
- // Behavioural options
- //
- startDragHover: function(){},
- endDragHover: function(){},
-
- autostart: true, // On change if using input box
- autoclear: true, // Clears the file upload input box once complete
- http_headers: {
- 'Cache-Control':'no-cache',
- 'X-Requested-With':'XMLHttpRequest',
- 'X-File-Name': function(file){return file.name},
- 'X-File-Size': function(file){return file.size}
- },
-
- retry_part_errors: false, // Applies only to resumable uploads (auto detected)
- retry_limit: 3, // Number of part retries before giving up
-
- halt_on_error: false // Stop uploading further files
- }, options),
- $this = $(this); // End var declaration
+ function upload($this) {
+ var data = $this.data('resumable'),
+ options = data.options,
+ failures = 0,
+ uploaded = 0,
+ result;
+
+ if (data.files.length == 0) {
+ return false; // Safari will try and upload 0 files
+ }
+ //
+ // Allow onStart to cancel or remove unsupported files
+ //
+ result = $this.triggerHandler('onStart', [data.files]);
+ if (result !== false) {
+ if(result !== undefined)
+ data.files = result;
+ } else {
+ return false;
+ }
+
+ if (data.files.length == 0) {
+ return false; // Safari will try and upload 0 files
+ }
//
- // Uploads files using HTML5 FileAPI
+ // Disable input
//
- function upload(files) {
- var total = files.length,
- failures = 0;
-
- if (total == 0) {
- return false; // Safari will try and upload 0 files
- }
- if (!$this.triggerHandler('onStart.uploader', [total])) {
- return false;
- }
+ if ($this.is('input') && options.disableInput) {
+ $this.attr('disabled', true);
+ }
+
+ data.continue_after_abort = true;
+
+
+ //
+ // Call this when we stop uploading
+ //
+ function upload_finished() {
+ $this.triggerHandler('onFinish', [data.files.length, failures]);
+
+ data.state = 'idle';
if ($this.is('input')) {
- $this.attr('disabled', true);
+ if (options.disableInput)
+ $this.attr('disabled', false);
+ if (options.autoclear)
+ $this.val('');
+ }
+ }
+
+ //
+ // This manages the file upload
+ //
+ function upload_file(number) {
+ if (number == total) {
+ upload_finished(number, failures);
+ return;
}
- var uploaded = 0;
- $this.data('uploader')['continue_after_abort'] = true;
-
//
- // Call this when we stop uploading
+ // Files can be canceled at this point (maybe some are not supported)
//
- function upload_finished(number, failures) {
- $this.trigger('onFinish.uploader', [number, failures]);
-
- if ($this.is('input')) {
- $this.attr('disabled', false);
- if (options.autoclear) {
- $this.val('');
- }
- }
+ var file = files[number];
+ if (!$this.triggerHandler('onUploadStarted', [file.name, number, total])) {
+ return upload_file(number + 1);
}
+
//
- // This manages the file upload
+ // What to do if the user cancels upload
//
- function upload_file(number) {
- if (number == total) {
+ function abort() {
+ if ($this.data('resumable').continue_after_abort) {
+ upload_file(number + 1);
+ }
+ else {
upload_finished(number, failures);
- options.setStatus(options.genStatus(1, true));
- return;
}
+ };
+
+ //
+ // Complete upload error
+ //
+ function on_error(xhr, e) {
+ message = {error: null}
- var file = files[number];
- if (!$this.triggerHandler('onUploadStarted.uploader', [file.name, number, total])) {
- return upload_file(number + 1);
- }
- options.setStatus(options.genStatus(0));
- options.setName(options.genName(file.name, number, total));
- options.setProgress(options.genProgress(0, file.size));
+ if (xhr.status == 403) // Forbidden - ie not logged in or not your file
+ message.error = 'access denied';
+ else if (xhr.status == 406) // Not acceptable - could not save, maybe bad params or file format. There is a message avaliable
+ message.error = jQuery.parseJSON(xhr.responseText).error; // This will always be an array
+ //else if (xhr.status == 422) // unprocessable entity - unknown error. Could not save and no error message
+ // message['error'] = '';
+ failures = failures + 1;
- //
- // What to do if the user cancels upload
- //
- function abort() {
- if ($this.data('uploader')['continue_after_abort']) {
- upload_file(number + 1);
- }
- else {
- upload_finished(number, failures);
- }
- };
-
- //
- // Complete upload error
- //
- function on_error(xhr, e) {
- message = {error: null}
-
- if (xhr.status == 403) // Forbidden - ie not logged in or not your file
- message.error = 'access denied';
- else if (xhr.status == 406) // Not acceptable - could not save, maybe bad params or file format. There is a message avaliable
- message.error = jQuery.parseJSON(xhr.responseText).error; // This will always be an array
- //else if (xhr.status == 422) // unprocessable entity - unknown error. Could not save and no error message
- // message['error'] = '';
-
- failures = failures + 1;
-
- $this.trigger('onUploadError.uploader', [file.name, e, message]);
- if (options.halt_on_error) {
- upload_finished(number, failures);
- } else {
- upload_file(number + 1);
- }
+ $this.triggerHandler('onUploadError', [file.name, e, message]);
+ if (options.halt_on_error) {
+ upload_finished(number, failures);
+ } else {
+ upload_file(number + 1);
}
+ }
+
+
+ //
+ // Anything over a 1MB we chunk upload if we can slice the file
+ // FIXED in FF7.0!! -Removed mozSlice as it doesn't send a file name so the data is ignored by rack-
+ //
+ if(file.size > (1024 * 1024) && typeof(file.slice || file.webkitSlice || file.mozSlice) == 'function') {
+ var theurl = (typeof(options.baseURL) == 'function' ? options.baseURL() : options.baseURL) + '/resumable_upload.json',
+ params = {
+ 'resume[file_name]': file.name,
+ 'resume[file_size]': file.size,
+ 'resume[file_modified]': file.lastModifiedDate
+ };
+ if(!!options.additionalParameters)
+ params['resume[paramters]'] = JSON.stringify(typeof(options.additionalParameters) == 'function' ? options.additionalParameters(file) : options.additionalParameters);
//
- // Anything over a 1MB we chunk upload if we can slice the file
- // FIXED in FF7.0!! -Removed mozSlice as it doesn't send a file name so the data is ignored by rack-
+ // Ensure the slice method is defined
//
- if(file.size > (1024 * 1024) && typeof(file.slice || file.webkitSlice || file.mozSlice) == 'function') {
- var theurl = (typeof(options.baseURL) == 'function' ? options.baseURL() : options.baseURL) + '/resumable_upload.json',
- params = {
- 'resume[file_name]': file.name,
- 'resume[file_size]': file.size,
- 'resume[file_modified]': file.lastModifiedDate
- };
-
- if(!!options.additionalParameters)
- params['resume[paramters]'] = JSON.stringify(typeof(options.additionalParameters) == 'function' ? options.additionalParameters(file) : options.additionalParameters);
-
- //
- // Ensure the slice method is defined
- //
- if(typeof(file.slice) != 'function')
- file.slice = file.webkitSlice || file.mozSlice;
-
- $this.data('uploader')['xhr'] = $.ajax({
- url: theurl,
- data: params,
- type: 'GET',
- dataType: 'json',
- success: function (data, status, xhr) {
- var retries = 0;
+ if(typeof(file.slice) != 'function')
+ file.slice = file.webkitSlice || file.mozSlice;
+
+ $this.data('resumable').xhr = $.ajax({
+ url: theurl,
+ data: params,
+ type: 'GET',
+ dataType: 'json',
+ success: function (data, status, xhr) {
+ var retries = 0;
+
+ function sendChunk(currentPart) {
+ var offset = 1024 * 1024 * currentPart;
+ var limit = offset + 1024 * 1024;
+ if(file.size < limit)
+ limit = file.size;
- function sendChunk(currentPart) {
- var offset = 1024 * 1024 * currentPart;
- var limit = offset + 1024 * 1024;
- if(file.size < limit)
- limit = file.size;
-
- var chunk = file.slice(offset, limit),
- f = new FormData();
-
- f.append('id', data.file_id);
- f.append('part', currentPart);
- f.append('chunk', chunk);
-
- $this.data('uploader')['xhr'] = $.ajax({
- url: theurl,
- type: 'POST',
- data: f,
- processData: false, // Do not process the data
- contentType: false,
- xhr: function() {
- var xhr = new XMLHttpRequest();
- $this.data('uploader')['xhr'] = xhr;
- xhr.upload['onprogress'] = function(rpe){
- $this.trigger('onUploadProgress.uploader', [(offset + rpe.loaded) / file.size, file.name, number, total]);
- options.setStatus(options.genStatus((offset + rpe.loaded) / file.size));
- options.setProgress(options.genProgress((offset + rpe.loaded), file.size));
- };
- return xhr;
- },
- dataType: 'json',
- success: function (data, status, xhr) {
- if(data.next_part == false) {
- $this.triggerHandler('onUploadFinish.uploader', [xhr.responseText, file.name, number, total]);
- options.setStatus(options.genStatus(1, true));
- options.setProgress(options.genProgress(file.size, file.size));
- upload_file(number + 1);
- } else {
- retries = 0;
- sendChunk(data.next_part);
- }
- },
- error: function (xhr, status, error) {
- if(status == 'abort')
- abort();
- else {
- if(options.retry_part_errors && retries < options.retry_limit) {
- retries = retries + 1;
- sendChunk(currentPart);
- }
- else
- on_error(xhr, error);
+ var chunk = file.slice(offset, limit),
+ f = new FormData();
+
+ f.append('id', data.file_id);
+ f.append('part', currentPart);
+ f.append('chunk', chunk);
+
+ $this.data('resumable').xhr = $.ajax({
+ url: theurl,
+ type: 'POST',
+ data: f,
+ processData: false, // Do not process the data
+ contentType: false,
+ xhr: function() {
+ var xhr = new XMLHttpRequest();
+ $this.data('resumable').xhr = xhr;
+ xhr.upload['onprogress'] = function(rpe){
+ $this.triggerHandler('onUploadProgress', [(offset + rpe.loaded) / file.size, file.name, number, total]);
+ };
+ return xhr;
+ },
+ dataType: 'json',
+ success: function (data, status, xhr) {
+ if(data.next_part == false) {
+ $this.triggerHandler('onUploadFinish', [xhr.responseText, file.name, number, total]);
+ upload_file(number + 1);
+ } else {
+ retries = 0;
+ sendChunk(data.next_part);
+ }
+ },
+ error: function (xhr, status, error) {
+ if(status == 'abort')
+ abort();
+ else {
+ if(options.retry_part_errors && retries < options.retry_limit) {
+ retries = retries + 1;
+ sendChunk(currentPart);
}
+ else
+ on_error(xhr, error);
}
- });
- }
-
- sendChunk(data.next_part);
- },
- error: function (xhr, status, error) {
- if(status == 'abort')
- abort();
- else
- on_error(xhr, error);
- }
- });
-
- //
- // Smaller files are uploaded as per-usual (HTML5 Style)
- //
- } else {
- var f = new FormData(),
- params;
-
- if(!!options.additionalParameters)
- f.append('custom', JSON.stringify(typeof(options.additionalParameters) == 'function' ? options.additionalParameters(file) : options.additionalParameters));
-
- f.append('uploaded_file', file);
-
- $.ajax({
- url: (typeof(options.baseURL) == 'function' ? options.baseURL() : options.baseURL) + '/regular_upload.json',
- type: 'POST',
- data: f,
- dataType: 'json',
- processData: false, // Do not process the data
- contentType: false,
- xhr: function() {
- var xhr = new XMLHttpRequest();
- $this.data('uploader')['xhr'] = xhr;
- xhr.upload['onprogress'] = function(rpe){
- $this.trigger('onUploadProgress.uploader', [rpe.loaded / rpe.total, file.name, number, total]);
- options.setStatus(options.genStatus(rpe.loaded / rpe.total));
- options.setProgress(options.genProgress(rpe.loaded, rpe.total));
- };
- return xhr;
- },
- beforeSend: function (xhr) {
- $.each(options.http_headers, function(key,val){
- val = typeof(val) == 'function' ? val(file) : val; // resolve value
- if (val === false)
- return true; // if resolved value is boolean false, do not send this header
- xhr.setRequestHeader(key, val);
+ }
});
- },
- success: function (data, status, xhr) {
- $this.triggerHandler('onUploadFinish.uploader', [xhr.responseText, file.name, number, total]);
- options.setStatus(options.genStatus(1, true));
- options.setProgress(options.genProgress(file.size, file.size));
- upload_file(number + 1);
- },
- error: function (xhr, status, error) {
- on_error(xhr, error);
- },
- complete: function(xhr, status) {
- if(status == 'abort')
- abort();
}
- });
- }
- }
-
-
- upload_file(0);
- return true;
- }
-
- //
- // Helper function to obtain the files from the HTML5 Input element
- //
- function upload_input() {
- upload($this[0].files);
- }
-
- return $this.each(function() {
- $this.data('uploader', {
- continue_after_abort: true,
- xhr: new XMLHttpRequest()
- });
-
+
+ sendChunk(data.next_part);
+ },
+ error: function (xhr, status, error) {
+ if(status == 'abort')
+ abort();
+ else
+ on_error(xhr, error);
+ }
+ });
+
//
- // Bind the appropriate events (Depending on the object type)
+ // Smaller files are uploaded as per-usual (HTML5 Style)
//
- if ($this.is('input')) {
- if (options.autostart)
- $this.bind('change.uploader', upload_input);
- }
- else {
- $this.bind({
- 'dragenter.uploader': function() {
- options.startDragHover();
- return false;
+ } else {
+ var f = new FormData(),
+ params;
+
+ if(!!options.additionalParameters)
+ f.append('custom', JSON.stringify(typeof(options.additionalParameters) == 'function' ? options.additionalParameters(file) : options.additionalParameters));
+
+ f.append('uploaded_file', file);
+
+ $.ajax({
+ url: (typeof(options.baseURL) == 'function' ? options.baseURL() : options.baseURL) + '/regular_upload.json',
+ type: 'POST',
+ data: f,
+ dataType: 'json',
+ processData: false, // Do not process the data
+ contentType: false,
+ xhr: function() {
+ var xhr = new XMLHttpRequest();
+ $this.data('resumable').xhr = xhr;
+ xhr.upload['onprogress'] = function(rpe){
+ $this.triggerHandler('onUploadProgress', [rpe.loaded / rpe.total, file.name, number, total]);
+ };
+ return xhr;
},
- 'dragleave.uploader': function() {
- options.endDragHover();
- //return false;
+ beforeSend: function (xhr) {
+ $.each(options.http_headers, function(key,val){
+ val = typeof(val) == 'function' ? val(file) : val; // resolve value
+ if (val === false)
+ return true; // if resolved value is boolean false, do not send this header
+ xhr.setRequestHeader(key, val);
+ });
},
- 'dragover.uploader': function() {
- options.startDragHover();
- return false;
+ success: function (data, status, xhr) {
+ $this.triggerHandler('onUploadFinish', [xhr.responseText, file.name, number, total]);
+ upload_file(number + 1);
},
- 'drop.uploader': function(evt) {
- options.endDragHover();
- //try {
- upload(evt.originalEvent.dataTransfer.files);
- //}
- //catch (err) {
- // Anything not implementing File API will fail here
- // alert(err);
- //}
- return false;
+ error: function (xhr, status, error) {
+ on_error(xhr, error);
+ },
+ complete: function(xhr, status) {
+ if(status == 'abort')
+ abort();
}
});
}
+ }
+
+
+ upload_file(0);
+ return true;
+ }
+
+
+ //
+ // Helper function to obtain the files from the HTML5 Input element
+ //
+ function upload_input($this) {
+ add_files($this, $this[0].files);
+ }
+
+
+ function add_files($this, files) {
+ var data = $this.data('resumable');
+
+ if(data.state == 'idle') {
+ data.state = 'uploading';
+ data.files = files;
+ upload($this);
+ } else {
+ var result = $this.triggerHandler('onAppendFiles', [files])
+
+ //
+ // Add the files to the list
+ //
+ if(result === undefined)
+ result = files;
+
+ data.files.push.apply(data.files, files);
+ }
+ }
+
+
+ //
+ // This allows us to check for drag and drop upload support
+ //
+ $.extend($.support, {
+ filereader: !!window.FileReader, // Tests for support for the HTML5 File API (http://dev.w3.org/2006/webapi/FileAPI/)
+ formdata: !!window.FormData
+ });
+
+
+ //
+ //
+ //
+ var methods = {
+ init: function(settings){
+ var events = ['onStart', 'onAppendFiles', 'onUploadStarted', 'onUploadProgress', 'onUploadFinish', 'onUploadError', 'onFinish'],
+
+ options = jQuery.extend({
+ //
+ // Progress events
+ // If you set here you can cancel all uploads from the onStart
+ // or the current upload onUploadStarted
+ //
+ //onStart: return modified file list or undefined, // Passed: (event, file list)
+ //onAppendFiles: return modified file list or undefined, // Passed: (event, file list)
+ //onUploadStarted: returning false will skip the file, // Passed: (event, name, number, total)
+ //onUploadProgress: // Passed: (event, progress, name, number, total)
+ //onUploadFinish: // Passed: (event, response, name, number, total)
+ //onUploadError: // Passed: (event, name, error, messages)
+ //onFinish: // Passed: (event, total, failures)
+
+
+ //
+ // Application data required
+ //
+ additionalParameters: {}, //JS Object or function(file)
+ baseURL: '/uploads', // resumable_upload, regular_upload
-
+ autostart: true, // On change if using input box
+ autoclear: true, // Clears the file upload input box once complete
+ disableInput: true,
+ http_headers: {
+ 'Cache-Control':'no-cache',
+ 'X-Requested-With':'XMLHttpRequest',
+ 'X-File-Name': function(file){return file.name},
+ 'X-File-Size': function(file){return file.size}
+ },
+
+ retry_part_errors: false, // Applies only to resumable uploads (auto detected)
+ retry_limit: 3, // Number of part retries before giving up
+
+ halt_on_error: false // Stop uploading further files
+ }, settings || {}),
+
+ data = {
+ continue_after_abort: true, // Shared xhr, settings etc
+ xhr: new XMLHttpRequest(),
+ options: options,
+ count: 0,
+ state: 'idle',
+ files: []
+ };
+
+
//
- // Bind the callbacks
+ // Bind to all the elements any events specified
//
for (event in events) {
if (!!options[events[event]]) {
- $this.bind(events[event] + '.uploader', options[events[event]]);
+ this.bind(events[event] + '.resumable', options[events[event]]);
}
}
-
- //
- // Bind the other callable functions
- //
- $this.bind({
- 'start.uploader': upload_input,
- 'cancelCurrent.uploader': function() {
- $this.data('uploader')['xhr'].abort();
- },
- 'cancelAll.uploader': function() {
- $this.data('uploader')['continue_after_abort'] = false;
- $this.data('uploader')['xhr'].abort();
- },
- 'destroy.uploader': function() {
- $this.data('uploader')['continue_after_abort'] = false;
- try{
- $this.data('uploader')['xhr'].abort(); // if active
- } catch(e) {}
+ this.bind({
+ 'start.resumable': methods.start,
+ 'cancelCurrent.resumable': methods.cancelCurrent,
+ 'cancelAll.resumable': methods.cancelAll,
+ 'destroy.resumable': methods.destroy
+ });
+
+ return this.each(function() {
+ var $this = $(this);
+
+ //
+ // If it exists then destroy it before creating a new one
+ //
+ if(!!$this.data('resumable'))
+ $this.resumable('destroy');
+
+
+ $this.data('resumable', data);
+ data.count += 1;
+
+
+ //
+ // Bind the appropriate events (Depending on the object type)
+ //
+ if ($this.is('input')) {
+ if (options.autostart) {
+ $this.bind('change.resumable', function() {
+ upload_input($this);
+ });
+ }
+ } else {
+ $this.bind('drop.resumable', function(evt) {
+ add_files($this, evt.originalEvent.dataTransfer.files);
+ return false;
+ });
+ }
+ });
+ },
+ destroy: function() {
+ return this.each(function(){
+ var $this = $(this),
+ data = $this.data('resumable');
+
+ if(!!data) {
+ data.count -= 1;
+
+ //
+ // Only abort if this is the last upload elemnt sharing these settings
+ //
+ if(data.count <= 0) {
+ $this.resumable('cancelAll');
+ }
//
// Remove related events and data
//
- $this.unbind('.uploader');
- $this.removeData('uploader');
+ $this.unbind('.resumable');
+ $this.removeData('resumable');
+ }
+ });
+ },
+ start: function() {
+ return this.each(function(){
+ var $this = $(this);
+
+ if(!!$this.data('resumable')) {
+ if ($this.is('input'))
+ upload_input($this);
+ }
+ });
+ },
+ cancelCurrent: function() {
+ return this.each(function(){
+ var $this = $(this),
+ data = $this.data('resumable');
+
+ if(!!data && data.state != 'idle') {
+ try{
+ data.xhr.abort();
+ } catch(e) {}
}
});
- });
+ },
+ cancelAll: function() {
+ return this.each(function(){
+ var $this = $(this),
+ data = $this.data('resumable');
+
+ if(!!data) {
+ data.continue_after_abort = false;
+
+ if(data.state != 'idle') {
+ try{
+ data.xhr.abort();
+ } catch(e) {}
+ }
+ }
+ });
+ }
};
+
+
+ //
+ // Method proxy
+ //
+ $.fn.resumable = function(method) {
+ if ( !!methods[method] ) {
+ return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
+ } else if ( typeof method === 'object' || !!!method ) {
+ return methods.init.apply( this, arguments );
+ } else {
+ $.error( 'Method ' + method + ' does not exist on jQuery.resumable' );
+ }
+ };
+
})(jQuery);
View
2 lib/resolute/version.rb
@@ -1,3 +1,3 @@
module Resolute
- VERSION = "1.0.0"
+ VERSION = "1.1.0"
end
View
16 resolute.gemspec
@@ -7,17 +7,17 @@ require "resolute/version"
Gem::Specification.new do |s|
s.name = "resolute"
s.version = Resolute::VERSION
- s.authors = ["TODO: Your name"]
- s.email = ["TODO: Your email"]
- s.homepage = "TODO"
- s.summary = "TODO: Summary of Resolute."
- s.description = "TODO: Description of Resolute."
+ s.authors = ["Stephen von Takach"]
+ s.email = ["steve@advancedcontrol.com.au"]
+ s.homepage = "http://advancedcontrol.com.au/"
+ s.summary = "Resumable file uploads in HTML5"
+ s.description = "Provides code to manage and simplify resumable file uploads"
- s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.rdoc"]
+ s.files = Dir["{app,config,db,lib}/**/*"] + ["LGPL3-LICENSE", "Rakefile", "README.textile"]
s.test_files = Dir["test/**/*"]
- s.add_dependency "rails", "~> 3.1.0.rc6"
- # s.add_dependency "jquery-rails"
+ s.add_dependency "rails"
+ s.add_dependency "jquery-rails"
s.add_development_dependency "sqlite3"
end

0 comments on commit 7e6242a

Please sign in to comment.