Commit bcf8f0808d409c27d8452982681997c8a6e0dfb5
1 parent
6ee575f1
Exists in
master
and in
20 other branches
rails4: update jquery-ujs' rails.js
Showing
1 changed file
with
162 additions
and
54 deletions
Show diff stats
public/javascripts/rails.js
... | ... | @@ -4,7 +4,7 @@ |
4 | 4 | * Unobtrusive scripting adapter for jQuery |
5 | 5 | * https://github.com/rails/jquery-ujs |
6 | 6 | * |
7 | - * Requires jQuery 1.7.0 or later. | |
7 | + * Requires jQuery 1.8.0 or later. | |
8 | 8 | * |
9 | 9 | * Released under the MIT license |
10 | 10 | * |
... | ... | @@ -12,6 +12,8 @@ |
12 | 12 | |
13 | 13 | // Cut down on the number of issues from people inadvertently including jquery_ujs twice |
14 | 14 | // by detecting and raising an error when it happens. |
15 | + 'use strict'; | |
16 | + | |
15 | 17 | if ( $.rails !== undefined ) { |
16 | 18 | $.error('jquery-ujs has already been loaded!'); |
17 | 19 | } |
... | ... | @@ -22,10 +24,10 @@ |
22 | 24 | |
23 | 25 | $.rails = rails = { |
24 | 26 | // Link elements bound by jquery-ujs |
25 | - linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]', | |
27 | + linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]', | |
26 | 28 | |
27 | 29 | // Button elements bound by jquery-ujs |
28 | - buttonClickSelector: 'button[data-remote]', | |
30 | + buttonClickSelector: 'button[data-remote]:not(form button), button[data-confirm]:not(form button)', | |
29 | 31 | |
30 | 32 | // Select elements bound by jquery-ujs |
31 | 33 | inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]', |
... | ... | @@ -34,13 +36,13 @@ |
34 | 36 | formSubmitSelector: 'form', |
35 | 37 | |
36 | 38 | // Form input elements bound by jquery-ujs |
37 | - formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])', | |
39 | + 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])', | |
38 | 40 | |
39 | 41 | // Form input elements disabled during form submission |
40 | - disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]', | |
42 | + 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', | |
41 | 43 | |
42 | 44 | // Form input elements re-enabled after form submission |
43 | - enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled', | |
45 | + 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', | |
44 | 46 | |
45 | 47 | // Form required input elements |
46 | 48 | requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])', |
... | ... | @@ -49,19 +51,30 @@ |
49 | 51 | fileInputSelector: 'input[type=file]', |
50 | 52 | |
51 | 53 | // Link onClick disable selector with possible reenable after remote submission |
52 | - linkDisableSelector: 'a[data-disable-with]', | |
54 | + linkDisableSelector: 'a[data-disable-with], a[data-disable]', | |
55 | + | |
56 | + // Button onClick disable selector with possible reenable after remote submission | |
57 | + buttonDisableSelector: 'button[data-remote][data-disable-with], button[data-remote][data-disable]', | |
58 | + | |
59 | + // Up-to-date Cross-Site Request Forgery token | |
60 | + csrfToken: function() { | |
61 | + return $('meta[name=csrf-token]').attr('content'); | |
62 | + }, | |
63 | + | |
64 | + // URL param that must contain the CSRF token | |
65 | + csrfParam: function() { | |
66 | + return $('meta[name=csrf-param]').attr('content'); | |
67 | + }, | |
53 | 68 | |
54 | 69 | // Make sure that every Ajax request sends the CSRF token |
55 | 70 | CSRFProtection: function(xhr) { |
56 | - var token = $('meta[name="csrf-token"]').attr('content'); | |
71 | + var token = rails.csrfToken(); | |
57 | 72 | if (token) xhr.setRequestHeader('X-CSRF-Token', token); |
58 | 73 | }, |
59 | 74 | |
60 | 75 | // making sure that all forms have actual up-to-date token(cached forms contain old one) |
61 | 76 | refreshCSRFTokens: function(){ |
62 | - var csrfToken = $('meta[name=csrf-token]').attr('content'); | |
63 | - var csrfParam = $('meta[name=csrf-param]').attr('content'); | |
64 | - $('form input[name="' + csrfParam + '"]').val(csrfToken); | |
77 | + $('form input[name="' + rails.csrfParam() + '"]').val(rails.csrfToken()); | |
65 | 78 | }, |
66 | 79 | |
67 | 80 | // Triggers an event on an element and returns false if the event result is false |
... | ... | @@ -83,16 +96,19 @@ |
83 | 96 | |
84 | 97 | // Default way to get an element's href. May be overridden at $.rails.href. |
85 | 98 | href: function(element) { |
86 | - return element.attr('href'); | |
99 | + return element[0].href; | |
100 | + }, | |
101 | + | |
102 | + // Checks "data-remote" if true to handle the request through a XHR request. | |
103 | + isRemote: function(element) { | |
104 | + return element.data('remote') !== undefined && element.data('remote') !== false; | |
87 | 105 | }, |
88 | 106 | |
89 | 107 | // Submits "remote" forms and links with ajax |
90 | 108 | handleRemote: function(element) { |
91 | - var method, url, data, elCrossDomain, crossDomain, withCredentials, dataType, options; | |
109 | + var method, url, data, withCredentials, dataType, options; | |
92 | 110 | |
93 | 111 | if (rails.fire(element, 'ajax:before')) { |
94 | - elCrossDomain = element.data('cross-domain'); | |
95 | - crossDomain = elCrossDomain === undefined ? null : elCrossDomain; | |
96 | 112 | withCredentials = element.data('with-credentials') || null; |
97 | 113 | dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType); |
98 | 114 | |
... | ... | @@ -110,12 +126,12 @@ |
110 | 126 | method = element.data('method'); |
111 | 127 | url = element.data('url'); |
112 | 128 | data = element.serialize(); |
113 | - if (element.data('params')) data = data + "&" + element.data('params'); | |
129 | + if (element.data('params')) data = data + '&' + element.data('params'); | |
114 | 130 | } else if (element.is(rails.buttonClickSelector)) { |
115 | 131 | method = element.data('method') || 'get'; |
116 | 132 | url = element.data('url'); |
117 | 133 | data = element.serialize(); |
118 | - if (element.data('params')) data = data + "&" + element.data('params'); | |
134 | + if (element.data('params')) data = data + '&' + element.data('params'); | |
119 | 135 | } else { |
120 | 136 | method = element.data('method'); |
121 | 137 | url = rails.href(element); |
... | ... | @@ -129,7 +145,11 @@ |
129 | 145 | if (settings.dataType === undefined) { |
130 | 146 | xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script); |
131 | 147 | } |
132 | - return rails.fire(element, 'ajax:beforeSend', [xhr, settings]); | |
148 | + if (rails.fire(element, 'ajax:beforeSend', [xhr, settings])) { | |
149 | + element.trigger('ajax:send', xhr); | |
150 | + } else { | |
151 | + return false; | |
152 | + } | |
133 | 153 | }, |
134 | 154 | success: function(data, status, xhr) { |
135 | 155 | element.trigger('ajax:success', [data, status, xhr]); |
... | ... | @@ -140,7 +160,7 @@ |
140 | 160 | error: function(xhr, status, error) { |
141 | 161 | element.trigger('ajax:error', [xhr, status, error]); |
142 | 162 | }, |
143 | - crossDomain: crossDomain | |
163 | + crossDomain: rails.isCrossDomain(url) | |
144 | 164 | }; |
145 | 165 | |
146 | 166 | // There is no withCredentials for IE6-8 when |
... | ... | @@ -154,26 +174,45 @@ |
154 | 174 | // Only pass url to `ajax` options if not blank |
155 | 175 | if (url) { options.url = url; } |
156 | 176 | |
157 | - var jqxhr = rails.ajax(options); | |
158 | - element.trigger('ajax:send', jqxhr); | |
159 | - return jqxhr; | |
177 | + return rails.ajax(options); | |
160 | 178 | } else { |
161 | 179 | return false; |
162 | 180 | } |
163 | 181 | }, |
164 | 182 | |
183 | + // Determines if the request is a cross domain request. | |
184 | + isCrossDomain: function(url) { | |
185 | + var originAnchor = document.createElement('a'); | |
186 | + originAnchor.href = location.href; | |
187 | + var urlAnchor = document.createElement('a'); | |
188 | + | |
189 | + try { | |
190 | + urlAnchor.href = url; | |
191 | + // This is a workaround to a IE bug. | |
192 | + urlAnchor.href = urlAnchor.href; | |
193 | + | |
194 | + // Make sure that the browser parses the URL and that the protocols and hosts match. | |
195 | + return !urlAnchor.protocol || !urlAnchor.host || | |
196 | + (originAnchor.protocol + '//' + originAnchor.host !== | |
197 | + urlAnchor.protocol + '//' + urlAnchor.host); | |
198 | + } catch (e) { | |
199 | + // If there is an error parsing the URL, assume it is crossDomain. | |
200 | + return true; | |
201 | + } | |
202 | + }, | |
203 | + | |
165 | 204 | // Handles "data-method" on links such as: |
166 | 205 | // <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a> |
167 | 206 | handleMethod: function(link) { |
168 | 207 | var href = rails.href(link), |
169 | 208 | method = link.data('method'), |
170 | 209 | target = link.attr('target'), |
171 | - csrfToken = $('meta[name=csrf-token]').attr('content'), | |
172 | - csrfParam = $('meta[name=csrf-param]').attr('content'), | |
210 | + csrfToken = rails.csrfToken(), | |
211 | + csrfParam = rails.csrfParam(), | |
173 | 212 | form = $('<form method="post" action="' + href + '"></form>'), |
174 | 213 | metadataInput = '<input name="_method" value="' + method + '" type="hidden" />'; |
175 | 214 | |
176 | - if (csrfParam !== undefined && csrfToken !== undefined) { | |
215 | + if (csrfParam !== undefined && csrfToken !== undefined && !rails.isCrossDomain(href)) { | |
177 | 216 | metadataInput += '<input name="' + csrfParam + '" value="' + csrfToken + '" type="hidden" />'; |
178 | 217 | } |
179 | 218 | |
... | ... | @@ -183,32 +222,54 @@ |
183 | 222 | form.submit(); |
184 | 223 | }, |
185 | 224 | |
225 | + // Helper function that returns form elements that match the specified CSS selector | |
226 | + // If form is actually a "form" element this will return associated elements outside the from that have | |
227 | + // the html form attribute set | |
228 | + formElements: function(form, selector) { | |
229 | + return form.is('form') ? $(form[0].elements).filter(selector) : form.find(selector); | |
230 | + }, | |
231 | + | |
186 | 232 | /* Disables form elements: |
187 | 233 | - Caches element value in 'ujs:enable-with' data store |
188 | 234 | - Replaces element text with value of 'data-disable-with' attribute |
189 | 235 | - Sets disabled property to true |
190 | 236 | */ |
191 | 237 | disableFormElements: function(form) { |
192 | - form.find(rails.disableSelector).each(function() { | |
193 | - var element = $(this), method = element.is('button') ? 'html' : 'val'; | |
194 | - element.data('ujs:enable-with', element[method]()); | |
195 | - element[method](element.data('disable-with')); | |
196 | - element.prop('disabled', true); | |
238 | + rails.formElements(form, rails.disableSelector).each(function() { | |
239 | + rails.disableFormElement($(this)); | |
197 | 240 | }); |
198 | 241 | }, |
199 | 242 | |
243 | + disableFormElement: function(element) { | |
244 | + var method, replacement; | |
245 | + | |
246 | + method = element.is('button') ? 'html' : 'val'; | |
247 | + replacement = element.data('disable-with'); | |
248 | + | |
249 | + element.data('ujs:enable-with', element[method]()); | |
250 | + if (replacement !== undefined) { | |
251 | + element[method](replacement); | |
252 | + } | |
253 | + | |
254 | + element.prop('disabled', true); | |
255 | + }, | |
256 | + | |
200 | 257 | /* Re-enables disabled form elements: |
201 | 258 | - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`) |
202 | 259 | - Sets disabled property to false |
203 | 260 | */ |
204 | 261 | enableFormElements: function(form) { |
205 | - form.find(rails.enableSelector).each(function() { | |
206 | - var element = $(this), method = element.is('button') ? 'html' : 'val'; | |
207 | - if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with')); | |
208 | - element.prop('disabled', false); | |
262 | + rails.formElements(form, rails.enableSelector).each(function() { | |
263 | + rails.enableFormElement($(this)); | |
209 | 264 | }); |
210 | 265 | }, |
211 | 266 | |
267 | + enableFormElement: function(element) { | |
268 | + var method = element.is('button') ? 'html' : 'val'; | |
269 | + if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with')); | |
270 | + element.prop('disabled', false); | |
271 | + }, | |
272 | + | |
212 | 273 | /* For 'data-confirm' attribute: |
213 | 274 | - Fires `confirm` event |
214 | 275 | - Shows the confirmation dialog |
... | ... | @@ -225,7 +286,11 @@ |
225 | 286 | if (!message) { return true; } |
226 | 287 | |
227 | 288 | if (rails.fire(element, 'confirm')) { |
228 | - answer = rails.confirm(message); | |
289 | + try { | |
290 | + answer = rails.confirm(message); | |
291 | + } catch (e) { | |
292 | + (console.error || console.log).call(console, e.stack || e); | |
293 | + } | |
229 | 294 | callback = rails.fire(element, 'confirm:complete', [answer]); |
230 | 295 | } |
231 | 296 | return answer && callback; |
... | ... | @@ -239,9 +304,8 @@ |
239 | 304 | |
240 | 305 | allInputs.each(function() { |
241 | 306 | input = $(this); |
242 | - valueToCheck = input.is('input[type=checkbox],input[type=radio]') ? input.is(':checked') : input.val(); | |
243 | - // If nonBlank and valueToCheck are both truthy, or nonBlank and valueToCheck are both falsey | |
244 | - if (!valueToCheck === !nonBlank) { | |
307 | + valueToCheck = input.is('input[type=checkbox],input[type=radio]') ? input.is(':checked') : !!input.val(); | |
308 | + if (valueToCheck === nonBlank) { | |
245 | 309 | |
246 | 310 | // Don't count unchecked required radio if other radio with same name is checked |
247 | 311 | if (input.is('input[type=radio]') && allInputs.filter('input[type=radio]:checked[name="' + input.attr('name') + '"]').length) { |
... | ... | @@ -269,8 +333,13 @@ |
269 | 333 | // replace element's html with the 'data-disable-with' after storing original html |
270 | 334 | // and prevent clicking on it |
271 | 335 | disableElement: function(element) { |
336 | + var replacement = element.data('disable-with'); | |
337 | + | |
272 | 338 | element.data('ujs:enable-with', element.html()); // store enabled state |
273 | - element.html(element.data('disable-with')); // set to disabled state | |
339 | + if (replacement !== undefined) { | |
340 | + element.html(replacement); | |
341 | + } | |
342 | + | |
274 | 343 | element.bind('click.railsDisable', function(e) { // prevent further clicking |
275 | 344 | return rails.stopEverything(e); |
276 | 345 | }); |
... | ... | @@ -284,24 +353,50 @@ |
284 | 353 | } |
285 | 354 | element.unbind('click.railsDisable'); // enable element |
286 | 355 | } |
287 | - | |
288 | 356 | }; |
289 | 357 | |
290 | 358 | if (rails.fire($document, 'rails:attachBindings')) { |
291 | 359 | |
292 | 360 | $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }}); |
293 | 361 | |
362 | + // This event works the same as the load event, except that it fires every | |
363 | + // time the page is loaded. | |
364 | + // | |
365 | + // See https://github.com/rails/jquery-ujs/issues/357 | |
366 | + // See https://developer.mozilla.org/en-US/docs/Using_Firefox_1.5_caching | |
367 | + $(window).on('pageshow.rails', function () { | |
368 | + $($.rails.enableSelector).each(function () { | |
369 | + var element = $(this); | |
370 | + | |
371 | + if (element.data('ujs:enable-with')) { | |
372 | + $.rails.enableFormElement(element); | |
373 | + } | |
374 | + }); | |
375 | + | |
376 | + $($.rails.linkDisableSelector).each(function () { | |
377 | + var element = $(this); | |
378 | + | |
379 | + if (element.data('ujs:enable-with')) { | |
380 | + $.rails.enableElement(element); | |
381 | + } | |
382 | + }); | |
383 | + }); | |
384 | + | |
294 | 385 | $document.delegate(rails.linkDisableSelector, 'ajax:complete', function() { |
295 | 386 | rails.enableElement($(this)); |
296 | 387 | }); |
297 | 388 | |
389 | + $document.delegate(rails.buttonDisableSelector, 'ajax:complete', function() { | |
390 | + rails.enableFormElement($(this)); | |
391 | + }); | |
392 | + | |
298 | 393 | $document.delegate(rails.linkClickSelector, 'click.rails', function(e) { |
299 | 394 | var link = $(this), method = link.data('method'), data = link.data('params'), metaClick = e.metaKey || e.ctrlKey; |
300 | 395 | if (!rails.allowAction(link)) return rails.stopEverything(e); |
301 | 396 | |
302 | 397 | if (!metaClick && link.is(rails.linkDisableSelector)) rails.disableElement(link); |
303 | 398 | |
304 | - if (link.data('remote') !== undefined) { | |
399 | + if (rails.isRemote(link)) { | |
305 | 400 | if (metaClick && (!method || method === 'GET') && !data) { return true; } |
306 | 401 | |
307 | 402 | var handleRemote = rails.handleRemote(link); |
... | ... | @@ -309,11 +404,11 @@ |
309 | 404 | if (handleRemote === false) { |
310 | 405 | rails.enableElement(link); |
311 | 406 | } else { |
312 | - handleRemote.error( function() { rails.enableElement(link); } ); | |
407 | + handleRemote.fail( function() { rails.enableElement(link); } ); | |
313 | 408 | } |
314 | 409 | return false; |
315 | 410 | |
316 | - } else if (link.data('method')) { | |
411 | + } else if (method) { | |
317 | 412 | rails.handleMethod(link); |
318 | 413 | return false; |
319 | 414 | } |
... | ... | @@ -321,15 +416,24 @@ |
321 | 416 | |
322 | 417 | $document.delegate(rails.buttonClickSelector, 'click.rails', function(e) { |
323 | 418 | var button = $(this); |
324 | - if (!rails.allowAction(button)) return rails.stopEverything(e); | |
325 | 419 | |
326 | - rails.handleRemote(button); | |
420 | + if (!rails.allowAction(button) || !rails.isRemote(button)) return rails.stopEverything(e); | |
421 | + | |
422 | + if (button.is(rails.buttonDisableSelector)) rails.disableFormElement(button); | |
423 | + | |
424 | + var handleRemote = rails.handleRemote(button); | |
425 | + // response from rails.handleRemote() will either be false or a deferred object promise. | |
426 | + if (handleRemote === false) { | |
427 | + rails.enableFormElement(button); | |
428 | + } else { | |
429 | + handleRemote.fail( function() { rails.enableFormElement(button); } ); | |
430 | + } | |
327 | 431 | return false; |
328 | 432 | }); |
329 | 433 | |
330 | 434 | $document.delegate(rails.inputChangeSelector, 'change.rails', function(e) { |
331 | 435 | var link = $(this); |
332 | - if (!rails.allowAction(link)) return rails.stopEverything(e); | |
436 | + if (!rails.allowAction(link) || !rails.isRemote(link)) return rails.stopEverything(e); | |
333 | 437 | |
334 | 438 | rails.handleRemote(link); |
335 | 439 | return false; |
... | ... | @@ -337,18 +441,22 @@ |
337 | 441 | |
338 | 442 | $document.delegate(rails.formSubmitSelector, 'submit.rails', function(e) { |
339 | 443 | var form = $(this), |
340 | - remote = form.data('remote') !== undefined, | |
341 | - blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector), | |
342 | - nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); | |
444 | + remote = rails.isRemote(form), | |
445 | + blankRequiredInputs, | |
446 | + nonBlankFileInputs; | |
343 | 447 | |
344 | 448 | if (!rails.allowAction(form)) return rails.stopEverything(e); |
345 | 449 | |
346 | 450 | // skip other logic when required values are missing or file upload is present |
347 | - if (blankRequiredInputs && form.attr("novalidate") == undefined && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) { | |
348 | - return rails.stopEverything(e); | |
451 | + if (form.attr('novalidate') === undefined) { | |
452 | + blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector, false); | |
453 | + if (blankRequiredInputs && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) { | |
454 | + return rails.stopEverything(e); | |
455 | + } | |
349 | 456 | } |
350 | 457 | |
351 | 458 | if (remote) { |
459 | + nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); | |
352 | 460 | if (nonBlankFileInputs) { |
353 | 461 | // slight timeout so that the submit button gets properly serialized |
354 | 462 | // (make it easy for event handler to serialize form without disabled values) |
... | ... | @@ -382,12 +490,12 @@ |
382 | 490 | button.closest('form').data('ujs:submit-button', data); |
383 | 491 | }); |
384 | 492 | |
385 | - $document.delegate(rails.formSubmitSelector, 'ajax:beforeSend.rails', function(event) { | |
386 | - if (this == event.target) rails.disableFormElements($(this)); | |
493 | + $document.delegate(rails.formSubmitSelector, 'ajax:send.rails', function(event) { | |
494 | + if (this === event.target) rails.disableFormElements($(this)); | |
387 | 495 | }); |
388 | 496 | |
389 | 497 | $document.delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) { |
390 | - if (this == event.target) rails.enableFormElements($(this)); | |
498 | + if (this === event.target) rails.enableFormElements($(this)); | |
391 | 499 | }); |
392 | 500 | |
393 | 501 | $(function(){ | ... | ... |