rails-0ef96db73ac7041f07f39160115c8aeafeef288a4d9d1f9fe8a3022c3b9c1be7.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. (function($, undefined) {
  2. /**
  3. * Unobtrusive scripting adapter for jQuery
  4. * https://github.com/rails/jquery-ujs
  5. *
  6. * Requires jQuery 1.8.0 or later.
  7. *
  8. * Released under the MIT license
  9. *
  10. */
  11. // Cut down on the number of issues from people inadvertently including jquery_ujs twice
  12. // by detecting and raising an error when it happens.
  13. 'use strict';
  14. if ( $.rails !== undefined ) {
  15. $.error('jquery-ujs has already been loaded!');
  16. }
  17. // Shorthand to make it a little easier to call public rails functions from within rails.js
  18. var rails;
  19. var $document = $(document);
  20. $.rails = rails = {
  21. // Link elements bound by jquery-ujs
  22. linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]',
  23. // Button elements bound by jquery-ujs
  24. buttonClickSelector: 'button[data-remote]:not(form button), button[data-confirm]:not(form button)',
  25. // Select elements bound by jquery-ujs
  26. inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]',
  27. // Form elements bound by jquery-ujs
  28. formSubmitSelector: 'form',
  29. // Form input elements bound by jquery-ujs
  30. 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])',
  31. // Form input elements disabled during form submission
  32. 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',
  33. // Form input elements re-enabled after form submission
  34. 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',
  35. // Form required input elements
  36. requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])',
  37. // Form file input elements
  38. fileInputSelector: 'input[type=file]:not([disabled])',
  39. // Link onClick disable selector with possible reenable after remote submission
  40. linkDisableSelector: 'a[data-disable-with], a[data-disable]',
  41. // Button onClick disable selector with possible reenable after remote submission
  42. buttonDisableSelector: 'button[data-remote][data-disable-with], button[data-remote][data-disable]',
  43. // Up-to-date Cross-Site Request Forgery token
  44. csrfToken: function() {
  45. return $('meta[name=csrf-token]').attr('content');
  46. },
  47. // URL param that must contain the CSRF token
  48. csrfParam: function() {
  49. return $('meta[name=csrf-param]').attr('content');
  50. },
  51. // Make sure that every Ajax request sends the CSRF token
  52. CSRFProtection: function(xhr) {
  53. var token = rails.csrfToken();
  54. if (token) xhr.setRequestHeader('X-CSRF-Token', token);
  55. },
  56. // Make sure that all forms have actual up-to-date tokens (cached forms contain old ones)
  57. refreshCSRFTokens: function(){
  58. $('form input[name="' + rails.csrfParam() + '"]').val(rails.csrfToken());
  59. },
  60. // Triggers an event on an element and returns false if the event result is false
  61. fire: function(obj, name, data) {
  62. var event = $.Event(name);
  63. obj.trigger(event, data);
  64. return event.result !== false;
  65. },
  66. // Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm
  67. confirm: function(message) {
  68. return confirm(message);
  69. },
  70. // Default ajax function, may be overridden with custom function in $.rails.ajax
  71. ajax: function(options) {
  72. return $.ajax(options);
  73. },
  74. // Default way to get an element's href. May be overridden at $.rails.href.
  75. href: function(element) {
  76. return element[0].href;
  77. },
  78. // Checks "data-remote" if true to handle the request through a XHR request.
  79. isRemote: function(element) {
  80. return element.data('remote') !== undefined && element.data('remote') !== false;
  81. },
  82. // Submits "remote" forms and links with ajax
  83. handleRemote: function(element) {
  84. var method, url, data, withCredentials, dataType, options;
  85. if (rails.fire(element, 'ajax:before')) {
  86. withCredentials = element.data('with-credentials') || null;
  87. dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType);
  88. if (element.is('form')) {
  89. method = element.attr('method');
  90. url = element.attr('action');
  91. data = element.serializeArray();
  92. // memoized value from clicked submit button
  93. var button = element.data('ujs:submit-button');
  94. if (button) {
  95. data.push(button);
  96. element.data('ujs:submit-button', null);
  97. }
  98. } else if (element.is(rails.inputChangeSelector)) {
  99. method = element.data('method');
  100. url = element.data('url');
  101. data = element.serialize();
  102. if (element.data('params')) data = data + '&' + element.data('params');
  103. } else if (element.is(rails.buttonClickSelector)) {
  104. method = element.data('method') || 'get';
  105. url = element.data('url');
  106. data = element.serialize();
  107. if (element.data('params')) data = data + '&' + element.data('params');
  108. } else {
  109. method = element.data('method');
  110. url = rails.href(element);
  111. data = element.data('params') || null;
  112. }
  113. options = {
  114. type: method || 'GET', data: data, dataType: dataType,
  115. // stopping the "ajax:beforeSend" event will cancel the ajax request
  116. beforeSend: function(xhr, settings) {
  117. if (settings.dataType === undefined) {
  118. xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
  119. }
  120. if (rails.fire(element, 'ajax:beforeSend', [xhr, settings])) {
  121. element.trigger('ajax:send', xhr);
  122. } else {
  123. return false;
  124. }
  125. },
  126. success: function(data, status, xhr) {
  127. element.trigger('ajax:success', [data, status, xhr]);
  128. },
  129. complete: function(xhr, status) {
  130. element.trigger('ajax:complete', [xhr, status]);
  131. },
  132. error: function(xhr, status, error) {
  133. element.trigger('ajax:error', [xhr, status, error]);
  134. },
  135. crossDomain: rails.isCrossDomain(url)
  136. };
  137. // There is no withCredentials for IE6-8 when
  138. // "Enable native XMLHTTP support" is disabled
  139. if (withCredentials) {
  140. options.xhrFields = {
  141. withCredentials: withCredentials
  142. };
  143. }
  144. // Only pass url to `ajax` options if not blank
  145. if (url) { options.url = url; }
  146. return rails.ajax(options);
  147. } else {
  148. return false;
  149. }
  150. },
  151. // Determines if the request is a cross domain request.
  152. isCrossDomain: function(url) {
  153. var originAnchor = document.createElement('a');
  154. originAnchor.href = location.href;
  155. var urlAnchor = document.createElement('a');
  156. try {
  157. urlAnchor.href = url;
  158. // This is a workaround to a IE bug.
  159. urlAnchor.href = urlAnchor.href;
  160. // If URL protocol is false or is a string containing a single colon
  161. // *and* host are false, assume it is not a cross-domain request
  162. // (should only be the case for IE7 and IE compatibility mode).
  163. // Otherwise, evaluate protocol and host of the URL against the origin
  164. // protocol and host.
  165. return !(((!urlAnchor.protocol || urlAnchor.protocol === ':') && !urlAnchor.host) ||
  166. (originAnchor.protocol + '//' + originAnchor.host ===
  167. urlAnchor.protocol + '//' + urlAnchor.host));
  168. } catch (e) {
  169. // If there is an error parsing the URL, assume it is crossDomain.
  170. return true;
  171. }
  172. },
  173. // Handles "data-method" on links such as:
  174. // <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
  175. handleMethod: function(link) {
  176. var href = rails.href(link),
  177. method = link.data('method'),
  178. target = link.attr('target'),
  179. csrfToken = rails.csrfToken(),
  180. csrfParam = rails.csrfParam(),
  181. form = $('<form method="post" action="' + href + '"></form>'),
  182. metadataInput = '<input name="_method" value="' + method + '" type="hidden" />';
  183. if (csrfParam !== undefined && csrfToken !== undefined && !rails.isCrossDomain(href)) {
  184. metadataInput += '<input name="' + csrfParam + '" value="' + csrfToken + '" type="hidden" />';
  185. }
  186. if (target) { form.attr('target', target); }
  187. form.hide().append(metadataInput).appendTo('body');
  188. form.submit();
  189. },
  190. // Helper function that returns form elements that match the specified CSS selector
  191. // If form is actually a "form" element this will return associated elements outside the from that have
  192. // the html form attribute set
  193. formElements: function(form, selector) {
  194. return form.is('form') ? $(form[0].elements).filter(selector) : form.find(selector);
  195. },
  196. /* Disables form elements:
  197. - Caches element value in 'ujs:enable-with' data store
  198. - Replaces element text with value of 'data-disable-with' attribute
  199. - Sets disabled property to true
  200. */
  201. disableFormElements: function(form) {
  202. rails.formElements(form, rails.disableSelector).each(function() {
  203. rails.disableFormElement($(this));
  204. });
  205. },
  206. disableFormElement: function(element) {
  207. var method, replacement;
  208. method = element.is('button') ? 'html' : 'val';
  209. replacement = element.data('disable-with');
  210. if (replacement !== undefined) {
  211. element.data('ujs:enable-with', element[method]());
  212. element[method](replacement);
  213. }
  214. element.prop('disabled', true);
  215. element.data('ujs:disabled', true);
  216. },
  217. /* Re-enables disabled form elements:
  218. - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`)
  219. - Sets disabled property to false
  220. */
  221. enableFormElements: function(form) {
  222. rails.formElements(form, rails.enableSelector).each(function() {
  223. rails.enableFormElement($(this));
  224. });
  225. },
  226. enableFormElement: function(element) {
  227. var method = element.is('button') ? 'html' : 'val';
  228. if (element.data('ujs:enable-with') !== undefined) {
  229. element[method](element.data('ujs:enable-with'));
  230. element.removeData('ujs:enable-with'); // clean up cache
  231. }
  232. element.prop('disabled', false);
  233. element.removeData('ujs:disabled');
  234. },
  235. /* For 'data-confirm' attribute:
  236. - Fires `confirm` event
  237. - Shows the confirmation dialog
  238. - Fires the `confirm:complete` event
  239. Returns `true` if no function stops the chain and user chose yes; `false` otherwise.
  240. Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog.
  241. Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function
  242. return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog.
  243. */
  244. allowAction: function(element) {
  245. var message = element.data('confirm'),
  246. answer = false, callback;
  247. if (!message) { return true; }
  248. if (rails.fire(element, 'confirm')) {
  249. try {
  250. answer = rails.confirm(message);
  251. } catch (e) {
  252. (console.error || console.log).call(console, e.stack || e);
  253. }
  254. callback = rails.fire(element, 'confirm:complete', [answer]);
  255. }
  256. return answer && callback;
  257. },
  258. // Helper function which checks for blank inputs in a form that match the specified CSS selector
  259. blankInputs: function(form, specifiedSelector, nonBlank) {
  260. var inputs = $(), input, valueToCheck,
  261. selector = specifiedSelector || 'input,textarea',
  262. allInputs = form.find(selector);
  263. allInputs.each(function() {
  264. input = $(this);
  265. valueToCheck = input.is('input[type=checkbox],input[type=radio]') ? input.is(':checked') : !!input.val();
  266. if (valueToCheck === nonBlank) {
  267. // Don't count unchecked required radio if other radio with same name is checked
  268. if (input.is('input[type=radio]') && allInputs.filter('input[type=radio]:checked[name="' + input.attr('name') + '"]').length) {
  269. return true; // Skip to next input
  270. }
  271. inputs = inputs.add(input);
  272. }
  273. });
  274. return inputs.length ? inputs : false;
  275. },
  276. // Helper function which checks for non-blank inputs in a form that match the specified CSS selector
  277. nonBlankInputs: function(form, specifiedSelector) {
  278. return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank
  279. },
  280. // Helper function, needed to provide consistent behavior in IE
  281. stopEverything: function(e) {
  282. $(e.target).trigger('ujs:everythingStopped');
  283. e.stopImmediatePropagation();
  284. return false;
  285. },
  286. // Replace element's html with the 'data-disable-with' after storing original html
  287. // and prevent clicking on it
  288. disableElement: function(element) {
  289. var replacement = element.data('disable-with');
  290. if (replacement !== undefined) {
  291. element.data('ujs:enable-with', element.html()); // store enabled state
  292. element.html(replacement);
  293. }
  294. element.bind('click.railsDisable', function(e) { // prevent further clicking
  295. return rails.stopEverything(e);
  296. });
  297. element.data('ujs:disabled', true);
  298. },
  299. // Restore element to its original state which was disabled by 'disableElement' above
  300. enableElement: function(element) {
  301. if (element.data('ujs:enable-with') !== undefined) {
  302. element.html(element.data('ujs:enable-with')); // set to old enabled state
  303. element.removeData('ujs:enable-with'); // clean up cache
  304. }
  305. element.unbind('click.railsDisable'); // enable element
  306. element.removeData('ujs:disabled');
  307. }
  308. };
  309. if (rails.fire($document, 'rails:attachBindings')) {
  310. $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }});
  311. // This event works the same as the load event, except that it fires every
  312. // time the page is loaded.
  313. //
  314. // See https://github.com/rails/jquery-ujs/issues/357
  315. // See https://developer.mozilla.org/en-US/docs/Using_Firefox_1.5_caching
  316. $(window).on('pageshow.rails', function () {
  317. $($.rails.enableSelector).each(function () {
  318. var element = $(this);
  319. if (element.data('ujs:disabled')) {
  320. $.rails.enableFormElement(element);
  321. }
  322. });
  323. $($.rails.linkDisableSelector).each(function () {
  324. var element = $(this);
  325. if (element.data('ujs:disabled')) {
  326. $.rails.enableElement(element);
  327. }
  328. });
  329. });
  330. $document.delegate(rails.linkDisableSelector, 'ajax:complete', function() {
  331. rails.enableElement($(this));
  332. });
  333. $document.delegate(rails.buttonDisableSelector, 'ajax:complete', function() {
  334. rails.enableFormElement($(this));
  335. });
  336. $document.delegate(rails.linkClickSelector, 'click.rails', function(e) {
  337. var link = $(this), method = link.data('method'), data = link.data('params'), metaClick = e.metaKey || e.ctrlKey;
  338. if (!rails.allowAction(link)) return rails.stopEverything(e);
  339. if (!metaClick && link.is(rails.linkDisableSelector)) rails.disableElement(link);
  340. if (rails.isRemote(link)) {
  341. if (metaClick && (!method || method === 'GET') && !data) { return true; }
  342. var handleRemote = rails.handleRemote(link);
  343. // Response from rails.handleRemote() will either be false or a deferred object promise.
  344. if (handleRemote === false) {
  345. rails.enableElement(link);
  346. } else {
  347. handleRemote.fail( function() { rails.enableElement(link); } );
  348. }
  349. return false;
  350. } else if (method) {
  351. rails.handleMethod(link);
  352. return false;
  353. }
  354. });
  355. $document.delegate(rails.buttonClickSelector, 'click.rails', function(e) {
  356. var button = $(this);
  357. if (!rails.allowAction(button) || !rails.isRemote(button)) return rails.stopEverything(e);
  358. if (button.is(rails.buttonDisableSelector)) rails.disableFormElement(button);
  359. var handleRemote = rails.handleRemote(button);
  360. // Response from rails.handleRemote() will either be false or a deferred object promise.
  361. if (handleRemote === false) {
  362. rails.enableFormElement(button);
  363. } else {
  364. handleRemote.fail( function() { rails.enableFormElement(button); } );
  365. }
  366. return false;
  367. });
  368. $document.delegate(rails.inputChangeSelector, 'change.rails', function(e) {
  369. var link = $(this);
  370. if (!rails.allowAction(link) || !rails.isRemote(link)) return rails.stopEverything(e);
  371. rails.handleRemote(link);
  372. return false;
  373. });
  374. $document.delegate(rails.formSubmitSelector, 'submit.rails', function(e) {
  375. var form = $(this),
  376. remote = rails.isRemote(form),
  377. blankRequiredInputs,
  378. nonBlankFileInputs;
  379. if (!rails.allowAction(form)) return rails.stopEverything(e);
  380. // Skip other logic when required values are missing or file upload is present
  381. if (form.attr('novalidate') === undefined) {
  382. if (form.data('ujs:formnovalidate-button') === undefined) {
  383. blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector, false);
  384. if (blankRequiredInputs && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) {
  385. return rails.stopEverything(e);
  386. }
  387. } else {
  388. // Clear the formnovalidate in case the next button click is not on a formnovalidate button
  389. // Not strictly necessary to do here, since it is also reset on each button click, but just to be certain
  390. form.data('ujs:formnovalidate-button', undefined);
  391. }
  392. }
  393. if (remote) {
  394. nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector);
  395. if (nonBlankFileInputs) {
  396. // Slight timeout so that the submit button gets properly serialized
  397. // (make it easy for event handler to serialize form without disabled values)
  398. setTimeout(function(){ rails.disableFormElements(form); }, 13);
  399. var aborted = rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]);
  400. // Re-enable form elements if event bindings return false (canceling normal form submission)
  401. if (!aborted) { setTimeout(function(){ rails.enableFormElements(form); }, 13); }
  402. return aborted;
  403. }
  404. rails.handleRemote(form);
  405. return false;
  406. } else {
  407. // Slight timeout so that the submit button gets properly serialized
  408. setTimeout(function(){ rails.disableFormElements(form); }, 13);
  409. }
  410. });
  411. $document.delegate(rails.formInputClickSelector, 'click.rails', function(event) {
  412. var button = $(this);
  413. if (!rails.allowAction(button)) return rails.stopEverything(event);
  414. // Register the pressed submit button
  415. var name = button.attr('name'),
  416. data = name ? {name:name, value:button.val()} : null;
  417. var form = button.closest('form');
  418. form.data('ujs:submit-button', data);
  419. // Save formnovalidate attribute from button
  420. form.data('ujs:formnovalidate-button', button.attr('formnovalidate'));
  421. });
  422. $document.delegate(rails.formSubmitSelector, 'ajax:send.rails', function(event) {
  423. if (this === event.target) rails.disableFormElements($(this));
  424. });
  425. $document.delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) {
  426. if (this === event.target) rails.enableFormElements($(this));
  427. });
  428. $(function(){
  429. rails.refreshCSRFTokens();
  430. });
  431. }
  432. })( jQuery );