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

10 years ago
10 years ago
10 years ago
10 years ago
  1. /*
  2. * # Semantic - Form Validation
  3. * http://github.com/jlukic/semantic-ui/
  4. *
  5. *
  6. * Copyright 2014 Contributors
  7. * Released under the MIT license
  8. * http://opensource.org/licenses/MIT
  9. *
  10. */
  11. ;(function ( $, window, document, undefined ) {
  12. module.exports = function(fields, parameters) {
  13. var
  14. $allModules = $(this),
  15. settings = $.extend(true, {}, module.exports.settings, parameters),
  16. validation = $.extend({}, module.exports.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 && module.exports !== 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 && module.exports !== 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. module.exports.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. })( require("jquery"), window , document );