diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000..9bfd6603 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,71 @@ +{ + "maxerr" : 50, // {int} Maximum error before stopping + + // Enforcing + "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) + "camelcase" : true, // true: Identifiers must be in camelCase + "curly" : false, // true: Require {} for every new block or scope + "eqeqeq" : true, // true: Require triple equals (===) for comparison + "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() + "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. + "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` + "indent" : 2, // {int} Number of spaces to use for indentation + "latedef" : false, // true: Require variables/functions to be defined before being used + "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` + "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` + "noempty" : true, // true: Prohibit use of empty blocks + "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. + "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) + "plusplus" : false, // true: Prohibit use of `++` & `--` + "quotmark" : "single", // Quotation mark consistency: + // false : do nothing (default) + // true : ensure whatever is used is consistent + // "single" : require single quotes + // "double" : require double quotes + "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) + "unused" : true, // Unused variables: + // true : all variables, last function parameter + // "vars" : all variables only + // "strict" : all variables, all function parameters + "strict" : true, // true: Requires all functions run in ES5 Strict Mode + "maxparams" : false, // {int} Max number of formal params allowed per function + "maxdepth" : false, // {int} Max depth of nested blocks (within functions) + "maxstatements" : false, // {int} Max number statements per function + "maxcomplexity" : false, // {int} Max cyclomatic complexity per function + "maxlen" : false, // {int} Max number of characters per line + + // Relaxing + "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) + "boss" : false, // true: Tolerate assignments where comparisons would be expected + "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. + "eqnull" : false, // true: Tolerate use of `== null` + "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) + "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) + "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) + // (ex: `for each`, multiple try/catch, function expression…) + "evil" : false, // true: Tolerate use of `eval` and `new Function()` + "expr" : false, // true: Tolerate `ExpressionStatement` as Programs + "funcscope" : false, // true: Tolerate defining variables inside control statements + "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') + "iterator" : false, // true: Tolerate using the `__iterator__` property + "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block + "laxbreak" : false, // true: Tolerate possibly unsafe line breakings + "laxcomma" : false, // true: Tolerate comma-first style coding + "loopfunc" : false, // true: Tolerate functions being defined in loops + "multistr" : false, // true: Tolerate multi-line strings + "noyield" : false, // true: Tolerate generator functions with no yield statement in them. + "notypeof" : false, // true: Tolerate invalid typeof operator values + "proto" : false, // true: Tolerate using the `__proto__` property + "scripturl" : false, // true: Tolerate script-targeted URLs + "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` + "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation + "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` + "validthis" : false, // true: Tolerate using this in a non-constructor function + + // Environments + "browser" : true, // Web Browser (window, document, etc) + "jquery" : true, + "devel" : true, // Development/debugging (alert, confirm, etc) + + "globals" : {} +} diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..3af83847 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +script/ +test/ +Rakefile +Gemfile* diff --git a/.travis.yml b/.travis.yml index 8b7d9259..7e6cb080 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,12 @@ language: ruby +sudo: false +cache: + - bundler + - directories: + - $HOME/.npm script: ./script/cibuild +before_install: + - "npm install jshint -g" env: - JQUERY_VERSION: 1.8.0 - JQUERY_VERSION: 1.8.1 @@ -11,5 +18,12 @@ env: - JQUERY_VERSION: 1.10.1 - JQUERY_VERSION: 1.10.2 - JQUERY_VERSION: 1.11.0 + - JQUERY_VERSION: 1.11.1 + - JQUERY_VERSION: 1.11.2 + - JQUERY_VERSION: 1.12.0 - JQUERY_VERSION: 2.0.0 - JQUERY_VERSION: 2.1.0 + - JQUERY_VERSION: 2.1.1 + - JQUERY_VERSION: 2.1.2 + - JQUERY_VERSION: 2.1.3 + - JQUERY_VERSION: 2.2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 3005c6c4..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,19 +0,0 @@ -## v1.0.2 - -* Re-enables buttons and links after going back through the `pageshow` event. - - *Gabriel Sobrinho* - -## v1.0.1 - -* `confirm` is no longer called twice for `button` elements inside a `form`. - - *Lucas Mazza* - -* `button` or submit inputs using the `form` attribute are properly bound. - - *Marnen Laibow-Koser* - -## v1.0.0 - -* First tagged release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6827438d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,109 @@ +Contributing to jquery-ujs +===================== + +[](https://travis-ci.org/rails/jquery-ujs) + +jquery-ujs is work of [many contributors](https://github.com/rails/jquery-ujs/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/rails/jquery-ujs/pulls), [propose features and discuss issues](https://github.com/rails/jquery-ujs/issues). + +#### Fork the Project + +Fork the [project on Github](https://github.com/rails/jquery-ujs) and check out your copy. + +``` +git clone https://github.com/contributor/jquery-ujs.git +cd jquery-ujs +git remote add upstream https://github.com/rails/jquery-ujs.git +``` + +#### Create a Topic Branch + +Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. + +``` +git checkout master +git pull upstream master +git checkout -b my-feature-branch +``` + +#### Bundle Install and Test + +Ensure that you can build the project and run tests. Run `rake test:server` first, and then run the web tests by visiting [[http://localhost:4567]] in your browser. Click the version links at the top right of the page to run the test suite with the different supported versions of jQuery. + +``` +bundle install +rake test:server +``` + +#### Write Tests + +Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [test](test). + +Here are some additional notes to keep in mind when developing your patch for jquery-ujs. + +* The tests can be found in: +``` + |~test + |~public + |~test +``` +* Some tests ensure consistent behavior across the major browsers, meaning it is possible for tests to pass in Firefox, but fail in Internet Explorer. If possible, it helps if you can run the test suite in multiple browsers before submitting patches. + +We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. + +#### Write Code + +Implement your feature or bug fix. + +Make sure that `bundle exec rake test` completes without errors. + +#### Write Documentation + +Document any external behavior in the [README](README.md). + +#### Commit Changes + +Make sure git knows your name and email address: + +``` +git config --global user.name "Your Name" +git config --global user.email "contributor@example.com" +``` + +Writing good commit logs is important. A commit log should describe what changed and why. + +``` +git add ... +git commit +``` + +#### Push + +``` +git push origin my-feature-branch +``` + +#### Make a Pull Request + +Go to https://github.com/contributor/jquery-ujs and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. + +#### Rebase + +If you've been working on a change for a while, rebase with upstream/master. + +``` +git fetch upstream +git rebase upstream/master +git push origin my-feature-branch -f +``` + +#### Check on Your Pull Request + +Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. + +#### Be Patient + +It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! + +#### Thank You + +Please do know that we really appreciate and value your time and work. We love you, really. diff --git a/Gemfile.lock b/Gemfile.lock index 966496e1..8ced3297 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,21 +2,21 @@ GEM remote: https://rubygems.org/ specs: daemons (1.1.9) - eventmachine (1.0.3) - rack (1.5.2) + eventmachine (1.0.4) + rack (1.6.0) rack-protection (1.5.3) rack - rake (10.3.2) + rake (10.4.2) shotgun (0.9) rack (>= 1.0) sinatra (1.4.5) rack (~> 1.4) rack-protection (~> 1.4) tilt (~> 1.3, >= 1.3.4) - thin (1.6.2) - daemons (>= 1.0.9) - eventmachine (>= 1.0.0) - rack (>= 1.0.0) + thin (1.6.3) + daemons (~> 1.0, >= 1.0.9) + eventmachine (~> 1.0) + rack (~> 1.0) tilt (1.4.1) PLATFORMS diff --git a/MIT-LICENSE.txt b/MIT-LICENSE similarity index 94% rename from MIT-LICENSE.txt rename to MIT-LICENSE index ed37a233..9c7e3c0b 100644 --- a/MIT-LICENSE.txt +++ b/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2007-2010 Contributors at http://github.com/rails/jquery-ujs/contributors +Copyright (c) 2007-2016 Contributors at http://github.com/rails/jquery-ujs/contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 29ea432d..2e429bd1 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,26 @@ Require both `jquery` and `jquery_ujs` into your application.js manifest. //= require jquery_ujs ``` +Installation using npm. +------------ + +Run `npm install --save jquery-ujs` to install the jquery-ujs package. + +Installation using Rails and Webpacker +------------ + +If you're using [webpacker](https://github.com/rails/webpacker) (introduced in [Rails 5.1](http://edgeguides.rubyonrails.org/5_1_release_notes.html#optional-webpack-support)) to manage JavaScript assets, then you can add the jquery-ujs npm package to your project using the [yarn](https://yarnpkg.com/en/) CLI. + +``` +$ yarn add jquery-ujs +``` + +Then, from any of your included files (e.g. `app/javascript/packs/application.js`, or from a JavaScript file imported by such a pack), you need only import the package for jquery-ujs to be initialized: + +```js +import {} from 'jquery-ujs' +``` + Installation using Bower ------------ @@ -60,8 +80,17 @@ Require both `jquery` and `jquery-ujs` into your application.js manifest. How to run tests ------------ -Follow [this wiki](https://github.com/rails/jquery-ujs/wiki/Running-Tests-and-Contributing) to run tests . +Follow [this wiki](https://github.com/rails/jquery-ujs/wiki/Running-Tests-and-Contributing) to run tests. + +## Contributing to jquery-ujs + +jquery-ujs is work of many contributors. You're encouraged to submit pull requests, propose +features and discuss issues. + +See [CONTRIBUTING](CONTRIBUTING.md). +## License +jquery-ujs is released under the [MIT License](MIT-LICENSE). [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 diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..59316f77 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,23 @@ +## Releasing jquery-ujs + +### Releasing to npm + +Make sure npm's configuration `sign-git-tag` is set to true. + +``` +npm config set sign-git-tag true +``` + +Release it to npm using the [npm version command](https://docs.npmjs.com/cli/version). Like: + +``` +npm version patch +``` + +This will: + +* Bump a patch version +* Commit the change +* Generate the tag +* Push the commit and the tag to the repository +* Publish the package in https://www.npmjs.com diff --git a/bower.json b/bower.json index cfee5da0..14f93cf3 100644 --- a/bower.json +++ b/bower.json @@ -5,7 +5,6 @@ "description": "Ruby on Rails unobtrusive scripting adapter for jQuery", "main": "src/rails.js", "license": "MIT", - "version": "1.0.2", "dependencies": { "jquery": ">1.8.*" }, diff --git a/package.json b/package.json new file mode 100644 index 00000000..9299d5f5 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "jquery-ujs", + "version": "1.2.3", + "description": "Unobtrusive scripting adapter for jQuery", + "main": "src/rails.js", + "scripts": { + "test": "echo \"See the wiki: https://github.com/rails/jquery-ujs/wiki/Running-Tests-and-Contributing\" && exit 1", + "postversion": "git push && git push --tags && npm publish" + }, + "repository": { + "type": "git", + "url": "https://github.com/rails/jquery-ujs.git" + }, + "author": [ + "Stephen St. Martin", + "Steve Schwartz" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/rails/jquery-ujs/issues" + }, + "homepage": "https://github.com/rails/jquery-ujs#readme", + "peerDependencies": { + "jquery": ">=1.8.0" + } +} diff --git a/script/cibuild b/script/cibuild index cc661ece..45c97099 100755 --- a/script/cibuild +++ b/script/cibuild @@ -8,7 +8,7 @@ start_server() { } run_tests() { - phantomjs script/runner.js http://localhost:$port/ + jshint src/*.js && phantomjs script/runner.js http://localhost:$port/ } server_started() { diff --git a/src/rails.js b/src/rails.js index c0f45238..0e27110d 100644 --- a/src/rails.js +++ b/src/rails.js @@ -1,4 +1,4 @@ -(function($, undefined) { +/* jshint node: true */ /** * Unobtrusive scripting adapter for jQuery @@ -10,6 +10,11 @@ * */ +(function() { + 'use strict'; + + var jqueryUjsInit = function($, undefined) { + // 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 ) { @@ -22,19 +27,19 @@ $.rails = rails = { // Link elements bound by jquery-ujs - linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]', + linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]', // Button elements bound by jquery-ujs - buttonClickSelector: 'button[data-remote]:not(form button), button[data-confirm]:not(form button)', + buttonClickSelector: 'button[data-remote]:not([form]):not(form button), button[data-confirm]:not([form]):not(form button)', // Select elements bound by jquery-ujs inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]', // Form elements bound by jquery-ujs - formSubmitSelector: 'form', + formSubmitSelector: 'form:not([data-turbo=true])', // Form input elements bound by jquery-ujs - formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])', + formInputClickSelector: 'form:not([data-turbo=true]) input[type=submit], form:not([data-turbo=true]) input[type=image], form:not([data-turbo=true]) button[type=submit], form:not([data-turbo=true]) button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])', // Form input elements disabled during form submission disableSelector: 'input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled', @@ -43,10 +48,10 @@ 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])', + requiredInputSelector: 'input[name][required]:not([disabled]), textarea[name][required]:not([disabled])', // Form file input elements - fileInputSelector: 'input[type=file]', + fileInputSelector: 'input[name][type=file]:not([disabled])', // Link onClick disable selector with possible reenable after remote submission linkDisableSelector: 'a[data-disable-with], a[data-disable]', @@ -54,17 +59,25 @@ // Button onClick disable selector with possible reenable after remote submission buttonDisableSelector: 'button[data-remote][data-disable-with], button[data-remote][data-disable]', + // Up-to-date Cross-Site Request Forgery token + csrfToken: function() { + return $('meta[name=csrf-token]').attr('content'); + }, + + // URL param that must contain the CSRF token + csrfParam: function() { + return $('meta[name=csrf-param]').attr('content'); + }, + // Make sure that every Ajax request sends the CSRF token CSRFProtection: function(xhr) { - var token = $('meta[name="csrf-token"]').attr('content'); + var token = rails.csrfToken(); if (token) xhr.setRequestHeader('X-CSRF-Token', token); }, - // making sure that all forms have actual up-to-date token(cached forms contain old one) + // Make sure that all forms have actual up-to-date tokens (cached forms contain old ones) refreshCSRFTokens: function(){ - var csrfToken = $('meta[name=csrf-token]').attr('content'); - var csrfParam = $('meta[name=csrf-param]').attr('content'); - $('form input[name="' + csrfParam + '"]').val(csrfToken); + $('form input[name="' + rails.csrfParam() + '"]').val(rails.csrfToken()); }, // Triggers an event on an element and returns false if the event result is false @@ -86,39 +99,44 @@ // Default way to get an element's href. May be overridden at $.rails.href. href: function(element) { - return element.attr('href'); + return element[0].href; + }, + + // Checks "data-remote" if true to handle the request through a XHR request. + isRemote: function(element) { + return element.data('remote') !== undefined && element.data('remote') !== false; }, // Submits "remote" forms and links with ajax handleRemote: function(element) { - var method, url, data, elCrossDomain, crossDomain, withCredentials, dataType, options; + var method, url, data, 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'); - url = element.attr('action'); - data = element.serializeArray(); + method = element.data('ujs:submit-button-formmethod') || element.attr('method'); + url = element.data('ujs:submit-button-formaction') || element.attr('action'); + data = $(element[0]).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); } + element.data('ujs:submit-button-formmethod', null); + element.data('ujs:submit-button-formaction', 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'); + 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'); + if (element.data('params')) data = data + '&' + element.data('params'); } else { method = element.data('method'); url = rails.href(element); @@ -147,7 +165,7 @@ error: function(xhr, status, error) { element.trigger('ajax:error', [xhr, status, error]); }, - crossDomain: crossDomain + crossDomain: rails.isCrossDomain(url) }; // There is no withCredentials for IE6-8 when @@ -167,18 +185,43 @@ } }, + // Determines if the request is a cross domain request. + isCrossDomain: function(url) { + var originAnchor = document.createElement('a'); + originAnchor.href = location.href; + var urlAnchor = document.createElement('a'); + + try { + urlAnchor.href = url; + // This is a workaround to a IE bug. + urlAnchor.href = urlAnchor.href; + + // If URL protocol is false or is a string containing a single colon + // *and* host are false, assume it is not a cross-domain request + // (should only be the case for IE7 and IE compatibility mode). + // Otherwise, evaluate protocol and host of the URL against the origin + // protocol and host. + return !(((!urlAnchor.protocol || urlAnchor.protocol === ':') && !urlAnchor.host) || + (originAnchor.protocol + '//' + originAnchor.host === + urlAnchor.protocol + '//' + urlAnchor.host)); + } catch (e) { + // If there is an error parsing the URL, assume it is crossDomain. + return true; + } + }, + // Handles "data-method" on links such as: // Delete handleMethod: function(link) { var href = rails.href(link), method = link.data('method'), target = link.attr('target'), - csrfToken = $('meta[name=csrf-token]').attr('content'), - csrfParam = $('meta[name=csrf-param]').attr('content'), + csrfToken = rails.csrfToken(), + csrfParam = rails.csrfParam(), form = $('
'), metadataInput = ''; - if (csrfParam !== undefined && csrfToken !== undefined) { + if (csrfParam !== undefined && csrfToken !== undefined && !rails.isCrossDomain(href)) { metadataInput += ''; } @@ -212,12 +255,13 @@ method = element.is('button') ? 'html' : 'val'; replacement = element.data('disable-with'); - element.data('ujs:enable-with', element[method]()); if (replacement !== undefined) { + element.data('ujs:enable-with', element[method]()); element[method](replacement); } element.prop('disabled', true); + element.data('ujs:disabled', true); }, /* Re-enables disabled form elements: @@ -232,8 +276,12 @@ enableFormElement: function(element) { var method = element.is('button') ? 'html' : 'val'; - if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with')); + if (element.data('ujs:enable-with') !== undefined) { + element[method](element.data('ujs:enable-with')); + element.removeData('ujs:enable-with'); // clean up cache + } element.prop('disabled', false); + element.removeData('ujs:disabled'); }, /* For 'data-confirm' attribute: @@ -252,7 +300,11 @@ if (!message) { return true; } if (rails.fire(element, 'confirm')) { - answer = rails.confirm(message); + try { + answer = rails.confirm(message); + } catch (e) { + (console.error || console.log).call(console, e.stack || e); + } callback = rails.fire(element, 'confirm:complete', [answer]); } return answer && callback; @@ -260,25 +312,45 @@ // Helper function which checks for blank inputs in a form that match the specified CSS selector blankInputs: function(form, specifiedSelector, nonBlank) { - var inputs = $(), input, valueToCheck, - selector = specifiedSelector || 'input,textarea', - allInputs = form.find(selector); - - allInputs.each(function() { + var foundInputs = $(), + input, + valueToCheck, + radiosForNameWithNoneSelected, + radioName, + selector = specifiedSelector || 'input,textarea', + requiredInputs = form.find(selector), + checkedRadioButtonNames = {}; + + requiredInputs.each(function() { input = $(this); - 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) { + if (input.is('input[type=radio]')) { - // 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 - } + // Don't count unchecked required radio as blank if other radio with same name is checked, + // regardless of whether same-name radio input has required attribute or not. The spec + // states https://www.w3.org/TR/html5/forms.html#the-required-attribute + radioName = input.attr('name'); + + // Skip if we've already seen the radio with this name. + if (!checkedRadioButtonNames[radioName]) { + + // If none checked + if (form.find('input[type=radio]:checked[name="' + radioName + '"]').length === 0) { + radiosForNameWithNoneSelected = form.find( + 'input[type=radio][name="' + radioName + '"]'); + foundInputs = foundInputs.add(radiosForNameWithNoneSelected); + } - inputs = inputs.add(input); + // We only need to check each name once. + checkedRadioButtonNames[radioName] = radioName; + } + } else { + valueToCheck = input.is('input[type=checkbox],input[type=radio]') ? input.is(':checked') : !!input.val(); + if (valueToCheck === nonBlank) { + foundInputs = foundInputs.add(input); + } } }); - return inputs.length ? inputs : false; + return foundInputs.length ? foundInputs : false; }, // Helper function which checks for non-blank inputs in a form that match the specified CSS selector @@ -293,28 +365,30 @@ return false; }, - // replace element's html with the 'data-disable-with' after storing original html + // 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 if (replacement !== undefined) { + element.data('ujs:enable-with', element.html()); // store enabled state element.html(replacement); } - element.bind('click.railsDisable', function(e) { // prevent further clicking + element.on('click.railsDisable', function(e) { // prevent further clicking return rails.stopEverything(e); }); + element.data('ujs:disabled', true); }, - // restore element to its original state which was disabled by 'disableElement' above + // 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 element.removeData('ujs:enable-with'); // clean up cache } - element.unbind('click.railsDisable'); // enable element + element.off('click.railsDisable'); // enable element + element.removeData('ujs:disabled'); } }; @@ -327,11 +401,11 @@ // // See https://github.com/rails/jquery-ujs/issues/357 // See https://developer.mozilla.org/en-US/docs/Using_Firefox_1.5_caching - $(window).on("pageshow.rails", function () { + $(window).on('pageshow.rails', function () { $($.rails.enableSelector).each(function () { var element = $(this); - if (element.data("ujs:enable-with")) { + if (element.data('ujs:disabled')) { $.rails.enableFormElement(element); } }); @@ -339,35 +413,35 @@ $($.rails.linkDisableSelector).each(function () { var element = $(this); - if (element.data("ujs:enable-with")) { + if (element.data('ujs:disabled')) { $.rails.enableElement(element); } }); }); - $document.delegate(rails.linkDisableSelector, 'ajax:complete', function() { + $document.on('ajax:complete', rails.linkDisableSelector, function() { rails.enableElement($(this)); }); - $document.delegate(rails.buttonDisableSelector, 'ajax:complete', function() { + $document.on('ajax:complete', rails.buttonDisableSelector, function() { rails.enableFormElement($(this)); }); - $document.delegate(rails.linkClickSelector, 'click.rails', function(e) { + $document.on('click.rails', rails.linkClickSelector, 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 (!metaClick && link.is(rails.linkDisableSelector)) rails.disableElement(link); - if (link.data('remote') !== undefined) { + if (rails.isRemote(link)) { if (metaClick && (!method || method === 'GET') && !data) { return true; } var handleRemote = rails.handleRemote(link); - // response from rails.handleRemote() will either be false or a deferred object promise. + // 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); } ); + handleRemote.fail( function() { rails.enableElement(link); } ); } return false; @@ -377,56 +451,62 @@ } }); - $document.delegate(rails.buttonClickSelector, 'click.rails', function(e) { + $document.on('click.rails', rails.buttonClickSelector, function(e) { var button = $(this); - if (!rails.allowAction(button)) return rails.stopEverything(e); + if (!rails.allowAction(button) || !rails.isRemote(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. + // 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); } ); + handleRemote.fail( function() { rails.enableFormElement(button); } ); } return false; }); - $document.delegate(rails.inputChangeSelector, 'change.rails', function(e) { + $document.on('change.rails', rails.inputChangeSelector, function(e) { var link = $(this); - if (!rails.allowAction(link)) return rails.stopEverything(e); + if (!rails.allowAction(link) || !rails.isRemote(link)) return rails.stopEverything(e); rails.handleRemote(link); return false; }); - $document.delegate(rails.formSubmitSelector, 'submit.rails', function(e) { + $document.on('submit.rails', rails.formSubmitSelector, function(e) { var form = $(this), - remote = form.data('remote') !== undefined, + remote = rails.isRemote(form), blankRequiredInputs, nonBlankFileInputs; if (!rails.allowAction(form)) return rails.stopEverything(e); - // 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); + // Skip other logic when required values are missing or file upload is present + if (form.attr('novalidate') === undefined) { + if (form.data('ujs:formnovalidate-button') === undefined) { + blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector, false); + if (blankRequiredInputs && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) { + return rails.stopEverything(e); + } + } else { + // Clear the formnovalidate in case the next button click is not on a formnovalidate button + // Not strictly necessary to do here, since it is also reset on each button click, but just to be certain + form.data('ujs:formnovalidate-button', undefined); } } if (remote) { nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); if (nonBlankFileInputs) { - // slight timeout so that the submit button gets properly serialized + // 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]); - // re-enable form elements if event bindings return false (canceling normal form submission) + // Re-enable form elements if event bindings return false (canceling normal form submission) if (!aborted) { setTimeout(function(){ rails.enableFormElements(form); }, 13); } return aborted; @@ -436,29 +516,38 @@ return false; } else { - // slight timeout so that the submit button gets properly serialized + // Slight timeout so that the submit button gets properly serialized setTimeout(function(){ rails.disableFormElements(form); }, 13); } }); - $document.delegate(rails.formInputClickSelector, 'click.rails', function(event) { + $document.on('click.rails', rails.formInputClickSelector, function(event) { var button = $(this); if (!rails.allowAction(button)) return rails.stopEverything(event); - // register the pressed submit button + // 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); + var form = button.closest('form'); + if (form.length === 0) { + form = $('#' + button.attr('form')); + } + form.data('ujs:submit-button', data); + + // Save attributes from button + form.data('ujs:formnovalidate-button', button.attr('formnovalidate')); + form.data('ujs:submit-button-formaction', button.attr('formaction')); + form.data('ujs:submit-button-formmethod', button.attr('formmethod')); }); - $document.delegate(rails.formSubmitSelector, 'ajax:send.rails', function(event) { - if (this == event.target) rails.disableFormElements($(this)); + $document.on('ajax:send.rails', rails.formSubmitSelector, 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.on('ajax:complete.rails', rails.formSubmitSelector, function(event) { + if (this === event.target) rails.enableFormElements($(this)); }); $(function(){ @@ -466,4 +555,11 @@ }); } -})( jQuery ); + }; + + if (window.jQuery) { + jqueryUjsInit(jQuery); + } else if (typeof exports === 'object' && typeof module === 'object') { + module.exports = jqueryUjsInit; + } +})(); diff --git a/test/public/test/call-remote-callbacks.js b/test/public/test/call-remote-callbacks.js index c1791f6b..7dd1d6ac 100644 --- a/test/public/test/call-remote-callbacks.js +++ b/test/public/test/call-remote-callbacks.js @@ -7,32 +7,43 @@ module('call-remote-callbacks', { })); }, teardown: function() { - $(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'); + $(document).off('ajax:beforeSend', 'form[data-remote]'); + $(document).off('ajax:before', 'form[data-remote]'); + $(document).off('ajax:send', 'form[data-remote]'); + $(document).off('ajax:complete', 'form[data-remote]'); + $(document).off('ajax:success', 'form[data-remote]'); + $(document).off('ajaxStop'); + $(document).off('iframe:loading'); } }); +function start_after_submit(form) { + form.on('ajax:complete', function() { + ok(true, 'ajax:complete'); + start(); + }); +} + function submit(fn) { - var form = $('form') - .bind('ajax:complete', function(){ - ok(true, 'ajax:complete'); - start(); - }); + var form = $('form'); + start_after_submit(form); if (fn) fn(form); form.trigger('submit'); } +function submit_with_button(submit_button) { + var form = $('form'); + start_after_submit(form); + + submit_button.trigger('click'); +} + asyncTest('modifying form fields with "ajax:before" sends modified data in request', 4, function(){ $('form[data-remote]') .append($('')) .append($('')) - .bind('ajax:before', function() { + .on('ajax:before', function() { var form = $(this); form .append($('',{name: 'other_user_name',value: 'jonathan'})) @@ -42,7 +53,7 @@ asyncTest('modifying form fields with "ajax:before" sends modified data in reque }); submit(function(form) { - form.bind('ajax:success', function(e, data, status, xhr) { + form.on('ajax:success', function(e, data, status, xhr) { equal(data.params.user_name, 'steve', 'modified field value should have been submitted'); equal(data.params.other_user_name, 'jonathan', 'added field value should have been submitted'); equal(data.params.removed_user_name, undefined, 'removed field value should be undefined'); @@ -52,41 +63,27 @@ 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() { + .on('ajax:before', function() { var form = $(this); form.data('type','xml'); }); submit(function(form) { - form.bind('ajax:beforeSend', function(e, xhr, settings) { + form.on('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() { + .on('ajax:before', function() { var form = $(this); form.data('with-credentials',true); }); submit(function(form) { - form.bind('ajax:beforeSend', function(e, xhr, settings) { + form.on('ajax:beforeSend', function(e, xhr, settings) { equal(settings.xhrFields && settings.xhrFields.withCredentials, true, 'setting modified in ajax:before should have forced withCredentials request'); }); }); @@ -94,37 +91,37 @@ asyncTest('setting data("with-credentials",true) with "ajax:before" uses new set asyncTest('stopping the "ajax:beforeSend" event aborts the request', 1, function() { submit(function(form) { - form.bind('ajax:beforeSend', function() { + form.on('ajax:beforeSend', function() { ok(true, 'aborting request in ajax:beforeSend'); return false; }); - form.unbind('ajax:send').bind('ajax:send', function() { + form.off('ajax:send').on('ajax:send', function() { ok(false, 'ajax:send should not run'); }); - form.unbind('ajax:complete').bind('ajax:complete', function() { + form.off('ajax:complete').on('ajax:complete', function() { ok(false, 'ajax:complete should not run'); }); - form.bind('ajax:error', function(e, xhr, status, error) { + form.on('ajax:error', function(e, xhr, status, error) { ok(false, 'ajax:error should not run'); }); - $(document).bind('ajaxStop', function() { + $(document).on('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() { + $(document).on('iframe:loading', function() { ok(false, 'form should not get submitted'); }); var form = $('form[data-remote]') .append($('')) .append($('')) - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { ok(false, 'ajax:beforeSend should not run'); }) - .bind('ajax:aborted:required', function(e,data){ + .on('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'); ok(data.last().is('textarea[name="user_bio"]'), 'ajax:aborted:required adds blank required textarea to data'); @@ -134,7 +131,7 @@ asyncTest('blank required form input field should abort request and trigger "aja setTimeout(function() { form.find('input[required],textarea[required]').val('Tyler'); - form.unbind('ajax:beforeSend'); + form.off('ajax:beforeSend'); submit(); }, 13); }); @@ -143,7 +140,7 @@ asyncTest('blank required form input for non-remote form should abort normal sub var form = $('form[data-remote]') .append($('')) .removeAttr('data-remote') - .bind('ujs:everythingStopped', function() { + .on('ujs:everythingStopped', function() { ok(true, 'ujs:everythingStopped should run'); }) .trigger('submit'); @@ -156,10 +153,10 @@ asyncTest('blank required form input for non-remote form should abort normal sub asyncTest('form should be submitted with blank required fields if handler is bound to "ajax:aborted:required" event that returns false', 1, function(){ var form = $('form[data-remote]') .append($('')) - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { ok(true, 'ajax:beforeSend should run'); }) - .bind('ajax:aborted:required', function() { + .on('ajax:aborted:required', function() { return false; }) .trigger('submit'); @@ -173,10 +170,10 @@ asyncTest('disabled fields should not be included in blank required check', 2, f var form = $('form[data-remote]') .append($('')) .append($('')) - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { ok(true, 'ajax:beforeSend should run'); }) - .bind('ajax:aborted:required', function() { + .on('ajax:aborted:required', function() { ok(false, 'ajax:aborted:required should not run'); }); @@ -187,18 +184,33 @@ asyncTest('form should be submitted with blank required fields if it has the "no var form = $('form[data-remote]') .append($('')) .attr("novalidate", "novalidate") - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { ok(true, 'ajax:beforeSend should run'); }) - .bind('ajax:aborted:required', function() { + .on('ajax:aborted:required', function() { ok(false, 'ajax:aborted:required should not run'); }); submit(); }); +asyncTest('form should be submitted with blank required fields if the button has the "formnovalidate" attribute', 2, function(){ + var submit_button = $(''); + var form = $('form[data-remote]') + .append($('')) + .append(submit_button) + .on('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run'); + }) + .on('ajax:aborted:required', function() { + ok(false, 'ajax:aborted:required should not run'); + }); + + submit_with_button(submit_button); +}); + asyncTest('blank required form input for non-remote form with "novalidate" attribute should not abort normal submission', 1, function() { - $(document).bind('iframe:loading', function() { + $(document).on('iframe:loading', function() { ok(true, 'form should get submitted'); }); @@ -217,7 +229,7 @@ asyncTest('unchecked required checkbox should abort form submission', 1, functio var form = $('form[data-remote]') .append($('')) .removeAttr('data-remote') - .bind('ujs:everythingStopped', function() { + .on('ujs:everythingStopped', function() { ok(true, 'ujs:everythingStopped should run'); }) .trigger('submit'); @@ -229,10 +241,10 @@ asyncTest('unchecked required checkbox should abort form submission', 1, functio asyncTest('unchecked required radio should abort form submission', 1, function() { var form = $('form[data-remote]') - .append($('')) - .append($('')) + .append($('')) + .append($('')) .removeAttr('data-remote') - .bind('ujs:everythingStopped', function() { + .on('ujs:everythingStopped', function() { ok(true, 'ujs:everythingStopped should run'); }) .trigger('submit'); @@ -243,7 +255,7 @@ asyncTest('unchecked required radio should abort form submission', 1, function() }); asyncTest('required radio should only require one to be checked', 1, function() { - $(document).bind('iframe:loading', function() { + $(document).on('iframe:loading', function() { ok(true, 'form should get submitted'); }); @@ -251,7 +263,32 @@ asyncTest('required radio should only require one to be checked', 1, function() .append($('')) .append($('')) .removeAttr('data-remote') - .bind('ujs:everythingStopped', function() { + .on('ujs:everythingStopped', function() { + ok(false, 'ujs:everythingStopped should not run'); + }) + .find('#checkme').prop('checked', true) + .end() + .trigger('submit'); + + setTimeout(function() { + start(); + }, 13); +}); + +asyncTest('required radio should only require one to be checked if not all radios are required', 1, function() { + $(document).on('iframe:loading', function() { + ok(true, 'form should get submitted'); + }); + + var form = $('form[data-remote]') + // Check the radio that is not required + .append($('')) + // Check the radio that is not required + .append($('')) + // Only one needs to be required + .append($('')) + .removeAttr('data-remote') + .on('ujs:everythingStopped', function() { ok(false, 'ujs:everythingStopped should not run'); }) .find('#checkme').prop('checked', true) @@ -270,13 +307,13 @@ function skipIt() { 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() { + .on('ajax:beforeSend', function() { ok(false, 'ajax:beforeSend should not run'); }) - .bind('iframe:loading', function() { + .on('iframe:loading', function() { ok(true, 'form should get submitted'); }) - .bind('ajax:aborted:file', function(e,data) { + .on('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'); @@ -285,7 +322,28 @@ function skipIt() { setTimeout(function() { form.find('input[type="file"]').val(''); - form.unbind('ajax:beforeSend'); + form.off('ajax:beforeSend'); + submit(); + }, 13); + }); + + asyncTest('file form input field should not abort remote request if file form input does not have a name attribute', 5, function() { + var form = $('form[data-remote]') + .append($('')) + .on('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run'); + }) + .on('iframe:loading', function() { + ok(true, 'form should get submitted'); + }) + .on('ajax:aborted:file', function(e,data) { + ok(false, 'ajax:aborted:file should not run'); + }) + .trigger('submit'); + + setTimeout(function() { + form.find('input[type="file"]').val(''); + form.off('ajax:beforeSend'); submit(); }, 13); }); @@ -293,39 +351,39 @@ function skipIt() { 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() { + .on('ajax:beforeSend', function() { ok(false, 'ajax:beforeSend should not run'); }) - .bind('iframe:loading', function() { + .on('iframe:loading', function() { ok(false, 'form should not get submitted'); }) - .bind('ajax:aborted:file', function() { + .on('ajax:aborted:file', function() { return false; }) .trigger('submit'); setTimeout(function() { form.find('input[type="file"]').val(''); - form.unbind('ajax:beforeSend'); + form.off('ajax:beforeSend'); submit(); }, 13); }); } asyncTest('"ajax:beforeSend" can be observed and stopped with event delegation', 1, function() { - $(document).delegate('form[data-remote]', 'ajax:beforeSend', function() { + $(document).on('ajax:beforeSend', 'form[data-remote]', function() { ok(true, 'ajax:beforeSend observed with event delegation'); return false; }); submit(function(form) { - form.unbind('ajax:send').bind('ajax:send', function() { + form.off('ajax:send').on('ajax:send', function() { ok(false, 'ajax:send should not run'); }); - form.unbind('ajax:complete').bind('ajax:complete', function() { + form.off('ajax:complete').on('ajax:complete', function() { ok(false, 'ajax:complete should not run'); }); - $(document).bind('ajaxStop', function() { + $(document).on('ajaxStop', function() { start(); }); }); @@ -333,19 +391,19 @@ asyncTest('"ajax:beforeSend" can be observed and stopped with event delegation', 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) { + form.on('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) { + form.on('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) { + form.on('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'); ok(xhr.getResponseHeader, 'third argument to "ajax:success" should be an XHR object'); }); - form.bind('ajax:complete', function(e, xhr, status) { + form.on('ajax:complete', function(e, xhr, status) { ok(xhr.getResponseHeader, 'first argument to "ajax:complete" should be an XHR object'); equal(status, 'success', 'second argument to ajax:complete should be a status string'); }); @@ -356,9 +414,9 @@ if(window.phantom !== undefined) { 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) { + form.on('ajax:beforeSend', function(arg) { ok(true, 'ajax:beforeSend') }); + form.on('ajax:send', function(arg) { ok(true, 'ajax:send') }); + form.on('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'); // Firefox 8 returns "Forbidden " with trailing space @@ -371,18 +429,18 @@ if(window.phantom !== undefined) { } // IF THIS TEST IS FAILING, TRY INCREASING THE TIMEOUT AT THE BOTTOM TO > 100 -asyncTest('binding to ajax callbacks via .delegate() triggers handlers properly', 4, function() { +asyncTest('binding to ajax callbacks via .on() triggers handlers properly', 4, function() { $(document) - .delegate('form[data-remote]', 'ajax:beforeSend', function() { + .on('ajax:beforeSend', 'form[data-remote]', function() { ok(true, 'ajax:beforeSend handler is triggered'); }) - .delegate('form[data-remote]', 'ajax:send', function() { + .on('ajax:send', 'form[data-remote]', function() { ok(true, 'ajax:send handler is triggered'); }) - .delegate('form[data-remote]', 'ajax:complete', function() { + .on('ajax:complete', 'form[data-remote]', function() { ok(true, 'ajax:complete handler is triggered'); }) - .delegate('form[data-remote]', 'ajax:success', function() { + .on('ajax:success', 'form[data-remote]', function() { ok(true, 'ajax:success handler is triggered'); }); $('form[data-remote]').trigger('submit'); diff --git a/test/public/test/call-remote.js b/test/public/test/call-remote.js index d78ce565..0d9214a0 100644 --- a/test/public/test/call-remote.js +++ b/test/public/test/call-remote.js @@ -11,8 +11,8 @@ module('call-remote'); function submit(fn) { $('form') - .bind('ajax:success', fn) - .bind('ajax:complete', function() { start() }) + .on('ajax:success', fn) + .on('ajax:complete', function() { start() }) .trigger('submit'); } @@ -32,6 +32,19 @@ asyncTest('form method is not read from "data-method" attribute in case of missi }); }); +asyncTest('form method is read from submit button "formmethod" if submit is triggered by that button', 1, function() { + var submitButton = $('') + buildForm({ method: 'post' }); + + $('#qunit-fixture').find('form').append(submitButton) + .on('ajax:success', function(e, data, status, xhr) { + App.assertGetRequest(data); + }) + .on('ajax:complete', function() { start() }); + + submitButton.trigger('click'); +}); + asyncTest('form default method is GET', 1, function() { buildForm(); @@ -56,6 +69,19 @@ asyncTest('form url is read from "action" not "href"', 1, function() { }); }); +asyncTest('form url is read from submit button "formaction" if submit is triggered by that button', 1, function() { + var submitButton = $('') + buildForm({ method: 'post', href: '/echo2' }); + + $('#qunit-fixture').find('form').append(submitButton) + .on('ajax:success', function(e, data, status, xhr) { + App.assertRequestPath(data, '/echo'); + }) + .on('ajax:complete', function() { start() }); + + submitButton.trigger('click'); +}); + asyncTest('prefer JS, but accept any format', 1, function() { buildForm({ method: 'post' }); @@ -87,7 +113,7 @@ asyncTest('allow empty form "action"', 1, function() { buildForm({ action: '' }); $('#qunit-fixture').find('form') - .bind('ajax:beforeSend', function(e, xhr, settings) { + .on('ajax:beforeSend', function(e, xhr, settings) { // Get current location (the same way jQuery does) try { currentLocation = location.href; @@ -122,30 +148,14 @@ asyncTest('sends CSRF token in custom header', 1, function() { }); }); -asyncTest('does not send CSRF token in custom header if crossDomain', 1, function() { - buildForm({ 'data-cross-domain': 'true' }); - $('#qunit-fixture').append(''); - - // Manually set request header to be XHR, since setting crossDomain: true in .ajax() - // causes jQuery to skip setting the request header, to prevent our test/server.rb from - // raising an an error (when request.xhr? is false). - $('#qunit-fixture').find('form').bind('ajax:beforeSend', function(e, xhr) { - xhr.setRequestHeader('X-Requested-With', "XMLHttpRequest"); - }); - - submit(function(e, data, status, xhr) { - equal(data.HTTP_X_CSRF_TOKEN, undefined, 'X-CSRF-Token header should NOT be sent'); - }); -}); - -asyncTest('intelligently guesses crossDomain behavior when target URL is a different domain', 1, function(e, xhr) { +asyncTest('intelligently guesses crossDomain behavior when target URL has a different protocol and/or hostname', 1, function(e, xhr) { // Don't set data-cross-domain here, just set action to be a different domain than localhost buildForm({ action: 'http://www.alfajango.com' }); $('#qunit-fixture').append(''); $('#qunit-fixture').find('form') - .bind('ajax:beforeSend', function(e, xhr, settings) { + .on('ajax:beforeSend', function(e, xhr, settings) { equal(settings.crossDomain, true, 'crossDomain should be set to true'); @@ -157,13 +167,17 @@ 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 }); +asyncTest('intelligently guesses crossDomain behavior when target URL consists of only a path', 1, function(e, xhr) { + + // Don't set data-cross-domain here, just set action to be a different domain than localhost + buildForm({ action: '/just/a/path' }); $('#qunit-fixture').append(''); $('#qunit-fixture').find('form') - .bind('ajax:beforeSend', function(e, xhr, settings) { + .on('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; }) @@ -171,5 +185,4 @@ asyncTest('does not set crossDomain if explicitly set to false on element', 1, f setTimeout(function() { start(); }, 13); }); - })(); diff --git a/test/public/test/csrf-token.js b/test/public/test/csrf-token.js new file mode 100644 index 00000000..dfed3781 --- /dev/null +++ b/test/public/test/csrf-token.js @@ -0,0 +1,27 @@ +(function(){ + +module('csrf-token', {}); + +asyncTest('find csrf token', 1, function() { + var correctToken = "cf50faa3fe97702ca1ae"; + + $('#qunit-fixture').append(''); + + currentToken = $.rails.csrfToken(); + + start(); + equal(currentToken, correctToken); +}); + +asyncTest('find csrf param', 1, function() { + var correctParam = "authenticity_token"; + + $('#qunit-fixture').append(''); + + currentParam = $.rails.csrfParam(); + + start(); + equal(currentParam, correctParam); +}); + +})(); diff --git a/test/public/test/data-confirm.js b/test/public/test/data-confirm.js index da040b9a..2e905d42 100644 --- a/test/public/test/data-confirm.js +++ b/test/public/test/data-confirm.js @@ -39,11 +39,11 @@ asyncTest('clicking on a link with data-confirm attribute. Confirm yes.', 6, fun window.confirm = function(msg) { message = msg; return true }; $('a[data-confirm]') - .bind('confirm:complete', function(e, data) { + .on('confirm:complete', function(e, data) { App.assertCallbackInvoked('confirm:complete'); ok(data == true, 'confirm:complete passes in confirm answer (true)'); }) - .bind('ajax:success', function(e, data, status, xhr) { + .on('ajax:success', function(e, data, status, xhr) { App.assertCallbackInvoked('ajax:success'); App.assertRequestPath(data, '/echo'); App.assertGetRequest(data); @@ -60,11 +60,11 @@ asyncTest('clicking on a button with data-confirm attribute. Confirm yes.', 6, f window.confirm = function(msg) { message = msg; return true }; $('button[data-confirm]') - .bind('confirm:complete', function(e, data) { + .on('confirm:complete', function(e, data) { App.assertCallbackInvoked('confirm:complete'); ok(data == true, 'confirm:complete passes in confirm answer (true)'); }) - .bind('ajax:success', function(e, data, status, xhr) { + .on('ajax:success', function(e, data, status, xhr) { App.assertCallbackInvoked('ajax:success'); App.assertRequestPath(data, '/echo'); App.assertGetRequest(data); @@ -81,11 +81,11 @@ asyncTest('clicking on a link with data-confirm attribute. Confirm No.', 3, func window.confirm = function(msg) { message = msg; return false }; $('a[data-confirm]') - .bind('confirm:complete', function(e, data) { + .on('confirm:complete', function(e, data) { App.assertCallbackInvoked('confirm:complete'); ok(data == false, 'confirm:complete passes in confirm answer (false)'); }) - .bind('ajax:beforeSend', function(e, data, status, xhr) { + .on('ajax:beforeSend', function(e, data, status, xhr) { App.assertCallbackNotInvoked('ajax:beforeSend'); }) .trigger('click'); @@ -102,11 +102,32 @@ asyncTest('clicking on a button with data-confirm attribute. Confirm No.', 3, fu window.confirm = function(msg) { message = msg; return false }; $('button[data-confirm]') - .bind('confirm:complete', function(e, data) { + .on('confirm:complete', function(e, data) { App.assertCallbackInvoked('confirm:complete'); ok(data == false, 'confirm:complete passes in confirm answer (false)'); }) - .bind('ajax:beforeSend', function(e, data, status, xhr) { + .on('ajax:beforeSend', function(e, data, status, xhr) { + App.assertCallbackNotInvoked('ajax:beforeSend'); + }) + .trigger('click'); + + setTimeout(function() { + equal(message, 'Are you absolutely sure?'); + start(); + }, 50); +}); + +asyncTest('clicking on a button with data-confirm attribute. Confirm error.', 3, function() { + var message; + // auto-decline: + window.confirm = function(msg) { message = msg; throw "some random error"; }; + + $('button[data-confirm]') + .on('confirm:complete', function(e, data) { + App.assertCallbackInvoked('confirm:complete'); + ok(data == false, 'confirm:complete passes in confirm answer (false)'); + }) + .on('ajax:beforeSend', function(e, data, status, xhr) { App.assertCallbackNotInvoked('ajax:beforeSend'); }) .trigger('click'); @@ -123,11 +144,11 @@ asyncTest('clicking on a submit button with form and data-confirm attributes. Co window.confirm = function(msg) { message = msg; return false }; $('input[type=submit][form]') - .bind('confirm:complete', function(e, data) { + .on('confirm:complete', function(e, data) { App.assertCallbackInvoked('confirm:complete'); ok(data == false, 'confirm:complete passes in confirm answer (false)'); }) - .bind('ajax:beforeSend', function(e, data, status, xhr) { + .on('ajax:beforeSend', function(e, data, status, xhr) { App.assertCallbackNotInvoked('ajax:beforeSend'); }) .trigger('click'); @@ -145,11 +166,11 @@ asyncTest('binding to confirm event of a link and returning false', 1, function( }; $('a[data-confirm]') - .bind('confirm', function() { + .on('confirm', function() { App.assertCallbackInvoked('confirm'); return false; }) - .bind('confirm:complete', function() { + .on('confirm:complete', function() { App.assertCallbackNotInvoked('confirm:complete'); }) .trigger('click'); @@ -166,11 +187,11 @@ asyncTest('binding to confirm event of a button and returning false', 1, functio }; $('button[data-confirm]') - .bind('confirm', function() { + .on('confirm', function() { App.assertCallbackInvoked('confirm'); return false; }) - .bind('confirm:complete', function() { + .on('confirm:complete', function() { App.assertCallbackNotInvoked('confirm:complete'); }) .trigger('click'); @@ -188,11 +209,11 @@ asyncTest('binding to confirm:complete event of a link and returning false', 2, }; $('a[data-confirm]') - .bind('confirm:complete', function() { + .on('confirm:complete', function() { App.assertCallbackInvoked('confirm:complete'); return false; }) - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { App.assertCallbackNotInvoked('ajax:beforeSend'); }) .trigger('click'); @@ -210,11 +231,11 @@ asyncTest('binding to confirm:complete event of a button and returning false', 2 }; $('button[data-confirm]') - .bind('confirm:complete', function() { + .on('confirm:complete', function() { App.assertCallbackInvoked('confirm:complete'); return false; }) - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { App.assertCallbackNotInvoked('ajax:beforeSend'); }) .trigger('click'); diff --git a/test/public/test/data-disable-with.js b/test/public/test/data-disable-with.js index 5e2a67da..1bb67426 100644 --- a/test/public/test/data-disable-with.js +++ b/test/public/test/data-disable-with.js @@ -40,7 +40,7 @@ module('data-disable-with', { })); }, teardown: function() { - $(document).unbind('iframe:loaded'); + $(document).off('iframe:loaded'); } }); @@ -49,7 +49,7 @@ asyncTest('form input field with "data-disable-with" attribute', 7, function() { App.checkEnabledState(input, 'john'); - form.bind('ajax:success', function(e, data) { + form.on('ajax:success', function(e, data) { setTimeout(function() { App.checkEnabledState(input, 'john'); equal(data.params.user_name, 'john'); @@ -61,13 +61,31 @@ asyncTest('form input field with "data-disable-with" attribute', 7, function() { App.checkDisabledState(input, 'processing ...'); }); +asyncTest('blank form input field with "data-disable-with" attribute', 7, function() { + var form = $('form[data-remote]'), input = form.find('input[type=text]'); + + input.val(''); + App.checkEnabledState(input, ''); + + form.on('ajax:success', function(e, data) { + setTimeout(function() { + App.checkEnabledState(input, ''); + equal(data.params.user_name, ''); + start(); + }, 13); + }); + form.trigger('submit'); + + App.checkDisabledState(input, 'processing ...'); +}); + asyncTest('form button with "data-disable-with" attribute', 6, function() { var form = $('form[data-remote]'), button = $(''); form.append(button); App.checkEnabledState(button, 'Submit'); - form.bind('ajax:success', function(e, data) { + form.on('ajax:success', function(e, data) { setTimeout(function() { App.checkEnabledState(button, 'Submit'); start(); @@ -84,9 +102,9 @@ asyncTest('form input[type=submit][data-disable-with] disables', 6, function(){ App.checkEnabledState(input, 'Submit'); // WEEIRDD: attaching this handler makes the test work in IE7 - $(document).bind('iframe:loading', function(e, form) {}); + $(document).on('iframe:loading', function(e, form) {}); - $(document).bind('iframe:loaded', function(e, data) { + $(document).on('iframe:loaded', function(e, data) { setTimeout(function() { App.checkDisabledState(input, 'submitting ...'); start(); @@ -120,7 +138,7 @@ test('form input[type=submit][data-disable-with] re-enables when `pageshow` even 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.on('ajax:success', function(){ form.html(origFormContents); setTimeout(function(){ @@ -135,7 +153,7 @@ asyncTest('form[data-remote] input[data-disable-with] is replaced with disabled 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(){ + form.on('ajax:success', function(){ input.replaceWith(newDisabledInput); setTimeout(function(){ @@ -150,9 +168,9 @@ asyncTest('form input[type=submit][data-disable-with] using "form" attribute dis App.checkEnabledState(input, 'Form Attr Submit'); // WEEIRDD: attaching this handler makes the test work in IE7 - $(document).bind('iframe:loading', function(e, form) {}); + $(document).on('iframe:loading', function(e, form) {}); - $(document).bind('iframe:loaded', function(e, data) { + $(document).on('iframe:loaded', function(e, data) { setTimeout(function() { App.checkDisabledState(input, 'form attr submitting'); start(); @@ -170,7 +188,7 @@ 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) { + form.on('ajax:success', function(e, data) { setTimeout(function() { equal(data.params.user_bio, 'born, lived, died.'); start(); @@ -209,10 +227,10 @@ asyncTest('a[data-remote][data-disable-with] disables and re-enables', 6, functi App.checkEnabledState(link, 'Click me'); link - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { App.checkDisabledState(link, 'clicking...'); }) - .bind('ajax:complete', function() { + .on('ajax:complete', function() { setTimeout( function() { App.checkEnabledState(link, 'Click me'); start(); @@ -227,7 +245,7 @@ asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:before` event App.checkEnabledState(link, 'Click me'); link - .bind('ajax:before', function() { + .on('ajax:before', function() { App.checkDisabledState(link, 'clicking...'); return false; }) @@ -245,7 +263,7 @@ asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:beforeSend` e App.checkEnabledState(link, 'Click me'); link - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { App.checkDisabledState(link, 'clicking...'); return false; }) @@ -263,7 +281,7 @@ asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:error` event App.checkEnabledState(link, 'Click me'); link - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { App.checkDisabledState(link, 'clicking...'); }) .trigger('click'); @@ -282,7 +300,7 @@ asyncTest('form[data-remote] input|button|textarea[data-disable-with] does not d submit = $('').appendTo(form); form - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { return false; }) .trigger('submit'); @@ -319,10 +337,10 @@ asyncTest('button[data-remote][data-disable-with] disables and re-enables', 6, f App.checkEnabledState(button, 'Click me'); button - .bind('ajax:send', function() { + .on('ajax:send', function() { App.checkDisabledState(button, 'clicking...'); }) - .bind('ajax:complete', function() { + .on('ajax:complete', function() { setTimeout( function() { App.checkEnabledState(button, 'Click me'); start(); @@ -337,7 +355,7 @@ asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:before` App.checkEnabledState(button, 'Click me'); button - .bind('ajax:before', function() { + .on('ajax:before', function() { App.checkDisabledState(button, 'clicking...'); return false; }) @@ -355,7 +373,7 @@ asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:beforeSe App.checkEnabledState(button, 'Click me'); button - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { App.checkDisabledState(button, 'clicking...'); return false; }) @@ -373,7 +391,7 @@ asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:error` e App.checkEnabledState(button, 'Click me'); button - .bind('ajax:send', function() { + .on('ajax:send', function() { App.checkDisabledState(button, 'clicking...'); }) .trigger('click'); diff --git a/test/public/test/data-disable.js b/test/public/test/data-disable.js index 25e9dfc9..ed30b0ec 100644 --- a/test/public/test/data-disable.js +++ b/test/public/test/data-disable.js @@ -30,7 +30,7 @@ module('data-disable', { })); }, teardown: function() { - $(document).unbind('iframe:loaded'); + $(document).off('iframe:loaded'); } }); @@ -39,7 +39,7 @@ asyncTest('form input field with "data-disable" attribute', 7, function() { App.checkEnabledState(input, 'john'); - form.bind('ajax:success', function(e, data) { + form.on('ajax:success', function(e, data) { setTimeout(function() { App.checkEnabledState(input, 'john'); equal(data.params.user_name, 'john'); @@ -51,13 +51,13 @@ asyncTest('form input field with "data-disable" attribute', 7, function() { App.checkDisabledState(input, 'john'); }); -asyncTest('form button with "data-disable" attribute', 6, function() { +asyncTest('form button with "data-disable" attribute', 7, function() { var form = $('form[data-remote]'), button = $(''); form.append(button); App.checkEnabledState(button, 'Submit'); - form.bind('ajax:success', function(e, data) { + form.on('ajax:success', function(e, data) { setTimeout(function() { App.checkEnabledState(button, 'Submit'); start(); @@ -66,6 +66,7 @@ asyncTest('form button with "data-disable" attribute', 6, function() { form.trigger('submit'); App.checkDisabledState(button, 'Submit'); + equal(button.data('ujs:enable-with'), undefined); }); asyncTest('form input[type=submit][data-disable] disables', 6, function(){ @@ -74,9 +75,9 @@ asyncTest('form input[type=submit][data-disable] disables', 6, function(){ App.checkEnabledState(input, 'Submit'); // WEEIRDD: attaching this handler makes the test work in IE7 - $(document).bind('iframe:loading', function(e, form) {}); + $(document).on('iframe:loading', function(e, form) {}); - $(document).bind('iframe:loaded', function(e, data) { + $(document).on('iframe:loaded', function(e, data) { setTimeout(function() { App.checkDisabledState(input, 'Submit'); start(); @@ -92,7 +93,7 @@ asyncTest('form input[type=submit][data-disable] disables', 6, 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(){ + form.on('ajax:success', function(){ form.html(origFormContents); setTimeout(function(){ @@ -107,7 +108,7 @@ asyncTest('form[data-remote] input[data-disable] is replaced with disabled field 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(){ + form.on('ajax:success', function(){ input.replaceWith(newDisabledInput); setTimeout(function(){ @@ -121,7 +122,7 @@ asyncTest('form[data-remote] textarea[data-disable] attribute', 3, function() { var form = $('form[data-remote]'), textarea = $('').appendTo(form); - form.bind('ajax:success', function(e, data) { + form.on('ajax:success', function(e, data) { setTimeout(function() { equal(data.params.user_bio, 'born, lived, died.'); start(); @@ -132,13 +133,14 @@ asyncTest('form[data-remote] textarea[data-disable] attribute', 3, function() { App.checkDisabledState(textarea, 'born, lived, died.'); }); -asyncTest('a[data-disable] disables', 4, function() { +asyncTest('a[data-disable] disables', 5, function() { var link = $('a[data-disable]'); App.checkEnabledState(link, 'Click me'); link.trigger('click'); App.checkDisabledState(link, 'Click me'); + equal(link.data('ujs:enable-with'), undefined); start(); }); @@ -148,10 +150,10 @@ asyncTest('a[data-remote][data-disable] disables and re-enables', 6, function() App.checkEnabledState(link, 'Click me'); link - .bind('ajax:send', function() { + .on('ajax:send', function() { App.checkDisabledState(link, 'Click me'); }) - .bind('ajax:complete', function() { + .on('ajax:complete', function() { setTimeout( function() { App.checkEnabledState(link, 'Click me'); start(); @@ -166,7 +168,7 @@ asyncTest('a[data-remote][data-disable] re-enables when `ajax:before` event is c App.checkEnabledState(link, 'Click me'); link - .bind('ajax:before', function() { + .on('ajax:before', function() { App.checkDisabledState(link, 'Click me'); return false; }) @@ -184,7 +186,7 @@ asyncTest('a[data-remote][data-disable] re-enables when `ajax:beforeSend` event App.checkEnabledState(link, 'Click me'); link - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { App.checkDisabledState(link, 'Click me'); return false; }) @@ -202,7 +204,7 @@ asyncTest('a[data-remote][data-disable] re-enables when `ajax:error` event is tr App.checkEnabledState(link, 'Click me'); link - .bind('ajax:send', function() { + .on('ajax:send', function() { App.checkDisabledState(link, 'Click me'); }) .trigger('click'); @@ -221,7 +223,7 @@ asyncTest('form[data-remote] input|button|textarea[data-disable] does not disabl submit = $('').appendTo(form); form - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { return false; }) .trigger('submit'); @@ -258,10 +260,10 @@ asyncTest('button[data-remote][data-disable] disables and re-enables', 6, functi App.checkEnabledState(button, 'Click me'); button - .bind('ajax:send', function() { + .on('ajax:send', function() { App.checkDisabledState(button, 'Click me'); }) - .bind('ajax:complete', function() { + .on('ajax:complete', function() { setTimeout( function() { App.checkEnabledState(button, 'Click me'); start(); @@ -276,7 +278,7 @@ asyncTest('button[data-remote][data-disable] re-enables when `ajax:before` event App.checkEnabledState(button, 'Click me'); button - .bind('ajax:before', function() { + .on('ajax:before', function() { App.checkDisabledState(button, 'Click me'); return false; }) @@ -294,7 +296,7 @@ asyncTest('button[data-remote][data-disable] re-enables when `ajax:beforeSend` e App.checkEnabledState(button, 'Click me'); button - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { App.checkDisabledState(button, 'Click me'); return false; }) @@ -312,7 +314,7 @@ asyncTest('button[data-remote][data-disable] re-enables when `ajax:error` event App.checkEnabledState(button, 'Click me'); button - .bind('ajax:send', function() { + .on('ajax:send', function() { App.checkDisabledState(button, 'Click me'); }) .trigger('click'); diff --git a/test/public/test/data-method.js b/test/public/test/data-method.js index c4426624..32b90d70 100644 --- a/test/public/test/data-method.js +++ b/test/public/test/data-method.js @@ -7,12 +7,12 @@ module('data-method', { })); }, teardown: function() { - $(document).unbind('iframe:loaded'); + $(document).off('iframe:loaded'); } }); function submit(fn, options) { - $(document).bind('iframe:loaded', function(e, data) { + $(document).on('iframe:loaded', function(e, data) { fn(data); start(); }); @@ -46,4 +46,30 @@ asyncTest('link "target" should be carried over to generated form', 1, function( }); }); +asyncTest('link with "data-method" and cross origin', 1, function() { + var data = {}; + + $('#qunit-fixture') + .append('') + .append(''); + + $(document).on('submit', 'form', function(e) { + $(e.currentTarget).serializeArray().map(function(item) { + data[item.name] = item.value; + }); + + return false; + }); + + var link = $('#qunit-fixture').find('a'); + + link.attr('href', 'http://www.alfajango.com'); + + link.trigger('click'); + + start(); + + notEqual(data.authenticity_token, 'cf50faa3fe97702ca1ae'); +}); + })(); diff --git a/test/public/test/data-remote.js b/test/public/test/data-remote.js index f8c6b2a8..215d8217 100644 --- a/test/public/test/data-remote.js +++ b/test/public/test/data-remote.js @@ -16,7 +16,14 @@ module('data-remote', { .append($('', { action: '/echo', 'data-remote': 'true', - method: 'post' + method: 'post', + id: 'my-remote-form' + })) + .append($('', { + href: '/echo', + 'data-remote': 'true', + disabled: 'disabled', + text: 'Disabed link' })) .find('form').append($('')); @@ -32,7 +39,7 @@ asyncTest('ctrl-clicking on a link does not fire ajaxyness', 0, function() { // follow links using `trigger('click')`, it only fires bindings. link .removeAttr('data-params') - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { ok(false, 'ajax should not be triggered'); }); @@ -55,7 +62,7 @@ asyncTest('ctrl-clicking on a link still fires ajax for non-GET links and for li link .removeAttr('data-params') .attr('data-method', 'POST') - .bind('ajax:beforeSend', function() { + .on('ajax:beforeSend', function() { ok(true, 'ajax should be triggered'); }) .trigger(e); @@ -73,27 +80,40 @@ asyncTest('ctrl-clicking on a link still fires ajax for non-GET links and for li asyncTest('clicking on a link with data-remote attribute', 5, function() { $('a[data-remote]') - .bind('ajax:success', function(e, data, status, xhr) { + .on('ajax:success', function(e, data, status, xhr) { App.assertCallbackInvoked('ajax:success'); App.assertRequestPath(data, '/echo'); equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value'); equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value'); App.assertGetRequest(data); }) - .bind('ajax:complete', function() { start() }) + .on('ajax:complete', function() { start() }) .trigger('click'); }); +asyncTest('clicking on a link with disabled attribute', 0, function() { + $('a[disabled]') + .on("ajax:before", function(e, data, status, xhr) { + App.assertCallbackNotInvoked('ajax:success') + }) + .on('ajax:complete', function() { start() }) + .trigger('click') + + setTimeout(function() { + start(); + }, 13); +}); + asyncTest('clicking on a button with data-remote attribute', 5, function() { $('button[data-remote]') - .bind('ajax:success', function(e, data, status, xhr) { + .on('ajax:success', function(e, data, status, xhr) { App.assertCallbackInvoked('ajax:success'); App.assertRequestPath(data, '/echo'); equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value'); equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value'); App.assertGetRequest(data); }) - .bind('ajax:complete', function() { start() }) + .on('ajax:complete', function() { start() }) .trigger('click'); }); @@ -111,30 +131,90 @@ asyncTest('changing a select option with data-remote attribute', 5, function() { ); $('select[data-remote]') - .bind('ajax:success', function(e, data, status, xhr) { + .on('ajax:success', function(e, data, status, xhr) { App.assertCallbackInvoked('ajax:success'); App.assertRequestPath(data, '/echo'); equal(data.params.user_data, 'optionValue2', 'ajax arguments should have key term with right value'); equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value'); App.assertGetRequest(data); }) - .bind('ajax:complete', function() { start() }) + .on('ajax:complete', function() { start() }) .val('optionValue2') .trigger('change'); }); asyncTest('submitting form with data-remote attribute', 4, function() { $('form[data-remote]') - .bind('ajax:success', function(e, data, status, xhr) { + .on('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success'); + App.assertRequestPath(data, '/echo'); + equal(data.params.user_name, 'john', 'ajax arguments should have key user_name with right value'); + App.assertPostRequest(data); + }) + .on('ajax:complete', function() { start() }) + .trigger('submit'); +}); + +asyncTest('submitting form with data-remote attribute should include inputs in a fieldset only once', 3, function() { + $('form[data-remote]') + .append('') + .on('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success'); + equal(data.params.items.length, 1, 'ajax arguments should only have the item once') + App.assertPostRequest(data); + }) + .on('ajax:complete', function() { + $('form[data-remote], fieldset').remove() + start() + }) + .trigger('submit'); +}); + +asyncTest('submitting form with data-remote attribute submits input with matching [form] attribute', 5, function() { + $('#qunit-fixture') + .append($('')); + + $('form[data-remote]') + .on('ajax:success', function(e, data, status, xhr) { App.assertCallbackInvoked('ajax:success'); App.assertRequestPath(data, '/echo'); equal(data.params.user_name, 'john', 'ajax arguments should have key user_name with right value'); + equal(data.params.user_data, 'value1', 'ajax arguments should have key user_data with right value'); App.assertPostRequest(data); }) - .bind('ajax:complete', function() { start() }) + .on('ajax:complete', function() { start() }) .trigger('submit'); }); +asyncTest('submitting form with data-remote attribute by clicking button with matching [form] attribute', 5, function() { + $('form[data-remote]') + .on('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success'); + App.assertRequestPath(data, '/echo'); + equal(data.params.user_name, 'john', 'ajax arguments should have key user_name with right value'); + equal(data.params.user_data, 'value2', 'ajax arguments should have key user_data with right value'); + App.assertPostRequest(data); + }) + .on('ajax:complete', function() { start() }); + + $('', { + type: "submit", + name: "user_data", + value: "value1", + form: "my-remote-form" + }) + .appendTo($('#qunit-fixture')); + + $('', { + type: "submit", + name: "user_data", + value: "value2", + form: "my-remote-form" + }) + .appendTo($('#qunit-fixture')) + .trigger('click'); +}); + asyncTest('form\'s submit bindings in browsers that don\'t support submit bubbling', 5, function() { var form = $('form[data-remote]'), directBindingCalled = false; @@ -142,16 +222,16 @@ asyncTest('form\'s submit bindings in browsers that don\'t support submit bubbli form .append($('')) - .bind('submit', function(event){ + .on('submit', function(event){ ok(event.type == 'submit', 'submit event handlers are called with submit event'); ok(true, 'binding handler is called'); directBindingCalled = true; }) - .bind('ajax:beforeSend', function(){ + .on('ajax:beforeSend', function(){ ok(true, 'form being submitted via ajax'); ok(directBindingCalled, 'binding handler already called'); }) - .bind('ajax:complete', function(){ + .on('ajax:complete', function(){ start(); }); @@ -169,11 +249,11 @@ asyncTest('returning false in form\'s submit bindings in non-submit-bubbling bro form .append($('')) - .bind('submit', function(){ + .on('submit', function(){ ok(true, 'binding handler is called'); return false; }) - .bind('ajax:beforeSend', function(){ + .on('ajax:beforeSend', function(){ ok(false, 'form should not be submitted'); }); @@ -186,3 +266,96 @@ asyncTest('returning false in form\'s submit bindings in non-submit-bubbling bro setTimeout(function(){ start(); }, 13); }); + +asyncTest('clicking on a link with falsy "data-remote" attribute does not fire ajaxyness', 0, function() { + $('a[data-remote]') + .attr('data-remote', 'false') + .on('ajax:beforeSend', function() { + ok(false, 'ajax should not be triggered'); + }) + .on('click', function() { + return false; + }) + .trigger('click'); + + setTimeout(function(){ start(); }, 20); +}); + +asyncTest('ctrl-clicking on a link with falsy "data-remote" attribute does not fire ajaxyness even if "data-params" present', 0, function() { + var link = $('a[data-remote]'), e; + e = $.Event('click'); + e.metaKey = true; + + link + .removeAttr('data-params') + .attr('data-remote', 'false') + .attr('data-method', 'POST') + .on('ajax:beforeSend', function() { + ok(false, 'ajax should not be triggered'); + }) + .on('click', function() { + return false; + }) + .trigger(e); + + e = $.Event('click'); + e.metaKey = true; + + link + .removeAttr('data-method') + .attr('data-params', 'name=steve') + .trigger(e); + + setTimeout(function(){ start(); }, 20); +}); + +asyncTest('clicking on a button with falsy "data-remote" attribute', 0, function() { + $('button[data-remote]:first') + .attr('data-remote', 'false') + .on('ajax:beforeSend', function() { + ok(false, 'ajax should not be triggered'); + }) + .on('click', function() { + return false; + }) + .trigger('click'); + + setTimeout(function(){ start(); }, 20); +}); + +asyncTest('submitting a form with falsy "data-remote" attribute', 0, function() { + $('form[data-remote]:first') + .attr('data-remote', 'false') + .on('ajax:beforeSend', function() { + ok(false, 'ajax should not be triggered'); + }) + .on('submit', function() { + return false; + }) + .trigger('submit'); + + setTimeout(function(){ start(); }, 20); +}); + +asyncTest('changing a select option with falsy "data-remote" attribute', 0, function() { + $('form') + .append( + $('', { + 'name': 'user_data', + 'data-remote': 'false', + 'data-params': 'data1=value1', + 'data-url': '/echo' + }) + .append($('', {value: 'optionValue1', text: 'option1'})) + .append($('', {value: 'optionValue2', text: 'option2'})) + ); + + $('select[data-remote=false]:first') + .on('ajax:beforeSend', function() { + ok(false, 'ajax should not be triggered'); + }) + .val('optionValue2') + .trigger('change'); + + setTimeout(function(){ start(); }, 20); +}); diff --git a/test/public/test/override.js b/test/public/test/override.js index ba84b6dc..0dca60cc 100644 --- a/test/public/test/override.js +++ b/test/public/test/override.js @@ -32,7 +32,7 @@ asyncTest("the getter for an element's href is overridable", 1, function() { asyncTest("the getter for an element's href works normally if not overridden", 1, function() { $.rails.ajax = function(options) { - equal('/real/href', options.url); + equal(location.protocol + '//' + location.host + '/real/href', options.url); } $.rails.handleRemote($('#qunit-fixture').find('a')); start(); diff --git a/test/public/test/settings.js b/test/public/test/settings.js index 85a33ac1..e516e063 100644 --- a/test/public/test/settings.js +++ b/test/public/test/settings.js @@ -25,7 +25,7 @@ App.getVal = function(el) { }; App.disabled = function(el) { - return el.is('input,textarea,select,button') ? el.is(':disabled') : el.data('ujs:enable-with'); + return el.is('input,textarea,select,button') ? (el.is(':disabled') && el.data('ujs:disabled')) : el.data('ujs:disabled'); }; App.checkEnabledState = function(el, text) { @@ -40,7 +40,7 @@ App.checkDisabledState = function(el, text) { // hijacks normal form submit; lets it submit to an iframe to prevent // navigating away from the test suite -$(document).bind('submit', function(e) { +$(document).on('submit', function(e) { if (!e.isDefaultPrevented()) { var form = $(e.target), action = form.attr('action'), name = 'form-frame' + jQuery.guid++, diff --git a/test/server.rb b/test/server.rb index 85f11db2..db94368a 100644 --- a/test/server.rb +++ b/test/server.rb @@ -1,7 +1,7 @@ require 'sinatra' require 'json' -JQUERY_VERSIONS = %w[ 1.8.0 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.10.0 1.10.1 1.10.2 1.11.0 2.0.0 2.1.0].freeze +JQUERY_VERSIONS = %w[ 1.8.0 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.10.0 1.10.1 1.10.2 1.11.0 2.0.0 2.1.0 3.0.0].freeze use Rack::Static, :urls => ["/src"], :root => File.expand_path('..', settings.root) diff --git a/test/views/index.erb b/test/views/index.erb index f153cd12..776a4bab 100644 --- a/test/views/index.erb +++ b/test/views/index.erb @@ -1,6 +1,6 @@ <% @title = "jquery-ujs test" %> -<%= test 'data-confirm', 'data-remote', 'data-disable', 'data-disable-with', 'call-remote', 'call-remote-callbacks', 'data-method', 'override', 'csrf-refresh' %> +<%= test 'data-confirm', 'data-remote', 'data-disable', 'data-disable-with', 'call-remote', 'call-remote-callbacks', 'data-method', 'override', 'csrf-refresh', 'csrf-token' %>