validator.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. /*
  2. Validator v3.3.1
  3. (c) Yair Even Or
  4. https://github.com/yairEO/validator
  5. */
  6. ;(function(root, factory){
  7. var define = define || {};
  8. if( typeof define === 'function' && define.amd )
  9. define([], factory);
  10. else if( typeof exports === 'object' && typeof module === 'object' )
  11. module.exports = factory();
  12. else if(typeof exports === 'object')
  13. exports["FormValidator"] = factory();
  14. else
  15. root.FormValidator = factory();
  16. }(this, function(){
  17. function FormValidator( settings, formElm ){
  18. this.data = {}; // holds the form fields' data
  19. this.DOM = {
  20. scope : formElm
  21. };
  22. this.settings = this.extend({}, this.defaults, settings || {});
  23. this.texts = this.extend({}, this.texts, this.settings.texts || {});
  24. this.settings.events && this.events();
  25. }
  26. FormValidator.prototype = {
  27. // Validation error texts
  28. texts : {
  29. invalid : 'inupt is not as expected',
  30. short : 'input is too short',
  31. long : 'input is too long',
  32. checked : 'must be checked',
  33. empty : 'please put something here',
  34. select : 'Please select an option',
  35. number_min : 'too low',
  36. number_max : 'too high',
  37. url : 'invalid URL',
  38. number : 'not a number',
  39. email : 'email address is invalid',
  40. email_repeat : 'emails do not match',
  41. date : 'invalid date',
  42. time : 'invalid time',
  43. password_repeat : 'passwords do not match',
  44. no_match : 'no match',
  45. complete : 'input is not complete'
  46. },
  47. // default settings
  48. defaults : {
  49. alerts : true,
  50. events : false,
  51. regex : {
  52. url : /^(https?:\/\/)?([\w\d\-_]+\.+[A-Za-z]{2,})+\/?/,
  53. phone : /^\+?([0-9]|[-|' '])+$/i,
  54. numeric : /^[0-9]+$/i,
  55. alphanumeric : /^[a-zA-Z0-9]+$/i,
  56. email : {
  57. illegalChars : /[\(\)\<\>\,\;\:\\\/\"\[\]]/,
  58. filter : /^.+@.+\..{2,6}$/ // exmaple email "steve@s-i.photo"
  59. }
  60. },
  61. classes : {
  62. item : 'field',
  63. alert : 'alert',
  64. bad : 'bad'
  65. }
  66. },
  67. // Tests (per type)
  68. // each test return "true" when passes and a string of error text otherwise
  69. tests : {
  70. sameAsPlaceholder : function( field, data ){
  71. if( field.getAttribute('placeholder') )
  72. return data.value != field.getAttribute('placeholder') || this.texts.empty;
  73. else
  74. return true;
  75. },
  76. hasValue : function( value ){
  77. return value ? true : this.texts.empty;
  78. },
  79. // 'linked' is a special test case for inputs which their values should be equal to each other (ex. confirm email or retype password)
  80. linked : function(a, b, type){
  81. if( b != a ){
  82. // choose a specific message or a general one
  83. return this.texts[type + '_repeat'] || this.texts.no_match;
  84. }
  85. return true;
  86. },
  87. email : function(field, data){
  88. if ( !this.settings.regex.email.filter.test( data.value ) || data.value.match( this.settings.regex.email.illegalChars ) ){
  89. return this.texts.email;
  90. }
  91. return true;
  92. },
  93. // a "skip" will skip some of the tests (needed for keydown validation)
  94. text : function(field, data){
  95. var that = this;
  96. // make sure there are at least X number of words, each at least 2 chars long.
  97. // for example 'john F kenedy' should be at least 2 words and will pass validation
  98. if( data.validateWords ){
  99. var words = data.value.split(' ');
  100. // iterate on all the words
  101. var wordsLength = function(len){
  102. for( var w = words.length; w--; )
  103. if( words[w].length < len )
  104. return that.texts.short;
  105. return true;
  106. };
  107. if( words.length < data.validateWords || !wordsLength(2) )
  108. return this.texts.complete;
  109. return true;
  110. }
  111. if( data.lengthRange && data.value.length < data.lengthRange[0] ){
  112. return this.texts.short;
  113. }
  114. // check if there is max length & field length is greater than the allowed
  115. if( data.lengthRange && data.lengthRange[1] && data.value.length > data.lengthRange[1] ){
  116. return this.texts.long;
  117. }
  118. // check if the field's value should obey any length limits, and if so, make sure the length of the value is as specified
  119. if( data.lengthLimit && data.lengthLimit.length ){
  120. while( data.lengthLimit.length ){
  121. if( data.lengthLimit.pop() == data.value.length ){
  122. return true;
  123. }
  124. }
  125. return this.texts.complete;
  126. }
  127. if( data.pattern ){
  128. var regex, jsRegex;
  129. switch( data.pattern ){
  130. case 'alphanumeric' :
  131. regex = this.settings.regex.alphanumeric
  132. break;
  133. case 'numeric' :
  134. regex = this.settings.regex.numeric
  135. break;
  136. case 'phone' :
  137. regex = this.settings.regex.phone
  138. break;
  139. default :
  140. regex = data.pattern;
  141. }
  142. try{
  143. jsRegex = new RegExp(regex).test(data.value);
  144. if( data.value && !jsRegex ){
  145. return this.texts.invalid;
  146. }
  147. }
  148. catch(err){
  149. console.warn(err, field, 'regex is invalid');
  150. return this.texts.invalid;
  151. }
  152. }
  153. return true;
  154. },
  155. number : function( field, data ){
  156. var a = data.value;
  157. // if not not a number
  158. if( isNaN(parseFloat(a)) && !isFinite(a) ){
  159. return this.texts.number;
  160. }
  161. // not enough numbers
  162. else if( data.lengthRange && a.length < data.lengthRange[0] ){
  163. return this.texts.short;
  164. }
  165. // check if there is max length & field length is greater than the allowed
  166. else if( data.lengthRange && data.lengthRange[1] && a.length > data.lengthRange[1] ){
  167. return this.texts.long;
  168. }
  169. else if( data.minmax[0] && (a|0) < data.minmax[0] ){
  170. return this.texts.number_min;
  171. }
  172. else if( data.minmax[1] && (a|0) > data.minmax[1] ){
  173. return this.texts.number_max;
  174. }
  175. return true;
  176. },
  177. // Date is validated in European format (day,month,year)
  178. date : function( field, data ){
  179. var day, A = data.value.split(/[-./]/g), i;
  180. // if there is native HTML5 support:
  181. if( field.valueAsNumber )
  182. return true;
  183. for( i = A.length; i--; ){
  184. if( isNaN(parseFloat( data.value )) && !isFinite(data.value) )
  185. return this.texts.date;
  186. }
  187. try{
  188. day = new Date(A[2], A[1]-1, A[0]);
  189. if( day.getMonth()+1 == A[1] && day.getDate() == A[0] )
  190. return true;
  191. return this.texts.date;
  192. }
  193. catch(er){
  194. return this.texts.date;
  195. }
  196. },
  197. time : function( field, data ){
  198. var pattern = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/;
  199. if( pattern.test(data.value) )
  200. return true;
  201. else
  202. return this.texts.time;
  203. },
  204. url : function( field, data ){
  205. // minimalistic URL validation
  206. if( !this.settings.regex.url.test(data.value) )
  207. return this.texts.url;
  208. return true;
  209. },
  210. hidden : function( field, data ){
  211. if( data.lengthRange && data.value.length < data.lengthRange[0] )
  212. return this.texts.short;
  213. if( data.pattern ){
  214. if( data.pattern == 'alphanumeric' && !this.settings.regex.alphanumeric.test(data.value) )
  215. return this.texts.invalid;
  216. }
  217. return true;
  218. },
  219. select : function( field, data ){
  220. return data.value ? true : this.texts.select;
  221. },
  222. checkbox : function( field, data ){
  223. if( field.checked ) return true;
  224. return this.texts.checked;
  225. }
  226. },
  227. /**
  228. * bind events on form elements
  229. * @param {Array/String} types [description]
  230. * @param {Object} formElm [optional - form element, if one is not already defined on the instance]
  231. * @return {[type]} [description]
  232. */
  233. events : function( types, formElm ){
  234. var that = this;
  235. types = types || this.settings.events;
  236. formElm = formElm || this.DOM.scope;
  237. if( !formElm || !types ) return;
  238. if( types instanceof Array )
  239. types.forEach(bindEventByType);
  240. else if( typeof types == 'string' )
  241. bindEventByType(types)
  242. function bindEventByType( type ){
  243. formElm.addEventListener(type, function(e){
  244. that.checkField(e.target)
  245. }, true);
  246. }
  247. },
  248. /**
  249. * Marks an field as invalid
  250. * @param {DOM Object} field
  251. * @param {String} text
  252. * @return {String} - useless string (should be the DOM node added for warning)
  253. */
  254. mark : function( field, text ){
  255. if( !text || !field )
  256. return false;
  257. var that = this;
  258. // check if not already marked as 'bad' and add the 'alert' object.
  259. // if already is marked as 'bad', then make sure the text is set again because i might change depending on validation
  260. var item = this.closest(field, '.' + this.settings.classes.item),
  261. alert = item.querySelector('.'+this.settings.classes.alert),
  262. warning;
  263. if( this.settings.alerts ){
  264. if( alert )
  265. alert.innerHTML = text;
  266. else{
  267. warning = '<div class="'+ this.settings.classes.alert +'">' + text + '</div>';
  268. item.insertAdjacentHTML('beforeend', warning);
  269. }
  270. }
  271. item.classList.remove(this.settings.classes.bad);
  272. // a delay so the "alert" could be transitioned via CSS
  273. setTimeout(function(){
  274. item.classList.add( that.settings.classes.bad );
  275. });
  276. return warning;
  277. },
  278. /* un-marks invalid fields
  279. */
  280. unmark : function( field ){
  281. var warning;
  282. if( !field ){
  283. console.warn('no "field" argument, null or DOM object not found');
  284. return false;
  285. }
  286. var fieldWrap = this.closest(field, '.' + this.settings.classes.item);
  287. if( fieldWrap ){
  288. warning = fieldWrap.querySelector('.'+ this.settings.classes.alert);
  289. fieldWrap.classList.remove(this.settings.classes.bad);
  290. }
  291. if( warning )
  292. warning.parentNode.removeChild(warning);
  293. },
  294. /**
  295. * removes unmarks all fields
  296. * @return {[type]} [description]
  297. */
  298. reset : function( formElm ){
  299. var fieldsToCheck,
  300. that = this;
  301. formElm = formElm || this.DOM.scope;
  302. fieldsToCheck = this.filterFormElements( formElm.elements );
  303. fieldsToCheck.forEach(function(elm){
  304. that.unmark(elm);
  305. });
  306. },
  307. /**
  308. * Normalize types if needed & return the results of the test (per field)
  309. * @param {String} type [form field type]
  310. * @param {*} value
  311. * @return {Boolean} - validation test result
  312. */
  313. testByType : function( field, data ){
  314. data = this.extend({}, data); // clone the data
  315. var type = data.type;
  316. if( type == 'tel' )
  317. data.pattern = data.pattern || 'phone';
  318. if( !type || type == 'password' || type == 'tel' || type == 'search' || type == 'file' )
  319. type = 'text';
  320. return this.tests[type] ? this.tests[type].call(this, field, data) : true;
  321. },
  322. prepareFieldData : function( field ){
  323. var nodeName = field.nodeName.toLowerCase(),
  324. id = Math.random().toString(36).substr(2,9);
  325. field["_validatorId"] = id;
  326. this.data[id] = {};
  327. this.data[id].value = field.value.replace(/^\s+|\s+$/g, ""); // cache the value of the field and trim it
  328. this.data[id].valid = true; // initialize validity of field
  329. this.data[id].type = field.getAttribute('type'); // every field starts as 'valid=true' until proven otherwise
  330. this.data[id].pattern = field.getAttribute('pattern');
  331. // Special treatment
  332. if( nodeName === "select" )
  333. this.data[id].type = "select";
  334. else if( nodeName === "textarea" )
  335. this.data[id].type = "text";
  336. /* Gather Custom data attributes for specific validation:
  337. */
  338. this.data[id].validateWords = field.getAttribute('data-validate-words') || 0;
  339. this.data[id].lengthRange = field.getAttribute('data-validate-length-range') ? (field.getAttribute('data-validate-length-range')+'').split(',') : [1];
  340. this.data[id].lengthLimit = field.getAttribute('data-validate-length') ? (field.getAttribute('data-validate-length')+'').split(',') : false;
  341. this.data[id].minmax = field.getAttribute('data-validate-minmax') ? (field.getAttribute('data-validate-minmax')+'').split(',') : false; // for type 'number', defines the minimum and/or maximum for the value as a number.
  342. this.data[id].validateLinked = field.getAttribute('data-validate-linked');
  343. return this.data[id];
  344. },
  345. /**
  346. * Find the closeset element, by selector
  347. * @param {Object} el [DOM node]
  348. * @param {String} selector [CSS-valid selector]
  349. * @return {Object} [Found element or null if not found]
  350. */
  351. closest : function(el, selector){
  352. var matchesFn;
  353. // find vendor prefix
  354. ['matches','webkitMatchesSelector','mozMatchesSelector','msMatchesSelector','oMatchesSelector'].some(function(fn){
  355. if( typeof document.body[fn] == 'function' ){
  356. matchesFn = fn;
  357. return true;
  358. }
  359. return false;
  360. })
  361. var parent;
  362. // traverse parents
  363. while (el) {
  364. parent = el.parentElement;
  365. if (parent && parent[matchesFn](selector)) {
  366. return parent;
  367. }
  368. el = parent;
  369. }
  370. return null;
  371. },
  372. /**
  373. * MDN polyfill for Object.assign
  374. */
  375. extend : function( target, varArgs ){
  376. if( !target )
  377. throw new TypeError('Cannot convert undefined or null to object');
  378. var to = Object(target),
  379. nextKey, nextSource, index;
  380. for( index = 1; index < arguments.length; index++ ){
  381. nextSource = arguments[index];
  382. if( nextSource != null ) // Skip over if undefined or null
  383. for( nextKey in nextSource )
  384. // Avoid bugs when hasOwnProperty is shadowed
  385. if( Object.prototype.hasOwnProperty.call(nextSource, nextKey) )
  386. to[nextKey] = nextSource[nextKey];
  387. }
  388. return to;
  389. },
  390. /* Checks a single form field by it's type and specific (custom) attributes
  391. * {DOM Object} - the field to be checked
  392. * {Boolean} silent - don't mark a field and only return if it passed the validation or not
  393. */
  394. checkField : function( field, silent ){
  395. // skip testing fields whom their type is not HIDDEN but they are HIDDEN via CSS.
  396. if( field.type !='hidden' && !field.clientWidth )
  397. return { valid:true, error:"" }
  398. field = this.filterFormElements( [field] )[0];
  399. // if field did not pass filtering or is simply not passed
  400. if( !field )
  401. return { valid:true, error:"" }
  402. // this.unmark( field );
  403. var linkedTo,
  404. testResult,
  405. optional = field.className.indexOf('optional') != -1,
  406. data = this.prepareFieldData( field ),
  407. form = this.closest(field, 'form'); // if the field is part of a form, then cache it
  408. // check if field has any value
  409. /* Validate the field's value is different than the placeholder attribute (and attribute exists)
  410. * this is needed when fixing the placeholders for older browsers which does not support them.
  411. */
  412. // first, check if the field even has any value
  413. testResult = this.tests.hasValue.call(this, data.value);
  414. // if the field has value, check if that value is same as placeholder
  415. if( testResult === true )
  416. testResult = this.tests.sameAsPlaceholder.call(this, field, data );
  417. data.valid = optional || testResult === true;
  418. if( optional && !data.value ){
  419. return { valid:true, error:"" }
  420. }
  421. if( testResult !== true )
  422. data.valid = false;
  423. // validate by type of field. use 'attr()' is proffered to get the actual value and not what the browsers sees for unsupported types.
  424. if( data.valid ){
  425. testResult = this.testByType(field, data);
  426. data.valid = testResult === true ? true : false;
  427. }
  428. // if this field is linked to another field (their values should be the same)
  429. if( data.valid && data.validateLinked ){
  430. if( data['validateLinked'].indexOf('#') == 0 )
  431. linkedTo = document.body.querySelector(data['validateLinked'])
  432. else if( form.length )
  433. linkedTo = form.querySelector('[name=' + data['validateLinked'] + ']');
  434. else
  435. linkedTo = document.body.querySelector('[name=' + data['validateLinked'] + ']');
  436. testResult = this.tests.linked.call(this, field.value, linkedTo.value, data.type );
  437. data.valid = testResult === true ? true : false;
  438. }
  439. if( !silent )
  440. this[data.valid ? "unmark" : "mark"]( field, testResult ); // mark / unmark the field
  441. return {
  442. valid : data.valid,
  443. error : data.valid === true ? "" : testResult
  444. };
  445. },
  446. /**
  447. * Only allow certain form elements which are actual inputs to be validated
  448. * @param {HTMLCollection} form fields Array [description]
  449. * @return {Array} [description]
  450. */
  451. filterFormElements : function( fields ){
  452. var i,
  453. fieldsToCheck = [];
  454. for( i = fields.length; i--; ) {
  455. var isAllowedElement = fields[i].nodeName.match(/input|textarea|select/gi),
  456. isRequiredAttirb = fields[i].hasAttribute('required'),
  457. isDisabled = fields[i].hasAttribute('disabled'),
  458. isOptional = fields[i].className.indexOf('optional') != -1;
  459. if( isAllowedElement && (isRequiredAttirb || isOptional) && !isDisabled )
  460. fieldsToCheck.push(fields[i]);
  461. }
  462. return fieldsToCheck;
  463. },
  464. checkAll : function( formElm ){
  465. if( !formElm ){
  466. console.warn('element not found');
  467. return false;
  468. }
  469. var that = this,
  470. result = {
  471. valid : true, // overall form validation flag
  472. fields : [] // array of objects (per form field)
  473. },
  474. fieldsToCheck = this.filterFormElements( formElm.elements );
  475. // get all the input/textareas/select fields which are required or optional (meaning, they need validation only if they were filled)
  476. fieldsToCheck.forEach(function(elm, i){
  477. var fieldData = that.checkField(elm);
  478. // use an AND operation, so if any of the fields returns 'false' then the submitted result will be also FALSE
  479. result.valid = !!(result.valid * fieldData.valid);
  480. result.fields.push({
  481. field : elm,
  482. error : fieldData.error,
  483. valid : !!fieldData.valid
  484. })
  485. });
  486. return result;
  487. }
  488. }
  489. return FormValidator;
  490. }));