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.

738 lines
23 KiB

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