yii.activeForm.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. /**
  2. * Yii form widget.
  3. *
  4. * This is the JavaScript widget used by the yii\widgets\ActiveForm widget.
  5. *
  6. * @link http://www.yiiframework.com/
  7. * @copyright Copyright (c) 2008 Yii Software LLC
  8. * @license http://www.yiiframework.com/license/
  9. * @author Qiang Xue <[email protected]>
  10. * @since 2.0
  11. */
  12. (function ($) {
  13. $.fn.yiiActiveForm = function (method) {
  14. if (methods[method]) {
  15. return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
  16. } else if (typeof method === 'object' || !method) {
  17. return methods.init.apply(this, arguments);
  18. } else {
  19. $.error('Method ' + method + ' does not exist on jQuery.yiiActiveForm');
  20. return false;
  21. }
  22. };
  23. var defaults = {
  24. // the jQuery selector for the error summary
  25. errorSummary: undefined,
  26. // whether to perform validation before submitting the form.
  27. validateOnSubmit: true,
  28. // the container CSS class representing the corresponding attribute has validation error
  29. errorCssClass: 'error',
  30. // the container CSS class representing the corresponding attribute passes validation
  31. successCssClass: 'success',
  32. // the container CSS class representing the corresponding attribute is being validated
  33. validatingCssClass: 'validating',
  34. // the URL for performing AJAX-based validation. If not set, it will use the the form's action
  35. validationUrl: undefined,
  36. // a callback that is called before submitting the form. The signature of the callback should be:
  37. // function ($form) { ...return false to cancel submission...}
  38. beforeSubmit: undefined,
  39. // a callback that is called before validating each attribute. The signature of the callback should be:
  40. // function ($form, attribute, messages) { ...return false to cancel the validation...}
  41. beforeValidate: undefined,
  42. // a callback that is called after an attribute is validated. The signature of the callback should be:
  43. // function ($form, attribute, messages)
  44. afterValidate: undefined,
  45. // the GET parameter name indicating an AJAX-based validation
  46. ajaxVar: 'ajax'
  47. };
  48. var attributeDefaults = {
  49. // attribute name or expression (e.g. "[0]content" for tabular input)
  50. name: undefined,
  51. // the jQuery selector of the container of the input field
  52. container: undefined,
  53. // the jQuery selector of the input field
  54. input: undefined,
  55. // the jQuery selector of the error tag
  56. error: undefined,
  57. // whether to perform validation when a change is detected on the input
  58. validateOnChange: false,
  59. // whether to perform validation when the user is typing.
  60. validateOnType: false,
  61. // number of milliseconds that the validation should be delayed when a user is typing in the input field.
  62. validationDelay: 200,
  63. // whether to enable AJAX-based validation.
  64. enableAjaxValidation: false,
  65. // function (attribute, value, messages), the client-side validation function.
  66. validate: undefined,
  67. // status of the input field, 0: empty, not entered before, 1: validated, 2: pending validation, 3: validating
  68. status: 0,
  69. // the value of the input
  70. value: undefined
  71. };
  72. var methods = {
  73. init: function (attributes, options) {
  74. return this.each(function () {
  75. var $form = $(this);
  76. if ($form.data('yiiActiveForm')) {
  77. return;
  78. }
  79. var settings = $.extend({}, defaults, options || {});
  80. if (settings.validationUrl === undefined) {
  81. settings.validationUrl = $form.prop('action');
  82. }
  83. $.each(attributes, function (i) {
  84. attributes[i] = $.extend({value: getValue($form, this)}, attributeDefaults, this);
  85. });
  86. $form.data('yiiActiveForm', {
  87. settings: settings,
  88. attributes: attributes,
  89. submitting: false,
  90. validated: false
  91. });
  92. watchAttributes($form, attributes);
  93. /**
  94. * Clean up error status when the form is reset.
  95. * Note that $form.on('reset', ...) does work because the "reset" event does not bubble on IE.
  96. */
  97. $form.bind('reset.yiiActiveForm', methods.resetForm);
  98. if (settings.validateOnSubmit) {
  99. $form.on('mouseup.yiiActiveForm keyup.yiiActiveForm', ':submit', function () {
  100. $form.data('yiiActiveForm').submitObject = $(this);
  101. });
  102. $form.on('submit', methods.submitForm);
  103. }
  104. });
  105. },
  106. destroy: function () {
  107. return this.each(function () {
  108. $(window).unbind('.yiiActiveForm');
  109. $(this).removeData('yiiActiveForm');
  110. });
  111. },
  112. data: function() {
  113. return this.data('yiiActiveForm');
  114. },
  115. submitForm: function () {
  116. var $form = $(this),
  117. data = $form.data('yiiActiveForm');
  118. if (data.validated) {
  119. // continue submitting the form since validation passes
  120. return true;
  121. }
  122. if (data.settings.timer !== undefined) {
  123. clearTimeout(data.settings.timer);
  124. }
  125. data.submitting = true;
  126. if (!data.settings.beforeSubmit || data.settings.beforeSubmit($form)) {
  127. validate($form, function (messages) {
  128. var errors = [];
  129. $.each(data.attributes, function () {
  130. if (updateInput($form, this, messages)) {
  131. errors.push(this.input);
  132. }
  133. });
  134. updateSummary($form, messages);
  135. if (errors.length) {
  136. var top = $form.find(errors.join(',')).first().offset().top;
  137. var wtop = $(window).scrollTop();
  138. if (top < wtop || top > wtop + $(window).height) {
  139. $(window).scrollTop(top);
  140. }
  141. } else {
  142. data.validated = true;
  143. var $button = data.submitObject || $form.find(':submit:first');
  144. // TODO: if the submission is caused by "change" event, it will not work
  145. if ($button.length) {
  146. $button.click();
  147. } else {
  148. // no submit button in the form
  149. $form.submit();
  150. }
  151. return;
  152. }
  153. data.submitting = false;
  154. }, function () {
  155. data.submitting = false;
  156. });
  157. } else {
  158. data.submitting = false;
  159. }
  160. return false;
  161. },
  162. resetForm: function () {
  163. var $form = $(this);
  164. var data = $form.data('yiiActiveForm');
  165. // Because we bind directly to a form reset event instead of a reset button (that may not exist),
  166. // when this function is executed form input values have not been reset yet.
  167. // Therefore we do the actual reset work through setTimeout.
  168. setTimeout(function () {
  169. $.each(data.attributes, function () {
  170. // Without setTimeout() we would get the input values that are not reset yet.
  171. this.value = getValue($form, this);
  172. this.status = 0;
  173. var $container = $form.find(this.container);
  174. $container.removeClass(
  175. data.settings.validatingCssClass + ' ' +
  176. data.settings.errorCssClass + ' ' +
  177. data.settings.successCssClass
  178. );
  179. $container.find(this.error).html('');
  180. });
  181. $form.find(data.settings.summary).hide().find('ul').html('');
  182. }, 1);
  183. }
  184. };
  185. var watchAttributes = function ($form, attributes) {
  186. $.each(attributes, function (i, attribute) {
  187. var $input = findInput($form, attribute);
  188. if (attribute.validateOnChange) {
  189. $input.on('change.yiiActiveForm', function () {
  190. validateAttribute($form, attribute, false);
  191. }).on('blur.yiiActiveForm', function () {
  192. if (attribute.status == 0 || attribute.status == 1) {
  193. validateAttribute($form, attribute, !attribute.status);
  194. }
  195. });
  196. }
  197. if (attribute.validateOnType) {
  198. $input.on('keyup.yiiActiveForm', function () {
  199. if (attribute.value !== getValue($form, attribute)) {
  200. validateAttribute($form, attribute, false);
  201. }
  202. });
  203. }
  204. });
  205. };
  206. var validateAttribute = function ($form, attribute, forceValidate) {
  207. var data = $form.data('yiiActiveForm');
  208. if (forceValidate) {
  209. attribute.status = 2;
  210. }
  211. $.each(data.attributes, function () {
  212. if (this.value !== getValue($form, this)) {
  213. this.status = 2;
  214. forceValidate = true;
  215. }
  216. });
  217. if (!forceValidate) {
  218. return;
  219. }
  220. if (data.settings.timer !== undefined) {
  221. clearTimeout(data.settings.timer);
  222. }
  223. data.settings.timer = setTimeout(function () {
  224. if (data.submitting || $form.is(':hidden')) {
  225. return;
  226. }
  227. $.each(data.attributes, function () {
  228. if (this.status === 2) {
  229. this.status = 3;
  230. $form.find(this.container).addClass(data.settings.validatingCssClass);
  231. }
  232. });
  233. validate($form, function (messages) {
  234. var hasError = false;
  235. $.each(data.attributes, function () {
  236. if (this.status === 2 || this.status === 3) {
  237. hasError = updateInput($form, this, messages) || hasError;
  238. }
  239. });
  240. });
  241. }, data.settings.validationDelay);
  242. };
  243. /**
  244. * Performs validation.
  245. * @param $form jQuery the jquery representation of the form
  246. * @param successCallback function the function to be invoked if the validation completes
  247. * @param errorCallback function the function to be invoked if the ajax validation request fails
  248. */
  249. var validate = function ($form, successCallback, errorCallback) {
  250. var data = $form.data('yiiActiveForm'),
  251. needAjaxValidation = false,
  252. messages = {};
  253. $.each(data.attributes, function () {
  254. if (data.submitting || this.status === 2 || this.status === 3) {
  255. var msg = [];
  256. if (!data.settings.beforeValidate || data.settings.beforeValidate($form, this, msg)) {
  257. if (this.validate) {
  258. this.validate(this, getValue($form, this), msg);
  259. }
  260. if (msg.length) {
  261. messages[this.name] = msg;
  262. } else if (this.enableAjaxValidation) {
  263. needAjaxValidation = true;
  264. }
  265. }
  266. }
  267. });
  268. if (needAjaxValidation && (!data.submitting || $.isEmptyObject(messages))) {
  269. // Perform ajax validation when at least one input needs it.
  270. // If the validation is triggered by form submission, ajax validation
  271. // should be done only when all inputs pass client validation
  272. var $button = data.submitObject,
  273. extData = '&' + data.settings.ajaxVar + '=' + $form.prop('id');
  274. if ($button && $button.length && $button.prop('name')) {
  275. extData += '&' + $button.prop('name') + '=' + $button.prop('value');
  276. }
  277. $.ajax({
  278. url: data.settings.validationUrl,
  279. type: $form.prop('method'),
  280. data: $form.serialize() + extData,
  281. dataType: 'json',
  282. success: function (msgs) {
  283. if (msgs !== null && typeof msgs === 'object') {
  284. $.each(data.attributes, function () {
  285. if (!this.enableAjaxValidation) {
  286. delete msgs[this.name];
  287. }
  288. });
  289. successCallback($.extend({}, messages, msgs));
  290. } else {
  291. successCallback(messages);
  292. }
  293. },
  294. error: errorCallback
  295. });
  296. } else if (data.submitting) {
  297. // delay callback so that the form can be submitted without problem
  298. setTimeout(function () {
  299. successCallback(messages);
  300. }, 200);
  301. } else {
  302. successCallback(messages);
  303. }
  304. };
  305. /**
  306. * Updates the error message and the input container for a particular attribute.
  307. * @param $form the form jQuery object
  308. * @param attribute object the configuration for a particular attribute.
  309. * @param messages array the validation error messages
  310. * @return boolean whether there is a validation error for the specified attribute
  311. */
  312. var updateInput = function ($form, attribute, messages) {
  313. var data = $form.data('yiiActiveForm'),
  314. $input = findInput($form, attribute),
  315. hasError = false;
  316. if (data.settings.afterValidate) {
  317. data.settings.afterValidate($form, attribute, messages);
  318. }
  319. attribute.status = 1;
  320. if ($input.length) {
  321. hasError = messages && $.isArray(messages[attribute.name]) && messages[attribute.name].length;
  322. var $container = $form.find(attribute.container);
  323. var $error = $container.find(attribute.error);
  324. if (hasError) {
  325. $error.text(messages[attribute.name][0]);
  326. $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass)
  327. .addClass(data.settings.errorCssClass);
  328. } else {
  329. $error.text('');
  330. $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ')
  331. .addClass(data.settings.successCssClass);
  332. }
  333. attribute.value = getValue($form, attribute);
  334. }
  335. return hasError;
  336. };
  337. /**
  338. * Updates the error summary.
  339. * @param $form the form jQuery object
  340. * @param messages array the validation error messages
  341. */
  342. var updateSummary = function ($form, messages) {
  343. var data = $form.data('yiiActiveForm'),
  344. $summary = $form.find(data.settings.errorSummary),
  345. $ul = $summary.find('ul').html('');
  346. if ($summary.length && messages) {
  347. $.each(data.attributes, function () {
  348. if ($.isArray(messages[this.name]) && messages[this.name].length) {
  349. $ul.append($('<li/>').text(messages[this.name][0]));
  350. }
  351. });
  352. $summary.toggle($ul.find('li').length > 0);
  353. }
  354. };
  355. var getValue = function ($form, attribute) {
  356. var $input = findInput($form, attribute);
  357. var type = $input.prop('type');
  358. if (type === 'checkbox' || type === 'radio') {
  359. var $realInput = $input.filter(':checked');
  360. if (!$realInput.length) {
  361. $realInput = $form.find('input[type=hidden][name="'+$input.prop('name')+'"]');
  362. }
  363. return $realInput.val();
  364. } else {
  365. return $input.val();
  366. }
  367. };
  368. var findInput = function ($form, attribute) {
  369. var $input = $form.find(attribute.input);
  370. if ($input.length && $input[0].tagName.toLowerCase() === 'div') {
  371. // checkbox list or radio list
  372. return $input.find('input');
  373. } else {
  374. return $input;
  375. }
  376. };
  377. })(window.jQuery);