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.

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