├── .all-contributorsrc
├── .babelrc
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── LICENSE_996
├── README.md
├── eslintignore.json
├── example
├── global.d.ts
└── index.tsx
├── index.js
├── jest.config.js
├── package.json
├── postcss.config.js
├── react-codes-input.gif
├── src
├── __tests__
│ ├── index.js
│ └── utils.js
├── css
│ └── example.css
├── html
│ └── layout.html
└── js
│ └── Input
│ ├── ReactCodesInput.tsx
│ ├── global.d.ts
│ ├── index.global.ts
│ ├── index.ts
│ ├── react-codes-input.css
│ └── utils.ts
├── stylelint.config.js
├── tea.yaml
├── tsconfig.json
└── webpack
├── base.babel.js
├── build_path.js
├── development.config.babel.js
├── production.config.babel.js
├── umd.base.config.babel.js
├── umd.global.config.babel.js
└── umd.local.config.babel.js
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "contributors": [
8 | {
9 | "login": "edwardfxiao",
10 | "name": "Edward Xiao",
11 | "avatar_url": "https://avatars.githubusercontent.com/u/11728228?v=4",
12 | "profile": "https://github.com/edwardfxiao",
13 | "contributions": [
14 | "code",
15 | "doc",
16 | "infra",
17 | "test",
18 | "review"
19 | ]
20 | },
21 | {
22 | "login": "bulhiCzar",
23 | "name": "bulhiCzar",
24 | "avatar_url": "https://avatars.githubusercontent.com/u/70818351?v=4",
25 | "profile": "https://github.com/bulhiCzar",
26 | "contributions": [
27 | "code",
28 | "doc",
29 | "bug"
30 | ]
31 | },
32 | {
33 | "login": "alyona-mordas",
34 | "name": "Olena Mordas",
35 | "avatar_url": "https://avatars.githubusercontent.com/u/95859516?v=4",
36 | "profile": "https://github.com/alyona-mordas",
37 | "contributions": [
38 | "bug"
39 | ]
40 | }
41 | ],
42 | "contributorsPerLine": 7,
43 | "projectName": "react-codes-input",
44 | "projectOwner": "edwardfxiao",
45 | "repoType": "github",
46 | "repoHost": "https://github.com",
47 | "skipCi": true
48 | }
49 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"],
3 | "env":
4 | {
5 | "development":
6 | {
7 |
8 | },
9 | "production":
10 | {
11 |
12 | },
13 | "lib":
14 | {
15 | "plugins": [
16 | ["css-modules-transform",
17 | {
18 | "generateScopedName": "[name]__[local]___[hash:base64:5]"
19 | }],
20 | "@babel/proposal-class-properties",
21 | "@babel/proposal-object-rest-spread"
22 | ],
23 | },
24 | }
25 | }
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@babel/eslint-parser",
3 | // Extend existing configuration
4 | // from ESlint and eslint-plugin-react defaults.
5 | "extends": ["eslint:recommended", "plugin:react/recommended"],
6 | // Enable ES6 support. If you want to use custom Babel
7 | // features, you will need to enable a custom parser
8 | // as described in a section below.
9 | "parserOptions": {
10 | "ecmaVersion": 6,
11 | "sourceType": "module",
12 | "ecmaFeatures": {
13 | "experimentalObjectRestSpread": true
14 | }
15 | },
16 | "env": {
17 | "es6": true,
18 | "browser": true,
19 | "node": true,
20 | "jquery": true
21 | },
22 | // Enable custom plugin known as eslint-plugin-react
23 | "plugins": ["react", "react-hooks"],
24 | "rules": {
25 | // Disable `no-console` rule
26 | "no-console": 0,
27 | // Give a warning if identifiers contain underscores
28 | "no-underscore-dangle": 0,
29 | "no-empty-pattern": 0,
30 | "react/prop-types": 0,
31 | "react-hooks/rules-of-hooks": "error",
32 | "no-empty": [
33 | "error",
34 | {
35 | "allowEmptyCatch": true
36 | }
37 | ],
38 | "react/display-name": [0, { "ignoreTranspilerName": true }]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /*.log
3 | /log/*.log
4 | coverage
5 | /notes
6 | /lib
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /example
3 | /webpack
4 | /src
5 | /.babelrc
6 | /.eslintrc.json
7 | /eslintignore.json
8 | /.gitignore
9 | /.travis.yml
10 | /jest.config.js
11 | /postcss.config.js
12 | /stylelint.config.js
13 | /tsconfig.json
14 | /tslint.json
15 | /*.gif
16 | /dist
17 | /*.html
18 | /.github
19 | /rev-manifest.json
20 | /docs
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "10"
4 | before_script:
5 | - npm i
6 | script: npm run prepublish
7 | after_script: "cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js"
8 | env:
9 | - REACT=16
10 | notifications:
11 | email:
12 | - email:edwardfhsiao@gmail.com
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 2.2.3
2 |
3 | - Workaround #20
4 |
5 | # 2.2.2
6 |
7 | - Fix problem on android chrome only #15
8 |
9 | # 2.2.1
10 |
11 | - Update `README.md` since it is on cdnjs
12 |
13 | # 2.2.0
14 |
15 | - Add `auto` option to `letterCase` #17
16 |
17 | # 2.1.10
18 |
19 | - Fix potencial bugs
20 |
21 | # 2.1.9
22 |
23 | - Fix issues only on android browser #15
24 |
25 | # 2.1.8
26 |
27 | - Only set input and curselection once if initial value is passed
28 |
29 | # 2.1.7
30 |
31 | - set input value #14
32 |
33 | # 2.1.6
34 |
35 | - type check `navigator` #13
36 |
37 | # 2.1.5
38 |
39 | - Remove classname hash
40 |
41 | # 2.1.4
42 |
43 | - Enable `Tab` `Shift + Tab` for switching the cell
44 |
45 | # 2.1.3
46 |
47 | - Enable `ArrowLeft` `ArrowRight` `ArrowUp` `ArrowDown` for switching the cell, and `Delete` #11 (Not available in moblie)
48 | - Stable input ID
49 |
50 | # 2.1.2
51 |
52 | - Change `input` `type` to `password` from `text` (and `tel` if is on mobile device), due to input source switching issue
53 |
54 | # 2.1.1
55 |
56 | - Change `input` `type` to `text` from `password`
57 | - Change `input` `margin-left` to `-9999999px`
58 |
59 | # 2.1.0
60 |
61 | - #6
62 | - Uses `require` attribute of html input element, which not support IE9
63 |
64 | # 2.0.8
65 |
66 | - https://github.com/edwardfxiao/react-codes-input/issues/7
67 |
68 | # 2.0.7
69 |
70 | - Better TypeScript support (https://github.com/edwardfxiao/react-codes-input/pull/5)
71 | - Add global classname
72 | - Update readme
73 |
74 | # 2.0.6
75 |
76 | - Support IE9+ again when compiling with webpack
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-present Edward Xiao
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 |
--------------------------------------------------------------------------------
/LICENSE_996:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019-present Edward Xiao
2 |
3 | Anti 996 License Version 1.0 (Draft)
4 |
5 | Permission is hereby granted to any individual or legal entity obtaining a copy
6 | of this licensed work (including the source code, documentation and/or related
7 | items, hereinafter collectively referred to as the "licensed work"), free of
8 | charge, to deal with the licensed work for any purpose, including without
9 | limitation, the rights to use, reproduce, modify, prepare derivative works of,
10 | publish, distribute and sublicense the licensed work, subject to the following
11 | conditions:
12 |
13 | 1. The individual or the legal entity must conspicuously display, without
14 | modification, this License on each redistributed or derivative copy of the
15 | Licensed Work.
16 |
17 | 2. The individual or the legal entity must strictly comply with all applicable
18 | laws, regulations, rules and standards of the jurisdiction relating to
19 | labor and employment where the individual is physically located or where
20 | the individual was born or naturalized; or where the legal entity is
21 | registered or is operating (whichever is stricter). In case that the
22 | jurisdiction has no such laws, regulations, rules and standards or its
23 | laws, regulations, rules and standards are unenforceable, the individual
24 | or the legal entity are required to comply with Core International Labor
25 | Standards.
26 |
27 | 3. The individual or the legal entity shall not induce or force its
28 | employee(s), whether full-time or part-time, or its independent
29 | contractor(s), in any methods, to agree in oral or written form,
30 | to directly or indirectly restrict, weaken or relinquish his or
31 | her rights or remedies under such laws, regulations, rules and
32 | standards relating to labor and employment as mentioned above,
33 | no matter whether such written or oral agreement are enforceable
34 | under the laws of the said jurisdiction, nor shall such individual
35 | or the legal entity limit, in any methods, the rights of its employee(s)
36 | or independent contractor(s) from reporting or complaining to the copyright
37 | holder or relevant authorities monitoring the compliance of the license
38 | about its violation(s) of the said license.
39 |
40 | THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
42 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT
43 | HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
44 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION
45 | WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-codes-input
2 |
3 | [](#contributors-)
4 |
5 | [](https://badge.fury.io/js/react-codes-input)   [](https://raw.githubusercontent.com/edwardfxiao/react-codes-input/master/LICENSE) [](https://github.com/996icu/996.ICU/blob/master/LICENSE) [](https://996.icu)
6 |
7 | A react component for PIN, verify code and passcode entering. Online demo examples.
8 |
9 |
10 |
11 | # Online Demo
12 | Online demo example
13 |
14 | Demo source code
15 |
16 | # Codesandbox Examples
17 | * Examples
18 |
19 | ### Version of ```16.8.6``` or higher of react and react-dom is required.
20 | ```js
21 | "peerDependencies": {
22 | "react": ">= 16.8.6",
23 | "react-dom": ">= 16.8.6"
24 | }
25 | ```
26 |
27 | # Installation
28 | ```sh
29 | npm install react-codes-input --save
30 | ```
31 |
32 | #### By CDN (starting from v2.2.0)
33 | ```html
34 |
35 | ...
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 | ```
53 |
54 | # Donation
55 | Thanks for donating me a donut! ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄
56 |
57 | # Browser support
58 | Tested on ~~IE9+~~ IE10+ and Chrome and Safari(10.0.3)
59 |
60 | This library uses ```require``` attribute of html input element, which not support IE9 from v2.1.0
61 |
62 | # Docs
63 |
64 | |Props | |Type |Description |Default |
65 | |--- |--- |--- |--- | --- |
66 | |initialFocus | Opt | Bool |Options are ['false', 'true'] | false |
67 | |wrapperRef | Opt | React Ref | | none |
68 | |codeLength | Opt | Number | | 6 |
69 | |id | Opt | Str | | random ID |
70 | |onChange | Opt | Func |(value: string) => void | none |
71 | |type | Opt | Str |Options are ['alphanumeric', 'alpha', 'number']| "alphanumeric" |
72 | |letterCase | Opt | Str |Options are ['upper', 'lower', 'auto'] | "upper" |
73 | |value | Opt | Str | | "" |
74 | |disabled | Opt | Bool |Options are [false, true] | false |
75 | |hide | Opt | Bool |Options are [false, true] | false |
76 | |placeholder | Opt | Str | | "" |
77 | |focusColor | Opt | Str | | "#007bff" |
78 | |classNameComponent | Opt | Str | | "" |
79 | |classNameWrapper | Opt | Str | | "" |
80 | |classNameCodeWrapper | Opt | Str | | "" |
81 | |classNameEnteredValue | Opt | Str | | "" |
82 | |classNameCode | Opt | Str | | "" |
83 | |classNameCodeWrapperFocus | Opt | Str | | {} |
84 | |customStyleComponent | Opt | Obj | | {} |
85 | |customStyleWrapper | Opt | Obj | | {} |
86 | |customStyleCodeWrapper | Opt | Obj | | {} |
87 | |customStyleEnteredValue | Opt | Obj | | {} |
88 | |customStyleCode | Opt | Obj | | {} |
89 | |customStyleCodeWrapperFocus | Opt | Obj | | {} |
90 | |customStylePlaceholder | Opt | Obj | | {} |
91 |
92 |
93 |
94 | ```js
95 | import ReactCodesInput from 'react-codes-input';
96 | import 'react-codes-input/lib/react-codes-input.min.css';
97 |
98 |
99 |
126 | ```
127 |
128 |
129 | ## Contributors ✨
130 |
131 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
132 |
133 |
134 |
135 |
136 |
143 |
144 |
145 |
146 |
147 |
148 |
149 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
150 |
--------------------------------------------------------------------------------
/eslintignore.json:
--------------------------------------------------------------------------------
1 | coverage/
2 | dist/
3 | lib/
--------------------------------------------------------------------------------
/example/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css' {
2 | interface IClassNames {
3 | [className: string]: string
4 | }
5 | const classNames: IClassNames;
6 | export = classNames;
7 | }
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import 'raf/polyfill';
2 | import 'core-js/stable';
3 | import 'regenerator-runtime/runtime';
4 | import React, { useState, useRef } from 'react';
5 | import ReactDOM from 'react-dom';
6 | import CSS from '../src/css/example.css';
7 | import ReactCodesInput from '../src/js/Input/index';
8 |
9 | const Component = () => {
10 | const $passwordWrapperRef = useRef(null);
11 | const $pinWrapperRef = useRef(null);
12 | const [password, setPassword] = useState('');
13 | const [pin, setPin] = useState('');
14 | const [activation, setActivation] = useState('');
15 | return (
16 |
137 | );
138 | };
139 |
140 | //
141 |
142 | ReactDOM.render(, document.getElementById('root'));
143 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var ReactCodesInput = require('./lib/components/index.js');
2 | module.exports = ReactCodesInput;
3 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'jsdom',
3 | preset: 'ts-jest',
4 | transform: {
5 | '^.+\\.(js|jsx)$': 'babel-jest',
6 | '^.+\\.tsx?$': 'ts-jest',
7 | },
8 | verbose: true,
9 | moduleNameMapper: {
10 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/assetsTransformer.js',
11 | '\\.(css|scss)$': 'identity-obj-proxy',
12 | '^STYLES(.*)$': '/src/css$1',
13 | '^COMPONENTS(.*)$': '/src/js/app/components$1',
14 | '^API(.*)$': '/src/js/api$1',
15 | '^CONFIG(.*)$': '/src/config$1',
16 | '^IMAGES(.*)$': '/src/image$1',
17 | '^AUDIOS(.*)$': '/audio/api$1',
18 | '^VIDEOS(.*)$': '/src/video$1',
19 | '^LOCALES(.*)$': '/src/locales$1',
20 | '^COMMON(.*)$': '/src/js/common$1',
21 | '^APP(.*)$': '/src/js/app$1',
22 | '^CONSTS(.*)$': '/src/js/consts$1',
23 | '^PAGES(.*)$': '/src/js/api$1',
24 | '^ACTIONS(.*)$': '/src/js/actions$1',
25 | '^STORE(.*)$': '/src/js/store$1',
26 | '^REDUCERS(.*)$': '/src/js/reducers$1',
27 | '^VENDOR(.*)$': '/src/js/vendor$1',
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-codes-input",
3 | "version": "2.2.3",
4 | "description": "A react component for PIN, verify code and passcode entering.",
5 | "main": "index.js",
6 | "types": "./lib/index.d.ts",
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/edwardfxiao/react-codes-input.git"
10 | },
11 | "keywords": [
12 | "react",
13 | "input",
14 | "inputs",
15 | "code",
16 | "password",
17 | "form",
18 | "textbox",
19 | "PIN",
20 | "pin-code",
21 | "react-codes-input"
22 | ],
23 | "author": "Edward Xiao",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/edwardfxiao/react-codes-input/issues"
27 | },
28 | "homepage": "https://edwardfxiao.github.io/react-codes-input",
29 | "scripts": {
30 | "test": "NODE_ENV=test jest",
31 | "test:coverage": "npm run tslint && npm run jslint && npm test -- --coverage",
32 | "test_single": "node_modules/.bin/jest utils.js --coverage",
33 | "tslint": "tslint -c tslint.json 'src/**/*.{ts,tsx}'",
34 | "prepublish": "npm run test:coverage",
35 | "build_gh_page": "rm -rf lib && rm -rf dist && NODE_ENV=production ./node_modules/.bin/webpack --config ./webpack/production.config.babel.js --progress",
36 | "umd_local": "./node_modules/.bin/webpack --config ./webpack/umd.local.config.babel.js",
37 | "umd_global": "./node_modules/.bin/webpack --config ./webpack/umd.global.config.babel.js",
38 | "umd_global_min": "./node_modules/.bin/webpack --config ./webpack/umd.global.config.babel.js --env minify",
39 | "dev": "node_modules/.bin/webpack-dev-server --config ./webpack/development.config.babel.js",
40 | "compile": "rimraf dist lib && npm run umd_global && npm run umd_global_min && npm run umd_local && rm ./lib/components/*.css"
41 | },
42 | "peerDependencies": {
43 | "react": ">= 16.8.6",
44 | "react-dom": ">= 16.8.6"
45 | },
46 | "devDependencies": {
47 | "@babel/cli": "^7.17.0",
48 | "@babel/core": "^7.17.2",
49 | "@babel/eslint-parser": "^7.17.0",
50 | "@babel/plugin-proposal-class-properties": "^7.16.7",
51 | "@babel/preset-env": "^7.16.11",
52 | "@babel/preset-react": "^7.16.7",
53 | "@babel/register": "^7.17.0",
54 | "@swc/core": "^1.2.139",
55 | "@swc/wasm": "^1.2.139",
56 | "@types/jest": "^27.4.0",
57 | "@types/react": "^16.8.14",
58 | "@types/react-dom": "^16.8.4",
59 | "babel-jest": "^27.5.1",
60 | "babel-loader": "^8.2.3",
61 | "babel-plugin-css-modules-transform": "^1.6.2",
62 | "bufferutil": "^4.0.6",
63 | "chai": "^4.2.0",
64 | "core-js": "^3.21.0",
65 | "coveralls": "^3.1.1",
66 | "css-loader": "^6.4.0",
67 | "cssnano": "^5.0.8",
68 | "enzyme": "^3.10.0",
69 | "enzyme-adapter-react-16": "^1.13.2",
70 | "esbuild": "^0.14.21",
71 | "eslint": "^8.0.1",
72 | "eslint-plugin-react": "^7.26.1",
73 | "eslint-plugin-react-hooks": "^4.2.0",
74 | "eslint-webpack-plugin": "^3.1.1",
75 | "file-loader": "^6.2.0",
76 | "html-webpack-plugin": "^5.4.0",
77 | "identity-obj-proxy": "^3.0.0",
78 | "inline-style-prefix-all": "^2.0.2",
79 | "jest": "^27.4.0",
80 | "mini-css-extract-plugin": "^2.4.3",
81 | "node-notifier": "^10.0.1",
82 | "postcss-css-variables": "^0.17.0",
83 | "postcss-custom-properties": "^9.1.1",
84 | "postcss-import": "^14.0.2",
85 | "postcss-loader": "^6.2.0",
86 | "postcss-preset-env": "^7.3.1",
87 | "postcss-simple-vars": "^6.0.3",
88 | "prismjs": "^1.16.0",
89 | "react": "^16.8.6",
90 | "react-dom": "^16.8.6",
91 | "react-markdown": "^8.0.0",
92 | "regenerator-runtime": "^0.13.9",
93 | "rimraf": "^3.0.2",
94 | "ts-jest": "^27.1.3",
95 | "ts-loader": "^9.2.6",
96 | "ts-node": "^10.5.0",
97 | "typescript": "^4.5.5",
98 | "url-loader": "^4.1.1",
99 | "utf-8-validate": "^5.0.8",
100 | "webpack": "^5.68.0",
101 | "webpack-assets-manifest": "^5.1.0",
102 | "webpack-cli": "^4.9.2",
103 | "webpack-dev-server": "^4.7.4"
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
--------------------------------------------------------------------------------
/react-codes-input.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edwardfxiao/react-codes-input/7524e1aa12c6c821db2b5b915d3835a5088fdd4c/react-codes-input.gif
--------------------------------------------------------------------------------
/src/__tests__/index.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import { configure, mount } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 | // import ReactCodesInput from '../js/Input/index.tsx';
5 | configure({ adapter: new Adapter() });
6 |
7 | const ID = 'code';
8 |
9 | describe('ReactCodesInput component', () => {
10 | it('[Toggling "validate"]: Should show msgHtml(err) when toggling "validate"', () => {
11 | // const $ref = useRef(null);
12 | // const wrapper = mount();
13 | // console.log(wrapper.ref)
14 | // const $input = wrapper.find(`#${ID}0`);
15 | // console.log($input.instance())
16 | // $input.simulate('click');
17 | // expect(value).toEqual('clicked');
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/__tests__/utils.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { getCased, getNumeric, getAlpha, getAlphanumeric, cx, getRandomId } from '../js/Input/utils.ts';
3 |
4 | describe('message', () => {
5 | it('[getCased]: Should return FOOBAR', () => {
6 | expect(getCased('foobar', 0)).equal('FOOBAR');
7 | });
8 | it('[getCased]: Should return FOOBAR', () => {
9 | expect(getCased('foobar', 'upper')).equal('FOOBAR');
10 | });
11 | it('[getCased]: Should return foobar', () => {
12 | expect(getCased('foobar', 'lower')).equal('foobar');
13 | });
14 | it('[getNumeric]: Should return 123456', () => {
15 | expect(getNumeric('1foobar2345.6')).equal('123456');
16 | });
17 | it('[getAlpha]: Should return foobar', () => {
18 | expect(getAlpha('1foobar2345.6')).equal('foobar');
19 | });
20 | it('[getAlphanumeric]: Should return 1foobar23456', () => {
21 | expect(getAlphanumeric('1foobar2345.6')).equal('1foobar23456');
22 | });
23 | it('[cx]: Should return "a b"', () => {
24 | expect(cx('a', 'b')).equal('a b');
25 | });
26 | it('[cx]: Should return "a a"', () => {
27 | expect(cx('a', { a: 'b' })).equal('a a');
28 | });
29 | it('[cx]: Should return "a a"', () => {
30 | expect(cx('a', ['a', 'b'])).equal('a a b');
31 | });
32 | it('[cx]: Should return ""', () => {
33 | expect(cx(null)).equal('');
34 | });
35 | it('[cx]: Should return ""', () => {
36 | expect(cx([null])).equal('');
37 | });
38 | it('[cx]: Should return ""', () => {
39 | expect(cx({ a: null })).equal('');
40 | });
41 | it('[cx]: Should return "a"', () => {
42 | expect(cx({ a: 'b' })).equal('a');
43 | });
44 | it('[cx]: Should return ""', () => {
45 | expect(cx({ a: false })).equal('');
46 | });
47 | it('[cx]: Should return ""', () => {
48 | expect(cx([])).equal('');
49 | });
50 | it('[cx]: Should return ""', () => {
51 | expect(cx({})).equal('');
52 | });
53 | it('[getRandomId]: Should return a text with length of 8', () => {
54 | expect(getRandomId().length).equal(8);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/css/example.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | .nav {
6 | padding: 20px;
7 | background-color: #ececec;
8 | }
9 |
10 | .nav a {
11 | color: #006fb4;
12 | }
13 |
14 | .example-section {
15 | padding: 10px;
16 | margin-bottom: 10px;
17 | display: flex;
18 | justify-content: center;
19 | }
20 |
21 | :global .submit-btn {
22 | margin-top: 20px;
23 | -moz-box-shadow: inset 0px 1px 0px 0px #54a3f7;
24 | -webkit-box-shadow: inset 0px 1px 0px 0px #54a3f7;
25 | box-shadow: inset 0px 1px 0px 0px #54a3f7;
26 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0.05, #007dc1), color-stop(1, #0061a7));
27 | background: -moz-linear-gradient(top, #007dc1 5%, #0061a7 100%);
28 | background: -webkit-linear-gradient(top, #007dc1 5%, #0061a7 100%);
29 | background: -o-linear-gradient(top, #007dc1 5%, #0061a7 100%);
30 | background: -ms-linear-gradient(top, #007dc1 5%, #0061a7 100%);
31 | background: linear-gradient(to bottom, #007dc1 5%, #0061a7 100%);
32 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#007dc1', endColorstr='#0061a7', GradientType=0);
33 | background-color: #007dc1;
34 | -moz-border-radius: 3px;
35 | -webkit-border-radius: 3px;
36 | border-radius: 3px;
37 | border: 1px solid #124d77;
38 | display: inline-block;
39 | cursor: pointer;
40 | color: #ffffff;
41 | font-family: Arial;
42 | font-size: 13px;
43 | padding: 6px 24px;
44 | text-decoration: none;
45 | text-shadow: 0px 1px 0px #154682;
46 | }
47 |
48 | :global .submit-btn:hover {
49 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0.05, #0061a7), color-stop(1, #007dc1));
50 | background: -moz-linear-gradient(top, #0061a7 5%, #007dc1 100%);
51 | background: -webkit-linear-gradient(top, #0061a7 5%, #007dc1 100%);
52 | background: -o-linear-gradient(top, #0061a7 5%, #007dc1 100%);
53 | background: -ms-linear-gradient(top, #0061a7 5%, #007dc1 100%);
54 | background: linear-gradient(to bottom, #0061a7 5%, #007dc1 100%);
55 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0061a7', endColorstr='#007dc1', GradientType=0);
56 | background-color: #0061a7;
57 | }
58 |
59 | :global .submit-btn:active {
60 | position: relative;
61 | top: 1px;
62 | }
63 |
64 | @media only screen and (max-width: 1000px) {
65 | .example-section {
66 | display: block;
67 | }
68 | }
--------------------------------------------------------------------------------
/src/html/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= htmlWebpackPlugin.options.title %>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 | <%= htmlWebpackPlugin.options.customJs %>
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/js/Input/ReactCodesInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
2 | import { cx, getRandomId, getAlphanumeric, getAlpha, getNumeric, getCased, CASE_TYPES, getClassName, isMobile, isChrome, isAndroid } from './utils';
3 | import CSS from './react-codes-input.css';
4 | const DEFAULT_CODE_LENGTH = 6;
5 | const ALPHABETS_BASE = 'abcdefghijklmnopqrstuvwxyz';
6 | const ALPHABETS = ALPHABETS_BASE.split('');
7 | const ALPHABETS_CAP = ALPHABETS_BASE.toUpperCase().split('');
8 | const NUMBERS = '0123456789'.split('');
9 | const ALPHABETNUMERICS = [...ALPHABETS, ...ALPHABETS_CAP, ...NUMBERS];
10 | const TAB = 'tab';
11 | const ENTER = 'enter';
12 | const BACKSPACE = 'backspace';
13 | const DELETE = 'delete';
14 | const ARROW_LEFT = 'arrowleft';
15 | const ARROW_RIGHT = 'arrowright';
16 | const ARROW_UP = 'arrowup';
17 | const ARROW_DOWN = 'arrowdown';
18 | const OPERRATION_KEYS = [ENTER, BACKSPACE, DELETE, ARROW_LEFT, ARROW_RIGHT, ARROW_UP, ARROW_DOWN];
19 | const ALLOWED_KEYS = [...ALPHABETNUMERICS, ...OPERRATION_KEYS];
20 | const INVALID_KEY = '';
21 | const HIDDEN_INPUT_TYPE = 'password';
22 | const isMobileDevice = isMobile();
23 | const isChromeDevice = isChrome();
24 | const isAndroidDevice = isAndroid();
25 | const autoCase = (cap: boolean, shift: boolean, key: string) => {
26 | if (cap) {
27 | key = key.toUpperCase();
28 | if (shift) {
29 | key = key.toLowerCase();
30 | }
31 | } else {
32 | key = key.toLowerCase();
33 | if (shift) {
34 | key = key.toUpperCase();
35 | }
36 | }
37 | return key;
38 | };
39 | const isValidKey = (key: string, type: string, code: string) => {
40 | if (!ALLOWED_KEYS.includes(key)) {
41 | return INVALID_KEY;
42 | }
43 | if (OPERRATION_KEYS.includes(key)) {
44 | switch (key) {
45 | case BACKSPACE: {
46 | if (code === '') {
47 | return INVALID_KEY;
48 | } else {
49 | return BACKSPACE;
50 | }
51 | }
52 | case DELETE: {
53 | if (code === '') {
54 | return INVALID_KEY;
55 | } else {
56 | return DELETE;
57 | }
58 | }
59 | case ARROW_UP: {
60 | return ARROW_LEFT;
61 | }
62 | case ARROW_DOWN: {
63 | return ARROW_RIGHT;
64 | }
65 | default: {
66 | return key;
67 | }
68 | }
69 | }
70 | if (type === DEFAULT_TYPES.NUMBER) {
71 | if (NUMBERS.indexOf(key) < 0) {
72 | return INVALID_KEY;
73 | }
74 | }
75 | if (type === DEFAULT_TYPES.ALPHA) {
76 | if (ALPHABETS.indexOf(key) < 0) {
77 | return INVALID_KEY;
78 | }
79 | }
80 | if (type === DEFAULT_TYPES.ALPHANUMERTIC) {
81 | if (ALPHABETNUMERICS.indexOf(key) < 0) {
82 | return INVALID_KEY;
83 | }
84 | }
85 | return key;
86 | };
87 | export enum DEFAULT_TYPES {
88 | ALPHANUMERTIC = 'alphanumeric',
89 | ALPHA = 'alpha',
90 | NUMBER = 'number',
91 | }
92 | interface AttibutesObj {
93 | type?: string;
94 | pattern?: string;
95 | minLength?: number;
96 | }
97 | export interface ReactCodesInputProps {
98 | wrapperRef?: React.RefObject;
99 | value?: string;
100 | onChange?: (value: string) => void;
101 | initialFocus?: boolean;
102 | codeLength?: number;
103 | id?: string;
104 | type?: 'number' | 'alpha' | 'alphanumeric';
105 | letterCase?: 'upper' | 'lower' | 'auto';
106 | disabled?: boolean;
107 | hide?: boolean;
108 | focusColor?: string;
109 | classNameComponent?: string;
110 | classNameWrapper?: string;
111 | classNameCodeWrapper?: string;
112 | classNameEnteredValue?: string;
113 | classNameCode?: string;
114 | classNameCodeWrapperFocus?: string;
115 | customStyleComponent?: React.CSSProperties;
116 | customStyleWrapper?: React.CSSProperties;
117 | customStyleCodeWrapper?: React.CSSProperties;
118 | customStyleEnteredValue?: React.CSSProperties;
119 | customStyleCode?: React.CSSProperties;
120 | customStyleCodeWrapperFocus?: React.CSSProperties;
121 | placeholder?: string;
122 | customStylePlaceholder?: React.CSSProperties;
123 | }
124 | const ReactCodesInput: React.FC = ({
125 | initialFocus = false,
126 | wrapperRef,
127 | codeLength = DEFAULT_CODE_LENGTH,
128 | id = null,
129 | onChange,
130 | type = DEFAULT_TYPES.ALPHANUMERTIC,
131 | letterCase = CASE_TYPES.UPPERCASE,
132 | value = '',
133 | disabled = false,
134 | hide = false,
135 | focusColor = '#007bff',
136 | classNameComponent = '',
137 | classNameWrapper = '',
138 | classNameCodeWrapper = '',
139 | classNameEnteredValue = '',
140 | classNameCode = '',
141 | classNameCodeWrapperFocus = '',
142 | customStyleComponent = {},
143 | customStyleWrapper = {},
144 | customStyleCodeWrapper = {},
145 | customStyleEnteredValue = {},
146 | customStyleCode = {},
147 | customStyleCodeWrapperFocus = {},
148 | placeholder = '',
149 | customStylePlaceholder = {},
150 | }) => {
151 | const DEFAULT_CODES = useMemo(() => [...Array(codeLength).keys()], [codeLength]);
152 | const [code, setCode] = useState(value);
153 | const [pressKey, setPressKey] = useState({ key: undefined });
154 | const [isFocus, setIsFocus] = useState(false);
155 | const $wrapperRef = useRef(null);
156 | const $component = useRef(null);
157 | const $inputRef = useRef(null);
158 | const isInitial = useRef(true);
159 | const [curItemIndex, setCurItemIndex] = useState(0);
160 | const inputId = useMemo(() => id || getRandomId(), [id]);
161 | useEffect(() => {
162 | if (isAndroidDevice && isChromeDevice) {
163 | const textInput = (e: any) => {
164 | if (letterCase === CASE_TYPES.AUTO) {
165 | if (ALPHABETNUMERICS.includes(e.data)) {
166 | setPressKey({ key: e.data });
167 | return;
168 | }
169 | }
170 | const key = e.data.toLowerCase();
171 | if (key.match(/^[a-zA-Z0-9_]*$/gi)) {
172 | setPressKey({ key });
173 | }
174 | };
175 | $inputRef.current.addEventListener('textInput', textInput);
176 | return () => {
177 | if ($inputRef.current) {
178 | $inputRef.current.removeEventListener('textInput', textInput);
179 | }
180 | };
181 | }
182 | }, []);
183 | useEffect(() => {
184 | if (initialFocus) {
185 | document.getElementById(`${inputId}${0}`).click();
186 | }
187 | }, []);
188 | const onKeyDown = useCallback(
189 | (key: string) => {
190 | if (!isMobileDevice) {
191 | if (key === ARROW_LEFT) {
192 | const left = curItemIndex - 1;
193 | setCurItemIndex(left < 0 ? 0 : left);
194 | return;
195 | }
196 | if (key === ARROW_RIGHT) {
197 | if (code === '') {
198 | return;
199 | }
200 | const right = Math.min(curItemIndex + 1, DEFAULT_CODES.length - 1);
201 | if (typeof code[right] === 'undefined') {
202 | setCurItemIndex(code.length);
203 | return;
204 | }
205 | setCurItemIndex(right);
206 | return;
207 | }
208 | }
209 | handleOnCodeChange(key);
210 | },
211 | [type, code, curItemIndex],
212 | );
213 | useEffect(() => {
214 | if (pressKey.key) {
215 | onKeyDown(pressKey.key);
216 | }
217 | }, [pressKey]);
218 | useEffect(() => {
219 | if (curItemIndex >= DEFAULT_CODES.length) {
220 | setCurItemIndex(DEFAULT_CODES.length - 1);
221 | return;
222 | }
223 | // workaround #20, maybe it was for IE9
224 | // $inputRef.current.setSelectionRange(curItemIndex, curItemIndex);
225 | }, [curItemIndex, DEFAULT_CODES]);
226 | useEffect(() => {
227 | const code = getCased(value, letterCase);
228 | setCode(code);
229 | if (isInitial.current) {
230 | $inputRef.current.value = code;
231 | setCurItemIndex(code.length);
232 | isInitial.current = false;
233 | }
234 | }, [value, letterCase]);
235 | const handleOnCodeChange = useCallback(
236 | pressedKey => {
237 | const codeSplits = code.split('');
238 | if (pressedKey === BACKSPACE) {
239 | // BACKSPACE case: set current code item empty and setCurItemIndex
240 | let index = curItemIndex;
241 | if (curItemIndex === DEFAULT_CODES.length - 1) {
242 | if (code.length === DEFAULT_CODES.length) {
243 | codeSplits[index] = '';
244 | const newCode = codeSplits.join('');
245 | handleSetCode(index, newCode, onChange);
246 | return;
247 | } else {
248 | index -= 1;
249 | codeSplits[index] = '';
250 | const newCode = codeSplits.join('');
251 | handleSetCode(index, newCode, onChange);
252 | return;
253 | }
254 | }
255 | if (curItemIndex === code.length) {
256 | index -= 1;
257 | codeSplits[index] = '';
258 | const newCode = codeSplits.join('');
259 | handleSetCode(index, newCode, onChange);
260 | return;
261 | }
262 | codeSplits[index] = '';
263 | const newCode = codeSplits.join('');
264 | handleSetCode(Math.max(index - 1, 0), newCode, onChange);
265 | return;
266 | }
267 | if (pressedKey === DELETE) {
268 | // BACKSPACE case: set current code item empty and setCurItemIndex
269 | let index = curItemIndex;
270 | if (typeof codeSplits[curItemIndex] === 'undefined') {
271 | index += 1;
272 | }
273 | index = Math.min(index, code.length);
274 | codeSplits[index] = '';
275 | const newCode = codeSplits.join('');
276 | handleSetCode(index, newCode, onChange);
277 | return;
278 | }
279 | if (ALPHABETNUMERICS.includes(pressedKey)) {
280 | let v = '';
281 | switch (type) {
282 | case DEFAULT_TYPES.ALPHANUMERTIC:
283 | v = getAlphanumeric(pressedKey);
284 | break;
285 | case DEFAULT_TYPES.ALPHA:
286 | v = getAlpha(pressedKey);
287 | break;
288 | case DEFAULT_TYPES.NUMBER:
289 | v = getNumeric(pressedKey);
290 | break;
291 | default:
292 | v = getAlphanumeric(pressedKey);
293 | break;
294 | }
295 | v = getCased(v, letterCase);
296 | if (code === '') {
297 | // initial case: just setCode(v)
298 | const newCode = v;
299 | handleSetCode(newCode.length, newCode, onChange);
300 | return;
301 | }
302 | if (curItemIndex === code.length) {
303 | // typing case: appending. setCode with the value just typed and setCurItemIndex with newCode.length
304 | if (codeSplits.length < DEFAULT_CODES.length) {
305 | codeSplits.push(v);
306 | const newCode = codeSplits.join('');
307 | let index = newCode.length;
308 | if (newCode.length >= DEFAULT_CODES.length) {
309 | index -= 1;
310 | }
311 | handleSetCode(index, newCode, onChange);
312 | return;
313 | }
314 | } else {
315 | if (isMobileDevice) {
316 | // mobile devices don't have arrow key
317 | // typing case: appending. setCode with the value just typed and setCurItemIndex with newCode.length
318 | if (codeSplits.length < DEFAULT_CODES.length) {
319 | codeSplits.push(v);
320 | const newCode = codeSplits.join('');
321 | let index = newCode.length;
322 | if (newCode.length >= DEFAULT_CODES.length) {
323 | index -= 1;
324 | }
325 | handleSetCode(index, newCode, onChange);
326 | return;
327 | }
328 | } else {
329 | // typing case: replacing. setCode with the value just typed and do not setCurItemIndex
330 | codeSplits[curItemIndex] = v;
331 | const newCode = codeSplits.join('');
332 | handleSetCode(typeof codeSplits[curItemIndex + 1] === 'undefined' ? curItemIndex + 1 : null, newCode, onChange);
333 | return;
334 | }
335 | }
336 | }
337 | },
338 | [type, letterCase, DEFAULT_CODES, code, curItemIndex],
339 | );
340 | const handleSetCode = useCallback((selectionIndex, code, onChange) => {
341 | setCode(code);
342 | if (selectionIndex !== null) {
343 | setCurItemIndex(selectionIndex);
344 | }
345 | $inputRef.current.value = code;
346 | if (typeof onChange === 'function') {
347 | onChange(code);
348 | }
349 | }, []);
350 | const handleOnCodeFocus = useCallback(() => {
351 | setIsFocus(true);
352 | }, []);
353 | const handleOnCodeBlur = useCallback(() => {
354 | setIsFocus(false);
355 | }, []);
356 | const attributes = useMemo(() => {
357 | const res: AttibutesObj = {};
358 | switch (type) {
359 | case DEFAULT_TYPES.NUMBER:
360 | res['type'] = isMobileDevice ? 'tel' : HIDDEN_INPUT_TYPE;
361 | res['pattern'] = `[0-9]{${DEFAULT_CODES.length},}`;
362 | break;
363 | case DEFAULT_TYPES.ALPHA:
364 | res['type'] = HIDDEN_INPUT_TYPE;
365 | res['pattern'] = `[A-Za-z]{${DEFAULT_CODES.length},}`;
366 | break;
367 | case DEFAULT_TYPES.ALPHANUMERTIC:
368 | res['type'] = HIDDEN_INPUT_TYPE;
369 | res['pattern'] = `[0-9A-Za-z]{${DEFAULT_CODES.length},}`;
370 | break;
371 | }
372 | return res;
373 | }, [type]);
374 | return (
375 |
380 |
381 | {DEFAULT_CODES.map((i, k) => {
382 | const isLastItem = k === DEFAULT_CODES.length - 1 ? true : false;
383 | const isEntered = typeof code[k] === 'undefined' ? false : true;
384 | let isActive = false;
385 | const focusStyle: React.CSSProperties = {};
386 | if (isFocus) {
387 | isActive = curItemIndex === k;
388 | }
389 | if (isActive) {
390 | focusStyle['border'] = `1px solid ${focusColor}`;
391 | focusStyle['boxShadow'] = `0px 0px 5px 0px ${focusColor}`;
392 | }
393 | return (
394 |
429 | );
430 | })}
431 |
432 |
{
442 | let key = e.key;
443 | if (letterCase === CASE_TYPES.AUTO) {
444 | if (ALPHABETNUMERICS.includes(key)) {
445 | setPressKey({ key: isMobileDevice ? key : autoCase(e.getModifierState('CapsLock'), e.shiftKey, key) });
446 | return;
447 | }
448 | }
449 | key = key.toLowerCase();
450 | if (isAndroidDevice && isChromeDevice) {
451 | if (key === BACKSPACE) {
452 | setPressKey({ key });
453 | }
454 | return false;
455 | }
456 | if (key)
457 | if (key !== ENTER && key !== TAB) {
458 | e.preventDefault();
459 | }
460 | if (key === TAB) {
461 | if (!(code.length < 0 || code.length > DEFAULT_CODES.length)) {
462 | if (e.shiftKey) {
463 | if (curItemIndex !== 0) {
464 | key = ARROW_LEFT;
465 | e.preventDefault();
466 | }
467 | } else {
468 | if (!(curItemIndex === DEFAULT_CODES.length - 1 && code.length === DEFAULT_CODES.length)) {
469 | // right bound
470 | key = ARROW_RIGHT;
471 | e.preventDefault();
472 | }
473 | }
474 | }
475 | } else {
476 | if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) {
477 | return;
478 | }
479 | }
480 | const validKey = isValidKey(key, type, code);
481 | if (validKey === INVALID_KEY) {
482 | return;
483 | }
484 | setPressKey({ key: validKey });
485 | }}
486 | style={{
487 | position: 'absolute',
488 | opacity: '0',
489 | marginLeft: '-9999999px',
490 | }}
491 | {...attributes}
492 | />
493 |
494 | );
495 | };
496 |
497 | export default ReactCodesInput;
498 |
--------------------------------------------------------------------------------
/src/js/Input/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css' {
2 | interface IClassNames {
3 | [className: string]: string
4 | }
5 | const classNames: IClassNames;
6 | export = classNames;
7 | }
--------------------------------------------------------------------------------
/src/js/Input/index.global.ts:
--------------------------------------------------------------------------------
1 | import ReactCodesInput from './ReactCodesInput';
2 | if (typeof window !== 'undefined') {
3 | (window).ReactCodesInput = ReactCodesInput;
4 | }
5 |
6 | export default ReactCodesInput;
7 |
--------------------------------------------------------------------------------
/src/js/Input/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ReactCodesInput';
2 | export { default } from './ReactCodesInput';
3 |
--------------------------------------------------------------------------------
/src/js/Input/react-codes-input.css:
--------------------------------------------------------------------------------
1 | .component {
2 | width: 100%;
3 | height: 50px;
4 | }
5 |
6 | .component.disabled .code {
7 | cursor: not-allowed;
8 | background-color: #f5f5f5;
9 | }
10 |
11 | .wrapper {
12 | display: flex;
13 | align-items: center;
14 | width: 100%;
15 | border: 1px solid #e5e5e5;
16 | height: 100%;
17 | display: table\0/IE9;
18 | }
19 |
20 | .code-wrapper {
21 | position: relative;
22 | padding: 0;
23 | outline: none;
24 | color: #000;
25 | white-space: nowrap;
26 | overflow-x: hidden;
27 | overflow-y: hidden;
28 | word-break: normal;
29 | line-height: 50px;
30 | flex: 1 1 0px;
31 | height: 100%;
32 | -ms-overflow-style: -ms-autohiding-scrollbar;
33 | display: table-cell\0/IE9;
34 | }
35 |
36 | .code-wrapper .entered-value {
37 | position: absolute;
38 | text-align: center;
39 | display: block;
40 | top: 50%;
41 | left: 50%;
42 | transform: translate(-50%, -50%);
43 | }
44 |
45 | .code-wrapper .entered-value.hide {
46 | width: 10px;
47 | height: 10px;
48 | background-color: #000;
49 | border-radius: 50%;
50 | }
51 |
52 | .code-wrapper.active .code-wrapper--focus {
53 | position: absolute;
54 | top: 1px;
55 | left: 1px;
56 | right: 1px;
57 | bottom: 1px;
58 | }
59 |
60 | .code {
61 | cursor: pointer;
62 | height: 100%;
63 | border-left: 1px solid #e5e5e5;
64 | }
65 |
66 | .code-wrapper:first-child .code {
67 | border-left: 1px solid transparent;
68 | }
69 |
70 | .code-wrapper::-webkit-scrollbar {
71 | display: none;
72 | }
73 |
74 | .code-wrapper.active:not(.entered):after {
75 | position: absolute;
76 | content: '';
77 | display: block;
78 | top: 50%;
79 | left: 50%;
80 | transform: translate(-50%, -50%);
81 | height: 20px;
82 | width: 1px;
83 | animation: blink-empty 1s infinite;
84 | background-color: transparent;
85 | }
86 |
87 | .component.disabled .code-wrapper.active:not(.entered):after {
88 | display: none;
89 | }
90 |
91 | @keyframes blink-empty {
92 | 50% {
93 | background-color: #333;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/js/Input/utils.ts:
--------------------------------------------------------------------------------
1 | export enum CASE_TYPES {
2 | UPPERCASE = 'upper',
3 | LOWERCASE = 'lower',
4 | AUTO = 'auto',
5 | }
6 |
7 | export const cx = (...params: Array) => {
8 | const classes = [];
9 | for (let i = 0; i < params.length; i += 1) {
10 | const arg = params[i];
11 | if (!arg) continue;
12 | const argType = typeof arg;
13 | if (argType === 'string' || argType === 'number') {
14 | classes.push(arg);
15 | } else if (Array.isArray(arg) && arg.length) {
16 | const inner: string = cx.apply(null, arg);
17 | if (inner) {
18 | classes.push(inner);
19 | }
20 | } else if (argType === 'object') {
21 | for (const key in arg) {
22 | if ({}.hasOwnProperty.call(arg, key) && arg[key]) {
23 | classes.push(key);
24 | }
25 | }
26 | }
27 | }
28 | return classes.join(' ');
29 | };
30 |
31 | export const getRandomId = () => {
32 | return Math.random().toString(36).slice(-8);
33 | };
34 |
35 | export const getAlphanumeric = (v: string) => {
36 | let res = '';
37 | String(v)
38 | .split('')
39 | .forEach(i => {
40 | const charCode = i.toLowerCase().charCodeAt(0);
41 | if ((charCode >= 48 && charCode <= 57) || (charCode >= 97 && charCode <= 122)) {
42 | res += i;
43 | }
44 | });
45 | return res;
46 | };
47 |
48 | export const getAlpha = (v: string) => {
49 | let res = '';
50 | String(v)
51 | .split('')
52 | .forEach(i => {
53 | const charCode = i.toLowerCase().charCodeAt(0);
54 | if (charCode >= 97 && charCode <= 122) {
55 | res += i;
56 | }
57 | });
58 | return res;
59 | };
60 |
61 | export const getNumeric = (v: string) => {
62 | let res = '';
63 | v.split('').forEach(i => {
64 | const charCode = i.toLowerCase().charCodeAt(0);
65 | if (charCode >= 48 && charCode <= 57) {
66 | res += i;
67 | }
68 | });
69 | return res;
70 | };
71 |
72 | export const getCased = (v: string, type: string) => {
73 | if (type) {
74 | switch (type) {
75 | case CASE_TYPES.UPPERCASE:
76 | return v.toUpperCase();
77 | case CASE_TYPES.LOWERCASE:
78 | return v.toLowerCase();
79 | case CASE_TYPES.AUTO:
80 | return v;
81 | }
82 | } else {
83 | return v.toUpperCase();
84 | }
85 | };
86 |
87 | export const getClassName = (className: string) => `RCI-${className}`;
88 |
89 | export const isAndroid = () => {
90 | if (typeof navigator === 'undefined' || typeof navigator.onLine === 'undefined') {
91 | return false;
92 | }
93 | return /(android)/i.test(navigator.userAgent);
94 | };
95 | export const isChrome = () => {
96 | if (typeof navigator === 'undefined' || typeof navigator.onLine === 'undefined') {
97 | return false;
98 | }
99 | return /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
100 | };
101 | export const isMobile = () => {
102 | if (typeof navigator === 'undefined' || typeof navigator.onLine === 'undefined') {
103 | return false;
104 | } else {
105 | return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
106 | }
107 | };
--------------------------------------------------------------------------------
/stylelint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: "stylelint-config-standard",
3 | rules: {
4 | 'block-no-empty': true,
5 | 'color-hex-case': 'lower',
6 | 'color-hex-length': null,
7 | 'color-no-invalid-hex': true,
8 | 'length-zero-no-unit': true,
9 | 'comment-empty-line-before': ['always', {
10 | 'except': ['first-nested'],
11 | 'ignore': ['stylelint-commands', 'between-comments'],
12 | }],
13 | 'declaration-colon-space-after': 'always',
14 | 'max-empty-lines': 2,
15 | 'unit-whitelist': ['em', 'rem', '%', 's', 'ms', 'px', 'deg', 'vw', 'vh', 'dpi', 'dppx'],
16 | 'selector-combinator-space-after': null,
17 | 'selector-pseudo-element-colon-notation': null,
18 | 'selector-list-comma-newline-after': null,
19 | 'comment-empty-line-before': null,
20 | 'block-closing-brace-newline-before': null,
21 | 'number-leading-zero': null,
22 | 'rule-empty-line-before': null,
23 | 'declaration-block-trailing-semicolon': null
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/tea.yaml:
--------------------------------------------------------------------------------
1 | # https://tea.xyz/what-is-this-file
2 | ---
3 | version: 1.0.0
4 | codeOwners:
5 | - '0xb60FCefB862640cbfF058e04CD64B4ed8EaFCA1F'
6 | quorum: 1
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./lib/",
4 | "sourceMap": true,
5 | "strictNullChecks": false,
6 | "declaration": true,
7 | "noImplicitAny": true,
8 | "module": "commonjs",
9 | "target": "es5",
10 | "downlevelIteration": true,
11 | "lib": ["es2016", "dom"],
12 | "jsx": "react",
13 | "skipLibCheck": true,
14 | "esModuleInterop": true,
15 | "strict": true,
16 | },
17 | "include": ["./src/**/*"]
18 | }
19 |
--------------------------------------------------------------------------------
/webpack/base.babel.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import path from 'path';
3 | import PATH from './build_path';
4 | import WebpackAssetsManifest from 'webpack-assets-manifest';
5 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
6 | export default {
7 | context: PATH.ROOT_PATH,
8 | entry: {
9 | index: PATH.ROOT_PATH + 'example/index.tsx',
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.mp3?$/,
15 | include: [PATH.ROOT_PATH],
16 | exclude: [PATH.NODE_MODULES_PATH],
17 | use: [{ loader: 'file-loader?name=audio/[name]-[hash].[ext]' }],
18 | },
19 | {
20 | test: /\.(png|svg|jpg|jpeg|gif)$/i,
21 | type: 'asset/resource',
22 | },
23 | {
24 | test: /\.(woff|woff2|eot|ttf|otf)$/i,
25 | type: 'asset/resource',
26 | },
27 | {
28 | test: /\.jsx?$/,
29 | include: [PATH.ROOT_PATH],
30 | exclude: [PATH.NODE_MODULES_PATH],
31 | enforce: 'post',
32 | loader: 'babel-loader',
33 | },
34 | {
35 | test: /\.(ts|tsx)$/,
36 | include: [PATH.ROOT_PATH],
37 | exclude: [PATH.NODE_MODULES_PATH],
38 | enforce: 'post',
39 | loader: 'ts-loader',
40 | },
41 | {
42 | test: /\.css$/,
43 | include: [PATH.NODE_MODULES_PATH],
44 | enforce: 'post',
45 | use: [
46 | MiniCssExtractPlugin.loader,
47 | {
48 | loader: 'css-loader',
49 | options: {},
50 | },
51 | {
52 | loader: 'postcss-loader',
53 | options: {
54 | postcssOptions: {
55 | plugins: [
56 | ['postcss-import', {}],
57 | ['postcss-preset-env', {}],
58 | ],
59 | },
60 | },
61 | },
62 | ],
63 | },
64 | {
65 | test: /\.css$/,
66 | include: [PATH.SOURCE_PATH],
67 | exclude: [PATH.NODE_MODULES_PATH],
68 | enforce: 'post',
69 | use: [
70 | MiniCssExtractPlugin.loader,
71 | {
72 | loader: 'css-loader',
73 | options: {
74 | modules: {
75 | localIdentName: '[name]__[local]',
76 | },
77 | },
78 | },
79 | {
80 | loader: 'postcss-loader',
81 | options: {
82 | postcssOptions: {
83 | plugins: [
84 | ['postcss-import', {}],
85 | ['postcss-preset-env', { stage: 0 }],
86 | ['cssnano', { safe: true }],
87 | ],
88 | },
89 | },
90 | },
91 | ],
92 | },
93 | ],
94 | },
95 | resolve: {
96 | modules: ['node_modules', path.resolve(__dirname, 'app')],
97 | extensions: ['.ts', '.tsx', '.js', '.json', '.jsx', '.css'],
98 | fallback: {
99 | path: false,
100 | },
101 | },
102 | devtool: 'source-map',
103 | devServer: {
104 | compress: true,
105 | host: '0.0.0.0',
106 | port: 9000,
107 | // https: {
108 | // cert: helper.ROOT_PATH + 'src/https/cert.pem', // path to cert,
109 | // key: helper.ROOT_PATH + 'src/https/key.pem', // path to key,
110 | // },
111 | historyApiFallback: true,
112 | client: { overlay: false },
113 | static: [
114 | {
115 | directory: path.join(__dirname, 'dist'),
116 | watch: true,
117 | },
118 | ],
119 | devMiddleware: {
120 | writeToDisk: filePath => {
121 | return /\.css$/.test(filePath);
122 | },
123 | },
124 | },
125 | plugins: [
126 | new webpack.ContextReplacementPlugin(/\.\/locale$/, 'empty-module', false, /js$/),
127 | new webpack.ProvidePlugin({
128 | React: 'React',
129 | react: 'React',
130 | 'window.react': 'React',
131 | 'window.React': 'React',
132 | }),
133 | new WebpackAssetsManifest({
134 | output: 'manifest-rev.json',
135 | }),
136 | ],
137 | target: ['web', 'es5'],
138 | };
139 |
--------------------------------------------------------------------------------
/webpack/build_path.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const ROOT_PATH = path.join(__dirname, '../');
3 | const ASSET_PATH = path.join(ROOT_PATH, 'dist');
4 | const NODE_MODULES_PATH = path.join(ROOT_PATH, './node_modules');
5 | const HTML_PATH = path.join(ROOT_PATH, './src/html');
6 | const SOURCE_PATH = path.join(ROOT_PATH, './src');
7 |
8 | module.exports = {
9 | ROOT_PATH: ROOT_PATH,
10 | ASSET_PATH: ASSET_PATH,
11 | NODE_MODULES_PATH: NODE_MODULES_PATH,
12 | HTML_PATH: HTML_PATH,
13 | SOURCE_PATH: SOURCE_PATH
14 | };
15 |
--------------------------------------------------------------------------------
/webpack/development.config.babel.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
3 | import HtmlWebpackPlugin from 'html-webpack-plugin';
4 | import ESLintPlugin from 'eslint-webpack-plugin';
5 | import base from './base.babel.js';
6 | const PATH = require('./build_path');
7 | const config = {
8 | ...base,
9 | mode: 'development',
10 | output: {
11 | publicPath: '/',
12 | filename: '[name].js',
13 | },
14 | };
15 | config.plugins.push(
16 | new ESLintPlugin({
17 | context: 'src',
18 | emitWarning: true,
19 | failOnError: false,
20 | exclude: ['data', 'locales'],
21 | extensions: ['airbnb-typescript'],
22 | }),
23 | );
24 | config.plugins.push(
25 | new MiniCssExtractPlugin({ filename: 'css/[name].css' }),
26 | new HtmlWebpackPlugin({
27 | template: PATH.HTML_PATH + '/layout.html',
28 | title: 'react-codes-input',
29 | page: 'index',
30 | filename: 'index.html',
31 | hash: false,
32 | chunksSortMode: function (chunk1, chunk2) {
33 | var orders = ['index'];
34 | var order1 = orders.indexOf(chunk1.names[0]);
35 | var order2 = orders.indexOf(chunk2.names[0]);
36 | if (order1 > order2) {
37 | return 1;
38 | } else if (order1 < order2) {
39 | return -1;
40 | } else {
41 | return 0;
42 | }
43 | },
44 | }),
45 | );
46 | module.exports = config;
47 |
--------------------------------------------------------------------------------
/webpack/production.config.babel.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
3 | import HtmlWebpackPlugin from 'html-webpack-plugin';
4 | import TerserPlugin from 'terser-webpack-plugin';
5 | import base from './base.babel.js';
6 | const PATH = require('./build_path');
7 | const config = {
8 | ...base,
9 | mode: 'production',
10 | devtool: 'cheap-source-map',
11 | output: {
12 | publicPath: '/react-codes-input/dist/',
13 | filename: '[name]-[chunkhash].js',
14 | },
15 | optimization: {
16 | minimizer: [
17 | new TerserPlugin({
18 | terserOptions: {
19 | ecma: undefined,
20 | warnings: false,
21 | parse: {},
22 | compress: {},
23 | mangle: true,
24 | module: false,
25 | output: null,
26 | toplevel: false,
27 | nameCache: null,
28 | ie8: false,
29 | keep_classnames: undefined,
30 | keep_fnames: false,
31 | safari10: false,
32 | },
33 | extractComments: true,
34 | }),
35 | ],
36 | splitChunks: {
37 | chunks: 'all',
38 | minSize: 30000,
39 | minChunks: 1,
40 | maxAsyncRequests: 5,
41 | maxInitialRequests: 3,
42 | name: 'asset',
43 | cacheGroups: {
44 | vendors: {
45 | name: 'b',
46 | test: /[\\/]node_modules[\\/]/,
47 | priority: -10,
48 | },
49 | default: {
50 | name: 'c',
51 | minChunks: 2,
52 | priority: -20,
53 | reuseExistingChunk: true,
54 | },
55 | },
56 | },
57 | },
58 | };
59 | config.plugins.push(
60 | new MiniCssExtractPlugin({ filename: 'css/[name]-[hash].css' }),
61 | new HtmlWebpackPlugin({
62 | template: PATH.HTML_PATH + '/layout.html',
63 | title: 'react-codes-input',
64 | page: 'index',
65 | filename: '../index.html',
66 | hash: false,
67 | chunksSortMode: function(chunk1, chunk2) {
68 | var orders = ['index'];
69 | var order1 = orders.indexOf(chunk1.names[0]);
70 | var order2 = orders.indexOf(chunk2.names[0]);
71 | if (order1 > order2) {
72 | return 1;
73 | } else if (order1 < order2) {
74 | return -1;
75 | } else {
76 | return 0;
77 | }
78 | },
79 | }),
80 | );
81 | module.exports = config;
82 |
--------------------------------------------------------------------------------
/webpack/umd.base.config.babel.js:
--------------------------------------------------------------------------------
1 | const env = require('yargs').argv.env; // use --env with webpack 2
2 | const path = require('path');
3 | const PATH = require('./build_path');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 |
6 | let libraryName = 'react-codes-input';
7 |
8 | let plugins = [],
9 | outputFile;
10 |
11 | if (env === 'minify') {
12 | plugins.push(new MiniCssExtractPlugin({ filename: libraryName + '.min.css' }));
13 | outputFile = libraryName + '.min.js';
14 | } else {
15 | plugins.push(new MiniCssExtractPlugin({ filename: libraryName + '.css' }));
16 | outputFile = libraryName + '.js';
17 | }
18 |
19 | module.exports = {
20 | mode: 'production',
21 | context: PATH.ROOT_PATH,
22 | module: {
23 | rules: [
24 | {
25 | test: /\.mp3?$/,
26 | include: [PATH.ROOT_PATH],
27 | exclude: [PATH.NODE_MODULES_PATH],
28 | loader: 'file-loader',
29 | options: {
30 | name: 'audio/[name]-[hash].[ext]',
31 | },
32 | },
33 | {
34 | test: /\.(woff|woff2|eot|ttf|otf)\??.*$/,
35 | include: [PATH.ROOT_PATH],
36 | // exclude: [PATH.NODE_MODULES_PATH],
37 | loader: 'url-loader',
38 | options: {
39 | limit: 1,
40 | name: 'font/[name]-[hash].[ext]',
41 | },
42 | },
43 | {
44 | test: /\.(jpe?g|png|gif|svg)\??.*$/,
45 | include: [PATH.ROOT_PATH],
46 | // exclude: [PATH.NODE_MODULES_PATH],
47 | loader: 'url-loader',
48 | options: {
49 | limit: 1,
50 | name: 'img/[name]-[hash].[ext]',
51 | },
52 | },
53 | {
54 | test: /\.jsx?$/,
55 | include: [PATH.ROOT_PATH],
56 | exclude: [PATH.NODE_MODULES_PATH],
57 | enforce: 'post',
58 | loader: 'babel-loader',
59 | },
60 | {
61 | test: /\.(ts|tsx)$/,
62 | include: [PATH.ROOT_PATH],
63 | exclude: [PATH.NODE_MODULES_PATH],
64 | enforce: 'post',
65 | loader: 'ts-loader',
66 | },
67 | {
68 | test: /\.css$/,
69 | enforce: 'post',
70 | use: [
71 | MiniCssExtractPlugin.loader,
72 | {
73 | loader: 'css-loader',
74 | options: {
75 | modules: {
76 | localIdentName: '[name]__[local]',
77 | },
78 | },
79 | },
80 | {
81 | loader: 'postcss-loader',
82 | options: {
83 | postcssOptions: {
84 | plugins: [
85 | ['postcss-import', {}],
86 | ['postcss-preset-env', { stage: 0 }],
87 | ['cssnano', { safe: true }],
88 | ],
89 | },
90 | },
91 | },
92 | ],
93 | },
94 | ],
95 | },
96 | resolve: {
97 | modules: ['node_modules', path.resolve(__dirname, 'app')],
98 | extensions: ['.ts', '.tsx', '.js', '.json', '.jsx', '.css'],
99 | },
100 | devtool: 'source-map',
101 | output: {
102 | path: PATH.ROOT_PATH + '/lib',
103 | filename: outputFile,
104 | library: libraryName,
105 | libraryTarget: 'umd',
106 | globalObject: 'this',
107 | },
108 | plugins,
109 | target: ['web', 'es5'],
110 | };
111 |
--------------------------------------------------------------------------------
/webpack/umd.global.config.babel.js:
--------------------------------------------------------------------------------
1 | const baseConfig = require('./umd.base.config.babel.js');
2 | const PATH = require('./build_path');
3 | module.exports = {
4 | ...baseConfig,
5 | entry: PATH.ROOT_PATH + 'src/js/Input/index.global.ts',
6 | output: {
7 | ...baseConfig.output,
8 | path: PATH.ROOT_PATH + '/lib',
9 | },
10 | externals: {
11 | react: 'React',
12 | 'react-dom': 'ReactDOM',
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/webpack/umd.local.config.babel.js:
--------------------------------------------------------------------------------
1 | const baseConfig = require('./umd.base.config.babel.js');
2 | const PATH = require('./build_path');
3 | module.exports = {
4 | ...baseConfig,
5 | entry: PATH.ROOT_PATH + 'src/js/Input/index.ts',
6 | devtool: false,
7 | output: {
8 | ...baseConfig.output,
9 | path: PATH.ROOT_PATH + '/lib/components',
10 | filename: 'index.js',
11 | },
12 | externals: {
13 | react: 'react',
14 | 'react-dom': 'react-dom',
15 | },
16 | };
17 |
--------------------------------------------------------------------------------