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.

626 lines
19 KiB

  1. /* ******************************
  2. Form Validation Components
  3. Author: Jack Lukic
  4. Notes: First Commit April 08, 2012
  5. Refactored May 28, 2013
  6. Allows you to validate forms based on a form validation object
  7. Form validation objects are bound by either data-validate="" metadata, or form id or name tags
  8. ****************************** */
  9. ;(function ( $, window, document, undefined ) {
  10. $.fn.form = function(fields, parameters) {
  11. var
  12. $allModules = $(this),
  13. settings = $.extend(true, {}, $.fn.form.settings, parameters),
  14. validation = $.extend({}, $.fn.form.settings.defaults, fields),
  15. namespace = settings.namespace,
  16. metadata = settings.metadata,
  17. selector = settings.selector,
  18. className = settings.className,
  19. error = settings.error,
  20. eventNamespace = '.' + namespace,
  21. moduleNamespace = 'module-' + namespace,
  22. moduleSelector = $allModules.selector || '',
  23. time = new Date().getTime(),
  24. performance = [],
  25. query = arguments[0],
  26. methodInvoked = (typeof query == 'string'),
  27. queryArguments = [].slice.call(arguments, 1),
  28. invokedResponse
  29. ;
  30. $allModules
  31. .each(function() {
  32. var
  33. $module = $(this),
  34. $field = $(this).find(selector.field),
  35. $group = $(this).find(selector.group),
  36. $message = $(this).find(selector.message),
  37. $prompt = $(this).find(selector.prompt),
  38. $submit = $(this).find(selector.submit),
  39. formErrors = [],
  40. element = this,
  41. instance = $module.data(moduleNamespace),
  42. module
  43. ;
  44. module = {
  45. initialize: function() {
  46. module.verbose('Initializing form validation', $module, validation, settings);
  47. if(settings.keyboardShortcuts) {
  48. $field
  49. .on('keydown' + eventNamespace, module.event.field.keydown)
  50. ;
  51. }
  52. $module
  53. .on('submit' + eventNamespace, module.validate.form)
  54. ;
  55. $field
  56. .on('blur' + eventNamespace, module.event.field.change)
  57. ;
  58. $submit
  59. .on('click' + eventNamespace, module.submit)
  60. ;
  61. module.instantiate();
  62. },
  63. instantiate: function() {
  64. module.verbose('Storing instance of module', module);
  65. instance = module;
  66. $module
  67. .data(moduleNamespace, module)
  68. ;
  69. },
  70. destroy: function() {
  71. module.verbose('Destroying previous module', instance);
  72. $module
  73. .off(eventNamespace)
  74. .removeData(moduleNamespace)
  75. ;
  76. },
  77. refresh: function() {
  78. module.verbose('Refreshing selector cache');
  79. $field = $module.find(selector.field);
  80. },
  81. submit: function() {
  82. module.verbose('Submitting form', $module);
  83. $module
  84. .submit()
  85. ;
  86. },
  87. event: {
  88. field: {
  89. keydown: function(event) {
  90. var
  91. $field = $(this),
  92. key = event.which,
  93. keyCode = {
  94. enter : 13,
  95. escape : 27
  96. }
  97. ;
  98. if( key == keyCode.escape) {
  99. module.verbose('Escape key pressed blurring field');
  100. $field
  101. .blur()
  102. ;
  103. }
  104. if( key == keyCode.enter && $field.is(selector.input) ) {
  105. module.debug('Enter key pressed, submitting form');
  106. $submit
  107. .addClass(className.down)
  108. ;
  109. $field
  110. .one('keyup' + eventNamespace, module.event.field.keyup)
  111. ;
  112. event.preventDefault();
  113. return false;
  114. }
  115. },
  116. keyup: function() {
  117. module.verbose('Doing keyboard shortcut form submit');
  118. $submit.removeClass(className.down);
  119. module.submit();
  120. },
  121. change: function() {
  122. var
  123. $field = $(this),
  124. $fieldGroup = $field.closest($group)
  125. ;
  126. if( $fieldGroup.hasClass(className.error) ) {
  127. module.debug('Revalidating field', $field, module.get.validation($field));
  128. module.validate.field( module.get.validation($field) );
  129. }
  130. else if(settings.on == 'change') {
  131. module.validate.field( module.get.validation($field) );
  132. }
  133. }
  134. }
  135. },
  136. get: {
  137. field: function(identifier) {
  138. module.verbose('Finding field with identifier', identifier);
  139. if( $field.filter('#' + identifier).size() > 0 ) {
  140. return $field.filter('#' + identifier);
  141. }
  142. else if( $field.filter('[name="' + identifier +'"]').size() > 0 ) {
  143. return $field.filter('[name="' + identifier +'"]');
  144. }
  145. else if( $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]').size() > 0 ) {
  146. return $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]');
  147. }
  148. return $('<input/>');
  149. },
  150. validation: function($field) {
  151. var
  152. rules
  153. ;
  154. $.each(validation, function(fieldName, field) {
  155. if( module.get.field(field.identifier).get(0) == $field.get(0) ) {
  156. rules = field;
  157. }
  158. });
  159. return rules || false;
  160. }
  161. },
  162. has: {
  163. field: function(identifier) {
  164. module.verbose('Checking for existence of a field with identifier', identifier);
  165. if( $field.filter('#' + identifier).size() > 0 ) {
  166. return true;
  167. }
  168. else if( $field.filter('[name="' + identifier +'"]').size() > 0 ) {
  169. return true;
  170. }
  171. else if( $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]').size() > 0 ) {
  172. return true;
  173. }
  174. return false;
  175. }
  176. },
  177. add: {
  178. prompt: function(field, errors) {
  179. var
  180. $field = module.get.field(field.identifier),
  181. $fieldGroup = $field.closest($group),
  182. $prompt = $fieldGroup.find(selector.prompt),
  183. promptExists = ($prompt.size() !== 0)
  184. ;
  185. module.verbose('Adding inline validation prompt');
  186. $fieldGroup
  187. .addClass(className.error)
  188. ;
  189. if(settings.inlineError) {
  190. if(!promptExists) {
  191. $prompt = settings.templates.prompt(errors);
  192. $prompt
  193. .appendTo($fieldGroup)
  194. .hide()
  195. ;
  196. }
  197. $prompt
  198. .html(errors[0])
  199. ;
  200. if($prompt.is(':not(:visible)')) {
  201. $prompt
  202. .fadeIn(settings.animateSpeed)
  203. ;
  204. }
  205. }
  206. },
  207. errors: function(errors) {
  208. module.debug('Adding form error messages', errors);
  209. $message
  210. .html( settings.templates.error(errors) )
  211. ;
  212. }
  213. },
  214. remove: {
  215. prompt: function(field) {
  216. var
  217. $field = module.get.field(field.identifier),
  218. $fieldGroup = $field.closest($group),
  219. $prompt = $fieldGroup.find(selector.prompt)
  220. ;
  221. $fieldGroup
  222. .removeClass(className.error)
  223. ;
  224. if(settings.inlineError) {
  225. $prompt.hide();
  226. }
  227. }
  228. },
  229. validate: {
  230. form: function(event) {
  231. var
  232. allValid = true
  233. ;
  234. // reset errors
  235. formErrors = [];
  236. $.each(validation, function(fieldName, field) {
  237. if( !( module.validate.field(field) ) ) {
  238. allValid = false;
  239. }
  240. });
  241. if(allValid) {
  242. module.debug('Form has no validation errors, submitting');
  243. $module
  244. .removeClass(className.error)
  245. .addClass(className.success)
  246. ;
  247. $.proxy(settings.onSuccess, this)(event);
  248. }
  249. else {
  250. module.debug('Form has errors');
  251. $module.addClass(className.error);
  252. if(!settings.inlineError) {
  253. module.add.errors(formErrors);
  254. }
  255. return $.proxy(settings.onFailure, this)(formErrors);
  256. }
  257. },
  258. // takes a validation object and returns whether field passes validation
  259. field: function(field) {
  260. var
  261. $field = module.get.field(field.identifier),
  262. fieldValid = true,
  263. fieldErrors = []
  264. ;
  265. if(field.rules !== undefined) {
  266. $.each(field.rules, function(index, rule) {
  267. if( module.has.field(field.identifier) && !( module.validate.rule(field, rule) ) ) {
  268. module.debug('Field is invalid', field.identifier, rule.type);
  269. fieldErrors.push(rule.prompt);
  270. fieldValid = false;
  271. }
  272. });
  273. }
  274. if(fieldValid) {
  275. module.remove.prompt(field, fieldErrors);
  276. $.proxy(settings.onValid, $field)();
  277. }
  278. else {
  279. formErrors = formErrors.concat(fieldErrors);
  280. module.add.prompt(field, fieldErrors);
  281. $.proxy(settings.onInvalid, $field)(fieldErrors);
  282. return false;
  283. }
  284. return true;
  285. },
  286. // takes validation rule and returns whether field passes rule
  287. rule: function(field, validation) {
  288. var
  289. $field = module.get.field(field.identifier),
  290. type = validation.type,
  291. value = $field.val(),
  292. bracketRegExp = /\[(.*?)\]/i,
  293. bracket = bracketRegExp.exec(type),
  294. isValid = true,
  295. ancillary,
  296. functionType
  297. ;
  298. // if bracket notation is used, pass in extra parameters
  299. if(bracket !== undefined && bracket !== null) {
  300. ancillary = bracket[1];
  301. functionType = type.replace(bracket[0], '');
  302. isValid = $.proxy(settings.rules[functionType], $module)(value, ancillary);
  303. }
  304. // normal notation
  305. else {
  306. isValid = (type == 'checked')
  307. ? $field.filter(':checked').size() > 0
  308. : settings.rules[type](value)
  309. ;
  310. }
  311. return isValid;
  312. }
  313. },
  314. setting: function(name, value) {
  315. module.debug('Changing setting', name, value);
  316. if(value !== undefined) {
  317. if( $.isPlainObject(name) ) {
  318. $.extend(true, settings, name);
  319. }
  320. else {
  321. settings[name] = value;
  322. }
  323. }
  324. else {
  325. return settings[name];
  326. }
  327. },
  328. internal: function(name, value) {
  329. module.debug('Changing internal', name, value);
  330. if(value !== undefined) {
  331. if( $.isPlainObject(name) ) {
  332. $.extend(true, module, name);
  333. }
  334. else {
  335. module[name] = value;
  336. }
  337. }
  338. else {
  339. return module[name];
  340. }
  341. },
  342. debug: function() {
  343. if(settings.debug) {
  344. if(settings.performance) {
  345. module.performance.log(arguments);
  346. }
  347. else {
  348. module.debug = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  349. }
  350. }
  351. },
  352. verbose: function() {
  353. if(settings.verbose && settings.debug) {
  354. if(settings.performance) {
  355. module.performance.log(arguments);
  356. }
  357. else {
  358. module.verbose = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  359. }
  360. }
  361. },
  362. error: function() {
  363. module.error = Function.prototype.bind.call(console.log, console, settings.moduleName + ':');
  364. },
  365. performance: {
  366. log: function(message) {
  367. var
  368. currentTime,
  369. executionTime,
  370. previousTime
  371. ;
  372. if(settings.performance) {
  373. currentTime = new Date().getTime();
  374. previousTime = time || currentTime;
  375. executionTime = currentTime - previousTime;
  376. time = currentTime;
  377. performance.push({
  378. 'Element' : element,
  379. 'Name' : message[0],
  380. 'Arguments' : [].slice.call(message, 1) || '',
  381. 'Execution Time' : executionTime
  382. });
  383. }
  384. clearTimeout(module.performance.timer);
  385. module.performance.timer = setTimeout(module.performance.display, 100);
  386. },
  387. display: function() {
  388. var
  389. title = settings.moduleName + ':',
  390. totalTime = 0
  391. ;
  392. time = false;
  393. clearTimeout(module.performance.timer);
  394. $.each(performance, function(index, data) {
  395. totalTime += data['Execution Time'];
  396. });
  397. title += ' ' + totalTime + 'ms';
  398. if(moduleSelector) {
  399. title += ' \'' + moduleSelector + '\'';
  400. }
  401. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  402. console.groupCollapsed(title);
  403. if(console.table) {
  404. console.table(performance);
  405. }
  406. else {
  407. $.each(performance, function(index, data) {
  408. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  409. });
  410. }
  411. console.groupEnd();
  412. }
  413. performance = [];
  414. }
  415. },
  416. invoke: function(query, passedArguments, context) {
  417. var
  418. maxDepth,
  419. found
  420. ;
  421. passedArguments = passedArguments || queryArguments;
  422. context = element || context;
  423. if(typeof query == 'string' && instance !== undefined) {
  424. query = query.split(/[\. ]/);
  425. maxDepth = query.length - 1;
  426. $.each(query, function(depth, value) {
  427. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  428. instance = instance[value];
  429. return true;
  430. }
  431. else if( instance[value] !== undefined ) {
  432. found = instance[value];
  433. return true;
  434. }
  435. module.error(error.method);
  436. return false;
  437. });
  438. }
  439. if ( $.isFunction( found ) ) {
  440. return found.apply(context, passedArguments);
  441. }
  442. return found || false;
  443. }
  444. };
  445. if(methodInvoked) {
  446. if(instance === undefined) {
  447. module.initialize();
  448. }
  449. invokedResponse = module.invoke(query);
  450. }
  451. else {
  452. if(instance !== undefined) {
  453. module.destroy();
  454. }
  455. module.initialize();
  456. }
  457. })
  458. ;
  459. return (invokedResponse)
  460. ? invokedResponse
  461. : this
  462. ;
  463. };
  464. $.fn.form.settings = {
  465. // module info
  466. moduleName : 'Form',
  467. debug : true,
  468. verbose : true,
  469. performance : true,
  470. namespace : 'form',
  471. keyboardShortcuts : true,
  472. on : 'submit',
  473. animateSpeed : 150,
  474. inlineError : false,
  475. onValid : function() {},
  476. onInvalid : function() {},
  477. onSuccess : function() { return true; },
  478. onFailure : function() { return false; },
  479. metadata : {
  480. validate: 'validate'
  481. },
  482. selector : {
  483. message : '.error.message',
  484. field : 'input, textarea, select',
  485. group : '.field',
  486. input : 'input',
  487. prompt : '.prompt',
  488. submit : '.submit'
  489. },
  490. className : {
  491. error : 'error',
  492. success: 'success',
  493. down : 'down',
  494. label : 'ui label prompt'
  495. },
  496. // errors
  497. error: {
  498. method : 'The method you called is not defined.'
  499. },
  500. templates: {
  501. error: function(errors) {
  502. var
  503. html = '<ul class="list">'
  504. ;
  505. $.each(errors, function(index, value) {
  506. html += '<li>' + value + '</li>';
  507. });
  508. html += '</ul>';
  509. return $(html);
  510. },
  511. prompt: function(errors) {
  512. return $('<div/>')
  513. .addClass('ui red pointing prompt label')
  514. .html(errors[0])
  515. ;
  516. }
  517. },
  518. rules: {
  519. empty: function(value) {
  520. return !(value === undefined || '' === value);
  521. },
  522. email: function(value){
  523. var
  524. emailRegExp = new RegExp("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?")
  525. ;
  526. return emailRegExp.test(value);
  527. },
  528. length: function(value, requiredLength) {
  529. return (value !== undefined)
  530. ? (value.length >= requiredLength)
  531. : false
  532. ;
  533. },
  534. not: function(value, notValue) {
  535. return (value != notValue);
  536. },
  537. is: function(value, text) {
  538. return (value == text);
  539. },
  540. maxLength: function(value, maxLength) {
  541. return (value !== undefined)
  542. ? (value.length <= maxLength)
  543. : false
  544. ;
  545. },
  546. match: function(value, matchingField) {
  547. // use either id or name of field
  548. var
  549. $form = $(this),
  550. matchingValue
  551. ;
  552. if($form.find('#' + matchingField).size() > 0) {
  553. matchingValue = $form.find('#' + matchingField).val();
  554. }
  555. else if($form.find('[name=' + matchingField +']').size() > 0) {
  556. matchingValue = $form.find('[name=' + matchingField + ']').val();
  557. }
  558. else if( $form.find('[data-validate="'+ matchingField +'"]').size() > 0 ) {
  559. matchingValue = $form.find('[data-validate="'+ matchingField +'"]').val();
  560. }
  561. return (matchingValue !== undefined)
  562. ? ( value.toString() == matchingValue.toString() )
  563. : false
  564. ;
  565. },
  566. url: function(value) {
  567. var
  568. urlRegExp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
  569. ;
  570. return urlRegExp.test(value);
  571. }
  572. }
  573. };
  574. })( jQuery, window , document );