├── .babelrc ├── .editorconfig ├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── dist ├── index.html └── js │ ├── index.js │ └── index.min.js ├── index.test.js ├── jest.config.js ├── package-lock.json ├── package.json └── src ├── index.html └── js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "last 5 versions", 9 | "Explorer >= 11" 10 | ] 11 | } 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | 10 | [*.{js,css,scss}] 11 | indent_style = tab -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | coverage/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | 5 | settings: { 6 | srcPath: 'src/', 7 | distPath: 'dist/', 8 | fileName: 'index' 9 | }, 10 | 11 | babel: { 12 | dist: { 13 | files: { 14 | '<%= settings.distPath %>js/<%= settings.fileName %>.js': [ 15 | '<%= settings.srcPath %>js/<%= settings.fileName %>.js' 16 | ] 17 | } 18 | } 19 | }, 20 | 21 | uglify: { 22 | minify: { 23 | options: { 24 | beautify: false 25 | }, 26 | files: { 27 | '<%= settings.distPath %>js/<%= settings.fileName %>.min.js': [ 28 | '<%= settings.distPath %>js/<%= settings.fileName %>.js' 29 | ] 30 | } 31 | } 32 | }, 33 | 34 | umd: { 35 | all: { 36 | options: { 37 | src: '<%= settings.distPath %>js/<%= settings.fileName %>.js', 38 | dest: '<%= settings.distPath %>js/<%= settings.fileName %>.js', 39 | objectToExport: 'jCaptcha', 40 | } 41 | } 42 | }, 43 | 44 | htmlmin: { 45 | dist: { 46 | options: { 47 | removeComments: true, 48 | collapseWhitespace: true 49 | }, 50 | files: [{ 51 | expand: true, 52 | cwd: '<%= settings.srcPath %>', 53 | src: ['**/*.html'], 54 | dest: '<%= settings.distPath %>' 55 | }] 56 | } 57 | }, 58 | 59 | watch: { 60 | javascript: { 61 | expand: true, 62 | files: ['<%= settings.srcPath %>js/**/*.js', 'Gruntfile.js'], 63 | tasks: ['babel', 'umd', 'uglify'], 64 | options: { 65 | spawn: false 66 | } 67 | }, 68 | html: { 69 | files: ['<%= settings.srcPath %>*.html'], 70 | tasks: ['htmlmin'], 71 | options: { 72 | spawn: false 73 | } 74 | } 75 | } 76 | 77 | }); 78 | 79 | require('load-grunt-tasks')(grunt); 80 | 81 | grunt.registerTask('default', ['watch']); 82 | grunt.registerTask('build', ['babel', 'umd', 'uglify', 'htmlmin']); 83 | 84 | }; 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Robert Velickovski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript Captcha # 2 | ### Simple captcha component (<2KB) written in pure JavaScript with no dependencies ### 3 | 4 | Simple numeric captcha rendered within basic [canvas element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas). 5 | 6 | ### Demo ### 7 | 8 | Demo can be seen [here](https://robiveli.github.io/js-captcha/). 9 | 10 | ### Install ### 11 | 12 | ```console 13 | npm install js-captcha --save 14 | ``` 15 | 16 | ### Usage ### 17 | 18 | Just include required JavaScript: 19 | ```html 20 | 21 | ``` 22 | or 23 | ```js 24 | import jCaptcha from 'jCaptcha'; 25 | ``` 26 | Define main captcha input element in HTML: 27 | ```html 28 | 29 | ``` 30 | 31 | Initialize it: 32 | ```html 33 | 61 | ``` 62 | 63 | And then call `validate()` method when required (e.g. on form submit event): 64 | ```html 65 | 68 | ``` 69 | 70 | No stylings included, just style it as you wish, see options below. 71 | 72 | ### Options ### 73 | 74 | jCaptcha can take an optional parameter - an [Object] of key/value settings: 75 | 76 | Name | Required | Type | Default | Description | 77 | | --- | --- | --- | --- | --- | 78 | | el | false | [String] | 'jCaptcha' | CSS class for input element | 79 | | requiredValue | false | [String] | '*' | Render new random numbers on error validate | 80 | | resetOnError | false | [Boolean] | true | Mandatory field indicator | 81 | | focusOnError | false | [Boolean] | true | Focus input field on error validate | 82 | | clearOnSubmit | false | [Boolean] | true' | Clear input value on every validate | 83 | | callback | false | [Function] | null | As invoked function these useful arguments are returned: response (type: *String*, value: *'success'* or *'error'*), captcha (type: *Element*) and number of tries (type: *Number*) | 84 | | canvasClass | false | [String] | 'jCaptchaCanvas' | CSS class of canvas captcha 85 | | canvasStyle | true | [Object] | {} | Canvas stylings object, required for canvas appearance | 86 | | canvasStyle.width | false | [Number] | null | Width of canvas captcha element (in px) | 87 | | canvasStyle.height | false | [Number] | null | Height of canvas captcha element (in px) | 88 | | canvasStyle.font | false | [String] | '' | Font size and font family of canvas captcha element | 89 | | canvasStyle.fillStyle | false | [String] | '' | Text color of canvas captcha element | 90 | | canvasStyle.textAlign | false | [String] | '' | Text align of canvas captcha element | 91 | | canvasStyle.textBaseline | false | [String] | '' | Text baseline of canvas captcha element | 92 | 93 | 94 | ### API ### 95 | 96 | `reset()` - generate and render new random numbers 97 | 98 | `validate()` - validate entered result in input field 99 | 100 | 101 | ### Browser support ### 102 | 103 | Works in every modern browser which has support for [canvas element](http://caniuse.com/#feat=canvas-text). 104 | 105 | ### License ### 106 | 107 | js-captcha is licensed under the [MIT license](http://opensource.org/licenses/MIT). 108 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | jCaptcha - Simple captcha component written in pure JavaScript with no dependencies

