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.

823 lines
25 KiB

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