├── .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 dependenciesView 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 "));
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 ')),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 |
267 |
268 |
269 |
270 |
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 | `
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 |
--------------------------------------------------------------------------------