diff --git a/Gemfile b/Gemfile index 4f978d144c6a3d1217118b6cbfeb5c56360622da..ed7eea7a6c80a4a6359519f5d79240c292516055 100644 --- a/Gemfile +++ b/Gemfile @@ -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' diff --git a/Gemfile.lock b/Gemfile.lock index 32c069695cf6c207fe4093e9ded87d166a240d8b..98f6dc1e771782cd975ada1e1f1440b21ccc4e7f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index c2d1978da9acb6a8b906a6a1156f7dbe5c77d51c..30556bfc619863c4e24cc1c70624efcff037fc46 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -41,21 +41,23 @@ class PhotosController < ApplicationController end def create - begin - raise 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 + 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] - params[:photo][:user_file] = file_handler(params) + 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 - @photo = current_user.build_post(:photo, params[:photo]) + params[:photo][:user_file] = file_handler(params) - 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 diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 7fff7c82edc5c1eef2a1b5918206acd8249dd7ae..e4cb4b10462fe486821f67454b20bf3103ecf99d 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -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 diff --git a/app/controllers/status_messages_controller.rb b/app/controllers/status_messages_controller.rb index 5953993a925886268f86e03d565d8ec4bdac76da..b6764b890a61c456607587c7b1635797e44c5878 100644 --- a/app/controllers/status_messages_controller.rb +++ b/app/controllers/status_messages_controller.rb @@ -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 diff --git a/app/views/photos/_new_photo.haml b/app/views/photos/_new_photo.haml index 4da494f18861e0c3a665b7c713bf02794a0c5df7..95fa60e27b5fa8a9308ee2ab80df1d41ccf6c6b7 100644 --- a/app/views/photos/_new_photo.haml +++ b/app/views/photos/_new_photo.haml @@ -83,4 +83,4 @@ }); } - createUploader(); + createUploader(); \ No newline at end of file diff --git a/app/views/posts/new.html.haml b/app/views/posts/new.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..a20a287a24b03b6c48b7ca87ec401dfd068d35cb --- /dev/null +++ b/app/views/posts/new.html.haml @@ -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 + + }); + }); \ No newline at end of file diff --git a/config/assets.yml b/config/assets.yml index 5212580a3f517eacf6fbd6f3e1642499b7f0afc5..e7086b92b16730b5c9a8c798752f43fed64f82cc 100644 --- a/config/assets.yml +++ b/config/assets.yml @@ -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 diff --git a/public/javascripts/rails.js b/public/javascripts/rails.js index 2fbb9b83c06cc9339d4a82d28c02cb23a1fd41b3..028b0fc62b03ad387c76651faf114255c13b52ec 100644 --- a/public/javascripts/rails.js +++ b/public/javascripts/rails.js @@ -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) }; + // Shorthand to make it a little easier to call public rails functions from within rails.js + var rails; + + $.rails = rails = { + // Link elements bound by jquery-ujs + linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]', + + // Select elements bound by jquery-ujs + inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]', + + // Form elements bound by jquery-ujs + formSubmitSelector: 'form', + + // 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; + } + + 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> + 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" />'; + } + + if (target) { form.attr('target', target); } + + form.hide().append(metadata_input).appendTo('body'); + form.submit(); + }, + + /* 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); }); - return xhr; - }; - } - else $(document).ajaxSend(function(e, xhr) { - CSRFProtection(function(setHeader) { setHeader(xhr) }); - }); + }, + + /* 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); + }); + }, + + /* For 'data-confirm' attribute: + - Fires `confirm` event + - Shows the confirmation dialog + - Fires the `confirm:complete` event - // 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; - } - - // Submits "remote" forms and links with ajax - function handleRemote(element) { - var method, url, data, - dataType = element.attr('data-type') || ($.ajaxSettings && $.ajaxSettings.dataType); - - 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); + 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 (rails.fire(element, 'confirm')) { + answer = rails.confirm(message); + callback = rails.fire(element, 'confirm:complete', [answer]); } - } else { - method = element.attr('data-method'); - url = element.attr('href'); - data = null; - } + return answer && callback; + }, - $.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); + // 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 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]); + }); + 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; + }, + + // 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); + }); } - }); - } - - // 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'); - - if (csrf_param !== undefined && csrf_token !== undefined) { - metadata_input += '<input name="' + csrf_param + '" value="' + csrf_token + '" type="hidden" />'; - } + 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) + }); + }, - // 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]})); + // 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 } - form.hide().append(metadata_input).appendTo('body'); - form.submit(); - } - - 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'); - }); - } - - function enableFormElements(form) { - form.find('input[data-disable-with]').each(function() { - var input = $(this); - input.val(input.data('ujs:enable-with')).removeAttr('disabled'); - }); - } - - function allowAction(element) { - var message = element.attr('data-confirm'); - return !message || (fire(element, 'confirm') && confirm(message)); - } - - function requiredValuesMissing(form) { - var missing = false; - form.find('input[name][required]').each(function() { - if (!$(this).val()) missing = true; - }); - return missing; - } - - $('a[data-confirm], a[data-method], a[data-remote]').live('click.rails', function(e) { - var link = $(this); - if (!allowAction(link)) return false; + }; + + $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }}); + + $(document).delegate(rails.linkDisableSelector, 'ajax:complete', function() { + rails.enableElement($(this)); + }); - if (link.attr('data-remote') != undefined) { - handleRemote(link); + $(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.attr('data-method')) { - handleMethod(link); + + } 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); + + 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); - // skip other logic when required values are missing - if (requiredValuesMissing(form)) return !remote; + 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 ); +})( jQuery ); diff --git a/public/javascripts/vendor/jquery.iframe-transport.js b/public/javascripts/vendor/jquery.iframe-transport.js new file mode 100644 index 0000000000000000000000000000000000000000..3407da0a539ea94b1995c889e094ee79cc0d9528 --- /dev/null +++ b/public/javascripts/vendor/jquery.iframe-transport.js @@ -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); diff --git a/public/javascripts/vendor/jquery.remotipart.js b/public/javascripts/vendor/jquery.remotipart.js new file mode 100644 index 0000000000000000000000000000000000000000..9e00854e00937bf108c2ad7e730e83bf90e70da5 --- /dev/null +++ b/public/javascripts/vendor/jquery.remotipart.js @@ -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);