Commit bcf8f0808d409c27d8452982681997c8a6e0dfb5

Authored by Braulio Bhavamitra
1 parent 6ee575f1

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(){
... ...