├── .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 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) 4 | 5 | [![npm version](https://badge.fury.io/js/react-codes-input.svg)](https://badge.fury.io/js/react-codes-input) ![Cdnjs](https://img.shields.io/cdnjs/v/react-codes-input) ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/react-codes-input.svg) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/edwardfxiao/react-codes-input/master/LICENSE) [![LICENSE](https://img.shields.io/badge/license-Anti%20996-blue.svg)](https://github.com/996icu/996.ICU/blob/master/LICENSE) [![996.icu](https://img.shields.io/badge/link-996.icu-red.svg)](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 | 137 | 138 | 139 | 140 | 141 | 142 |

Edward Xiao

💻 📖 🚇 ⚠️ 👀

bulhiCzar

💻 📖 🐛

Olena Mordas

🐛
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 |
17 |
18 |
19 |
{ 21 | alert('submit alphanumeric'); 22 | e.preventDefault(); 23 | }} 24 | > 25 |
26 |
27 |

Input type "alphanumeric"

28 |
29 |
30 | { 39 | console.log(res); 40 | setActivation(res); 41 | }} 42 | letterCase="upper" 43 | customStyleComponent={{ maxWidth: '300px', margin: '0 auto' }} 44 | /> 45 |
46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
{ 60 | alert('submit alpha'); 61 | e.preventDefault(); 62 | }} 63 | > 64 |
65 |
66 |

Input type "alpha" hide: true

67 |
68 |
69 | { 77 | console.log(res); 78 | setPassword(res); 79 | }} 80 | customStyleComponent={{ maxWidth: '300px', margin: '0 auto' }} 81 | /> 82 |
83 |
84 |
85 | 86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
{ 97 | // const input = document.getElementById('22') as HTMLInputElement | null; 98 | // if (input != null) { 99 | // // ⛔️ Error: Property 'value' does not exist on type 'HTMLElement'.ts(2339) 100 | // const value = input.value; 101 | // console.log(value); 102 | // } 103 | alert('submit number'); 104 | e.preventDefault(); 105 | }} 106 | > 107 |
108 |
109 |

Input type "number" only

110 |
111 |
112 | { 120 | console.log(res); 121 | setPin(res); 122 | }} 123 | customStyleComponent={{ maxWidth: '300px', margin: '0 auto' }} 124 | /> 125 |
126 |
127 |
128 | 129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
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 |
15 |

react-codes-input

16 | fork me on Github@edwardfxiao
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 |
{ 398 | let focusedIndex = -1; 399 | for (let index = 0; index < DEFAULT_CODES.length; index += 1) { 400 | if (typeof code[index] === 'undefined') { 401 | document.getElementById(`${inputId}${index}`).click(); 402 | focusedIndex = index; 403 | break; 404 | } 405 | } 406 | document.getElementById(inputId).focus(); 407 | }} 408 | className={cx( 409 | CSS['code-wrapper'], 410 | getClassName('code-wrapper'), 411 | classNameCodeWrapper, 412 | isActive && CSS['active'], 413 | isActive && getClassName('active'), 414 | isEntered && CSS['entered'], 415 | isEntered && getClassName('entered'), 416 | )} 417 | style={customStyleCodeWrapper} 418 | > 419 |
423 | {typeof code[k] === 'undefined' && {placeholder.split('')[k]}} {hide ? '' : code[k]} 424 |
425 |
426 |
427 |
428 |
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 | --------------------------------------------------------------------------------