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.

795 lines
24 KiB

10 years ago
  1. /*
  2. * # Semantic - Form Validation
  3. * http://github.com/semantic-org/semantic-ui/
  4. *
  5. *
  6. * Copyright 2014 Contributor
  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. set: {
  317. success: function() {
  318. $module
  319. .removeClass(className.error)
  320. .addClass(className.success)
  321. ;
  322. },
  323. error: function() {
  324. $module
  325. .removeClass(className.success)
  326. .addClass(className.error)
  327. ;
  328. }
  329. },
  330. validate: {
  331. form: function(event) {
  332. var
  333. allValid = true,
  334. apiRequest
  335. ;
  336. // reset errors
  337. formErrors = [];
  338. $.each(validation, function(fieldName, field) {
  339. if( !( module.validate.field(field) ) ) {
  340. allValid = false;
  341. }
  342. });
  343. if(allValid) {
  344. module.debug('Form has no validation errors, submitting');
  345. module.set.success();
  346. return $.proxy(settings.onSuccess, this)(event);
  347. }
  348. else {
  349. module.debug('Form has errors');
  350. module.set.error();
  351. if(!settings.inline) {
  352. module.add.errors(formErrors);
  353. }
  354. // prevent ajax submit
  355. if($module.data('moduleApi') !== undefined) {
  356. event.stopImmediatePropagation();
  357. }
  358. return $.proxy(settings.onFailure, this)(formErrors);
  359. }
  360. },
  361. // takes a validation object and returns whether field passes validation
  362. field: function(field) {
  363. var
  364. $field = module.get.field(field.identifier),
  365. fieldValid = true,
  366. fieldErrors = []
  367. ;
  368. if(field.rules !== undefined) {
  369. $.each(field.rules, function(index, rule) {
  370. if( module.has.field(field.identifier) && !( module.validate.rule(field, rule) ) ) {
  371. module.debug('Field is invalid', field.identifier, rule.type);
  372. fieldErrors.push(rule.prompt);
  373. fieldValid = false;
  374. }
  375. });
  376. }
  377. if(fieldValid) {
  378. module.remove.prompt(field, fieldErrors);
  379. $.proxy(settings.onValid, $field)();
  380. }
  381. else {
  382. formErrors = formErrors.concat(fieldErrors);
  383. module.add.prompt(field.identifier, fieldErrors);
  384. $.proxy(settings.onInvalid, $field)(fieldErrors);
  385. return false;
  386. }
  387. return true;
  388. },
  389. // takes validation rule and returns whether field passes rule
  390. rule: function(field, validation) {
  391. var
  392. $field = module.get.field(field.identifier),
  393. type = validation.type,
  394. value = $.trim($field.val() + ''),
  395. bracketRegExp = /\[(.*)\]/i,
  396. bracket = bracketRegExp.exec(type),
  397. isValid = true,
  398. ancillary,
  399. functionType
  400. ;
  401. // if bracket notation is used, pass in extra parameters
  402. if(bracket !== undefined && bracket !== null) {
  403. ancillary = '' + bracket[1];
  404. functionType = type.replace(bracket[0], '');
  405. isValid = $.proxy(settings.rules[functionType], $module)(value, ancillary);
  406. }
  407. // normal notation
  408. else {
  409. isValid = $.proxy(settings.rules[type], $field)(value);
  410. }
  411. return isValid;
  412. }
  413. },
  414. setting: function(name, value) {
  415. if( $.isPlainObject(name) ) {
  416. $.extend(true, settings, name);
  417. }
  418. else if(value !== undefined) {
  419. settings[name] = value;
  420. }
  421. else {
  422. return settings[name];
  423. }
  424. },
  425. internal: function(name, value) {
  426. if( $.isPlainObject(name) ) {
  427. $.extend(true, module, name);
  428. }
  429. else if(value !== undefined) {
  430. module[name] = value;
  431. }
  432. else {
  433. return module[name];
  434. }
  435. },
  436. debug: function() {
  437. if(settings.debug) {
  438. if(settings.performance) {
  439. module.performance.log(arguments);
  440. }
  441. else {
  442. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  443. module.debug.apply(console, arguments);
  444. }
  445. }
  446. },
  447. verbose: function() {
  448. if(settings.verbose && settings.debug) {
  449. if(settings.performance) {
  450. module.performance.log(arguments);
  451. }
  452. else {
  453. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  454. module.verbose.apply(console, arguments);
  455. }
  456. }
  457. },
  458. error: function() {
  459. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  460. module.error.apply(console, arguments);
  461. },
  462. performance: {
  463. log: function(message) {
  464. var
  465. currentTime,
  466. executionTime,
  467. previousTime
  468. ;
  469. if(settings.performance) {
  470. currentTime = new Date().getTime();
  471. previousTime = time || currentTime;
  472. executionTime = currentTime - previousTime;
  473. time = currentTime;
  474. performance.push({
  475. 'Name' : message[0],
  476. 'Arguments' : [].slice.call(message, 1) || '',
  477. 'Element' : element,
  478. 'Execution Time' : executionTime
  479. });
  480. }
  481. clearTimeout(module.performance.timer);
  482. module.performance.timer = setTimeout(module.performance.display, 100);
  483. },
  484. display: function() {
  485. var
  486. title = settings.name + ':',
  487. totalTime = 0
  488. ;
  489. time = false;
  490. clearTimeout(module.performance.timer);
  491. $.each(performance, function(index, data) {
  492. totalTime += data['Execution Time'];
  493. });
  494. title += ' ' + totalTime + 'ms';
  495. if(moduleSelector) {
  496. title += ' \'' + moduleSelector + '\'';
  497. }
  498. if($allModules.size() > 1) {
  499. title += ' ' + '(' + $allModules.size() + ')';
  500. }
  501. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  502. console.groupCollapsed(title);
  503. if(console.table) {
  504. console.table(performance);
  505. }
  506. else {
  507. $.each(performance, function(index, data) {
  508. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  509. });
  510. }
  511. console.groupEnd();
  512. }
  513. performance = [];
  514. }
  515. },
  516. invoke: function(query, passedArguments, context) {
  517. var
  518. object = instance,
  519. maxDepth,
  520. found,
  521. response
  522. ;
  523. passedArguments = passedArguments || queryArguments;
  524. context = element || context;
  525. if(typeof query == 'string' && object !== undefined) {
  526. query = query.split(/[\. ]/);
  527. maxDepth = query.length - 1;
  528. $.each(query, function(depth, value) {
  529. var camelCaseValue = (depth != maxDepth)
  530. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  531. : query
  532. ;
  533. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  534. object = object[camelCaseValue];
  535. }
  536. else if( object[camelCaseValue] !== undefined ) {
  537. found = object[camelCaseValue];
  538. return false;
  539. }
  540. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  541. object = object[value];
  542. }
  543. else if( object[value] !== undefined ) {
  544. found = object[value];
  545. return false;
  546. }
  547. else {
  548. return false;
  549. }
  550. });
  551. }
  552. if ( $.isFunction( found ) ) {
  553. response = found.apply(context, passedArguments);
  554. }
  555. else if(found !== undefined) {
  556. response = found;
  557. }
  558. if($.isArray(returnedValue)) {
  559. returnedValue.push(response);
  560. }
  561. else if(returnedValue !== undefined) {
  562. returnedValue = [returnedValue, response];
  563. }
  564. else if(response !== undefined) {
  565. returnedValue = response;
  566. }
  567. return found;
  568. }
  569. };
  570. if(methodInvoked) {
  571. if(instance === undefined) {
  572. module.initialize();
  573. }
  574. module.invoke(query);
  575. }
  576. else {
  577. if(instance !== undefined) {
  578. module.destroy();
  579. }
  580. module.initialize();
  581. }
  582. })
  583. ;
  584. return (returnedValue !== undefined)
  585. ? returnedValue
  586. : this
  587. ;
  588. };
  589. $.fn.form.settings = {
  590. name : 'Form',
  591. namespace : 'form',
  592. debug : false,
  593. verbose : true,
  594. performance : true,
  595. keyboardShortcuts : true,
  596. on : 'submit',
  597. inline : false,
  598. delay : 200,
  599. revalidate : true,
  600. transition : 'scale',
  601. duration : 150,
  602. onValid : function() {},
  603. onInvalid : function() {},
  604. onSuccess : function() { return true; },
  605. onFailure : function() { return false; },
  606. metadata : {
  607. validate: 'validate'
  608. },
  609. selector : {
  610. message : '.error.message',
  611. field : 'input, textarea, select',
  612. group : '.field',
  613. checkbox: 'input[type="checkbox"], input[type="radio"]',
  614. input : 'input',
  615. prompt : '.prompt',
  616. submit : '.submit'
  617. },
  618. className : {
  619. error : 'error',
  620. success : 'success',
  621. down : 'down',
  622. label : 'ui label prompt'
  623. },
  624. // errors
  625. error: {
  626. method : 'The method you called is not defined.'
  627. },
  628. templates: {
  629. error: function(errors) {
  630. var
  631. html = '<ul class="list">'
  632. ;
  633. $.each(errors, function(index, value) {
  634. html += '<li>' + value + '</li>';
  635. });
  636. html += '</ul>';
  637. return $(html);
  638. },
  639. prompt: function(errors) {
  640. return $('<div/>')
  641. .addClass('ui red pointing prompt label')
  642. .html(errors[0])
  643. ;
  644. }
  645. },
  646. rules: {
  647. checked: function() {
  648. return ($(this).filter(':checked').size() > 0);
  649. },
  650. empty: function(value) {
  651. return !(value === undefined || '' === value);
  652. },
  653. email: function(value){
  654. var
  655. 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")
  656. ;
  657. return emailRegExp.test(value);
  658. },
  659. length: function(value, requiredLength) {
  660. return (value !== undefined)
  661. ? (value.length >= requiredLength)
  662. : false
  663. ;
  664. },
  665. not: function(value, notValue) {
  666. return (value != notValue);
  667. },
  668. contains: function(value, text) {
  669. text = text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
  670. return (value.search(text) !== -1);
  671. },
  672. is: function(value, text) {
  673. return (value == text);
  674. },
  675. maxLength: function(value, maxLength) {
  676. return (value !== undefined)
  677. ? (value.length <= maxLength)
  678. : false
  679. ;
  680. },
  681. match: function(value, fieldIdentifier) {
  682. // use either id or name of field
  683. var
  684. $form = $(this),
  685. matchingValue
  686. ;
  687. if($form.find('#' + fieldIdentifier).size() > 0) {
  688. matchingValue = $form.find('#' + fieldIdentifier).val();
  689. }
  690. else if($form.find('[name=' + fieldIdentifier +']').size() > 0) {
  691. matchingValue = $form.find('[name=' + fieldIdentifier + ']').val();
  692. }
  693. else if( $form.find('[data-validate="'+ fieldIdentifier +'"]').size() > 0 ) {
  694. matchingValue = $form.find('[data-validate="'+ fieldIdentifier +'"]').val();
  695. }
  696. return (matchingValue !== undefined)
  697. ? ( value.toString() == matchingValue.toString() )
  698. : false
  699. ;
  700. },
  701. url: function(value) {
  702. var
  703. urlRegExp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
  704. ;
  705. return urlRegExp.test(value);
  706. },
  707. integer: function(value, range) {
  708. var
  709. intRegExp = /^\-?\d+$/,
  710. min,
  711. max,
  712. parts
  713. ;
  714. if (range === undefined || range === '' || range === '..') {
  715. // do nothing
  716. }
  717. else if (range.indexOf('..') == -1) {
  718. if (intRegExp.test(range)) {
  719. min = max = range - 0;
  720. }
  721. }
  722. else {
  723. parts = range.split('..', 2);
  724. if (intRegExp.test(parts[0])) {
  725. min = parts[0] - 0;
  726. }
  727. if (intRegExp.test(parts[1])) {
  728. max = parts[1] - 0;
  729. }
  730. }
  731. return (
  732. intRegExp.test(value) &&
  733. (min === undefined || value >= min) &&
  734. (max === undefined || value <= max)
  735. );
  736. }
  737. }
  738. };
  739. })( jQuery, window , document );