├── postcss.config.js ├── src ├── css │ ├── variables │ │ └── index.js │ ├── transition.css │ ├── index.css │ ├── clock.css │ └── calendar.css ├── js │ └── component │ │ ├── index.ts │ │ ├── global.d.ts │ │ ├── index.global.ts │ │ ├── locale.ts │ │ ├── utils.ts │ │ ├── constValue.ts │ │ ├── ReactPickyDateTime.tsx │ │ ├── Calendar │ │ └── index.tsx │ │ └── Clock │ │ └── index.tsx └── html │ └── layout.html ├── .gitignore ├── react-picky-date-time.gif ├── tea.yaml ├── index.js ├── .npmignore ├── webpack ├── umd.global.config.babel.js ├── umd.local.config.babel.js ├── build_path.js ├── development.config.babel.js ├── production.config.babel.js ├── umd.base.config.babel.js └── base.babel.js ├── tsconfig.json ├── .babelrc ├── .all-contributorsrc ├── CHANGELOG.md ├── stylelint.config.js ├── LICENSE ├── .eslintrc.json ├── LICENSE_996 ├── package.json ├── CLASS-VERSION.md ├── README.md └── example └── index.js /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /src/css/variables/index.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /*.log 3 | /log/*.log 4 | coverage 5 | /notes 6 | /lib 7 | -------------------------------------------------------------------------------- /react-picky-date-time.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardfxiao/react-picky-date-time/HEAD/react-picky-date-time.gif -------------------------------------------------------------------------------- /src/js/component/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ReactPickyDateTime'; 2 | export { default } from './ReactPickyDateTime'; 3 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0xb60FCefB862640cbfF058e04CD64B4ed8EaFCA1F' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var PickyDateTime = require('./lib/components/index.js').default; 2 | require('./lib/react-picky-date-time.min.css'); 3 | module.exports = PickyDateTime; 4 | -------------------------------------------------------------------------------- /src/js/component/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/component/index.global.ts: -------------------------------------------------------------------------------- 1 | import ReactPickyDateTime from './ReactPickyDateTime'; 2 | if (typeof window !== 'undefined') { 3 | (window).ReactPickyDateTime = ReactPickyDateTime; 4 | } 5 | 6 | export default ReactPickyDateTime; 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 -------------------------------------------------------------------------------- /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/component/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/component/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], 3 | "env": { 4 | "development": { 5 | "comments": false 6 | }, 7 | "production": { 8 | "comments": false 9 | }, 10 | "lib": { 11 | "plugins": ["css-modules-transform", "@babel/proposal-class-properties", "@babel/proposal-object-rest-spread"], 12 | "comments": false 13 | }, 14 | "test": { 15 | "plugins": ["babel-plugin-rewire"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | "contributorsPerLine": 7, 23 | "projectName": "react-picky-date-time", 24 | "projectOwner": "edwardfxiao", 25 | "repoType": "github", 26 | "repoHost": "https://github.com", 27 | "skipCi": true 28 | } 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.9 2 | 3 | - Update package.json 4 | 5 | # 2.0.8 6 | 7 | - Update package.json 8 | 9 | # 2.0.7 10 | 11 | - Add tea.yaml 12 | 13 | # 2.0.6 14 | 15 | - Update README 16 | 17 | # 2.0.5 18 | 19 | - Potential Calendar bugfix 20 | 21 | # 2.0.4 22 | 23 | - Better TypeScript support (export interface ReactPickyDateTimeProps) 24 | 25 | # 2.0.3 26 | 27 | - Change animationInterval to 200 from 1000 28 | 29 | # 2.0.2 30 | 31 | - Support IE9+ 32 | 33 | # 2.0.1 34 | 35 | - Fix markedDates not showing correctly bug 36 | 37 | # 2.0.0 38 | 39 | - Rewite with React Hooks 40 | - Support TS 41 | - Using https://gist.github.com/jakearchibald/cb03f15670817001b1157e62a076fe95 method to do the timer 42 | - Not supporting IE anymore -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/html/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= htmlWebpackPlugin.options.title %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 18 |
19 |

react-picky-date-time

20 | fork me on Github@edwardfxiao 21 |
22 |
23 |
24 |
25 | <%= htmlWebpackPlugin.options.customJs %> 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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-picky-date-time', 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-picky-date-time/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-picky-date-time', 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 | -------------------------------------------------------------------------------- /src/css/transition.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --body-container-height-l: 420px; 3 | --body-container-height-m: 310px; 4 | --body-container-height-s: 240px; 5 | --body-container-height-xs: 200px; 6 | } 7 | 8 | .forwardEnter { 9 | will-change: transform; 10 | transition: opacity .5s ease-in, transform .3s; 11 | opacity: 1; 12 | transform: translate3d(100%, 0, 0); 13 | &.forwardEnterActive { 14 | transform: translate3d(0, 0, 0); 15 | } 16 | } 17 | 18 | .forwardLeave { 19 | opacity: 1; 20 | transition: opacity .5s ease-in; 21 | &.forwardLeaveActive { 22 | opacity: 0; 23 | } 24 | } 25 | 26 | .backwardEnter { 27 | &.backwardEnterActive {} 28 | } 29 | 30 | .backwardLeave { 31 | will-change: transform, opacity; 32 | transition: transform .3s ease-in; 33 | transform: translate3d(100%, 0, 0); 34 | &.backwardLeaveActive {} 35 | } 36 | 37 | .picky-date-time-calendar__title-container { 38 | position: relative; 39 | display: block; 40 | height: 18px; 41 | line-height: 18px; 42 | overflow: hidden; 43 | width: 100%; 44 | text-align: center; 45 | } 46 | 47 | .picky-date-time-calendar__selector-panel-year-set-container { 48 | position: relative; 49 | display: block; 50 | height: 24px; 51 | overflow: hidden; 52 | text-align: center; 53 | width: 100%; 54 | margin: 0 auto; 55 | } 56 | 57 | .picky-date-time-calendar__body-container { 58 | position: relative; 59 | display: block; 60 | transition: height .3s; 61 | overflow: hidden; 62 | text-align: center; 63 | &.l { 64 | height: var(--body-container-height-l); 65 | } 66 | &.m { 67 | height: var(--body-container-height-m); 68 | } 69 | &.s { 70 | height: var(--body-container-height-s); 71 | } 72 | &.xs { 73 | height: var(--body-container-height-xs); 74 | } 75 | } 76 | 77 | .slide { 78 | position: absolute; 79 | } 80 | 81 | .forward-enter { 82 | transform: translateX(100%); 83 | transition: 0.3s transform ease-in-out; 84 | } 85 | 86 | .forward-enter-active { 87 | transform: translateX(0); 88 | } 89 | 90 | .backward-enter { 91 | transform: translateX(-100%); 92 | transition: 0.3s transform ease-in-out; 93 | } 94 | 95 | .backward-enter-active { 96 | transform: translateX(0); 97 | } 98 | 99 | .forward-exit { 100 | transform: translateX(0); 101 | transition: 0.3s transform ease-in-out; 102 | } 103 | 104 | .forward-exit-active { 105 | transform: translateX(-100%); 106 | } 107 | 108 | .backward-exit { 109 | transform: translateX(0); 110 | transition: 0.3s transform ease-in-out; 111 | } 112 | 113 | .backward-exit-active { 114 | transform: translateX(100%); 115 | } 116 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/css/index.css: -------------------------------------------------------------------------------- 1 | @import 'open-color/open-color.css'; 2 | @import './transition.css'; 3 | @import './calendar.css'; 4 | @import './clock.css'; 5 | [class*=' picky-date-time'], 6 | [class^='picky-date-time'] { 7 | speak: none; 8 | font-style: normal; 9 | font-weight: 400; 10 | font-feature-settings: normal; 11 | font-variant: normal; 12 | text-transform: none; 13 | line-height: 1; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | :root { 18 | --oc-white: #ffffff; 19 | --breaker-padding-l: 0 5px; 20 | --breaker-padding-m: 0 5px; 21 | --breaker-padding-s: 0 5px; 22 | --breaker-padding-xs: 0 5px; 23 | } 24 | 25 | .picky-date-time { 26 | opacity: 0; 27 | position: relative; 28 | box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.10), 0 0 4px 0 rgba(0, 0, 0, 0.08); 29 | display: inline-block; 30 | transition: all 0.3s; 31 | transform: translateY(-1em) perspective(600px) rotateX(10deg); 32 | padding: 20px; 33 | } 34 | 35 | .picky-date-time.visible { 36 | opacity: 1; 37 | transform: translateY(0) perspective(600px) rotateX(0); 38 | } 39 | 40 | .picky-date-time__calendar, 41 | .picky-date-time__clock { 42 | display: inline-block; 43 | vertical-align: top; 44 | margin-bottom: 40px; 45 | position: relative; 46 | } 47 | 48 | .picky-date-time__calendar:before, 49 | .picky-date-time__clock:before { 50 | content: ''; 51 | display: inline-block; 52 | height: 100%; 53 | vertical-align: middle; 54 | } 55 | 56 | .picky-date-time__calendar { 57 | } 58 | 59 | .picky-date-time__clock { 60 | } 61 | 62 | .picky-date-time__close { 63 | cursor: pointer; 64 | position: absolute; 65 | top: 10px; 66 | right: 10px; 67 | color: var(--oc-gray-5); 68 | } 69 | 70 | .picky-date-time__breaker.l { 71 | padding: var(--breaker-padding-l); 72 | } 73 | 74 | .picky-date-time__breaker.m { 75 | padding: var(--breaker-padding-m); 76 | } 77 | 78 | .picky-date-time__breaker.s { 79 | padding: var(--breaker-padding-s); 80 | } 81 | 82 | .picky-date-time__breaker.xs { 83 | padding: var(--breaker-padding-xs); 84 | } 85 | 86 | @media only screen and (max-width: 550px) { 87 | .picky-date-time__calendar.xs, 88 | .picky-date-time__clock.xs { 89 | display: block; 90 | } 91 | .picky-date-time__breaker.xs { 92 | display: none; 93 | } 94 | } 95 | 96 | @media only screen and (max-width: 660px) { 97 | .picky-date-time__calendar.s, 98 | .picky-date-time__clock.s { 99 | display: block; 100 | } 101 | .picky-date-time__breaker.s { 102 | display: none; 103 | } 104 | } 105 | 106 | @media only screen and (max-width: 800px) { 107 | .picky-date-time__calendar.m, 108 | .picky-date-time__clock.m { 109 | display: block; 110 | } 111 | .picky-date-time__breaker.m { 112 | display: none; 113 | } 114 | } 115 | 116 | @media only screen and (max-width: 1024px) { 117 | .picky-date-time__calendar.l, 118 | .picky-date-time__clock.l { 119 | display: block; 120 | } 121 | .picky-date-time__breaker.l { 122 | display: none; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /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-picky-date-time'; 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: '[path][name]__[local]--[hash:base64:5]', 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 | -------------------------------------------------------------------------------- /src/js/component/locale.ts: -------------------------------------------------------------------------------- 1 | const REACT_PICKY_DATE_TIME_CUSTOM_LOCALE_EXAMPLE = 2 | "Invalid window.REACT_PICKY_DATE_TIME['customErrorMessage']. EXAMPLE: window.REACT_PICKY_DATE_TIME={customErrorMessage:{'en-US':{textbox:{empty:function empty(name){return getEnglishName(name)+'cannot be empty'},invalid:function invalid(name){return getEnglishName(name)+'invalid format'},invalidFormat:function invalidFormat(name){return getEnglishName(name)+'is not a number'},inBetween:function inBetween(name){return function(min){return function(max){return getEnglishName(name)+'must be '+min+'-'+max}}},lessThan:function lessThan(name){return function(min){return getEnglishName(name)+'cannot less than '+min}},greaterThan:function greaterThan(name){return function(max){return getEnglishName(name)+'cannot greater than '+max}},lengthEqual:function lengthEqual(name){return function(length){return getEnglishName(name)+'length must be '+length}},twoInputsNotEqual:function twoInputsNotEqual(){return'two inputs are not equal'}},radiobox:{empty:function empty(name){return'Please choose one '+getEnglishName(name)}},checkbox:{unchecked:function unchecked(name){return getEnglishName(name)+'must be checked'}},select:{empty:function empty(name){return'Please select a '+getEnglishName(name)}},textarea:{empty:function empty(name){return getEnglishName(name)+'cannot be empty'},invalid:function invalid(name){return getEnglishName(name)+'invalid format'},invalidFormat:function invalidFormat(name){return getEnglishName(name)+'is not a number'},inBetween:function inBetween(name){return function(min){return function(max){return getEnglishName(name)+'must be '+min+'-'+max}}},lessThan:function lessThan(name){return function(min){return getEnglishName(name)+'cannot less than '+min}},greaterThan:function greaterThan(name){return function(max){return getEnglishName(name)+'cannot greater than '+max}},lengthEqual:function lengthEqual(name){return function(length){return getEnglishName(name)+'length must be '+length}},twoInputsNotEqual:function twoInputsNotEqual(){return'two inputs are not equal'}}}}};"; 3 | 4 | interface IObjectKeys { 5 | [key: string]: any; 6 | } 7 | 8 | const DEFAULT_LACALE = 'en-us'; 9 | 10 | let locale: IObjectKeys = { 11 | 'en-us': { 12 | today: 'Today', 13 | reset: 'Reset', 14 | 'reset-date': 'Reset Date', 15 | clear: 'Clear', 16 | now: 'Now', 17 | weeks: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 18 | months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], 19 | }, 20 | 'zh-cn': { 21 | today: '今天', 22 | reset: '重置', 23 | 'reset-date': '重置日期', 24 | clear: '清零', 25 | now: '现在', 26 | weeks: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'], 27 | months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'], 28 | }, 29 | }; 30 | 31 | const getCustomLocale = (o: any, m: any) => { 32 | if (!o || typeof o !== 'object' || o.constructor !== Object || !Object.keys(o).length) { 33 | console.error(REACT_PICKY_DATE_TIME_CUSTOM_LOCALE_EXAMPLE); 34 | return false; 35 | } 36 | Object.keys(o).map(i => { 37 | if (!m[i]) { 38 | m[i] = o[i]; 39 | } else { 40 | if (Object.keys(o[i]).length) { 41 | Object.keys(o[i]).map(j => { 42 | m[i][j] = o[i][j]; 43 | }); 44 | } 45 | } 46 | }); 47 | return m; 48 | }; 49 | 50 | declare global { 51 | interface Window { 52 | REACT_PICKY_DATE_TIME: any; 53 | } 54 | } 55 | 56 | const handleCustomLocale = (locale: any, w: Window) => { 57 | let res; 58 | if (typeof w !== 'undefined') { 59 | if (w.REACT_PICKY_DATE_TIME && w.REACT_PICKY_DATE_TIME['customLocale']) { 60 | res = getCustomLocale(w.REACT_PICKY_DATE_TIME['customLocale'], locale); 61 | } 62 | } 63 | if (typeof res === 'undefined' || res === false) { 64 | return locale; 65 | } 66 | return res; 67 | }; 68 | 69 | if (typeof window !== 'undefined') { 70 | window.REACT_PICKY_DATE_TIME = window.REACT_PICKY_DATE_TIME || {}; 71 | locale = handleCustomLocale(locale, window); 72 | } 73 | 74 | const LOCALE = locale; 75 | 76 | export { LOCALE, DEFAULT_LACALE }; 77 | -------------------------------------------------------------------------------- /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.js', 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: '[path][name]__[local]--[hash:base64:5]', 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-picky-date-time", 3 | "version": "2.0.9", 4 | "description": "A react component for date time picker.", 5 | "main": "index.js", 6 | "types": "./lib/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/edwardfxiao/react-picky-date-time.git" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "picky", 14 | "date", 15 | "time", 16 | "picker", 17 | "datepicker", 18 | "date-picker", 19 | "timepicker", 20 | "time-picker", 21 | "calendar", 22 | "react-picky-date-time" 23 | ], 24 | "author": "Edward Xiao", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/edwardfxiao/react-picky-date-time/issues" 28 | }, 29 | "homepage": "https://github.com/edwardfxiao/react-picky-date-time", 30 | "scripts": { 31 | "tslint": "tslint -c tslint.json 'src/**/*.{ts,tsx}'", 32 | "build_gh_page": "rm -rf lib && rm -rf dist && NODE_ENV=production ./node_modules/.bin/webpack --config ./webpack/production.config.babel.js --progress", 33 | "umd_local": "./node_modules/.bin/webpack --config ./webpack/umd.local.config.babel.js", 34 | "umd_global": "./node_modules/.bin/webpack --config ./webpack/umd.global.config.babel.js", 35 | "umd_global_min": "./node_modules/.bin/webpack --config ./webpack/umd.global.config.babel.js --env minify", 36 | "dev": "node_modules/.bin/webpack-dev-server --config ./webpack/development.config.babel.js", 37 | "compile": "rimraf dist lib && npm run umd_global && npm run umd_global_min && npm run umd_local && rm ./lib/components/*.css" 38 | }, 39 | "peerDependencies": { 40 | "react": ">= 16.8.6", 41 | "react-dom": ">= 16.8.6", 42 | "react-transition-group": ">= 4.2.2" 43 | }, 44 | "devDependencies": { 45 | "@babel/cli": "^7.17.0", 46 | "@babel/core": "^7.17.2", 47 | "@babel/eslint-parser": "^7.17.0", 48 | "@babel/plugin-proposal-class-properties": "^7.16.7", 49 | "@babel/preset-env": "^7.16.11", 50 | "@babel/preset-react": "^7.16.7", 51 | "@babel/preset-typescript": "^7.16.7", 52 | "@babel/register": "^7.17.0", 53 | "@swc/core": "^1.2.139", 54 | "@swc/wasm": "^1.2.139", 55 | "@types/jest": "^27.4.0", 56 | "@types/react": "^16.8.14", 57 | "@types/react-dom": "^16.8.4", 58 | "@types/react-transition-group": "^4.4.4", 59 | "abortcontroller-polyfill": "^1.7.3", 60 | "babel-jest": "^27.5.1", 61 | "babel-loader": "^8.2.3", 62 | "babel-plugin-css-modules-transform": "^1.6.2", 63 | "bufferutil": "^4.0.6", 64 | "chai": "^4.2.0", 65 | "core-js": "^3.21.0", 66 | "coveralls": "^3.1.1", 67 | "css-loader": "^6.4.0", 68 | "cssnano": "^5.0.8", 69 | "enzyme": "^3.10.0", 70 | "enzyme-adapter-react-16": "^1.13.2", 71 | "esbuild": "^0.14.21", 72 | "eslint": "^8.0.1", 73 | "eslint-plugin-react": "^7.26.1", 74 | "eslint-plugin-react-hooks": "^4.2.0", 75 | "eslint-webpack-plugin": "^3.1.1", 76 | "file-loader": "^6.2.0", 77 | "html-webpack-plugin": "^5.4.0", 78 | "identity-obj-proxy": "^3.0.0", 79 | "inline-style-prefix-all": "^2.0.2", 80 | "jest": "^27.4.0", 81 | "mini-css-extract-plugin": "^2.4.3", 82 | "node-notifier": "^10.0.1", 83 | "open-color": "^1.9.1", 84 | "postcss-css-variables": "^0.17.0", 85 | "postcss-custom-properties": "^9.1.1", 86 | "postcss-import": "^14.0.2", 87 | "postcss-loader": "^6.2.0", 88 | "postcss-preset-env": "^7.3.1", 89 | "postcss-simple-vars": "^6.0.3", 90 | "prismjs": "^1.16.0", 91 | "promise-polyfill": "^8.2.1", 92 | "react": "^16.8.6", 93 | "react-dom": "^16.8.6", 94 | "react-markdown": "^8.0.0", 95 | "react-transition-group": "^4.4.2", 96 | "regenerator-runtime": "^0.13.9", 97 | "rimraf": "^3.0.2", 98 | "ts-jest": "^27.1.3", 99 | "ts-loader": "^9.2.6", 100 | "ts-node": "^10.5.0", 101 | "typescript": "^4.5.5", 102 | "url-loader": "^4.1.1", 103 | "utf-8-validate": "^5.0.8", 104 | "webpack": "^5.68.0", 105 | "webpack-assets-manifest": "^5.1.0", 106 | "webpack-cli": "^4.9.2", 107 | "webpack-dev-server": "^4.7.4" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /CLASS-VERSION.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | ```js 3 | import ReactPickyDateTime from 'react-picky-date-time'; 4 | 5 | ... 6 | class YourOwnComponent extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | showPickyDateTime: true, 11 | date: '30', 12 | month: '01', 13 | year: '2000', 14 | hour: '03', 15 | minute: '10', 16 | second: '40', 17 | meridiem: 'PM' 18 | }; 19 | } 20 | ... 21 | render() { 22 | const { 23 | showPickyDateTime, 24 | date, 25 | month, 26 | year, 27 | hour, 28 | minute, 29 | second, 30 | meridiem 31 | } = this.state; 32 | 33 | return( 34 | this.setState({ showPickyDateTime: false })} 40 | defaultTime={`${hour}:${minute}:${second} ${meridiem}`} // OPTIONAL. format: "HH:MM:SS AM" 41 | defaultDate={`${month}/${date}/${year}`} // OPTIONAL. format: "MM/DD/YYYY" 42 | onYearPicked={res => this.onYearPicked(res)} 43 | onMonthPicked={res => this.onMonthPicked(res)} 44 | onDatePicked={res => this.onDatePicked(res)} 45 | onResetDate={res => this.onResetDate(res)} 46 | onResetDefaultDate={res => this.onResetDefaultDate(res)} 47 | onSecondChange={res => this.onSecondChange(res)} 48 | onMinuteChange={res => this.onMinuteChange(res)} 49 | onHourChange={res => this.onHourChange(res)} 50 | onMeridiemChange={res => this.onMeridiemChange(res)} 51 | onResetTime={res => this.onResetTime(res)} 52 | onResetDefaultTime={res => this.onResetDefaultTime(res)} 53 | onClearTime={res => this.onClearTime(res)} 54 | // markedDates={['10/19/2021']} // OPTIONAL. format: "MM/DD/YYYY" 55 | // supportDateRange={['12/03/2021', '12/05/2021']} // OPTIONAL. min date and max date. format: "MM/DD/YYYY" 56 | /> 57 | ); 58 | } 59 | } 60 | ``` 61 | 62 | 63 | # Events 64 | 65 | Also consoled out on the demo page examples 66 | 67 | ```js 68 | onYearPicked(res) { 69 | const { year } = res; 70 | this.setState({ year: year}); 71 | } 72 | 73 | onMonthPicked(res) { 74 | const { month, year } = res; 75 | this.setState({ year: year, month: month}); 76 | } 77 | 78 | onDatePicked(res) { 79 | const { date, month, year } = res; 80 | this.setState({ year: year, month: month, date: date }); 81 | } 82 | 83 | onResetDate(res) { 84 | const { date, month, year } = res; 85 | this.setState({ year: year, month: month, date: date }); 86 | } 87 | 88 | onResetDefaultDate(res) { 89 | const { date, month, year } = res; 90 | this.setState({ year: year, month: month, date: date }); 91 | } 92 | 93 | onSecondChange(res) { 94 | this.setState({ second: res.value }); 95 | } 96 | 97 | onMinuteChange(res) { 98 | this.setState({ minute: res.value }); 99 | } 100 | 101 | onHourChange(res) { 102 | this.setState({ hour: res.value }); 103 | } 104 | 105 | onMeridiemChange(res) { 106 | this.setState({ meridiem: res }); 107 | } 108 | 109 | onResetTime(res) { 110 | this.setState({ 111 | second: res.clockHandSecond.value, 112 | minute: res.clockHandMinute.value, 113 | hour: res.clockHandHour.value 114 | }); 115 | } 116 | 117 | onResetDefaultTime(res) { 118 | this.setState({ 119 | second: res.clockHandSecond.value, 120 | minute: res.clockHandMinute.value, 121 | hour: res.clockHandHour.value 122 | }); 123 | } 124 | 125 | onClearTime(res) { 126 | this.setState({ 127 | second: res.clockHandSecond.value, 128 | minute: res.clockHandMinute.value, 129 | hour: res.clockHandHour.value 130 | }); 131 | } 132 | 133 | // just toggle your outter component state to true or false to show or hide 134 | openPickyDateTime() { 135 | this.setState({showPickyDateTime: true}); 136 | } 137 | 138 | onClose() { 139 | this.setState({showPickyDateTime: false}); 140 | } 141 | 142 | ``` -------------------------------------------------------------------------------- /src/js/component/utils.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | export const cx = (...params: Array) => { 3 | const classes = []; 4 | for (let i = 0; i < params.length; i += 1) { 5 | const arg = params[i]; 6 | if (!arg) continue; 7 | const argType = typeof arg; 8 | if (argType === 'string' || argType === 'number') { 9 | classes.push(arg); 10 | } else if (Array.isArray(arg) && arg.length) { 11 | const inner: string = cx.apply(null, arg); 12 | if (inner) { 13 | classes.push(inner); 14 | } 15 | } else if (argType === 'object') { 16 | for (const key in arg) { 17 | if ({}.hasOwnProperty.call(arg, key) && arg[key]) { 18 | classes.push(key); 19 | } 20 | } 21 | } 22 | } 23 | return classes.join(' '); 24 | }; 25 | 26 | export const isValidDate = (str: string) => { 27 | try { 28 | const d = new Date(str); 29 | if (!isNaN(d.getTime())) { 30 | return true; 31 | } 32 | return false; 33 | } catch (e) { 34 | return false; 35 | } 36 | }; 37 | 38 | export const isValidDates = (arr: Array) => { 39 | let isValid = false; 40 | if (arr.length) { 41 | isValid = true; 42 | arr.forEach(v => { 43 | if (!isValidDate(v)) { 44 | isValid = false; 45 | } 46 | }); 47 | } 48 | return isValid; 49 | }; 50 | 51 | export const useWillUnmount = (f: Function) => useEffect(() => () => f && f(), []); 52 | export const usePrevious = (value: any) => { 53 | const ref = useRef(); 54 | useEffect(() => { 55 | ref.current = value; 56 | }); 57 | return ref.current; 58 | }; 59 | 60 | export const formatClockNumber = (value: number) => { 61 | if (value < 10 && value >= 0) { 62 | return '0' + String(value); 63 | } 64 | return String(value); 65 | }; 66 | 67 | export const isValidTime = (value: string) => { 68 | // Checks if time is in HH:MM:SS AM/PM format. 69 | // The seconds and AM/PM are optional. 70 | if (value == '') { 71 | return {}; 72 | } 73 | const timePat = /^(\d{1,2}):(\d{2})(:(\d{2}))?(\s?(AM|am|PM|pm))?$/; 74 | 75 | const matchArray = value.match(timePat); 76 | if (matchArray == null) { 77 | console.error('Time is not in a valid format.'); 78 | return {}; 79 | } 80 | let hour = matchArray[1]; 81 | let minute = matchArray[2]; 82 | let second = matchArray[4]; 83 | let meridiem = matchArray[6]; 84 | 85 | const numberHour = Number(hour); 86 | const numberMinute = Number(minute); 87 | const numberSecond = Number(second); 88 | if (second == '') { 89 | second = null; 90 | } 91 | if (meridiem == '') { 92 | meridiem = null; 93 | } 94 | 95 | if (numberHour < 0 || numberHour > 23) { 96 | console.error('Hour must be between 1 and 12.'); 97 | return {}; 98 | } 99 | if (numberHour <= 12 && meridiem == null) { 100 | console.error('You must specify AM or PM.'); 101 | return {}; 102 | } 103 | if (numberHour > 12 && meridiem != null) { 104 | console.error("You can't specify AM or PM for military time."); 105 | return {}; 106 | } 107 | if (numberMinute < 0 || numberMinute > 59) { 108 | console.error('Minute must be between 0 and 59.'); 109 | return {}; 110 | } 111 | if (second != null && (numberSecond < 0 || numberSecond > 59)) { 112 | console.error('Second must be between 0 and 59.'); 113 | return {}; 114 | } 115 | second = formatClockNumber(Number(second)); 116 | minute = formatClockNumber(Number(minute)); 117 | const hourText = formatClockNumber(Number(hour)); 118 | return { 119 | hour, 120 | minute, 121 | second, 122 | meridiem, 123 | hourText, 124 | }; 125 | }; 126 | // Using https://gist.github.com/jakearchibald/cb03f15670817001b1157e62a076fe95 method 127 | export const animationInterval = (ms: number, signal: any, callback: Function) => { 128 | // Prefer currentTime, as it'll better sync animtions queued in the 129 | // same frame, but if it isn't supported, performance.now() is fine. 130 | const start = document.timeline ? document.timeline.currentTime : performance.now(); 131 | 132 | function frame(time: number) { 133 | if (signal.aborted) return; 134 | callback(time); 135 | scheduleFrame(time); 136 | } 137 | 138 | function scheduleFrame(time: number) { 139 | const elapsed = time - start; 140 | const roundedElapsed = Math.round(elapsed / ms) * ms; 141 | const targetNext = start + roundedElapsed + ms; 142 | const delay = targetNext - performance.now(); 143 | setTimeout(() => requestAnimationFrame(frame), delay); 144 | } 145 | 146 | scheduleFrame(start); 147 | }; 148 | -------------------------------------------------------------------------------- /src/js/component/constValue.ts: -------------------------------------------------------------------------------- 1 | // GENERAL 2 | const SIZE_RANGE = ['l', 'm', 's', 'xs']; 3 | const DEFAULT_SIZE = 'm'; 4 | // CALENDAR 5 | 6 | const PREV_TRANSITION = 'prev'; 7 | const NEXT_TRANSITION = 'next'; 8 | 9 | const SELECTOR_YEAR_SET_NUMBER = 5; 10 | 11 | const POINTER_ROTATE = 0; 12 | 13 | const WEEK_NUMBER = 7; 14 | 15 | const getDaysArray = (year: number, month: number) => { 16 | let prevMonth; 17 | let nextMonth; 18 | let prevYear; 19 | let nextYear; 20 | if (month == 12) { 21 | prevMonth = 11; 22 | nextMonth = 1; 23 | prevYear = year - 1; 24 | nextYear = year + 1; 25 | } else if (month == 1) { 26 | prevMonth = 12; 27 | nextMonth = 2; 28 | prevYear = year - 1; 29 | nextYear = year + 1; 30 | } else { 31 | prevMonth = month - 1; 32 | nextMonth = month + 1; 33 | prevYear = year; 34 | nextYear = year; 35 | } 36 | const date = new Date(year, month - 1, 1); 37 | let prevMonthDate = null; 38 | let thisMonthDate = null; 39 | let nextMonthDate = null; 40 | let res = []; 41 | let startOffset = date.getDay(); 42 | if (startOffset != 0) { 43 | prevMonthDate = getDaysListByMonth(prevYear, prevMonth); 44 | for (let i = prevMonthDate.length - startOffset; i <= prevMonthDate.length - 1; i++) { 45 | res.push(prevMonthDate[i]); 46 | } 47 | } 48 | thisMonthDate = getDaysListByMonth(year, month); 49 | res = [...res, ...thisMonthDate]; 50 | let endOffset = WEEK_NUMBER - thisMonthDate[thisMonthDate.length - 1].day - 1; 51 | if (endOffset != 0) { 52 | nextMonthDate = getDaysListByMonth(nextYear, nextMonth); 53 | for (let i = 0; i <= endOffset - 1; i++) { 54 | res.push(nextMonthDate[i]); 55 | } 56 | } 57 | return res; 58 | }; 59 | 60 | const getDaysListByMonth = (year: number, month: number) => { 61 | const date = new Date(year, month - 1, 1); 62 | let res = []; 63 | const monthName = formatDateString(month); 64 | while (date.getMonth() == month - 1) { 65 | const dayName = formatDateString(date.getDate()); 66 | let item = { 67 | name: dayName, 68 | day: date.getDay(), 69 | month: monthName, 70 | year: year, 71 | value: `${year}-${monthName}-${dayName}`, 72 | }; 73 | res.push(item); 74 | date.setDate(date.getDate() + 1); 75 | } 76 | return res; 77 | }; 78 | 79 | const formatDateString = (value: number) => { 80 | if (Number(value) < 10) { 81 | return '0' + String(value); 82 | } 83 | return String(value); 84 | }; 85 | 86 | const getYearSet = (year: number) => { 87 | let res = []; 88 | let itemNumber; 89 | let startOffset; 90 | let endOffset; 91 | if (SELECTOR_YEAR_SET_NUMBER % 2 == 1) { 92 | itemNumber = (SELECTOR_YEAR_SET_NUMBER - 1) / 2 + 1; 93 | startOffset = SELECTOR_YEAR_SET_NUMBER - itemNumber; 94 | } else { 95 | itemNumber = SELECTOR_YEAR_SET_NUMBER / 2 - 1; 96 | startOffset = itemNumber - 1; 97 | } 98 | 99 | endOffset = SELECTOR_YEAR_SET_NUMBER - itemNumber; 100 | 101 | for (let i = year - startOffset; i <= year - 1; i++) { 102 | res.push(i); 103 | } 104 | res.push(year); 105 | for (let i = 0; i <= endOffset - 1; i++) { 106 | year = year + 1; 107 | res.push(year); 108 | } 109 | return res; 110 | }; 111 | 112 | // CLOCK 113 | 114 | const R2D = 180 / Math.PI; 115 | 116 | const SECOND_DEGREE_NUMBER = 6; 117 | const MINUTE_DEGREE_NUMBER = 6; 118 | const HOUR_DEGREE_NUMBER = 30; 119 | 120 | const QUARTER = [0, 15, 30, 45]; 121 | 122 | const TIME_SELECTION_FIRST_CHAR_POS_LIST = [0, 3, 6]; 123 | const TIME_SELECTION_FIRST_CHAR_POS_BACKSPACE_LIST = [1, 4, 7]; 124 | const TIME_SELECTION_SECOND_CHAR_POS_LIST = [1, 4, 7]; 125 | const TIME_SELECTION_SECOND_CHAR_POS_BACKSPACE_LIST = [2, 5, 8]; 126 | const TIME_JUMP_CHAR_POS_LIST = [1, 4, 7]; 127 | const TIME_CURSOR_POSITION_OBJECT: { [k: number]: string } = { 128 | 0: 'clockHandHour', 129 | 1: 'clockHandHour', 130 | 2: 'clockHandHour', 131 | 3: 'clockHandMinute', 132 | 4: 'clockHandMinute', 133 | 5: 'clockHandMinute', 134 | 6: 'clockHandSecond', 135 | 7: 'clockHandSecond', 136 | 8: 'clockHandSecond', 137 | 9: 'meridiem', 138 | 10: 'meridiem', 139 | 11: 'meridiem', 140 | }; 141 | const TIME_TYPE = ['clockHandHour', 'clockHandMinute', 'clockHandSecond', 'meridiem']; 142 | 143 | export { 144 | // GENERAL 145 | SIZE_RANGE, 146 | DEFAULT_SIZE, 147 | // CALENDAR 148 | PREV_TRANSITION, 149 | NEXT_TRANSITION, 150 | SELECTOR_YEAR_SET_NUMBER, 151 | WEEK_NUMBER, 152 | POINTER_ROTATE, 153 | getDaysArray, 154 | getDaysListByMonth, 155 | getYearSet, 156 | formatDateString, 157 | // CLOCK 158 | R2D, 159 | SECOND_DEGREE_NUMBER, 160 | MINUTE_DEGREE_NUMBER, 161 | HOUR_DEGREE_NUMBER, 162 | QUARTER, 163 | TIME_SELECTION_FIRST_CHAR_POS_LIST, 164 | TIME_SELECTION_FIRST_CHAR_POS_BACKSPACE_LIST, 165 | TIME_SELECTION_SECOND_CHAR_POS_LIST, 166 | TIME_SELECTION_SECOND_CHAR_POS_BACKSPACE_LIST, 167 | TIME_JUMP_CHAR_POS_LIST, 168 | TIME_CURSOR_POSITION_OBJECT, 169 | TIME_TYPE, 170 | }; 171 | -------------------------------------------------------------------------------- /src/js/component/ReactPickyDateTime.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, memo, Fragment } from 'react'; 2 | import { cx } from './utils'; 3 | import Calendar from './Calendar/index'; 4 | import Clock from './Clock/index'; 5 | import '../../css/index.css'; 6 | import { SIZE_RANGE, DEFAULT_SIZE } from './constValue'; 7 | import { LOCALE, DEFAULT_LACALE } from './locale'; 8 | 9 | export interface ReactPickyDateTimeProps { 10 | mode: number; 11 | size: string; 12 | locale: string; 13 | markedDates?: Array; 14 | supportDateRange?: Array; 15 | defaultDate?: string; 16 | defaultTime?: string; 17 | show?: boolean; 18 | onClose?: () => void; 19 | onYearPicked?: (res: object) => void; 20 | onMonthPicked?: (res: object) => void; 21 | onDatePicked?: (res: object) => void; 22 | onResetDate?: (res: object) => void; 23 | onSecondChange?: (res: object) => void; 24 | onMinuteChange?: (res: object) => void; 25 | onHourChange?: (res: object) => void; 26 | onMeridiemChange?: (res: string) => void; 27 | onResetTime?: (res: object) => void; 28 | onClearTime?: (res: object) => void; 29 | onResetDefaultDate?: (res: object) => void; 30 | onResetDefaultTime?: (res: object) => void; 31 | } 32 | 33 | const ReactPickyDateTime: React.FC = memo( 34 | ({ 35 | locale = DEFAULT_LACALE, 36 | size = DEFAULT_SIZE, 37 | markedDates = [], 38 | supportDateRange = [], 39 | show = false, 40 | mode = 0, 41 | // GENERAL 42 | onClose = () => {}, 43 | // CALENDAR 44 | defaultDate = '', 45 | onYearPicked = () => {}, 46 | onMonthPicked = () => {}, 47 | onDatePicked = () => {}, 48 | onResetDate = () => {}, 49 | onResetDefaultDate = () => {}, 50 | // CLOCK 51 | defaultTime = '', 52 | onSecondChange = () => {}, 53 | onMinuteChange = () => {}, 54 | onHourChange = () => {}, 55 | onMeridiemChange = () => {}, 56 | onResetTime = () => {}, 57 | onClearTime = () => {}, 58 | onResetDefaultTime = () => {}, 59 | }) => { 60 | const handleOnClose = useCallback(() => { 61 | onClose && onClose(); 62 | }, []); 63 | const componentClass = cx('picky-date-time', show && 'visible'); 64 | 65 | size = size.toLowerCase(); 66 | if (SIZE_RANGE.indexOf(size) == -1) { 67 | size = DEFAULT_SIZE; 68 | } 69 | 70 | locale = locale.toLowerCase(); 71 | if (typeof LOCALE[locale] === 'undefined') { 72 | locale = DEFAULT_LACALE; 73 | } 74 | 75 | const contentHtml = ( 76 | 77 |
78 | {(mode === 0 || mode === 1) && ( 79 | 91 | )} 92 |
93 | {mode === 1 &&   } 94 | {(mode === 1 || mode === 2) && ( 95 |
96 | 108 |
109 | )} 110 |
111 | ); 112 | return ( 113 |
114 | 115 | 119 | 120 | {contentHtml} 121 |
122 | ); 123 | }, 124 | ); 125 | 126 | export default ReactPickyDateTime; 127 | -------------------------------------------------------------------------------- /src/css/clock.css: -------------------------------------------------------------------------------- 1 | @import 'open-color/open-color.css'; 2 | :root { 3 | --oc-white: #ffffff; 4 | --clock-container-width-l: 380px; 5 | --clock-container-width-m: 320px; 6 | --clock-container-width-s: 280px; 7 | --clock-container-width-xs: 220px; 8 | --clock-width-l: 320px; 9 | --clock-width-m: 260px; 10 | --clock-width-s: 200px; 11 | --clock-width-xs: 180px; 12 | --clock-height-l: 320px; 13 | --clock-height-m: 260px; 14 | --clock-height-s: 200px; 15 | --clock-height-xs: 180px; 16 | } 17 | 18 | .picky-date-time-clock__inline-div { 19 | display: inline-block; 20 | } 21 | 22 | .picky-date-time-clock__inline-div--middle { 23 | vertical-align: middle; 24 | } 25 | 26 | .picky-date-time-clock { 27 | margin: 0 auto; 28 | &.l { 29 | width: var(--clock-container-width-l); 30 | } 31 | &.m { 32 | width: var(--clock-container-width-m); 33 | } 34 | &.s { 35 | width: var(--clock-container-width-s); 36 | } 37 | &.xs { 38 | width: var(--clock-container-width-xs); 39 | } 40 | } 41 | 42 | .picky-date-time-clock__header { 43 | } 44 | 45 | .picky-date-time-clock__inputer-wrapper { 46 | margin-top: 30px; 47 | text-align: center; 48 | } 49 | 50 | .picky-date-time-clock__inputer { 51 | position: relative; 52 | display: inline-block; 53 | vertical-align: middle; 54 | margin-right: 10px; 55 | text-align: center; 56 | margin-top: 3px; 57 | border: 1px solid var(--oc-gray-5); 58 | padding: 5px 10px; 59 | width: 65%; 60 | } 61 | 62 | .picky-date-time-clock__input { 63 | height: 20px; 64 | line-height: 20px; 65 | width: 100%; 66 | border: none; 67 | outline: 0; 68 | background: transparent; 69 | touch-action: none; 70 | } 71 | 72 | .picky-date-time-clock__input::-ms-clear { 73 | width: 0; 74 | height: 0; 75 | } 76 | 77 | .picky-date-time-clock__icon--refresh, 78 | .picky-date-time-clock__icon--schedule, 79 | .picky-date-time-clock__icon--remove_circle_outline { 80 | cursor: pointer; 81 | color: var(--oc-gray-6); 82 | } 83 | 84 | .picky-date-time-clock__icon--schedule { 85 | } 86 | 87 | .picky-date-time-clock__icon--remove_circle_outline { 88 | position: absolute; 89 | right: 10px; 90 | top: 50%; 91 | transform: translateY(-50%); 92 | } 93 | 94 | .picky-date-time-clock__circle { 95 | margin: 0 auto; 96 | position: relative; 97 | border: 1px solid var(--oc-gray-5); 98 | border-radius: 50%; 99 | box-shadow: 2px 2px 8px 0 rgba(0, 0, 0, 0.2); 100 | margin-top: 18px; 101 | &.l { 102 | width: var(--clock-width-l); 103 | height: var(--clock-height-l); 104 | } 105 | &.m { 106 | width: var(--clock-width-m); 107 | height: var(--clock-height-m); 108 | } 109 | &.s { 110 | width: var(--clock-width-s); 111 | height: var(--clock-height-s); 112 | } 113 | &.xs { 114 | width: var(--clock-width-xs); 115 | height: var(--clock-height-xs); 116 | } 117 | } 118 | 119 | .picky-date-time-clock__clock-minute { 120 | position: absolute; 121 | left: 50%; 122 | top: 50%; 123 | height: 2px; 124 | background: var(--oc-gray-5); 125 | width: 1px; 126 | } 127 | 128 | .picky-date-time-clock__clock-minute--five { 129 | height: 3px; 130 | background: var(--oc-gray-7); 131 | width: 1px; 132 | &.picky-date-time-clock__clock-minute--quarter { 133 | height: 6px; 134 | background: var(--oc-gray-8); 135 | width: 1px; 136 | } 137 | } 138 | 139 | .picky-date-time-clock__clock-center, 140 | .picky-date-time-clock__clock-hand { 141 | position: absolute; 142 | left: 50%; 143 | top: 50%; 144 | box-shadow: 2px 2px 8px 0 rgba(0, 0, 0, 0.2); 145 | -webkit-font-smoothing: antialiased; 146 | transform-origin: 50% 50%; 147 | } 148 | 149 | .picky-date-time-clock__clock-center { 150 | transform: translate(-50%, -50%); 151 | width: 8px; 152 | height: 8px; 153 | border-radius: 50%; 154 | background: var(--oc-white); 155 | border: 2px solid var(--oc-gray-6); 156 | box-shadow: 2px 2px 8px 0 rgba(0, 0, 0, 0.5); 157 | z-index: 5; 158 | } 159 | 160 | .picky-date-time-clock__clock-hand--second { 161 | width: 2px; 162 | height: 70px; 163 | background: var(--oc-red-5); 164 | z-index: 3; 165 | cursor: move; 166 | } 167 | 168 | .picky-date-time-clock__clock-hand--minute { 169 | width: 2px; 170 | height: 65px; 171 | background: var(--oc-gray-6); 172 | z-index: 2; 173 | cursor: move; 174 | } 175 | 176 | .picky-date-time-clock__clock-hand--hour { 177 | width: 3px; 178 | height: 50px; 179 | background: var(--oc-gray-7); 180 | z-index: 1; 181 | cursor: move; 182 | } 183 | 184 | .picky-date-time-clock__today { 185 | font-size: 12px; 186 | margin-top: 10px; 187 | } 188 | 189 | .picky-date-time-clock__inline-span span { 190 | display: inline-block; 191 | vertical-align: middle; 192 | } 193 | 194 | .picky-date-time-clock__inline-span:before { 195 | content: ''; 196 | display: inline-block; 197 | height: 100%; 198 | vertical-align: middle; 199 | } 200 | 201 | .picky-date-time-clock__today .picky-date-time-clock__icon { 202 | font-size: 15px; 203 | } 204 | 205 | .picky-date-time-clock__icon { 206 | font-size: 15px; 207 | } 208 | 209 | .picky-date-time-clock__button { 210 | position: absolute; 211 | bottom: 20px; 212 | right: 20px; 213 | display: inline-block; 214 | color: var(--oc-gray-6); 215 | cursor: pointer; 216 | padding: 5px 10px; 217 | border: 1px solid var(--oc-gray-4); 218 | transition: all 0.3s; 219 | background: var(--oc-white); 220 | &:hover { 221 | border: 1px solid var(--oc-blue-4); 222 | background: var(--oc-blue-4); 223 | color: var(--oc-white); 224 | } 225 | } 226 | 227 | @media only screen and (max-width: 550px) { 228 | } 229 | -------------------------------------------------------------------------------- /src/css/calendar.css: -------------------------------------------------------------------------------- 1 | @import 'open-color/open-color.css'; 2 | :root { 3 | --oc-white: #ffffff; 4 | --item-width-l: 60px; 5 | --item-width-m: 40px; 6 | --item-width-s: 25px; 7 | --item-width-xs: 25px; 8 | --item-height-l: 60px; 9 | --item-height-m: 40px; 10 | --item-height-s: 25px; 11 | --item-height-xs: 25px; 12 | } 13 | 14 | /* dropdown section */ 15 | 16 | .picky-date-time-dropdown { 17 | position: relative; 18 | } 19 | 20 | .picky-date-time-dropdown.visible .picky-date-time-dropdown-calendar__menu { 21 | transform: translate3d(-50%, 0, 0) scale3d(1, 1, 1); 22 | transform: translate(-50%, 0) scale(1, 1) \9; 23 | opacity: 1; 24 | padding: 10px; 25 | z-index: 1000; 26 | } 27 | 28 | .picky-date-time-dropdown.visible .picky-date-time-dropdown-calendar__menu-no-effect { 29 | display: block; 30 | } 31 | 32 | .picky-date-time-dropdown .picky-date-time-dropdown-calendar__menu { 33 | will-change: transform, opacity; 34 | transform: translate3d(-50%, 0, 0) scale3d(1, 0, 1); 35 | transform: translate(-50%, 0) scale(1, 0) \9; 36 | opacity: 0; 37 | left: 50%; 38 | &.l { 39 | width: 380px; 40 | margin-top: 40px; 41 | } 42 | &.m { 43 | width: 300px; 44 | margin-top: 35px; 45 | } 46 | &.s { 47 | width: 280px; 48 | margin-top: 30px; 49 | } 50 | &.xs { 51 | width: 280px; 52 | margin-top: 30px; 53 | } 54 | text-align: center; 55 | transform-origin: 0 0; 56 | transition: transform 0.4s, opacity 0.2s; 57 | position: absolute; 58 | box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.3), 0 0 1px 0 rgba(0, 0, 0, 0.12); 59 | /*z-index: -1;*/ 60 | background: var(--oc-white); 61 | } 62 | 63 | .picky-date-time-dropdown .picky-date-time-dropdown-calendar__container { 64 | border-radius: 3px; 65 | overflow: hidden; 66 | margin-top: 10px; 67 | } 68 | 69 | .picky-date-time-dropdown .picky-date-time-dropdown-calendar__item { 70 | padding: 1px 4px; 71 | line-height: 20px; 72 | transition: background 0.4s; 73 | cursor: pointer; 74 | display: block; 75 | } 76 | 77 | .picky-date-time-dropdown-calendar__month { 78 | background: var(--oc-white); 79 | *zoom: 1; 80 | &:after { 81 | content: '\200B'; 82 | display: block; 83 | height: 0; 84 | clear: both; 85 | } 86 | } 87 | 88 | .picky-date-time-dropdown-calendar__month-item { 89 | background: var(--oc-white); 90 | cursor: pointer; 91 | float: left; 92 | width: 33.3%; 93 | } 94 | 95 | .picky-date-time-dropdown-calendar__month-item > div { 96 | &.xs { 97 | padding: 10px 0; 98 | } 99 | padding: 10px 2px; 100 | font-size: 12px; 101 | margin: 5px; 102 | background: var(--oc-white); 103 | transition: all 0.3s; 104 | &:hover { 105 | background: var(--oc-blue-3); 106 | color: var(--oc-white); 107 | } 108 | } 109 | 110 | .picky-date-time-dropdown-calendar__month-item.active > div { 111 | background: var(--oc-blue-5); 112 | color: var(--oc-white); 113 | } 114 | 115 | .picky-date-time-calendar__previous, 116 | .picky-date-time-calendar__next { 117 | } 118 | 119 | .picky-date-time-dropdown .picky-date-time-dropdown-calendar__item:hover { 120 | background: #eee; 121 | } 122 | 123 | .picky-date-time-dropdown-calendar__year { 124 | position: absolute; 125 | overflow: hidden; 126 | width: 100%; 127 | height: 100%; 128 | background: var(--oc-white); 129 | } 130 | 131 | .picky-date-time-dropdown-calendar__year-item { 132 | background: var(--oc-white); 133 | cursor: pointer; 134 | float: left; 135 | height: 100%; 136 | display: table; 137 | width: 20%; 138 | } 139 | 140 | .picky-date-time-dropdown-calendar__year-item > div { 141 | height: 100%; 142 | display: table-cell; 143 | vertical-align: middle; 144 | font-size: 12px; 145 | margin: 2px; 146 | font-size: 14px; 147 | background: var(--oc-white); 148 | transition: all 0.3s; 149 | &:hover { 150 | background: var(--oc-blue-3); 151 | color: var(--oc-white); 152 | } 153 | } 154 | 155 | .picky-date-time-dropdown-calendar__year-item.active > div { 156 | background: var(--oc-blue-5); 157 | color: var(--oc-white); 158 | } 159 | 160 | /* end of dropdown section */ 161 | 162 | .picky-date-time-calendar__default-day, 163 | .picky-date-time-calendar__today { 164 | font-size: 12px; 165 | margin-top: 10px; 166 | } 167 | 168 | .picky-date-time-calendar__today { 169 | left: 0; 170 | } 171 | 172 | .picky-date-time-calendar__default-day { 173 | right: 0; 174 | } 175 | 176 | .picky-date-time-calendar__default-day .picky-date-time-calendar__icon, 177 | .picky-date-time-calendar__today .picky-date-time-calendar__icon { 178 | font-size: 15px; 179 | } 180 | 181 | .picky-date-time-calendar__clicker { 182 | cursor: pointer; 183 | } 184 | 185 | .picky-date-time__col { 186 | display: inline-block; 187 | vertical-align: middle; 188 | } 189 | 190 | .picky-date-time-calendar__title { 191 | cursor: pointer; 192 | width: 100%; 193 | position: absolute; 194 | color: var(--oc-gray-8); 195 | &:hover { 196 | color: var(--oc-blue-3); 197 | } 198 | } 199 | 200 | .picky-date-time-calendar__inline-span span { 201 | display: inline-block; 202 | vertical-align: middle; 203 | } 204 | 205 | .picky-date-time-calendar__inline-span:before { 206 | content: ''; 207 | display: inline-block; 208 | height: 100%; 209 | vertical-align: middle; 210 | } 211 | 212 | .picky-date-time-calendar__content { 213 | } 214 | 215 | .picky-date-time-calendar__icon { 216 | cursor: pointer; 217 | font-size: 20px; 218 | } 219 | 220 | .picky-date-time__col-0-5 { 221 | width: 5%; 222 | } 223 | 224 | .picky-date-time__col-9 { 225 | width: 90%; 226 | } 227 | 228 | .picky-date-time__col-3 { 229 | width: 25%; 230 | } 231 | 232 | .picky-date-time__col-6 { 233 | width: 50%; 234 | } 235 | 236 | .picky-date-time-calendar__header { 237 | text-align: center; 238 | } 239 | 240 | .picky-date-time--inline-block { 241 | display: inline-block; 242 | vertical-align: middle; 243 | } 244 | 245 | .picky-date-time-calendar__table { 246 | display: table; 247 | border-collapse: collapse; 248 | border-collapse: initial!important\9; 249 | margin: 0 auto; 250 | } 251 | 252 | @media all and (-ms-high-contrast: none) { 253 | .picky-date-time-calendar__table { 254 | border-collapse: initial; 255 | } 256 | } 257 | 258 | @supports (-ms-ime-align: auto) { 259 | .picky-date-time-calendar__table { 260 | border-collapse: initial; 261 | } 262 | } 263 | 264 | .picky-date-time-calendar__table-row { 265 | display: table-row; 266 | } 267 | 268 | .picky-date-time-calendar__table-cel { 269 | font-size: 12px; 270 | display: table-cell; 271 | text-align: center; 272 | vertical-align: middle; 273 | border: 1px solid var(--oc-gray-2); 274 | width: 40px; 275 | height: 40px; 276 | padding: 10px; 277 | cursor: default; 278 | transition: all 0.3s; 279 | background: var(--oc-white); 280 | color: var(--oc-gray-7); 281 | fill: var(--oc-gray-7); 282 | & svg { 283 | position: absolute; 284 | right: 0; 285 | bottom: 0; 286 | font-size: 12px; 287 | } 288 | &.l { 289 | font-size: 14px; 290 | width: var(--item-width-l); 291 | height: var(--item-height-l); 292 | } 293 | &.m { 294 | width: var(--item-width-m); 295 | height: var(--item-height-m); 296 | } 297 | &.s { 298 | width: var(--item-width-s); 299 | height: var(--item-height-s); 300 | } 301 | &.xs { 302 | padding: 6px; 303 | width: var(--item-width-xs); 304 | height: var(--item-height-xs); 305 | } 306 | &.disabled { 307 | color: var(--oc-gray-5); 308 | fill: var(--oc-gray-5); 309 | } 310 | &.today { 311 | color: var(--oc-red-5); 312 | fill: var(--oc-red-5); 313 | } 314 | &.marked { 315 | position: relative; 316 | &:after { 317 | position: absolute; 318 | content: ''; 319 | width: 5px; 320 | height: 5px; 321 | background-color: var(--oc-gray-4); 322 | border-radius: 50%; 323 | left: 50%; 324 | bottom: 3px; 325 | transform: translateX(-50%); 326 | } 327 | } 328 | &.active { 329 | &:not(.today) { 330 | color: var(--oc-white); 331 | fill: var(--oc-white); 332 | background: var(--oc-blue-3); 333 | } 334 | } 335 | &.no-border { 336 | border: 1px solid transparent; 337 | } 338 | &.picky-date-time-calendar__date-item { 339 | position: relative; 340 | &:not(.disabled) { 341 | cursor: pointer; 342 | &:hover { 343 | color: var(--oc-white); 344 | fill: var(--oc-white); 345 | background: var(--oc-blue-3); 346 | } 347 | } 348 | } 349 | &.picky-date-time-calendar__date-item .picky-date-time-calendar__icon { 350 | position: absolute; 351 | right: 0; 352 | bottom: 0; 353 | font-size: 12px; 354 | } 355 | } 356 | 357 | .picky-date-time-calendar__table-caption { 358 | color: var(--oc-gray-7); 359 | } 360 | 361 | .picky-date-time-calendar__mask { 362 | opacity: 0; 363 | filter: alpha(opacity=0); 364 | position: absolute; 365 | top: 0; 366 | left: 0; 367 | z-index: -1; 368 | width: 100%; 369 | height: 100%; 370 | background: rgba(0, 0, 0, 0.3); 371 | &.visible { 372 | opacity: 1 !important; 373 | filter: alpha(opacity=100); 374 | background-color: rgba(0, 0, 0, 0.3) !important; 375 | z-index: 1 !important; 376 | } 377 | } 378 | 379 | .picky-date-time-calendar__button { 380 | position: absolute; 381 | bottom: -40px; 382 | display: inline-block; 383 | color: var(--oc-gray-6); 384 | cursor: pointer; 385 | padding: 5px 10px; 386 | border: 1px solid var(--oc-gray-4); 387 | transition: all 0.3s; 388 | background: var(--oc-white); 389 | &:hover { 390 | border: 1px solid var(--oc-blue-4); 391 | background: var(--oc-blue-4); 392 | color: var(--oc-white); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-picky-date-time 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) 4 | 5 | [![npm version](https://badge.fury.io/js/react-picky-date-time.svg)](https://badge.fury.io/js/react-picky-date-time) ![Cdnjs](https://img.shields.io/cdnjs/v/react-picky-date-time) ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/react-picky-date-time.svg) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/edwardfxiao/react-picky-date-time/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 date time picker. 8 | 9 | 10 | 11 | NO Jquery, NO Moment.js 12 | 13 | ### :tada: For range surpport, please take a look at react-minimal-datetime-range 14 | 15 | # Demo & Examples 16 | Please check the online demo example 17 | 18 | Attention: you can find demo source here :) 19 | 20 | # Codesandbox Examples 21 | * Online demo example playground 22 | * Custom locales(when providing ```window.REACT_PICKY_DATE_TIME['customLocale']```) 23 | 24 | ### :tada: For version >= 2.0.0, please update react and react-dom to at least ```16.8.6```, since it is rewrited with hooks. 25 | ```js 26 | "peerDependencies": { 27 | "react": ">= 16.8.6", 28 | "react-dom": ">= 16.8.6" 29 | } 30 | ``` 31 | 32 | # Docs Link 33 | [Custom Locale Guide(can be multiple locales)](#custom-locale) 34 | 35 | # Usage 36 | 37 | ```js 38 | import ReactPickyDateTime from 'react-picky-date-time'; 39 | 40 | ... 41 | const Index = memo(() => { 42 | const [showPickyDateTime, setShowPickyDateTime] = useState(true); 43 | const [date, setDate] = useState('30'); 44 | const [month, setMonth] = useState('01'); 45 | const [year, setYear] = useState('2000'); 46 | const [hour, setHour] = useState('03'); 47 | const [minute, setMinute] = useState('10'); 48 | const [second, setSecond] = useState('40'); 49 | const [meridiem, setMeridiem] = useState('PM'); 50 | ... 51 | // See events section 52 | ... 53 | return ( 54 | setShowPickyDateTime(false)} 60 | defaultTime={`${hour}:${minute}:${second} ${meridiem}`} // OPTIONAL. format: "HH:MM:SS AM" 61 | defaultDate={`${month}/${date}/${year}`} // OPTIONAL. format: "MM/DD/YYYY" 62 | onYearPicked={res => onYearPicked(res)} 63 | onMonthPicked={res => onMonthPicked(res)} 64 | onDatePicked={res => onDatePicked(res)} 65 | onResetDate={res => onResetDate(res)} 66 | onResetDefaultDate={res => onResetDefaultDate(res)} 67 | onSecondChange={res => onSecondChange(res)} 68 | onMinuteChange={res => onMinuteChange(res)} 69 | onHourChange={res => onHourChange(res)} 70 | onMeridiemChange={res => onMeridiemChange(res)} 71 | onResetTime={res => onResetTime(res)} 72 | onResetDefaultTime={res => onResetDefaultTime(res)} 73 | onClearTime={res => onClearTime(res)} 74 | // markedDates={['10/19/2021']} // OPTIONAL. format: "MM/DD/YYYY" 75 | // supportDateRange={['12/03/2021', '12/05/2021']} // OPTIONAL. min date and max date. format: "MM/DD/YYYY" 76 | /> 77 | ); 78 | }); 79 | ``` 80 | 81 | ##### [Class components version example goes here ->](/CLASS-VERSION.md) 82 | 83 | # Installation 84 | ```sh 85 | npm install react-picky-date-time --save 86 | ``` 87 | 88 | #### By CDN (starting from v1.9.1) 89 | ```html 90 | 91 | ... 92 | 93 | 94 | 95 |
96 | 97 | 98 | 99 | 100 | 106 | 107 | 108 | 109 | ``` 110 | 111 | # Donate 112 | Thanks for donating me a donut!  ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄ 113 | 114 | # Browser support 115 | Tested on IE9+ and Chrome and Safari(10.0.3) 116 | 117 | This library relies ```new AbortController```, so if you are developing for IE10+ you should include the polyfill like below 118 | ```js 119 | import 'promise-polyfill/src/polyfill'; 120 | import 'unfetch/polyfill'; 121 | import 'abortcontroller-polyfill'; 122 | ``` 123 | 124 | For IE9, you also need ```performance``` ```requestAnimationFrame``` polyfill for clock ticking 125 | 126 | # Events 127 | 128 | Also consoled out on the demo page examples 129 | 130 | ```js 131 | const onYearPicked = res => { 132 | const { year } = res; 133 | setYear(year); 134 | }; 135 | 136 | const onMonthPicked = res => { 137 | const { month, year } = res; 138 | setMonth(month); 139 | setYear(year); 140 | }; 141 | 142 | const onDatePicked = res => { 143 | const { date, month, year } = res; 144 | setDate(date); 145 | setMonth(month); 146 | setYear(year); 147 | }; 148 | 149 | const onResetDate = res => { 150 | const { date, month, year } = res; 151 | setDate(date); 152 | setMonth(month); 153 | setYear(year); 154 | }; 155 | 156 | const onResetDefaultDate = res => { 157 | const { date, month, year } = res; 158 | setDate(date); 159 | setMonth(month); 160 | setYear(year); 161 | }; 162 | 163 | const onSecondChange = res => { 164 | const { value } = res; 165 | setSecond(value); 166 | }; 167 | 168 | const onMinuteChange = res => { 169 | const { value } = res; 170 | setMinute(value); 171 | }; 172 | 173 | const onHourChange = res => { 174 | const { value } = res; 175 | setHour(value); 176 | }; 177 | 178 | const onMeridiemChange = res => { 179 | setMeridiem(res); 180 | }; 181 | 182 | const onResetTime = res => { 183 | const { clockHandSecond, clockHandMinute, clockHandHour } = res; 184 | setSecond(clockHandSecond.value); 185 | setMinute(clockHandMinute.value); 186 | setHour(clockHandHour.value); 187 | }; 188 | 189 | const onResetDefaultTime = res => { 190 | const { clockHandSecond, clockHandMinute, clockHandHour } = res; 191 | setSecond(clockHandSecond.value); 192 | setMinute(clockHandMinute.value); 193 | setHour(clockHandHour.value); 194 | }; 195 | 196 | const onClearTime = res => { 197 | const { clockHandSecond, clockHandMinute, clockHandHour } = res; 198 | setSecond(clockHandSecond.value); 199 | setMinute(clockHandMinute.value); 200 | setHour(clockHandHour.value); 201 | }; 202 | 203 | // just toggle your outter component state to true or false to show or hide 204 | openPickyDateTime() { 205 | setShowPickyDateTime(true) 206 | } 207 | 208 | onClose() { 209 | setShowPickyDateTime(false) 210 | } 211 | 212 | ``` 213 | 214 | ### Custom Locale (can be multiple locales) 215 | By providing ```window.REACT_PICKY_DATE_TIME['customLocale']```, you can overwrite the current locale if you like or add a new locale. 216 | 217 | 218 | codesandbox example(located in index.html) 219 | 220 | ```html 221 | 237 | ``` 238 | 239 | ## Contributors ✨ 240 | 241 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 |

Edward Xiao

💻 📖 🚇 ⚠️ 👀
251 | 252 | 253 | 254 | 255 | 256 | 257 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 258 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import 'promise-polyfill/src/polyfill'; 2 | import 'unfetch/polyfill'; 3 | import 'abortcontroller-polyfill'; 4 | import 'core-js/stable'; 5 | import 'regenerator-runtime/runtime'; 6 | 7 | // performance polyfill from https://gist.github.com/paulirish/5438650 8 | (function () { 9 | if ('performance' in window == false) { 10 | window.performance = {}; 11 | } 12 | 13 | Date.now = 14 | Date.now || 15 | function () { 16 | // thanks IE8 17 | return new Date().getTime(); 18 | }; 19 | 20 | if ('now' in window.performance == false) { 21 | var nowOffset = Date.now(); 22 | 23 | if (performance.timing && performance.timing.navigationStart) { 24 | nowOffset = performance.timing.navigationStart; 25 | } 26 | 27 | window.performance.now = function now() { 28 | return Date.now() - nowOffset; 29 | }; 30 | } 31 | })(); 32 | 33 | // requestAnimationFrame polyfill from https://stackoverflow.com/questions/24676874/error-requestanimationframe-in-ie9-any-alternate-solution 34 | (function () { 35 | var lastTime = 0; 36 | var vendors = ['webkit', 'moz']; 37 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 38 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; 39 | window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']; 40 | } 41 | 42 | if (!window.requestAnimationFrame) 43 | window.requestAnimationFrame = function (callback, element) { 44 | var currTime = new Date().getTime(); 45 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 46 | var id = window.setTimeout(function () { 47 | callback(currTime + timeToCall); 48 | }, timeToCall); 49 | lastTime = currTime + timeToCall; 50 | return id; 51 | }; 52 | 53 | if (!window.cancelAnimationFrame) 54 | window.cancelAnimationFrame = function (id) { 55 | clearTimeout(id); 56 | }; 57 | })(); 58 | 59 | import React, { useState, memo } from 'react'; 60 | import ReactDOM from 'react-dom'; 61 | import ReactPickyDateTime from '../src/js/component/ReactPickyDateTime'; 62 | 63 | const Index = memo(() => { 64 | const [showPickyDateTime, setShowPickyDateTime] = useState(true); 65 | const [date, setDate] = useState('30'); 66 | const [month, setMonth] = useState('01'); 67 | const [year, setYear] = useState('2000'); 68 | const [hour, setHour] = useState('03'); 69 | const [minute, setMinute] = useState('10'); 70 | const [second, setSecond] = useState('40'); 71 | const [meridiem, setMeridiem] = useState('PM'); 72 | 73 | const onClose = () => {}; 74 | 75 | const onYearPicked = res => { 76 | const { year } = res; 77 | setYear(year); 78 | }; 79 | 80 | const onMonthPicked = res => { 81 | const { month, year } = res; 82 | setMonth(month); 83 | setYear(year); 84 | }; 85 | 86 | const onDatePicked = res => { 87 | const { date, month, year } = res; 88 | setDate(date); 89 | setMonth(month); 90 | setYear(year); 91 | }; 92 | 93 | const onResetDate = res => { 94 | const { date, month, year } = res; 95 | setDate(date); 96 | setMonth(month); 97 | setYear(year); 98 | }; 99 | 100 | const onResetDefaultDate = res => { 101 | const { date, month, year } = res; 102 | setDate(date); 103 | setMonth(month); 104 | setYear(year); 105 | }; 106 | 107 | const onSecondChange = res => { 108 | const { value } = res; 109 | setSecond(value); 110 | }; 111 | 112 | const onMinuteChange = res => { 113 | const { value } = res; 114 | setMinute(value); 115 | }; 116 | 117 | const onHourChange = res => { 118 | const { value } = res; 119 | setHour(value); 120 | }; 121 | 122 | const onMeridiemChange = res => { 123 | setMeridiem(res); 124 | }; 125 | 126 | const onResetTime = res => { 127 | const { clockHandSecond, clockHandMinute, clockHandHour } = res; 128 | setSecond(clockHandSecond.value); 129 | setMinute(clockHandMinute.value); 130 | setHour(clockHandHour.value); 131 | }; 132 | 133 | const onResetDefaultTime = res => { 134 | const { clockHandSecond, clockHandMinute, clockHandHour } = res; 135 | setSecond(clockHandSecond.value); 136 | setMinute(clockHandMinute.value); 137 | setHour(clockHandHour.value); 138 | }; 139 | 140 | const onClearTime = res => { 141 | const { clockHandSecond, clockHandMinute, clockHandHour } = res; 142 | setSecond(clockHandSecond.value); 143 | setMinute(clockHandMinute.value); 144 | setHour(clockHandHour.value); 145 | }; 146 | 147 | const today = new Date(); 148 | const todayY = today.getFullYear(); 149 | const todayM = today.getMonth() + 1; 150 | const todayD = today.getDate(); 151 | 152 | return ( 153 |
154 |
155 |
156 |
157 |

Example 1 DEMO: Calendar and Clock

158 | setShowPickyDateTime(!showPickyDateTime)} 160 | style={{ 161 | textDecoration: 'underline', 162 | color: '#4a4a4a', 163 | cursor: 'pointer', 164 | }} 165 | > 166 | Click to toggle Picky Date Time 167 | 168 |
169 |
170 | setShowPickyDateTime(false)} 176 | onYearPicked={res => onYearPicked(res)} 177 | onMonthPicked={res => onMonthPicked(res)} 178 | onDatePicked={res => onDatePicked(res)} 179 | onResetDate={res => onResetDate(res)} 180 | onSecondChange={res => onSecondChange(res)} 181 | onMinuteChange={res => onMinuteChange(res)} 182 | onHourChange={res => onHourChange(res)} 183 | onMeridiemChange={res => onMeridiemChange(res)} 184 | onResetTime={res => onResetTime(res)} 185 | onClearTime={res => onClearTime(res)} 186 | markedDates={[`${todayM}/${todayD - 1}/${todayY}`, `${todayM}/${todayD}/${todayY}`, `${todayM}/${todayD + 1}/${todayY}`]} 187 | // supportDateRange={[`02/10/2022`, `12/05/2022`]} // "MM/DD/YYYY" 188 | /> 189 |
190 |
191 |
192 |
193 |
194 |

