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.

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