REMOTIPART WIP

This commit is contained in:
Dennis Collinson 2012-03-12 17:53:28 -07:00
parent 7a5841a1e1
commit 37bf5c5aee
11 changed files with 693 additions and 183 deletions

View file

@ -17,6 +17,7 @@ gem 'rack-cors', '~> 0.2.4', :require => 'rack/cors'
gem 'devise', '1.5.3'
gem 'jwt'
gem 'oauth2-provider', '0.0.19'
gem 'remotipart', '~> 1.0'
gem 'omniauth', '1.0.1'
gem 'omniauth-facebook'

View file

@ -162,7 +162,7 @@ GEM
fixture_builder (0.3.1)
activerecord (>= 2)
activesupport (>= 2)
fog (1.2.0)
fog (1.3.0)
builder
excon (~> 0.12.0)
formatador (~> 0.2.0)
@ -341,6 +341,7 @@ GEM
redis (2.2.2)
redis-namespace (1.0.3)
redis (< 3.0.0)
remotipart (1.0.2)
resque (1.20.0)
multi_json (~> 1.0)
redis-namespace (~> 1.0.2)
@ -513,6 +514,7 @@ DEPENDENCIES
rails-i18n
rails_autolink
redcarpet (= 2.0.1)
remotipart (~> 1.0)
resque (= 1.20.0)
resque-timeout (= 1.0.0)
rest-client (= 1.6.7)

View file

@ -41,21 +41,23 @@ class PhotosController < ApplicationController
end
def create
begin
raise unless params[:photo][:aspect_ids]
rescuing_photo_errors do |p|
if remotipart_submitted?
@photo = current_user.build_post(:photo, params[:photo])
else
raise "not remotipart" unless params[:photo][:aspect_ids]
if params[:photo][:aspect_ids] == "all"
params[:photo][:aspect_ids] = current_user.aspects.collect{|x| x.id}
elsif params[:photo][:aspect_ids].is_a?(Hash)
params[:photo][:aspect_ids] = params[:photo][:aspect_ids].values
end
if params[:photo][:aspect_ids] == "all"
params[:photo][:aspect_ids] = current_user.aspects.collect { |x| x.id }
elsif params[:photo][:aspect_ids].is_a?(Hash)
params[:photo][:aspect_ids] = params[:photo][:aspect_ids].values
end
params[:photo][:user_file] = file_handler(params)
params[:photo][:user_file] = file_handler(params)
@photo = current_user.build_post(:photo, params[:photo])
if @photo.save
@photo = current_user.build_post(:photo, params[:photo])
if @photo.save
aspects = current_user.aspects_from_ids(params[:photo][:aspect_ids])
unless @photo.pending
@ -65,11 +67,14 @@ class PhotosController < ApplicationController
if params[:photo][:set_profile_photo]
profile_params = {:image_url => @photo.url(:thumb_large),
:image_url_medium => @photo.url(:thumb_medium),
:image_url_small => @photo.url(:thumb_small)}
:image_url_medium => @photo.url(:thumb_medium),
:image_url_small => @photo.url(:thumb_small)}
current_user.update_profile(profile_params)
end
end
end
if @photo.save
respond_to do |format|
format.json{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )}
format.html{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )}
@ -77,19 +82,6 @@ class PhotosController < ApplicationController
else
respond_with @photo, :location => photos_path, :error => message
end
rescue TypeError
message = I18n.t 'photos.create.type_error'
respond_with @photo, :location => photos_path, :error => message
rescue CarrierWave::IntegrityError
message = I18n.t 'photos.create.integrity_error'
respond_with @photo, :location => photos_path, :error => message
rescue RuntimeError => e
message = I18n.t 'photos.create.runtime_error'
respond_with @photo, :location => photos_path, :error => message
raise e
end
end
@ -200,4 +192,23 @@ class PhotosController < ApplicationController
file
end
end
def rescuing_photo_errors
begin
yield
rescue TypeError
message = I18n.t 'photos.create.type_error'
respond_with @photo, :location => photos_path, :error => message
rescue CarrierWave::IntegrityError
message = I18n.t 'photos.create.integrity_error'
respond_with @photo, :location => photos_path, :error => message
rescue RuntimeError => e
message = I18n.t 'photos.create.runtime_error'
respond_with @photo, :location => photos_path, :error => message
raise e
end
end
end

