diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e768658..fe03033a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,4 +22,24 @@ to newest): - [`8063d1d47ea6a08e`](https://github.com/rails/jquery-ujs/commit/8063d1d47ea6a08e545e9a6ba3e84af584200e41) made $.rails.confirm and $.rails.ajax functions available in $.rails object - [`a96c4e9b074998c6`](https://github.com/rails/jquery-ujs/commit/a96c4e9b074998c6b6d102e4573b81c8a76f07a7) added support for jQuery 1.6.1 - [`dad6982dc5926866`](https://github.com/rails/jquery-ujs/commit/dad6982dc592686677e6845e681233c40d2ead27) added support for `data-params` attribute on remote links -- [`5433841d01622345`](https://github.com/rails/jquery-ujs/commit/5433841d01622345f734f22f82394ac035c2f783) removed support for jquery 1.4.4 and 1.5.x, and added support for jquery 1.6.2 +- [`5433841d01622345`](https://github.com/rails/jquery-ujs/commit/5433841d01622345f734f22f82394ac035c2f783) removed support for jQuery 1.4.4 and 1.5.x, and added support for jQuery 1.6.2 +- [`cd619df9f0daad33`](https://github.com/rails/jquery-ujs/commit/cd619df9f0daad3303aacd4f992fff19158b1e5d) added support for html5 `novalidate` attribute, so required fields validation is not enforced +- [`840ab6ac76b2d5ab`](https://github.com/rails/jquery-ujs/commit/840ab6ac76b2d5ab931841bc3d8567e5b57f183e) added support for jQuery 1.6.3 +- [`ba5808e73111fb65`](https://github.com/rails/jquery-ujs/commit/ba5808e73111fb65e91610b078577bb014d9b6d8) added `data-remote` support for checkboxes +- [`6e9a06d45eaf2da1`](https://github.com/rails/jquery-ujs/commit/6e9a06d45eaf2da1036d4c2ead25ff57d0127d03) added `data-disable-with` support for links +- [`89396108ce574080`](https://github.com/rails/jquery-ujs/commit/89396108ce574080f9b877cad74573c5d1ae9aa2) added `data-remote` support for all input types +- [`c01215c3d48ebb9f`](https://github.com/rails/jquery-ujs/commit/c01215c3d48ebb9f9f1059f26efa0c0c9092da2b) added support for jQuery 1.6.4 +- [`17f4004310b6ece3`](https://github.com/rails/jquery-ujs/commit/17f4004310b6ece3cb240914932b4d6d46032c24) added support for jQuery 1.7 +- [`cb54ae287f5c7320`](https://github.com/rails/jquery-ujs/commit/cb54ae287f5c73207aef2891cdf22212aea5fb86) added support for jQuery 1.7.1 +- [`dbb1b5f72a62e59f`](https://github.com/rails/jquery-ujs/commit/dbb1b5f72a62e59f34f6b5be4bee291ee7f3318f) added support for jQuery 1.7.2 +- [`8100cf3b2462f144`](https://github.com/rails/jquery-ujs/commit/8100cf3b2462f144e6a0bcef7cb78d05be41755d) created `rails:attachBindings` to allow for customization of + $.rails object settings +- [`e4ca2045b202cd7a`](https://github.com/rails/jquery-ujs/commit/e4ca2045b202cd7ade97d78c20caa2822c5c28da) created `ajax:send` event to provide access to jqXHR object from + ajax requests +- [`4382f580766fcdd1`](https://github.com/rails/jquery-ujs/commit/4382f580766fcdd14201c204f43ca5aeb0928501) added support for `data-with-credentials` +- [`12da9fc2f175c8e4`](https://github.com/rails/jquery-ujs/commit/12da9fc2f175c8e445413b15cf6b685deb271d6e) added support for jQuery 1.8.0, removed support for jquery 1.6.x +- [`faeb0ad734ff6867`](https://github.com/rails/jquery-ujs/commit/faeb0ad734ff6867149b8522f9a29081734442e6) added support for jQuery 1.8.1 +- [`b6dae4ef4a2d031a`](https://github.com/rails/jquery-ujs/commit/b6dae4ef4a2d031a222627c7f6a4284602f99160) added support for jQuery 1.8.2 +- [`6927b82cadf3146c`](https://github.com/rails/jquery-ujs/commit/6927b82cadf3146c2b9ae3028e9b197af64011ca) added support for jQuery 1.8.3 +- [`cc356656cc3edf15`](https://github.com/rails/jquery-ujs/commit/cc356656cc3edf1596fd685265187d2f75d1bc7c) added support for jQuery 1.9.0 +- [`2f8ccdf26eac199a`](https://github.com/rails/jquery-ujs/commit/2f8ccdf26eac199a11aa1a893a8909bb4650d0fb) added support for jQuery 1.9.1 diff --git a/Gemfile b/Gemfile index e32e523b..aca39a6b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ -source 'http://rubygems.org' +source 'https://rubygems.org' gem 'sinatra', '~> 1.0' gem 'shotgun', :group => :reloadable gem 'thin', :group => :reloadable -gem 'json' +gem 'rake' diff --git a/Gemfile.lock b/Gemfile.lock index 42460e58..aa541a84 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,10 @@ GEM - remote: http://rubygems.org/ + remote: https://rubygems.org/ specs: daemons (1.1.0) eventmachine (0.12.10) - json (1.4.6) rack (1.2.1) + rake (10.1.1) shotgun (0.8) rack (>= 1.0) sinatra (1.1.2) @@ -20,7 +20,7 @@ PLATFORMS ruby DEPENDENCIES - json + rake shotgun sinatra (~> 1.0) thin diff --git a/README.md b/README.md index 0a200d8e..d2442a5e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Full [documentation is on the wiki][wiki], including the [list of published Ajax Requirements ------------ -- [jQuery 1.6][jquery] or later; +- [jQuery 1.8.x or higher and less than 2.0][jquery]; - HTML5 doctype (optional). If you don't use HTML5, adding "data" attributes to your HTML4 or XHTML pages might make them fail [W3C markup validation][validator]. However, this shouldn't create any issues for web browsers or other user agents. @@ -26,7 +26,7 @@ Installation For automated installation in Rails, use the "jquery-rails" gem. Place this in your Gemfile: ```ruby -gem 'jquery-rails', '>= 1.0.12' +gem 'jquery-rails', '~> 2.1' ``` And run: @@ -42,7 +42,7 @@ a. For Rails 3.1, add these lines to the top of your app/assets/javascripts/appl //= require jquery_ujs ``` -b. For Rails 3.0, run this command (add `--ui` if you want jQuery UI): +b. For Rails 3.0, run this command: *Be sure to get rid of the rails.js file if it exists, and instead use the new jquery_ujs.js file that gets copied to the public directory. @@ -50,36 +50,15 @@ Choose to overwrite jquery_ujs.js if prompted.* $ rails generate jquery:install +c. For Rails 2.x and for manual installation follow [this wiki](https://github.com/rails/jquery-ujs/wiki/Manual-installing-and-Rails-2) . -### Manual installation (including Rails 2) - -[Download jQuery][jquery] and ["rails.js"][adapter] and place them in your "javascripts" directory. - -Configure the following in your application startup file: - -```ruby - config.action_view.javascript_expansions[:defaults] = %w(jquery rails) -``` - -Now the template helper `javascript_include_tag :defaults` will generate SCRIPT tags to load jQuery and rails.js. - -For Rails 2, you will need to manually implement the `csrf_meta_tag` helper and include it inside the `` of your application layout. +How to run tests +------------ -The `csrf_meta_tags` (Rails 3.1) and `csrf_meta_tag` (Rails 3.0) helpers generate two meta tags containing values necessary for the [cross-site request forgery protection][csrf] built into Rails. Here is how to implement that helper in Rails 2: +Follow [this wiki](https://github.com/rails/jquery-ujs/wiki/Running-Tests-and-Contributing) to run tests . -```ruby - # app/helpers/application_helper.rb - def csrf_meta_tag - if protect_against_forgery? - out = %(\n) - out << %() - out % [ Rack::Utils.escape_html(request_forgery_protection_token), - Rack::Utils.escape_html(form_authenticity_token) ] - end - end -``` -[data]: http://dev.w3.org/html5/spec/elements.html#embedding-custom-non-visible-data-with-the-data-attributes "Embedding custom non-visible data with the data-* attributes" +[data]: http://www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes "Embedding custom non-visible data with the data-* attributes" [wiki]: https://github.com/rails/jquery-ujs/wiki [events]: https://github.com/rails/jquery-ujs/wiki/ajax [jquery]: http://docs.jquery.com/Downloading_jQuery diff --git a/bower.json b/bower.json new file mode 100644 index 00000000..ecbb1382 --- /dev/null +++ b/bower.json @@ -0,0 +1,18 @@ +{ + "name": "jquery-ujs", + "homepage": "https://github.com/rails/jquery-ujs", + "authors": [], + "description": "Ruby on Rails unobtrusive scripting adapter for jQuery", + "main": "src/rails.js", + "license": "MIT", + "dependencies": { + "jquery": ">1.7.* <2.0.0" + }, + "ignore": [ + "**/.*", + "Gemfile*", + "Rakefile", + "bower_components", + "test" + ] +} diff --git a/src/rails.js b/src/rails.js index 06b4e0b5..6705a6de 100644 --- a/src/rails.js +++ b/src/rails.js @@ -2,55 +2,30 @@ /** * Unobtrusive scripting adapter for jQuery - * - * 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. + * Requires jQuery 1.7.0 or later. * - * !! 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. + * Released under the MIT license * - * 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?"); - * }); */ + // Cut down on the number of issues from people inadvertently including jquery_ujs twice + // by detecting and raising an error when it happens. + if ( $.rails !== undefined ) { + $.error('jquery-ujs has already been loaded!'); + } + // Shorthand to make it a little easier to call public rails functions from within rails.js var rails; + var $document = $(document); $.rails = rails = { // Link elements bound by jquery-ujs - linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]', + linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]', + + // Button elements bound by jquery-ujs + buttonClickSelector: 'button[data-remote], button[data-confirm]', // Select elements bound by jquery-ujs inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]', @@ -59,22 +34,25 @@ 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])', + formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])', // Form input elements disabled during form submission - disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]', + disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with], input[data-disable], button[data-disable], textarea[data-disable]', // Form input elements re-enabled after form submission - enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled', + enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled', // Form required input elements requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])', // Form file input elements - fileInputSelector: 'input:file', + fileInputSelector: 'input[type=file]', // Link onClick disable selector with possible reenable after remote submission - linkDisableSelector: 'a[data-disable-with]', + linkDisableSelector: 'a[data-disable-with], a[data-disable]', + + // Button onClick disable selector with possible reenable after remote submission + buttonDisableSelector: 'button[data-remote][data-disable-with], button[data-remote][data-disable]', // Make sure that every Ajax request sends the CSRF token CSRFProtection: function(xhr) { @@ -82,6 +60,13 @@ if (token) xhr.setRequestHeader('X-CSRF-Token', token); }, + // making sure that all forms have actual up-to-date token(cached forms contain old one) + refreshCSRFTokens: function(){ + var csrfToken = $('meta[name=csrf-token]').attr('content'); + var csrfParam = $('meta[name=csrf-param]').attr('content'); + $('form input[name="' + csrfParam + '"]').val(csrfToken); + }, + // Triggers an event on an element and returns false if the event result is false fire: function(obj, name, data) { var event = $.Event(name); @@ -99,14 +84,20 @@ return $.ajax(options); }, + // Default way to get an element's href. May be overridden at $.rails.href. + href: function(element) { + return element.attr('href'); + }, + // 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; + var method, url, data, elCrossDomain, crossDomain, withCredentials, dataType, options; if (rails.fire(element, 'ajax:before')) { + elCrossDomain = element.data('cross-domain'); + crossDomain = elCrossDomain === undefined ? null : elCrossDomain; + withCredentials = element.data('with-credentials') || null; + dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType); if (element.is('form')) { method = element.attr('method'); @@ -123,20 +114,29 @@ url = element.data('url'); data = element.serialize(); if (element.data('params')) data = data + "&" + element.data('params'); + } else if (element.is(rails.buttonClickSelector)) { + method = element.data('method') || 'get'; + 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'); + url = rails.href(element); data = element.data('params') || null; } options = { - type: method || 'GET', data: data, dataType: dataType, crossDomain: crossDomain, + 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); } - return rails.fire(element, 'ajax:beforeSend', [xhr, settings]); + if (rails.fire(element, 'ajax:beforeSend', [xhr, settings])) { + element.trigger('ajax:send', xhr); + } else { + return false; + } }, success: function(data, status, xhr) { element.trigger('ajax:success', [data, status, xhr]); @@ -146,8 +146,18 @@ }, error: function(xhr, status, error) { element.trigger('ajax:error', [xhr, status, error]); - } + }, + crossDomain: crossDomain }; + + // There is no withCredentials for IE6-8 when + // "Enable native XMLHTTP support" is disabled + if (withCredentials) { + options.xhrFields = { + withCredentials: withCredentials + }; + } + // Only pass url to `ajax` options if not blank if (url) { options.url = url; } @@ -160,50 +170,72 @@ // Handles "data-method" on links such as: // Delete handleMethod: function(link) { - var href = link.attr('href'), + var href = rails.href(link), method = link.data('method'), target = link.attr('target'), - csrf_token = $('meta[name=csrf-token]').attr('content'), - csrf_param = $('meta[name=csrf-param]').attr('content'), + csrfToken = $('meta[name=csrf-token]').attr('content'), + csrfParam = $('meta[name=csrf-param]').attr('content'), form = $('
'), - metadata_input = ''; + metadataInput = ''; - if (csrf_param !== undefined && csrf_token !== undefined) { - metadata_input += ''; + if (csrfParam !== undefined && csrfToken !== undefined) { + metadataInput += ''; } if (target) { form.attr('target', target); } - form.hide().append(metadata_input).appendTo('body'); + form.hide().append(metadataInput).appendTo('body'); form.submit(); }, + // Helper function that returns form elements that match the specified CSS selector + // If form is actually a "form" element this will return associated elements outside the from that have + // the html form attribute set + formElements: function(form, selector) { + return form.is('form') ? $(form[0].elements).filter(selector) : form.find(selector); + }, + /* 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); + rails.formElements(form, rails.disableSelector).each(function() { + rails.disableFormElement($(this)); }); }, + disableFormElement: function(element) { + var method, replacement; + + method = element.is('button') ? 'html' : 'val'; + replacement = element.data('disable-with'); + + element.data('ujs:enable-with', element[method]()); + if (replacement !== undefined) { + element[method](replacement); + } + + element.prop('disabled', true); + }, + /* 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); + rails.formElements(form, rails.enableSelector).each(function() { + rails.enableFormElement($(this)); }); }, + enableFormElement: function(element) { + var 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 @@ -228,12 +260,21 @@ // 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() { + var inputs = $(), input, valueToCheck, + selector = specifiedSelector || 'input,textarea', + allInputs = form.find(selector); + + allInputs.each(function() { input = $(this); - // Collect non-blank inputs if nonBlank option is true, otherwise, collect blank inputs - if (nonBlank ? input.val() : !input.val()) { + valueToCheck = input.is('input[type=checkbox],input[type=radio]') ? input.is(':checked') : input.val(); + // If nonBlank and valueToCheck are both truthy, or nonBlank and valueToCheck are both falsey + if (!valueToCheck === !nonBlank) { + + // Don't count unchecked required radio if other radio with same name is checked + if (input.is('input[type=radio]') && allInputs.filter('input[type=radio]:checked[name="' + input.attr('name') + '"]').length) { + return true; // Skip to next input + } + inputs = inputs.add(input); } }); @@ -252,25 +293,18 @@ 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); - }); - } - return continuePropagation; - }, - // replace element's html with the 'data-disable-with' after storing original html // and prevent clicking on it disableElement: function(element) { + var replacement = element.data('disable-with'); + element.data('ujs:enable-with', element.html()); // store enabled state - element.html(element.data('disable-with')); // set to disabled state + if (replacement !== undefined) { + element.html(replacement); + } + element.bind('click.railsDisable', function(e) { // prevent further clicking - return rails.stopEverything(e) + return rails.stopEverything(e); }); }, @@ -278,96 +312,134 @@ 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.removeData('ujs:enable-with'); // clean up cache } element.unbind('click.railsDisable'); // enable element } - }; - $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }}); + if (rails.fire($document, 'rails:attachBindings')) { + + $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }}); + + $document.delegate(rails.linkDisableSelector, 'ajax:complete', function() { + rails.enableElement($(this)); + }); - $(document).delegate(rails.linkDisableSelector, 'ajax:complete', function() { - rails.enableElement($(this)); - }); + $document.delegate(rails.buttonDisableSelector, 'ajax:complete', function() { + rails.enableFormElement($(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); + $document.delegate(rails.linkClickSelector, 'click.rails', function(e) { + var link = $(this), method = link.data('method'), data = link.data('params'), metaClick = e.metaKey || e.ctrlKey; + if (!rails.allowAction(link)) return rails.stopEverything(e); - if (link.is(rails.linkDisableSelector)) rails.disableElement(link); + if (!metaClick && link.is(rails.linkDisableSelector)) rails.disableElement(link); - if (link.data('remote') !== undefined) { - if ( (e.metaKey || e.ctrlKey) && (!method || method === 'GET') && !data ) { return true; } + if (link.data('remote') !== undefined) { + if (metaClick && (!method || method === 'GET') && !data) { return true; } - if (rails.handleRemote(link) === false) { rails.enableElement(link); } + var handleRemote = rails.handleRemote(link); + // response from rails.handleRemote() will either be false or a deferred object promise. + if (handleRemote === false) { + rails.enableElement(link); + } else { + handleRemote.error( function() { rails.enableElement(link); } ); + } + return false; + + } else if (link.data('method')) { + rails.handleMethod(link); + return false; + } + }); + + $document.delegate(rails.buttonClickSelector, 'click.rails', function(e) { + var button = $(this); + if (!rails.allowAction(button)) return rails.stopEverything(e); + + if (button.is(rails.buttonDisableSelector)) rails.disableFormElement(button); + + var handleRemote = rails.handleRemote(button); + // response from rails.handleRemote() will either be false or a deferred object promise. + if (handleRemote === false) { + rails.enableFormElement(button); + } else { + handleRemote.error( function() { rails.enableFormElement(button); } ); + } return false; + }); - } else if (link.data('method')) { - rails.handleMethod(link); + $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.inputChangeSelector, 'change.rails', function(e) { - var link = $(this); - if (!rails.allowAction(link)) return rails.stopEverything(e); + $document.delegate(rails.formSubmitSelector, 'submit.rails', function(e) { + var form = $(this), + remote = form.data('remote') !== undefined, + blankRequiredInputs, + nonBlankFileInputs; - rails.handleRemote(link); - return false; - }); + if (!rails.allowAction(form)) return rails.stopEverything(e); - $(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 or file upload is present + if (form.attr('novalidate') == undefined) { + blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector); + if (blankRequiredInputs && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) { + return rails.stopEverything(e); + } + } - if (!rails.allowAction(form)) return rails.stopEverything(e); + if (remote) { + nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); + if (nonBlankFileInputs) { + // slight timeout so that the submit button gets properly serialized + // (make it easy for event handler to serialize form without disabled values) + setTimeout(function(){ rails.disableFormElements(form); }, 13); + var aborted = rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]); - // 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); - } + // re-enable form elements if event bindings return false (canceling normal form submission) + if (!aborted) { setTimeout(function(){ rails.enableFormElements(form); }, 13); } - if (remote) { - if (nonBlankFileInputs) { - return rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]); - } + return aborted; + } - // 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; - rails.handleRemote(form); - return false; + } else { + // slight timeout so that the submit button gets properly serialized + setTimeout(function(){ rails.disableFormElements(form); }, 13); + } + }); - } else { - // slight timeout so that the submit button gets properly serialized - setTimeout(function(){ rails.disableFormElements(form); }, 13); - } - }); + $document.delegate(rails.formInputClickSelector, 'click.rails', function(event) { + var button = $(this); - $(document).delegate(rails.formInputClickSelector, 'click.rails', function(event) { - var button = $(this); + if (!rails.allowAction(button)) return rails.stopEverything(event); - 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; - // register the pressed submit button - var name = button.attr('name'), - data = name ? {name:name, value:button.val()} : null; + button.closest('form').data('ujs:submit-button', data); + }); - button.closest('form').data('ujs:submit-button', data); - }); + $document.delegate(rails.formSubmitSelector, 'ajax:send.rails', function(event) { + if (this == event.target) rails.disableFormElements($(this)); + }); - $(document).delegate(rails.formSubmitSelector, 'ajax:beforeSend.rails', function(event) { - if (this == event.target) rails.disableFormElements($(this)); - }); + $document.delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) { + if (this == event.target) rails.enableFormElements($(this)); + }); - $(document).delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) { - if (this == event.target) rails.enableFormElements($(this)); - }); + $(function(){ + rails.refreshCSRFTokens(); + }); + } })( jQuery ); diff --git a/test/public/test/call-remote-callbacks.js b/test/public/test/call-remote-callbacks.js index 939f523f..fa785be9 100644 --- a/test/public/test/call-remote-callbacks.js +++ b/test/public/test/call-remote-callbacks.js @@ -7,10 +7,13 @@ module('call-remote-callbacks', { })); }, teardown: function() { - $('form[data-remote]').die('ajax:beforeSend'); - $('form[data-remote]').die('ajax:before'); - $('form[data-remote]').die('ajax:complete'); - $('form[data-remote]').die('ajax:success'); + $(document).undelegate('form[data-remote]', 'ajax:beforeSend'); + $(document).undelegate('form[data-remote]', 'ajax:before'); + $(document).undelegate('form[data-remote]', 'ajax:send'); + $(document).undelegate('form[data-remote]', 'ajax:complete'); + $(document).undelegate('form[data-remote]', 'ajax:success'); + $(document).unbind('ajaxStop'); + $(document).unbind('iframe:loading'); } }); @@ -29,7 +32,7 @@ asyncTest('modifying form fields with "ajax:before" sends modified data in reque $('form[data-remote]') .append($('')) .append($('')) - .live('ajax:before', function() { + .bind('ajax:before', function() { var form = $(this); form .append($('',{name: 'other_user_name',value: 'jonathan'})) @@ -47,34 +50,80 @@ asyncTest('modifying form fields with "ajax:before" sends modified data in reque }); }); +asyncTest('modifying data("type") with "ajax:before" requests new dataType in request', 2, function(){ + $('form[data-remote]').data('type','html') + .bind('ajax:before', function() { + var form = $(this); + form.data('type','xml'); + }); + + submit(function(form) { + form.bind('ajax:beforeSend', function(e, xhr, settings) { + equal(settings.dataType, 'xml', 'modified dataType should have been requested'); + }); + }); +}); + +asyncTest('setting data("cross-domain",true) with "ajax:before" uses new setting in request', 2, function(){ + $('form[data-remote]').data('cross-domain',false) + .bind('ajax:before', function() { + var form = $(this); + form.data('cross-domain',true); + }); + + submit(function(form) { + form.bind('ajax:beforeSend', function(e, xhr, settings) { + equal(settings.crossDomain, true, 'setting modified in ajax:before should have forced cross-domain request'); + }); + }); +}); + +asyncTest('setting data("with-credentials",true) with "ajax:before" uses new setting in request', 2, function(){ + $('form[data-remote]').data('with-credentials',false) + .bind('ajax:before', function() { + var form = $(this); + form.data('with-credentials',true); + }); + + submit(function(form) { + form.bind('ajax:beforeSend', function(e, xhr, settings) { + equal(settings.xhrFields && settings.xhrFields.withCredentials, true, 'setting modified in ajax:before should have forced withCredentials request'); + }); + }); +}); + asyncTest('stopping the "ajax:beforeSend" event aborts the request', 1, function() { submit(function(form) { form.bind('ajax:beforeSend', function() { - ok(true, 'aborting request in ajax:beforeSend') + ok(true, 'aborting request in ajax:beforeSend'); return false; }); + form.unbind('ajax:send').bind('ajax:send', function() { + ok(false, 'ajax:send should not run'); + }); form.unbind('ajax:complete').bind('ajax:complete', function() { ok(false, 'ajax:complete should not run'); }); form.bind('ajax:error', function(e, xhr, status, error) { ok(false, 'ajax:error should not run'); }); - form.bind('ajaxStop', function() { + $(document).bind('ajaxStop', function() { start(); }); }); }); asyncTest('blank required form input field should abort request and trigger "ajax:aborted:required" event', 5, function() { + $(document).bind('iframe:loading', function() { + ok(false, 'form should not get submitted'); + }); + var form = $('form[data-remote]') .append($('')) .append($('')) .bind('ajax:beforeSend', function() { ok(false, 'ajax:beforeSend should not run'); }) - .bind('iframe:loading', function() { - ok(false, 'form should not get submitted'); - }) .bind('ajax:aborted:required', function(e,data){ ok(data.length == 2, 'ajax:aborted:required event is passed all blank required inputs (jQuery objects)'); ok(data.first().is('input[name="user_name"]') , 'ajax:aborted:required adds blank required input to data'); @@ -149,12 +198,42 @@ asyncTest('form should be submitted with blank required fields if it has the "no }); asyncTest('blank required form input for non-remote form with "novalidate" attribute should not abort normal submission', 1, function() { + $(document).bind('iframe:loading', function() { + ok(true, 'form should get submitted'); + }); + var form = $('form[data-remote]') .append($('')) .removeAttr('data-remote') .attr("novalidate","novalidate") - .bind('iframe:loading', function() { - ok(true, 'form should get submitted'); + .trigger('submit'); + + setTimeout(function() { + start(); + }, 13); +}); + +asyncTest('unchecked required checkbox should abort form submission', 1, function() { + var form = $('form[data-remote]') + .append($('')) + .removeAttr('data-remote') + .bind('ujs:everythingStopped', function() { + ok(true, 'ujs:everythingStopped should run'); + }) + .trigger('submit'); + + setTimeout(function() { + start(); + }, 13); +}); + +asyncTest('unchecked required radio should abort form submission', 1, function() { + var form = $('form[data-remote]') + .append($('')) + .append($('')) + .removeAttr('data-remote') + .bind('ujs:everythingStopped', function() { + ok(true, 'ujs:everythingStopped should run'); }) .trigger('submit'); @@ -163,77 +242,104 @@ asyncTest('blank required form input for non-remote form with "novalidate" attri }, 13); }); +asyncTest('required radio should only require one to be checked', 1, function() { + $(document).bind('iframe:loading', function() { + ok(true, 'form should get submitted'); + }); + + var form = $('form[data-remote]') + .append($('')) + .append($('')) + .removeAttr('data-remote') + .bind('ujs:everythingStopped', function() { + ok(false, 'ujs:everythingStopped should not run'); + }) + .find('#checkme').prop('checked', true) + .end() + .trigger('submit'); + + setTimeout(function() { + start(); + }, 13); +}); + function skipIt() { - // This test cannot work due to the security feature in browsers which makes the value - // attribute of file input fields readonly, so it cannot be set with default value. - // This is what the test would look like though if browsers let us automate this test. - asyncTest('non-blank file form input field should abort remote request, but submit normally', 5, function() { - var form = $('form[data-remote]') - .append($('')) - .bind('ajax:beforeSend', function() { - ok(false, 'ajax:beforeSend should not run'); - }) - .bind('iframe:loading', function() { - ok(true, 'form should get submitted'); - }) - .bind('ajax:aborted:file', function(e,data) { - ok(data.length == 1, 'ajax:aborted:file event is passed all non-blank file inputs (jQuery objects)'); - ok(data.first().is('input[name="attachment"]') , 'ajax:aborted:file adds non-blank file input to data'); - ok(true, 'ajax:aborted:file event should run'); - }) - .trigger('submit'); - - setTimeout(function() { - form.find('input[type="file"]').val(''); - form.unbind('ajax:beforeSend'); - submit(); - }, 13); - }); + // This test cannot work due to the security feature in browsers which makes the value + // attribute of file input fields readonly, so it cannot be set with default value. + // This is what the test would look like though if browsers let us automate this test. + asyncTest('non-blank file form input field should abort remote request, but submit normally', 5, function() { + var form = $('form[data-remote]') + .append($('')) + .bind('ajax:beforeSend', function() { + ok(false, 'ajax:beforeSend should not run'); + }) + .bind('iframe:loading', function() { + ok(true, 'form should get submitted'); + }) + .bind('ajax:aborted:file', function(e,data) { + ok(data.length == 1, 'ajax:aborted:file event is passed all non-blank file inputs (jQuery objects)'); + ok(data.first().is('input[name="attachment"]') , 'ajax:aborted:file adds non-blank file input to data'); + ok(true, 'ajax:aborted:file event should run'); + }) + .trigger('submit'); + + setTimeout(function() { + form.find('input[type="file"]').val(''); + form.unbind('ajax:beforeSend'); + submit(); + }, 13); + }); asyncTest('blank file input field should abort request entirely if handler bound to "ajax:aborted:file" event that returns false', 1, function() { - var form = $('form[data-remote]') - .append($('')) - .bind('ajax:beforeSend', function() { - ok(false, 'ajax:beforeSend should not run'); - }) - .bind('iframe:loading', function() { - ok(false, 'form should not get submitted'); - }) - .bind('ajax:aborted:file', function() { - return false; - }) - .trigger('submit'); - - setTimeout(function() { - form.find('input[type="file"]').val(''); - form.unbind('ajax:beforeSend'); - submit(); - }, 13); - }); + var form = $('form[data-remote]') + .append($('')) + .bind('ajax:beforeSend', function() { + ok(false, 'ajax:beforeSend should not run'); + }) + .bind('iframe:loading', function() { + ok(false, 'form should not get submitted'); + }) + .bind('ajax:aborted:file', function() { + return false; + }) + .trigger('submit'); + + setTimeout(function() { + form.find('input[type="file"]').val(''); + form.unbind('ajax:beforeSend'); + submit(); + }, 13); + }); } asyncTest('"ajax:beforeSend" can be observed and stopped with event delegation', 1, function() { - $('form[data-remote]').live('ajax:beforeSend', function() { + $(document).delegate('form[data-remote]', 'ajax:beforeSend', function() { ok(true, 'ajax:beforeSend observed with event delegation'); return false; }); submit(function(form) { + form.unbind('ajax:send').bind('ajax:send', function() { + ok(false, 'ajax:send should not run'); + }); form.unbind('ajax:complete').bind('ajax:complete', function() { ok(false, 'ajax:complete should not run'); }); - form.bind('ajaxStop', function() { + $(document).bind('ajaxStop', function() { start(); }); }); }); -asyncTest('"ajax:beforeSend", "ajax:success" and "ajax:complete" are triggered', 8, function() { +asyncTest('"ajax:beforeSend", "ajax:send", "ajax:success" and "ajax:complete" are triggered', 9, function() { submit(function(form) { form.bind('ajax:beforeSend', function(e, xhr, settings) { ok(xhr.setRequestHeader, 'first argument to "ajax:beforeSend" should be an XHR object'); equal(settings.url, '/echo', 'second argument to "ajax:beforeSend" should be a settings object'); }); + form.bind('ajax:send', function(e, xhr) { + ok(xhr.abort, 'first argument to "ajax:send" should be an XHR object'); + }); form.bind('ajax:success', function(e, data, status, xhr) { ok(data.REQUEST_METHOD, 'first argument to ajax:success should be a data object'); equal(status, 'success', 'second argument to ajax:success should be a status string'); @@ -246,14 +352,16 @@ asyncTest('"ajax:beforeSend", "ajax:success" and "ajax:complete" are triggered', }); }); -asyncTest('"ajax:beforeSend", "ajax:error" and "ajax:complete" are triggered on error', 6, function() { +asyncTest('"ajax:beforeSend", "ajax:send", "ajax:error" and "ajax:complete" are triggered on error', 7, function() { submit(function(form) { form.attr('action', '/error'); form.bind('ajax:beforeSend', function(arg) { ok(true, 'ajax:beforeSend') }); + form.bind('ajax:send', function(arg) { ok(true, 'ajax:send') }); form.bind('ajax:error', function(e, xhr, status, error) { ok(xhr.getResponseHeader, 'first argument to "ajax:error" should be an XHR object'); equal(status, 'error', 'second argument to ajax:error should be a status string'); - equal(error, 'Forbidden', 'third argument to ajax:error should be an HTTP status response'); + // Firefox 8 returns "Forbidden " with trailing space + equal($.trim(error), 'Forbidden', 'third argument to ajax:error should be an HTTP status response'); // Opera returns "0" for HTTP code equal(xhr.status, window.opera ? 0 : 403, 'status code should be 403'); }); @@ -261,22 +369,37 @@ asyncTest('"ajax:beforeSend", "ajax:error" and "ajax:complete" are triggered on }); // IF THIS TEST IS FAILING, TRY INCREASING THE TIMEOUT AT THE BOTTOM TO > 100 -asyncTest('binding to ajax callbacks via .live() triggers handlers properly', 3, function() { - $('form[data-remote]') - .live('ajax:beforeSend', function() { +asyncTest('binding to ajax callbacks via .delegate() triggers handlers properly', 4, function() { + $(document) + .delegate('form[data-remote]', 'ajax:beforeSend', function() { ok(true, 'ajax:beforeSend handler is triggered'); }) - .live('ajax:complete', function() { + .delegate('form[data-remote]', 'ajax:send', function() { + ok(true, 'ajax:send handler is triggered'); + }) + .delegate('form[data-remote]', 'ajax:complete', function() { ok(true, 'ajax:complete handler is triggered'); }) - .live('ajax:success', function() { + .delegate('form[data-remote]', 'ajax:success', function() { ok(true, 'ajax:success handler is triggered'); - }) - .trigger('submit'); + }); + $('form[data-remote]').trigger('submit'); setTimeout(function() { start(); }, 63); }); +asyncTest('binding to ajax:send event to call jquery methods on ajax object', 2, function() { + $('form[data-remote]') + .bind('ajax:send', function(e, xhr) { + ok(true, 'event should fire'); + equal(typeof(xhr.abort), 'function', 'event should pass jqXHR object'); + xhr.abort(); + }) + .trigger('submit'); + + setTimeout(function() { start(); }, 35); +}); + })(); diff --git a/test/public/test/call-remote.js b/test/public/test/call-remote.js index 5d48b00c..d78ce565 100644 --- a/test/public/test/call-remote.js +++ b/test/public/test/call-remote.js @@ -1,6 +1,6 @@ (function(){ -function build_form(attrs) { +function buildForm(attrs) { attrs = $.extend({ action: '/echo', 'data-remote': 'true' }, attrs); $('#qunit-fixture').append($('
', attrs)) @@ -17,47 +17,47 @@ function submit(fn) { } asyncTest('form method is read from "method" and not from "data-method"', 1, function() { - build_form({ method: 'post', 'data-method': 'get' }); + buildForm({ method: 'post', 'data-method': 'get' }); submit(function(e, data, status, xhr) { - App.assert_post_request(data); + App.assertPostRequest(data); }); }); asyncTest('form method is not read from "data-method" attribute in case of missing "method"', 1, function() { - build_form({ 'data-method': 'put' }); + buildForm({ 'data-method': 'put' }); submit(function(e, data, status, xhr) { - App.assert_get_request(data); + App.assertGetRequest(data); }); }); asyncTest('form default method is GET', 1, function() { - build_form(); + buildForm(); submit(function(e, data, status, xhr) { - App.assert_get_request(data); + App.assertGetRequest(data); }); }); asyncTest('form url is picked up from "action"', 1, function() { - build_form({ method: 'post' }); + buildForm({ method: 'post' }); submit(function(e, data, status, xhr) { - App.assert_request_path(data, '/echo'); + App.assertRequestPath(data, '/echo'); }); }); asyncTest('form url is read from "action" not "href"', 1, function() { - build_form({ method: 'post', href: '/echo2' }); + buildForm({ method: 'post', href: '/echo2' }); submit(function(e, data, status, xhr) { - App.assert_request_path(data, '/echo'); + App.assertRequestPath(data, '/echo'); }); }); asyncTest('prefer JS, but accept any format', 1, function() { - build_form({ method: 'post' }); + buildForm({ method: 'post' }); submit(function(e, data, status, xhr) { var accept = data.HTTP_ACCEPT; @@ -66,7 +66,7 @@ asyncTest('prefer JS, but accept any format', 1, function() { }); asyncTest('accept application/json if "data-type" is json', 1, function() { - build_form({ method: 'post', 'data-type': 'json' }); + buildForm({ method: 'post', 'data-type': 'json' }); submit(function(e, data, status, xhr) { equal(data.HTTP_ACCEPT, 'application/json, text/javascript, */*; q=0.01'); @@ -75,7 +75,7 @@ asyncTest('accept application/json if "data-type" is json', 1, function() { asyncTest('allow empty "data-remote" attribute', 1, function() { var form = $('#qunit-fixture').append($('')).find('form'); - + submit(function() { ok(true, 'form with empty "data-remote" attribute is also allowed'); }); @@ -84,7 +84,7 @@ asyncTest('allow empty "data-remote" attribute', 1, function() { asyncTest('allow empty form "action"', 1, function() { var currentLocation, ajaxLocation; - build_form({ action: '' }); + buildForm({ action: '' }); $('#qunit-fixture').find('form') .bind('ajax:beforeSend', function(e, xhr, settings) { @@ -114,7 +114,7 @@ asyncTest('allow empty form "action"', 1, function() { }); asyncTest('sends CSRF token in custom header', 1, function() { - build_form({ method: 'post' }); + buildForm({ method: 'post' }); $('#qunit-fixture').append(''); submit(function(e, data, status, xhr) { @@ -123,7 +123,7 @@ asyncTest('sends CSRF token in custom header', 1, function() { }); asyncTest('does not send CSRF token in custom header if crossDomain', 1, function() { - build_form({ 'data-cross-domain': 'true' }); + buildForm({ 'data-cross-domain': 'true' }); $('#qunit-fixture').append(''); // Manually set request header to be XHR, since setting crossDomain: true in .ajax() @@ -141,7 +141,7 @@ asyncTest('does not send CSRF token in custom header if crossDomain', 1, functio asyncTest('intelligently guesses crossDomain behavior when target URL is a different domain', 1, function(e, xhr) { // Don't set data-cross-domain here, just set action to be a different domain than localhost - build_form({ action: 'http://www.alfajango.com' }); + buildForm({ action: 'http://www.alfajango.com' }); $('#qunit-fixture').append(''); $('#qunit-fixture').find('form') @@ -157,4 +157,19 @@ asyncTest('intelligently guesses crossDomain behavior when target URL is a diffe setTimeout(function() { start(); }, 13); }); +asyncTest('does not set crossDomain if explicitly set to false on element', 1, function() { + buildForm({ action: 'http://www.alfajango.com', 'data-cross-domain': false }); + $('#qunit-fixture').append(''); + + $('#qunit-fixture').find('form') + .bind('ajax:beforeSend', function(e, xhr, settings) { + equal(settings.crossDomain, false, 'crossDomain should be set to false'); + // prevent request from actually getting sent off-domain + return false; + }) + .trigger('submit'); + + setTimeout(function() { start(); }, 13); +}); + })(); diff --git a/test/public/test/csrf-refresh.js b/test/public/test/csrf-refresh.js new file mode 100644 index 00000000..65642433 --- /dev/null +++ b/test/public/test/csrf-refresh.js @@ -0,0 +1,24 @@ +(function(){ + +module('csrf-refresh', {}); + +asyncTest('refresh all csrf tokens', 1, function() { + var correctToken = "cf50faa3fe97702ca1ae"; + + var form = $('') + var input = $('').attr({ type: 'hidden', name: 'authenticity_token', id: 'authenticity_token', value: 'foo' }) + input.appendTo(form) + + $('#qunit-fixture') + .append('') + .append('') + .append(form); + + $.rails.refreshCSRFTokens(); + currentToken = $('#qunit-fixture #authenticity_token').val(); + + start(); + equal(currentToken, correctToken); +}); + +})(); diff --git a/test/public/test/data-confirm.js b/test/public/test/data-confirm.js index 68784b84..cf95b20b 100644 --- a/test/public/test/data-confirm.js +++ b/test/public/test/data-confirm.js @@ -7,6 +7,13 @@ module('data-confirm', { text: 'my social security number' })); + $('#qunit-fixture').append($(''); + form.append(button); + + App.checkEnabledState(button, 'Submit'); + + form.bind('ajax:success', function(e, data) { + setTimeout(function() { + App.checkEnabledState(button, 'Submit'); + start(); + }, 13); + }); + form.trigger('submit'); + + App.checkDisabledState(button, 'submitting ...'); +}); + +asyncTest('form input[type=submit][data-disable-with] disables', 6, function(){ + var form = $('form:not([data-remote])'), input = form.find('input[type=submit]'); + + App.checkEnabledState(input, 'Submit'); + + // WEEIRDD: attaching this handler makes the test work in IE7 + $(document).bind('iframe:loading', function(e, form) {}); + + $(document).bind('iframe:loaded', function(e, data) { + setTimeout(function() { + App.checkDisabledState(input, 'submitting ...'); + start(); + }, 30); + }); + form.trigger('submit'); + + setTimeout(function() { + App.checkDisabledState(input, 'submitting ...'); + }, 30); +}); + +asyncTest('form[data-remote] input[type=submit][data-disable-with] is replaced in ajax callback', 2, function(){ + var form = $('form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html(); + + form.bind('ajax:success', function(){ + form.html(origFormContents); + + setTimeout(function(){ + var input = form.find('input[type=submit]'); + App.checkEnabledState(input, 'Submit'); + start(); + }, 30); + }).trigger('submit'); +}); + +asyncTest('form[data-remote] input[data-disable-with] is replaced with disabled field in ajax callback', 2, function(){ + var form = $('form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'), + newDisabledInput = input.clone().attr('disabled', 'disabled'); + + form.bind('ajax:success', function(){ + input.replaceWith(newDisabledInput); + + setTimeout(function(){ + App.checkEnabledState(newDisabledInput, 'Submit'); + start(); + }, 30); + }).trigger('submit'); +}); + +asyncTest('form input[type=submit][data-disable-with] using "form" attribute disables', 6, function() { + var form = $('#not_remote'), input = $('input[form=not_remote]'); + App.checkEnabledState(input, 'Form Attr Submit'); + + // WEEIRDD: attaching this handler makes the test work in IE7 + $(document).bind('iframe:loading', function(e, form) {}); + + $(document).bind('iframe:loaded', function(e, data) { + setTimeout(function() { + App.checkDisabledState(input, 'form attr submitting'); + start(); + }, 30); + }); + form.trigger('submit'); + + setTimeout(function() { + App.checkDisabledState(input, 'form attr submitting'); + }, 30); + +}); + +asyncTest('form[data-remote] textarea[data-disable-with] attribute', 3, function() { + var form = $('form[data-remote]'), + textarea = $('').appendTo(form); + + form.bind('ajax:success', function(e, data) { + setTimeout(function() { + equal(data.params.user_bio, 'born, lived, died.'); + start(); + }, 13); + }); + form.trigger('submit'); + + App.checkDisabledState(textarea, 'processing ...'); +}); + +asyncTest('a[data-disable-with] disables', 4, function() { + var link = $('a[data-disable-with]'); + + App.checkEnabledState(link, 'Click me'); + + link.trigger('click'); + App.checkDisabledState(link, 'clicking...'); + start(); +}); + +asyncTest('a[data-remote][data-disable-with] disables and re-enables', 6, function() { + var link = $('a[data-disable-with]').attr('data-remote', true); + + App.checkEnabledState(link, 'Click me'); + + link + .bind('ajax:beforeSend', function() { + App.checkDisabledState(link, 'clicking...'); + }) + .bind('ajax:complete', function() { + setTimeout( function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 15); + }) + .trigger('click'); +}); + +asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:before` event is cancelled', 6, function() { + var link = $('a[data-disable-with]').attr('data-remote', true); + + App.checkEnabledState(link, 'Click me'); + + link + .bind('ajax:before', function() { + App.checkDisabledState(link, 'clicking...'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 30); +}); + +asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { + var link = $('a[data-disable-with]').attr('data-remote', true); + + App.checkEnabledState(link, 'Click me'); + + link + .bind('ajax:beforeSend', function() { + App.checkDisabledState(link, 'clicking...'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 30); +}); + +asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:error` event is triggered', 6, function() { + var link = $('a[data-disable-with]').attr('data-remote', true).attr('href', '/error'); + + App.checkEnabledState(link, 'Click me'); + + link + .bind('ajax:beforeSend', function() { + App.checkDisabledState(link, 'clicking...'); + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 30); +}); + +asyncTest('form[data-remote] input|button|textarea[data-disable-with] does not disable when `ajax:beforeSend` event is cancelled', 8, function() { + var form = $('form[data-remote]'), + input = form.find('input:text'), + button = $('').appendTo(form), + textarea = $('').appendTo(form), + submit = $('').appendTo(form); + + form + .bind('ajax:beforeSend', function() { + return false; + }) + .trigger('submit'); + + App.checkEnabledState(input, 'john'); + App.checkEnabledState(button, 'Submit'); + App.checkEnabledState(textarea, 'born, lived, died.'); + App.checkEnabledState(submit, 'Submit'); + + start(); +}); + +asyncTest('ctrl-clicking on a link does not disables the link', 6, function() { + var link = $('a[data-disable-with]'), e; + e = $.Event('click'); + e.metaKey = true; + + App.checkEnabledState(link, 'Click me'); + + link.trigger(e); + App.checkEnabledState(link, 'Click me'); + + e = $.Event('click'); + e.ctrlKey = true; + + link.trigger(e); + App.checkEnabledState(link, 'Click me'); + start(); +}); + +asyncTest('button[data-remote][data-disable-with] disables and re-enables', 6, function() { + var button = $('button[data-remote][data-disable-with]'); + + App.checkEnabledState(button, 'Click me'); + + button + .bind('ajax:send', function() { + App.checkDisabledState(button, 'clicking...'); + }) + .bind('ajax:complete', function() { + setTimeout( function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 15); + }) + .trigger('click'); +}); + +asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:before` event is cancelled', 6, function() { + var button = $('button[data-remote][data-disable-with]'); + + App.checkEnabledState(button, 'Click me'); + + button + .bind('ajax:before', function() { + App.checkDisabledState(button, 'clicking...'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 30); +}); + +asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { + var button = $('button[data-remote][data-disable-with]'); + + App.checkEnabledState(button, 'Click me'); + + button + .bind('ajax:beforeSend', function() { + App.checkDisabledState(button, 'clicking...'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 30); +}); + +asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:error` event is triggered', 6, function() { + var button = $('a[data-disable-with]').attr('data-remote', true).attr('href', '/error'); + + App.checkEnabledState(button, 'Click me'); + + button + .bind('ajax:send', function() { + App.checkDisabledState(button, 'clicking...'); + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 30); +}); diff --git a/test/public/test/data-disable.js b/test/public/test/data-disable.js index d80a5102..25e9dfc9 100644 --- a/test/public/test/data-disable.js +++ b/test/public/test/data-disable.js @@ -6,7 +6,7 @@ module('data-disable', { method: 'post' })) .find('form') - .append($('')); + .append($('')); $('#qunit-fixture').append($('', { action: '/echo', @@ -14,89 +14,82 @@ module('data-disable', { })) .find('form:last') // WEEIRDD: the form won't submit to an iframe if the button is name="submit" (??!) - .append($('')); + .append($('')); $('#qunit-fixture').append($('', { text: 'Click me', href: '/echo', - 'data-disable-with': 'clicking...' + 'data-disable': 'true' })); + + $('#qunit-fixture').append($(''); +asyncTest('form button with "data-disable" attribute', 6, function() { + var form = $('form[data-remote]'), button = $(''); form.append(button); - checkEnabledState(button, 'Submit'); + App.checkEnabledState(button, 'Submit'); form.bind('ajax:success', function(e, data) { setTimeout(function() { - checkEnabledState(button, 'Submit'); + App.checkEnabledState(button, 'Submit'); start(); }, 13) }) form.trigger('submit'); - checkDisabledState(button, 'submitting ...'); + App.checkDisabledState(button, 'Submit'); }); -asyncTest('form input[type=submit][data-disable-with] disables', 6, function(){ +asyncTest('form input[type=submit][data-disable] disables', 6, function(){ var form = $('form:not([data-remote])'), input = form.find('input[type=submit]'); - checkEnabledState(input, 'Submit'); + App.checkEnabledState(input, 'Submit'); // WEEIRDD: attaching this handler makes the test work in IE7 - form.bind('iframe:loading', function(e, form) {}); + $(document).bind('iframe:loading', function(e, form) {}); - form.bind('iframe:loaded', function(e, data) { + $(document).bind('iframe:loaded', function(e, data) { setTimeout(function() { - checkDisabledState(input, 'submitting ...'); + App.checkDisabledState(input, 'Submit'); start(); }, 30); - }).trigger('submit'); + }); + form.trigger('submit'); setTimeout(function() { - checkDisabledState(input, 'submitting ...'); + App.checkDisabledState(input, 'Submit'); }, 30); }); -asyncTest('form[data-remote] input[type=submit][data-disable-with] is replaced in ajax callback', 2, function(){ +asyncTest('form[data-remote] input[type=submit][data-disable] is replaced in ajax callback', 2, function(){ var form = $('form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html(); form.bind('ajax:success', function(){ @@ -104,13 +97,13 @@ asyncTest('form[data-remote] input[type=submit][data-disable-with] is replaced i setTimeout(function(){ var input = form.find('input[type=submit]'); - checkEnabledState(input, 'Submit'); + App.checkEnabledState(input, 'Submit'); start(); }, 30); }).trigger('submit'); }); -asyncTest('form[data-remote] input[data-disable-with] is replaced with disabled field in ajax callback', 2, function(){ +asyncTest('form[data-remote] input[data-disable] is replaced with disabled field in ajax callback', 2, function(){ var form = $('form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'), newDisabledInput = input.clone().attr('disabled', 'disabled'); @@ -118,15 +111,15 @@ asyncTest('form[data-remote] input[data-disable-with] is replaced with disabled input.replaceWith(newDisabledInput); setTimeout(function(){ - checkEnabledState(newDisabledInput, 'Submit'); + App.checkEnabledState(newDisabledInput, 'Submit'); start(); }, 30); }).trigger('submit'); }); -asyncTest('form[data-remote] textarea[data-disable-with] attribute', 3, function() { +asyncTest('form[data-remote] textarea[data-disable] attribute', 3, function() { var form = $('form[data-remote]'), - textarea = $('').appendTo(form); + textarea = $('').appendTo(form); form.bind('ajax:success', function(e, data) { setTimeout(function() { @@ -136,77 +129,96 @@ asyncTest('form[data-remote] textarea[data-disable-with] attribute', 3, function }) form.trigger('submit'); - checkDisabledState(textarea, 'processing ...'); + App.checkDisabledState(textarea, 'born, lived, died.'); }); -asyncTest('a[data-disable-with] disables', 4, function() { - var link = $('a[data-disable-with]'); +asyncTest('a[data-disable] disables', 4, function() { + var link = $('a[data-disable]'); - checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me'); link.trigger('click'); - checkDisabledState(link, 'clicking...'); + App.checkDisabledState(link, 'Click me'); start(); }); -asyncTest('a[data-remote][data-disable-with] disables and re-enables', 6, function() { - var link = $('a[data-disable-with]').attr('data-remote', true); +asyncTest('a[data-remote][data-disable] disables and re-enables', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true); - checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me'); link - .bind('ajax:beforeSend', function() { - checkDisabledState(link, 'clicking...'); + .bind('ajax:send', function() { + App.checkDisabledState(link, 'Click me'); }) - .live('ajax:complete', function() { - checkEnabledState(link, 'Click me'); - start(); + .bind('ajax:complete', function() { + setTimeout( function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 15); }) .trigger('click'); }); -asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:before` event is cancelled', 6, function() { - var link = $('a[data-disable-with]').attr('data-remote', true); +asyncTest('a[data-remote][data-disable] re-enables when `ajax:before` event is cancelled', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true); - checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me'); link .bind('ajax:before', function() { - checkDisabledState(link, 'clicking...'); + App.checkDisabledState(link, 'Click me'); return false; }) .trigger('click'); setTimeout(function() { - checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me'); start(); }, 30); }); -asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { - var link = $('a[data-disable-with]').attr('data-remote', true); +asyncTest('a[data-remote][data-disable] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true); - checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me'); link .bind('ajax:beforeSend', function() { - checkDisabledState(link, 'clicking...'); + App.checkDisabledState(link, 'Click me'); return false; }) .trigger('click'); setTimeout(function() { - checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me'); start(); }, 30); }); -asyncTest('form[data-remote] input|button|textarea[data-disable-with] does not disable when `ajax:beforeSend` event is cancelled', 8, function() { +asyncTest('a[data-remote][data-disable] re-enables when `ajax:error` event is triggered', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true).attr('href', '/error'); + + App.checkEnabledState(link, 'Click me'); + + link + .bind('ajax:send', function() { + App.checkDisabledState(link, 'Click me'); + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 30); +}); + +asyncTest('form[data-remote] input|button|textarea[data-disable] does not disable when `ajax:beforeSend` event is cancelled', 8, function() { var form = $('form[data-remote]'), input = form.find('input:text'), - button = $('').appendTo(form), - textarea = $('').appendTo(form), - submit = $('').appendTo(form); + button = $('').appendTo(form), + textarea = $('').appendTo(form), + submit = $('').appendTo(form); form .bind('ajax:beforeSend', function() { @@ -214,11 +226,99 @@ asyncTest('form[data-remote] input|button|textarea[data-disable-with] does not d }) .trigger('submit'); - checkEnabledState(input, 'john'); - checkEnabledState(button, 'Submit'); - checkEnabledState(textarea, 'born, lived, died.'); - checkEnabledState(submit, 'Submit'); + App.checkEnabledState(input, 'john'); + App.checkEnabledState(button, 'Submit'); + App.checkEnabledState(textarea, 'born, lived, died.'); + App.checkEnabledState(submit, 'Submit'); start(); +}); + +asyncTest('ctrl-clicking on a link does not disables the link', 6, function() { + var link = $('a[data-disable]'), e; + e = $.Event('click'); + e.metaKey = true; + + App.checkEnabledState(link, 'Click me'); + + link.trigger(e); + App.checkEnabledState(link, 'Click me'); + + e = $.Event('click'); + e.ctrlKey = true; + link.trigger(e); + App.checkEnabledState(link, 'Click me'); + start(); +}); + +asyncTest('button[data-remote][data-disable] disables and re-enables', 6, function() { + var button = $('button[data-remote][data-disable]'); + + App.checkEnabledState(button, 'Click me'); + + button + .bind('ajax:send', function() { + App.checkDisabledState(button, 'Click me'); + }) + .bind('ajax:complete', function() { + setTimeout( function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 15); + }) + .trigger('click'); +}); + +asyncTest('button[data-remote][data-disable] re-enables when `ajax:before` event is cancelled', 6, function() { + var button = $('button[data-remote][data-disable]'); + + App.checkEnabledState(button, 'Click me'); + + button + .bind('ajax:before', function() { + App.checkDisabledState(button, 'Click me'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 30); +}); + +asyncTest('button[data-remote][data-disable] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { + var button = $('button[data-remote][data-disable]'); + + App.checkEnabledState(button, 'Click me'); + + button + .bind('ajax:beforeSend', function() { + App.checkDisabledState(button, 'Click me'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 30); +}); + +asyncTest('button[data-remote][data-disable] re-enables when `ajax:error` event is triggered', 6, function() { + var button = $('a[data-disable]').attr('data-remote', true).attr('href', '/error'); + + App.checkEnabledState(button, 'Click me'); + + button + .bind('ajax:send', function() { + App.checkDisabledState(button, 'Click me'); + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 30); }); diff --git a/test/public/test/data-method.js b/test/public/test/data-method.js index ef50500f..c4426624 100644 --- a/test/public/test/data-method.js +++ b/test/public/test/data-method.js @@ -5,16 +5,20 @@ module('data-method', { $('#qunit-fixture').append($('', { href: '/echo', 'data-method': 'delete', text: 'destroy!' })); + }, + teardown: function() { + $(document).unbind('iframe:loaded'); } }); function submit(fn, options) { + $(document).bind('iframe:loaded', function(e, data) { + fn(data); + start(); + }); + $('#qunit-fixture').find('a') - .bind('iframe:loaded', function(e, data) { - fn(data); - start(); - }) - .trigger('click'); + .trigger('click'); } asyncTest('link with "data-method" set to "delete"', 3, function() { @@ -29,7 +33,7 @@ asyncTest('link with "data-method" and CSRF', 1, function() { $('#qunit-fixture') .append('') .append(''); - + submit(function(data) { equal(data.params.authenticity_token, 'cf50faa3fe97702ca1ae'); }); diff --git a/test/public/test/data-remote.js b/test/public/test/data-remote.js index 8242b71e..f8c6b2a8 100644 --- a/test/public/test/data-remote.js +++ b/test/public/test/data-remote.js @@ -7,6 +7,12 @@ module('data-remote', { 'data-params': 'data1=value1&data2=value2', text: 'my address' })) + .append($('