JavaScript Captcha

jCaptcha

Tiny captcha component written in pure JavaScript with no dependencies based on canvas element

View demo source code
-------------------------------------------------------------------------------- /dist/js/index.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (root === undefined && window !== undefined) root = window; 3 | if (typeof define === 'function' && define.amd) { 4 | // AMD. Register as an anonymous module unless amdModuleId is set 5 | define([], function () { 6 | return (root['jCaptcha'] = factory()); 7 | }); 8 | } else if (typeof module === 'object' && module.exports) { 9 | // Node. Does not work with strict CommonJS, but 10 | // only CommonJS-like environments that support module.exports, 11 | // like Node. 12 | module.exports = factory(); 13 | } else { 14 | root['jCaptcha'] = factory(); 15 | } 16 | }(this, function () { 17 | 18 | "use strict"; 19 | 20 | { 21 | var generateRandomNum = function generateRandomNum() { 22 | num1 = Math.round(Math.random() * 8) + 1; 23 | num2 = Math.round(Math.random() * 8) + 1; 24 | sumNum = num1 + num2; 25 | }; 26 | /** 27 | * @param {Object} 28 | * @param {Object} 29 | * @param {Boolean} 30 | */ 31 | 32 | 33 | var setCaptcha = function setCaptcha($el, options, shouldReset) { 34 | if (!shouldReset) { 35 | $el.insertAdjacentHTML('beforebegin', "\n \n ")); 36 | this.$captchaEl = document.querySelector(".".concat(options.canvasClass)); 37 | this.$captchaTextContext = this.$captchaEl.getContext('2d'); 38 | this.$captchaTextContext = Object.assign(this.$captchaTextContext, options.canvasStyle); 39 | } 40 | 41 | this.$captchaTextContext.clearRect(0, 0, options.canvasStyle.width, options.canvasStyle.height); 42 | this.$captchaTextContext.fillText("".concat(num1, " + ").concat(num2, " ").concat(options.requiredValue), 0, 0); 43 | }; 44 | /** 45 | * @param {Object} 46 | */ 47 | 48 | 49 | var jCaptcha = function jCaptcha() { 50 | var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 51 | this.options = Object.assign({}, { 52 | el: '.jCaptcha', 53 | canvasClass: 'jCaptchaCanvas', 54 | requiredValue: '*', 55 | resetOnError: true, 56 | focusOnError: true, 57 | clearOnSubmit: true, 58 | callback: null, 59 | canvasStyle: {} 60 | }, options); 61 | 62 | this._init(); 63 | }; 64 | 65 | var sumNum, num1, num2; 66 | var numberOfTries = 0; 67 | ; 68 | jCaptcha.prototype = { 69 | _init: function _init() { 70 | this.$el = document.querySelector(this.options.el); 71 | generateRandomNum(); 72 | setCaptcha.call(this, this.$el, this.options); 73 | }, 74 | validate: function validate() { 75 | numberOfTries++; 76 | this.callbackReceived = this.callbackReceived || typeof this.options.callback == 'function'; 77 | 78 | if (this.$el.value != sumNum) { 79 | this.callbackReceived && this.options.callback('error', this.$el, numberOfTries); 80 | this.options.resetOnError === true && this.reset(); 81 | this.options.focusOnError === true && this.$el.focus(); 82 | this.options.clearOnSubmit === true && (this.$el.value = ''); 83 | } else { 84 | this.callbackReceived && this.options.callback('success', this.$el, numberOfTries); 85 | this.options.clearOnSubmit === true && (this.$el.value = ''); 86 | } 87 | }, 88 | reset: function reset() { 89 | generateRandomNum(); 90 | setCaptcha.call(this, this.$el, this.options, true); 91 | } 92 | }; 93 | } 94 | 95 | return jCaptcha; 96 | 97 | })); 98 | -------------------------------------------------------------------------------- /dist/js/index.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){void 0===t&&void 0!==window&&(t=window),"function"==typeof define&&define.amd?define([],function(){return t.jCaptcha=e()}):"object"==typeof module&&module.exports?module.exports=e():t.jCaptcha=e()}(this,function(){"use strict";function t(){i=Math.round(8*Math.random())+1,n=Math.round(8*Math.random())+1,c=i+n}function e(t,e,a){a||(t.insertAdjacentHTML("beforebegin",'\n \n ')),this.$captchaEl=document.querySelector(".".concat(e.canvasClass)),this.$captchaTextContext=this.$captchaEl.getContext("2d"),this.$captchaTextContext=Object.assign(this.$captchaTextContext,e.canvasStyle)),this.$captchaTextContext.clearRect(0,0,e.canvasStyle.width,e.canvasStyle.height),this.$captchaTextContext.fillText("".concat(i," + ").concat(n," ").concat(e.requiredValue),0,0)}function a(t){var e=0 { 7 | 8 | test('should throw an error if no default `.jCaptcha` element present in DOM', () => { 9 | expect(() => { 10 | myCaptcha = new jCaptcha(); 11 | }).toThrowError(); 12 | }); 13 | }); 14 | 15 | describe('initialize success', () => { 16 | 17 | test('should be initialized if default `.jCaptcha` element exist', () => { 18 | document.body.innerHTML = ``; 19 | myCaptcha = new jCaptcha(); 20 | 21 | expect(myCaptcha).toBeDefined(); 22 | }); 23 | 24 | test('should create default canvas `.jCaptchaCanvas` element', () => { 25 | expect(document.querySelector('.jCaptchaCanvas')).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | setupFiles: ['jest-canvas-mock'], 4 | globals: { 5 | myCaptcha: null 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-captcha", 3 | "title": "JavaScript Captcha", 4 | "description": "Simple captcha component written in pure JavaScript with no dependencies based on canvas element", 5 | "version": "1.2.1", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Robert Velickovski", 9 | "email": "roby@rvdizajn.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/robiveli/js-captcha.git" 14 | }, 15 | "main": "dist/js/index.js", 16 | "keywords": [ 17 | "captcha", 18 | "vanilla", 19 | "simple", 20 | "dependency-free", 21 | "lightweight", 22 | "javascript" 23 | ], 24 | "devDependencies": { 25 | "@babel/core": "^7.9.6", 26 | "@babel/preset-env": "^7.9.6", 27 | "babel-core": "^6.26.3", 28 | "babel-preset-env": "^1.7.0", 29 | "grunt": "^1.1.0", 30 | "grunt-babel": "^8.0.0", 31 | "grunt-contrib-htmlmin": "^3.1.0", 32 | "grunt-contrib-uglify": "^4.0.1", 33 | "grunt-contrib-watch": "^1.1.0", 34 | "grunt-umd": "^3.0.0", 35 | "jest": "^26.0.1", 36 | "jest-canvas-mock": "^2.2.0", 37 | "load-grunt-tasks": "^5.1.0" 38 | }, 39 | "scripts": { 40 | "preinstall": "npm install -g np npm-check-updates", 41 | "watch": "grunt watch", 42 | "build": "grunt build && ncu", 43 | "dependency-check": "ncu", 44 | "dependency-update": "ncu -u && npm install && npm audit fix", 45 | "test": "jest --coverage", 46 | "publish": "npm run test && np --no-yarn" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jCaptcha - Simple captcha component written in pure JavaScript with no dependencies 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 255 | 256 | 257 | 258 |
259 | 260 |

JavaScript Captcha

261 | 262 |

jCaptcha

263 | 264 |

Tiny captcha component written in pure JavaScript with no dependencies based on canvas element

265 | 266 |
267 | 268 |
269 | 270 |
271 | 272 | 273 | 274 | 275 |
276 | 277 | View demo source code 278 | 279 |
280 | 281 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 478 | 479 | 480 | 481 | 482 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | { 2 | 3 | let sumNum, num1, num2; 4 | let numberOfTries = 0; 5 | 6 | function generateRandomNum() { 7 | 8 | num1 = Math.round(Math.random() * (8)) + 1; 9 | num2 = Math.round(Math.random() * (8)) + 1; 10 | 11 | sumNum = num1 + num2; 12 | } 13 | 14 | /** 15 | * @param {Object} 16 | * @param {Object} 17 | * @param {Boolean} 18 | */ 19 | function setCaptcha($el, options, shouldReset) { 20 | 21 | if (!shouldReset) { 22 | $el.insertAdjacentHTML('beforebegin', 23 | ` 25 | 26 | `); 27 | 28 | this.$captchaEl = document.querySelector(`.${options.canvasClass}`); 29 | this.$captchaTextContext = this.$captchaEl.getContext('2d'); 30 | 31 | this.$captchaTextContext = Object.assign(this.$captchaTextContext, options.canvasStyle); 32 | } 33 | 34 | this.$captchaTextContext.clearRect(0, 0, options.canvasStyle.width, options.canvasStyle.height); 35 | this.$captchaTextContext.fillText(`${num1} + ${num2} ${options.requiredValue}`, 0, 0); 36 | } 37 | 38 | /** 39 | * @param {Object} 40 | */ 41 | function jCaptcha(options = {}) { 42 | 43 | this.options = Object.assign({}, { 44 | el: '.jCaptcha', 45 | canvasClass: 'jCaptchaCanvas', 46 | requiredValue: '*', 47 | resetOnError: true, 48 | focusOnError: true, 49 | clearOnSubmit: true, 50 | callback: null, 51 | canvasStyle: { 52 | } 53 | }, options); 54 | 55 | this._init(); 56 | 57 | }; 58 | 59 | jCaptcha.prototype = { 60 | 61 | _init() { 62 | 63 | this.$el = document.querySelector(this.options.el); 64 | 65 | generateRandomNum(); 66 | setCaptcha.call(this, this.$el, this.options); 67 | }, 68 | 69 | validate() { 70 | numberOfTries++; 71 | 72 | this.callbackReceived = this.callbackReceived || (typeof this.options.callback == 'function'); 73 | 74 | if (this.$el.value != sumNum) { 75 | 76 | this.callbackReceived && this.options.callback('error', this.$el, numberOfTries); 77 | 78 | (this.options.resetOnError === true) && this.reset(); 79 | (this.options.focusOnError === true) && this.$el.focus(); 80 | (this.options.clearOnSubmit === true) && (this.$el.value = ''); 81 | 82 | } else { 83 | 84 | this.callbackReceived && this.options.callback('success', this.$el, numberOfTries); 85 | (this.options.clearOnSubmit === true) && (this.$el.value = ''); 86 | } 87 | }, 88 | 89 | reset() { 90 | 91 | generateRandomNum(); 92 | setCaptcha.call(this, this.$el, this.options, true); 93 | } 94 | }; 95 | } 96 | --------------------------------------------------------------------------------