Example 2 DEMO: Calendar and Clock (with DefaultDate and DefaultTime provided)

195 |
196 | {}} 200 | onClick={() => setShowPickyDateTime(true)} 201 | /> 202 |
203 | setShowPickyDateTime(false)} 211 | onYearPicked={res => onYearPicked(res)} 212 | onMonthPicked={res => onMonthPicked(res)} 213 | onDatePicked={res => onDatePicked(res)} 214 | onResetDate={res => onResetDate(res)} 215 | onResetDefaultDate={res => onResetDefaultDate(res)} 216 | onSecondChange={res => onSecondChange(res)} 217 | onMinuteChange={res => onMinuteChange(res)} 218 | onHourChange={res => onHourChange(res)} 219 | onMeridiemChange={res => onMeridiemChange(res)} 220 | onResetTime={res => onResetTime(res)} 221 | onResetDefaultTime={res => onResetDefaultTime(res)} 222 | onClearTime={res => onClearTime(res)} 223 | /> 224 |
225 |
226 |
227 |

Example 3 DEMO: Calendar only (with size of M)

228 |
229 |
230 |
231 | onClose()} 237 | onYearPicked={res => onYearPicked(res)} 238 | onMonthPicked={res => onMonthPicked(res)} 239 | onDatePicked={res => onDatePicked(res)} 240 | onResetDate={res => onResetDate(res)} 241 | onResetDefaultDate={res => onResetDefaultDate(res)} 242 | onSecondChange={res => onSecondChange(res)} 243 | onMinuteChange={res => onMinuteChange(res)} 244 | onHourChange={res => onHourChange(res)} 245 | onMeridiemChange={res => onMeridiemChange(res)} 246 | onResetTime={res => onResetTime(res)} 247 | onResetDefaultTime={res => onResetDefaultTime(res)} 248 | onClearTime={res => onClearTime(res)} 249 | /> 250 |
251 |
252 |

