foundation.abide.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. ;(function ($, window, document, undefined) {
  2. 'use strict';
  3. Foundation.libs.abide = {
  4. name : 'abide',
  5. version : '5.5.3',
  6. settings : {
  7. live_validate : true, // validate the form as you go
  8. validate_on_blur : true, // validate whenever you focus/blur on an input field
  9. // validate_on: 'tab', // tab (when user tabs between fields), change (input changes), manual (call custom events)
  10. focus_on_invalid : true, // automatically bring the focus to an invalid input field
  11. error_labels : true, // labels with a for="inputId" will receive an `error` class
  12. error_class : 'error', // labels with a for="inputId" will receive an `error` class
  13. // the amount of time Abide will take before it validates the form (in ms).
  14. // smaller time will result in faster validation
  15. timeout : 1000,
  16. patterns : {
  17. alpha : /^[a-zA-Z]+$/,
  18. alpha_numeric : /^[a-zA-Z0-9]+$/,
  19. integer : /^[-+]?\d+$/,
  20. number : /^[-+]?\d*(?:[\.\,]\d+)?$/,
  21. // amex, visa, diners
  22. card : /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/,
  23. cvv : /^([0-9]){3,4}$/,
  24. // http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address
  25. email : /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/,
  26. // http://blogs.lse.ac.uk/lti/2008/04/23/a-regular-expression-to-match-any-url/
  27. url: /^(https?|ftp|file|ssh):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+~%\/\.\w]+)?\??([-\+=&;%@\.\w]+)?#?([\w]+)?)?/,
  28. // abc.de
  29. domain : /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,8}$/,
  30. datetime : /^([0-2][0-9]{3})\-([0-1][0-9])\-([0-3][0-9])T([0-5][0-9])\:([0-5][0-9])\:([0-5][0-9])(Z|([\-\+]([0-1][0-9])\:00))$/,
  31. // YYYY-MM-DD
  32. date : /(?:19|20)[0-9]{2}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9])|(?:(?!02)(?:0[1-9]|1[0-2])-(?:30))|(?:(?:0[13578]|1[02])-31))$/,
  33. // HH:MM:SS
  34. time : /^(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}$/,
  35. dateISO : /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/,
  36. // MM/DD/YYYY
  37. month_day_year : /^(0[1-9]|1[012])[- \/.](0[1-9]|[12][0-9]|3[01])[- \/.]\d{4}$/,
  38. // DD/MM/YYYY
  39. day_month_year : /^(0[1-9]|[12][0-9]|3[01])[- \/.](0[1-9]|1[012])[- \/.]\d{4}$/,
  40. // #FFF or #FFFFFF
  41. color : /^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/
  42. },
  43. validators : {
  44. equalTo : function (el, required, parent) {
  45. var from = document.getElementById(el.getAttribute(this.add_namespace('data-equalto'))).value,
  46. to = el.value,
  47. valid = (from === to);
  48. return valid;
  49. }
  50. }
  51. },
  52. timer : null,
  53. init : function (scope, method, options) {
  54. this.bindings(method, options);
  55. },
  56. events : function (scope) {
  57. var self = this,
  58. form = self.S(scope).attr('novalidate', 'novalidate'),
  59. settings = form.data(this.attr_name(true) + '-init') || {};
  60. this.invalid_attr = this.add_namespace('data-invalid');
  61. function validate(originalSelf, e) {
  62. clearTimeout(self.timer);
  63. self.timer = setTimeout(function () {
  64. self.validate([originalSelf], e);
  65. }.bind(originalSelf), settings.timeout);
  66. }
  67. form
  68. .off('.abide')
  69. .on('submit.fndtn.abide', function (e) {
  70. var is_ajax = /ajax/i.test(self.S(this).attr(self.attr_name()));
  71. return self.validate(self.S(this).find('input, textarea, select').not(":hidden, [data-abide-ignore]").get(), e, is_ajax);
  72. })
  73. .on('validate.fndtn.abide', function (e) {
  74. if (settings.validate_on === 'manual') {
  75. self.validate([e.target], e);
  76. }
  77. })
  78. .on('reset', function (e) {
  79. return self.reset($(this), e);
  80. })
  81. .find('input, textarea, select').not(":hidden, [data-abide-ignore]")
  82. .off('.abide')
  83. .on('blur.fndtn.abide change.fndtn.abide', function (e) {
  84. var id = this.getAttribute('id'),
  85. eqTo = form.find('[data-equalto="'+ id +'"]');
  86. // old settings fallback
  87. // will be deprecated with F6 release
  88. if (settings.validate_on_blur && settings.validate_on_blur === true) {
  89. validate(this, e);
  90. }
  91. // checks if there is an equalTo equivalent related by id
  92. if(typeof eqTo.get(0) !== "undefined" && eqTo.val().length){
  93. validate(eqTo.get(0),e);
  94. }
  95. // new settings combining validate options into one setting
  96. if (settings.validate_on === 'change') {
  97. validate(this, e);
  98. }
  99. })
  100. .on('keydown.fndtn.abide', function (e) {
  101. var id = this.getAttribute('id'),
  102. eqTo = form.find('[data-equalto="'+ id +'"]');
  103. // old settings fallback
  104. // will be deprecated with F6 release
  105. if (settings.live_validate && settings.live_validate === true && e.which != 9) {
  106. validate(this, e);
  107. }
  108. // checks if there is an equalTo equivalent related by id
  109. if(typeof eqTo.get(0) !== "undefined" && eqTo.val().length){
  110. validate(eqTo.get(0),e);
  111. }
  112. // new settings combining validate options into one setting
  113. if (settings.validate_on === 'tab' && e.which === 9) {
  114. validate(this, e);
  115. }
  116. else if (settings.validate_on === 'change') {
  117. validate(this, e);
  118. }
  119. })
  120. .on('focus', function (e) {
  121. if (navigator.userAgent.match(/iPad|iPhone|Android|BlackBerry|Windows Phone|webOS/i)) {
  122. $('html, body').animate({
  123. scrollTop: $(e.target).offset().top
  124. }, 100);
  125. }
  126. });
  127. },
  128. reset : function (form, e) {
  129. var self = this;
  130. form.removeAttr(self.invalid_attr);
  131. $('[' + self.invalid_attr + ']', form).removeAttr(self.invalid_attr);
  132. $('.' + self.settings.error_class, form).not('small').removeClass(self.settings.error_class);
  133. $(':input', form).not(':button, :submit, :reset, :hidden, [data-abide-ignore]').val('').removeAttr(self.invalid_attr);
  134. },
  135. validate : function (els, e, is_ajax) {
  136. var validations = this.parse_patterns(els),
  137. validation_count = validations.length,
  138. form = this.S(els[0]).closest('form'),
  139. submit_event = /submit/.test(e.type);
  140. // Has to count up to make sure the focus gets applied to the top error
  141. for (var i = 0; i < validation_count; i++) {
  142. if (!validations[i] && (submit_event || is_ajax)) {
  143. if (this.settings.focus_on_invalid) {
  144. els[i].focus();
  145. }
  146. form.trigger('invalid.fndtn.abide');
  147. this.S(els[i]).closest('form').attr(this.invalid_attr, '');
  148. return false;
  149. }
  150. }
  151. if (submit_event || is_ajax) {
  152. form.trigger('valid.fndtn.abide');
  153. }
  154. form.removeAttr(this.invalid_attr);
  155. if (is_ajax) {
  156. return false;
  157. }
  158. return true;
  159. },
  160. parse_patterns : function (els) {
  161. var i = els.length,
  162. el_patterns = [];
  163. while (i--) {
  164. el_patterns.push(this.pattern(els[i]));
  165. }
  166. return this.check_validation_and_apply_styles(el_patterns);
  167. },
  168. pattern : function (el) {
  169. var type = el.getAttribute('type'),
  170. required = typeof el.getAttribute('required') === 'string';
  171. var pattern = el.getAttribute('pattern') || '';
  172. if (this.settings.patterns.hasOwnProperty(pattern) && pattern.length > 0) {
  173. return [el, this.settings.patterns[pattern], required];
  174. } else if (pattern.length > 0) {
  175. return [el, new RegExp(pattern), required];
  176. }
  177. if (this.settings.patterns.hasOwnProperty(type)) {
  178. return [el, this.settings.patterns[type], required];
  179. }
  180. pattern = /.*/;
  181. return [el, pattern, required];
  182. },
  183. // TODO: Break this up into smaller methods, getting hard to read.
  184. check_validation_and_apply_styles : function (el_patterns) {
  185. var i = el_patterns.length,
  186. validations = [];
  187. if (i == 0) {
  188. return validations;
  189. }
  190. var form = this.S(el_patterns[0][0]).closest('[data-' + this.attr_name(true) + ']'),
  191. settings = form.data(this.attr_name(true) + '-init') || {};
  192. while (i--) {
  193. var el = el_patterns[i][0],
  194. required = el_patterns[i][2],
  195. value = el.value.trim(),
  196. direct_parent = this.S(el).parent(),
  197. validator = el.getAttribute(this.add_namespace('data-abide-validator')),
  198. is_radio = el.type === 'radio',
  199. is_checkbox = el.type === 'checkbox',
  200. label = this.S('label[for="' + el.getAttribute('id') + '"]'),
  201. valid_length = (required) ? (el.value.length > 0) : true,
  202. el_validations = [];
  203. var parent, valid;
  204. // support old way to do equalTo validations
  205. if (el.getAttribute(this.add_namespace('data-equalto'))) { validator = 'equalTo' }
  206. if (!direct_parent.is('label')) {
  207. parent = direct_parent;
  208. } else {
  209. parent = direct_parent.parent();
  210. }
  211. if (is_radio && required) {
  212. el_validations.push(this.valid_radio(el, required));
  213. } else if (is_checkbox && required) {
  214. el_validations.push(this.valid_checkbox(el, required));
  215. } else if (validator) {
  216. // Validate using each of the specified (space-delimited) validators.
  217. var validators = validator.split(' ');
  218. var last_valid = true, all_valid = true;
  219. for (var iv = 0; iv < validators.length; iv++) {
  220. valid = this.settings.validators[validators[iv]].apply(this, [el, required, parent])
  221. el_validations.push(valid);
  222. all_valid = valid && last_valid;
  223. last_valid = valid;
  224. }
  225. if (all_valid) {
  226. this.S(el).removeAttr(this.invalid_attr);
  227. parent.removeClass('error');
  228. if (label.length > 0 && this.settings.error_labels) {
  229. label.removeClass(this.settings.error_class).removeAttr('role');
  230. }
  231. $(el).triggerHandler('valid');
  232. } else {
  233. this.S(el).attr(this.invalid_attr, '');
  234. parent.addClass('error');
  235. if (label.length > 0 && this.settings.error_labels) {
  236. label.addClass(this.settings.error_class).attr('role', 'alert');
  237. }
  238. $(el).triggerHandler('invalid');
  239. }
  240. } else {
  241. if (el_patterns[i][1].test(value) && valid_length ||
  242. !required && el.value.length < 1 || $(el).attr('disabled')) {
  243. el_validations.push(true);
  244. } else {
  245. el_validations.push(false);
  246. }
  247. el_validations = [el_validations.every(function (valid) {return valid;})];
  248. if (el_validations[0]) {
  249. this.S(el).removeAttr(this.invalid_attr);
  250. el.setAttribute('aria-invalid', 'false');
  251. el.removeAttribute('aria-describedby');
  252. parent.removeClass(this.settings.error_class);
  253. if (label.length > 0 && this.settings.error_labels) {
  254. label.removeClass(this.settings.error_class).removeAttr('role');
  255. }
  256. $(el).triggerHandler('valid');
  257. } else {
  258. this.S(el).attr(this.invalid_attr, '');
  259. el.setAttribute('aria-invalid', 'true');
  260. // Try to find the error associated with the input
  261. var errorElem = parent.find('small.' + this.settings.error_class, 'span.' + this.settings.error_class);
  262. var errorID = errorElem.length > 0 ? errorElem[0].id : '';
  263. if (errorID.length > 0) {
  264. el.setAttribute('aria-describedby', errorID);
  265. }
  266. // el.setAttribute('aria-describedby', $(el).find('.error')[0].id);
  267. parent.addClass(this.settings.error_class);
  268. if (label.length > 0 && this.settings.error_labels) {
  269. label.addClass(this.settings.error_class).attr('role', 'alert');
  270. }
  271. $(el).triggerHandler('invalid');
  272. }
  273. }
  274. validations = validations.concat(el_validations);
  275. }
  276. return validations;
  277. },
  278. valid_checkbox : function (el, required) {
  279. var el = this.S(el),
  280. valid = (el.is(':checked') || !required || el.get(0).getAttribute('disabled'));
  281. if (valid) {
  282. el.removeAttr(this.invalid_attr).parent().removeClass(this.settings.error_class);
  283. $(el).triggerHandler('valid');
  284. } else {
  285. el.attr(this.invalid_attr, '').parent().addClass(this.settings.error_class);
  286. $(el).triggerHandler('invalid');
  287. }
  288. return valid;
  289. },
  290. valid_radio : function (el, required) {
  291. var name = el.getAttribute('name'),
  292. group = this.S(el).closest('[data-' + this.attr_name(true) + ']').find("[name='" + name + "']"),
  293. count = group.length,
  294. valid = false,
  295. disabled = false;
  296. // Has to count up to make sure the focus gets applied to the top error
  297. for (var i=0; i < count; i++) {
  298. if( group[i].getAttribute('disabled') ){
  299. disabled=true;
  300. valid=true;
  301. } else {
  302. if (group[i].checked){
  303. valid = true;
  304. } else {
  305. if( disabled ){
  306. valid = false;
  307. }
  308. }
  309. }
  310. }
  311. // Has to count up to make sure the focus gets applied to the top error
  312. for (var i = 0; i < count; i++) {
  313. if (valid) {
  314. this.S(group[i]).removeAttr(this.invalid_attr).parent().removeClass(this.settings.error_class);
  315. $(group[i]).triggerHandler('valid');
  316. } else {
  317. this.S(group[i]).attr(this.invalid_attr, '').parent().addClass(this.settings.error_class);
  318. $(group[i]).triggerHandler('invalid');
  319. }
  320. }
  321. return valid;
  322. },
  323. valid_equal : function (el, required, parent) {
  324. var from = document.getElementById(el.getAttribute(this.add_namespace('data-equalto'))).value,
  325. to = el.value,
  326. valid = (from === to);
  327. if (valid) {
  328. this.S(el).removeAttr(this.invalid_attr);
  329. parent.removeClass(this.settings.error_class);
  330. if (label.length > 0 && settings.error_labels) {
  331. label.removeClass(this.settings.error_class);
  332. }
  333. } else {
  334. this.S(el).attr(this.invalid_attr, '');
  335. parent.addClass(this.settings.error_class);
  336. if (label.length > 0 && settings.error_labels) {
  337. label.addClass(this.settings.error_class);
  338. }
  339. }
  340. return valid;
  341. },
  342. valid_oneof : function (el, required, parent, doNotValidateOthers) {
  343. var el = this.S(el),
  344. others = this.S('[' + this.add_namespace('data-oneof') + ']'),
  345. valid = others.filter(':checked').length > 0;
  346. if (valid) {
  347. el.removeAttr(this.invalid_attr).parent().removeClass(this.settings.error_class);
  348. } else {
  349. el.attr(this.invalid_attr, '').parent().addClass(this.settings.error_class);
  350. }
  351. if (!doNotValidateOthers) {
  352. var _this = this;
  353. others.each(function () {
  354. _this.valid_oneof.call(_this, this, null, null, true);
  355. });
  356. }
  357. return valid;
  358. },
  359. reflow : function(scope, options) {
  360. var self = this,
  361. form = self.S('[' + this.attr_name() + ']').attr('novalidate', 'novalidate');
  362. self.S(form).each(function (idx, el) {
  363. self.events(el);
  364. });
  365. }
  366. };
  367. }(jQuery, window, window.document));