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.

748 lines
23 KiB

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