View file

@ -16,7 +16,7 @@ class PostsController < ApplicationController
:xml
def new
render :text => "", :layout => true
end
def show
@ -40,7 +40,7 @@ class PostsController < ApplicationController
format.xml{ render :xml => @post.to_diaspora_xml }
format.mobile{render 'posts/show.mobile.haml'}
format.json{ render :json => PostPresenter.new(@post, current_user).to_json }
format.any{render 'posts/show.html.haml'}
format.any{render 'posts/show.html.haml'}
end
else

View file

@ -53,7 +53,8 @@ class StatusMessagesController < ApplicationController
receiving_services = Service.titles(services)
current_user.dispatch_post(@status_message, :url => short_post_url(@status_message.guid), :service_types => receiving_services)
#this is done implicitly, somewhere else, apparently, says max. :'(
# @status_message.photos.each do |photo|
# current_user.dispatch_post(photo)
# end

View file

@ -83,4 +83,4 @@
});
}
createUploader();
createUploader();

View file

@ -0,0 +1,15 @@
= form_for Photo.new, :html => { :multipart => true }, :remote => true do |f|
= f.label :user_file
= f.file_field :user_file
= f.submit
:javascript
$(function(){
console.log($('#new_photo'))
$('#new_photo').bind('ajax:success', function(event, data) {
alert("happy day")
console.log(data)
console.log(new Backbone.Model(data)); // Your newly created Backbone.js model
});
});

View file

@ -25,6 +25,8 @@ javascripts:
- public/javascripts/vendor/timeago.js
- public/javascripts/vendor/facebox.js
- public/javascripts/vendor/underscore.js
- public/javascripts/vendor/jquery.iframe-transport.js
- public/javascripts/vendor/jquery.remotipart.js
- public/javascripts/vendor/jquery.events.input.js
- public/javascripts/vendor/jquery.elastic.js
- public/javascripts/vendor/jquery.mentionsInput.js

View file

