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.

706 lines
22 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 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. if(settings.keyboardShortcuts) {
  50. $field
  51. .on('keydown' + eventNamespace, module.event.field.keydown)
  52. ;
  53. }
  54. $module
  55. .on('submit' + eventNamespace, module.validate.form)
  56. ;
  57. $field
  58. .on('blur' + eventNamespace, module.event.field.blur)
  59. ;
  60. $submit
  61. .on('click' + eventNamespace, module.submit)
  62. ;
  63. $field
  64. .on(module.get.changeEvent() + eventNamespace, module.event.field.change)
  65. ;
  66. module.instantiate();
  67. },
  68. instantiate: function() {
  69. module.verbose('Storing instance of module', module);
  70. instance = module;
  71. $module
  72. .data(moduleNamespace, module)
  73. ;
  74. },
  75. destroy: function() {
  76. module.verbose('Destroying previous module', instance);
  77. $module
  78. .off(eventNamespace)
  79. .removeData(moduleNamespace)
  80. ;
  81. },
  82. refresh: function() {
  83. module.verbose('Refreshing selector cache');
  84. $field = $module.find(selector.field);
  85. },
  86. submit: function() {
  87. module.verbose('Submitting form', $module);
  88. $module
  89. .submit()
  90. ;
  91. },
  92. event: {
  93. field: {
  94. keydown: function(event) {
  95. var
  96. $field = $(this),
  97. key = event.which,
  98. keyCode = {
  99. enter : 13,
  100. escape : 27
  101. }
  102. ;
  103. if( key == keyCode.escape) {
  104. module.verbose('Escape key pressed blurring field');
  105. $field
  106. .blur()
  107. ;
  108. }
  109. if(!event.ctrlKey && key == keyCode.enter && $field.is(selector.input) ) {
  110. module.debug('Enter key pressed, submitting form');
  111. $submit
  112. .addClass(className.down)
  113. ;
  114. $field
  115. .one('keyup' + eventNamespace, module.event.field.keyup)
  116. ;
  117. event.preventDefault();
  118. return false;
  119. }
  120. },
  121. keyup: function() {
  122. module.verbose('Doing keyboard shortcut form submit');
  123. $submit.removeClass(className.down);
  124. module.submit();
  125. },
  126. blur: function() {
  127. var
  128. $field = $(this),
  129. $fieldGroup = $field.closest($group)
  130. ;
  131. if( $fieldGroup.hasClass(className.error) ) {
  132. module.debug('Revalidating field', $field, module.get.validation($field));
  133. module.validate.field( module.get.validation($field) );
  134. }
  135. else if(settings.on == 'blur' || settings.on == 'change') {
  136. module.validate.field( module.get.validation($field) );
  137. }
  138. },
  139. change: function() {
  140. var
  141. $field = $(this),
  142. $fieldGroup = $field.closest($group)
  143. ;
  144. if(settings.on == 'change' || ( $fieldGroup.hasClass(className.error) && settings.revalidate) ) {
  145. clearTimeout(module.timer);
  146. module.timer = setTimeout(function() {
  147. module.debug('Revalidating field', $field, module.get.validation($field));
  148. module.validate.field( module.get.validation($field) );
  149. }, settings.delay);
  150. }
  151. }
  152. }
  153. },
  154. get: {
  155. changeEvent: function() {
  156. return (document.createElement('input').oninput !== undefined)
  157. ? 'input'
  158. : (document.createElement('input').onpropertychange !== undefined)
  159. ? 'propertychange'
  160. : 'keyup'
  161. ;
  162. },
  163. field: function(identifier) {
  164. module.verbose('Finding field with identifier', identifier);
  165. if( $field.filter('#' + identifier).size() > 0 ) {
  166. return $field.filter('#' + identifier);
  167. }
  168. else if( $field.filter('[name="' + identifier +'"]').size() > 0 ) {
  169. return $field.filter('[name="' + identifier +'"]');
  170. }
  171. else if( $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]').size() > 0 ) {
  172. return $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]');
  173. }
  174. return $('<input/>');
  175. },
  176. validation: function($field) {
  177. var
  178. rules
  179. ;
  180. $.each(validation, function(fieldName, field) {
  181. if( module.get.field(field.identifier).get(0) == $field.get(0) ) {
  182. rules = field;
  183. }
  184. });
  185. return rules || false;
  186. }
  187. },
  188. has: {
  189. field: function(identifier) {
  190. module.verbose('Checking for existence of a field with identifier', identifier);
  191. if( $field.filter('#' + identifier).size() > 0 ) {
  192. return true;
  193. }
  194. else if( $field.filter('[name="' + identifier +'"]').size() > 0 ) {
  195. return true;
  196. }
  197. else if( $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]').size() > 0 ) {
  198. return true;
  199. }
  200. return false;
  201. }
  202. },
  203. add: {
  204. prompt: function(identifier, errors) {
  205. var
  206. $field = module.get.field(identifier),
  207. $fieldGroup = $field.closest($group),
  208. $prompt = $fieldGroup.find(selector.prompt),
  209. promptExists = ($prompt.size() !== 0)
  210. ;
  211. errors = (typeof errors == 'string')
  212. ? [errors]
  213. : errors
  214. ;
  215. module.verbose('Adding field error state', identifier);
  216. $fieldGroup
  217. .addClass(className.error)
  218. ;
  219. if(settings.inline) {
  220. if(!promptExists) {
  221. $prompt = settings.templates.prompt(errors);
  222. $prompt
  223. .appendTo($fieldGroup)
  224. ;
  225. }
  226. $prompt
  227. .html(errors[0])
  228. ;
  229. if(!promptExists) {
  230. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  231. module.verbose('Displaying error with css transition', settings.transition);
  232. $prompt.transition(settings.transition + ' in', settings.duration);
  233. }
  234. else {
  235. module.verbose('Displaying error with fallback javascript animation');
  236. $prompt
  237. .fadeIn(settings.duration)
  238. ;
  239. }
  240. }
  241. else {
  242. module.verbose('Inline errors are disabled, no inline error added', identifier);
  243. }
  244. }
  245. },
  246. errors: function(errors) {
  247. module.debug('Adding form error messages', errors);
  248. $message
  249. .html( settings.templates.error(errors) )
  250. ;
  251. }
  252. },
  253. remove: {
  254. prompt: function(field) {
  255. var
  256. $field = module.get.field(field.identifier),
  257. $fieldGroup = $field.closest($group),
  258. $prompt = $fieldGroup.find(selector.prompt)
  259. ;
  260. $fieldGroup
  261. .removeClass(className.error)
  262. ;
  263. if(settings.inline && $prompt.is(':visible')) {
  264. module.verbose('Removing prompt for field', field);
  265. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  266. $prompt.transition(settings.transition + ' out', settings.duration, function() {
  267. $prompt.remove();
  268. });
  269. }
  270. else {
  271. $prompt
  272. .fadeOut(settings.duration, function(){
  273. $prompt.remove();
  274. })
  275. ;
  276. }
  277. }
  278. }
  279. },
  280. validate: {
  281. form: function(event) {
  282. var
  283. allValid = true
  284. ;
  285. // reset errors
  286. formErrors = [];
  287. $.each(validation, function(fieldName, field) {
  288. if( !( module.validate.field(field) ) ) {
  289. allValid = false;
  290. }
  291. });
  292. if(allValid) {
  293. module.debug('Form has no validation errors, submitting');
  294. $module
  295. .removeClass(className.error)
  296. .addClass(className.success)
  297. ;
  298. $.proxy(settings.onSuccess, this)(event);
  299. }
  300. else {
  301. module.debug('Form has errors');
  302. $module.addClass(className.error);
  303. if(!settings.inline) {
  304. module.add.errors(formErrors);
  305. }
  306. return $.proxy(settings.onFailure, this)(formErrors);
  307. }
  308. },
  309. // takes a validation object and returns whether field passes validation
  310. field: function(field) {
  311. var
  312. $field = module.get.field(field.identifier),
  313. fieldValid = true,
  314. fieldErrors = []
  315. ;
  316. if(field.rules !== undefined) {
  317. $.each(field.rules, function(index, rule) {
  318. if( module.has.field(field.identifier) && !( module.validate.rule(field, rule) ) ) {
  319. module.debug('Field is invalid', field.identifier, rule.type);
  320. fieldErrors.push(rule.prompt);
  321. fieldValid = false;
  322. }
  323. });
  324. }
  325. if(fieldValid) {
  326. module.remove.prompt(field, fieldErrors);
  327. $.proxy(settings.onValid, $field)();
  328. }
  329. else {
  330. formErrors = formErrors.concat(fieldErrors);
  331. module.add.prompt(field.identifier, fieldErrors);
  332. $.proxy(settings.onInvalid, $field)(fieldErrors);
  333. return false;
  334. }
  335. return true;
  336. },
  337. // takes validation rule and returns whether field passes rule
  338. rule: function(field, validation) {
  339. var
  340. $field = module.get.field(field.identifier),
  341. type = validation.type,
  342. value = $.trim($field.val() + ''),
  343. bracketRegExp = /\[(.*?)\]/i,
  344. bracket = bracketRegExp.exec(type),
  345. isValid = true,
  346. ancillary,
  347. functionType
  348. ;
  349. // if bracket notation is used, pass in extra parameters
  350. if(bracket !== undefined && bracket !== null) {
  351. ancillary = '' + bracket[1];
  352. functionType = type.replace(bracket[0], '');
  353. isValid = $.proxy(settings.rules[functionType], $module)(value, ancillary);
  354. }
  355. // normal notation
  356. else {
  357. isValid = $.proxy(settings.rules[type], $field)(value);
  358. }
  359. return isValid;
  360. }
  361. },
  362. setting: function(name, value) {
  363. if( $.isPlainObject(name) ) {
  364. $.extend(true, settings, name);
  365. }
  366. else if(value !== undefined) {
  367. settings[name] = value;
  368. }
  369. else {
  370. return settings[name];
  371. }
  372. },
  373. internal: function(name, value) {
  374. if( $.isPlainObject(name) ) {
  375. $.extend(true, module, name);
  376. }
  377. else if(value !== undefined) {
  378. module[name] = value;
  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.name + ':');
  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.name + ':');
  402. module.verbose.apply(console, arguments);
  403. }
  404. }
  405. },
  406. error: function() {
  407. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  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. object = instance,
  467. maxDepth,
  468. found,
  469. response
  470. ;
  471. passedArguments = passedArguments || queryArguments;
  472. context = element || context;
  473. if(typeof query == 'string' && object !== undefined) {
  474. query = query.split(/[\. ]/);
  475. maxDepth = query.length - 1;
  476. $.each(query, function(depth, value) {
  477. var camelCaseValue = (depth != maxDepth)
  478. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  479. : query
  480. ;
  481. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  482. object = object[camelCaseValue];
  483. }
  484. else if( object[camelCaseValue] !== undefined ) {
  485. found = object[camelCaseValue];
  486. return false;
  487. }
  488. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  489. object = object[value];
  490. }
  491. else if( object[value] !== undefined ) {
  492. found = object[value];
  493. return false;
  494. }
  495. else {
  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(returnedValue)) {
  507. returnedValue.push(response);
  508. }
  509. else if(returnedValue !== undefined) {
  510. returnedValue = [returnedValue, response];
  511. }
  512. else if(response !== undefined) {
  513. returnedValue = 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 (returnedValue !== undefined)
  533. ? returnedValue
  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. delay : 200,
  547. revalidate : true,
  548. transition : 'scale',
  549. duration : 150,
  550. onValid : function() {},
  551. onInvalid : function() {},
  552. onSuccess : function() { return true; },
  553. onFailure : function() { return false; },
  554. metadata : {
  555. validate: 'validate'
  556. },
  557. selector : {
  558. message : '.error.message',
  559. field : 'input, textarea, select',
  560. group : '.field',
  561. input : 'input',
  562. prompt : '.prompt',
  563. submit : '.submit'
  564. },
  565. className : {
  566. error : 'error',
  567. success : 'success',
  568. down : 'down',
  569. label : 'ui label prompt'
  570. },
  571. // errors
  572. error: {
  573. method : 'The method you called is not defined.'
  574. },
  575. templates: {
  576. error: function(errors) {
  577. var
  578. html = '<ul class="list">'
  579. ;
  580. $.each(errors, function(index, value) {
  581. html += '<li>' + value + '</li>';
  582. });
  583. html += '</ul>';
  584. return $(html);
  585. },
  586. prompt: function(errors) {
  587. return $('<div/>')
  588. .addClass('ui red pointing prompt label')
  589. .html(errors[0])
  590. ;
  591. }
  592. },
  593. rules: {
  594. checked: function() {
  595. return ($(this).filter(':checked').size() > 0);
  596. },
  597. empty: function(value) {
  598. return !(value === undefined || '' === value);
  599. },
  600. email: function(value){
  601. var
  602. 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")
  603. ;
  604. return emailRegExp.test(value);
  605. },
  606. length: function(value, requiredLength) {
  607. return (value !== undefined)
  608. ? (value.length >= requiredLength)
  609. : false
  610. ;
  611. },
  612. not: function(value, notValue) {
  613. return (value != notValue);
  614. },
  615. contains: function(value, text) {
  616. text = text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
  617. return (value.search(text) !== -1);
  618. },
  619. is: function(value, text) {
  620. return (value == text);
  621. },
  622. maxLength: function(value, maxLength) {
  623. return (value !== undefined)
  624. ? (value.length <= maxLength)
  625. : false
  626. ;
  627. },
  628. match: function(value, fieldIdentifier) {
  629. // use either id or name of field
  630. var
  631. $form = $(this),
  632. matchingValue
  633. ;
  634. if($form.find('#' + fieldIdentifier).size() > 0) {
  635. matchingValue = $form.find('#' + fieldIdentifier).val();
  636. }
  637. else if($form.find('[name=' + fieldIdentifier +']').size() > 0) {
  638. matchingValue = $form.find('[name=' + fieldIdentifier + ']').val();
  639. }
  640. else if( $form.find('[data-validate="'+ fieldIdentifier +'"]').size() > 0 ) {
  641. matchingValue = $form.find('[data-validate="'+ fieldIdentifier +'"]').val();
  642. }
  643. return (matchingValue !== undefined)
  644. ? ( value.toString() == matchingValue.toString() )
  645. : false
  646. ;
  647. },
  648. url: function(value) {
  649. var
  650. urlRegExp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
  651. ;
  652. return urlRegExp.test(value);
  653. }
  654. }
  655. };
  656. })( jQuery, window , document );