Example 4 DEMO: Clock only

253 |
254 |
{`//size="xs" mode={2}`}
255 |
256 | onClose()} 262 | onYearPicked={res => onYearPicked(res)} 263 | onMonthPicked={res => onMonthPicked(res)} 264 | onDatePicked={res => onDatePicked(res)} 265 | onResetDate={res => onResetDate(res)} 266 | onResetDefaultDate={res => onResetDefaultDate(res)} 267 | onSecondChange={res => onSecondChange(res)} 268 | onMinuteChange={res => onMinuteChange(res)} 269 | onHourChange={res => onHourChange(res)} 270 | onMeridiemChange={res => onMeridiemChange(res)} 271 | onResetTime={res => onResetTime(res)} 272 | onResetDefaultTime={res => onResetDefaultTime(res)} 273 | onClearTime={res => onClearTime(res)} 274 | /> 275 |
276 |
277 |
278 |
279 |
280 | ); 281 | }); 282 | 283 | ReactDOM.render(, document.getElementById('root')); 284 | -------------------------------------------------------------------------------- /src/js/component/Calendar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useMemo, useEffect, useRef, memo } from 'react'; 2 | import { CSSTransition, TransitionGroup } from 'react-transition-group'; 3 | import { cx, isValidDates, useWillUnmount } from '../utils'; 4 | import { LOCALE } from '../locale'; 5 | import { WEEK_NUMBER, PREV_TRANSITION, NEXT_TRANSITION, SELECTOR_YEAR_SET_NUMBER, getDaysArray, getYearSet, formatDateString } from '../constValue'; 6 | const TODAY = new Date(); 7 | const YEAR = TODAY.getFullYear(); 8 | const MONTH = TODAY.getMonth() + 1; 9 | const DATE = TODAY.getDate(); 10 | const isValidDate = (value: string) => { 11 | const userFormat = 'mm/dd/yyyy'; 12 | const delimiter = /[^mdy]/.exec(userFormat)[0]; 13 | const theFormat = userFormat.split(delimiter); 14 | const theDate = value.split(delimiter); 15 | 16 | function isDate(date: Array, format: Array) { 17 | let m, 18 | d, 19 | y, 20 | i = 0, 21 | len = format.length, 22 | f; 23 | for (i; i < len; i++) { 24 | f = format[i]; 25 | if (/m/.test(f)) m = date[i]; 26 | if (/d/.test(f)) d = date[i]; 27 | if (/y/.test(f)) y = date[i]; 28 | } 29 | const nm = Number(m); 30 | const nd = Number(d); 31 | const ny = Number(y); 32 | return ( 33 | nm > 0 && 34 | nm < 13 && 35 | y && 36 | y.length === 4 && 37 | nd > 0 && 38 | // Is it a valid day of the month? 39 | nd <= new Date(ny, nm, 0).getDate() 40 | ); 41 | } 42 | return isDate(theDate, theFormat); 43 | }; 44 | interface IObjectKeysAny { 45 | [key: string]: any; 46 | } 47 | interface IObjectKeysObject { 48 | [key: string]: object; 49 | } 50 | interface IObjectKeysBool { 51 | [key: string]: boolean; 52 | } 53 | interface IObjectKeysArray { 54 | [key: string]: Array; 55 | } 56 | interface CalendarProps { 57 | size: string; 58 | locale: string; 59 | defaultDate: string; 60 | markedDates: Array; 61 | supportDateRange: Array; 62 | onYearPicked?: (res: object) => void; 63 | onMonthPicked?: (res: object) => void; 64 | onDatePicked?: (res: object) => void; 65 | onResetDate?: (res: object) => void; 66 | onResetDefaultDate?: (res: object) => void; 67 | } 68 | const Calendar: React.FC = memo( 69 | ({ size, locale, defaultDate, markedDates, supportDateRange, onYearPicked = () => {}, onMonthPicked = () => {}, onDatePicked = () => {}, onResetDate = () => {}, onResetDefaultDate = () => {} }) => { 70 | const isMouseIsDownOnSelectorPanelClicker = useRef(false); 71 | let defaultDateDate = DATE; 72 | let defaultDateMonth = MONTH; 73 | let defaultDateYear = YEAR; 74 | let defaultDates = getDaysArray(YEAR, MONTH); 75 | const isDefaultDateValid = useMemo(() => isValidDate(defaultDate), [defaultDate]); 76 | if (isDefaultDateValid) { 77 | const dateStr = defaultDate.split('/'); 78 | // MM/DD/YYYY 79 | defaultDateYear = Number(dateStr[2]); 80 | defaultDateMonth = Number(dateStr[0]); 81 | defaultDateDate = Number(dateStr[1]); 82 | defaultDates = getDaysArray(defaultDateYear, defaultDateMonth); 83 | } 84 | const defaultYearStr = String(defaultDateYear); 85 | const defaultMonthStr = formatDateString(defaultDateMonth); 86 | const defaultDateStr = formatDateString(defaultDateDate); 87 | const [dates, setDates] = useState(defaultDates); 88 | 89 | const [pickedYearMonth, setPickedYearMonth] = useState({ 90 | year: defaultYearStr, 91 | month: defaultMonthStr, 92 | string: `${defaultYearStr}-${defaultMonthStr}`, 93 | }); 94 | const [pickedDateInfo, setPickedDateInfo] = useState({ 95 | year: defaultYearStr, 96 | month: defaultMonthStr, 97 | date: defaultDateStr, 98 | }); 99 | const [currentYearMonthDate] = useState({ 100 | year: String(YEAR), 101 | month: String(MONTH), 102 | date: String(DATE), 103 | }); 104 | const [direction, setDirection] = useState(NEXT_TRANSITION); 105 | const [yearSelectorPanelList, setYearSelectorPanelList] = useState(getYearSet(defaultDateYear)); 106 | const [yearSelectorPanel, setYearSelectorPanel] = useState(defaultDateYear); 107 | const [showMask, setShowMask] = useState(false); 108 | const [showSelectorPanel, setShowSelectorPanel] = useState(false); 109 | 110 | const markedDatesHash: IObjectKeysBool = {}; 111 | if (markedDates && isValidDates(markedDates)) { 112 | markedDates.forEach(d => { 113 | markedDatesHash[d] = true; 114 | }); 115 | } 116 | const onMouseDown = useCallback(() => { 117 | isMouseIsDownOnSelectorPanelClicker.current = true; 118 | }, []); 119 | const onMouseUp = useCallback(() => { 120 | isMouseIsDownOnSelectorPanelClicker.current = false; 121 | }, []); 122 | const $monthSelectorPanel = useRef(null); 123 | useEffect(() => { 124 | setDates(getDaysArray(Number(pickedYearMonth.year), Number(pickedYearMonth.month))); 125 | }, [pickedYearMonth]); 126 | const minSupportDate = supportDateRange.length > 0 && isValidDate(supportDateRange[0]) ? supportDateRange[0] : ''; 127 | const maxSupportDate = supportDateRange.length > 1 && isValidDate(supportDateRange[1]) ? supportDateRange[1] : ''; 128 | 129 | const pickYear = useCallback( 130 | (year, direction) => { 131 | year = Number(year); 132 | if (direction === PREV_TRANSITION) { 133 | year = year - 1; 134 | } else { 135 | year = year + 1; 136 | } 137 | setPickedYearMonth({ ...pickedYearMonth, year, string: `${year}-${pickedYearMonth.month}` }); 138 | setDirection(direction); 139 | onYearPicked({ year }); 140 | }, 141 | [pickedYearMonth], 142 | ); 143 | const pickMonth = useCallback( 144 | (month, direction) => { 145 | month = Number(month); 146 | let year = Number(pickedYearMonth.year); 147 | if (direction === PREV_TRANSITION) { 148 | if (month === 1) { 149 | month = 12; 150 | year = year - 1; 151 | } else { 152 | month = month - 1; 153 | } 154 | } else { 155 | if (month === 12) { 156 | month = 1; 157 | year = year + 1; 158 | } else { 159 | month = month + 1; 160 | } 161 | } 162 | const yearStr = String(year); 163 | const monthStr = formatDateString(month); 164 | setPickedYearMonth({ ...pickedYearMonth, year: yearStr, month: monthStr, string: `${yearStr}-${monthStr}` }); 165 | setDirection(direction); 166 | onMonthPicked({ year: yearStr, month: monthStr }); 167 | }, 168 | [pickedYearMonth], 169 | ); 170 | const pickDate = useCallback( 171 | pickedDate => { 172 | const newPickedDateInfo = { 173 | ...pickedDateInfo, 174 | year: pickedYearMonth.year, 175 | month: pickedYearMonth.month, 176 | date: formatDateString(Number(pickedDate)), 177 | }; 178 | setPickedDateInfo(newPickedDateInfo); 179 | onDatePicked(newPickedDateInfo); 180 | }, 181 | [pickedYearMonth, pickedDateInfo], 182 | ); 183 | const reset = useCallback( 184 | (today = false) => { 185 | let year = YEAR; 186 | let month = MONTH; 187 | let date = DATE; 188 | if (!today) { 189 | const dateStr = defaultDate.split('/'); 190 | // MM/DD/YYYY 191 | year = Number(dateStr[2]); 192 | month = Number(dateStr[0]); 193 | date = Number(dateStr[1]); 194 | } 195 | let direction = NEXT_TRANSITION; 196 | if (year < Number(pickedYearMonth.year)) { 197 | direction = PREV_TRANSITION; 198 | } else if (year === Number(pickedYearMonth.year)) { 199 | if (month < Number(pickedYearMonth.month)) { 200 | direction = PREV_TRANSITION; 201 | } 202 | } 203 | const yearStr = formatDateString(year); 204 | const monthStr = formatDateString(month); 205 | const dateStr = formatDateString(date); 206 | setPickedDateInfo({ 207 | ...pickedDateInfo, 208 | year: yearStr, 209 | month: monthStr, 210 | date: dateStr, 211 | }); 212 | setPickedYearMonth({ 213 | ...pickedYearMonth, 214 | year: yearStr, 215 | month: monthStr, 216 | string: `${yearStr}-${monthStr}`, 217 | }); 218 | changeSelectorPanelYearSet(year, direction); 219 | if (!today) { 220 | onResetDefaultDate(pickedDateInfo); 221 | } else { 222 | onResetDate(pickedDateInfo); 223 | } 224 | }, 225 | [pickedYearMonth], 226 | ); 227 | const changeSelectorPanelYearSet = useCallback((yearSelectorPanel, direction) => { 228 | setDirection(direction); 229 | setYearSelectorPanel(yearSelectorPanel); 230 | setYearSelectorPanelList(getYearSet(yearSelectorPanel)); 231 | }, []); 232 | const handleShowSelectorPanel = useCallback(() => { 233 | setShowSelectorPanel(!showSelectorPanel); 234 | setShowMask(!showMask); 235 | }, [showSelectorPanel, showMask]); 236 | 237 | let transitionContainerStyle; 238 | let content; 239 | 240 | if (dates.length) { 241 | let row = dates.length / WEEK_NUMBER; 242 | let rowIndex = 1; 243 | let rowObj: IObjectKeysArray = {}; 244 | dates.map((item, key) => { 245 | if (key < rowIndex * WEEK_NUMBER) { 246 | if (!rowObj[rowIndex]) { 247 | rowObj[rowIndex] = []; 248 | } 249 | rowObj[rowIndex].push(item); 250 | } else { 251 | rowIndex = rowIndex + 1; 252 | if (!rowObj[rowIndex]) { 253 | rowObj[rowIndex] = []; 254 | } 255 | rowObj[rowIndex].push(item); 256 | } 257 | }); 258 | content = ( 259 | 271 | ); 272 | if (row == 6) { 273 | let height = 385; 274 | if (size == 'l') { 275 | height = 500; 276 | } 277 | if (size == 's') { 278 | height = 285; 279 | } 280 | if (size == 'xs') { 281 | height = 235; 282 | } 283 | transitionContainerStyle = { 284 | height: `${height}px`, 285 | }; 286 | } 287 | } 288 | 289 | const captionHtml = LOCALE[locale].weeks.map((item: string, key: string) => { 290 | return ( 291 |
292 | {item} 293 |
294 | ); 295 | }); 296 | let selectorPanelClass = cx('picky-date-time-dropdown', 'picky-date-time-calendar__selector-panel', showSelectorPanel && 'visible'); 297 | let selectorPanelMonthHtml = LOCALE[locale].months.map((item: string, key: string) => { 298 | let itemMonth: number = Number(key) + 1; 299 | const numberMonth = Number(pickedYearMonth.month); 300 | let monthItemClass = cx('picky-date-time-dropdown-calendar__month-item', itemMonth == numberMonth && 'active'); 301 | let month = itemMonth - 1; 302 | let direction = NEXT_TRANSITION; 303 | if (itemMonth < numberMonth) { 304 | direction = PREV_TRANSITION; 305 | month = itemMonth + 1; 306 | } 307 | return ( 308 |
pickMonth(month, direction) 313 | : () => { 314 | return; 315 | } 316 | } 317 | key={key} 318 | > 319 |
{item}
320 |
321 | ); 322 | }); 323 | let selectorPanelYearHtml; 324 | if (yearSelectorPanelList.length) { 325 | selectorPanelYearHtml = yearSelectorPanelList.map((item, key) => { 326 | const numberYearMonth = Number(pickedYearMonth.year); 327 | let yearItemClass = cx('picky-date-time-dropdown-calendar__year-item', item == numberYearMonth && 'active'); 328 | let year = item - 1; 329 | let direction = NEXT_TRANSITION; 330 | if (item < numberYearMonth) { 331 | direction = PREV_TRANSITION; 332 | year = item + 1; 333 | } 334 | return ( 335 |
pickYear(year, direction) 340 | : () => { 341 | return; 342 | } 343 | } 344 | key={key} 345 | > 346 |
{item}
347 |
348 | ); 349 | }); 350 | } 351 | const classNames = direction == NEXT_TRANSITION ? 'forward' : 'backward'; 352 | 353 | const pageClick = useCallback(() => { 354 | if (isMouseIsDownOnSelectorPanelClicker.current) { 355 | return; 356 | } 357 | setShowSelectorPanel(false); 358 | setShowMask(false); 359 | }, []); 360 | 361 | useEffect(() => { 362 | window.addEventListener('mousedown', pageClick, false); 363 | window.addEventListener('touchend', pageClick, false); 364 | }, []); 365 | 366 | useWillUnmount(() => { 367 | window.removeEventListener('mousedown', pageClick, false); 368 | window.removeEventListener('touchend', pageClick, false); 369 | }); 370 | return ( 371 |
372 |
373 |
374 |
375 |
{selectorPanelMonthHtml}
376 |
377 | 378 |
379 | changeSelectorPanelYearSet(yearSelectorPanel - SELECTOR_YEAR_SET_NUMBER, PREV_TRANSITION)} 385 | > 386 | 387 | 388 | 389 |
390 |
391 | React.cloneElement(child, { classNames })}> 392 | 393 |
{selectorPanelYearHtml}
394 |
395 |
396 |
397 |
398 | changeSelectorPanelYearSet(yearSelectorPanel + SELECTOR_YEAR_SET_NUMBER, NEXT_TRANSITION)} 404 | > 405 | 406 | 407 | 408 |
409 |
410 |
411 |
412 |
pickYear(pickedYearMonth.year, PREV_TRANSITION)}> 413 | 414 | 415 | 416 | 417 |
418 |
pickMonth(pickedYearMonth.month, PREV_TRANSITION)}> 419 | 420 | 421 | 422 | 423 |
424 |
425 |
426 | React.cloneElement(child, { classNames })}> 427 | 428 | 429 | 430 | {`${LOCALE[locale].months[Number(pickedYearMonth.month) - 1]}`} 431 | 432 |   433 | 434 | {`${pickedYearMonth.year}`} 435 | 436 | 437 | 438 | 439 |
440 |
441 |
pickMonth(pickedYearMonth.month, NEXT_TRANSITION)}> 442 | 443 | 444 | 445 | 446 |
447 |
pickYear(pickedYearMonth.year, NEXT_TRANSITION)}> 448 | 449 | 450 | 451 | 452 |
453 |
454 |
455 |
456 |
457 |
{captionHtml}
458 |
459 | React.cloneElement(child, { classNames })}> 460 | 461 | {content} 462 | 463 | 464 |
465 |
reset(true)}> 466 | {LOCALE[locale]['today']} 467 | 468 | 472 | 473 | 474 |
475 | {isDefaultDateValid ? ( 476 |
reset(false)}> 477 | {LOCALE[locale]['reset-date']} 478 | 479 | 483 | 484 | 485 |
486 | ) : ( 487 | `` 488 | )} 489 |
490 | ); 491 | }, 492 | ); 493 | interface pickedDateInfo { 494 | date: string; 495 | month: string; 496 | year: string; 497 | } 498 | interface pickedYearMonth { 499 | month: string; 500 | year: string; 501 | } 502 | interface CalendarBodyProps { 503 | size?: string; 504 | data?: IObjectKeysArray; 505 | currentYearMonthDate: pickedDateInfo; 506 | pickedDateInfo?: pickedDateInfo; 507 | pickedYearMonth?: pickedYearMonth; 508 | markedDatesHash: IObjectKeysBool; 509 | minSupportDate: string; 510 | maxSupportDate: string; 511 | onClick?: (res: string) => void; 512 | } 513 | const CalendarBody: React.FC = memo(({ size = 'm', data = {}, currentYearMonthDate, pickedDateInfo, pickedYearMonth, onClick, markedDatesHash, minSupportDate, maxSupportDate }) => { 514 | const { year, month, date } = currentYearMonthDate; 515 | const pickedDate = `${Number(pickedDateInfo.month)}/${Number(pickedDateInfo.date)}/${Number(pickedDateInfo.year)}`; 516 | const pickedMonth = pickedYearMonth.month; 517 | const content = Object.keys(data).map(key => { 518 | let colHtml; 519 | if (data[key].length) { 520 | colHtml = data[key].map((item: { [k: string]: any }, key: any) => { 521 | const itemDate = `${Number(item.month)}/${Number(item.name)}/${item.year}`; 522 | const isPicked = pickedDate === itemDate; 523 | let isDisabled = pickedMonth != item.month; 524 | if (minSupportDate) { 525 | if (new Date(itemDate) < new Date(minSupportDate)) { 526 | isDisabled = true; 527 | } 528 | } 529 | if (maxSupportDate) { 530 | if (new Date(itemDate) > new Date(maxSupportDate)) { 531 | isDisabled = true; 532 | } 533 | } 534 | const datePickerItemClass = cx( 535 | 'picky-date-time-calendar__table-cel', 536 | 'picky-date-time-calendar__date-item', 537 | size, 538 | isDisabled && 'disabled', 539 | date == item.name && month == item.month && year == item.year && 'today', 540 | markedDatesHash[itemDate] && 'marked', 541 | isPicked && 'active', 542 | ); 543 | return ; 544 | }); 545 | } 546 | return ( 547 |
548 | {colHtml} 549 |
550 | ); 551 | }); 552 | return
{content}
; 553 | }); 554 | interface CalendarItemProps { 555 | item?: IObjectKeysAny; 556 | isPicked?: boolean; 557 | isDisabled?: boolean; 558 | datePickerItemClass?: string; 559 | onClick?: (res: string) => void; 560 | } 561 | const CalendarItem: React.FC = memo(({ item = {}, isPicked = false, isDisabled = false, datePickerItemClass = '', onClick = () => {} }) => { 562 | const handleOnClick = useCallback(() => { 563 | onClick(item.name); 564 | }, [item.name]); 565 | return ( 566 |
{ 572 | return; 573 | } 574 | } 575 | > 576 | {item.name} 577 | {isPicked ? ( 578 | 579 | 580 | 581 | 582 | ) : ( 583 | '' 584 | )} 585 |
586 | ); 587 | }); 588 | export default Calendar; 589 | -------------------------------------------------------------------------------- /src/js/component/Clock/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, memo, useCallback, useEffect } from 'react'; 2 | import { cx, usePrevious, useWillUnmount, animationInterval, isValidTime, formatClockNumber } from '../utils'; 3 | import { 4 | R2D, 5 | SECOND_DEGREE_NUMBER, 6 | MINUTE_DEGREE_NUMBER, 7 | HOUR_DEGREE_NUMBER, 8 | QUARTER, 9 | TIME_SELECTION_FIRST_CHAR_POS_LIST, 10 | TIME_SELECTION_FIRST_CHAR_POS_BACKSPACE_LIST, 11 | TIME_SELECTION_SECOND_CHAR_POS_LIST, 12 | TIME_SELECTION_SECOND_CHAR_POS_BACKSPACE_LIST, 13 | TIME_JUMP_CHAR_POS_LIST, 14 | TIME_CURSOR_POSITION_OBJECT, 15 | TIME_TYPE, 16 | } from '../constValue'; 17 | const isDragging = (hash: { [k: string]: boolean }) => { 18 | let draggingItem = ''; 19 | Object.keys(hash).forEach(key => { 20 | if (hash[key] === true) { 21 | draggingItem = key; 22 | } 23 | }); 24 | return draggingItem; 25 | }; 26 | const updateClockHandObj = (o: clockHandObj, value: string, degree: string, startAngle: string, angle: string, isMouseOver: boolean = false) => { 27 | o = { ...o, value, degree, startAngle, angle, isMouseOver }; 28 | return o; 29 | }; 30 | 31 | const TRANSLATE_FIRST_SIZE: { [k: string]: string } = { 32 | l: '-2px, -1px', 33 | m: '-2px, -1px', 34 | s: '-2px, -1px', 35 | xs: '0px, -1px', 36 | }; 37 | 38 | const TRANSLATE_SECOND_SIZE: { [k: string]: string } = { 39 | l: '0px, 155px', 40 | m: '0px, 125px', 41 | s: '0px, 95px', 42 | xs: '0px, 85px', 43 | }; 44 | 45 | const TRANSLATE_QUARTER_SECOND_SIZE: { [k: string]: string } = { 46 | l: '0px, -3px', 47 | m: '0px, -3px', 48 | s: '0px, -3px', 49 | xs: '0px, -3px', 50 | }; 51 | 52 | const SECONDS_TRANSLATE_FIRST_SIZE: { [k: string]: string } = { 53 | l: '-1px, -34.5px', 54 | m: '-1px, -34.5px', 55 | s: '-1px, -34.5px', 56 | xs: '-1px, -34.5px', 57 | }; 58 | 59 | const SECONDS_TRANSLATE_SECOND_SIZE: { [k: string]: string } = { 60 | l: '0px, -22.5px', 61 | m: '0px, -22.5px', 62 | s: '0px, -22.5px', 63 | xs: '0px, -22.5px', 64 | }; 65 | 66 | const MINUTES_TRANSLATE_FIRST_SIZE: { [k: string]: string } = { 67 | l: '-1px, -32.5px', 68 | m: '-1px, -32.5px', 69 | s: '-1px, -32.5px', 70 | xs: '-1px, -32.5px', 71 | }; 72 | 73 | const MINUTES_TRANSLATE_SECOND_SIZE: { [k: string]: string } = { 74 | l: '0px, -20.5px', 75 | m: '0px, -20.5px', 76 | s: '0px, -20.5px', 77 | xs: '0px, -20.5px', 78 | }; 79 | 80 | const HOURS_TRANSLATE_FIRST_SIZE: { [k: string]: string } = { 81 | l: '-1.5px, -24.5px', 82 | m: '-1.5px, -24.5px', 83 | s: '-1.5px, -24.5px', 84 | xs: '-1.5px, -24.5px', 85 | }; 86 | 87 | const HOURS_TRANSLATE_SECOND_SIZE: { [k: string]: string } = { 88 | l: '0px, -14.5px', 89 | m: '0px, -14.5px', 90 | s: '0px, -14.5px', 91 | xs: '0px, -14.5px', 92 | }; 93 | 94 | const getTodayObj = function () { 95 | const today = new Date(); 96 | const year = today.getFullYear(); 97 | const month = today.getMonth() + 1; 98 | const date = today.getDate(); 99 | 100 | const hour = today.getHours(); 101 | const minute = today.getMinutes(); 102 | const second = today.getSeconds(); 103 | 104 | const meridiem = Number(hour) < 12 ? 'AM' : 'PM'; 105 | 106 | const finalSecond = formatClockNumber(Number(second)); 107 | const finalMinute = formatClockNumber(Number(minute)); 108 | const finalHourText = formatClockNumber(Number(hour > 12 ? hour - 12 : hour)); 109 | 110 | return { year, month, date, hour, minute: finalMinute, second: finalSecond, meridiem, hourText: finalHourText }; 111 | }; 112 | 113 | const getInputCharSkipNum = (pos: number) => { 114 | let num = 1; 115 | if (TIME_JUMP_CHAR_POS_LIST.indexOf(pos) != -1) { 116 | num = 2; 117 | } 118 | return num; 119 | }; 120 | 121 | interface clockHandObj { 122 | value: string; 123 | degree: string; 124 | startAngle: string; 125 | angle: string; 126 | isMouseOver: boolean; 127 | } 128 | 129 | interface ClockProps { 130 | size: string; 131 | locale: string; 132 | defaultTime: string; 133 | onSecondChange?: (res: object) => void; 134 | onMinuteChange?: (res: object) => void; 135 | onHourChange?: (res: object) => void; 136 | onMeridiemChange?: (res: string) => void; 137 | onResetTime?: (res: object) => void; 138 | onClearTime?: (res: object) => void; 139 | onResetDefaultTime?: (res: object) => void; 140 | } 141 | const Clock: React.FC = memo( 142 | ({ 143 | size = 'm', 144 | locale = 'en-US', 145 | defaultTime = '', 146 | onSecondChange = () => {}, 147 | onMinuteChange = () => {}, 148 | onHourChange = () => {}, 149 | onMeridiemChange = () => {}, 150 | onResetTime = () => {}, 151 | onClearTime = () => {}, 152 | onResetDefaultTime = () => {}, 153 | }) => { 154 | const $clock = useRef(null); 155 | const $clockCenter = useRef(null); 156 | const $clockCircle = useRef(null); 157 | const $clockHandSecond = useRef(null); 158 | const $clockHandMinute = useRef(null); 159 | const $clockHandHour = useRef(null); 160 | const $timeInput = useRef(null); 161 | const startIntervalRef = useRef(null); 162 | const isDraggingHashRef = useRef<{ [k: string]: boolean }>({ 163 | clockHandSecond: false, 164 | clockHandMinute: false, 165 | clockHandHour: false, 166 | }); 167 | const originXRef = useRef(null); 168 | const originYRef = useRef(null); 169 | const todayObj = getTodayObj(); 170 | 171 | let thour = String(todayObj.hour); 172 | let tminute = String(todayObj.minute); 173 | let tsecond = String(todayObj.second); 174 | let tmeridiem = String(todayObj.meridiem); 175 | let thourText = String(todayObj.hourText); 176 | 177 | const defaultTimeObj = isValidTime(defaultTime); 178 | if (Object.keys(defaultTimeObj).length) { 179 | thour = defaultTimeObj.hour; 180 | thourText = defaultTimeObj.hourText; 181 | tminute = defaultTimeObj.minute; 182 | tsecond = defaultTimeObj.second; 183 | tmeridiem = defaultTimeObj.meridiem; 184 | } 185 | 186 | const secondDegree = String(Number(tsecond) * SECOND_DEGREE_NUMBER); 187 | const minuteDegree = String(Number(tminute) * MINUTE_DEGREE_NUMBER); 188 | const hourDegree = String(Number(thour) * HOUR_DEGREE_NUMBER); 189 | const clockHandObj: clockHandObj = { 190 | value: '', 191 | degree: '', 192 | startAngle: '', 193 | angle: '', 194 | isMouseOver: false, 195 | }; 196 | const [clockHandSecond, setClockHandSecond] = useState(updateClockHandObj(clockHandObj, tsecond, secondDegree, secondDegree, secondDegree)); 197 | const [clockHandMinute, setClockHandMinute] = useState(updateClockHandObj(clockHandObj, tminute, minuteDegree, minuteDegree, minuteDegree)); 198 | const [clockHandHour, setClockHandHour] = useState(updateClockHandObj(clockHandObj, thourText, hourDegree, hourDegree, hourDegree)); 199 | const [meridiem, setMeridiem] = useState(tmeridiem); 200 | const prevStateClockHandSecond = usePrevious(clockHandSecond); 201 | const prevStateClockHandMinute = usePrevious(clockHandMinute); 202 | const prevStateClockHandHour = usePrevious(clockHandHour); 203 | const prevStateMeridiem = usePrevious(meridiem); 204 | const [selectionRange, setSelectionRange] = useState({ start: 0, end: 0 }); 205 | const prevStateSelectionRange = usePrevious(selectionRange); 206 | const [pressKey, setPressKey] = useState({ key: undefined }); 207 | const [counter, setCounter] = useState(0); 208 | // initial call from here 209 | const [abortController, setAbortController] = useState(new AbortController()); 210 | const isAborted = useCallback(() => abortController.signal.aborted, [abortController]); 211 | 212 | // counter here 213 | const initializeClock = useCallback(abortController => { 214 | animationInterval(200, abortController.signal, (time: number) => { 215 | if (!$clock.current) { 216 | abortController.abort(); 217 | return; 218 | } 219 | setCounter(time); 220 | }); 221 | }, []); 222 | 223 | useWillUnmount(() => { 224 | abortController.abort(); 225 | }); 226 | 227 | // initiate the ticking here 228 | useEffect(() => { 229 | if (abortController && abortController.signal.aborted === false) { 230 | startIntervalRef.current = setInterval(() => { 231 | if (new Date().getMilliseconds() > 0 && new Date().getMilliseconds() < 99) { 232 | resetTime(); 233 | initializeClock(abortController); 234 | clearInterval(startIntervalRef.current); 235 | } 236 | }, 4); 237 | } 238 | }, [abortController]); 239 | 240 | useEffect(() => { 241 | // actual ticking updated every second 242 | updateClock(); 243 | }, [counter]); 244 | 245 | const updateClock = useCallback(() => { 246 | if (!$clock.current) { 247 | return; 248 | } 249 | if (isDragging(isDraggingHashRef.current)) { 250 | abortInterval(); 251 | return; 252 | } 253 | resetClockHandObj(); 254 | }, [abortController, defaultTimeObj]); 255 | 256 | const abortInterval = useCallback(() => { 257 | abortController.abort(); 258 | }, [abortController]); 259 | 260 | const resetClockHandObj = useCallback( 261 | (clear = false, defaultTime = false) => { 262 | let hour = '12', 263 | minute = '00', 264 | second = '00', 265 | hourText = '12', 266 | meridiem = 'AM'; 267 | if (!clear) { 268 | const todayObj = getTodayObj(); 269 | hour = String(todayObj.hour); 270 | minute = String(todayObj.minute); 271 | second = String(todayObj.second); 272 | hourText = String(todayObj.hourText); 273 | meridiem = String(todayObj.meridiem); 274 | } 275 | if (defaultTime) { 276 | hour = String(defaultTimeObj.hour); 277 | minute = String(defaultTimeObj.minute); 278 | second = String(defaultTimeObj.second); 279 | hourText = String(defaultTimeObj.hourText); 280 | meridiem = String(defaultTimeObj.meridiem); 281 | } 282 | let secondDegree = String(Number(second) * SECOND_DEGREE_NUMBER); 283 | let minuteDegree = String(Number(minute) * MINUTE_DEGREE_NUMBER); 284 | let hourDegree = String(Number(hour) * HOUR_DEGREE_NUMBER); 285 | const _clockHandSecond = updateClockHandObj(clockHandSecond, second, secondDegree, secondDegree, secondDegree); 286 | const _clockHandMinute = updateClockHandObj(clockHandMinute, minute, minuteDegree, minuteDegree, minuteDegree); 287 | const _clockHandHour = updateClockHandObj(clockHandHour, hourText, hourDegree, hourDegree, hourDegree); 288 | setClockHandSecond(_clockHandSecond); 289 | setClockHandMinute(_clockHandMinute); 290 | setClockHandHour(_clockHandHour); 291 | setMeridiem(meridiem); 292 | return { clockHandSecond: _clockHandSecond, clockHandMinute: _clockHandMinute, clockHandHour: _clockHandHour, meridiem, defaultTimeObj }; 293 | }, 294 | [clockHandSecond, clockHandMinute, clockHandHour, defaultTimeObj], 295 | ); 296 | 297 | const onClick = useCallback( 298 | e => { 299 | abortInterval(); 300 | setSelectionRange({ start: e.target.selectionStart, end: e.target.selectionEnd }); 301 | }, 302 | [abortController], 303 | ); 304 | 305 | const handleMouseWheel = useCallback(e => { 306 | e.preventDefault(); 307 | setPressKey({ key: e.deltaY > 0 ? 'ArrowUp' : 'ArrowDown' }); 308 | }, []); 309 | 310 | const onKeyDown = useCallback( 311 | key => { 312 | const el = $timeInput.current; 313 | const pos = { start: el.selectionStart, end: el.selectionEnd }; 314 | if (typeof key == 'undefined') { 315 | setSelectionRange(pos); 316 | return; 317 | } 318 | const range = { start: 0, end: 0 }; 319 | let elObj, refName; 320 | const o: { [k: string]: boolean } = {}; 321 | if (TIME_CURSOR_POSITION_OBJECT[pos.start]) { 322 | o[TIME_CURSOR_POSITION_OBJECT[pos.start]] = true; 323 | range.start = pos.start == pos.end ? pos.start - 2 : pos.start; 324 | range.end = pos.start; 325 | } 326 | TIME_TYPE.map(i => { 327 | if (typeof o[i] != 'undefined' && o[i]) { 328 | refName = i; 329 | switch (refName) { 330 | case 'clockHandHour': 331 | elObj = clockHandHour; 332 | break; 333 | case 'clockHandMinute': 334 | elObj = clockHandMinute; 335 | break; 336 | case 'clockHandSecond': 337 | elObj = clockHandSecond; 338 | break; 339 | case 'meridiem': 340 | elObj = meridiem; 341 | break; 342 | } 343 | } 344 | }); 345 | if (!elObj) { 346 | return; 347 | } 348 | let newValue; 349 | const obj: { [k: string]: string } = elObj; 350 | if (key == 'ArrowUp' || key == 'ArrowDown') { 351 | range.start = pos.start; 352 | range.end = pos.start != pos.end ? pos.start + 2 : pos.start; 353 | let val = Number(obj.value); 354 | let diff = 1; 355 | if (key == 'ArrowDown') { 356 | diff = -diff; 357 | } 358 | newValue = val + diff; 359 | if (refName == 'clockHandMinute' || refName == 'clockHandSecond') { 360 | if (newValue == 60) { 361 | newValue = 0; 362 | } 363 | if (newValue == -1) { 364 | newValue = 59; 365 | } 366 | } else if (refName == 'clockHandHour') { 367 | if (newValue == 13) { 368 | newValue = 1; 369 | } 370 | if (newValue == -1) { 371 | newValue = 11; 372 | } 373 | } 374 | } else if (!isNaN(Number(key)) || key == 'Backspace' || key == 'Delete') { 375 | let number = Number(key), 376 | start, 377 | end; 378 | let skipNum = getInputCharSkipNum(pos.start); 379 | 380 | if (key == 'Backspace') { 381 | skipNum = -skipNum; 382 | number = 0; 383 | start = pos.start + skipNum; 384 | end = pos.start + skipNum; 385 | if (!obj.value) { 386 | setSelectionRange({ start: start, end: end }); 387 | return; 388 | } 389 | } 390 | if (key == 'Delete') { 391 | number = 0; 392 | } 393 | if (obj.value) { 394 | newValue = number; 395 | let strValue = obj.value.toString(); 396 | if (pos.start == pos.end) { 397 | if (skipNum > 0) { 398 | if (TIME_SELECTION_FIRST_CHAR_POS_LIST.indexOf(pos.start) != -1) { 399 | // 0* 400 | newValue = Number(number + strValue.substr(strValue.length - 1)); 401 | } else if (TIME_SELECTION_SECOND_CHAR_POS_LIST.indexOf(pos.start) != -1) { 402 | // *0 403 | newValue = Number(strValue.substr(0, 1) + number); 404 | } 405 | } else { 406 | if (TIME_SELECTION_FIRST_CHAR_POS_BACKSPACE_LIST.indexOf(pos.start) != -1) { 407 | // 0* 408 | newValue = Number(number + strValue.substr(strValue.length - 1)); 409 | } else if (TIME_SELECTION_SECOND_CHAR_POS_BACKSPACE_LIST.indexOf(pos.start) != -1) { 410 | // *0 411 | newValue = Number(strValue.substr(0, 1) + number); 412 | } 413 | } 414 | range.start = range.end = pos.start + skipNum; 415 | } else { 416 | if (TIME_SELECTION_FIRST_CHAR_POS_LIST.indexOf(pos.start) != -1) { 417 | if (pos.end < pos.start) { 418 | newValue = Number(strValue.substr(0, 1) + number); 419 | range.start = range.end = pos.start; 420 | } else { 421 | newValue = Number(number + strValue.substr(strValue.length - 1)); 422 | range.start = range.end = pos.start + skipNum; 423 | } 424 | } 425 | } 426 | if (refName == 'clockHandHour' && (newValue == 0 || newValue > 12)) { 427 | newValue = 12; 428 | } else { 429 | if (newValue > 60) { 430 | newValue = key; 431 | range.start = range.end = pos.start + skipNum; 432 | } 433 | } 434 | } 435 | } 436 | if (!isNaN(newValue) && refName != 'meridiem') { 437 | let newDegree; 438 | if (refName == 'clockHandSecond') { 439 | newDegree = Number(newValue) * SECOND_DEGREE_NUMBER; 440 | } 441 | if (refName == 'clockHandMinute') { 442 | newDegree = Number(newValue) * MINUTE_DEGREE_NUMBER; 443 | } 444 | if (refName == 'clockHandHour') { 445 | if (Number(newValue) == 0) { 446 | newValue = 12; 447 | } 448 | newDegree = Number(newValue) * HOUR_DEGREE_NUMBER; 449 | } 450 | setSelectionRange({ start: range.start, end: range.end }); 451 | switchSetClockState(refName, { ...obj, value: formatClockNumber(newValue), degree: newDegree, startAngle: newDegree, angle: newDegree }); 452 | } 453 | if (key == 'ArrowUp' || key == 'ArrowDown') { 454 | if (refName == 'meridiem') { 455 | setMeridiem(prevState => (prevState == 'AM' ? 'PM' : 'AM')); 456 | setSelectionRange({ start: range.start, end: range.end }); 457 | } 458 | } 459 | }, 460 | [clockHandHour, clockHandMinute, clockHandSecond, meridiem], 461 | ); 462 | 463 | useEffect(() => { 464 | if (prevStateSelectionRange != selectionRange) { 465 | $timeInput.current.setSelectionRange(selectionRange.start, selectionRange.end); 466 | } 467 | }, [selectionRange]); 468 | 469 | useEffect(() => { 470 | if (isAborted()) { 471 | if (prevStateClockHandSecond != clockHandSecond) { 472 | onSecondChange && onSecondChange({ ...clockHandSecond, value: formatClockNumber(Number(clockHandSecond.value)) }); 473 | } 474 | } 475 | }, [clockHandSecond]); 476 | 477 | useEffect(() => { 478 | if (isAborted()) { 479 | if (prevStateClockHandMinute != clockHandMinute) { 480 | onMinuteChange && onMinuteChange({ ...clockHandMinute, value: formatClockNumber(Number(clockHandMinute.value)) }); 481 | } 482 | } 483 | }, [clockHandMinute]); 484 | 485 | useEffect(() => { 486 | if (isAborted()) { 487 | if (prevStateClockHandHour != clockHandHour) { 488 | onHourChange && onHourChange({ ...clockHandHour, value: formatClockNumber(Number(clockHandHour.value)) }); 489 | } 490 | } 491 | }, [clockHandHour]); 492 | 493 | useEffect(() => { 494 | if (isAborted()) { 495 | if (prevStateMeridiem != meridiem) { 496 | onMeridiemChange && onMeridiemChange(meridiem); 497 | } 498 | } 499 | }, [meridiem]); 500 | 501 | useEffect(() => { 502 | if (pressKey.key) { 503 | onKeyDown(pressKey.key); 504 | } 505 | }, [pressKey]); 506 | 507 | const onMouseOver = useCallback( 508 | refName => { 509 | switchSetClockState(refName, { isMouseOver: true }); 510 | }, 511 | [clockHandSecond, clockHandMinute, clockHandHour], 512 | ); 513 | 514 | const onMouseOut = useCallback( 515 | refName => { 516 | switchSetClockState(refName, { isMouseOver: false }); 517 | }, 518 | [clockHandSecond, clockHandMinute, clockHandHour], 519 | ); 520 | 521 | const switchSetClockState = useCallback( 522 | (refName, v) => { 523 | switch (refName) { 524 | case 'clockHandSecond': 525 | setClockHandSecond(prevState => ({ ...prevState, ...v })); 526 | break; 527 | case 'clockHandMinute': 528 | setClockHandMinute(prevState => ({ ...prevState, ...v })); 529 | break; 530 | case 'clockHandHour': 531 | setClockHandHour(prevState => ({ ...prevState, ...v })); 532 | break; 533 | } 534 | }, 535 | [clockHandSecond, clockHandMinute, clockHandHour], 536 | ); 537 | 538 | const handleMouseDown = useCallback( 539 | (refName, e) => { 540 | abortInterval(); 541 | let x = e.clientX - originXRef.current; 542 | let y = e.clientY - originYRef.current; 543 | let startAngle = R2D * Math.atan2(y, x); 544 | switchSetClockState(refName, { startAngle: startAngle }); 545 | isDraggingHashRef.current[refName] = true; 546 | }, 547 | [clockHandSecond, clockHandMinute, clockHandHour, abortController], 548 | ); 549 | 550 | const handleMouseMove = useCallback( 551 | e => { 552 | const refName = isDragging(isDraggingHashRef.current); 553 | if (refName) { 554 | let roundingAngle; 555 | let elObj; 556 | switch (refName) { 557 | case 'clockHandSecond': 558 | roundingAngle = SECOND_DEGREE_NUMBER; 559 | elObj = clockHandSecond; 560 | break; 561 | case 'clockHandMinute': 562 | roundingAngle = SECOND_DEGREE_NUMBER; 563 | elObj = clockHandMinute; 564 | break; 565 | case 'clockHandHour': 566 | roundingAngle = HOUR_DEGREE_NUMBER; 567 | elObj = clockHandHour; 568 | break; 569 | } 570 | let x = e.clientX - originXRef.current; 571 | let y = e.clientY - originYRef.current; 572 | let d = R2D * Math.atan2(y, x); 573 | let rotation = Number(d - Number(elObj.startAngle)); 574 | rotation = Math.floor(((rotation % 360) + roundingAngle / 2) / roundingAngle) * roundingAngle; 575 | let degree = Number(elObj.angle) + rotation; 576 | if (degree >= 360) { 577 | degree = degree - 360; 578 | } 579 | if (degree < 0) { 580 | degree = degree + 360; 581 | } 582 | let value = degree / roundingAngle; 583 | if (refName === 'clockHandHour') { 584 | if (formatClockNumber(value) == '00') { 585 | value = 12; 586 | } 587 | } 588 | switchSetClockState(refName, { value, degree }); 589 | } 590 | }, 591 | [clockHandSecond, clockHandMinute, clockHandHour], 592 | ); 593 | 594 | const handleMouseUp = useCallback(() => { 595 | Object.keys(isDraggingHashRef.current).forEach(key => { 596 | isDraggingHashRef.current[key] = false; 597 | }); 598 | setClockHandSecond(prevState => ({ ...prevState, angle: clockHandSecond.degree })); 599 | setClockHandMinute(prevState => ({ ...prevState, angle: clockHandMinute.degree })); 600 | setClockHandHour(prevState => ({ ...prevState, angle: clockHandHour.degree })); 601 | }, [clockHandSecond, clockHandMinute, clockHandHour]); 602 | 603 | const initCoordinates = useCallback(() => { 604 | if ($clockCenter.current == null) { 605 | return; 606 | } 607 | const centerPointPos = $clockCenter.current.getBoundingClientRect(); 608 | const top = centerPointPos.top, 609 | left = centerPointPos.left, 610 | height = centerPointPos.height, 611 | width = centerPointPos.width; 612 | originXRef.current = left + width / 2; 613 | originYRef.current = top + height / 2; 614 | }, []); 615 | 616 | const resetTime = useCallback(() => { 617 | const resetedTime = resetClockHandObj(); 618 | onResetTime && onResetTime(resetedTime); 619 | }, []); 620 | 621 | const resetToDefaultTime = useCallback(() => { 622 | const resetedTime = resetClockHandObj(false, true); 623 | onResetDefaultTime && onResetDefaultTime(resetedTime); 624 | }, []); 625 | 626 | const clear = useCallback(() => { 627 | const res = resetClockHandObj(true); 628 | onClearTime && onClearTime(res); 629 | }, [abortController]); 630 | 631 | useEffect(() => { 632 | setTimeout(() => initCoordinates(), 1000); 633 | document.addEventListener('resize', initCoordinates, true); 634 | document.addEventListener('scroll', initCoordinates, true); 635 | document.addEventListener('mousemove', handleMouseMove, true); 636 | document.addEventListener('mouseup', handleMouseUp, true); 637 | $timeInput.current.addEventListener('mousewheel', handleMouseWheel, { passive: false }); 638 | return () => { 639 | document.removeEventListener('resize', initCoordinates, true); 640 | document.removeEventListener('scroll', initCoordinates, true); 641 | document.removeEventListener('mousemove', handleMouseMove, true); 642 | document.removeEventListener('mouseup', handleMouseUp, true); 643 | $timeInput.current.removeEventListener('mousewheel', handleMouseWheel, { passive: false }); 644 | }; 645 | }, [clockHandSecond, clockHandMinute, clockHandHour]); 646 | 647 | const secondStyle = { 648 | transform: `translate(${SECONDS_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandSecond.degree}deg) translate(${SECONDS_TRANSLATE_SECOND_SIZE[size]})`, 649 | WebkitTransform: `translate(${SECONDS_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandSecond.degree}deg) translate(${SECONDS_TRANSLATE_SECOND_SIZE[size]})`, 650 | MozTransform: `translate(${SECONDS_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandSecond.degree}deg) translate(${SECONDS_TRANSLATE_SECOND_SIZE[size]})`, 651 | msTransform: `translate(${SECONDS_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandSecond.degree}deg) translate(${SECONDS_TRANSLATE_SECOND_SIZE[size]})`, 652 | OTransform: `translate(${SECONDS_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandSecond.degree}deg) translate(${SECONDS_TRANSLATE_SECOND_SIZE[size]})`, 653 | }; 654 | const minuteStyle = { 655 | transform: `translate(${MINUTES_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandMinute.degree}deg) translate(${MINUTES_TRANSLATE_SECOND_SIZE[size]})`, 656 | WebkitTransform: `translate(${MINUTES_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandMinute.degree}deg) translate(${MINUTES_TRANSLATE_SECOND_SIZE})`, 657 | MozTransform: `translate(${MINUTES_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandMinute.degree}deg) translate(${MINUTES_TRANSLATE_SECOND_SIZE[size]})`, 658 | msTransform: `translate(${MINUTES_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandMinute.degree}deg) translate(${MINUTES_TRANSLATE_SECOND_SIZE[size]})`, 659 | OTransform: `translate(${MINUTES_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandMinute.degree}deg) translate(${MINUTES_TRANSLATE_SECOND_SIZE[size]})`, 660 | }; 661 | const hourStyle = { 662 | transform: `translate(${HOURS_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandHour.degree}deg) translate(${HOURS_TRANSLATE_SECOND_SIZE[size]})`, 663 | WebkitTransform: `translate(${HOURS_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandHour.degree}deg) translate(${HOURS_TRANSLATE_SECOND_SIZE[size]})`, 664 | MozTransform: `translate(${HOURS_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandHour.degree}deg) translate(${HOURS_TRANSLATE_SECOND_SIZE[size]})`, 665 | msTransform: `translate(${HOURS_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandHour.degree}deg) translate(${HOURS_TRANSLATE_SECOND_SIZE[size]})`, 666 | OTransform: `translate(${HOURS_TRANSLATE_FIRST_SIZE[size]}) rotate(${clockHandHour.degree}deg) translate(${HOURS_TRANSLATE_SECOND_SIZE[size]})`, 667 | }; 668 | 669 | const minutesItem = []; 670 | for (let i = 0; i < 60; i++) { 671 | let isQuarter = false; 672 | let isFive = false; 673 | let translateFirst = TRANSLATE_FIRST_SIZE[size]; 674 | let translateSecond = TRANSLATE_SECOND_SIZE[size]; 675 | if (QUARTER.indexOf(i) != -1) { 676 | isQuarter = true; 677 | translateFirst = TRANSLATE_QUARTER_SECOND_SIZE[size]; 678 | } 679 | if (i % 5 == 0) { 680 | isFive = true; 681 | } 682 | let minutesItemClass = cx('picky-date-time-clock__clock-minute', isQuarter && 'picky-date-time-clock__clock-minute--quarter', isFive && 'picky-date-time-clock__clock-minute--five'); 683 | let degree = i * 6 + 180; 684 | let minutesItemStyle = { 685 | transform: `translate(${translateFirst}) rotate(${degree}deg) translate(${translateSecond})`, 686 | WebkitTransform: `translate(${translateFirst}) rotate(${degree}deg) translate(${translateSecond})`, 687 | MozTransform: `translate(${translateFirst}) rotate(${degree}deg) translate(${translateSecond})`, 688 | msTransform: `translate(${translateFirst}) rotate(${degree}deg) translate(${translateSecond})`, 689 | OTransform: `translate(${translateFirst}) rotate(${degree}deg) translate(${translateSecond})`, 690 | }; 691 | minutesItem.push(
); 692 | } 693 | 694 | return ( 695 |
696 |
697 |
onMouseOver('clockHandSecond')} 701 | onMouseOut={() => onMouseOut('clockHandSecond')} 702 | onMouseDown={e => handleMouseDown('clockHandSecond', e)} 703 | ref={$clockHandSecond} 704 | /> 705 |
onMouseOver('clockHandMinute')} 709 | onMouseOut={() => onMouseOut('clockHandMinute')} 710 | onMouseDown={e => handleMouseDown('clockHandMinute', e)} 711 | ref={$clockHandMinute} 712 | /> 713 |
onMouseOver('clockHandHour')} 717 | onMouseOut={() => onMouseOut('clockHandHour')} 718 | onMouseDown={e => handleMouseDown('clockHandHour', e)} 719 | ref={$clockHandHour} 720 | /> 721 | {minutesItem} 722 |
723 |
724 |
725 |
726 | { 730 | setPressKey({ key: e.key }); 731 | if (!(e.key == 'ArrowLeft' || e.key == 'ArrowRight')) { 732 | e.preventDefault(); 733 | } 734 | }} 735 | onChange={() => {}} 736 | onClick={e => onClick(e)} 737 | ref={$timeInput} 738 | /> 739 | { 746 | abortInterval(); 747 | clear(); 748 | }} 749 | > 750 | 751 | 752 | 753 |
754 |
755 | { 762 | resetTime(); 763 | if (abortController.signal.aborted) { 764 | setAbortController(new AbortController()); 765 | } 766 | }} 767 | style={{ verticalAlign: 'middle' }} 768 | > 769 | 770 | 771 | 772 | 773 |
774 | {Object.keys(defaultTimeObj).length > 0 ? ( 775 |
776 | { 783 | abortInterval(); 784 | resetToDefaultTime(); 785 | }} 786 | > 787 | 791 | 792 | 793 |
794 | ) : ( 795 | `` 796 | )} 797 |
798 |
799 | ); 800 | }, 801 | ); 802 | 803 | export default Clock; 804 | --------------------------------------------------------------------------------