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.

654 lines
20 KiB

11 years ago
  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. module.debug.apply(console, arguments);
  350. }
  351. }
  352. },
  353. verbose: function() {
  354. if(settings.verbose && settings.debug) {
  355. if(settings.performance) {
  356. module.performance.log(arguments);
  357. }
  358. else {
  359. module.verbose = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  360. module.verbose.apply(console, arguments);
  361. }
  362. }
  363. },
  364. error: function() {
  365. module.error = Function.prototype.bind.call(console.error, console, settings.moduleName + ':');
  366. module.error.apply(console, arguments);
  367. },
  368. performance: {
  369. log: function(message) {
  370. var
  371. currentTime,
  372. executionTime,
  373. previousTime
  374. ;
  375. if(settings.performance) {
  376. currentTime = new Date().getTime();
  377. previousTime = time || currentTime;
  378. executionTime = currentTime - previousTime;
  379. time = currentTime;
  380. performance.push({
  381. 'Element' : element,
  382. 'Name' : message[0],
  383. 'Arguments' : [].slice.call(message, 1) || '',
  384. 'Execution Time' : executionTime
  385. });
  386. }
  387. clearTimeout(module.performance.timer);
  388. module.performance.timer = setTimeout(module.performance.display, 100);
  389. },
  390. display: function() {
  391. var
  392. title = settings.moduleName + ':',
  393. totalTime = 0
  394. ;
  395. time = false;
  396. clearTimeout(module.performance.timer);
  397. $.each(performance, function(index, data) {
  398. totalTime += data['Execution Time'];
  399. });
  400. title += ' ' + totalTime + 'ms';
  401. if(moduleSelector) {
  402. title += ' \'' + moduleSelector + '\'';
  403. }
  404. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  405. console.groupCollapsed(title);
  406. if(console.table) {
  407. console.table(performance);
  408. }
  409. else {
  410. $.each(performance, function(index, data) {
  411. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  412. });
  413. }
  414. console.groupEnd();
  415. }
  416. performance = [];
  417. }
  418. },
  419. invoke: function(query, passedArguments, context) {
  420. var
  421. maxDepth,
  422. found,
  423. response
  424. ;
  425. passedArguments = passedArguments || queryArguments;
  426. context = element || context;
  427. if(typeof query == 'string' && instance !== undefined) {
  428. query = query.split(/[\. ]/);
  429. maxDepth = query.length - 1;
  430. $.each(query, function(depth, value) {
  431. var camelCaseValue = (depth != maxDepth)
  432. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  433. : query
  434. ;
  435. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  436. instance = instance[value];
  437. }
  438. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  439. instance = instance[camelCaseValue];
  440. }
  441. else if( instance[value] !== undefined ) {
  442. found = instance[value];
  443. return false;
  444. }
  445. else if( instance[camelCaseValue] !== undefined ) {
  446. found = instance[camelCaseValue];
  447. return false;
  448. }
  449. else {
  450. module.error(error.method);
  451. return false;
  452. }
  453. });
  454. }
  455. if ( $.isFunction( found ) ) {
  456. response = found.apply(context, passedArguments);
  457. }
  458. else if(found !== undefined) {
  459. response = found;
  460. }
  461. if($.isArray(invokedResponse)) {
  462. invokedResponse.push(response);
  463. }
  464. else if(typeof invokedResponse == 'string') {
  465. invokedResponse = [invokedResponse, response];
  466. }
  467. else if(response !== undefined) {
  468. invokedResponse = response;
  469. }
  470. return found;
  471. }
  472. };
  473. if(methodInvoked) {
  474. if(instance === undefined) {
  475. module.initialize();
  476. }
  477. module.invoke(query);
  478. }
  479. else {
  480. if(instance !== undefined) {
  481. module.destroy();
  482. }
  483. module.initialize();
  484. }
  485. })
  486. ;
  487. return (invokedResponse !== undefined)
  488. ? invokedResponse
  489. : this
  490. ;
  491. };
  492. $.fn.form.settings = {
  493. // module info
  494. moduleName : 'Form',
  495. debug : true,
  496. verbose : true,
  497. performance : true,
  498. namespace : 'form',
  499. keyboardShortcuts : true,
  500. on : 'submit',
  501. animateSpeed : 150,
  502. inlineError : false,
  503. onValid : function() {},
  504. onInvalid : function() {},
  505. onSuccess : function() { return true; },
  506. onFailure : function() { return false; },
  507. metadata : {
  508. validate: 'validate'
  509. },
  510. selector : {
  511. message : '.error.message',
  512. field : 'input, textarea, select',
  513. group : '.field',
  514. input : 'input',
  515. prompt : '.prompt',
  516. submit : '.submit'
  517. },
  518. className : {
  519. error : 'error',
  520. success: 'success',
  521. down : 'down',
  522. label : 'ui label prompt'
  523. },
  524. // errors
  525. error: {
  526. method : 'The method you called is not defined.'
  527. },
  528. templates: {
  529. error: function(errors) {
  530. var
  531. html = '<ul class="list">'
  532. ;
  533. $.each(errors, function(index, value) {
  534. html += '<li>' + value + '</li>';
  535. });
  536. html += '</ul>';
  537. return $(html);
  538. },
  539. prompt: function(errors) {
  540. return $('<div/>')
  541. .addClass('ui red pointing prompt label')
  542. .html(errors[0])
  543. ;
  544. }
  545. },
  546. rules: {
  547. empty: function(value) {
  548. return !(value === undefined || '' === value);
  549. },
  550. email: function(value){
  551. var
  552. 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])?")
  553. ;
  554. return emailRegExp.test(value);
  555. },
  556. length: function(value, requiredLength) {
  557. return (value !== undefined)
  558. ? (value.length >= requiredLength)
  559. : false
  560. ;
  561. },
  562. not: function(value, notValue) {
  563. return (value != notValue);
  564. },
  565. is: function(value, text) {
  566. return (value == text);
  567. },
  568. maxLength: function(value, maxLength) {
  569. return (value !== undefined)
  570. ? (value.length <= maxLength)
  571. : false
  572. ;
  573. },
  574. match: function(value, matchingField) {
  575. // use either id or name of field
  576. var
  577. $form = $(this),
  578. matchingValue
  579. ;
  580. if($form.find('#' + matchingField).size() > 0) {
  581. matchingValue = $form.find('#' + matchingField).val();
  582. }
  583. else if($form.find('[name=' + matchingField +']').size() > 0) {
  584. matchingValue = $form.find('[name=' + matchingField + ']').val();
  585. }
  586. else if( $form.find('[data-validate="'+ matchingField +'"]').size() > 0 ) {
  587. matchingValue = $form.find('[data-validate="'+ matchingField +'"]').val();
  588. }
  589. return (matchingValue !== undefined)
  590. ? ( value.toString() == matchingValue.toString() )
  591. : false
  592. ;
  593. },
  594. url: function(value) {
  595. var
  596. urlRegExp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
  597. ;
  598. return urlRegExp.test(value);
  599. }
  600. }
  601. };
  602. })( jQuery, window , document );