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.

740 lines
23 KiB

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