diff --git a/.blade.yml b/.blade.yml new file mode 100644 index 00000000..fe447507 --- /dev/null +++ b/.blade.yml @@ -0,0 +1,10 @@ +load_paths: + - src + +logical_paths: + - rails.js + +build: + logical_paths: + - rails.js + path: dist diff --git a/.gitignore b/.gitignore index 251f2cfc..35bf0e42 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ node_modules .bundle .DS_Store *~ +/npm-debug.log +/log +/tmp diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 9bfd6603..00000000 --- a/.jshintrc +++ /dev/null @@ -1,71 +0,0 @@ -{ - "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/.travis.yml b/.travis.yml index 7e6cb080..38962d3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,29 +1,17 @@ language: ruby sudo: false +rvm: 2.3.0 cache: - - bundler - - directories: - - $HOME/.npm + bundler: true + directories: + - node_modules + - $HOME/.nvm script: ./script/cibuild before_install: - - "npm install jshint -g" + - nvm install node + - node --version + - npm install + - "[[ $(phantomjs --version) > '2' ]] || npm install -g phantomjs-prebuilt" env: - JQUERY_VERSION: 1.8.0 - - JQUERY_VERSION: 1.8.1 - - JQUERY_VERSION: 1.8.2 - - JQUERY_VERSION: 1.8.3 - - JQUERY_VERSION: 1.9.0 - - JQUERY_VERSION: 1.9.1 - - JQUERY_VERSION: 1.10.0 - - 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/Gemfile b/Gemfile index aca39a6b..8aadabed 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,8 @@ +$:.unshift 'lib' source 'https://rubygems.org' gem 'sinatra', '~> 1.0' gem 'shotgun', :group => :reloadable gem 'thin', :group => :reloadable gem 'rake' +gem 'blade' diff --git a/Gemfile.lock b/Gemfile.lock index 8ced3297..447e45da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,96 @@ GEM remote: https://rubygems.org/ specs: - daemons (1.1.9) - eventmachine (1.0.4) - rack (1.6.0) + activesupport (5.0.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) + addressable (2.4.0) + blade (0.5.6) + activesupport (>= 3.0.0) + blade-qunit_adapter (~> 1.20.0) + coffee-script + coffee-script-source + curses (~> 1.0.0) + eventmachine + faye + sprockets (>= 3.0) + sprockets-export (~> 0.9.1) + thin (>= 1.6.0) + thor (~> 0.19.1) + useragent (~> 0.16.7) + blade-qunit_adapter (1.20.0) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.10.0) + concurrent-ruby (1.0.2) + cookiejar (0.3.3) + curses (1.0.2) + daemons (1.2.3) + em-http-request (1.1.5) + addressable (>= 2.3.4) + cookiejar (!= 0.3.1) + em-socksify (>= 0.3) + eventmachine (>= 1.0.3) + http_parser.rb (>= 0.6.0) + em-socksify (0.3.1) + eventmachine (>= 1.0.0.beta.4) + eventmachine (1.2.0.1) + execjs (2.7.0) + faye (1.2.2) + cookiejar (>= 0.3.0) + em-http-request (>= 0.3.0) + eventmachine (>= 0.12.0) + faye-websocket (>= 0.9.1) + multi_json (>= 1.0.0) + rack (>= 1.0.0) + websocket-driver (>= 0.5.1) + faye-websocket (0.10.4) + eventmachine (>= 0.12.0) + websocket-driver (>= 0.5.1) + http_parser.rb (0.6.0) + i18n (0.7.0) + minitest (5.9.0) + multi_json (1.12.1) + rack (1.6.4) rack-protection (1.5.3) rack - rake (10.4.2) - shotgun (0.9) + rake (11.2.2) + shotgun (0.9.1) rack (>= 1.0) - sinatra (1.4.5) - rack (~> 1.4) + sinatra (1.4.7) + rack (~> 1.5) rack-protection (~> 1.4) - tilt (~> 1.3, >= 1.3.4) - thin (1.6.3) + tilt (>= 1.3, < 3) + sprockets (3.7.0) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-export (0.9.1) + thin (1.7.0) daemons (~> 1.0, >= 1.0.9) - eventmachine (~> 1.0) - rack (~> 1.0) - tilt (1.4.1) + eventmachine (~> 1.0, >= 1.0.4) + rack (>= 1, < 3) + thor (0.19.1) + thread_safe (0.3.5) + tilt (2.0.5) + tzinfo (1.2.2) + thread_safe (~> 0.1) + useragent (0.16.7) + websocket-driver (0.6.4) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) PLATFORMS ruby DEPENDENCIES + blade rake shotgun sinatra (~> 1.0) thin + +BUNDLED WITH + 1.13.1 diff --git a/Rakefile b/Rakefile index e0e44e67..f0e4123a 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,10 @@ desc %(Starts the test server and opens it in a web browser) -multitask :default => ['test:server', 'test:open'] +multitask :develop => ['test:server', 'test:open'] + +desc %(Build source files into dist/rails.js) +task :build do + system "bundle exec blade build" +end PORT = 4567 @@ -11,7 +16,7 @@ namespace :test do desc %(Starts the test server which reloads everything on each refresh) task :reloadable do - exec "bundle exec shotgun test/config.ru -p #{PORT} --server thin" + system "bundle exec shotgun test/config.ru -p #{PORT} --server thin" end task :open do diff --git a/coffeelint.json b/coffeelint.json new file mode 100644 index 00000000..bba4b9bf --- /dev/null +++ b/coffeelint.json @@ -0,0 +1,135 @@ +{ + "arrow_spacing": { + "level": "warn" + }, + "braces_spacing": { + "level": "warn", + "spaces": 1, + "empty_object_spaces": 0 + }, + "camel_case_classes": { + "level": "error" + }, + "coffeescript_error": { + "level": "error" + }, + "colon_assignment_spacing": { + "level": "warn", + "spacing": { + "left": 0, + "right": 1 + } + }, + "cyclomatic_complexity": { + "level": "warn", + "value": 10 + }, + "duplicate_key": { + "level": "error" + }, + "empty_constructor_needs_parens": { + "level": "warn" + }, + "ensure_comprehensions": { + "level": "warn" + }, + "eol_last": { + "level": "warn" + }, + "indentation": { + "value": 2, + "level": "error" + }, + "line_endings": { + "level": "warn", + "value": "unix" + }, + "max_line_length": { + "value": 80, + "level": "ignore", + "limitComments": true + }, + "missing_fat_arrows": { + "level": "warn", + "is_strict": false + }, + "newlines_after_classes": { + "value": 3, + "level": "warn" + }, + "no_backticks": { + "level": "error" + }, + "no_debugger": { + "level": "warn", + "console": false + }, + "no_empty_functions": { + "level": "warn" + }, + "no_empty_param_list": { + "level": "warn" + }, + "no_implicit_braces": { + "level": "ignore", + "strict": true + }, + "no_implicit_parens": { + "level": "ignore", + "strict": true + }, + "no_interpolation_in_single_quotes": { + "level": "warn" + }, + "no_nested_string_interpolation": { + "level": "warn" + }, + "no_plusplus": { + "level": "warn" + }, + "no_private_function_fat_arrows": { + "level": "warn" + }, + "no_stand_alone_at": { + "level": "warn" + }, + "no_tabs": { + "level": "error" + }, + "no_this": { + "level": "warn" + }, + "no_throwing_strings": { + "level": "error" + }, + "no_trailing_semicolons": { + "level": "error" + }, + "no_trailing_whitespace": { + "level": "error", + "allowed_in_comments": false, + "allowed_in_empty_lines": true + }, + "no_unnecessary_double_quotes": { + "level": "warn" + }, + "no_unnecessary_fat_arrows": { + "level": "warn" + }, + "non_empty_constructor_needs_parens": { + "level": "warn" + }, + "prefer_english_operator": { + "level": "ignore", + "doubleNotLevel": "warn" + }, + "space_operators": { + "level": "warn" + }, + "spacing_after_comma": { + "level": "warn" + }, + "transform_messes_up_line_numbers": { + "level": "warn" + } +} diff --git a/dist/rails.js b/dist/rails.js new file mode 100644 index 00000000..659af3d7 --- /dev/null +++ b/dist/rails.js @@ -0,0 +1,700 @@ +(function() { + (function() { + (function() { + this.Rails = { + linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]', + buttonClickSelector: { + selector: 'button[data-remote]:not([form]), button[data-confirm]:not([form])', + exclude: 'form button' + }, + inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]', + formSubmitSelector: 'form', + 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])', + formDisableSelector: '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', + formEnableSelector: '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', + requiredInputSelector: 'input[name][required]:not([disabled]), textarea[name][required]:not([disabled])', + fileInputSelector: 'input[name][type=file]:not([disabled])', + linkDisableSelector: 'a[data-disable-with], a[data-disable]', + buttonDisableSelector: 'button[data-remote][data-disable-with], button[data-remote][data-disable]' + }; + + }).call(this); + }).call(this); + + var Rails = this.Rails; + + (function() { + (function() { + var expando, m; + + m = Element.prototype.matches || Element.prototype.matchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector; + + Rails.matches = function(element, selector) { + if (selector.exclude != null) { + return m.call(element, selector.selector) && !m.call(element, selector.exclude); + } else { + return m.call(element, selector); + } + }; + + expando = '_ujsData'; + + Rails.getData = function(element, key) { + var ref; + return (ref = element[expando]) != null ? ref[key] : void 0; + }; + + Rails.setData = function(element, key, value) { + if (element[expando] == null) { + element[expando] = {}; + } + return element[expando][key] = value; + }; + + Rails.$ = function(selector) { + return Array.prototype.slice.call(document.querySelectorAll(selector)); + }; + + }).call(this); + (function() { + var $, csrfParam, csrfToken; + + $ = Rails.$; + + csrfToken = Rails.csrfToken = function() { + var meta; + meta = document.querySelector('meta[name=csrf-token]'); + return meta && meta.content; + }; + + csrfParam = Rails.csrfParam = function() { + var meta; + meta = document.querySelector('meta[name=csrf-param]'); + return meta && meta.content; + }; + + Rails.CSRFProtection = function(xhr) { + var token; + token = csrfToken(); + if (token != null) { + return xhr.setRequestHeader('X-CSRF-Token', token); + } + }; + + Rails.refreshCSRFTokens = function() { + var param, token; + token = csrfToken(); + param = csrfParam(); + if ((token != null) && (param != null)) { + return $('form input[name="' + param + '"]').forEach(function(input) { + return input.value = token; + }); + } + }; + + }).call(this); + (function() { + var CustomEvent, fire, matches; + + matches = Rails.matches; + + CustomEvent = window.CustomEvent; + + if (typeof CustomEvent === 'function') { + CustomEvent = function(event, params) { + var evt; + evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + }; + CustomEvent.prototype = window.Event.prototype; + } + + fire = Rails.fire = function(obj, name, data) { + var event; + event = new CustomEvent(name, { + bubbles: true, + cancelable: true, + detail: data + }); + obj.dispatchEvent(event); + return !event.defaultPrevented; + }; + + Rails.stopEverything = function(e) { + fire(e.target, 'ujs:everythingStopped'); + e.preventDefault(); + e.stopPropagation(); + return e.stopImmediatePropagation(); + }; + + Rails.delegate = function(element, selector, eventType, handler) { + return element.addEventListener(eventType, function(e) { + if (matches(e.target, selector)) { + if (handler.call(e.target, e) === false) { + e.preventDefault(); + return e.stopPropagation(); + } + } + }); + }; + + }).call(this); + (function() { + var AcceptHeaders, CSRFProtection, createXHR, fire, prepareOptions, processResponse; + + CSRFProtection = Rails.CSRFProtection, fire = Rails.fire; + + AcceptHeaders = { + '*': '*/*', + text: 'text/plain', + html: 'text/html', + xml: 'application/xml, text/xml', + json: 'application/json, text/javascript', + script: 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript' + }; + + Rails.ajax = function(options) { + var xhr; + options = prepareOptions(options); + xhr = createXHR(options, function() { + var response; + response = processResponse(xhr.response, xhr.getResponseHeader('Content-Type')); + if (Math.floor(xhr.status / 100) === 2) { + if (typeof options.success === "function") { + options.success(response, xhr.statusText, xhr); + } + } else { + if (typeof options.error === "function") { + options.error(response, xhr.statusText, xhr); + } + } + return typeof options.complete === "function" ? options.complete(xhr, xhr.statusText) : void 0; + }); + if (typeof options.beforeSend === "function") { + options.beforeSend(xhr, options); + } + if (xhr.readyState === XMLHttpRequest.OPENED) { + return xhr.send(options.data); + } else { + return fire(document, 'ajaxStop'); + } + }; + + prepareOptions = function(options) { + options.type = options.type.toUpperCase(); + if (options.type === 'GET' && options.data) { + if (options.url.indexOf('?') < 0) { + options.url += '?' + options.data; + } else { + options.url += '&' + options.data; + } + } + if (AcceptHeaders[options.dataType] == null) { + options.dataType = '*'; + } + options.accept = AcceptHeaders[options.dataType]; + if (options.dataType !== '*') { + options.accept += ', */*; q=0.01'; + } + return options; + }; + + createXHR = function(options, done) { + var xhr; + xhr = new XMLHttpRequest(); + xhr.open(options.type, options.url, true); + xhr.setRequestHeader('Accept', options.accept); + if (typeof options.data === 'string') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); + } + if (!options.crossDomain) { + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + } + CSRFProtection(xhr); + xhr.withCredentials = !!options.withCredentials; + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) { + return done(xhr); + } + }; + return xhr; + }; + + processResponse = function(response, type) { + var parser, script; + if (typeof response === 'string' && typeof type === 'string') { + if (type.match(/\bjson\b/)) { + try { + response = JSON.parse(response); + } catch (undefined) {} + } else if (type.match(/\bjavascript\b/)) { + script = document.createElement('script'); + script.innerHTML = response; + document.body.appendChild(script); + } else if (type.match(/\b(xml|html|svg)\b/)) { + parser = new DOMParser(); + type = type.replace(/;.+/, ''); + try { + response = parser.parseFromString(response, type); + } catch (undefined) {} + } + } + return response; + }; + + Rails.href = function(element) { + return element.href; + }; + + Rails.isCrossDomain = function(url) { + var e, error, originAnchor, urlAnchor; + originAnchor = document.createElement('a'); + originAnchor.href = location.href; + urlAnchor = document.createElement('a'); + try { + urlAnchor.href = url; + return !(((!urlAnchor.protocol || urlAnchor.protocol === ':') && !urlAnchor.host) || (originAnchor.protocol + '//' + originAnchor.host === urlAnchor.protocol + '//' + urlAnchor.host)); + } catch (error) { + e = error; + return true; + } + }; + + }).call(this); + (function() { + var matches, toArray; + + matches = Rails.matches; + + toArray = function(e) { + return Array.prototype.slice.call(e); + }; + + Rails.serializeElement = function(element, additionalParam) { + var inputs, params; + inputs = [element]; + if (matches(element, 'form')) { + inputs = toArray(element.elements); + } + params = []; + inputs.forEach(function(input) { + if (!input.name) { + return; + } + if (matches(input, 'select')) { + return toArray(input.options).forEach(function(option) { + if (option.selected) { + return params.push({ + name: input.name, + value: option.value + }); + } + }); + } else if (input.type !== 'radio' && input.type !== 'checkbox' || input.checked) { + return params.push({ + name: input.name, + value: input.value + }); + } + }); + if (additionalParam) { + params.push(additionalParam); + } + return params.map(function(param) { + if (param.name != null) { + return (encodeURIComponent(param.name)) + "=" + (encodeURIComponent(param.value)); + } else { + return param; + } + }).join('&'); + }; + + Rails.formElements = function(form, selector) { + if (matches(form, 'form')) { + return toArray(form.elements).filter(function(el) { + return matches(el, selector); + }); + } else { + return toArray(form.querySelectorAll(selector)); + } + }; + + Rails.blankInputs = function(form, selector, nonBlank) { + var checkedRadioButtonNames, foundInputs, requiredInputs; + foundInputs = []; + requiredInputs = toArray(form.querySelectorAll(selector || 'input, textarea')); + checkedRadioButtonNames = {}; + requiredInputs.forEach(function(input) { + var radioName, radios, valueToCheck; + if (input.type === 'radio') { + radioName = input.name; + if (!checkedRadioButtonNames[radioName]) { + if (form.querySelectorAll("input[type=radio][name='" + radioName + "']:checked").length === 0) { + radios = form.querySelectorAll("input[type=radio][name='" + radioName + "']"); + foundInputs = foundInputs.concat(toArray(radios)); + } + return checkedRadioButtonNames[radioName] = radioName; + } + } else { + valueToCheck = input.type === 'checkbox' ? input.checked : !!input.value; + if (valueToCheck === nonBlank) { + return foundInputs.push(input); + } + } + }); + return foundInputs; + }; + + }).call(this); + (function() { + var allowAction, fire, stopEverything; + + fire = Rails.fire, stopEverything = Rails.stopEverything; + + Rails.handleConfirm = function(e) { + if (!allowAction(e.target)) { + return stopEverything(e); + } + }; + + allowAction = function(element) { + var answer, callback, message; + message = element.getAttribute('data-confirm'); + if (!message) { + return true; + } + answer = false; + if (fire(element, 'confirm')) { + try { + answer = confirm(message); + } catch (undefined) {} + callback = fire(element, 'confirm:complete', [answer]); + } + return answer && callback; + }; + + }).call(this); + (function() { + var disableFormElement, disableFormElements, disableLinkElement, enableFormElement, enableFormElements, enableLinkElement, formElements, getData, matches, setData, stopEverything; + + matches = Rails.matches, getData = Rails.getData, setData = Rails.setData, stopEverything = Rails.stopEverything, formElements = Rails.formElements; + + Rails.enableElement = function(e) { + var element; + element = e instanceof Event ? e.target : e; + if (matches(element, Rails.linkDisableSelector)) { + return enableLinkElement(element); + } else if (matches(element, Rails.buttonDisableSelector) || matches(element, Rails.formEnableSelector)) { + return enableFormElement(element); + } else if (matches(element, Rails.formSubmitSelector)) { + return enableFormElements(element); + } + }; + + Rails.disableElement = function(e) { + var element; + element = e instanceof Event ? e.target : e; + if (matches(element, Rails.linkDisableSelector)) { + return disableLinkElement(element); + } else if (matches(element, Rails.buttonDisableSelector) || matches(element, Rails.formDisableSelector)) { + return disableFormElement(element); + } else if (matches(element, Rails.formSubmitSelector)) { + return disableFormElements(element); + } + }; + + disableLinkElement = function(element) { + var replacement; + replacement = element.getAttribute('data-disable-with'); + if (replacement != null) { + setData(element, 'ujs:enable-with', element.innerHTML); + element.innerHTML = replacement; + } + element.addEventListener('click', stopEverything); + return setData(element, 'ujs:disabled', true); + }; + + enableLinkElement = function(element) { + var originalText; + originalText = getData(element, 'ujs:enable-with'); + if (originalText != null) { + element.innerHTML = originalText; + setData(element, 'ujs:enable-with', null); + } + element.removeEventListener('click', stopEverything); + return setData(element, 'ujs:disabled', null); + }; + + disableFormElements = function(form) { + return formElements(form, Rails.formDisableSelector).forEach(disableFormElement); + }; + + disableFormElement = function(element) { + var replacement; + replacement = element.getAttribute('data-disable-with'); + if (replacement != null) { + if (matches(element, 'button')) { + setData(element, 'ujs:enable-with', element.innerHTML); + element.innerHTML = replacement; + } else { + setData(element, 'ujs:enable-with', element.value); + element.value = replacement; + } + } + element.disabled = true; + return setData(element, 'ujs:disabled', true); + }; + + enableFormElements = function(form) { + return formElements(form, Rails.formEnableSelector).forEach(enableFormElement); + }; + + enableFormElement = function(element) { + var originalText; + originalText = getData(element, 'ujs:enable-with'); + if (originalText != null) { + if (matches(element, 'button')) { + element.innerHTML = originalText; + } else { + element.value = originalText; + } + setData(element, 'ujs:enable-with', null); + } + element.disabled = false; + return setData(element, 'ujs:disabled', null); + }; + + }).call(this); + (function() { + var stopEverything; + + stopEverything = Rails.stopEverything; + + Rails.handleMethod = function(e) { + var csrfParam, csrfToken, form, formContent, href, link, method; + link = e.target; + method = link.getAttribute('data-method'); + if (!method) { + return; + } + href = Rails.href(link); + csrfToken = Rails.csrfToken(); + csrfParam = Rails.csrfParam(); + form = document.createElement('form'); + formContent = ""; + if ((csrfParam != null) && (csrfToken != null) && !Rails.isCrossDomain(href)) { + formContent += ""; + } + formContent += ''; + form.method = 'post'; + form.action = href; + form.target = link.target; + form.innerHTML = formContent; + form.style.display = 'none'; + document.body.appendChild(form); + form.querySelector('[type="submit"]').click(); + return stopEverything(e); + }; + + }).call(this); + (function() { + var ajax, blankInputs, fire, getData, isCrossDomain, isRemote, matches, serializeElement, setData, stopEverything, + slice = [].slice; + + matches = Rails.matches, getData = Rails.getData, setData = Rails.setData, fire = Rails.fire, stopEverything = Rails.stopEverything, ajax = Rails.ajax, isCrossDomain = Rails.isCrossDomain, blankInputs = Rails.blankInputs, serializeElement = Rails.serializeElement; + + isRemote = function(element) { + var value; + value = element.getAttribute('data-remote'); + return (value != null) && value !== 'false'; + }; + + Rails.handleRemote = function(e) { + var button, data, dataType, element, method, url, withCredentials; + element = e.target; + if (!isRemote(element)) { + return true; + } + if (!fire(element, 'ajax:before')) { + fire(element, 'ajax:stopped'); + return false; + } + withCredentials = element.getAttribute('data-with-credentials'); + dataType = element.getAttribute('data-type') || 'script'; + if (matches(element, Rails.formSubmitSelector)) { + button = getData(element, 'ujs:submit-button'); + method = getData(element, 'ujs:submit-button-formmethod') || element.method; + url = getData(element, 'ujs:submit-button-formaction') || element.getAttribute('action') || location.href; + if (method.toUpperCase() === 'GET') { + url = url.replace(/\?.*$/, ''); + } + if (element.enctype === 'multipart/form-data') { + data = new FormData(element); + if (button != null) { + data.append(button.name, button.value); + } + } else { + data = serializeElement(element, button); + } + setData(element, 'ujs:submit-button', null); + setData(element, 'ujs:submit-button-formmethod', null); + setData(element, 'ujs:submit-button-formaction', null); + } else if (matches(element, Rails.buttonClickSelector) || matches(element, Rails.inputChangeSelector)) { + method = element.getAttribute('data-method'); + url = element.getAttribute('data-url'); + data = serializeElement(element, element.getAttribute('data-params')); + } else { + method = element.getAttribute('data-method'); + url = Rails.href(element); + data = element.getAttribute('data-params'); + } + ajax({ + type: method || 'GET', + url: url, + data: data, + dataType: dataType, + beforeSend: function(xhr, options) { + if (fire(element, 'ajax:beforeSend', [xhr, options])) { + return fire(element, 'ajax:send', [xhr]); + } else { + fire(element, 'ajax:stopped'); + return xhr.abort(); + } + }, + success: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return fire(element, 'ajax:success', args); + }, + error: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return fire(element, 'ajax:error', args); + }, + complete: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return fire(element, 'ajax:complete', args); + }, + crossDomain: isCrossDomain(url), + withCredentials: (withCredentials != null) && withCredentials !== 'false' + }); + return stopEverything(e); + }; + + Rails.validateForm = function(e) { + var blankRequiredInputs, form; + form = e.target; + if (form.noValidate || getData(form, 'ujs:formnovalidate-button')) { + return; + } + blankRequiredInputs = blankInputs(form, Rails.requiredInputSelector, false); + if (blankRequiredInputs.length > 0 && fire(form, 'ajax:aborted:required', [blankRequiredInputs])) { + return stopEverything(e); + } + }; + + Rails.formSubmitButtonClick = function(e) { + var button, form; + button = e.target; + form = button.form; + if (!form) { + return; + } + if (button.name) { + setData(form, 'ujs:submit-button', { + name: button.name, + value: button.value + }); + } + setData(form, 'ujs:formnovalidate-button', button.formNoValidate); + setData(form, 'ujs:submit-button-formaction', button.getAttribute('formaction')); + return setData(form, 'ujs:submit-button-formmethod', button.getAttribute('formmethod')); + }; + + Rails.handleMetaClick = function(e) { + var data, link, metaClick, method; + link = e.target; + method = (link.getAttribute('data-method') || 'GET').toUpperCase(); + data = link.getAttribute('data-params'); + metaClick = e.metaKey || e.ctrlKey; + if (metaClick && method === 'GET' && !data) { + return e.stopImmediatePropagation(); + } + }; + + }).call(this); + (function() { + var $, CSRFProtection, delegate, disableElement, enableElement, fire, formSubmitButtonClick, getData, handleConfirm, handleMetaClick, handleMethod, handleRemote, refreshCSRFTokens, validateForm; + + fire = Rails.fire, delegate = Rails.delegate, getData = Rails.getData, $ = Rails.$, refreshCSRFTokens = Rails.refreshCSRFTokens, CSRFProtection = Rails.CSRFProtection, enableElement = Rails.enableElement, disableElement = Rails.disableElement, handleConfirm = Rails.handleConfirm, handleRemote = Rails.handleRemote, validateForm = Rails.validateForm, formSubmitButtonClick = Rails.formSubmitButtonClick, handleMetaClick = Rails.handleMetaClick, handleMethod = Rails.handleMethod; + + if ((typeof jQuery !== "undefined" && jQuery !== null) && !jQuery.rails) { + jQuery.rails = Rails; + jQuery.ajaxPrefilter(function(options, originalOptions, xhr) { + if (!options.crossDomain) { + return CSRFProtection(xhr); + } + }); + } + + Rails.start = function() { + if (window._rails_loaded) { + throw new Error('jquery-ujs has already been loaded!'); + } + window.addEventListener('pageshow', function() { + $(Rails.formEnableSelector).forEach(function(el) { + if (getData(el, 'ujs:disabled')) { + return enableElement(el); + } + }); + return $(Rails.linkDisableSelector).forEach(function(el) { + if (getData(el, 'ujs:disabled')) { + return enableElement(el); + } + }); + }); + delegate(document, Rails.linkDisableSelector, 'ajax:complete', enableElement); + delegate(document, Rails.linkDisableSelector, 'ajax:stopped', enableElement); + delegate(document, Rails.buttonDisableSelector, 'ajax:complete', enableElement); + delegate(document, Rails.buttonDisableSelector, 'ajax:stopped', enableElement); + delegate(document, Rails.linkClickSelector, 'click', handleConfirm); + delegate(document, Rails.linkClickSelector, 'click', handleMetaClick); + delegate(document, Rails.linkClickSelector, 'click', disableElement); + delegate(document, Rails.linkClickSelector, 'click', handleRemote); + delegate(document, Rails.linkClickSelector, 'click', handleMethod); + delegate(document, Rails.buttonClickSelector, 'click', handleConfirm); + delegate(document, Rails.buttonClickSelector, 'click', disableElement); + delegate(document, Rails.buttonClickSelector, 'click', handleRemote); + delegate(document, Rails.inputChangeSelector, 'change', handleConfirm); + delegate(document, Rails.inputChangeSelector, 'change', handleRemote); + delegate(document, Rails.formSubmitSelector, 'submit', handleConfirm); + delegate(document, Rails.formSubmitSelector, 'submit', validateForm); + delegate(document, Rails.formSubmitSelector, 'submit', handleRemote); + delegate(document, Rails.formSubmitSelector, 'submit', function(e) { + return setTimeout((function() { + return disableElement(e); + }), 13); + }); + delegate(document, Rails.formSubmitSelector, 'ajax:send', disableElement); + delegate(document, Rails.formSubmitSelector, 'ajax:complete', enableElement); + delegate(document, Rails.formInputClickSelector, 'click', handleConfirm); + delegate(document, Rails.formInputClickSelector, 'click', formSubmitButtonClick); + document.addEventListener('DOMContentLoaded', refreshCSRFTokens); + return window._rails_loaded = true; + }; + + if (window.Rails === Rails && fire(document, 'rails:attachBindings')) { + Rails.start(); + } + + }).call(this); + }).call(this); + + if (typeof module === "object" && module.exports) { + module.exports = Rails; + } else if (typeof define === "function" && define.amd) { + define(Rails); + } +}).call(this); diff --git a/package.json b/package.json new file mode 100644 index 00000000..592da830 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "rails-ujs", + "version": "0.0.1", + "description": "Unobtrusive JavaScript used in Rails", + "main": "dist/rails.js", + "directories": { + "test": "test" + }, + "scripts": { + "lint": "coffeelint src && eslint test/public/test" + }, + "repository": { + "type": "git", + "url": "https://github.com/rails/jquery-ujs.git" + }, + "author": { + "name": "All contributors", + "url": "https://github.com/rails/jquery-ujs/graphs/contributors" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/rails/jquery-ujs/issues" + }, + "homepage": "https://github.com/rails/jquery-ujs#readme", + "devDependencies": { + "coffeelint": "^1.15.7", + "eslint": "^2.13.1" + } +} diff --git a/script/cibuild b/script/cibuild index 45c97099..53cdaa45 100755 --- a/script/cibuild +++ b/script/cibuild @@ -8,7 +8,7 @@ start_server() { } run_tests() { - jshint src/*.js && phantomjs script/runner.js http://localhost:$port/ + npm run lint && phantomjs script/runner.js http://localhost:$port/ } server_started() { diff --git a/src/config.coffee b/src/config.coffee new file mode 100644 index 00000000..3d4706b0 --- /dev/null +++ b/src/config.coffee @@ -0,0 +1,37 @@ +#= export Rails + +@Rails = + # Link elements bound by jquery-ujs + 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: + selector: 'button[data-remote]:not([form]), button[data-confirm]:not([form])' + exclude: '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' + + # 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])' + + # Form input elements disabled during form submission + formDisableSelector: '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' + + # Form input elements re-enabled after form submission + formEnableSelector: '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[name][type=file]:not([disabled])' + + # Link onClick disable selector with possible reenable after remote submission + 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]' diff --git a/src/features/confirm.coffee b/src/features/confirm.coffee new file mode 100644 index 00000000..01ece1a1 --- /dev/null +++ b/src/features/confirm.coffee @@ -0,0 +1,26 @@ +#= require_tree ../utils + +{ fire, stopEverything } = Rails + +Rails.handleConfirm = (e) -> + stopEverything(e) unless allowAction(e.target) + +# For 'data-confirm' attribute: +# - Fires `confirm` event +# - Shows the confirmation dialog +# - Fires the `confirm:complete` event +# +# Returns `true` if no function stops the chain and user chose yes `false` otherwise. +# Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog. +# Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function +# return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog. +allowAction = (element) -> + message = element.getAttribute('data-confirm') + return true unless message + + answer = false + if fire(element, 'confirm') + try answer = confirm(message) + callback = fire(element, 'confirm:complete', [answer]) + + answer and callback diff --git a/src/features/disable.coffee b/src/features/disable.coffee new file mode 100644 index 00000000..e8cce7da --- /dev/null +++ b/src/features/disable.coffee @@ -0,0 +1,78 @@ +#= require_tree ../utils + +{ matches, getData, setData, stopEverything, formElements } = Rails + +# Unified function to enable an element (link, button and form) +Rails.enableElement = (e) -> + element = if e instanceof Event then e.target else e + if matches(element, Rails.linkDisableSelector) + enableLinkElement(element) + else if matches(element, Rails.buttonDisableSelector) or matches(element, Rails.formEnableSelector) + enableFormElement(element) + else if matches(element, Rails.formSubmitSelector) + enableFormElements(element) + +# Unified function to disable an element (link, button and form) +Rails.disableElement = (e) -> + element = if e instanceof Event then e.target else e + if matches(element, Rails.linkDisableSelector) + disableLinkElement(element) + else if matches(element, Rails.buttonDisableSelector) or matches(element, Rails.formDisableSelector) + disableFormElement(element) + else if matches(element, Rails.formSubmitSelector) + disableFormElements(element) + +# Replace element's html with the 'data-disable-with' after storing original html +# and prevent clicking on it +disableLinkElement = (element) -> + replacement = element.getAttribute('data-disable-with') + if replacement? + setData(element, 'ujs:enable-with', element.innerHTML) # store enabled state + element.innerHTML = replacement + element.addEventListener('click', stopEverything) # prevent further clicking + setData(element, 'ujs:disabled', true) + +# Restore element to its original state which was disabled by 'disableLinkElement' above +enableLinkElement = (element) -> + originalText = getData(element, 'ujs:enable-with') + if originalText? + element.innerHTML = originalText # set to old enabled state + setData(element, 'ujs:enable-with', null) # clean up cache + element.removeEventListener('click', stopEverything) # enable element + setData(element, 'ujs:disabled', null) + +# 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 = (form) -> + formElements(form, Rails.formDisableSelector).forEach(disableFormElement) + +disableFormElement = (element) -> + replacement = element.getAttribute('data-disable-with') + if replacement? + if matches(element, 'button') + setData(element, 'ujs:enable-with', element.innerHTML) + element.innerHTML = replacement + else + setData(element, 'ujs:enable-with', element.value) + element.value = replacement + element.disabled = true + setData(element, 'ujs: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 = (form) -> + formElements(form, Rails.formEnableSelector).forEach(enableFormElement) + +enableFormElement = (element) -> + originalText = getData(element, 'ujs:enable-with') + if originalText? + if matches(element, 'button') + element.innerHTML = originalText + else + element.value = originalText + setData(element, 'ujs:enable-with', null) # clean up cache + element.disabled = false + setData(element, 'ujs:disabled', null) diff --git a/src/features/method.coffee b/src/features/method.coffee new file mode 100644 index 00000000..60f777c9 --- /dev/null +++ b/src/features/method.coffee @@ -0,0 +1,34 @@ +#= require_tree ../utils + +{ stopEverything } = Rails + +# Handles "data-method" on links such as: +# Delete +Rails.handleMethod = (e) -> + link = e.target + method = link.getAttribute('data-method') + return unless method + + href = Rails.href(link) + csrfToken = Rails.csrfToken() + csrfParam = Rails.csrfParam() + form = document.createElement('form') + formContent = "" + + if csrfParam? and csrfToken? and not Rails.isCrossDomain(href) + formContent += "" + + # Must trigger submit by click on a button, else "submit" event handler won't work! + # https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit + formContent += '' + + form.method = 'post' + form.action = href + form.target = link.target + form.innerHTML = formContent + form.style.display = 'none' + + document.body.appendChild(form) + form.querySelector('[type="submit"]').click() + + stopEverything(e) diff --git a/src/features/remote.coffee b/src/features/remote.coffee new file mode 100644 index 00000000..76530f48 --- /dev/null +++ b/src/features/remote.coffee @@ -0,0 +1,100 @@ +#= require_tree ../utils + +{ + matches, getData, setData + fire, stopEverything + ajax, isCrossDomain + blankInputs, serializeElement +} = Rails + +# Checks "data-remote" if true to handle the request through a XHR request. +isRemote = (element) -> + value = element.getAttribute('data-remote') + value? and value isnt 'false' + +# Submits "remote" forms and links with ajax +Rails.handleRemote = (e) -> + element = e.target + + return true unless isRemote(element) + unless fire(element, 'ajax:before') + fire(element, 'ajax:stopped') + return false + + withCredentials = element.getAttribute('data-with-credentials') + dataType = element.getAttribute('data-type') or 'script' + + if matches(element, Rails.formSubmitSelector) + # memoized value from clicked submit button + button = getData(element, 'ujs:submit-button') + method = getData(element, 'ujs:submit-button-formmethod') or element.method + url = getData(element, 'ujs:submit-button-formaction') or element.getAttribute('action') or location.href + + # strip query string if it's a GET request + url = url.replace(/\?.*$/, '') if method.toUpperCase() is 'GET' + + if element.enctype is 'multipart/form-data' + data = new FormData(element) + data.append(button.name, button.value) if button? + else + data = serializeElement(element, button) + + setData(element, 'ujs:submit-button', null) + setData(element, 'ujs:submit-button-formmethod', null) + setData(element, 'ujs:submit-button-formaction', null) + else if matches(element, Rails.buttonClickSelector) or matches(element, Rails.inputChangeSelector) + method = element.getAttribute('data-method') + url = element.getAttribute('data-url') + data = serializeElement(element, element.getAttribute('data-params')) + else + method = element.getAttribute('data-method') + url = Rails.href(element) + data = element.getAttribute('data-params') + + ajax( + type: method or 'GET' + url: url + data: data + dataType: dataType + # stopping the "ajax:beforeSend" event will cancel the ajax request + beforeSend: (xhr, options) -> + if fire(element, 'ajax:beforeSend', [xhr, options]) + fire(element, 'ajax:send', [xhr]) + else + fire(element, 'ajax:stopped') + xhr.abort() + success: (args...) -> fire(element, 'ajax:success', args) + error: (args...) -> fire(element, 'ajax:error', args) + complete: (args...) -> fire(element, 'ajax:complete', args) + crossDomain: isCrossDomain(url) + withCredentials: withCredentials? and withCredentials isnt 'false' + ) + stopEverything(e) + +# Check whether any required fields are empty +# In both ajax mode and normal mode +Rails.validateForm = (e) -> + form = e.target + return if form.noValidate or getData(form, 'ujs:formnovalidate-button') + # Skip other logic when required values are missing or file upload is present + blankRequiredInputs = blankInputs(form, Rails.requiredInputSelector, false) + if blankRequiredInputs.length > 0 and fire(form, 'ajax:aborted:required', [blankRequiredInputs]) + stopEverything(e) + +Rails.formSubmitButtonClick = (e) -> + button = e.target + form = button.form + return unless form + # Register the pressed submit button + setData(form, 'ujs:submit-button', name: button.name, value: button.value) if button.name + # Save attributes from button + setData(form, 'ujs:formnovalidate-button', button.formNoValidate) + setData(form, 'ujs:submit-button-formaction', button.getAttribute('formaction')) + setData(form, 'ujs:submit-button-formmethod', button.getAttribute('formmethod')) + +Rails.handleMetaClick = (e) -> + link = e.target + method = (link.getAttribute('data-method') or 'GET').toUpperCase() + data = link.getAttribute('data-params') + metaClick = e.metaKey or e.ctrlKey + e.stopImmediatePropagation() if metaClick and method is 'GET' and not data diff --git a/src/rails.coffee b/src/rails.coffee new file mode 100644 index 00000000..471cb73c --- /dev/null +++ b/src/rails.coffee @@ -0,0 +1,76 @@ +# +# Unobtrusive JavaScript +# https://github.com/rails/jquery-ujs +# +# Released under the MIT license +# +#= require ./config +#= require_tree ./utils +#= require_tree ./features + +{ + fire, delegate + getData, $ + refreshCSRFTokens, CSRFProtection + enableElement, disableElement + handleConfirm + handleRemote, validateForm, formSubmitButtonClick, handleMetaClick + handleMethod +} = Rails + +# For backward compatibility +if jQuery? and not jQuery.rails + jQuery.rails = Rails + jQuery.ajaxPrefilter (options, originalOptions, xhr) -> + CSRFProtection(xhr) unless options.crossDomain + +Rails.start = -> + # Cut down on the number of issues from people inadvertently including jquery_ujs twice + # by detecting and raising an error when it happens. + throw new Error('jquery-ujs has already been loaded!') if window._rails_loaded + + # This event works the same as the load event, except that it fires every + # time the page is loaded. + # See https://github.com/rails/jquery-ujs/issues/357 + # See https://developer.mozilla.org/en-US/docs/Using_Firefox_1.5_caching + window.addEventListener 'pageshow', -> + $(Rails.formEnableSelector).forEach (el) -> + enableElement(el) if getData(el, 'ujs:disabled') + $(Rails.linkDisableSelector).forEach (el) -> + enableElement(el) if getData(el, 'ujs:disabled') + + delegate document, Rails.linkDisableSelector, 'ajax:complete', enableElement + delegate document, Rails.linkDisableSelector, 'ajax:stopped', enableElement + delegate document, Rails.buttonDisableSelector, 'ajax:complete', enableElement + delegate document, Rails.buttonDisableSelector, 'ajax:stopped', enableElement + + delegate document, Rails.linkClickSelector, 'click', handleConfirm + delegate document, Rails.linkClickSelector, 'click', handleMetaClick + delegate document, Rails.linkClickSelector, 'click', disableElement + delegate document, Rails.linkClickSelector, 'click', handleRemote + delegate document, Rails.linkClickSelector, 'click', handleMethod + + delegate document, Rails.buttonClickSelector, 'click', handleConfirm + delegate document, Rails.buttonClickSelector, 'click', disableElement + delegate document, Rails.buttonClickSelector, 'click', handleRemote + + delegate document, Rails.inputChangeSelector, 'change', handleConfirm + delegate document, Rails.inputChangeSelector, 'change', handleRemote + + delegate document, Rails.formSubmitSelector, 'submit', handleConfirm + delegate document, Rails.formSubmitSelector, 'submit', validateForm + delegate document, Rails.formSubmitSelector, 'submit', handleRemote + # Normal mode submit + # Slight timeout so that the submit button gets properly serialized + delegate document, Rails.formSubmitSelector, 'submit', (e) -> setTimeout((-> disableElement(e)), 13) + delegate document, Rails.formSubmitSelector, 'ajax:send', disableElement + delegate document, Rails.formSubmitSelector, 'ajax:complete', enableElement + + delegate document, Rails.formInputClickSelector, 'click', handleConfirm + delegate document, Rails.formInputClickSelector, 'click', formSubmitButtonClick + + document.addEventListener('DOMContentLoaded', refreshCSRFTokens) + window._rails_loaded = true + +if window.Rails is Rails and fire(document, 'rails:attachBindings') + Rails.start() diff --git a/src/rails.js b/src/rails.js deleted file mode 100644 index 9e192a2e..00000000 --- a/src/rails.js +++ /dev/null @@ -1,555 +0,0 @@ -(function($, undefined) { - -/** - * Unobtrusive scripting adapter for jQuery - * https://github.com/rails/jquery-ujs - * - * Requires jQuery 1.8.0 or later. - * - * Released under the MIT license - * - */ - - // Cut down on the number of issues from people inadvertently including jquery_ujs twice - // by detecting and raising an error when it happens. - 'use strict'; - - 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]:not([disabled]), a[data-disable-with], a[data-disable]', - - // Button elements bound by jquery-ujs - 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', - - // 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])', - - // 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', - - // Form input elements re-enabled after form submission - 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[name][type=file]:not([disabled])', - - // Link onClick disable selector with possible reenable after remote submission - 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]', - - // 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 = rails.csrfToken(); - if (token) xhr.setRequestHeader('X-CSRF-Token', token); - }, - - // Make sure that all forms have actual up-to-date tokens (cached forms contain old ones) - refreshCSRFTokens: function(){ - $('form input[name="' + rails.csrfParam() + '"]').val(rails.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); - obj.trigger(event, data); - return event.result !== false; - }, - - // Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm - confirm: function(message) { - return confirm(message); - }, - - // Default ajax function, may be overridden with custom function in $.rails.ajax - ajax: function(options) { - return $.ajax(options); - }, - - // Default way to get an element's href. May be overridden at $.rails.href. - href: function(element) { - 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, withCredentials, dataType, options; - - if (rails.fire(element, 'ajax:before')) { - withCredentials = element.data('with-credentials') || null; - dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType); - - if (element.is('form')) { - 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'); - } 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 = rails.href(element); - data = element.data('params') || null; - } - - options = { - 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); - } - 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]); - }, - complete: function(xhr, status) { - element.trigger('ajax:complete', [xhr, status]); - }, - error: function(xhr, status, error) { - element.trigger('ajax:error', [xhr, status, error]); - }, - crossDomain: rails.isCrossDomain(url) - }; - - // 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; } - - return rails.ajax(options); - } else { - return false; - } - }, - - // 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 = rails.csrfToken(), - csrfParam = rails.csrfParam(), - form = $('
'), - metadataInput = ''; - - if (csrfParam !== undefined && csrfToken !== undefined && !rails.isCrossDomain(href)) { - metadataInput += ''; - } - - if (target) { form.attr('target', target); } - - 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) { - 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'); - - 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: - - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`) - - Sets disabled property to false - */ - enableFormElements: function(form) { - 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') !== 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: - - Fires `confirm` event - - Shows the confirmation dialog - - Fires the `confirm:complete` event - - Returns `true` if no function stops the chain and user chose yes; `false` otherwise. - Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog. - Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function - return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog. - */ - allowAction: function(element) { - var message = element.data('confirm'), - answer = false, callback; - if (!message) { return true; } - - if (rails.fire(element, 'confirm')) { - 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; - }, - - // Helper function which checks for blank inputs in a form that match the specified CSS selector - blankInputs: function(form, specifiedSelector, nonBlank) { - var foundInputs = $(), - input, - valueToCheck, - radiosForNameWithNoneSelected, - radioName, - selector = specifiedSelector || 'input,textarea', - requiredInputs = form.find(selector), - checkedRadioButtonNames = {}; - - requiredInputs.each(function() { - input = $(this); - if (input.is('input[type=radio]')) { - - // 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); - } - - // 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 foundInputs.length ? foundInputs : false; - }, - - // Helper function which checks for non-blank inputs in a form that match the specified CSS selector - nonBlankInputs: function(form, specifiedSelector) { - return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank - }, - - // Helper function, needed to provide consistent behavior in IE - stopEverything: function(e) { - $(e.target).trigger('ujs:everythingStopped'); - e.stopImmediatePropagation(); - return false; - }, - - // 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'); - - 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 - return rails.stopEverything(e); - }); - element.data('ujs:disabled', true); - }, - - // 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.removeData('ujs:disabled'); - } - }; - - if (rails.fire($document, 'rails:attachBindings')) { - - $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }}); - - // This event works the same as the load event, except that it fires every - // time the page is loaded. - // - // 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 () { - $($.rails.enableSelector).each(function () { - var element = $(this); - - if (element.data('ujs:disabled')) { - $.rails.enableFormElement(element); - } - }); - - $($.rails.linkDisableSelector).each(function () { - var element = $(this); - - if (element.data('ujs:disabled')) { - $.rails.enableElement(element); - } - }); - }); - - $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'), metaClick = e.metaKey || e.ctrlKey; - if (!rails.allowAction(link)) return rails.stopEverything(e); - - if (!metaClick && link.is(rails.linkDisableSelector)) rails.disableElement(link); - - 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. - if (handleRemote === false) { - rails.enableElement(link); - } else { - handleRemote.fail( function() { rails.enableElement(link); } ); - } - return false; - - } else if (method) { - rails.handleMethod(link); - return false; - } - }); - - $document.delegate(rails.buttonClickSelector, 'click.rails', function(e) { - var button = $(this); - - 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. - if (handleRemote === false) { - rails.enableFormElement(button); - } else { - handleRemote.fail( function() { rails.enableFormElement(button); } ); - } - return false; - }); - - $document.delegate(rails.inputChangeSelector, 'change.rails', function(e) { - var link = $(this); - if (!rails.allowAction(link) || !rails.isRemote(link)) return rails.stopEverything(e); - - rails.handleRemote(link); - return false; - }); - - $document.delegate(rails.formSubmitSelector, 'submit.rails', function(e) { - var form = $(this), - 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) { - 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 - // (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) - if (!aborted) { setTimeout(function(){ rails.enableFormElements(form); }, 13); } - - return aborted; - } - - rails.handleRemote(form); - return false; - - } 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); - - if (!rails.allowAction(button)) return rails.stopEverything(event); - - // Register the pressed submit button - var name = button.attr('name'), - data = name ? {name:name, value:button.val()} : null; - - var 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.delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) { - if (this === event.target) rails.enableFormElements($(this)); - }); - - $(function(){ - rails.refreshCSRFTokens(); - }); - } - -})( jQuery ); diff --git a/src/utils/ajax.coffee b/src/utils/ajax.coffee new file mode 100644 index 00000000..9af515be --- /dev/null +++ b/src/utils/ajax.coffee @@ -0,0 +1,95 @@ +#= require ./csrf +#= require ./event + +{ CSRFProtection, fire } = Rails + +AcceptHeaders = + '*': '*/*' + text: 'text/plain' + html: 'text/html' + xml: 'application/xml, text/xml' + json: 'application/json, text/javascript' + script: 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript' + +Rails.ajax = (options) -> + options = prepareOptions(options) + xhr = createXHR options, -> + response = processResponse(xhr.response, xhr.getResponseHeader('Content-Type')) + if xhr.status // 100 == 2 + options.success?(response, xhr.statusText, xhr) + else + options.error?(response, xhr.statusText, xhr) + options.complete?(xhr, xhr.statusText) + # Call beforeSend hook + options.beforeSend?(xhr, options) + # Send the request + if xhr.readyState is XMLHttpRequest.OPENED + xhr.send(options.data) + else + fire(document, 'ajaxStop') # to be compatible with jQuery.ajax + +prepareOptions = (options) -> + options.type = options.type.toUpperCase() + # append data to url if it's a GET request + if options.type is 'GET' and options.data + if options.url.indexOf('?') < 0 + options.url += '?' + options.data + else + options.url += '&' + options.data + # Use "*" as default dataType + options.dataType = '*' unless AcceptHeaders[options.dataType]? + options.accept = AcceptHeaders[options.dataType] + options.accept += ', */*; q=0.01' if options.dataType isnt '*' + options + +createXHR = (options, done) -> + xhr = new XMLHttpRequest() + # Open and setup xhr + xhr.open(options.type, options.url, true) + xhr.setRequestHeader('Accept', options.accept) + # Set Content-Type only when sending a string + # Sending FormData will automatically set Content-Type to multipart/form-data + if typeof options.data is 'string' + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8') + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest') unless options.crossDomain + # Add X-CSRF-Token + CSRFProtection(xhr) + xhr.withCredentials = !!options.withCredentials + xhr.onreadystatechange = -> + done(xhr) if xhr.readyState is XMLHttpRequest.DONE + xhr + +processResponse = (response, type) -> + if typeof response is 'string' and typeof type is 'string' + if type.match(/\bjson\b/) + try response = JSON.parse(response) + else if type.match(/\bjavascript\b/) + script = document.createElement('script') + script.innerHTML = response + document.body.appendChild(script) + else if type.match(/\b(xml|html|svg)\b/) + parser = new DOMParser() + type = type.replace(/;.+/, '') # remove something like ';charset=utf-8' + try response = parser.parseFromString(response, type) + response + +# Default way to get an element's href. May be overridden at Rails.href. +Rails.href = (element) -> element.href + +# Determines if the request is a cross domain request. +Rails.isCrossDomain = (url) -> + originAnchor = document.createElement('a') + originAnchor.href = location.href + urlAnchor = document.createElement('a') + try + urlAnchor.href = url + # 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. + !(((!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. + true diff --git a/src/utils/csrf.coffee b/src/utils/csrf.coffee new file mode 100644 index 00000000..4eb5ebb4 --- /dev/null +++ b/src/utils/csrf.coffee @@ -0,0 +1,25 @@ +#= require ./dom + +{ $ } = Rails + +# Up-to-date Cross-Site Request Forgery token +csrfToken = Rails.csrfToken = -> + meta = document.querySelector('meta[name=csrf-token]') + meta and meta.content + +# URL param that must contain the CSRF token +csrfParam = Rails.csrfParam = -> + meta = document.querySelector('meta[name=csrf-param]') + meta and meta.content + +# Make sure that every Ajax request sends the CSRF token +Rails.CSRFProtection = (xhr) -> + token = csrfToken() + xhr.setRequestHeader('X-CSRF-Token', token) if token? + +# Make sure that all forms have actual up-to-date tokens (cached forms contain old ones) +Rails.refreshCSRFTokens = -> + token = csrfToken() + param = csrfParam() + if token? and param? + $('form input[name="' + param + '"]').forEach (input) -> input.value = token diff --git a/src/utils/dom.coffee b/src/utils/dom.coffee new file mode 100644 index 00000000..6bef6181 --- /dev/null +++ b/src/utils/dom.coffee @@ -0,0 +1,28 @@ +m = Element.prototype.matches or + Element.prototype.matchesSelector or + Element.prototype.mozMatchesSelector or + Element.prototype.msMatchesSelector or + Element.prototype.oMatchesSelector or + Element.prototype.webkitMatchesSelector + +Rails.matches = (element, selector) -> + if selector.exclude? + m.call(element, selector.selector) and not m.call(element, selector.exclude) + else + m.call(element, selector) + +# get and set data on a given element using "expando properties" +# See: https://developer.mozilla.org/en-US/docs/Glossary/Expando +expando = '_ujsData' + +Rails.getData = (element, key) -> + element[expando]?[key] + +Rails.setData = (element, key, value) -> + element[expando] ?= {} + element[expando][key] = value + +# a wrapper for document.querySelectorAll +# returns an Array +Rails.$ = (selector) -> + Array.prototype.slice.call(document.querySelectorAll(selector)) diff --git a/src/utils/event.coffee b/src/utils/event.coffee new file mode 100644 index 00000000..35e29986 --- /dev/null +++ b/src/utils/event.coffee @@ -0,0 +1,39 @@ +#= require ./dom + +{ matches } = Rails + +# Polyfill for CustomEvent in IE9+ +# https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill +CustomEvent = window.CustomEvent + +if typeof CustomEvent is 'function' + CustomEvent = (event, params) -> + evt = document.createEvent('CustomEvent') + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail) + evt + CustomEvent.prototype = window.Event.prototype + +# Triggers an custom event on an element and returns false if the event result is false +fire = Rails.fire = (obj, name, data) -> + event = new CustomEvent( + name, + bubbles: true, + cancelable: true, + detail: data, + ) + obj.dispatchEvent(event) + !event.defaultPrevented + +# Helper function, needed to provide consistent behavior in IE +Rails.stopEverything = (e) -> + fire(e.target, 'ujs:everythingStopped') + e.preventDefault() + e.stopPropagation() + e.stopImmediatePropagation() + +Rails.delegate = (element, selector, eventType, handler) -> + element.addEventListener eventType, (e) -> + if matches(e.target, selector) + if handler.call(e.target, e) == false + e.preventDefault() + e.stopPropagation() diff --git a/src/utils/form.coffee b/src/utils/form.coffee new file mode 100644 index 00000000..251113de --- /dev/null +++ b/src/utils/form.coffee @@ -0,0 +1,61 @@ +#= require ./dom + +{ matches } = Rails + +toArray = (e) -> Array.prototype.slice.call(e) + +Rails.serializeElement = (element, additionalParam) -> + inputs = [element] + inputs = toArray(element.elements) if matches(element, 'form') + params = [] + + inputs.forEach (input) -> + return unless input.name + if matches(input, 'select') + toArray(input.options).forEach (option) -> + params.push(name: input.name, value: option.value) if option.selected + else if input.type isnt 'radio' and input.type isnt 'checkbox' or input.checked + params.push(name: input.name, value: input.value) + + params.push(additionalParam) if additionalParam + + params.map (param) -> + if param.name? + "#{encodeURIComponent(param.name)}=#{encodeURIComponent(param.value)}" + else + param + .join('&') + +# 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 +Rails.formElements = (form, selector) -> + if matches(form, 'form') + toArray(form.elements).filter (el) -> matches(el, selector) + else + toArray(form.querySelectorAll(selector)) + +# Helper function which checks for blank inputs in a form that match the specified CSS selector +Rails.blankInputs = (form, selector, nonBlank) -> + foundInputs = [] + requiredInputs = toArray(form.querySelectorAll(selector or 'input, textarea')) + checkedRadioButtonNames = {} + + requiredInputs.forEach (input) -> + if input.type is 'radio' + # 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.name + # Skip if we've already seen the radio with this name. + unless checkedRadioButtonNames[radioName] + # If none checked + if form.querySelectorAll("input[type=radio][name='#{radioName}']:checked").length == 0 + radios = form.querySelectorAll("input[type=radio][name='#{radioName}']") + foundInputs = foundInputs.concat(toArray(radios)) + # We only need to check each name once. + checkedRadioButtonNames[radioName] = radioName + else + valueToCheck = if input.type is 'checkbox' then input.checked else !!input.value + foundInputs.push(input) if valueToCheck is nonBlank + foundInputs diff --git a/test/public/test/.eslintrc.yml b/test/public/test/.eslintrc.yml new file mode 100644 index 00000000..06d7dd36 --- /dev/null +++ b/test/public/test/.eslintrc.yml @@ -0,0 +1,21 @@ +env: + browser: true +extends: eslint:recommended +rules: + no-undef: off + no-unused-vars: off + indent: off + linebreak-style: ['error', 'unix'] + quotes: ['error', 'single'] + semi: ['error', 'never'] + no-shadow: ['error'] # Prevent potential errors + no-console: 'off' + # styles + space-before-function-paren: ['error', 'never'] + space-before-blocks: 'error' + brace-style: ['error', '1tbs', { allowSingleLine: true }] + key-spacing: 'error' + array-bracket-spacing: 'error' + comma-spacing: 'error' + comma-dangle: 'off' + eol-last: 'error' diff --git a/test/public/test/call-remote-callbacks.js b/test/public/test/call-remote-callbacks.js index e7ee0dab..082d10bf 100644 --- a/test/public/test/call-remote-callbacks.js +++ b/test/public/test/call-remote-callbacks.js @@ -1,284 +1,288 @@ -(function(){ +(function() { module('call-remote-callbacks', { setup: function() { $('#qunit-fixture').append($('
', { action: '/echo', method: 'get', 'data-remote': 'true' - })); + })) }, 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).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') } -}); +}) function start_after_submit(form) { - form.bind('ajax:complete', function() { - ok(true, 'ajax:complete'); - start(); - }); + form.bindNative('ajax:complete', function() { + ok(true, 'ajax:complete') + start() + }) } function submit(fn) { - var form = $('form'); - start_after_submit(form); + var form = $('form') + start_after_submit(form) - if (fn) fn(form); - form.trigger('submit'); + if (fn) fn(form) + form.triggerNative('submit') } function submit_with_button(submit_button) { - var form = $('form'); - start_after_submit(form); + var form = $('form') + start_after_submit(form) - submit_button.trigger('click'); + submit_button.triggerNative('click') } -asyncTest('modifying form fields with "ajax:before" sends modified data in request', 4, function(){ +asyncTest('modifying form fields with "ajax:before" sends modified data in request', 4, function() { $('form[data-remote]') .append($('')) .append($('')) - .bind('ajax:before', function() { - var form = $(this); + .bindNative('ajax:before', function() { + var form = $(this) form - .append($('',{name: 'other_user_name',value: 'jonathan'})) - .find('input[name="removed_user_name"]').remove(); + .append($('', {name: 'other_user_name', value: 'jonathan'})) + .find('input[name="removed_user_name"]').remove() form - .find('input[name="user_name"]').val('steve'); - }); + .find('input[name="user_name"]').val('steve') + }) submit(function(form) { - form.bind('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'); - }); - }); -}); - -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'); - }); + form.bindNative('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') + }) + }) +}) + +asyncTest('modifying data("type") with "ajax:before" requests new dataType in request', 2, function() { + $('form[data-remote]').data('type', 'html') + .bindNative('ajax:before', function() { + this.setAttribute('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("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); - }); + form.bindNative('ajax:beforeSend', function(e, xhr, settings) { + equal(settings.dataType, 'xml', 'modified dataType should have been requested') + }) + }) +}) + +asyncTest('setting data("with-credentials",true) with "ajax:before" uses new setting in request', 2, function() { + $('form[data-remote]').data('with-credentials', false) + .bindNative('ajax:before', function() { + this.setAttribute('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'); - }); - }); -}); + form.bindNative('ajax:beforeSend', function(e, xhr, settings) { + equal(settings.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'); - 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'); - }); - $(document).bind('ajaxStop', function() { - start(); - }); - }); -}); + form.bindNative('ajax:beforeSend', function() { + ok(true, 'aborting request in ajax:beforeSend') + return false + }) + form.unbind('ajax:send').bindNative('ajax:send', function() { + ok(false, 'ajax:send should not run') + }) + form.unbind('ajax:complete').bindNative('ajax:complete', function() { + ok(false, 'ajax:complete should not run') + }) + form.bindNative('ajax:error', function(e, xhr, status, error) { + ok(false, 'ajax:error should not run') + }) + $(document).bindNative('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'); - }); + 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'); + .bindNative('ajax:beforeSend', function() { + ok(false, 'ajax:beforeSend should not run') }) - .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'); - ok(data.last().is('textarea[name="user_bio"]'), 'ajax:aborted:required adds blank required textarea to data'); - ok(true, 'ajax:aborted:required should run'); + .bindNative('ajax:aborted:required', function(e, data) { + data = $(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') + ok(true, 'ajax:aborted:required should run') }) - .trigger('submit'); + .triggerNative('submit') setTimeout(function() { - form.find('input[required],textarea[required]').val('Tyler'); - form.unbind('ajax:beforeSend'); - submit(); - }, 13); -}); + form.find('input[required],textarea[required]').val('Tyler') + form.unbind('ajax:beforeSend') + submit() + }, 13) +}) asyncTest('blank required form input for non-remote form should abort normal submission', 1, function() { var form = $('form[data-remote]') .append($('')) .removeAttr('data-remote') - .bind('ujs:everythingStopped', function() { - ok(true, 'ujs:everythingStopped should run'); + .bindNative('ujs:everythingStopped', function() { + ok(true, 'ujs:everythingStopped should run') }) - .trigger('submit'); + .triggerNative('submit') setTimeout(function() { - start(); - }, 13); -}); + start() + }, 13) +}) -asyncTest('form should be submitted with blank required fields if handler is bound to "ajax:aborted:required" event that returns false', 1, function(){ +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() { - ok(true, 'ajax:beforeSend should run'); + .bindNative('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run') }) - .bind('ajax:aborted:required', function() { - return false; + .bindNative('ajax:aborted:required', function() { + return false }) - .trigger('submit'); + .triggerNative('submit') setTimeout(function() { - start(); - }, 13); -}); + start() + }, 13) +}) asyncTest('disabled fields should not be included in blank required check', 2, function() { var form = $('form[data-remote]') .append($('')) .append($('')) - .bind('ajax:beforeSend', function() { - ok(true, 'ajax:beforeSend should run'); + .bindNative('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run') + }) + .bindNative('ajax:aborted:required', function() { + ok(false, 'ajax:aborted:required should not run') }) - .bind('ajax:aborted:required', function() { - ok(false, 'ajax:aborted:required should not run'); - }); - submit(); -}); + submit() +}) -asyncTest('form should be submitted with blank required fields if it has the "novalidate" attribute', 2, function(){ +asyncTest('form should be submitted with blank required fields if it has the "novalidate" attribute', 2, function() { var form = $('form[data-remote]') .append($('')) - .attr("novalidate", "novalidate") - .bind('ajax:beforeSend', function() { - ok(true, 'ajax:beforeSend should run'); + .attr('novalidate', 'novalidate') + .bindNative('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run') + }) + .bindNative('ajax:aborted:required', function() { + ok(false, 'ajax:aborted:required should not run') }) - .bind('ajax:aborted:required', function() { - ok(false, 'ajax:aborted:required should not run'); - }); - submit(); -}); + submit() +}) -asyncTest('form should be submitted with blank required fields if the button has the "formnovalidate" attribute', 2, function(){ - var submit_button = $(''); +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) - .bind('ajax:beforeSend', function() { - ok(true, 'ajax:beforeSend should run'); + .bindNative('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run') + }) + .bindNative('ajax:aborted:required', function() { + ok(false, 'ajax:aborted:required should not run') }) - .bind('ajax:aborted:required', function() { - ok(false, 'ajax:aborted:required should not run'); - }); - submit_with_button(submit_button); -}); + 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() { - ok(true, 'form should get submitted'); - }); + ok(true, 'form should get submitted') + }) var form = $('form[data-remote]') .append($('')) .removeAttr('data-remote') - .attr("novalidate","novalidate") - .trigger('submit'); + .attr('novalidate', 'novalidate') + .triggerNative('submit') setTimeout(function() { - start(); - }, 13); -}); + 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'); + .bindNative('ujs:everythingStopped', function() { + ok(true, 'ujs:everythingStopped should run') }) - .trigger('submit'); + .triggerNative('submit') setTimeout(function() { - start(); - }, 13); -}); + start() + }, 13) +}) -asyncTest('unchecked required radio should abort form submission', 1, function() { +asyncTest('unchecked required radio should abort form submission', 3, function() { var form = $('form[data-remote]') .append($('')) .append($('')) .removeAttr('data-remote') - .bind('ujs:everythingStopped', function() { - ok(true, 'ujs:everythingStopped should run'); + .bindNative('ujs:everythingStopped', function() { + ok(true, 'ujs:everythingStopped should run') + }) + .bindNative('ajax:aborted:required', function(e, data) { + data = $(data) + equal(data.length, 2, 'blankRequiredInputs should include both radios') + ok(data.first().is('input[type=radio][value=1]'), 'blankRequiredInputs[0] should be the first radio') }) - .trigger('submit'); + .triggerNative('submit') setTimeout(function() { - start(); - }, 13); -}); + start() + }, 13) +}) asyncTest('required radio should only require one to be checked', 1, function() { $(document).bind('iframe:loading', function() { - ok(true, 'form should get submitted'); - }); + 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'); + .bindNative('ujs:everythingStopped', function() { + ok(false, 'ujs:everythingStopped should not run') }) .find('#checkme').prop('checked', true) .end() - .trigger('submit'); + .triggerNative('submit') setTimeout(function() { - start(); - }, 13); -}); + start() + }, 13) +}) asyncTest('required radio should only require one to be checked if not all radios are required', 1, function() { $(document).bind('iframe:loading', function() { - ok(true, 'form should get submitted'); - }); + ok(true, 'form should get submitted') + }) var form = $('form[data-remote]') // Check the radio that is not required @@ -288,17 +292,17 @@ asyncTest('required radio should only require one to be checked if not all radio // Only one needs to be required .append($('')) .removeAttr('data-remote') - .bind('ujs:everythingStopped', function() { - ok(false, 'ujs:everythingStopped should not run'); + .bindNative('ujs:everythingStopped', function() { + ok(false, 'ujs:everythingStopped should not run') }) .find('#checkme').prop('checked', true) .end() - .trigger('submit'); + .triggerNative('submit') setTimeout(function() { - start(); - }, 13); -}); + start() + }, 13) +}) function skipIt() { // This test cannot work due to the security feature in browsers which makes the value @@ -307,159 +311,159 @@ 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() { - ok(false, 'ajax:beforeSend should not run'); + .bindNative('ajax:beforeSend', function() { + ok(false, 'ajax:beforeSend should not run') }) .bind('iframe:loading', function() { - ok(true, 'form should get submitted'); + 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'); + .bindNative('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'); + .triggerNative('submit') setTimeout(function() { - form.find('input[type="file"]').val(''); - form.unbind('ajax:beforeSend'); - submit(); - }, 13); - }); + form.find('input[type="file"]').val('') + form.unbind('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($('')) - .bind('ajax:beforeSend', function() { - ok(true, 'ajax:beforeSend should run'); + .bindNative('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run') }) .bind('iframe:loading', function() { - ok(true, 'form should get submitted'); + ok(true, 'form should get submitted') }) - .bind('ajax:aborted:file', function(e,data) { - ok(false, 'ajax:aborted:file should not run'); + .bindNative('ajax:aborted:file', function(e, data) { + ok(false, 'ajax:aborted:file should not run') }) - .trigger('submit'); + .triggerNative('submit') setTimeout(function() { - form.find('input[type="file"]').val(''); - form.unbind('ajax:beforeSend'); - submit(); - }, 13); - }); + 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'); + .bindNative('ajax:beforeSend', function() { + ok(false, 'ajax:beforeSend should not run') }) .bind('iframe:loading', function() { - ok(false, 'form should not get submitted'); + ok(false, 'form should not get submitted') }) - .bind('ajax:aborted:file', function() { - return false; + .bindNative('ajax:aborted:file', function() { + return false }) - .trigger('submit'); + .triggerNative('submit') setTimeout(function() { - form.find('input[type="file"]').val(''); - form.unbind('ajax:beforeSend'); - submit(); - }, 13); - }); + 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() { $(document).delegate('form[data-remote]', 'ajax:beforeSend', function() { - ok(true, 'ajax:beforeSend observed with event delegation'); - return false; - }); + 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'); - }); - $(document).bind('ajaxStop', function() { - start(); - }); - }); -}); + form.unbind('ajax:send').bindNative('ajax:send', function() { + ok(false, 'ajax:send should not run') + }) + form.unbind('ajax:complete').bindNative('ajax:complete', function() { + ok(false, 'ajax:complete should not run') + }) + $(document).bindNative('ajaxStop', function() { + start() + }) + }) +}) 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'); - ok(xhr.getResponseHeader, 'third argument to "ajax:success" should be an XHR object'); - }); - form.bind('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'); - }); - }); -}); + form.bindNative('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.bindNative('ajax:send', function(e, xhr) { + ok(xhr.abort, 'first argument to "ajax:send" should be an XHR object') + }) + form.bindNative('ajax:success', function(e, data, status, xhr) { + ok(data.REQUEST_METHOD, 'first argument to ajax:success should be a data object') + equal(status, 'OK', 'second argument to ajax:success should be a status string') + ok(xhr.getResponseHeader, 'third argument to "ajax:success" should be an XHR object') + }) + form.bindNative('ajax:complete', function(e, xhr, status) { + ok(xhr.getResponseHeader, 'first argument to "ajax:complete" should be an XHR object') + equal(status, 'OK', 'second argument to ajax:complete should be a status string') + }) + }) +}) 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) { - 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'); + form.attr('action', '/error') + form.bindNative('ajax:beforeSend', function(arg) { ok(true, 'ajax:beforeSend') }) + form.bindNative('ajax:send', function(arg) { ok(true, 'ajax:send') }) + form.bindNative('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 - equal($.trim(error), 'Forbidden', 'third argument to ajax:error should be an HTTP status response'); + 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'); - }); - }); - }); + equal(xhr.status, window.opera ? 0 : 403, 'status code should be 403') + }) + }) + }) } // 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() { $(document) .delegate('form[data-remote]', 'ajax:beforeSend', function() { - ok(true, 'ajax:beforeSend handler is triggered'); + ok(true, 'ajax:beforeSend handler is triggered') }) .delegate('form[data-remote]', 'ajax:send', function() { - ok(true, 'ajax:send handler is triggered'); + ok(true, 'ajax:send handler is triggered') }) .delegate('form[data-remote]', 'ajax:complete', function() { - ok(true, 'ajax:complete handler is triggered'); + ok(true, 'ajax:complete handler is triggered') }) .delegate('form[data-remote]', 'ajax:success', function() { - ok(true, 'ajax:success handler is triggered'); - }); - $('form[data-remote]').trigger('submit'); + ok(true, 'ajax:success handler is triggered') + }) + $('form[data-remote]').triggerNative('submit') setTimeout(function() { - start(); - }, 63); -}); + 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(); + .bindNative('ajax:send', function(e, xhr) { + ok(true, 'event should fire') + equal(typeof(xhr.abort), 'function', 'event should pass jqXHR object') + xhr.abort() }) - .trigger('submit'); + .triggerNative('submit') - setTimeout(function() { start(); }, 35); -}); + setTimeout(function() { start() }, 35) +}) -})(); +})() diff --git a/test/public/test/call-remote.js b/test/public/test/call-remote.js index 2a6e3a61..dbeb8ad8 100644 --- a/test/public/test/call-remote.js +++ b/test/public/test/call-remote.js @@ -1,188 +1,247 @@ -(function(){ +(function() { function buildForm(attrs) { - attrs = $.extend({ action: '/echo', 'data-remote': 'true' }, attrs); + attrs = $.extend({ action: '/echo', 'data-remote': 'true' }, attrs) $('#qunit-fixture').append($('', attrs)) - .find('form').append($('')); -}; + .find('form').append($('')) +} -module('call-remote'); +module('call-remote') function submit(fn) { $('form') - .bind('ajax:success', fn) - .bind('ajax:complete', function() { start() }) - .trigger('submit'); + .bindNative('ajax:success', fn) + .bindNative('ajax:complete', function() { start() }) + .triggerNative('submit') } asyncTest('form method is read from "method" and not from "data-method"', 1, function() { - buildForm({ method: 'post', 'data-method': 'get' }); + buildForm({ method: 'post', 'data-method': 'get' }) submit(function(e, data, status, xhr) { - App.assertPostRequest(data); - }); -}); + App.assertPostRequest(data) + }) +}) asyncTest('form method is not read from "data-method" attribute in case of missing "method"', 1, function() { - buildForm({ 'data-method': 'put' }); + buildForm({ 'data-method': 'put' }) submit(function(e, data, status, xhr) { - App.assertGetRequest(data); - }); -}); + App.assertGetRequest(data) + }) +}) asyncTest('form method is read from submit button "formmethod" if submit is triggered by that button', 1, function() { var submitButton = $('') - buildForm({ method: 'post' }); + buildForm({ method: 'post' }) $('#qunit-fixture').find('form').append(submitButton) - .bind('ajax:success', function(e, data, status, xhr) { - App.assertGetRequest(data); + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertGetRequest(data) }) - .bind('ajax:complete', function() { start() }); + .bindNative('ajax:complete', function() { start() }) - submitButton.trigger('click'); -}); + submitButton.triggerNative('click') +}) asyncTest('form default method is GET', 1, function() { - buildForm(); + buildForm() submit(function(e, data, status, xhr) { - App.assertGetRequest(data); - }); -}); + App.assertGetRequest(data) + }) +}) asyncTest('form url is picked up from "action"', 1, function() { - buildForm({ method: 'post' }); + buildForm({ method: 'post' }) submit(function(e, data, status, xhr) { - App.assertRequestPath(data, '/echo'); - }); -}); + App.assertRequestPath(data, '/echo') + }) +}) asyncTest('form url is read from "action" not "href"', 1, function() { - buildForm({ method: 'post', href: '/echo2' }); + buildForm({ method: 'post', href: '/echo2' }) submit(function(e, data, status, xhr) { - App.assertRequestPath(data, '/echo'); - }); -}); + App.assertRequestPath(data, '/echo') + }) +}) 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' }); + buildForm({ method: 'post', href: '/echo2' }) $('#qunit-fixture').find('form').append(submitButton) - .bind('ajax:success', function(e, data, status, xhr) { - App.assertRequestPath(data, '/echo'); + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertRequestPath(data, '/echo') }) - .bind('ajax:complete', function() { start() }); + .bindNative('ajax:complete', function() { start() }) - submitButton.trigger('click'); -}); + submitButton.triggerNative('click') +}) asyncTest('prefer JS, but accept any format', 1, function() { - buildForm({ method: 'post' }); + buildForm({ method: 'post' }) submit(function(e, data, status, xhr) { - var accept = data.HTTP_ACCEPT; - ok(accept.indexOf('*/*;q=0.5, text/javascript, application/javascript') === 0, 'Accept: ' + accept); - }); -}); + var accept = data.HTTP_ACCEPT + ok(accept.match(/text\/javascript.+\*\/\*/), 'Accept: ' + accept) + }) +}) + +asyncTest('JS code should be executed', 1, function() { + buildForm({ method: 'post', 'data-type': 'script' }) + + $('form').append('') + $('form').append('') + + submit() +}) + +asyncTest('XML document should be parsed', 1, function() { + buildForm({ method: 'post', 'data-type': 'html' }) + + $('form').append('') + $('form').append('') + + submit(function(e, data, status, xhr) { + ok(data instanceof Document, 'returned data should be an XML document') + }) +}) asyncTest('accept application/json if "data-type" is json', 1, function() { - buildForm({ 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'); - }); -}); + equal(data.HTTP_ACCEPT, 'application/json, text/javascript, */*; q=0.01') + }) +}) asyncTest('allow empty "data-remote" attribute', 1, function() { - var form = $('#qunit-fixture').append($('')).find('form'); + var form = $('#qunit-fixture').append($('')).find('form') submit(function() { - ok(true, 'form with empty "data-remote" attribute is also allowed'); - }); -}); + ok(true, 'form with empty "data-remote" attribute is also allowed') + }) +}) + +asyncTest('query string in form action should be stripped in a GET request in normal submit', 1, function() { + buildForm({ action: '/echo?param1=abc', 'data-remote': 'false' }) + + $(document).one('iframe:loaded', function(e, data) { + equal(data.params.param1, undefined, '"param1" should not be passed to server') + start() + }) + + $('#qunit-fixture form').triggerNative('submit') +}) + +asyncTest('query string in form action should be stripped in a GET request in ajax submit', 1, function() { + buildForm({ action: '/echo?param1=abc' }) + + submit(function(e, data, status, xhr) { + equal(data.params.param1, undefined, '"param1" should not be passed to server') + }) +}) + +asyncTest('query string in form action should not be stripped in a POST request in normal submit', 1, function() { + buildForm({ action: '/echo?param1=abc', method: 'post', 'data-remote': 'false' }) + + $(document).one('iframe:loaded', function(e, data) { + equal(data.params.param1, 'abc', '"param1" should be passed to server') + start() + }) + + $('#qunit-fixture form').triggerNative('submit') +}) + +asyncTest('query string in form action should not be stripped in a POST request in ajax submit', 1, function() { + buildForm({ action: '/echo?param1=abc', method: 'post' }) + + submit(function(e, data, status, xhr) { + equal(data.params.param1, 'abc', '"param1" should be passed to server') + }) +}) asyncTest('allow empty form "action"', 1, function() { - var currentLocation, ajaxLocation; + var currentLocation, ajaxLocation - buildForm({ action: '' }); + buildForm({ action: '' }) $('#qunit-fixture').find('form') - .bind('ajax:beforeSend', function(e, xhr, settings) { + .bindNative('ajax:beforeSend', function(e, xhr, settings) { // Get current location (the same way jQuery does) try { - currentLocation = location.href; - } catch(e) { - currentLocation = document.createElement( "a" ); - currentLocation.href = ""; - currentLocation = currentLocation.href; + currentLocation = location.href + } catch(err) { + currentLocation = document.createElement( 'a' ) + currentLocation.href = '' + currentLocation = currentLocation.href } - currentLocation = currentLocation.replace(/\?$/, ''); + currentLocation = currentLocation.replace(/\?.*$/, '') // Actual location (strip out settings.data that jQuery serializes and appends) // HACK: can no longer use settings.data below to see what was appended to URL, as of // jQuery 1.6.3 (see http://bugs.jquery.com/ticket/10202 and https://github.com/jquery/jquery/pull/544) - ajaxLocation = settings.url.replace("user_name=john","").replace(/&$/, "").replace(/\?$/, ""); - equal(ajaxLocation.match(/^(.*)/)[1], currentLocation, 'URL should be current page by default'); + ajaxLocation = settings.url.replace('user_name=john', '').replace(/&$/, '').replace(/\?$/, '') + equal(ajaxLocation.match(/^(.*)/)[1], currentLocation, 'URL should be current page by default') // Prevent the request from actually getting sent to the current page and // causing an error. - return false; + return false }) - .trigger('submit'); + .triggerNative('submit') - setTimeout(function() { start(); }, 13); -}); + setTimeout(function() { start() }, 13) +}) asyncTest('sends CSRF token in custom header', 1, function() { - buildForm({ method: 'post' }); - $('#qunit-fixture').append(''); + buildForm({ method: 'post' }) + $('#qunit-fixture').append('') submit(function(e, data, status, xhr) { - equal(data.HTTP_X_CSRF_TOKEN, 'cf50faa3fe97702ca1ae', 'X-CSRF-Token header should be sent'); - }); -}); + equal(data.HTTP_X_CSRF_TOKEN, 'cf50faa3fe97702ca1ae', 'X-CSRF-Token header should be sent') + }) +}) -asyncTest('intelligently guesses crossDomain behavior when target URL has a different protocol and/or hostname', 1, function(e, xhr) { +asyncTest('intelligently guesses crossDomain behavior when target URL has a different protocol and/or hostname', 1, function() { // 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(''); + buildForm({ action: 'http://www.alfajango.com' }) + $('#qunit-fixture').append('') $('#qunit-fixture').find('form') - .bind('ajax:beforeSend', function(e, xhr, settings) { + .bindNative('ajax:beforeSend', function(evt, req, settings) { - equal(settings.crossDomain, true, 'crossDomain should be set to true'); + equal(settings.crossDomain, true, 'crossDomain should be set to true') // prevent request from actually getting sent off-domain - return false; + return false }) - .trigger('submit'); + .triggerNative('submit') - setTimeout(function() { start(); }, 13); -}); + setTimeout(function() { start() }, 13) +}) -asyncTest('intelligently guesses crossDomain behavior when target URL consists of only a path', 1, function(e, xhr) { +asyncTest('intelligently guesses crossDomain behavior when target URL consists of only a path', 1, function() { // 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(''); + buildForm({ action: '/just/a/path' }) + $('#qunit-fixture').append('') $('#qunit-fixture').find('form') - .bind('ajax:beforeSend', function(e, xhr, settings) { + .bindNative('ajax:beforeSend', function(evt, req, settings) { - equal(settings.crossDomain, false, 'crossDomain should be set to false'); + equal(settings.crossDomain, false, 'crossDomain should be set to false') // prevent request from actually getting sent off-domain - return false; + return false }) - .trigger('submit'); + .triggerNative('submit') + + setTimeout(function() { start() }, 13) +}) - setTimeout(function() { start(); }, 13); -}); -})(); +})() diff --git a/test/public/test/csrf-refresh.js b/test/public/test/csrf-refresh.js index 65642433..e3020425 100644 --- a/test/public/test/csrf-refresh.js +++ b/test/public/test/csrf-refresh.js @@ -1,9 +1,9 @@ -(function(){ +(function() { -module('csrf-refresh', {}); +module('csrf-refresh', {}) asyncTest('refresh all csrf tokens', 1, function() { - var correctToken = "cf50faa3fe97702ca1ae"; + var correctToken = 'cf50faa3fe97702ca1ae' var form = $('') var input = $('').attr({ type: 'hidden', name: 'authenticity_token', id: 'authenticity_token', value: 'foo' }) @@ -12,13 +12,13 @@ asyncTest('refresh all csrf tokens', 1, function() { $('#qunit-fixture') .append('') .append('') - .append(form); + .append(form) - $.rails.refreshCSRFTokens(); - currentToken = $('#qunit-fixture #authenticity_token').val(); + $.rails.refreshCSRFTokens() + currentToken = $('#qunit-fixture #authenticity_token').val() - start(); - equal(currentToken, correctToken); -}); + start() + equal(currentToken, correctToken) +}) -})(); +})() diff --git a/test/public/test/csrf-token.js b/test/public/test/csrf-token.js index dfed3781..388b40e0 100644 --- a/test/public/test/csrf-token.js +++ b/test/public/test/csrf-token.js @@ -1,27 +1,27 @@ -(function(){ +(function() { -module('csrf-token', {}); +module('csrf-token', {}) asyncTest('find csrf token', 1, function() { - var correctToken = "cf50faa3fe97702ca1ae"; + var correctToken = 'cf50faa3fe97702ca1ae' - $('#qunit-fixture').append(''); + $('#qunit-fixture').append('') - currentToken = $.rails.csrfToken(); + currentToken = $.rails.csrfToken() - start(); - equal(currentToken, correctToken); -}); + start() + equal(currentToken, correctToken) +}) asyncTest('find csrf param', 1, function() { - var correctParam = "authenticity_token"; + var correctParam = 'authenticity_token' - $('#qunit-fixture').append(''); + $('#qunit-fixture').append('') - currentParam = $.rails.csrfParam(); + currentParam = $.rails.csrfParam() - start(); - equal(currentParam, correctParam); -}); + start() + equal(currentParam, correctParam) +}) -})(); +})() diff --git a/test/public/test/data-confirm.js b/test/public/test/data-confirm.js index 8add713c..b49f965b 100644 --- a/test/public/test/data-confirm.js +++ b/test/public/test/data-confirm.js @@ -5,261 +5,261 @@ module('data-confirm', { 'data-remote': 'true', 'data-confirm': 'Are you absolutely sure?', text: 'my social security number' - })); + })) $('#qunit-fixture').append($(''); - form.append(button); + var form = $('form[data-remote]'), button = $('') + form.append(button) - App.checkEnabledState(button, 'Submit'); + App.checkEnabledState(button, 'Submit') - form.bind('ajax:success', function(e, data) { + form.bindNative('ajax:success', function(e, data) { setTimeout(function() { - App.checkEnabledState(button, 'Submit'); - start(); - }, 13); - }); - form.trigger('submit'); - - App.checkDisabledState(button, 'submitting ...'); -}); + App.checkEnabledState(button, 'Submit') + start() + }, 13) + }) + form.triggerNative('submit') -asyncTest('form input[type=submit][data-disable-with] disables', 6, function(){ - var form = $('form:not([data-remote])'), input = form.find('input[type=submit]'); + App.checkDisabledState(button, 'submitting ...') +}) - App.checkEnabledState(input, 'Submit'); +asyncTest('form input[type=submit][data-disable-with] disables', 6, function() { + var form = $('form:not([data-remote])'), input = form.find('input[type=submit]') - // WEEIRDD: attaching this handler makes the test work in IE7 - $(document).bind('iframe:loading', function(e, form) {}); + App.checkEnabledState(input, 'Submit') $(document).bind('iframe:loaded', function(e, data) { setTimeout(function() { - App.checkDisabledState(input, 'submitting ...'); - start(); - }, 30); - }); - form.trigger('submit'); + App.checkDisabledState(input, 'submitting ...') + start() + }, 30) + }) + form.triggerNative('submit') setTimeout(function() { - App.checkDisabledState(input, 'submitting ...'); - }, 30); -}); + App.checkDisabledState(input, 'submitting ...') + }, 30) +}) -test('form input[type=submit][data-disable-with] re-enables when `pageshow` event is triggered', function(){ - var form = $('form:not([data-remote])'), input = form.find('input[type=submit]'); +test('form input[type=submit][data-disable-with] re-enables when `pageshow` event is triggered', function() { + var form = $('form:not([data-remote])'), input = form.find('input[type=submit]') - App.checkEnabledState(input, 'Submit'); + App.checkEnabledState(input, 'Submit') // Emulate the disabled state without submitting the form at all, what is the // state after going back on firefox after submitting a form. // // See https://github.com/rails/jquery-ujs/issues/357 - $.rails.disableFormElements(form); + $.rails.disableElement(form[0]) - App.checkDisabledState(input, 'submitting ...'); + App.checkDisabledState(input, 'submitting ...') - $(window).trigger('pageshow'); + $(window).triggerNative('pageshow') - App.checkEnabledState(input, 'Submit'); -}); + App.checkEnabledState(input, 'Submit') +}) -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(); +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); + form.bindNative('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(){ + setTimeout(function() { + var input = form.find('input[type=submit]') + App.checkEnabledState(input, 'Submit') + start() + }, 30) + }).triggerNative('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'); + newDisabledInput = input.clone().attr('disabled', 'disabled') - form.bind('ajax:success', function(){ - input.replaceWith(newDisabledInput); + form.bindNative('ajax:success', function() { + input.replaceWith(newDisabledInput) - setTimeout(function(){ - App.checkEnabledState(newDisabledInput, 'Submit'); - start(); - }, 30); - }).trigger('submit'); -}); + setTimeout(function() { + App.checkEnabledState(newDisabledInput, 'Submit') + start() + }, 30) + }).triggerNative('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) {}); + var form = $('#not_remote'), input = $('input[form=not_remote]') + App.checkEnabledState(input, 'Form Attr Submit') $(document).bind('iframe:loaded', function(e, data) { setTimeout(function() { - App.checkDisabledState(input, 'form attr submitting'); - start(); - }, 30); - }); - form.trigger('submit'); + App.checkDisabledState(input, 'form attr submitting') + start() + }, 30) + }) + form.triggerNative('submit') setTimeout(function() { - App.checkDisabledState(input, 'form attr submitting'); - }, 30); + 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); + textarea = $('').appendTo(form) - form.bind('ajax:success', function(e, data) { + form.bindNative('ajax:success', function(e, data) { setTimeout(function() { - equal(data.params.user_bio, 'born, lived, died.'); - start(); - }, 13); - }); - form.trigger('submit'); + equal(data.params.user_bio, 'born, lived, died.') + start() + }, 13) + }) + form.triggerNative('submit') - App.checkDisabledState(textarea, 'processing ...'); -}); + App.checkDisabledState(textarea, 'processing ...') +}) asyncTest('a[data-disable-with] disables', 4, function() { - var link = $('a[data-disable-with]'); + var link = $('a[data-disable-with]') - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') - link.trigger('click'); - App.checkDisabledState(link, 'clicking...'); - start(); -}); + link.triggerNative('click') + App.checkDisabledState(link, 'clicking...') + start() +}) test('a[data-disable-with] re-enables when `pageshow` event is triggered', function() { - var link = $('a[data-disable-with]'); + var link = $('a[data-disable-with]') - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') - link.trigger('click'); - App.checkDisabledState(link, 'clicking...'); + link.triggerNative('click') + App.checkDisabledState(link, 'clicking...') - $(window).trigger('pageshow'); - App.checkEnabledState(link, 'Click me'); -}); + $(window).triggerNative('pageshow') + App.checkEnabledState(link, 'Click me') +}) asyncTest('a[data-remote][data-disable-with] disables and re-enables', 6, function() { - var link = $('a[data-disable-with]').attr('data-remote', true); + var link = $('a[data-disable-with]').attr('data-remote', true) - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') link - .bind('ajax:beforeSend', function() { - App.checkDisabledState(link, 'clicking...'); + .bindNative('ajax:beforeSend', function() { + App.checkDisabledState(link, 'clicking...') }) - .bind('ajax:complete', function() { + .bindNative('ajax:complete', function() { setTimeout( function() { - App.checkEnabledState(link, 'Click me'); - start(); - }, 15); + App.checkEnabledState(link, 'Click me') + start() + }, 15) }) - .trigger('click'); -}); + .triggerNative('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); + var link = $('a[data-disable-with]').attr('data-remote', true) - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') link - .bind('ajax:before', function() { - App.checkDisabledState(link, 'clicking...'); - return false; + .bindNative('ajax:before', function() { + App.checkDisabledState(link, 'clicking...') + return false }) - .trigger('click'); + .triggerNative('click') setTimeout(function() { - App.checkEnabledState(link, 'Click me'); - start(); - }, 30); -}); + 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); + var link = $('a[data-disable-with]').attr('data-remote', true) - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') link - .bind('ajax:beforeSend', function() { - App.checkDisabledState(link, 'clicking...'); - return false; + .bindNative('ajax:beforeSend', function() { + App.checkDisabledState(link, 'clicking...') + return false }) - .trigger('click'); + .triggerNative('click') setTimeout(function() { - App.checkEnabledState(link, 'Click me'); - start(); - }, 30); -}); + 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'); + var link = $('a[data-disable-with]').attr('data-remote', true).attr('href', '/error') - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') link - .bind('ajax:beforeSend', function() { - App.checkDisabledState(link, 'clicking...'); + .bindNative('ajax:beforeSend', function() { + App.checkDisabledState(link, 'clicking...') }) - .trigger('click'); + .triggerNative('click') setTimeout(function() { - App.checkEnabledState(link, 'Click me'); - start(); - }, 30); -}); + 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); + submit = $('').appendTo(form) form - .bind('ajax:beforeSend', function() { - return false; + .bindNative('ajax:beforeSend', function() { + return false }) - .trigger('submit'); + .triggerNative('submit') - App.checkEnabledState(input, 'john'); - App.checkEnabledState(button, 'Submit'); - App.checkEnabledState(textarea, 'born, lived, died.'); - App.checkEnabledState(submit, 'Submit'); + App.checkEnabledState(input, 'john') + App.checkEnabledState(button, 'Submit') + App.checkEnabledState(textarea, 'born, lived, died.') + App.checkEnabledState(submit, 'Submit') - start(); -}); + 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'); + var link = $('a[data-disable-with]') - link.trigger(e); - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') - e = $.Event('click'); - e.ctrlKey = true; + link.triggerNative('click', { metaKey: true }) + App.checkEnabledState(link, 'Click me') - link.trigger(e); - App.checkEnabledState(link, 'Click me'); - start(); -}); + link.triggerNative('click', { metaKey: true }) + 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]'); + var button = $('button[data-remote][data-disable-with]') - App.checkEnabledState(button, 'Click me'); + App.checkEnabledState(button, 'Click me') button - .bind('ajax:send', function() { - App.checkDisabledState(button, 'clicking...'); + .bindNative('ajax:send', function() { + App.checkDisabledState(button, 'clicking...') }) - .bind('ajax:complete', function() { + .bindNative('ajax:complete', function() { setTimeout( function() { - App.checkEnabledState(button, 'Click me'); - start(); - }, 15); + App.checkEnabledState(button, 'Click me') + start() + }, 15) }) - .trigger('click'); -}); + .triggerNative('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]'); + var button = $('button[data-remote][data-disable-with]') - App.checkEnabledState(button, 'Click me'); + App.checkEnabledState(button, 'Click me') button - .bind('ajax:before', function() { - App.checkDisabledState(button, 'clicking...'); - return false; + .bindNative('ajax:before', function() { + App.checkDisabledState(button, 'clicking...') + return false }) - .trigger('click'); + .triggerNative('click') setTimeout(function() { - App.checkEnabledState(button, 'Click me'); - start(); - }, 30); -}); + 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]'); + var button = $('button[data-remote][data-disable-with]') - App.checkEnabledState(button, 'Click me'); + App.checkEnabledState(button, 'Click me') button - .bind('ajax:beforeSend', function() { - App.checkDisabledState(button, 'clicking...'); - return false; + .bindNative('ajax:beforeSend', function() { + App.checkDisabledState(button, 'clicking...') + return false }) - .trigger('click'); + .triggerNative('click') setTimeout(function() { - App.checkEnabledState(button, 'Click me'); - start(); - }, 30); -}); + 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'); + var button = $('a[data-disable-with]').attr('data-remote', true).attr('href', '/error') - App.checkEnabledState(button, 'Click me'); + App.checkEnabledState(button, 'Click me') button - .bind('ajax:send', function() { - App.checkDisabledState(button, 'clicking...'); + .bindNative('ajax:send', function() { + App.checkDisabledState(button, 'clicking...') }) - .trigger('click'); + .triggerNative('click') setTimeout(function() { - App.checkEnabledState(button, 'Click me'); - start(); - }, 30); -}); + App.checkEnabledState(button, 'Click me') + start() + }, 30) +}) diff --git a/test/public/test/data-disable.js b/test/public/test/data-disable.js index 3df1bb9c..ccc38cf9 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,313 +14,308 @@ 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': 'true' - })); + })) $('#qunit-fixture').append($(''); - form.append(button); + var form = $('form[data-remote]'), button = $('') + form.append(button) - App.checkEnabledState(button, 'Submit'); + App.checkEnabledState(button, 'Submit') - form.bind('ajax:success', function(e, data) { + form.bindNative('ajax:success', function(e, data) { setTimeout(function() { - App.checkEnabledState(button, 'Submit'); - start(); + App.checkEnabledState(button, 'Submit') + start() }, 13) }) - form.trigger('submit'); + form.triggerNative('submit') - App.checkDisabledState(button, 'Submit'); - equal(button.data('ujs:enable-with'), undefined); -}); + App.checkDisabledState(button, 'Submit') + equal(button.data('ujs:enable-with'), undefined) +}) -asyncTest('form input[type=submit][data-disable] disables', 6, function(){ - var form = $('form:not([data-remote])'), input = form.find('input[type=submit]'); +asyncTest('form input[type=submit][data-disable] disables', 6, function() { + var form = $('form:not([data-remote])'), input = form.find('input[type=submit]') - App.checkEnabledState(input, '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:loading', function(e, f) {}) $(document).bind('iframe:loaded', function(e, data) { setTimeout(function() { - App.checkDisabledState(input, 'Submit'); - start(); - }, 30); - }); - form.trigger('submit'); + App.checkDisabledState(input, 'Submit') + start() + }, 30) + }) + form.triggerNative('submit') setTimeout(function() { - App.checkDisabledState(input, 'Submit'); - }, 30); -}); - -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(); + App.checkDisabledState(input, 'Submit') + }, 30) +}) - form.bind('ajax:success', function(){ - form.html(origFormContents); +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() - setTimeout(function(){ - var input = form.find('input[type=submit]'); - App.checkEnabledState(input, 'Submit'); - start(); - }, 30); - }).trigger('submit'); -}); + form.bindNative('ajax:success', function() { + form.html(origFormContents) -asyncTest('form[data-remote] input[data-disable] is replaced with disabled field in ajax callback', 2, function(){ + setTimeout(function() { + var input = form.find('input[type=submit]') + App.checkEnabledState(input, 'Submit') + start() + }, 30) + }).triggerNative('submit') +}) + +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'); + newDisabledInput = input.clone().attr('disabled', 'disabled') - form.bind('ajax:success', function(){ - input.replaceWith(newDisabledInput); + form.bindNative('ajax:success', function() { + input.replaceWith(newDisabledInput) - setTimeout(function(){ - App.checkEnabledState(newDisabledInput, 'Submit'); - start(); - }, 30); - }).trigger('submit'); -}); + setTimeout(function() { + App.checkEnabledState(newDisabledInput, 'Submit') + start() + }, 30) + }).triggerNative('submit') +}) 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) { + form.bindNative('ajax:success', function(e, data) { setTimeout(function() { - equal(data.params.user_bio, 'born, lived, died.'); - start(); + equal(data.params.user_bio, 'born, lived, died.') + start() }, 13) }) - form.trigger('submit'); + form.triggerNative('submit') - App.checkDisabledState(textarea, 'born, lived, died.'); -}); + App.checkDisabledState(textarea, 'born, lived, died.') +}) asyncTest('a[data-disable] disables', 5, function() { - var link = $('a[data-disable]'); + var link = $('a[data-disable]') - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') - link.trigger('click'); - App.checkDisabledState(link, 'Click me'); - equal(link.data('ujs:enable-with'), undefined); - start(); -}); + link.triggerNative('click') + App.checkDisabledState(link, 'Click me') + equal(link.data('ujs:enable-with'), undefined) + start() +}) asyncTest('a[data-remote][data-disable] disables and re-enables', 6, function() { - var link = $('a[data-disable]').attr('data-remote', true); + var link = $('a[data-disable]').attr('data-remote', true) - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') link - .bind('ajax:send', function() { - App.checkDisabledState(link, 'Click me'); + .bindNative('ajax:send', function() { + App.checkDisabledState(link, 'Click me') }) - .bind('ajax:complete', function() { + .bindNative('ajax:complete', function() { setTimeout( function() { - App.checkEnabledState(link, 'Click me'); - start(); - }, 15); + App.checkEnabledState(link, 'Click me') + start() + }, 15) }) - .trigger('click'); -}); + .triggerNative('click') +}) 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); + var link = $('a[data-disable]').attr('data-remote', true) - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') link - .bind('ajax:before', function() { - App.checkDisabledState(link, 'Click me'); - return false; + .bindNative('ajax:before', function() { + App.checkDisabledState(link, 'Click me') + return false }) - .trigger('click'); + .triggerNative('click') setTimeout(function() { - App.checkEnabledState(link, 'Click me'); - start(); - }, 30); -}); + App.checkEnabledState(link, 'Click me') + start() + }, 30) +}) 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); + var link = $('a[data-disable]').attr('data-remote', true) - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') link - .bind('ajax:beforeSend', function() { - App.checkDisabledState(link, 'Click me'); - return false; + .bindNative('ajax:beforeSend', function() { + App.checkDisabledState(link, 'Click me') + return false }) - .trigger('click'); + .triggerNative('click') setTimeout(function() { - App.checkEnabledState(link, 'Click me'); - start(); - }, 30); -}); + App.checkEnabledState(link, 'Click me') + start() + }, 30) +}) 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'); + var link = $('a[data-disable]').attr('data-remote', true).attr('href', '/error') - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') link - .bind('ajax:send', function() { - App.checkDisabledState(link, 'Click me'); + .bindNative('ajax:send', function() { + App.checkDisabledState(link, 'Click me') }) - .trigger('click'); + .triggerNative('click') setTimeout(function() { - App.checkEnabledState(link, 'Click me'); - start(); - }, 30); -}); + 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); + submit = $('').appendTo(form) form - .bind('ajax:beforeSend', function() { - return false; + .bindNative('ajax:beforeSend', function() { + return false }) - .trigger('submit'); + .triggerNative('submit') - App.checkEnabledState(input, 'john'); - App.checkEnabledState(button, 'Submit'); - App.checkEnabledState(textarea, 'born, lived, died.'); - App.checkEnabledState(submit, 'Submit'); + App.checkEnabledState(input, 'john') + App.checkEnabledState(button, 'Submit') + App.checkEnabledState(textarea, 'born, lived, died.') + App.checkEnabledState(submit, 'Submit') - start(); -}); + 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'); + var link = $('a[data-disable]') - link.trigger(e); - App.checkEnabledState(link, 'Click me'); + App.checkEnabledState(link, 'Click me') - e = $.Event('click'); - e.ctrlKey = true; + link.triggerNative('click', { metaKey: true }) + App.checkEnabledState(link, 'Click me') - link.trigger(e); - App.checkEnabledState(link, 'Click me'); - start(); -}); + link.triggerNative('click', { ctrlKey: true }) + 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]'); + var button = $('button[data-remote][data-disable]') - App.checkEnabledState(button, 'Click me'); + App.checkEnabledState(button, 'Click me') button - .bind('ajax:send', function() { - App.checkDisabledState(button, 'Click me'); + .bindNative('ajax:send', function() { + App.checkDisabledState(button, 'Click me') }) - .bind('ajax:complete', function() { + .bindNative('ajax:complete', function() { setTimeout( function() { - App.checkEnabledState(button, 'Click me'); - start(); - }, 15); + App.checkEnabledState(button, 'Click me') + start() + }, 15) }) - .trigger('click'); -}); + .triggerNative('click') +}) asyncTest('button[data-remote][data-disable] re-enables when `ajax:before` event is cancelled', 6, function() { - var button = $('button[data-remote][data-disable]'); + var button = $('button[data-remote][data-disable]') - App.checkEnabledState(button, 'Click me'); + App.checkEnabledState(button, 'Click me') button - .bind('ajax:before', function() { - App.checkDisabledState(button, 'Click me'); - return false; + .bindNative('ajax:before', function() { + App.checkDisabledState(button, 'Click me') + return false }) - .trigger('click'); + .triggerNative('click') setTimeout(function() { - App.checkEnabledState(button, 'Click me'); - start(); - }, 30); -}); + 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]'); + var button = $('button[data-remote][data-disable]') - App.checkEnabledState(button, 'Click me'); + App.checkEnabledState(button, 'Click me') button - .bind('ajax:beforeSend', function() { - App.checkDisabledState(button, 'Click me'); - return false; + .bindNative('ajax:beforeSend', function() { + App.checkDisabledState(button, 'Click me') + return false }) - .trigger('click'); + .triggerNative('click') setTimeout(function() { - App.checkEnabledState(button, 'Click me'); - start(); - }, 30); -}); + 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'); + var button = $('a[data-disable]').attr('data-remote', true).attr('href', '/error') - App.checkEnabledState(button, 'Click me'); + App.checkEnabledState(button, 'Click me') button - .bind('ajax:send', function() { - App.checkDisabledState(button, 'Click me'); + .bindNative('ajax:send', function() { + App.checkDisabledState(button, 'Click me') }) - .trigger('click'); + .triggerNative('click') setTimeout(function() { - App.checkEnabledState(button, 'Click me'); - start(); - }, 30); -}); + App.checkEnabledState(button, 'Click me') + start() + }, 30) +}) diff --git a/test/public/test/data-method.js b/test/public/test/data-method.js index 57528377..615342ea 100644 --- a/test/public/test/data-method.js +++ b/test/public/test/data-method.js @@ -1,75 +1,75 @@ -(function(){ +(function() { module('data-method', { setup: function() { $('#qunit-fixture').append($('', { href: '/echo', 'data-method': 'delete', text: 'destroy!' - })); + })) }, teardown: function() { - $(document).unbind('iframe:loaded'); + $(document).unbind('iframe:loaded') } -}); +}) function submit(fn, options) { $(document).bind('iframe:loaded', function(e, data) { - fn(data); - start(); - }); + fn(data) + start() + }) $('#qunit-fixture').find('a') - .trigger('click'); + .triggerNative('click') } asyncTest('link with "data-method" set to "delete"', 3, function() { submit(function(data) { - equal(data.REQUEST_METHOD, 'DELETE'); - strictEqual(data.params.authenticity_token, undefined); - strictEqual(data.HTTP_X_CSRF_TOKEN, undefined); - }); -}); + equal(data.REQUEST_METHOD, 'DELETE') + strictEqual(data.params.authenticity_token, undefined) + strictEqual(data.HTTP_X_CSRF_TOKEN, undefined) + }) +}) asyncTest('link with "data-method" and CSRF', 1, function() { $('#qunit-fixture') .append('') - .append(''); + .append('') submit(function(data) { - equal(data.params.authenticity_token, 'cf50faa3fe97702ca1ae'); - }); -}); + equal(data.params.authenticity_token, 'cf50faa3fe97702ca1ae') + }) +}) asyncTest('link "target" should be carried over to generated form', 1, function() { - $('a[data-method]').attr('target', 'super-special-frame'); + $('a[data-method]').attr('target', 'super-special-frame') submit(function(data) { - equal(data.params._target, 'super-special-frame'); - }); -}); + equal(data.params._target, 'super-special-frame') + }) +}) asyncTest('link with "data-method" and cross origin', 1, function() { - var data = {}; + var data = {} $('#qunit-fixture') .append('') - .append(''); + .append('') $(document).on('submit', 'form', function(e) { $(e.currentTarget).serializeArray().map(function(item) { - data[item.name] = item.value; - }); + data[item.name] = item.value + }) - return false; - }); + return false + }) - var link = $('#qunit-fixture').find('a'); + var link = $('#qunit-fixture').find('a') - link.attr('href', 'http://www.alfajango.com'); + link.attr('href', 'http://www.alfajango.com') - link.trigger('click'); + link.triggerNative('click') - start(); + start() - notEqual(data.authenticity_token, 'cf50faa3fe97702ca1ae'); -}); + notEqual(data.authenticity_token, 'cf50faa3fe97702ca1ae') +}) -})(); +})() diff --git a/test/public/test/data-remote.js b/test/public/test/data-remote.js index e751c8e0..b91ff5ba 100644 --- a/test/public/test/data-remote.js +++ b/test/public/test/data-remote.js @@ -25,13 +25,13 @@ module('data-remote', { disabled: 'disabled', text: 'Disabed link' })) - .find('form').append($('')); + .find('form').append($('')) } -}); +}) asyncTest('ctrl-clicking on a link does not fire ajaxyness', 0, function() { - var link = $('a[data-remote]'), e; + var link = $('a[data-remote]') // Ideally, we'd setup an iframe to intercept normal link clicks // and add a test to make sure the iframe:loaded event is triggered. @@ -39,83 +39,100 @@ 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() { - ok(false, 'ajax should not be triggered'); - }); - - e = $.Event('click'); - e.metaKey = true; - link.trigger(e); + .bindNative('ajax:beforeSend', function() { + ok(false, 'ajax should not be triggered') + }) - e = $.Event('click'); - e.ctrlKey = true; - link.trigger(e); + link.triggerNative('click', { metaKey: true }) + link.triggerNative('click', { ctrlKey: true }) - setTimeout(function(){ start(); }, 13); -}); + setTimeout(function() { start() }, 13) +}) asyncTest('ctrl-clicking on a link still fires ajax for non-GET links and for links with "data-params"', 2, function() { - var link = $('a[data-remote]'), e; - e = $.Event('click'); - e.metaKey = true; + var link = $('a[data-remote]') link .removeAttr('data-params') .attr('data-method', 'POST') - .bind('ajax:beforeSend', function() { - ok(true, 'ajax should be triggered'); + .bindNative('ajax:beforeSend', function() { + ok(true, 'ajax should be triggered') }) - .trigger(e); - - e = $.Event('click'); - e.metaKey = true; + .triggerNative('click', { metaKey: true }) link .removeAttr('data-method') .attr('data-params', 'name=steve') - .trigger(e); + .triggerNative('click', { metaKey: true }) - setTimeout(function(){ start(); }, 13); -}); + setTimeout(function() { start() }, 13) +}) asyncTest('clicking on a link with data-remote attribute', 5, function() { $('a[data-remote]') - .bind('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); + .bindNative('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) + }) + .bindNative('ajax:complete', function() { start() }) + .triggerNative('click') +}) + +asyncTest('clicking on a link with both query string in href and data-params', 4, function() { + $('a[data-remote]') + .attr('href', '/echo?data3=value3') + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertGetRequest(data) + 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') + equal(data.params.data3, 'value3', 'query string in url should be passed to server with right value') }) - .bind('ajax:complete', function() { start() }) - .trigger('click'); -}); + .bindNative('ajax:complete', function() { start() }) + .triggerNative('click') +}) + +asyncTest('clicking on a link with both query string in href and data-params with POST method', 4, function() { + $('a[data-remote]') + .attr('href', '/echo?data3=value3') + .attr('data-method', 'post') + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertPostRequest(data) + 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') + equal(data.params.data3, 'value3', 'query string in url should be passed to server with right value') + }) + .bindNative('ajax:complete', function() { start() }) + .triggerNative('click') +}) asyncTest('clicking on a link with disabled attribute', 0, function() { $('a[disabled]') - .bind("ajax:before", function(e, data, status, xhr) { + .bindNative('ajax:before', function(e, data, status, xhr) { App.assertCallbackNotInvoked('ajax:success') }) - .bind('ajax:complete', function() { start() }) - .trigger('click') + .bindNative('ajax:complete', function() { start() }) + .triggerNative('click') setTimeout(function() { - start(); - }, 13); -}); + start() + }, 13) +}) asyncTest('clicking on a button with data-remote attribute', 5, function() { $('button[data-remote]') - .bind('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); + .bindNative('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() }) - .trigger('click'); -}); + .bindNative('ajax:complete', function() { start() }) + .triggerNative('click') +}) asyncTest('changing a select option with data-remote attribute', 5, function() { $('form') @@ -128,214 +145,209 @@ asyncTest('changing a select option with data-remote attribute', 5, function() { }) .append($('