@ -1,198 +1,374 @@
/* Clear form plugin - called using $("elem").clearForm(); */
$.fn.clearForm = function() {
return this.each(function() {
if ($(this).is('form')) {
return $(':input', this).clearForm();
}
if ($(this).hasClass('clear_on_submit') || $(this).is(':text') || $(this).is(':password') || $(this).is('textarea')) {
$(this).val('');
} else if ($(this).is(':checkbox') || $(this).is(':radio')) {
$(this).attr('checked', false);
} else if ($(this).is('select')) {
this.selectedIndex = -1;
} else if ($(this).attr('name') == 'photos[]') {
$(this).val('');
}
$(this).blur();
});
};
(function($, undefined) {
/**
* Unobtrusive scripting adapter for jQuery
*
* Requires jQuery 1.4.3 or later.
* Requires jQuery 1.6.0 or later.
* https://github.com/rails/jquery-ujs
* Uploading file using rails.js
* =============================
*
* By default, browsers do not allow files to be uploaded via AJAX. As a result, if there are any non-blank file fields
* in the remote form, this adapter aborts the AJAX submission and allows the form to submit through standard means.
*
* The `ajax:aborted:file` event allows you to bind your own handler to process the form submission however you wish.
*
* Ex:
* $('form').live('ajax:aborted:file', function(event, elements){
* // Implement own remote file-transfer handler here for non-blank file inputs passed in `elements`.
* // Returning false in this handler tells rails.js to disallow standard form submission
* return false;
* });
*
* The `ajax:aborted:file` event is fired when a file-type input is detected with a non-blank value.
*
* Third-party tools can use this hook to detect when an AJAX file upload is attempted, and then use
* techniques like the iframe method to upload the file instead.
*
* Required fields in rails.js
* ===========================
*
* If any blank required inputs (required="required") are detected in the remote form, the whole form submission
* is canceled. Note that this is unlike file inputs, which still allow standard (non-AJAX) form submission.
*
* The `ajax:aborted:required` event allows you to bind your own handler to inform the user of blank required inputs.
*
* !! Note that Opera does not fire the form's submit event if there are blank required inputs, so this event may never
* get fired in Opera. This event is what causes other browsers to exhibit the same submit-aborting behavior.
*
* Ex:
* $('form').live('ajax:aborted:required', function(event, elements){
* // Returning false in this handler tells rails.js to submit the form anyway.
* // The blank required inputs are passed to this function in `elements`.
* return ! confirm("Would you like to submit the form with missing info?");
* });
*/
(function($) {
// Make sure that every Ajax request sends the CSRF token
function CSRFProtection(fn) {
var token = $('meta[name="csrf-token"]').attr('content');
if (token) fn(function(xhr) { xhr.setRequestHeader('X-CSRF-Token', token) });
}
if ($().jquery == '1.5') { // gruesome hack
var factory = $.ajaxSettings.xhr;
$.ajaxSettings.xhr = function() {
var xhr = factory();
CSRFProtection(function(setHeader) {
var open = xhr.open;
xhr.open = function() { open.apply(this, arguments); setHeader(this) };
});
return xhr;
};
}
else $(document).ajaxSend(function(e, xhr) {
CSRFProtection(function(setHeader) { setHeader(xhr) });
});
// Shorthand to make it a little easier to call public rails functions from within rails.js
var rails;
// Triggers an event on an element and returns the event result
function fire(obj, name, data) {
var event = new $.Event(name);
obj.trigger(event, data);
return event.result !== false;
}
$.rails = rails = {
// Link elements bound by jquery-ujs
linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]',
// Submits "remote" forms and links with ajax
function handleRemote(element) {
var method, url, data,
dataType = element.attr('data-type') || ($.ajaxSettings && $.ajaxSettings.dataType);
// Select elements bound by jquery-ujs
inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]',
if (element.is('form')) {
method = element.attr('method');
url = element.attr('action');
data = element.serializeArray();
// memoized value from clicked submit button
var button = element.data('ujs:submit-button');
if (button) {
data.push(button);
element.data('ujs:submit-button', null);
}
} else {
method = element.attr('data-method');
url = element.attr('href');
data = null;
}
// Form elements bound by jquery-ujs
formSubmitSelector: 'form',
$.ajax({
url: url, type: method || 'GET', data: data, dataType: dataType,
// stopping the "ajax:beforeSend" event will cancel the ajax request
beforeSend: function(xhr, settings) {
if (settings.dataType === undefined) {
xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
// Form input elements bound by jquery-ujs
formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not(button[type])',
// Form input elements disabled during form submission
disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]',
// Form input elements re-enabled after form submission
enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled',
// Form required input elements
requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])',
// Form file input elements
fileInputSelector: 'input:file',
// Link onClick disable selector with possible reenable after remote submission
linkDisableSelector: 'a[data-disable-with]',
// Make sure that every Ajax request sends the CSRF token
CSRFProtection: function(xhr) {
var token = $('meta[name="csrf-token"]').attr('content');
if (token) xhr.setRequestHeader('X-CSRF-Token', token);
},
// Triggers an event on an element and returns false if the event result is false
fire: function(obj, name, data) {
var event = $.Event(name);
obj.trigger(event, data);
return event.result !== false;
},
// Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm
confirm: function(message) {
return confirm(message);
},
// Default ajax function, may be overridden with custom function in $.rails.ajax
ajax: function(options) {
return $.ajax(options);
},
// Submits "remote" forms and links with ajax
handleRemote: function(element) {
var method, url, data,
crossDomain = element.data('cross-domain') || null,
dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType),
options;
if (rails.fire(element, 'ajax:before')) {
if (element.is('form')) {
method = element.attr('method');
url = element.attr('action');
data = element.serializeArray();
// memoized value from clicked submit button
var button = element.data('ujs:submit-button');
if (button) {
data.push(button);
element.data('ujs:submit-button', null);
}
} else if (element.is(rails.inputChangeSelector)) {
method = element.data('method');
url = element.data('url');
data = element.serialize();
if (element.data('params')) data = data + "&" + element.data('params');
} else {
method = element.data('method');
url = element.attr('href');
data = element.data('params') || null;
}
return fire(element, 'ajax:beforeSend', [xhr, settings]);
},
success: function(data, status, xhr) {
element.trigger('ajax:success', [data, status, xhr]);
},
complete: function(xhr, status) {
element.trigger('ajax:complete', [xhr, status]);
},
error: function(xhr, status, error) {
element.trigger('ajax:error', [xhr, status, error]);
options = {
type: method || 'GET', data: data, dataType: dataType, crossDomain: crossDomain,
// stopping the "ajax:beforeSend" event will cancel the ajax request
beforeSend: function(xhr, settings) {
if (settings.dataType === undefined) {
xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
}
return rails.fire(element, 'ajax:beforeSend', [xhr, settings]);
},
success: function(data, status, xhr) {
alert("hella boner jamz")
element.trigger('ajax:success', [data, status, xhr]);
},
complete: function(xhr, status) {
element.trigger('ajax:complete', [xhr, status]);
},
error: function(xhr, status, error) {
element.trigger('ajax:error', [xhr, status, error]);
}
};
// Only pass url to `ajax` options if not blank
if (url) { options.url = url; }
return rails.ajax(options);
} else {
return false;
}
});
}
},
// Handles "data-method" on links such as:
// <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
function handleMethod(link) {
var href = link.attr('href'),
method = link.attr('data-method'),
csrf_token = $('meta[name=csrf-token]').attr('content'),
csrf_param = $('meta[name=csrf-param]').attr('content'),
form = $('<form method="post" action="' + href + '"></form>'),
metadata_input = '<input name="_method" value="' + method + '" type="hidden" />',
form_params = link.data('form-params');
// Handles "data-method" on links such as:
// <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
handleMethod: function(link) {
var href = link.attr('href'),
method = link.data('method'),
target = link.attr('target'),
csrf_token = $('meta[name=csrf-token]').attr('content'),
csrf_param = $('meta[name=csrf-param]').attr('content'),
form = $('<form method="post" action="' + href + '"></form>'),
metadata_input = '<input name="_method" value="' + method + '" type="hidden" />';
if (csrf_param !== undefined && csrf_token !== undefined) {
metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />';
}
// support non-nested JSON encoded params for links
if (form_params != undefined) {
var params = $.parseJSON(form_params);
for (key in params) {
form.append($("<input>").attr({"type": "hidden", "name": key, "value": params[key]}));
if (csrf_param !== undefined && csrf_token !== undefined) {
metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />';
}
}
form.hide().append(metadata_input).appendTo('body');
form.submit();
}
if (target) { form.attr('target', target); }
function disableFormElements(form) {
form.find('input[data-disable-with]').each(function() {
var input = $(this);
input.data('ujs:enable-with', input.val())
.val(input.attr('data-disable-with'))
.attr('disabled', 'disabled');
});
}
form.hide().append(metadata_input).appendTo('body');
form.submit();
},
function enableFormElements(form) {
form.find('input[data-disable-with]').each(function() {
var input = $(this);
input.val(input.data('ujs:enable-with')).removeAttr('disabled');
});
}
/* Disables form elements:
- Caches element value in 'ujs:enable-with' data store
- Replaces element text with value of 'data-disable-with' attribute
- Sets disabled property to true
*/
disableFormElements: function(form) {
form.find(rails.disableSelector).each(function() {
var element = $(this), method = element.is('button') ? 'html' : 'val';
element.data('ujs:enable-with', element[method]());
element[method](element.data('disable-with'));
element.prop('disabled', true);
});
},
function allowAction(element) {
var message = element.attr('data-confirm');
return !message || (fire(element, 'confirm') && confirm(message));
}
/* Re-enables disabled form elements:
- Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`)
- Sets disabled property to false
*/
enableFormElements: function(form) {
form.find(rails.enableSelector).each(function() {
var element = $(this), method = element.is('button') ? 'html' : 'val';
if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with'));
element.prop('disabled', false);
});
},
function requiredValuesMissing(form) {
var missing = false;
form.find('input[name][required]').each(function() {
if (!$(this).val()) missing = true;
});
return missing;
}
/* For 'data-confirm' attribute:
- Fires `confirm` event
- Shows the confirmation dialog
- Fires the `confirm:complete` event
$('a[data-confirm], a[data-method], a[data-remote]').live('click.rails', function(e) {
var link = $(this);
if (!allowAction(link)) return false;
Returns `true` if no function stops the chain and user chose yes; `false` otherwise.
Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog.
Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function
return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog.
*/
allowAction: function(element) {
var message = element.data('confirm'),
answer = false, callback;
if (!message) { return true; }
if (link.attr('data-remote') != undefined) {
handleRemote(link);
if (rails.fire(element, 'confirm')) {
answer = rails.confirm(message);
callback = rails.fire(element, 'confirm:complete', [answer]);
}
return answer && callback;
},
// Helper function which checks for blank inputs in a form that match the specified CSS selector
blankInputs: function(form, specifiedSelector, nonBlank) {
var inputs = $(), input,
selector = specifiedSelector || 'input,textarea';
form.find(selector).each(function() {
input = $(this);
// Collect non-blank inputs if nonBlank option is true, otherwise, collect blank inputs
if (nonBlank ? input.val() : !input.val()) {
inputs = inputs.add(input);
}
});
return inputs.length ? inputs : false;
},
// Helper function which checks for non-blank inputs in a form that match the specified CSS selector
nonBlankInputs: function(form, specifiedSelector) {
return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank
},
// Helper function, needed to provide consistent behavior in IE
stopEverything: function(e) {
$(e.target).trigger('ujs:everythingStopped');
e.stopImmediatePropagation();
return false;
} else if (link.attr('data-method')) {
handleMethod(link);
},
// find all the submit events directly bound to the form and
// manually invoke them. If anyone returns false then stop the loop
callFormSubmitBindings: function(form, event) {
var events = form.data('events'), continuePropagation = true;
if (events !== undefined && events['submit'] !== undefined) {
$.each(events['submit'], function(i, obj){
if (typeof obj.handler === 'function') return continuePropagation = obj.handler(event);
});
}
return continuePropagation;
},
// replace element's html with the 'data-disable-with' after storing original html
// and prevent clicking on it
disableElement: function(element) {
element.data('ujs:enable-with', element.html()); // store enabled state
element.html(element.data('disable-with')); // set to disabled state
element.bind('click.railsDisable', function(e) { // prevent further clicking
return rails.stopEverything(e)
});
},
// restore element to its original state which was disabled by 'disableElement' above
enableElement: function(element) {
if (element.data('ujs:enable-with') !== undefined) {
element.html(element.data('ujs:enable-with')); // set to old enabled state
// this should be element.removeData('ujs:enable-with')
// but, there is currently a bug in jquery which makes hyphenated data attributes not get removed
element.data('ujs:enable-with', false); // clean up cache
}
element.unbind('click.railsDisable'); // enable element
}
};
$.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }});
$(document).delegate(rails.linkDisableSelector, 'ajax:complete', function() {
rails.enableElement($(this));
});
$(document).delegate(rails.linkClickSelector, 'click.rails', function(e) {
var link = $(this), method = link.data('method'), data = link.data('params');
if (!rails.allowAction(link)) return rails.stopEverything(e);
if (link.is(rails.linkDisableSelector)) rails.disableElement(link);
if (link.data('remote') !== undefined) {
if ( (e.metaKey || e.ctrlKey) && (!method || method === 'GET') && !data ) { return true; }
if (rails.handleRemote(link) === false) { rails.enableElement(link); }
return false;
} else if (link.data('method')) {
rails.handleMethod(link);
return false;
}
});
$('form').live('submit.rails', function(e) {
var form = $(this), remote = form.attr('data-remote') != undefined;
if (!allowAction(form)) return false;
$(document).delegate(rails.inputChangeSelector, 'change.rails', function(e) {
var link = $(this);
if (!rails.allowAction(link)) return rails.stopEverything(e);
// skip other logic when required values are missing
if (requiredValuesMissing(form)) return !remote;
rails.handleRemote(link);
return false;
});
$(document).delegate(rails.formSubmitSelector, 'submit.rails', function(e) {
var form = $(this),
remote = form.data('remote') !== undefined,
blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector),
nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector);
if (!rails.allowAction(form)) return rails.stopEverything(e);
// skip other logic when required values are missing or file upload is present
if (blankRequiredInputs && form.attr("novalidate") == undefined && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) {
return rails.stopEverything(e);
}
if (remote) {
handleRemote(form);
if (nonBlankFileInputs) {
return rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]);
}
// If browser does not support submit bubbling, then this live-binding will be called before direct
// bindings. Therefore, we should directly call any direct bindings before remotely submitting form.
if (!$.support.submitBubbles && $().jquery < '1.7' && rails.callFormSubmitBindings(form, e) === false) return rails.stopEverything(e);
rails.handleRemote(form);
return false;
} else {
// slight timeout so that the submit button gets properly serialized
setTimeout(function(){ disableFormElements(form) }, 13);
setTimeout(function(){ rails.disableFormElements(form); }, 13);
}
});
$('form input[type=submit], form button[type=submit], form button:not([type])').live('click.rails', function() {
$(document).delegate(rails.formInputClickSelector, 'click.rails', function(event) {
var button = $(this);
if (!allowAction(button)) return false;
if (!rails.allowAction(button)) return rails.stopEverything(event);
// register the pressed submit button
var name = button.attr('name'), data = name ? {name:name, value:button.val()} : null;
var name = button.attr('name'),
data = name ? {name:name, value:button.val()} : null;
button.closest('form').data('ujs:submit-button', data);
});
$('form').live('ajax:beforeSend.rails', function(event) {
if (this == event.target) disableFormElements($(this));
$(document).delegate(rails.formSubmitSelector, 'ajax:beforeSend.rails', function(event) {
if (this == event.target) rails.disableFormElements($(this));
});
$('form').live('ajax:complete.rails', function(event) {
if (this == event.target) enableFormElements($(this));
$(document).delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) {
if (this == event.target) rails.enableFormElements($(this));
});
})( jQuery );

View file

@ -0,0 +1,233 @@
// This [jQuery](http://jquery.com/) plugin implements an `<iframe>`
// [transport](http://api.jquery.com/extending-ajax/#Transports) so that
// `$.ajax()` calls support the uploading of files using standard HTML file
// input fields. This is done by switching the exchange from `XMLHttpRequest` to
// a hidden `iframe` element containing a form that is submitted.
// The [source for the plugin](http://github.com/cmlenz/jquery-iframe-transport)
// is available on [Github](http://github.com/) and dual licensed under the MIT
// or GPL Version 2 licenses.
// ## Usage
// To use this plugin, you simply add a `iframe` option with the value `true`
// to the Ajax settings an `$.ajax()` call, and specify the file fields to
// include in the submssion using the `files` option, which can be a selector,
// jQuery object, or a list of DOM elements containing one or more
// `<input type="file">` elements:
// $("#myform").submit(function() {
// $.ajax(this.action, {
// files: $(":file", this),
// iframe: true
// }).complete(function(data) {
// console.log(data);
// });
// });
// The plugin will construct a hidden `<iframe>` element containing a copy of
// the form the file field belongs to, will disable any form fields not
// explicitly included, submit that form, and process the response.
// If you want to include other form fields in the form submission, include them
// in the `data` option, and set the `processData` option to `false`:
// $("#myform").submit(function() {
// $.ajax(this.action, {
// data: $(":text", this).serializeArray(),
// files: $(":file", this),
// iframe: true,
// processData: false
// }).complete(function(data) {
// console.log(data);
// });
// });
// ### The Server Side
// If the response is not HTML or XML, you (unfortunately) need to apply some
// trickery on the server side. To send back a JSON payload, send back an HTML
// `<textarea>` element with a `data-type` attribute that contains the MIME
// type, and put the actual payload in the textarea:
// <textarea data-type="application/json">
// {"ok": true, "message": "Thanks so much"}
// </textarea>
// The iframe transport plugin will detect this and attempt to apply the same
// conversions that jQuery applies to regular responses. That means for the
// example above you should get a Javascript object as the `data` parameter of
// the `complete` callback, with the properties `ok: true` and
// `message: "Thanks so much"`.
// ### Compatibility
// This plugin has primarily been tested on Safari 5, Firefox 4, and Internet
// Explorer all the way back to version 6. While I haven't found any issues with
// it so far, I'm fairly sure it still doesn't work around all the quirks in all
// different browsers. But the code is still pretty simple overall, so you
// should be able to fix it and contribute a patch :)
// ## Annotated Source
(function($, undefined) {
// Register a prefilter that checks whether the `iframe` option is set, and
// switches to the iframe transport if it is `true`.
$.ajaxPrefilter(function(options, origOptions, jqXHR) {
if (options.iframe) {
return "iframe";
}
});
// Register an iframe transport, independent of requested data type. It will
// only activate when the "files" option has been set to a non-empty list of
// enabled file inputs.
$.ajaxTransport("iframe", function(options, origOptions, jqXHR) {
var form = null,
iframe = null,
origAction = null,
origTarget = null,
origEnctype = null,
addedFields = [],
disabledFields = [],
files = $(options.files).filter(":file:enabled");
// This function gets called after a successful submission or an abortion
// and should revert all changes made to the page to enable the
// submission via this transport.
function cleanUp() {
$(addedFields).each(function() {
this.remove();
});
$(disabledFields).each(function() {
this.disabled = false;
});
form.attr("action", origAction || "")
.attr("target", origTarget || "")
.attr("enctype", origEnctype || "");
iframe.attr("src", "javascript:false;").remove();
}
// Remove "iframe" from the data types list so that further processing is
// based on the content type returned by the server, without attempting an
// (unsupported) conversion from "iframe" to the actual type.
options.dataTypes.shift();
if (files.length) {
// Determine the form the file fields belong to, and make sure they all
// actually belong to the same form.
files.each(function() {
if (form !== null && this.form !== form) {
jQuery.error("All file fields must belong to the same form");
}
form = this.form;
});
form = $(form);
// Store the original form attributes that we'll be replacing temporarily.
origAction = form.attr("action");
origTarget = form.attr("target");
origEnctype = form.attr("enctype");
// We need to disable all other inputs in the form so that they don't get
// included in the submitted data unexpectedly.
form.find(":input:not(:submit)").each(function() {
if (!this.disabled && (this.type != "file" || files.index(this) < 0)) {
this.disabled = true;
disabledFields.push(this);
}
});
// If there is any additional data specified via the `data` option,
// we add it as hidden fields to the form. This (currently) requires
// the `processData` option to be set to false so that the data doesn't
// get serialized to a string.
if (typeof(options.data) === "string" && options.data.length > 0) {
jQuery.error("data must not be serialized");
}
$.each(options.data || {}, function(name, value) {
if ($.isPlainObject(value)) {
name = value.name;
value = value.value;
}
addedFields.push($("<input type='hidden'>").attr("name", name)
.attr("value", value).appendTo(form));
});
// Add a hidden `X-Requested-With` field with the value `IFrame` to the
// field, to help server-side code to determine that the upload happened
// through this transport.
addedFields.push($("<input type='hidden' name='X-Requested-With'>")
.attr("value", "IFrame").appendTo(form));
// Borrowed straight from the JQuery source
// Provides a way of specifying the accepted data type similar to HTTP_ACCEPTS
accepts = options.dataTypes[ 0 ] && options.accepts[ options.dataTypes[0] ] ?
options.accepts[ options.dataTypes[0] ] + ( options.dataTypes[ 0 ] !== "*" ? ", */*; q=0.01" : "" ) :
options.accepts[ "*" ]
addedFields.push($("<input type='hidden' name='X-Http-Accept'>")
.attr("value", accepts).appendTo(form));
return {
// The `send` function is called by jQuery when the request should be
// sent.
send: function(headers, completeCallback) {
iframe = $("<iframe src='javascript:false;' name='iframe-" + $.now()
+ "' style='display:none'></iframe>");
// The first load event gets fired after the iframe has been injected
// into the DOM, and is used to prepare the actual submission.
iframe.bind("load", function() {
// The second load event gets fired when the response to the form
// submission is received. The implementation detects whether the
// actual payload is embedded in a `<textarea>` element, and
// prepares the required conversions to be made in that case.
iframe.unbind("load").bind("load", function() {
var doc = this.contentWindow ? this.contentWindow.document :
(this.contentDocument ? this.contentDocument : this.document),
root = doc.documentElement ? doc.documentElement : doc.body,
textarea = root.getElementsByTagName("textarea")[0],
type = textarea ? textarea.getAttribute("data-type") : null;
var status = textarea ? parseInt(textarea.getAttribute("response-code")) : 200,
statusText = "OK",
responses = { text: type ? textarea.value : root ? root.innerHTML : null },
headers = "Content-Type: " + (type || "text/html")
completeCallback(status, statusText, responses, headers);
setTimeout(cleanUp, 50);
});
// Now that the load handler has been set up, reconfigure and
// submit the form.
form.attr("action", options.url)
.attr("target", iframe.attr("name"))
.attr("enctype", "multipart/form-data")
.get(0).submit();
});
// After everything has been set up correctly, the iframe gets
// injected into the DOM so that the submission can be initiated.
iframe.insertAfter(form);
},
// The `abort` function is called by jQuery when the request should be
// aborted.
abort: function() {
if (iframe !== null) {
iframe.unbind("load").attr("src", "javascript:false;");
cleanUp();
}
}
};
}
});
})(jQuery);

View file

@ -0,0 +1,69 @@
//= require jquery.iframe-transport.js
//= require_self
(function($) {
var remotipart;
$.remotipart = remotipart = {
setup: function(form) {
form
// Allow setup part of $.rails.handleRemote to setup remote settings before canceling default remote handler
// This is required in order to change the remote settings using the form details
.one('ajax:beforeSend.remotipart', function(e, xhr, settings){
// Delete the beforeSend bindings, since we're about to re-submit via ajaxSubmit with the beforeSubmit
// hook that was just setup and triggered via the default `$.rails.handleRemote`
// delete settings.beforeSend;
delete settings.beforeSend;
settings.iframe = true;
settings.files = $($.rails.fileInputSelector, form);
settings.data = form.serializeArray();
settings.processData = false;
// Modify some settings to integrate JS request with rails helpers and middleware
if (settings.dataType === undefined) { settings.dataType = 'script *'; }
settings.data.push({name: 'remotipart_submitted', value: true});
// Allow remotipartSubmit to be cancelled if needed
if ($.rails.fire(form, 'ajax:remotipartSubmit', [xhr, settings])) {
// Second verse, same as the first
$.rails.ajax(settings);
}
//Run cleanup
remotipart.teardown(form);
// Cancel the jQuery UJS request
return false;
})
// Keep track that we just set this particular form with Remotipart bindings
// Note: The `true` value will get over-written with the `settings.dataType` from the `ajax:beforeSend` handler
.data('remotipartSubmitted', true);
},
teardown: function(form) {
form
.unbind('ajax:beforeSend.remotipart')
.removeData('remotipartSubmitted')
}
};
$('form').live('ajax:aborted:file', function(){
var form = $(this);
remotipart.setup(form);
// If browser does not support submit bubbling, then this live-binding will be called before direct
// bindings. Therefore, we should directly call any direct bindings before remotely submitting form.
if (!$.support.submitBubbles && $().jquery < '1.7' && $.rails.callFormSubmitBindings(form) === false) return $.rails.stopEverything(e);
// Manually call jquery-ujs remote call so that it can setup form and settings as usual,
// and trigger the `ajax:beforeSend` callback to which remotipart binds functionality.
$.rails.handleRemote(form);
return false;
});
})(jQuery);