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.

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