You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

616 lines
25KB

  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 <qiang.xue@gmail.com>
  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 events = {
  24. /**
  25. * beforeValidate event is triggered before validating the whole form.
  26. * The signature of the event handler should be:
  27. * function (event, messages, deferreds)
  28. * where
  29. * - event: an Event object.
  30. * - messages: an associative array with keys being attribute IDs and values being error message arrays
  31. * for the corresponding attributes.
  32. * - deferreds: an array of Deferred objects. You can use deferreds.add(callback) to add a new deferred validation.
  33. *
  34. * If the handler returns a boolean false, it will stop further form validation after this event. And as
  35. * a result, afterValidate event will not be triggered.
  36. */
  37. beforeValidate: 'beforeValidate',
  38. /**
  39. * afterValidate event is triggered after validating the whole form.
  40. * The signature of the event handler should be:
  41. * function (event, messages)
  42. * where
  43. * - event: an Event object.
  44. * - messages: an associative array with keys being attribute IDs and values being error message arrays
  45. * for the corresponding attributes.
  46. */
  47. afterValidate: 'afterValidate',
  48. /**
  49. * beforeValidateAttribute event is triggered before validating an attribute.
  50. * The signature of the event handler should be:
  51. * function (event, attribute, messages, deferreds)
  52. * where
  53. * - event: an Event object.
  54. * - attribute: the attribute to be validated. Please refer to attributeDefaults for the structure of this parameter.
  55. * - messages: an array to which you can add validation error messages for the specified attribute.
  56. * - deferreds: an array of Deferred objects. You can use deferreds.add(callback) to add a new deferred validation.
  57. *
  58. * If the handler returns a boolean false, it will stop further validation of the specified attribute.
  59. * And as a result, afterValidateAttribute event will not be triggered.
  60. */
  61. beforeValidateAttribute: 'beforeValidateAttribute',
  62. /**
  63. * afterValidateAttribute event is triggered after validating the whole form and each attribute.
  64. * The signature of the event handler should be:
  65. * function (event, attribute, messages)
  66. * where
  67. * - event: an Event object.
  68. * - attribute: the attribute being validated. Please refer to attributeDefaults for the structure of this parameter.
  69. * - messages: an array to which you can add additional validation error messages for the specified attribute.
  70. */
  71. afterValidateAttribute: 'afterValidateAttribute',
  72. /**
  73. * beforeSubmit event is triggered before submitting the form after all validations have passed.
  74. * The signature of the event handler should be:
  75. * function (event)
  76. * where event is an Event object.
  77. *
  78. * If the handler returns a boolean false, it will stop form submission.
  79. */
  80. beforeSubmit: 'beforeSubmit',
  81. /**
  82. * ajaxBeforeSend event is triggered before sending an AJAX request for AJAX-based validation.
  83. * The signature of the event handler should be:
  84. * function (event, jqXHR, settings)
  85. * where
  86. * - event: an Event object.
  87. * - jqXHR: a jqXHR object
  88. * - settings: the settings for the AJAX request
  89. */
  90. ajaxBeforeSend: 'ajaxBeforeSend',
  91. /**
  92. * ajaxComplete event is triggered after completing an AJAX request for AJAX-based validation.
  93. * The signature of the event handler should be:
  94. * function (event, jqXHR, textStatus)
  95. * where
  96. * - event: an Event object.
  97. * - jqXHR: a jqXHR object
  98. * - settings: the status of the request ("success", "notmodified", "error", "timeout", "abort", or "parsererror").
  99. */
  100. ajaxComplete: 'ajaxComplete'
  101. };
  102. // NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveForm::getClientOptions() as well
  103. var defaults = {
  104. // whether to encode the error summary
  105. encodeErrorSummary: true,
  106. // the jQuery selector for the error summary
  107. errorSummary: '.error-summary',
  108. // whether to perform validation before submitting the form.
  109. validateOnSubmit: true,
  110. // the container CSS class representing the corresponding attribute has validation error
  111. errorCssClass: 'has-error',
  112. // the container CSS class representing the corresponding attribute passes validation
  113. successCssClass: 'has-success',
  114. // the container CSS class representing the corresponding attribute is being validated
  115. validatingCssClass: 'validating',
  116. // the GET parameter name indicating an AJAX-based validation
  117. ajaxParam: 'ajax',
  118. // the type of data that you're expecting back from the server
  119. ajaxDataType: 'json',
  120. // the URL for performing AJAX-based validation. If not set, it will use the the form's action
  121. validationUrl: undefined
  122. };
  123. // NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveField::getClientOptions() as well
  124. var attributeDefaults = {
  125. // a unique ID identifying an attribute (e.g. "loginform-username") in a form
  126. id: undefined,
  127. // attribute name or expression (e.g. "[0]content" for tabular input)
  128. name: undefined,
  129. // the jQuery selector of the container of the input field
  130. container: undefined,
  131. // the jQuery selector of the input field under the context of the container
  132. input: undefined,
  133. // the jQuery selector of the error tag under the context of the container
  134. error: '.help-block',
  135. // whether to encode the error
  136. encodeError: true,
  137. // whether to perform validation when a change is detected on the input
  138. validateOnChange: true,
  139. // whether to perform validation when the input loses focus
  140. validateOnBlur: true,
  141. // whether to perform validation when the user is typing.
  142. validateOnType: false,
  143. // number of milliseconds that the validation should be delayed when a user is typing in the input field.
  144. validationDelay: 500,
  145. // whether to enable AJAX-based validation.
  146. enableAjaxValidation: false,
  147. // function (attribute, value, messages, deferred, $form), the client-side validation function.
  148. validate: undefined,
  149. // status of the input field, 0: empty, not entered before, 1: validated, 2: pending validation, 3: validating
  150. status: 0,
  151. // whether the validation is cancelled by beforeValidateAttribute event handler
  152. cancelled: false,
  153. // the value of the input
  154. value: undefined
  155. };
  156. var methods = {
  157. init: function (attributes, options) {
  158. return this.each(function () {
  159. var $form = $(this);
  160. if ($form.data('yiiActiveForm')) {
  161. return;
  162. }
  163. var settings = $.extend({}, defaults, options || {});
  164. if (settings.validationUrl === undefined) {
  165. settings.validationUrl = $form.prop('action');
  166. }
  167. $.each(attributes, function (i) {
  168. attributes[i] = $.extend({value: getValue($form, this)}, attributeDefaults, this);
  169. watchAttribute($form, attributes[i]);
  170. });
  171. $form.data('yiiActiveForm', {
  172. settings: settings,
  173. attributes: attributes,
  174. submitting: false,
  175. validated: false
  176. });
  177. /**
  178. * Clean up error status when the form is reset.
  179. * Note that $form.on('reset', ...) does work because the "reset" event does not bubble on IE.
  180. */
  181. $form.bind('reset.yiiActiveForm', methods.resetForm);
  182. if (settings.validateOnSubmit) {
  183. $form.on('mouseup.yiiActiveForm keyup.yiiActiveForm', ':submit', function () {
  184. $form.data('yiiActiveForm').submitObject = $(this);
  185. });
  186. $form.on('submit.yiiActiveForm', methods.submitForm);
  187. }
  188. });
  189. },
  190. // add a new attribute to the form dynamically.
  191. // please refer to attributeDefaults for the structure of attribute
  192. add: function (attribute) {
  193. var $form = $(this);
  194. attribute = $.extend({value: getValue($form, attribute)}, attributeDefaults, attribute);
  195. $form.data('yiiActiveForm').attributes.push(attribute);
  196. watchAttribute($form, attribute);
  197. },
  198. // remove the attribute with the specified ID from the form
  199. remove: function (id) {
  200. var $form = $(this),
  201. attributes = $form.data('yiiActiveForm').attributes,
  202. index = -1,
  203. attribute = undefined;
  204. $.each(attributes, function (i) {
  205. if (attributes[i]['id'] == id) {
  206. index = i;
  207. attribute = attributes[i];
  208. return false;
  209. }
  210. });
  211. if (index >= 0) {
  212. attributes.splice(index, 1);
  213. unwatchAttribute($form, attribute);
  214. }
  215. return attribute;
  216. },
  217. // manually trigger the validation of the attribute with the specified ID
  218. validateAttribute: function (id) {
  219. var attribute = methods.find.call(this, id);
  220. if (attribute != undefined) {
  221. validateAttribute($(this), attribute, true);
  222. }
  223. },
  224. // find an attribute config based on the specified attribute ID
  225. find: function (id) {
  226. var attributes = $(this).data('yiiActiveForm').attributes,
  227. result = undefined;
  228. $.each(attributes, function (i) {
  229. if (attributes[i]['id'] == id) {
  230. result = attributes[i];
  231. return false;
  232. }
  233. });
  234. return result;
  235. },
  236. destroy: function () {
  237. return this.each(function () {
  238. $(this).unbind('.yiiActiveForm');
  239. $(this).removeData('yiiActiveForm');
  240. });
  241. },
  242. data: function () {
  243. return this.data('yiiActiveForm');
  244. },
  245. // validate all applicable inputs in the form
  246. validate: function () {
  247. var $form = $(this),
  248. data = $form.data('yiiActiveForm'),
  249. needAjaxValidation = false,
  250. messages = {},
  251. deferreds = deferredArray(),
  252. submitting = data.submitting;
  253. if (submitting) {
  254. var event = $.Event(events.beforeValidate);
  255. $form.trigger(event, [messages, deferreds]);
  256. if (event.result === false) {
  257. data.submitting = false;
  258. return;
  259. }
  260. }
  261. // client-side validation
  262. $.each(data.attributes, function () {
  263. this.cancelled = false;
  264. // perform validation only if the form is being submitted or if an attribute is pending validation
  265. if (data.submitting || this.status === 2 || this.status === 3) {
  266. var msg = messages[this.id];
  267. if (msg === undefined) {
  268. msg = [];
  269. messages[this.id] = msg;
  270. }
  271. var event = $.Event(events.beforeValidateAttribute);
  272. $form.trigger(event, [this, msg, deferreds]);
  273. if (event.result !== false) {
  274. if (this.validate) {
  275. this.validate(this, getValue($form, this), msg, deferreds, $form);
  276. }
  277. if (this.enableAjaxValidation) {
  278. needAjaxValidation = true;
  279. }
  280. } else {
  281. this.cancelled = true;
  282. }
  283. }
  284. });
  285. // ajax validation
  286. $.when.apply(this, deferreds).always(function() {
  287. // Remove empty message arrays
  288. for (var i in messages) {
  289. if (0 === messages[i].length) {
  290. delete messages[i];
  291. }
  292. }
  293. if (needAjaxValidation) {
  294. var $button = data.submitObject,
  295. extData = '&' + data.settings.ajaxParam + '=' + $form.prop('id');
  296. if ($button && $button.length && $button.prop('name')) {
  297. extData += '&' + $button.prop('name') + '=' + $button.prop('value');
  298. }
  299. $.ajax({
  300. url: data.settings.validationUrl,
  301. type: $form.prop('method'),
  302. data: $form.serialize() + extData,
  303. dataType: data.settings.ajaxDataType,
  304. complete: function (jqXHR, textStatus) {
  305. $form.trigger(events.ajaxComplete, [jqXHR, textStatus]);
  306. },
  307. beforeSend: function (jqXHR, settings) {
  308. $form.trigger(events.ajaxBeforeSend, [jqXHR, settings]);
  309. },
  310. success: function (msgs) {
  311. if (msgs !== null && typeof msgs === 'object') {
  312. $.each(data.attributes, function () {
  313. if (!this.enableAjaxValidation || this.cancelled) {
  314. delete msgs[this.id];
  315. }
  316. });
  317. updateInputs($form, $.extend(messages, msgs), submitting);
  318. } else {
  319. updateInputs($form, messages, submitting);
  320. }
  321. },
  322. error: function () {
  323. data.submitting = false;
  324. }
  325. });
  326. } else if (data.submitting) {
  327. // delay callback so that the form can be submitted without problem
  328. setTimeout(function () {
  329. updateInputs($form, messages, submitting);
  330. }, 200);
  331. } else {
  332. updateInputs($form, messages, submitting);
  333. }
  334. });
  335. },
  336. submitForm: function () {
  337. var $form = $(this),
  338. data = $form.data('yiiActiveForm');
  339. if (data.validated) {
  340. data.submitting = false;
  341. var event = $.Event(events.beforeSubmit);
  342. $form.trigger(event);
  343. if (event.result === false) {
  344. data.validated = false;
  345. return false;
  346. }
  347. return true; // continue submitting the form since validation passes
  348. } else {
  349. if (data.settings.timer !== undefined) {
  350. clearTimeout(data.settings.timer);
  351. }
  352. data.submitting = true;
  353. methods.validate.call($form);
  354. return false;
  355. }
  356. },
  357. resetForm: function () {
  358. var $form = $(this);
  359. var data = $form.data('yiiActiveForm');
  360. // Because we bind directly to a form reset event instead of a reset button (that may not exist),
  361. // when this function is executed form input values have not been reset yet.
  362. // Therefore we do the actual reset work through setTimeout.
  363. setTimeout(function () {
  364. $.each(data.attributes, function () {
  365. // Without setTimeout() we would get the input values that are not reset yet.
  366. this.value = getValue($form, this);
  367. this.status = 0;
  368. var $container = $form.find(this.container);
  369. $container.removeClass(
  370. data.settings.validatingCssClass + ' ' +
  371. data.settings.errorCssClass + ' ' +
  372. data.settings.successCssClass
  373. );
  374. $container.find(this.error).html('');
  375. });
  376. $form.find(data.settings.errorSummary).hide().find('ul').html('');
  377. }, 1);
  378. }
  379. };
  380. var watchAttribute = function ($form, attribute) {
  381. var $input = findInput($form, attribute);
  382. if (attribute.validateOnChange) {
  383. $input.on('change.yiiActiveForm', function () {
  384. validateAttribute($form, attribute, false);
  385. });
  386. }
  387. if (attribute.validateOnBlur) {
  388. $input.on('blur.yiiActiveForm', function () {
  389. if (attribute.status == 0 || attribute.status == 1) {
  390. validateAttribute($form, attribute, !attribute.status);
  391. }
  392. });
  393. }
  394. if (attribute.validateOnType) {
  395. $input.on('keyup.yiiActiveForm', function () {
  396. if (attribute.value !== getValue($form, attribute)) {
  397. validateAttribute($form, attribute, false, attribute.validationDelay);
  398. }
  399. });
  400. }
  401. };
  402. var unwatchAttribute = function ($form, attribute) {
  403. findInput($form, attribute).off('.yiiActiveForm');
  404. };
  405. var validateAttribute = function ($form, attribute, forceValidate, validationDelay) {
  406. var data = $form.data('yiiActiveForm');
  407. if (forceValidate) {
  408. attribute.status = 2;
  409. }
  410. $.each(data.attributes, function () {
  411. if (this.value !== getValue($form, this)) {
  412. this.status = 2;
  413. forceValidate = true;
  414. }
  415. });
  416. if (!forceValidate) {
  417. return;
  418. }
  419. if (data.settings.timer !== undefined) {
  420. clearTimeout(data.settings.timer);
  421. }
  422. data.settings.timer = setTimeout(function () {
  423. if (data.submitting || $form.is(':hidden')) {
  424. return;
  425. }
  426. $.each(data.attributes, function () {
  427. if (this.status === 2) {
  428. this.status = 3;
  429. $form.find(this.container).addClass(data.settings.validatingCssClass);
  430. }
  431. });
  432. methods.validate.call($form);
  433. }, validationDelay ? validationDelay : 200);
  434. };
  435. /**
  436. * Returns an array prototype with a shortcut method for adding a new deferred.
  437. * The context of the callback will be the deferred object so it can be resolved like ```this.resolve()```
  438. * @returns Array
  439. */
  440. var deferredArray = function () {
  441. var array = [];
  442. array.add = function(callback) {
  443. this.push(new $.Deferred(callback));
  444. };
  445. return array;
  446. };
  447. /**
  448. * Updates the error messages and the input containers for all applicable attributes
  449. * @param $form the form jQuery object
  450. * @param messages array the validation error messages
  451. * @param submitting whether this method is called after validation triggered by form submission
  452. */
  453. var updateInputs = function ($form, messages, submitting) {
  454. var data = $form.data('yiiActiveForm');
  455. if (submitting) {
  456. var errorInputs = [];
  457. $.each(data.attributes, function () {
  458. if (!this.cancelled && updateInput($form, this, messages)) {
  459. errorInputs.push(this.input);
  460. }
  461. });
  462. $form.trigger(events.afterValidate, [messages]);
  463. updateSummary($form, messages);
  464. if (errorInputs.length) {
  465. var top = $form.find(errorInputs.join(',')).first().closest(':visible').offset().top;
  466. var wtop = $(window).scrollTop();
  467. if (top < wtop || top > wtop + $(window).height) {
  468. $(window).scrollTop(top);
  469. }
  470. data.submitting = false;
  471. } else {
  472. data.validated = true;
  473. var $button = data.submitObject || $form.find(':submit:first');
  474. // TODO: if the submission is caused by "change" event, it will not work
  475. if ($button.length) {
  476. $button.click();
  477. } else {
  478. // no submit button in the form
  479. $form.submit();
  480. }
  481. }
  482. } else {
  483. $.each(data.attributes, function () {
  484. if (!this.cancelled && (this.status === 2 || this.status === 3)) {
  485. updateInput($form, this, messages);
  486. }
  487. });
  488. }
  489. };
  490. /**
  491. * Updates the error message and the input container for a particular attribute.
  492. * @param $form the form jQuery object
  493. * @param attribute object the configuration for a particular attribute.
  494. * @param messages array the validation error messages
  495. * @return boolean whether there is a validation error for the specified attribute
  496. */
  497. var updateInput = function ($form, attribute, messages) {
  498. var data = $form.data('yiiActiveForm'),
  499. $input = findInput($form, attribute),
  500. hasError = false;
  501. if (!$.isArray(messages[attribute.id])) {
  502. messages[attribute.id] = [];
  503. }
  504. $form.trigger(events.afterValidateAttribute, [attribute, messages[attribute.id]]);
  505. attribute.status = 1;
  506. if ($input.length) {
  507. hasError = messages[attribute.id].length > 0;
  508. var $container = $form.find(attribute.container);
  509. var $error = $container.find(attribute.error);
  510. if (hasError) {
  511. if (attribute.encodeError) {
  512. $error.text(messages[attribute.id][0]);
  513. } else {
  514. $error.html(messages[attribute.id][0]);
  515. }
  516. $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass)
  517. .addClass(data.settings.errorCssClass);
  518. } else {
  519. $error.empty();
  520. $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ')
  521. .addClass(data.settings.successCssClass);
  522. }
  523. attribute.value = getValue($form, attribute);
  524. }
  525. return hasError;
  526. };
  527. /**
  528. * Updates the error summary.
  529. * @param $form the form jQuery object
  530. * @param messages array the validation error messages
  531. */
  532. var updateSummary = function ($form, messages) {
  533. var data = $form.data('yiiActiveForm'),
  534. $summary = $form.find(data.settings.errorSummary),
  535. $ul = $summary.find('ul').empty();
  536. if ($summary.length && messages) {
  537. $.each(data.attributes, function () {
  538. if ($.isArray(messages[this.id]) && messages[this.id].length) {
  539. var error = $('<li/>');
  540. if (data.settings.encodeErrorSummary) {
  541. error.text(messages[this.id][0]);
  542. } else {
  543. error.html(messages[this.id][0]);
  544. }
  545. $ul.append(error);
  546. }
  547. });
  548. $summary.toggle($ul.find('li').length > 0);
  549. }
  550. };
  551. var getValue = function ($form, attribute) {
  552. var $input = findInput($form, attribute);
  553. var type = $input.prop('type');
  554. if (type === 'checkbox' || type === 'radio') {
  555. var $realInput = $input.filter(':checked');
  556. if (!$realInput.length) {
  557. $realInput = $form.find('input[type=hidden][name="' + $input.prop('name') + '"]');
  558. }
  559. return $realInput.val();
  560. } else {
  561. return $input.val();
  562. }
  563. };
  564. var findInput = function ($form, attribute) {
  565. var $input = $form.find(attribute.input);
  566. if ($input.length && $input[0].tagName.toLowerCase() === 'div') {
  567. // checkbox list or radio list
  568. return $input.find('input');
  569. } else {
  570. return $input;
  571. }
  572. };
  573. })(window.jQuery);