├── .all-contributorsrc ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── LICENSE_996 ├── README.md ├── eslintignore.json ├── example └── index.js ├── index.js ├── jest.config.js ├── package.json ├── postcss.config.js ├── react-minimal-datetime-range.gif ├── src ├── css │ └── example.css ├── html │ └── layout.html └── js │ └── component │ ├── Calendar.tsx │ ├── RangeDate.tsx │ ├── RangeTime.tsx │ ├── ReactMinimalRange.tsx │ ├── const.ts │ ├── global.d.ts │ ├── index.global.ts │ ├── index.ts │ ├── index.umd.js │ ├── locale.ts │ ├── react-minimal-datetime-range.css │ └── utils.ts ├── stylelint.config.js ├── tea.yaml ├── tsconfig.json └── webpack ├── base.babel.js ├── build_path.js ├── development.config.babel.js ├── production.config.babel.js ├── umd.base.config.babel.js ├── umd.global.config.babel.js └── umd.local.config.babel.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "edwardfxiao", 10 | "name": "Edward Xiao", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/11728228?v=4", 12 | "profile": "https://github.com/edwardfxiao", 13 | "contributions": [ 14 | "code", 15 | "doc", 16 | "infra", 17 | "test", 18 | "review" 19 | ] 20 | }, 21 | { 22 | "login": "ryush00", 23 | "name": "ryush00", 24 | "avatar_url": "https://avatars.githubusercontent.com/u/4997174?v=4", 25 | "profile": "https://github.com/ryush00", 26 | "contributions": [ 27 | "code" 28 | ] 29 | } 30 | ], 31 | "contributorsPerLine": 7, 32 | "projectName": "react-minimal-datetime-range", 33 | "projectOwner": "edwardfxiao", 34 | "repoType": "github", 35 | "repoHost": "https://github.com", 36 | "skipCi": true 37 | } 38 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "env": 4 | { 5 | "development": 6 | { 7 | 8 | }, 9 | "production": 10 | { 11 | 12 | }, 13 | "lib": 14 | { 15 | "plugins": [ 16 | ["css-modules-transform", 17 | { 18 | "generateScopedName": "[name]__[local]" 19 | }], 20 | "@babel/proposal-class-properties", 21 | "@babel/proposal-object-rest-spread" 22 | ], 23 | }, 24 | } 25 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-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 | "react/jsx-filename-extension": { 26 | "extensions": [".jsx", ".tsx"] 27 | }, 28 | // Disable `no-console` rule 29 | "no-console": 0, 30 | // Give a warning if identifiers contain underscores 31 | "no-underscore-dangle": 0, 32 | "no-empty-pattern": 0, 33 | "react/prop-types": 0, 34 | "react-hooks/rules-of-hooks": "error", 35 | "no-empty": [ 36 | "error", 37 | { 38 | "allowEmptyCatch": true 39 | } 40 | ], 41 | "react/display-name": [0, { "ignoreTranspilerName": true }] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /*.log 3 | /log/*.log 4 | coverage 5 | /notes 6 | /lib 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /example 3 | /webpack 4 | /src 5 | /.babelrc 6 | /.eslintrc.json 7 | /eslintignore.json 8 | /.gitignore 9 | /.travis.yml 10 | /jest.config.js 11 | /postcss.config.js 12 | /stylelint.config.js 13 | /tsconfig.json 14 | /tslint.json 15 | /*.gif 16 | /dist 17 | /*.html 18 | /.github 19 | /rev-manifest.json 20 | /docs -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | before_script: 5 | - npm i 6 | script: npm run prepublish 7 | after_script: "cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js" 8 | env: 9 | - REACT=16 10 | notifications: 11 | email: 12 | - email:edwardfxiao@gmail.com 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.1.0 2 | 3 | - Add ```onChooseDate``` to RangePicker component as #9 states 4 | 5 | # 2.0.9 6 | 7 | - Bugfix as #8 states 8 | 9 | # 2.0.8 10 | 11 | - Add ```duration``` to RangePicker component as #8 states 12 | 13 | # 2.0.7 14 | 15 | - Add ```supportDateRange``` to RangePicker component as #7 states 16 | 17 | # 2.0.6 18 | 19 | - CalendarPicker bugfix 20 | 21 | # 2.0.5 22 | 23 | - Better TypeScript support (export interface CalendarPickerProps and RangePickerProps) 24 | 25 | # 2.0.4 26 | 27 | - Update README about date_format in ```customLocale``` 28 | 29 | # 2.0.3 30 | 31 | - Add ```supportDateRange``` to Calendar component 32 | 33 | # 2.0.2 34 | 35 | - Support Korean 36 | 37 | # 2.0.0 38 | 39 | - Support TS -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present Edward Xiao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE_996: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-present Edward Xiao 2 | 3 | Anti 996 License Version 1.0 (Draft) 4 | 5 | Permission is hereby granted to any individual or legal entity obtaining a copy 6 | of this licensed work (including the source code, documentation and/or related 7 | items, hereinafter collectively referred to as the "licensed work"), free of 8 | charge, to deal with the licensed work for any purpose, including without 9 | limitation, the rights to use, reproduce, modify, prepare derivative works of, 10 | publish, distribute and sublicense the licensed work, subject to the following 11 | conditions: 12 | 13 | 1. The individual or the legal entity must conspicuously display, without 14 | modification, this License on each redistributed or derivative copy of the 15 | Licensed Work. 16 | 17 | 2. The individual or the legal entity must strictly comply with all applicable 18 | laws, regulations, rules and standards of the jurisdiction relating to 19 | labor and employment where the individual is physically located or where 20 | the individual was born or naturalized; or where the legal entity is 21 | registered or is operating (whichever is stricter). In case that the 22 | jurisdiction has no such laws, regulations, rules and standards or its 23 | laws, regulations, rules and standards are unenforceable, the individual 24 | or the legal entity are required to comply with Core International Labor 25 | Standards. 26 | 27 | 3. The individual or the legal entity shall not induce or force its 28 | employee(s), whether full-time or part-time, or its independent 29 | contractor(s), in any methods, to agree in oral or written form, 30 | to directly or indirectly restrict, weaken or relinquish his or 31 | her rights or remedies under such laws, regulations, rules and 32 | standards relating to labor and employment as mentioned above, 33 | no matter whether such written or oral agreement are enforceable 34 | under the laws of the said jurisdiction, nor shall such individual 35 | or the legal entity limit, in any methods, the rights of its employee(s) 36 | or independent contractor(s) from reporting or complaining to the copyright 37 | holder or relevant authorities monitoring the compliance of the license 38 | about its violation(s) of the said license. 39 | 40 | THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 42 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT 43 | HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 44 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION 45 | WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK. 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-minimal-datetime-range 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) 4 | 5 | [![npm version](https://badge.fury.io/js/react-minimal-datetime-range.svg)](https://badge.fury.io/js/react-minimal-datetime-range) ![Cdnjs](https://img.shields.io/cdnjs/v/react-minimal-datetime-range) ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/react-minimal-datetime-range.svg) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/edwardfxiao/react-minimal-datetime-range/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 range picker. Online demo examples. 8 | # 9 | 10 | # Online Demo 11 | Online demo example 12 | 13 | Demo source code 14 | 15 | # Codesandbox Examples 16 | * Live playground (Make sure window width is greater than 900 for better experience) 17 | * Example of custom locales (when providing ```window.REACT_MINIMAL_DATETIME_RANGE['customLocale']```) 18 | 19 | # Docs Link 20 | [Custom Locale Guide(can be multiple locales)](#custom-locale) 21 | 22 | ### Version of ```16.8.6``` or higher of react and react-dom is required. 23 | ```js 24 | "peerDependencies": { 25 | "react": ">= 16.8.6", 26 | "react-dom": ">= 16.8.6" 27 | } 28 | ``` 29 | 30 | # Installation 31 | ```sh 32 | npm install react-minimal-datetime-range --save 33 | ``` 34 | #### By CDN (starting from v2.0.0) 35 | ```html 36 | 37 | ... 38 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | 46 | 52 | 53 | 54 | 55 | ``` 56 | 57 | # Donation 58 | Thanks for donating me a donut!  ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄ 59 | 60 | # Browser support 61 | Tested on IE9+ and Chrome and Safari(10.0.3) 62 | 63 | # Docs 64 | 65 | ```js 66 | import { CalendarPicker, RangePicker } from 'react-minimal-datetime-range'; 67 | import 'react-minimal-datetime-range/lib/react-minimal-datetime-range.min.css'; 68 | 69 | setShowCalendarPicker(false)} 74 | defaultDate={year + '-' + month + '-' + date} // OPTIONAL. format: "YYYY-MM-DD" 75 | onYearPicked={res => console.log(res)} 76 | onMonthPicked={res => console.log(res)} 77 | onDatePicked={res => console.log(res)} 78 | onResetDate={res => console.log(res)} 79 | onResetDefaultDate={res => console.log(res)} 80 | style={{ width: '300px', margin: '10px auto 0' }} 81 | // markedDates={[`${todayY}-${todayM}-${todayD - 1}`, `${todayY}-${todayM}-${todayD}`]} // OPTIONAL. ['YYYY-MM-DD'] 82 | // supportDateRange={[`2022-02-16`, `2022-12-10`]} // "YYYY-MM-DD" 83 | // defaultTimes={['10:12']} // OPTIONAL 84 | // enableTimeSelection={true} // OPTIONAL 85 | // handleChooseHourPick={res => console.log(res)} // OPTIONAL 86 | // handleChooseMinutePick={res => console.log(res)} // OPTIONAL 87 | /> 88 | 89 | console.log(res)} 95 | onClose={() => console.log('onClose')} 96 | onClear={() => console.log('onClear')} 97 | style={{ width: '300px', margin: '0 auto' }} 98 | placeholder={['Start Time', 'End Time']} 99 | // markedDates={[`${todayY}-${todayM}-${todayD - 1}`, `${todayY}-${todayM}-${todayD}`]} // OPTIONAL. ['YYYY-MM-DD'] 100 | showOnlyTime={false} // default is false, only select time 101 | // duration={2} // day count. default is 0. End date will be automatically added 2 days when the start date is picked. 102 | // onChooseDate={res => {}} // on date clicked 103 | //////////////////// 104 | // IMPORTANT DESC // 105 | //////////////////// 106 | defaultDates={[year+'-'+month+'-'+date,year+'-'+month+'-'+date]} 107 | // ['YYYY-MM-DD', 'YYYY-MM-DD'] 108 | // This is the value you choosed every time. 109 | defaultTimes={[hour+':'+minute,hour+':'+minute]} 110 | // ['hh:mm', 'hh:mm'] 111 | // This is the value you choosed every time. 112 | initialDates={[year+'-'+month+'-'+date,year+'-'+month+'-'+date]} 113 | // ['YYYY-MM-DD', 'YYYY-MM-DD'] 114 | // This is the initial dates. 115 | // If provied, input will be reset to this value when the clear icon hits, 116 | // otherwise input will be display placeholder 117 | initialTimes={[hour+':'+minute,hour+':'+minute]} 118 | // ['hh:mm', 'hh:mm'] 119 | // This is the initial times. 120 | // If provied, input will be reset to this value when the clear icon hits, 121 | // otherwise input will be display placeholder 122 | /> 123 | ``` 124 | 125 | 126 | ### Custom Locale (can be multiple locales) 127 | By providing ```window.REACT_MINIMAL_DATETIME_RANGE['customLocale']```, you can overwrite the current locale if you like or add a new locale. 128 | 129 | codesandbox example(located in index.html) 130 | 131 | ```html 132 | 151 | ``` 152 | 153 | ## Contributors ✨ 154 | 155 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 |

Edward Xiao

💻 📖 🚇 ⚠️ 👀

ryush00

💻
166 | 167 | 168 | 169 | 170 | 171 | 172 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 173 | -------------------------------------------------------------------------------- /eslintignore.json: -------------------------------------------------------------------------------- 1 | coverage/ 2 | dist/ 3 | lib/ -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import 'core-js/stable'; 2 | import 'regenerator-runtime/runtime'; 3 | import React, { useState, useRef, useCallback } from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import prefixAll from 'inline-style-prefix-all'; 6 | import '../src/css/example.css'; 7 | import { CalendarPicker, RangePicker } from '../src/js/component/index'; 8 | import '../src/js/component/react-minimal-datetime-range.css'; 9 | const now = new Date(); 10 | const todayY = now.getFullYear(); 11 | const todayM = now.getMonth() + 1; 12 | const todayD = now.getDate(); 13 | if (!String.prototype.padStart) { 14 | String.prototype.padStart = function padStart(targetLength, padString) { 15 | targetLength = targetLength >> 0; //truncate if number, or convert non-number to 0; 16 | padString = String(typeof padString !== 'undefined' ? padString : ' '); 17 | if (this.length >= targetLength) { 18 | return String(this); 19 | } else { 20 | targetLength = targetLength - this.length; 21 | if (targetLength > padString.length) { 22 | padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed 23 | } 24 | return padString.slice(0, targetLength) + String(this); 25 | } 26 | }; 27 | } 28 | const Component = () => { 29 | const $passwordWrapperRef = useRef(null); 30 | const $pinWrapperRef = useRef(null); 31 | const $activationWrapperRef = useRef(null); 32 | const [showCalendarPicker, setShowCalendarPicker] = useState(true); 33 | const [hour, setHour] = useState('01'); 34 | const [minute, setMinute] = useState('01'); 35 | const [month, setMonth] = useState(String(now.getMonth() + 1).padStart(2, '0')); 36 | const [date, setDate] = useState(String(now.getDate()).padStart(2, '0')); 37 | const [year, setYear] = useState(String(now.getFullYear())); 38 | return ( 39 |
40 |
41 |

CalendarPicker

42 |
43 |
44 |
45 |
46 |
{ 48 | setShowCalendarPicker(!showCalendarPicker); 49 | }} 50 | style={{ textAlign: 'center', cursor: 'pointer' }} 51 | > 52 | {!showCalendarPicker ? Show CalendarPicker : Close CalendarPicker} 53 |
54 | setShowCalendarPicker(false)} 58 | allowPageClickToClose={true} // default is true 59 | defaultDate={year + '-' + month + '-' + date} // OPTIONAL. format: "YYYY-MM-DD" 60 | onYearPicked={res => console.log(res)} 61 | onMonthPicked={res => console.log(res)} 62 | onDatePicked={res => console.log(res)} 63 | onResetDate={res => console.log(res)} 64 | onResetDefaultDate={res => console.log(res)} 65 | style={{ width: '300px', margin: '10px auto 0' }} 66 | markedDates={[`${todayY}-${todayM}-${todayD - 1}`, `${todayY}-${todayM}-${todayD}`]} // OPTIONAL. ['YYYY-MM-DD'] 67 | // supportDateRange={[`2022-02-16`, `2022-12-10`]} // "YYYY-MM-DD" 68 | // defaultTimes={['10:12']} // OPTIONAL 69 | // enableTimeSelection={true} // OPTIONAL 70 | // handleChooseHourPick={res => console.log(res)} // OPTIONAL 71 | // handleChooseMinutePick={res => console.log(res)} // OPTIONAL 72 | /> 73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |

RangePicker

81 |
82 |
83 |
84 |
85 | console.log(res, 1)} 96 | onClose={() => console.log('closed')} 97 | style={{ width: '300px', margin: '0 auto' }} 98 | // markedDates={[`${todayY}-${todayM}-${todayD - 1}`, `${todayY}-${todayM}-${todayD}`]} // OPTIONAL. ['YYYY-MM-DD'] 99 | // supportDateRange={[`2022-02-16`, `2022-12-10`]} // "YYYY-MM-DD" 100 | // showOnlyTime={true} // default is false 101 | // duration={2} // day count default is 0. End date will be automatically added 2 days when the start date is picked. 102 | /> 103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | ); 111 | }; 112 | 113 | // 114 | 115 | ReactDOM.render(, document.getElementById('root')); 116 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var ReactMinimalRange = require('./lib/components/index.js'); 2 | exports.CalendarPicker = ReactMinimalRange.CalendarPicker; 3 | exports.RangePicker = ReactMinimalRange.RangePicker; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ['src/**/*.{js,jsx}'], 3 | transform: { 4 | '^.+\\.(js|jsx)$': 'babel-jest', 5 | }, 6 | verbose: true, 7 | moduleNameMapper: { 8 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/assetsTransformer.js', 9 | '\\.(css|less)$': 'identity-obj-proxy', 10 | '^STYLES(.*)$': '/src/css$1', 11 | '^COMPONENTS(.*)$': '/src/js/app/components$1', 12 | '^API(.*)$': '/src/js/api$1', 13 | '^CONFIG(.*)$': '/src/config$1', 14 | '^IMAGES(.*)$': '/src/image$1', 15 | '^AUDIOS(.*)$': '/audio/api$1', 16 | '^VIDEOS(.*)$': '/src/video$1', 17 | '^LOCALES(.*)$': '/src/locales$1', 18 | '^COMMON(.*)$': '/src/js/common$1', 19 | '^APP(.*)$': '/src/js/app$1', 20 | '^CONSTS(.*)$': '/src/js/consts$1', 21 | '^PAGES(.*)$': '/src/js/api$1', 22 | '^ACTIONS(.*)$': '/src/js/actions$1', 23 | '^STORE(.*)$': '/src/js/store$1', 24 | '^REDUCERS(.*)$': '/src/js/reducers$1', 25 | '^VENDOR(.*)$': '/src/js/vendor$1', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-minimal-datetime-range", 3 | "version": "2.1.0", 4 | "description": "A react component for date time range picker.", 5 | "main": "index.js", 6 | "types": "./lib/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/edwardfxiao/react-minimal-datetime-range.git" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "minimal", 14 | "range", 15 | "picky", 16 | "date", 17 | "time", 18 | "picker", 19 | "datepicker", 20 | "date-picker", 21 | "timepicker", 22 | "time-picker", 23 | "calendar", 24 | "react-minimal-datetime-range" 25 | ], 26 | "author": "Edward Xiao", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/edwardfxiao/react-minimal-datetime-range/issues" 30 | }, 31 | "homepage": "https://edwardfxiao.github.io/react-minimal-datetime-range", 32 | "scripts": { 33 | "tslint": "tslint -c tslint.json 'src/**/*.{ts,tsx}'", 34 | "build_gh_page": "rm -rf lib && rm -rf dist && NODE_ENV=production ./node_modules/.bin/webpack --config ./webpack/production.config.babel.js --progress", 35 | "umd_local": "./node_modules/.bin/webpack --config ./webpack/umd.local.config.babel.js", 36 | "umd_global": "./node_modules/.bin/webpack --config ./webpack/umd.global.config.babel.js", 37 | "umd_global_min": "./node_modules/.bin/webpack --config ./webpack/umd.global.config.babel.js --env minify", 38 | "dev": "node_modules/.bin/webpack-dev-server --config ./webpack/development.config.babel.js", 39 | "compile": "rimraf dist lib && npm run umd_global && npm run umd_global_min && npm run umd_local && rm ./lib/components/*.css" 40 | }, 41 | "peerDependencies": { 42 | "react": ">= 16.8.6", 43 | "react-dom": ">= 16.8.6", 44 | "react-transition-group": ">= 4.2.2" 45 | }, 46 | "devDependencies": { 47 | "@babel/cli": "^7.17.0", 48 | "@babel/core": "^7.17.2", 49 | "@babel/eslint-parser": "^7.17.0", 50 | "@babel/plugin-proposal-class-properties": "^7.16.7", 51 | "@babel/preset-env": "^7.16.11", 52 | "@babel/preset-react": "^7.16.7", 53 | "@babel/register": "^7.17.0", 54 | "@swc/core": "^1.2.139", 55 | "@swc/wasm": "^1.2.139", 56 | "@types/jest": "^27.4.0", 57 | "@types/react": "^16.8.14", 58 | "@types/react-dom": "^16.8.4", 59 | "@types/react-transition-group": "^4.4.4", 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 | "postcss-css-variables": "^0.17.0", 84 | "postcss-custom-properties": "^9.1.1", 85 | "postcss-import": "^14.0.2", 86 | "postcss-loader": "^6.2.0", 87 | "postcss-preset-env": "^7.3.1", 88 | "postcss-simple-vars": "^6.0.3", 89 | "prismjs": "^1.16.0", 90 | "react": "^16.8.6", 91 | "react-dom": "^16.8.6", 92 | "react-markdown": "^8.0.0", 93 | "react-transition-group": "^4.4.2", 94 | "regenerator-runtime": "^0.13.9", 95 | "rimraf": "^3.0.2", 96 | "ts-jest": "^27.1.3", 97 | "ts-loader": "^9.2.6", 98 | "ts-node": "^10.5.0", 99 | "typescript": "^4.5.5", 100 | "url-loader": "^4.1.1", 101 | "utf-8-validate": "^5.0.8", 102 | "webpack": "^5.68.0", 103 | "webpack-assets-manifest": "^5.1.0", 104 | "webpack-cli": "^4.9.2", 105 | "webpack-dev-server": "^4.7.4" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /react-minimal-datetime-range.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardfxiao/react-minimal-datetime-range/022d6855c44633164fbeba265f986c6cb1cc39f7/react-minimal-datetime-range.gif -------------------------------------------------------------------------------- /src/css/example.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .wrapper { 6 | padding: 20px; 7 | } 8 | 9 | .nav { 10 | padding: 20px; 11 | background-color: #ececec; 12 | } 13 | 14 | .nav a { 15 | color: #006fb4; 16 | } 17 | 18 | .example-section { 19 | padding: 10px; 20 | margin-bottom: 10px; 21 | display: flex; 22 | justify-content: center; 23 | } 24 | 25 | :global .submit-btn { 26 | margin-top: 20px; 27 | -moz-box-shadow: inset 0px 1px 0px 0px #54a3f7; 28 | -webkit-box-shadow: inset 0px 1px 0px 0px #54a3f7; 29 | box-shadow: inset 0px 1px 0px 0px #54a3f7; 30 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0.05, #007dc1), color-stop(1, #0061a7)); 31 | background: -moz-linear-gradient(top, #007dc1 5%, #0061a7 100%); 32 | background: -webkit-linear-gradient(top, #007dc1 5%, #0061a7 100%); 33 | background: -o-linear-gradient(top, #007dc1 5%, #0061a7 100%); 34 | background: -ms-linear-gradient(top, #007dc1 5%, #0061a7 100%); 35 | background: linear-gradient(to bottom, #007dc1 5%, #0061a7 100%); 36 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#007dc1', endColorstr='#0061a7', GradientType=0); 37 | background-color: #007dc1; 38 | -moz-border-radius: 3px; 39 | -webkit-border-radius: 3px; 40 | border-radius: 3px; 41 | border: 1px solid #124d77; 42 | display: inline-block; 43 | cursor: pointer; 44 | color: #ffffff; 45 | font-family: Arial; 46 | font-size: 13px; 47 | padding: 6px 24px; 48 | text-decoration: none; 49 | text-shadow: 0px 1px 0px #154682; 50 | } 51 | 52 | :global .submit-btn:hover { 53 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0.05, #0061a7), color-stop(1, #007dc1)); 54 | background: -moz-linear-gradient(top, #0061a7 5%, #007dc1 100%); 55 | background: -webkit-linear-gradient(top, #0061a7 5%, #007dc1 100%); 56 | background: -o-linear-gradient(top, #0061a7 5%, #007dc1 100%); 57 | background: -ms-linear-gradient(top, #0061a7 5%, #007dc1 100%); 58 | background: linear-gradient(to bottom, #0061a7 5%, #007dc1 100%); 59 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0061a7', endColorstr='#007dc1', GradientType=0); 60 | background-color: #0061a7; 61 | } 62 | 63 | :global .submit-btn:active { 64 | position: relative; 65 | top: 1px; 66 | } 67 | 68 | @media only screen and (max-width: 1000px) { 69 | .example-section { 70 | display: block; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/html/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.title %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

react-minimal-datetime-range

16 | fork me on Github@edwardfxiao
17 |
18 |
19 |
20 | <%= htmlWebpackPlugin.options.customJs %> 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/js/component/Calendar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useMemo, useCallback, memo } from 'react'; 2 | import { CSSTransition, TransitionGroup } from 'react-transition-group'; 3 | import LOCALE from './locale'; 4 | import { WEEK_NUMBER, PREV_TRANSITION, NEXT_TRANSITION, SELECTOR_YEAR_SET_NUMBER, getDaysArray, getYearSet, formatDateString } from './const'; 5 | import { cx, isValidDate } from './utils'; 6 | interface IObjectKeysAny { 7 | [key: string]: any; 8 | } 9 | interface IObjectKeysBool { 10 | [key: string]: boolean; 11 | } 12 | interface IObjectKeysArray { 13 | [key: string]: Array; 14 | } 15 | const TODAY = new Date(); 16 | const YEAR = TODAY.getFullYear(); 17 | const MONTH = TODAY.getMonth() + 1; 18 | const DATE = TODAY.getDate(); 19 | 20 | const ITEM_HEIGHT = 40; 21 | 22 | interface IndexProps { 23 | locale?: string; 24 | defaultDate?: string; 25 | markedDates?: Array; 26 | supportDateRange?: Array; 27 | onYearPicked?: (res: object) => void; 28 | onMonthPicked?: (res: object) => void; 29 | onDatePicked?: (res: object) => void; 30 | onResetDate?: (res: object) => void; 31 | onResetDefaultDate?: (res: object) => void; 32 | } 33 | const Index: React.FC = memo( 34 | ({ 35 | locale = 'en-us', 36 | defaultDate = '', 37 | markedDates = [], 38 | supportDateRange = [], 39 | onYearPicked = () => {}, 40 | onMonthPicked = () => {}, 41 | onDatePicked = () => {}, 42 | onResetDate = () => {}, 43 | onResetDefaultDate = () => {}, 44 | }) => { 45 | const markedDatesHash: IObjectKeysBool = useMemo(() => { 46 | const res: IObjectKeysBool = {}; 47 | if (markedDates && markedDates.length) { 48 | let isValid = true; 49 | for (let i = 0; i < markedDates.length; i += 1) { 50 | if (!isValidDate(markedDates[i])) { 51 | isValid = false; 52 | break; 53 | } 54 | } 55 | if (isValid) { 56 | markedDates.forEach(d => { 57 | res[d] = true; 58 | }); 59 | } 60 | } 61 | return res; 62 | }, [markedDates]); 63 | const LOCALE_DATA: IObjectKeysAny = useMemo(() => (LOCALE[locale] ? LOCALE[locale] : LOCALE['en-us']), [locale]); 64 | let defaultDateDate = DATE; 65 | let defaultDateMonth = MONTH; 66 | let defaultDateYear = YEAR; 67 | let defaultDates = getDaysArray(YEAR, MONTH); 68 | const isDefaultDateValid = useMemo(() => isValidDate(defaultDate), [defaultDate]); 69 | if (isDefaultDateValid) { 70 | const dateStr = defaultDate.split('-'); 71 | defaultDateYear = Number(dateStr[0]); 72 | defaultDateMonth = Number(dateStr[1]); 73 | defaultDateDate = Number(dateStr[2]); 74 | defaultDates = getDaysArray(defaultDateYear, defaultDateMonth); 75 | } 76 | const defaultYearStr = String(defaultDateYear); 77 | const defaultMonthStr = formatDateString(defaultDateMonth); 78 | const defaultDateStr = formatDateString(defaultDateDate); 79 | const [dates, setDates] = useState(defaultDates); 80 | const [pickedYearMonth, setPickedYearMonth] = useState({ 81 | year: defaultYearStr, 82 | month: defaultMonthStr, 83 | string: `${defaultYearStr}-${defaultMonthStr}`, 84 | }); 85 | const [defaultDateObj, setDefaultDateObj] = useState({ 86 | year: defaultYearStr, 87 | month: defaultMonthStr, 88 | date: defaultDateStr, 89 | }); 90 | const [pickedDateInfo, setPickedDateInfo] = useState({ 91 | year: defaultYearStr, 92 | month: defaultMonthStr, 93 | date: defaultDateStr, 94 | }); 95 | const [direction, setDirection] = useState(NEXT_TRANSITION); 96 | const [yearSelectorPanelList, setYearSelectorPanelList] = useState(getYearSet(defaultDateYear)); 97 | const [yearSelectorPanel, setYearSelectorPanel] = useState(defaultDateYear); 98 | const [showMask, setShowMask] = useState(false); 99 | const [showSelectorPanel, setShowSelectorPanel] = useState(false); 100 | const $monthSelectorPanel = useRef(null); 101 | const onMouseDown = useCallback(() => {}, []); 102 | const onMouseUp = useCallback(() => {}, []); 103 | useEffect(() => { 104 | setDates(getDaysArray(Number(pickedYearMonth.year), Number(pickedYearMonth.month))); 105 | }, [pickedYearMonth]); 106 | const minSupportDate = supportDateRange.length > 0 && isValidDate(supportDateRange[0]) ? supportDateRange[0] : ''; 107 | const maxSupportDate = supportDateRange.length > 1 && isValidDate(supportDateRange[1]) ? supportDateRange[1] : ''; 108 | const pickYear = useCallback( 109 | (year, direction) => { 110 | year = Number(year); 111 | if (direction === PREV_TRANSITION) { 112 | year = year - 1; 113 | } else { 114 | year = year + 1; 115 | } 116 | setPickedYearMonth({ ...pickedYearMonth, year, string: `${year}-${pickedYearMonth.month}` }); 117 | setDirection(direction); 118 | onYearPicked({ year }); 119 | }, 120 | [pickedYearMonth], 121 | ); 122 | const pickMonth = useCallback( 123 | (month, direction) => { 124 | month = Number(month); 125 | let year = Number(pickedYearMonth.year); 126 | if (direction === PREV_TRANSITION) { 127 | if (month === 1) { 128 | month = 12; 129 | year = year - 1; 130 | } else { 131 | month = month - 1; 132 | } 133 | } else { 134 | if (month === 12) { 135 | month = 1; 136 | year = year + 1; 137 | } else { 138 | month = month + 1; 139 | } 140 | } 141 | const yearStr = String(year); 142 | const monthStr = formatDateString(month); 143 | setPickedYearMonth({ ...pickedYearMonth, year: yearStr, month: monthStr, string: `${yearStr}-${monthStr}` }); 144 | setDirection(direction); 145 | onMonthPicked({ year: yearStr, month: monthStr }); 146 | }, 147 | [pickedYearMonth], 148 | ); 149 | const pickDate = useCallback( 150 | pickedDate => { 151 | const newPickedDateInfo = { 152 | ...pickedDateInfo, 153 | year: pickedYearMonth.year, 154 | month: pickedYearMonth.month, 155 | date: formatDateString(Number(pickedDate)), 156 | }; 157 | setPickedDateInfo(newPickedDateInfo); 158 | onDatePicked(newPickedDateInfo); 159 | }, 160 | [pickedYearMonth, pickedDateInfo], 161 | ); 162 | const reset = useCallback( 163 | (today = false) => { 164 | let year = YEAR; 165 | let month = MONTH; 166 | let date = DATE; 167 | if (!today) { 168 | const dateStr = defaultDate.split('-'); 169 | year = Number(dateStr[0]); 170 | month = Number(dateStr[1]); 171 | date = Number(dateStr[2]); 172 | } 173 | let direction = NEXT_TRANSITION; 174 | if (year < Number(pickedYearMonth.year)) { 175 | direction = PREV_TRANSITION; 176 | } else if (year === Number(pickedYearMonth.year)) { 177 | if (month < Number(pickedYearMonth.month)) { 178 | direction = PREV_TRANSITION; 179 | } 180 | } 181 | const yearStr = formatDateString(year); 182 | const monthStr = formatDateString(month); 183 | const dateStr = formatDateString(date); 184 | setPickedDateInfo({ 185 | ...pickedDateInfo, 186 | year: yearStr, 187 | month: monthStr, 188 | date: dateStr, 189 | }); 190 | setPickedYearMonth({ 191 | ...pickedYearMonth, 192 | year: yearStr, 193 | month: monthStr, 194 | string: `${yearStr}-${monthStr}`, 195 | }); 196 | changeSelectorPanelYearSet(year, direction); 197 | if (!today) { 198 | onResetDefaultDate(pickedDateInfo); 199 | } else { 200 | onResetDate(pickedDateInfo); 201 | } 202 | }, 203 | [pickedYearMonth], 204 | ); 205 | const changeSelectorPanelYearSet = useCallback((yearSelectorPanel, direction) => { 206 | setDirection(direction); 207 | setYearSelectorPanel(yearSelectorPanel); 208 | setYearSelectorPanelList(getYearSet(yearSelectorPanel)); 209 | }, []); 210 | const handleShowSelectorPanel = useCallback(() => { 211 | setShowSelectorPanel(!showSelectorPanel); 212 | setShowMask(!showMask); 213 | }, [showSelectorPanel, showMask]); 214 | let transitionContainerStyle; 215 | let content; 216 | if (dates.length) { 217 | let row = dates.length / WEEK_NUMBER; 218 | let rowIndex = 1; 219 | let rowObj: IObjectKeysArray = {}; 220 | dates.map((item, key) => { 221 | if (key < rowIndex * WEEK_NUMBER) { 222 | if (!rowObj[rowIndex]) { 223 | rowObj[rowIndex] = []; 224 | } 225 | rowObj[rowIndex].push(item); 226 | } else { 227 | rowIndex = rowIndex + 1; 228 | if (!rowObj[rowIndex]) { 229 | rowObj[rowIndex] = []; 230 | } 231 | rowObj[rowIndex].push(item); 232 | } 233 | }); 234 | content = ( 235 | 245 | ); 246 | transitionContainerStyle = { 247 | height: `${row * ITEM_HEIGHT}px`, 248 | }; 249 | } 250 | const captionHtml = LOCALE_DATA.weeks.map((item: string, key: string) => { 251 | return ( 252 |
253 | {item} 254 |
255 | ); 256 | }); 257 | let selectorPanelClass = cx('react-minimal-datetime-range-dropdown', 'react-minimal-datetime-range-calendar__selector-panel', showSelectorPanel && 'visible'); 258 | let selectorPanelMonthHtml = LOCALE_DATA.months.map((item: string, key: string) => { 259 | let itemMonth: number = Number(key) + 1; 260 | const numberMonth = Number(pickedYearMonth.month); 261 | let monthItemClass = cx('react-minimal-datetime-range-dropdown-calendar__month-item', itemMonth === numberMonth && 'active'); 262 | let month = itemMonth - 1; 263 | let direction = NEXT_TRANSITION; 264 | if (itemMonth < numberMonth) { 265 | direction = PREV_TRANSITION; 266 | month = itemMonth + 1; 267 | } 268 | return ( 269 |
pickMonth(month, direction) 274 | : () => { 275 | return; 276 | } 277 | } 278 | key={key} 279 | > 280 |
{item}
281 |
282 | ); 283 | }); 284 | let selectorPanelYearHtml; 285 | if (yearSelectorPanelList.length) { 286 | selectorPanelYearHtml = yearSelectorPanelList.map((item, key) => { 287 | const numberYearMonth = Number(pickedYearMonth.year); 288 | let yearItemClass = cx('react-minimal-datetime-range-dropdown-calendar__year-item', item === numberYearMonth && 'active'); 289 | let year = item - 1; 290 | let direction = NEXT_TRANSITION; 291 | if (item < numberYearMonth) { 292 | direction = PREV_TRANSITION; 293 | year = item + 1; 294 | } 295 | return ( 296 |
pickYear(year, direction) 301 | : () => { 302 | return; 303 | } 304 | } 305 | key={key} 306 | > 307 | {item} 308 |
309 | ); 310 | }); 311 | } 312 | const classNames = direction == NEXT_TRANSITION ? 'forward' : 'backward'; 313 | return ( 314 |
315 |
316 |
317 |
318 |
{selectorPanelMonthHtml}
319 |
320 |
321 | changeSelectorPanelYearSet(yearSelectorPanel - SELECTOR_YEAR_SET_NUMBER, PREV_TRANSITION)} 327 | > 328 | 329 | 330 | 331 |
332 |
333 | React.cloneElement(child, { classNames })}> 334 | 335 |
{selectorPanelYearHtml}
336 |
337 |
338 |
339 |
340 | changeSelectorPanelYearSet(yearSelectorPanel + SELECTOR_YEAR_SET_NUMBER, NEXT_TRANSITION)} 346 | > 347 | 348 | 349 | 350 |
351 |
352 |
353 |
354 |
pickYear(pickedYearMonth.year, PREV_TRANSITION)}> 355 | 356 | 357 | 358 | 359 |
360 |
pickMonth(pickedYearMonth.month, PREV_TRANSITION)}> 361 | 362 | 363 | 364 | 365 |
366 |
367 |
368 | React.cloneElement(child, { classNames })}> 369 | 370 | 371 | {LOCALE_DATA.date_format(LOCALE_DATA.months[Number(pickedYearMonth.month) - 1], pickedYearMonth.year)} 372 | 373 | 374 | 375 |
376 |
377 |
pickMonth(pickedYearMonth.month, NEXT_TRANSITION)}> 378 | 379 | 380 | 381 | 382 |
383 |
pickYear(pickedYearMonth.year, NEXT_TRANSITION)}> 384 | 385 | 386 | 387 | 388 |
389 |
390 |
391 |
392 |
393 |
{captionHtml}
394 |
395 | React.cloneElement(child, { classNames })}> 396 | 397 | {content} 398 | 399 | 400 |
401 |
reset(true)}> 402 | {LOCALE_DATA['today']} 403 | 404 |
405 | {isDefaultDateValid ? ( 406 |
reset(false)}> 407 | {LOCALE_DATA['reset-date']} 408 | 409 |
410 | ) : ( 411 | `` 412 | )} 413 |
414 | ); 415 | }, 416 | ); 417 | interface pickedDateInfo { 418 | date: string; 419 | month: string; 420 | year: string; 421 | } 422 | interface pickedYearMonth { 423 | month: string; 424 | year: string; 425 | } 426 | interface CalendarBodyProps { 427 | data?: IObjectKeysArray; 428 | pickedDateInfo?: pickedDateInfo; 429 | pickedYearMonth?: pickedYearMonth; 430 | markedDates?: Array; 431 | markedDatesHash: IObjectKeysBool; 432 | minSupportDate: string; 433 | maxSupportDate: string; 434 | onClick?: (res: string) => void; 435 | } 436 | const CalendarBody: React.FC = memo(({ data = {}, pickedDateInfo = {}, pickedYearMonth = {}, onClick = () => {}, markedDatesHash = {}, minSupportDate, maxSupportDate }) => { 437 | const pickedDate = `${Number(pickedDateInfo.year)}-${Number(pickedDateInfo.month)}-${Number(pickedDateInfo.date)}`; 438 | const content = Object.keys(data).map(key => { 439 | let colHtml; 440 | if (data[key].length) { 441 | colHtml = data[key].map((item: { [k: string]: any }, key: any) => { 442 | const itemDate = `${Number(item.year)}-${Number(item.month)}-${Number(item.name)}`; 443 | const isPicked = itemDate === pickedDate; 444 | let isDisabled = pickedYearMonth.month !== item.month; 445 | if (minSupportDate) { 446 | if (new Date(itemDate) < new Date(minSupportDate)) { 447 | isDisabled = true; 448 | } 449 | } 450 | if (maxSupportDate) { 451 | if (new Date(itemDate) > new Date(maxSupportDate)) { 452 | isDisabled = true; 453 | } 454 | } 455 | const datePickerItemClass = cx( 456 | 'react-minimal-datetime-range-calendar__table-cel', 457 | 'react-minimal-datetime-range-calendar__date-item', 458 | isDisabled && 'disabled', 459 | DATE == item.name && MONTH == item.month && YEAR == item.year && 'today', 460 | markedDatesHash[`${item.year}-${item.month}-${item.name}`] && 'marked', 461 | isPicked && 'active', 462 | ); 463 | return ; 464 | }); 465 | } 466 | return ( 467 |
468 | {colHtml} 469 |
470 | ); 471 | }); 472 | return
{content}
; 473 | }); 474 | interface CalendarItemProps { 475 | item?: IObjectKeysAny; 476 | isPicked?: boolean; 477 | isDisabled?: boolean; 478 | datePickerItemClass?: string; 479 | onClick?: (res: string) => void; 480 | } 481 | const CalendarItem: React.FC = memo(({ item = {}, isPicked = false, isDisabled = false, datePickerItemClass = '', onClick = () => {} }) => { 482 | const handleOnClick = useCallback(() => { 483 | onClick(item.name); 484 | }, [item.name]); 485 | return ( 486 |
{ 492 | return; 493 | } 494 | } 495 | > 496 | {item.name} 497 | {isPicked && ( 498 | 499 | 500 | 501 | 502 | )} 503 |
504 | ); 505 | }); 506 | 507 | export default Index; 508 | -------------------------------------------------------------------------------- /src/js/component/RangeDate.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useMemo, useCallback, memo } from 'react'; 2 | import { CSSTransition, TransitionGroup } from 'react-transition-group'; 3 | import LOCALE from './locale'; 4 | import { WEEK_NUMBER, PREV_TRANSITION, NEXT_TRANSITION, SELECTOR_YEAR_SET_NUMBER, getDaysArray, getYearSet, formatDateString, isWith1Month, getEndDateItemByDuration } from './const'; 5 | import { cx, isValidDate } from './utils'; 6 | 7 | const TODAY = new Date(); 8 | const YEAR = TODAY.getFullYear(); 9 | const MONTH = TODAY.getMonth() + 1; 10 | const DATE = TODAY.getDate(); 11 | 12 | const ITEM_HEIGHT = 40; 13 | 14 | interface IObjectKeysAny { 15 | [key: string]: any; 16 | } 17 | interface IObjectKeysBool { 18 | [key: string]: boolean; 19 | } 20 | interface IObjectKeysArray { 21 | [key: string]: Array; 22 | } 23 | interface IndexProps { 24 | selected: boolean; 25 | setSelected: (res: boolean) => void; 26 | locale?: string; 27 | defaultDateStart?: string; 28 | defaultDateEnd?: string; 29 | rangeDirection?: string; 30 | startDatePickedArray?: Array; 31 | endDatePickedArray?: Array; 32 | markedDates?: Array; 33 | supportDateRange?: Array; 34 | duration?: number; 35 | handleChooseStartDate?: (res: object) => void; 36 | handleChooseEndDate?: (res: object) => void; 37 | currentDateObjStart?: IObjectKeysAny; 38 | setCurrentDateObjStart?: (res: object) => void; 39 | currentDateObjEnd?: IObjectKeysAny; 40 | setCurrentDateObjEnd?: (res: object) => void; 41 | onChooseDate?: (res: object) => void; 42 | } 43 | const Index: React.FC = memo( 44 | ({ 45 | selected, 46 | setSelected, 47 | locale = 'en-us', 48 | defaultDateStart = '', 49 | defaultDateEnd = '', 50 | rangeDirection = 'start', 51 | startDatePickedArray = [], // [YY, MM, DD] 52 | endDatePickedArray = [], // [YY, MM, DD] 53 | handleChooseStartDate = () => {}, 54 | handleChooseEndDate = () => {}, 55 | currentDateObjStart = {}, 56 | currentDateObjEnd = {}, 57 | setCurrentDateObjStart = () => {}, 58 | setCurrentDateObjEnd = () => {}, 59 | markedDates = [], 60 | supportDateRange = [], 61 | duration = 0, 62 | onChooseDate = () => {}, 63 | }) => { 64 | const markedDatesHash: IObjectKeysBool = useMemo(() => { 65 | const res: IObjectKeysBool = {}; 66 | if (markedDates && markedDates.length) { 67 | let isValid = true; 68 | for (let i = 0; i < markedDates.length; i += 1) { 69 | if (!isValidDate(markedDates[i])) { 70 | isValid = false; 71 | break; 72 | } 73 | } 74 | if (isValid) { 75 | markedDates.forEach(d => { 76 | res[d] = true; 77 | }); 78 | } 79 | } 80 | return res; 81 | }, [markedDates]); 82 | const LOCALE_DATA: IObjectKeysAny = useMemo(() => (LOCALE[locale] ? LOCALE[locale] : LOCALE['en-us']), [locale]); 83 | let defaultDateDateStart = DATE; 84 | let defaultDateMonthStart = MONTH; 85 | let defaultDateYearStart = YEAR; 86 | 87 | let defaultDateDateEnd = defaultDateDateStart; 88 | let defaultDateMonthEnd; 89 | let defaultDateYearEnd = defaultDateYearStart; 90 | 91 | if (defaultDateMonthStart === 12) { 92 | defaultDateMonthEnd = 1; 93 | defaultDateYearEnd = defaultDateYearStart + 1; 94 | } else { 95 | defaultDateMonthEnd = defaultDateMonthStart + 1; 96 | } 97 | 98 | const isDefaultDateValidStart = useMemo(() => isValidDate(defaultDateStart), [defaultDateStart]); 99 | if (isDefaultDateValidStart) { 100 | const dateStr = defaultDateStart.split('-'); 101 | defaultDateYearStart = Number(dateStr[0]); 102 | defaultDateMonthStart = Number(dateStr[1]); 103 | defaultDateDateStart = Number(dateStr[2]); 104 | } 105 | const isDefaultDateValidEnd = useMemo(() => isValidDate(defaultDateEnd), [defaultDateEnd]); 106 | if (isDefaultDateValidEnd) { 107 | const dateStr = defaultDateEnd.split('-'); 108 | defaultDateYearEnd = Number(dateStr[0]); 109 | defaultDateMonthEnd = Number(dateStr[1]); 110 | defaultDateDateEnd = Number(dateStr[2]); 111 | // special handle 112 | if (defaultDateMonthStart === 12) { 113 | defaultDateMonthEnd = 1; 114 | defaultDateYearEnd = defaultDateYearStart + 1; 115 | } else { 116 | defaultDateMonthEnd = defaultDateMonthStart + 1; 117 | } 118 | } 119 | 120 | let showPrevYearArrow = true; 121 | let showPrevMonthArrow = true; 122 | let showNextYearArrow = true; 123 | let showNextMonthArrow = true; 124 | 125 | if (currentDateObjStart.string && currentDateObjEnd.string) { 126 | if (rangeDirection === 'start') { 127 | if (isWith1Month(currentDateObjStart.year, currentDateObjEnd.year, currentDateObjStart.month, currentDateObjEnd.month, 'add')) { 128 | showNextYearArrow = false; 129 | showNextMonthArrow = false; 130 | } 131 | } else { 132 | if (isWith1Month(currentDateObjEnd.year, currentDateObjStart.year, currentDateObjEnd.month, currentDateObjStart.month, 'minus')) { 133 | showPrevYearArrow = false; 134 | showPrevMonthArrow = false; 135 | } 136 | } 137 | } 138 | 139 | const defaultDatesStart = getDaysArray(defaultDateYearStart, defaultDateMonthStart); 140 | const defaultDatesEnd = getDaysArray(defaultDateYearEnd, defaultDateMonthEnd); 141 | 142 | let defaultDateMonth: number; 143 | let defaultDateDate; 144 | let defaultDateYear: number; 145 | let defaultDates; 146 | let defaultYearStr; 147 | let defaultMonthStr; 148 | let defaultDateStr; 149 | 150 | if (rangeDirection === 'start') { 151 | defaultDateMonth = defaultDateMonthStart; 152 | defaultDateDate = defaultDateDateStart; 153 | defaultDateYear = defaultDateYearStart; 154 | defaultDates = defaultDatesStart; 155 | defaultYearStr = formatDateString(defaultDateYearStart); 156 | defaultMonthStr = formatDateString(defaultDateMonthStart); 157 | defaultDateStr = formatDateString(defaultDateDateStart); 158 | } else { 159 | defaultDateMonth = defaultDateMonthEnd; 160 | defaultDateDate = defaultDateDateEnd; 161 | defaultDateYear = defaultDateYearEnd; 162 | defaultDates = defaultDatesEnd; 163 | defaultYearStr = formatDateString(defaultDateYearEnd); 164 | defaultMonthStr = formatDateString(defaultDateMonthEnd); 165 | defaultDateStr = formatDateString(defaultDateDateEnd); 166 | } 167 | 168 | useEffect(() => { 169 | if (rangeDirection === 'start') { 170 | setCurrentDateObjStart({ year: defaultDateYear, month: defaultDateMonth, string: `${defaultDateYear}-${defaultDateMonth}` }); 171 | } else { 172 | setCurrentDateObjEnd({ year: defaultDateYear, month: defaultDateMonth, string: `${defaultDateYear}-${defaultDateMonth}` }); 173 | } 174 | }, [rangeDirection, defaultDateYear, defaultDateMonth]); 175 | 176 | const [dates, setDates] = useState(defaultDates); 177 | const [pickedYearMonth, setPickedYearMonth] = useState({ 178 | year: defaultYearStr, 179 | month: defaultMonthStr, 180 | string: `${defaultYearStr}-${defaultMonthStr}`, 181 | }); 182 | const [defaultDateObj, setDefaultDateObj] = useState({ 183 | year: defaultYearStr, 184 | month: defaultMonthStr, 185 | date: defaultDateStr, 186 | }); 187 | const [pickedDateInfo, setPickedDateInfo] = useState({ 188 | year: defaultYearStr, 189 | month: defaultMonthStr, 190 | date: defaultDateStr, 191 | }); 192 | const [direction, setDirection] = useState(NEXT_TRANSITION); 193 | const [yearSelectorPanelList, setYearSelectorPanelList] = useState(getYearSet(defaultDateYear)); 194 | const [yearSelectorPanel, setYearSelectorPanel] = useState(defaultDateYear); 195 | const [showMask, setShowMask] = useState(false); 196 | const [showSelectorPanel, setShowSelectorPanel] = useState(false); 197 | const $monthSelectorPanel = useRef(null); 198 | const onMouseDown = useCallback(() => {}, []); 199 | const onMouseUp = useCallback(() => {}, []); 200 | useEffect(() => { 201 | setDates(getDaysArray(Number(pickedYearMonth.year), Number(pickedYearMonth.month))); 202 | }, [pickedYearMonth]); 203 | const minSupportDate = supportDateRange.length > 0 && isValidDate(supportDateRange[0]) ? supportDateRange[0] : ''; 204 | const maxSupportDate = supportDateRange.length > 1 && isValidDate(supportDateRange[1]) ? supportDateRange[1] : ''; 205 | const pickYear = useCallback( 206 | (year, direction) => { 207 | year = Number(year); 208 | if (direction === PREV_TRANSITION) { 209 | year = year - 1; 210 | } else { 211 | year = year + 1; 212 | } 213 | const newData = { ...pickedYearMonth, year, string: `${year}-${pickedYearMonth.month}` }; 214 | setPickedYearMonth(newData); 215 | if (rangeDirection === 'start') { 216 | setCurrentDateObjStart(newData); 217 | } else { 218 | setCurrentDateObjEnd(newData); 219 | } 220 | setDirection(direction); 221 | }, 222 | [pickedYearMonth], 223 | ); 224 | const pickMonth = useCallback( 225 | (month, direction) => { 226 | month = Number(month); 227 | let year = Number(pickedYearMonth.year); 228 | if (direction === PREV_TRANSITION) { 229 | if (month === 1) { 230 | month = 12; 231 | year = year - 1; 232 | } else { 233 | month = month - 1; 234 | } 235 | } else { 236 | if (month === 12) { 237 | month = 1; 238 | year = year + 1; 239 | } else { 240 | month = month + 1; 241 | } 242 | } 243 | const yearStr = String(year); 244 | const monthStr = formatDateString(month); 245 | const newData = { ...pickedYearMonth, year: yearStr, month: monthStr, string: `${yearStr}-${monthStr}` }; 246 | setPickedYearMonth(newData); 247 | if (rangeDirection === 'start') { 248 | setCurrentDateObjStart(newData); 249 | } else { 250 | setCurrentDateObjEnd(newData); 251 | } 252 | setDirection(direction); 253 | }, 254 | [pickedYearMonth], 255 | ); 256 | const pickDate = useCallback(pickedDate => {}, []); 257 | const changeSelectorPanelYearSet = useCallback((yearSelectorPanel, direction) => { 258 | setDirection(direction); 259 | setYearSelectorPanel(yearSelectorPanel); 260 | setYearSelectorPanelList(getYearSet(yearSelectorPanel)); 261 | }, []); 262 | const handleShowSelectorPanel = useCallback(() => { 263 | setShowSelectorPanel(!showSelectorPanel); 264 | setShowMask(!showMask); 265 | }, [showSelectorPanel, showMask]); 266 | let transitionContainerStyle; 267 | let content; 268 | if (dates.length) { 269 | let row = dates.length / WEEK_NUMBER; 270 | let rowIndex = 1; 271 | let rowObj: IObjectKeysArray = {}; 272 | dates.map((item, key) => { 273 | if (key < rowIndex * WEEK_NUMBER) { 274 | if (!rowObj[rowIndex]) { 275 | rowObj[rowIndex] = []; 276 | } 277 | rowObj[rowIndex].push(item); 278 | } else { 279 | rowIndex = rowIndex + 1; 280 | if (!rowObj[rowIndex]) { 281 | rowObj[rowIndex] = []; 282 | } 283 | rowObj[rowIndex].push(item); 284 | } 285 | }); 286 | content = ( 287 | 306 | ); 307 | transitionContainerStyle = { 308 | height: `${row * ITEM_HEIGHT}px`, 309 | }; 310 | } 311 | const captionHtml = LOCALE_DATA.weeks.map((item: string, key: string) => { 312 | return ( 313 |
314 | {item} 315 |
316 | ); 317 | }); 318 | let selectorPanelClass = cx('react-minimal-datetime-range-dropdown', 'react-minimal-datetime-range-calendar__selector-panel', showSelectorPanel && 'visible'); 319 | let selectorPanelMonthHtml = LOCALE_DATA.months.map((item: string, key: string) => { 320 | let itemMonth: number = Number(key) + 1; 321 | const numberMonth = Number(pickedYearMonth.month); 322 | let monthItemClass = cx('react-minimal-datetime-range-dropdown-calendar__month-item', itemMonth == numberMonth && 'active'); 323 | let month = itemMonth - 1; 324 | let direction = NEXT_TRANSITION; 325 | if (itemMonth < numberMonth) { 326 | direction = PREV_TRANSITION; 327 | month = itemMonth + 1; 328 | } 329 | return ( 330 |
pickMonth(month, direction) 335 | : () => { 336 | return; 337 | } 338 | } 339 | key={key} 340 | > 341 |
{item}
342 |
343 | ); 344 | }); 345 | let selectorPanelYearHtml; 346 | if (yearSelectorPanelList.length) { 347 | selectorPanelYearHtml = yearSelectorPanelList.map((item, key) => { 348 | const numberYearMonth = Number(pickedYearMonth.year); 349 | let yearItemClass = cx('react-minimal-datetime-range-dropdown-calendar__year-item', item == numberYearMonth && 'active'); 350 | let year = item - 1; 351 | let direction = NEXT_TRANSITION; 352 | if (item < numberYearMonth) { 353 | direction = PREV_TRANSITION; 354 | year = item + 1; 355 | } 356 | return ( 357 |
pickYear(year, direction) 362 | : () => { 363 | return; 364 | } 365 | } 366 | key={key} 367 | > 368 | {item} 369 |
370 | ); 371 | }); 372 | } 373 | const classNames = direction == NEXT_TRANSITION ? 'forward' : 'backward'; 374 | return ( 375 |
376 |
377 |
378 |
379 |
{selectorPanelMonthHtml}
380 |
381 |
382 | changeSelectorPanelYearSet(yearSelectorPanel - SELECTOR_YEAR_SET_NUMBER, PREV_TRANSITION)} 388 | > 389 | 390 | 391 | 392 |
393 |
394 | React.cloneElement(child, { classNames })}> 395 | 396 |
{selectorPanelYearHtml}
397 |
398 |
399 |
400 |
401 | changeSelectorPanelYearSet(yearSelectorPanel + SELECTOR_YEAR_SET_NUMBER, NEXT_TRANSITION)} 407 | > 408 | 409 | 410 | 411 |
412 |
413 |
414 |
415 | {showPrevYearArrow && ( 416 |
pickYear(pickedYearMonth.year, PREV_TRANSITION)}> 417 | 418 | 419 | 420 | 421 |
422 | )} 423 | {showPrevMonthArrow && ( 424 |
pickMonth(pickedYearMonth.month, PREV_TRANSITION)}> 425 | 426 | 427 | 428 | 429 |
430 | )} 431 |
432 |
433 | React.cloneElement(child, { classNames })}> 434 | 435 | 436 | {LOCALE_DATA.date_format(LOCALE_DATA.months[Number(pickedYearMonth.month) - 1], pickedYearMonth.year)} 437 | 438 | 439 | 440 |
441 |
442 | {showNextMonthArrow && ( 443 |
pickMonth(pickedYearMonth.month, NEXT_TRANSITION)}> 444 | 445 | 446 | 447 | 448 |
449 | )} 450 | {showNextYearArrow && ( 451 |
pickYear(pickedYearMonth.year, NEXT_TRANSITION)}> 452 | 453 | 454 | 455 | 456 |
457 | )} 458 |
459 |
460 |
461 |
462 |
{captionHtml}
463 |
464 | React.cloneElement(child, { classNames })}> 465 | 466 | {content} 467 | 468 | 469 |
470 |
471 | ); 472 | }, 473 | ); 474 | interface pickedDateInfo { 475 | date: string; 476 | month: string; 477 | year: string; 478 | } 479 | interface pickedYearMonth { 480 | month: string; 481 | year: string; 482 | } 483 | interface CalendarBodyProps { 484 | selected: boolean; 485 | setSelected: (res: boolean) => void; 486 | startDatePickedArray: Array; 487 | endDatePickedArray: Array; 488 | handleChooseStartDate: (res: object) => void; 489 | handleChooseEndDate: (res: object) => void; 490 | rangeDirection: string; 491 | data?: IObjectKeysArray; 492 | pickedDateInfo?: pickedDateInfo; 493 | pickedYearMonth?: pickedYearMonth; 494 | markedDates?: Array; 495 | duration?: number; 496 | markedDatesHash: IObjectKeysBool; 497 | minSupportDate: string; 498 | maxSupportDate: string; 499 | onClick?: (res: string) => void; 500 | onChooseDate?: (res: object) => void; 501 | } 502 | const CalendarBody: React.FC = memo( 503 | ({ 504 | selected, 505 | setSelected, 506 | startDatePickedArray, 507 | endDatePickedArray, 508 | handleChooseStartDate, 509 | handleChooseEndDate, 510 | rangeDirection, 511 | data = {}, 512 | pickedDateInfo = {}, 513 | pickedYearMonth = {}, 514 | markedDatesHash = {}, 515 | minSupportDate, 516 | maxSupportDate, 517 | duration, 518 | onChooseDate, 519 | }) => { 520 | const content = Object.keys(data).map(key => { 521 | let colHtml; 522 | if (data[key].length) { 523 | colHtml = data[key].map((item: { [k: string]: any }, key: any) => { 524 | const itemDate = new Date(`${item.year}-${item.month}-${item.name}`); 525 | let isDisabled = pickedYearMonth.month !== item.month; 526 | if (minSupportDate) { 527 | if (new Date(itemDate) < new Date(minSupportDate)) { 528 | isDisabled = true; 529 | } 530 | } 531 | if (maxSupportDate) { 532 | if (new Date(itemDate) > new Date(maxSupportDate)) { 533 | isDisabled = true; 534 | } 535 | } 536 | let isPickedStart = false; 537 | let isPickedEnd = false; 538 | let isHighlight = false; 539 | if (isDisabled === false) { 540 | let starts = startDatePickedArray; 541 | let ends = endDatePickedArray; 542 | if (startDatePickedArray.length && endDatePickedArray.length) { 543 | const a = new Date(startDatePickedArray.join('-')); 544 | const b = new Date(endDatePickedArray.join('-')); 545 | starts = a < b ? startDatePickedArray : endDatePickedArray; 546 | ends = a > b ? startDatePickedArray : endDatePickedArray; 547 | } 548 | if (starts.length) { 549 | isPickedStart = starts[0] === item.year && starts[1] === item.month && starts[2] === item.name; 550 | const targetDate = new Date(starts.join('-')); 551 | if (!ends.length) { 552 | if (itemDate > targetDate) { 553 | isHighlight = true; 554 | } 555 | } else { 556 | if (itemDate > targetDate && itemDate < new Date(ends.join('-'))) { 557 | isHighlight = true; 558 | } 559 | } 560 | } 561 | if (ends.length) { 562 | isPickedEnd = ends[0] === item.year && ends[1] === item.month && ends[2] === item.name; 563 | } 564 | } 565 | const datePickerItemClass = cx( 566 | 'react-minimal-datetime-range-calendar__table-cel', 567 | 'react-minimal-datetime-range-calendar__date-item', 568 | 'range', 569 | isDisabled && 'disabled', 570 | isPickedStart && 'active', 571 | isPickedEnd && 'active', 572 | isHighlight && 'highlight', 573 | DATE == item.name && MONTH == item.month && YEAR == item.year && 'today', 574 | markedDatesHash[`${item.year}-${item.month}-${item.name}`] && 'marked', 575 | ); 576 | return ( 577 | 591 | ); 592 | }); 593 | } 594 | return ( 595 |
596 | {colHtml} 597 |
598 | ); 599 | }); 600 | return
{content}
; 601 | }, 602 | ); 603 | interface CalendarItemProps { 604 | selected: boolean; 605 | setSelected: (res: boolean) => void; 606 | startDatePickedArray: Array; 607 | endDatePickedArray: Array; 608 | handleChooseStartDate: (res: object) => void; 609 | handleChooseEndDate: (res: object) => void; 610 | item?: IObjectKeysAny; 611 | isPicked?: boolean; 612 | isDisabled?: boolean; 613 | datePickerItemClass?: string; 614 | duration?: number; 615 | onChooseDate?: (res: object) => void; 616 | } 617 | const CalendarItem: React.FC = memo( 618 | ({ 619 | selected, 620 | setSelected, 621 | startDatePickedArray, 622 | endDatePickedArray, 623 | handleChooseStartDate, 624 | handleChooseEndDate, 625 | item = {}, 626 | isDisabled = false, 627 | datePickerItemClass = '', 628 | duration = 0, 629 | onChooseDate, 630 | }) => { 631 | const handleDuration = useCallback(() => { 632 | const endDateItem = getEndDateItemByDuration(item, duration); 633 | handleChooseEndDate(endDateItem); 634 | handleChooseStartDate(item); 635 | setSelected(true); 636 | }, [item, duration]); 637 | const handleOnClick = useCallback(() => { 638 | if (isDisabled) return; 639 | onChooseDate && onChooseDate(item); 640 | if (startDatePickedArray.length) { 641 | setSelected(true); 642 | handleChooseEndDate(item); 643 | } else { 644 | if (duration > 0) { 645 | handleDuration(); 646 | } else { 647 | handleChooseStartDate(item); 648 | return; 649 | } 650 | } 651 | if (selected) { 652 | if (duration > 0) { 653 | handleDuration(); 654 | } else { 655 | handleChooseEndDate({ year: '', month: '', name: '', value: '' }); 656 | handleChooseStartDate(item); 657 | setSelected(false); 658 | } 659 | } 660 | }, [item, selected, startDatePickedArray, endDatePickedArray, duration]); 661 | const handleOnMouseOver = useCallback(() => { 662 | if (duration > 0) return; 663 | if (isDisabled) return; 664 | if (!selected) { 665 | if (startDatePickedArray.length) { 666 | handleChooseEndDate(item); 667 | } 668 | } 669 | }, [item, selected, startDatePickedArray, endDatePickedArray, duration]); 670 | return ( 671 |
672 | {item.name} 673 |
674 | ); 675 | }, 676 | ); 677 | export default Index; 678 | -------------------------------------------------------------------------------- /src/js/component/RangeTime.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { formatDateString } from './const'; 3 | import { cx } from './utils'; 4 | 5 | const HOURS = [...Array(24).keys()]; 6 | const MINUTES = [...Array(60).keys()]; 7 | interface IObjectKeysAny { 8 | [key: string]: any; 9 | } 10 | interface IndexProps { 11 | showOnlyTime: boolean; 12 | LOCALE_DATA: IObjectKeysAny; 13 | singleMode?: boolean; 14 | startDatePickedArray?: Array; 15 | endDatePickedArray?: Array; 16 | startTimePickedArray?: Array; 17 | endTimePickedArray?: Array; 18 | handleChooseStartTimeHour: (res: string) => void; 19 | handleChooseStartTimeMinute: (res: string) => void; 20 | handleChooseEndTimeHour?: (res: string) => void; 21 | handleChooseEndTimeMinute?: (res: string) => void; 22 | } 23 | const Index: React.FC = memo( 24 | ({ 25 | startDatePickedArray, 26 | endDatePickedArray, 27 | handleChooseStartTimeHour, 28 | handleChooseStartTimeMinute, 29 | handleChooseEndTimeHour, 30 | handleChooseEndTimeMinute, 31 | startTimePickedArray, 32 | endTimePickedArray, 33 | showOnlyTime, 34 | LOCALE_DATA, 35 | singleMode = false, 36 | }) => { 37 | if (singleMode) { 38 | return ( 39 |
40 |
41 |
{startDatePickedArray.join('-')}
42 |
43 |
44 | {HOURS.map(i => { 45 | const item = formatDateString(i); 46 | return ( 47 |
handleChooseStartTimeHour(item)}> 48 | {item} 49 |
50 | ); 51 | })} 52 |
53 |
54 | {MINUTES.map(i => { 55 | const item = formatDateString(i); 56 | return ( 57 |
handleChooseStartTimeMinute(item)}> 58 | {item} 59 |
60 | ); 61 | })} 62 |
63 |
64 | ); 65 | } 66 | return ( 67 |
68 |
69 |
{showOnlyTime ? LOCALE_DATA['start'] : startDatePickedArray.join('-')}
70 |
{showOnlyTime ? LOCALE_DATA['end'] : endDatePickedArray.join('-')}
71 |
72 |
73 | {HOURS.map(i => { 74 | const item = formatDateString(i); 75 | return ( 76 |
handleChooseStartTimeHour(item)}> 77 | {item} 78 |
79 | ); 80 | })} 81 |
82 |
83 | {MINUTES.map(i => { 84 | const item = formatDateString(i); 85 | return ( 86 |
handleChooseStartTimeMinute(item)}> 87 | {item} 88 |
89 | ); 90 | })} 91 |
92 |
93 | {HOURS.map(i => { 94 | const item = formatDateString(i); 95 | return ( 96 |
handleChooseEndTimeHour(item)}> 97 | {item} 98 |
99 | ); 100 | })} 101 |
102 |
103 | {MINUTES.map(i => { 104 | const item = formatDateString(i); 105 | return ( 106 |
handleChooseEndTimeMinute(item)}> 107 | {item} 108 |
109 | ); 110 | })} 111 |
112 |
113 | ); 114 | }, 115 | ); 116 | 117 | export default Index; 118 | -------------------------------------------------------------------------------- /src/js/component/ReactMinimalRange.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useMemo, useCallback, memo } from 'react'; 2 | import { cx, isValidDate, isValidDates } from './utils'; 3 | import LOCALE from './locale'; 4 | import Calendar from './Calendar'; 5 | import RangeDate from './RangeDate'; 6 | import RangeTime from './RangeTime'; 7 | import './react-minimal-datetime-range.css'; 8 | const DEFAULT_LACALE = 'en-us'; 9 | interface IObjectKeysAny { 10 | [key: string]: any; 11 | } 12 | export interface CalendarPickerProps { 13 | show?: boolean; 14 | locale?: string; 15 | allowPageClickToClose?: boolean; 16 | defaultDate?: string; 17 | style?: React.CSSProperties; 18 | defaultTimes?: Array; 19 | enableTimeSelection?: boolean; 20 | markedDates?: Array; 21 | supportDateRange?: Array; 22 | duration?: number; 23 | onClose?: () => void; 24 | onYearPicked?: (res: object) => void; 25 | onMonthPicked?: (res: object) => void; 26 | onDatePicked?: (res: object) => void; 27 | onResetDate?: (res: object) => void; 28 | onResetDefaultDate?: (res: object) => void; 29 | handleChooseHourPick?: (res: Array) => void; 30 | handleChooseMinutePick?: (res: Array) => void; 31 | } 32 | export const CalendarPicker: React.FC = memo( 33 | ({ 34 | show = false, 35 | locale = DEFAULT_LACALE, 36 | allowPageClickToClose = true, 37 | defaultDate = '', 38 | style = {}, 39 | defaultTimes = ['', ''], 40 | enableTimeSelection = false, 41 | markedDates = [], 42 | supportDateRange = [], 43 | duration = 0, 44 | onClose = () => {}, 45 | onYearPicked = () => {}, 46 | onMonthPicked = () => {}, 47 | onDatePicked = () => {}, 48 | onResetDate = () => {}, 49 | onResetDefaultDate = () => {}, 50 | handleChooseHourPick = () => {}, 51 | handleChooseMinutePick = () => {}, 52 | }) => { 53 | const [internalShow, setInternalShow] = useState(show); 54 | const handleOnClose = useCallback(() => { 55 | setInternalShow(false); 56 | onClose && onClose(); 57 | }, []); 58 | const handleOnYearPicked = useCallback(yearObj => { 59 | onYearPicked && onYearPicked(yearObj); 60 | }, []); 61 | const handleOnMonthPicked = useCallback(monthObj => { 62 | onMonthPicked && onMonthPicked(monthObj); 63 | }, []); 64 | const handleOnDatePicked = useCallback(dateObj => { 65 | onDatePicked && onDatePicked(dateObj); 66 | }, []); 67 | const handleOnResetDate = useCallback(dateObj => { 68 | onResetDate && onResetDate(dateObj); 69 | }, []); 70 | const handleOnResetDefaultDate = useCallback(dateObj => { 71 | onResetDefaultDate && onResetDefaultDate(dateObj); 72 | }, []); 73 | useEffect(() => { 74 | setInternalShow(show); 75 | }, [show]); 76 | const $elWrapper = useRef(null); 77 | useEffect(() => { 78 | if (typeof window !== 'undefined') { 79 | window.addEventListener('mousedown', pageClick); 80 | window.addEventListener('touchstart', pageClick); 81 | return () => { 82 | window.removeEventListener('mousedown', pageClick); 83 | window.removeEventListener('touchstart', pageClick); 84 | }; 85 | } 86 | }, []); 87 | const pageClick = useCallback( 88 | e => { 89 | if (!allowPageClickToClose) { 90 | return; 91 | } 92 | if ($elWrapper.current.contains(e.target)) { 93 | return; 94 | } 95 | handleOnClose(); 96 | }, 97 | [allowPageClickToClose], 98 | ); 99 | return ( 100 |
101 | {internalShow && ( 102 | 120 | )} 121 |
122 | ); 123 | }, 124 | ); 125 | interface CalendarPickerComponentProps { 126 | show?: boolean; 127 | locale?: string; 128 | allowPageClickToClose?: boolean; 129 | defaultDate?: string; 130 | defaultTimes?: Array; 131 | enableTimeSelection?: boolean; 132 | markedDates?: Array; 133 | supportDateRange?: Array; 134 | duration?: number; 135 | onClose?: () => void; 136 | handleOnYearPicked?: (res: object) => void; 137 | handleOnMonthPicked?: (res: object) => void; 138 | handleOnDatePicked?: (res: object) => void; 139 | handleOnResetDate?: (res: object) => void; 140 | handleOnResetDefaultDate?: (res: object) => void; 141 | handleChooseHourPick?: (res: Array) => void; 142 | handleChooseMinutePick?: (res: Array) => void; 143 | } 144 | const CalendarPickerComponent: React.FC = memo( 145 | ({ 146 | show, 147 | defaultDate, 148 | locale, 149 | defaultTimes, 150 | markedDates, 151 | supportDateRange, 152 | enableTimeSelection, 153 | onClose, 154 | handleOnYearPicked, 155 | handleOnMonthPicked, 156 | handleOnDatePicked, 157 | handleOnResetDate, 158 | handleOnResetDefaultDate, 159 | handleChooseHourPick, 160 | handleChooseMinutePick, 161 | }) => { 162 | const isDefaultDatesValid = isValidDate(defaultDate); 163 | const [internalShow, setInternalShow] = useState(false); 164 | const [type, setType] = useState(TYPES[0]); 165 | const [startDatePickedArray, setStartDatePickedArray] = useState(defaultDate ? defaultDate.split('-') : []); 166 | const [startTimePickedArray, setStartTimePickedArray] = useState([defaultTimes[0].split(':')[0], defaultTimes[0].split(':')[1] || '']); 167 | const [selected, setSelected] = useState(isDefaultDatesValid ? true : false); 168 | const handleChooseStartTimeHour = useCallback( 169 | res => { 170 | setStartTimePickedArray([res, startTimePickedArray[1]]); 171 | handleChooseHourPick(res); 172 | }, 173 | [startTimePickedArray], 174 | ); 175 | const handleChooseStartTimeMinute = useCallback( 176 | res => { 177 | setStartTimePickedArray([startTimePickedArray[0], res]); 178 | handleChooseMinutePick(res); 179 | }, 180 | [startTimePickedArray], 181 | ); 182 | const handleOnClose = useCallback(() => { 183 | setInternalShow(false); 184 | onClose && onClose(); 185 | }, []); 186 | useEffect(() => { 187 | if (show) { 188 | setTimeout(() => { 189 | setInternalShow(true); 190 | }, 0); 191 | } 192 | }, [show]); 193 | const handleOnChangeType = useCallback(() => { 194 | if (type === TYPES[0]) { 195 | setType(TYPES[1]); 196 | } else { 197 | setType(TYPES[0]); 198 | } 199 | }, [type]); 200 | const componentClass = useMemo(() => cx('react-minimal-datetime-range', internalShow && 'visible'), [internalShow]); 201 | const LOCALE_DATA: IObjectKeysAny = useMemo(() => (LOCALE[locale] ? LOCALE[locale] : LOCALE['en-us']), [locale]); 202 | return ( 203 |
204 | 205 | 206 | 207 |
208 |
209 | 220 |
221 | {type === TYPES[1] && ( 222 |
223 | {/* */} 232 |
233 | )} 234 |
235 | {enableTimeSelection && ( 236 |
{}} 239 | style={{ padding: '0', marginTop: '10px' }} 240 | > 241 | {type === TYPES[0] ? LOCALE_DATA[TYPES[1]] : LOCALE_DATA[TYPES[0]]} 242 |
243 | )} 244 |
245 | ); 246 | }, 247 | ); 248 | 249 | const TYPES = ['date', 'time']; 250 | 251 | export interface RangePickerProps { 252 | show?: boolean; 253 | disabled?: boolean; 254 | locale?: string; 255 | allowPageClickToClose?: boolean; 256 | showOnlyTime?: boolean; 257 | defaultDate?: string; 258 | placeholder?: Array; 259 | defaultDates?: Array; 260 | defaultTimes?: Array; 261 | initialDates?: Array; 262 | initialTimes?: Array; 263 | enableTimeSelection?: boolean; 264 | markedDates?: Array; 265 | supportDateRange?: Array; 266 | duration?: number; 267 | style?: React.CSSProperties; 268 | onConfirm?: (res: Array) => void; 269 | onClear?: () => void; 270 | onClose?: () => void; 271 | onChooseDate?: (res: object) => void; 272 | } 273 | export const RangePicker: React.FC = memo( 274 | ({ 275 | show = false, 276 | disabled = false, 277 | locale = DEFAULT_LACALE, 278 | allowPageClickToClose = true, 279 | showOnlyTime = false, 280 | placeholder = ['', ''], 281 | defaultDates = ['', ''], 282 | defaultTimes = ['', ''], 283 | initialDates = ['', ''], 284 | initialTimes = ['', ''], 285 | markedDates = [], 286 | supportDateRange = [], 287 | duration = 0, 288 | style = {}, 289 | onChooseDate = () => {}, 290 | onConfirm = () => {}, 291 | onClear = () => {}, 292 | onClose = () => {}, 293 | }) => { 294 | // ['YYYY-MM-DD', 'YYYY-MM-DD'] // ['hh:mm', 'hh:mm'] 295 | const isDefaultDatesValid = isValidDates(defaultDates); 296 | const isInitialDatesValid = isValidDates(initialDates); 297 | const [selected, setSelected] = useState(isDefaultDatesValid ? true : false); 298 | const [start, setStart] = useState(defaultDates[0] ? `${defaultDates[0]} ${defaultTimes[0] ? defaultTimes[0] : ''}` : ''); 299 | const [end, setEnd] = useState(defaultDates[1] ? `${defaultDates[1]} ${defaultTimes[1] ? defaultTimes[1] : ''}` : ''); 300 | const [type, setType] = useState(TYPES[0]); 301 | const [internalShow, setInternalShow] = useState(show); 302 | const [startDatePickedArray, setStartDatePickedArray] = useState(defaultDates[0] ? defaultDates[0].split('-') : []); 303 | const [endDatePickedArray, setEndDatePickedArray] = useState(defaultDates[1] ? defaultDates[1].split('-') : []); 304 | const [currentDateObjStart, setCurrentDateObjStart] = useState({}); 305 | const [currentDateObjEnd, setCurrentDateObjEnd] = useState({}); 306 | const [startTimePickedArray, setStartTimePickedArray] = useState([defaultTimes[0].split(':')[0], defaultTimes[0].split(':')[1] || '']); 307 | const [endTimePickedArray, setEndTimePickedArray] = useState([defaultTimes[1].split(':')[0], defaultTimes[1].split(':')[1] || '']); 308 | const [dates, setDates] = useState(defaultDates); 309 | const [times, setTimes] = useState(defaultTimes); 310 | const handleChooseStartDate = useCallback( 311 | ({ name, month, year, value }) => { 312 | setDates([value, dates[1]]); 313 | setStartDatePickedArray(value === '' ? [] : [year, month, name]); 314 | }, 315 | [dates], 316 | ); 317 | const handleChooseEndDate = useCallback( 318 | ({ name, month, year, value }) => { 319 | setDates([dates[0], value]); 320 | setEndDatePickedArray(value === '' ? [] : [year, month, name]); 321 | }, 322 | [dates], 323 | ); 324 | const handleChooseStartTimeHour = useCallback( 325 | res => { 326 | setStartTimePickedArray([res, startTimePickedArray[1]]); 327 | }, 328 | [startTimePickedArray], 329 | ); 330 | const handleChooseStartTimeMinute = useCallback( 331 | res => { 332 | setStartTimePickedArray([startTimePickedArray[0], res]); 333 | }, 334 | [startTimePickedArray], 335 | ); 336 | const handleChooseEndTimeHour = useCallback( 337 | res => { 338 | setEndTimePickedArray([res, endTimePickedArray[1]]); 339 | }, 340 | [endTimePickedArray], 341 | ); 342 | const handleChooseEndTimeMinute = useCallback( 343 | res => { 344 | setEndTimePickedArray([endTimePickedArray[0], res]); 345 | }, 346 | [endTimePickedArray], 347 | ); 348 | const handleOnChangeType = useCallback(() => { 349 | if (type === TYPES[0]) { 350 | setType(TYPES[1]); 351 | } else { 352 | setType(TYPES[0]); 353 | } 354 | }, [type]); 355 | const handleOnConfirm = useCallback( 356 | (sd = null, ed = null, st = null, et = null) => { 357 | if (!sd) { 358 | sd = startDatePickedArray; 359 | } 360 | if (!ed) { 361 | ed = endDatePickedArray; 362 | } 363 | if (!st) { 364 | st = startTimePickedArray; 365 | } 366 | if (!et) { 367 | et = endTimePickedArray; 368 | } 369 | const a = new Date(sd.join('-')); 370 | const b = new Date(ed.join('-')); 371 | const starts = a < b ? sd : ed; 372 | const ends = a > b ? sd : ed; 373 | const startStr = `${starts.join('-')} ${st[0] && st[1] ? st.join(':') : ''}`; 374 | const endStr = `${ends.join('-')} ${et[0] && et[1] ? et.join(':') : ''}`; 375 | setStart(startStr); 376 | setEnd(endStr); 377 | setStartDatePickedArray(starts); 378 | setEndDatePickedArray(ends); 379 | setStartTimePickedArray(st); 380 | setEndTimePickedArray(et); 381 | setDates([starts.join('-'), ends.join('-')]); 382 | setInternalShow(false); 383 | onConfirm && onConfirm([startStr, endStr]); 384 | }, 385 | [startDatePickedArray, endDatePickedArray, startTimePickedArray, endTimePickedArray], 386 | ); 387 | const handleOnClear = useCallback( 388 | e => { 389 | if (disabled) { 390 | return; 391 | } 392 | e.stopPropagation(); 393 | if (isInitialDatesValid) { 394 | handleOnConfirm(initialDates[0].split('-'), initialDates[1].split('-'), initialTimes[0].split(':'), initialTimes[1].split(':')); 395 | return; 396 | } 397 | setSelected(false); 398 | setInternalShow(false); 399 | setStart(''); 400 | setEnd(''); 401 | setStartDatePickedArray([]); 402 | setEndDatePickedArray([]); 403 | setDates(['', '']); 404 | setTimes(['', '']); 405 | setStartTimePickedArray(['00', '00']); 406 | setEndTimePickedArray(['00', '00']); 407 | onClear && onClear(); 408 | }, 409 | [disabled, initialDates, initialTimes], 410 | ); 411 | useEffect(() => { 412 | setType(TYPES[0]); 413 | }, [internalShow]); 414 | useEffect(() => { 415 | if (!internalShow) { 416 | onClose && onClose(); 417 | } 418 | }, [internalShow]); 419 | useEffect(() => { 420 | setStart(defaultDates[0] ? `${defaultDates[0]} ${defaultTimes[0] ? defaultTimes[0] : ''}` : ''); 421 | setEnd(defaultDates[1] ? `${defaultDates[1]} ${defaultTimes[1] ? defaultTimes[1] : ''}` : ''); 422 | }, [defaultDates]); 423 | const $elWrapper = useRef(null); 424 | useEffect(() => { 425 | if (typeof window !== 'undefined') { 426 | window.addEventListener('mousedown', pageClick); 427 | window.addEventListener('touchstart', pageClick); 428 | return () => { 429 | window.removeEventListener('mousedown', pageClick); 430 | window.removeEventListener('touchstart', pageClick); 431 | }; 432 | } 433 | }, []); 434 | const pageClick = useCallback( 435 | e => { 436 | if (!allowPageClickToClose) { 437 | return; 438 | } 439 | if ($elWrapper.current.contains(e.target)) { 440 | return; 441 | } 442 | setInternalShow(false); 443 | }, 444 | [allowPageClickToClose], 445 | ); 446 | const isInitial = useMemo(() => start === `${initialDates[0]} ${initialTimes[0]}` && end === `${initialDates[1]} ${initialTimes[1]}`, [initialDates, initialTimes, start, end]); 447 | const isEmpty = useMemo(() => !start && !end, [start, end]); 448 | const valueStart = useMemo(() => (showOnlyTime ? start.split(' ')[1] : start), [showOnlyTime, start]); 449 | const valueEnd = useMemo(() => (showOnlyTime ? end.split(' ')[1] : end), [showOnlyTime, end]); 450 | const handleOnConfirmClick = useCallback(() => { 451 | handleOnConfirm(); 452 | }, [startDatePickedArray, endDatePickedArray, startTimePickedArray, endTimePickedArray]); 453 | return ( 454 |
455 | !disabled && setInternalShow(!internalShow)}> 456 | 457 | ~ 458 | 459 | {!isInitial && !isEmpty ? ( 460 | 461 | 465 | 466 | 467 | ) : ( 468 | 469 | 473 | 474 | 475 | )} 476 | 477 |
478 | {internalShow && ( 479 | 509 | )} 510 |
511 |
512 | ); 513 | }, 514 | ); 515 | 516 | interface RangePickerComponentProps { 517 | show: boolean; 518 | locale: string; 519 | selected: boolean; 520 | setSelected: (res: boolean) => void; 521 | dates: Array; 522 | times: Array; 523 | type: string; 524 | startDatePickedArray: Array; 525 | endDatePickedArray: Array; 526 | startTimePickedArray: Array; 527 | endTimePickedArray: Array; 528 | currentDateObjStart: object; 529 | setCurrentDateObjStart: (res: object) => void; 530 | currentDateObjEnd: object; 531 | setCurrentDateObjEnd: (res: object) => void; 532 | showOnlyTime: boolean; 533 | markedDates: Array; 534 | supportDateRange?: Array; 535 | duration?: number; 536 | handleOnChangeType: () => void; 537 | onChooseDate: (res: object) => void; 538 | handleOnConfirmClick: () => void; 539 | handleChooseStartTimeHour: (res: string) => void; 540 | handleChooseStartTimeMinute: (res: string) => void; 541 | handleChooseEndTimeHour: (res: string) => void; 542 | handleChooseEndTimeMinute: (res: string) => void; 543 | handleChooseStartDate: (res: object) => void; 544 | handleChooseEndDate: (res: object) => void; 545 | } 546 | const RangePickerComponent: React.FC = memo( 547 | ({ 548 | show, 549 | locale, 550 | selected, 551 | setSelected, 552 | dates, 553 | type, 554 | startDatePickedArray, 555 | endDatePickedArray, 556 | startTimePickedArray, 557 | endTimePickedArray, 558 | handleChooseStartDate, 559 | handleChooseEndDate, 560 | currentDateObjStart, 561 | setCurrentDateObjStart, 562 | currentDateObjEnd, 563 | setCurrentDateObjEnd, 564 | showOnlyTime, 565 | markedDates, 566 | supportDateRange, 567 | duration, 568 | onChooseDate, 569 | handleOnChangeType, 570 | handleOnConfirmClick, 571 | handleChooseStartTimeHour, 572 | handleChooseStartTimeMinute, 573 | handleChooseEndTimeHour, 574 | handleChooseEndTimeMinute, 575 | }) => { 576 | const [internalShow, setInternalShow] = useState(false); 577 | useEffect(() => { 578 | if (show) { 579 | setTimeout(() => { 580 | setInternalShow(true); 581 | }, 0); 582 | } 583 | }, [show]); 584 | const componentClass = useMemo(() => cx('react-minimal-datetime-range', internalShow && 'visible'), [internalShow]); 585 | const LOCALE_DATA: IObjectKeysAny = useMemo(() => (LOCALE[locale] ? LOCALE[locale] : LOCALE['en-us']), [locale]); 586 | return ( 587 |
588 |
589 | 609 |
610 | 630 | {(showOnlyTime || type === TYPES[1]) && ( 631 |
632 | 644 |
645 | )} 646 |
647 |
648 | {!showOnlyTime && ( 649 |
{}}> 650 | {type === TYPES[0] ? LOCALE_DATA[TYPES[1]] : LOCALE_DATA[TYPES[0]]} 651 |
652 | )} 653 |
{}}> 654 | {LOCALE_DATA['confirm']} 655 |
656 |
657 |
658 | ); 659 | }, 660 | ); 661 | -------------------------------------------------------------------------------- /src/js/component/const.ts: -------------------------------------------------------------------------------- 1 | export const PREV_TRANSITION = 'prev'; 2 | export const NEXT_TRANSITION = 'next'; 3 | 4 | export const SELECTOR_YEAR_SET_NUMBER = 5; 5 | 6 | export const POINTER_ROTATE = 0; 7 | 8 | export const WEEK_NUMBER = 7; 9 | 10 | export const getDaysArray = (year: number, month: number) => { 11 | let prevMonth; 12 | let nextMonth; 13 | let prevYear; 14 | let nextYear; 15 | if (month === 12) { 16 | prevMonth = 11; 17 | nextMonth = 1; 18 | prevYear = year; 19 | nextYear = year + 1; 20 | } else if (month === 1) { 21 | prevMonth = 12; 22 | nextMonth = 2; 23 | prevYear = year - 1; 24 | nextYear = year; 25 | } else { 26 | prevMonth = month - 1; 27 | nextMonth = month + 1; 28 | prevYear = year; 29 | nextYear = year; 30 | } 31 | const date = new Date(year, month - 1, 1); 32 | let prevMonthDate = null; 33 | let thisMonthDate = null; 34 | let nextMonthDate = null; 35 | let res = []; 36 | let startOffset = date.getDay(); 37 | if (startOffset != 0) { 38 | prevMonthDate = getDaysListByMonth(prevYear, prevMonth); 39 | for (let i = prevMonthDate.length - startOffset; i <= prevMonthDate.length - 1; i++) { 40 | res.push(prevMonthDate[i]); 41 | } 42 | } 43 | thisMonthDate = getDaysListByMonth(year, month); 44 | res = [...res, ...thisMonthDate]; 45 | let endOffset = WEEK_NUMBER - thisMonthDate[thisMonthDate.length - 1].day - 1; 46 | if (endOffset != 0) { 47 | nextMonthDate = getDaysListByMonth(nextYear, nextMonth); 48 | for (let i = 0; i <= endOffset - 1; i++) { 49 | res.push(nextMonthDate[i]); 50 | } 51 | } 52 | return res; 53 | }; 54 | 55 | export const getDaysListByMonth = (year: number, month: number) => { 56 | const date = new Date(year, month - 1, 1); 57 | let res = []; 58 | let stringYear = String(year); 59 | const monthName = formatDateString(month); 60 | while (date.getMonth() == month - 1) { 61 | const dayName = formatDateString(date.getDate()); 62 | let item = { 63 | name: dayName, 64 | day: date.getDay(), 65 | month: monthName, 66 | year: stringYear, 67 | value: `${stringYear}-${monthName}-${dayName}`, 68 | }; 69 | res.push(item); 70 | date.setDate(date.getDate() + 1); 71 | } 72 | return res; 73 | }; 74 | 75 | export const formatDateString = (val: number) => { 76 | if (val < 10) { 77 | return String('0' + val); 78 | } 79 | return String(val); 80 | }; 81 | 82 | export const getYearSet = (year: number) => { 83 | let res = []; 84 | let itemNumber; 85 | let startOffset; 86 | let endOffset; 87 | if (SELECTOR_YEAR_SET_NUMBER % 2 == 1) { 88 | itemNumber = (SELECTOR_YEAR_SET_NUMBER - 1) / 2 + 1; 89 | startOffset = SELECTOR_YEAR_SET_NUMBER - itemNumber; 90 | } else { 91 | itemNumber = SELECTOR_YEAR_SET_NUMBER / 2 - 1; 92 | startOffset = itemNumber - 1; 93 | } 94 | 95 | endOffset = SELECTOR_YEAR_SET_NUMBER - itemNumber; 96 | 97 | for (let i = year - startOffset; i <= year - 1; i++) { 98 | res.push(i); 99 | } 100 | res.push(year); 101 | for (let i = 0; i <= endOffset - 1; i++) { 102 | year = year + 1; 103 | res.push(year); 104 | } 105 | return res; 106 | }; 107 | 108 | // CLOCK 109 | 110 | export const R2D = 180 / Math.PI; 111 | 112 | export const SECOND_DEGREE_NUMBER = 6; 113 | export const MINUTE_DEGREE_NUMBER = 6; 114 | export const HOUR_DEGREE_NUMBER = 30; 115 | 116 | export const QUARTER = [0, 15, 30, 45]; 117 | 118 | export const TIME_SELECTION_FIRST_CHAR_POS_LIST = [0, 3, 6]; 119 | export const TIME_SELECTION_FIRST_CHAR_POS_BACKSPACE_LIST = [1, 4, 7]; 120 | export const TIME_SELECTION_SECOND_CHAR_POS_LIST = [1, 4, 7]; 121 | export const TIME_SELECTION_SECOND_CHAR_POS_BACKSPACE_LIST = [2, 5, 8]; 122 | export const TIME_JUMP_CHAR_POS_LIST = [1, 4, 7]; 123 | export const TIME_CURSOR_POSITION_OBJECT = { 124 | 0: 'clockHandHour', 125 | 1: 'clockHandHour', 126 | 2: 'clockHandHour', 127 | 3: 'clockHandMinute', 128 | 4: 'clockHandMinute', 129 | 5: 'clockHandMinute', 130 | 6: 'clockHandSecond', 131 | 7: 'clockHandSecond', 132 | 8: 'clockHandSecond', 133 | 9: 'meridiem', 134 | 10: 'meridiem', 135 | 11: 'meridiem', 136 | }; 137 | export const TIME_TYPE = ['clockHandHour', 'clockHandMinute', 'clockHandSecond', 'meridiem']; 138 | 139 | export const KEY_CODE = { 140 | '8': 'Backspace', 141 | '46': 'Delete', 142 | '38': 'ArrowUp', 143 | '37': 'ArrowLeft', 144 | '39': 'ArrowRight', 145 | '40': 'ArrowDown', 146 | '48': '0', 147 | '49': '1', 148 | '50': '2', 149 | '51': '3', 150 | '52': '4', 151 | '53': '5', 152 | '54': '6', 153 | '55': '7', 154 | '56': '8', 155 | '57': '9', 156 | }; 157 | // Number(currentDateObjStart.year) === Number(currentDateObjEnd.year) && Number(currentDateObjStart.month) + 1 === Number(currentDateObjEnd.month)) 158 | // Number(currentDateObjEnd.year) === Number(currentDateObjStart.year) && Number(currentDateObjEnd.month) - 1 === Number(currentDateObjStart.month)) 159 | export const isWith1Month = (year1: number, year2: number, month1: number, month2: number, type: string) => { 160 | year1 = year1; 161 | month1 = month1; 162 | year2 = year2; 163 | month2 = month2; 164 | if (type === 'add') { 165 | if (month1 === 12) { 166 | if (year1 + 1 === year2 && month2 === 1) { 167 | return true; 168 | } 169 | } else { 170 | if (year1 === year2 && month1 + 1 === month2) { 171 | return true; 172 | } 173 | } 174 | } else { 175 | if (month1 === 1) { 176 | if (year1 - 1 === year2 && month2 === 12) { 177 | return true; 178 | } 179 | } else { 180 | if (year1 === year2 && month1 - 1 === month2) { 181 | return true; 182 | } 183 | } 184 | } 185 | 186 | return false; 187 | // if (type === 'minus') { 188 | // if (month === 1) { 189 | // res['year'] = year - 1; 190 | // res['month'] = 12; 191 | // } 192 | // } else { 193 | // if (month === 12) { 194 | // res['year'] = year + 1; 195 | // res['month'] = 1; 196 | // } 197 | // } 198 | }; 199 | 200 | interface IObjectKeysAny { 201 | [key: string]: any; 202 | } 203 | 204 | export const getEndDateItemByDuration = (item: IObjectKeysAny, duration: number) => { 205 | const { year, month, name } = item; 206 | const date = new Date(`${year}-${month}-${name}`); 207 | const endDate = new Date(date.getTime() + duration * 24 * 60 * 60 * 1000); 208 | const yearString = String(endDate.getFullYear()); 209 | const monthString = formatDateString(endDate.getMonth() + 1); 210 | const dateString = formatDateString(endDate.getDate()); 211 | const endDateItem = { 212 | year: yearString, 213 | month: monthString, 214 | name: dateString, 215 | day: endDate.getDay(), 216 | value: `${yearString}-${monthString}-${dateString}`, 217 | }; 218 | return endDateItem; 219 | }; 220 | -------------------------------------------------------------------------------- /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 { CalendarPicker, RangePicker } from './ReactMinimalRange'; 2 | if (typeof window !== 'undefined') { 3 | (window).CalendarPicker = CalendarPicker; 4 | (window).RangePicker = RangePicker; 5 | } 6 | 7 | export { CalendarPicker, RangePicker }; 8 | -------------------------------------------------------------------------------- /src/js/component/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ReactMinimalRange'; 2 | -------------------------------------------------------------------------------- /src/js/component/index.umd.js: -------------------------------------------------------------------------------- 1 | import * as Index from './index.js'; 2 | 3 | if (typeof window !== 'undefined') { 4 | window.ReactMinimalDateTimeRange = Index; 5 | } 6 | 7 | export default Index; 8 | -------------------------------------------------------------------------------- /src/js/component/locale.ts: -------------------------------------------------------------------------------- 1 | interface IObjectKeys { 2 | [key: string]: object; 3 | } 4 | let locale: IObjectKeys = { 5 | 'en-us': { 6 | today: 'Today', 7 | reset: 'Reset', 8 | 'reset-date': 'Reset Date', 9 | clear: 'Clear', 10 | now: 'Now', 11 | weeks: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 12 | months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], 13 | date_format: (month: Number, year: Number) => { 14 | return `${month} ${year}`; 15 | }, 16 | date: 'Select date', 17 | time: 'Select time', 18 | confirm: 'Confirm', 19 | start: 'Start', 20 | end: 'End', 21 | }, 22 | 'zh-cn': { 23 | today: '今天', 24 | reset: '重置', 25 | 'reset-date': '重置日期', 26 | clear: '清零', 27 | now: '现在', 28 | weeks: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'], 29 | months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'], 30 | date_format: (month: Number, year: Number) => { 31 | return `${year} ${month}`; 32 | }, 33 | date: '选择日期', 34 | time: '选择时间', 35 | confirm: '确定', 36 | start: '开始', 37 | end: '结束', 38 | }, 39 | 'ko-kr': { 40 | today: '오늘', 41 | reset: '초기화', 42 | 'reset-date': '날짜 초기화', 43 | clear: '지우기', 44 | now: '지금', 45 | weeks: ['일', '월', '화', '수', '목', '금', '토'], 46 | months: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'], 47 | date_format: (month: Number, year: Number) => { 48 | return `${year}년 ${month}`; 49 | }, 50 | date: '날짜 선택', 51 | time: '시간 선택', 52 | confirm: '확인', 53 | start: '시작', 54 | end: '끝', 55 | }, 56 | }; 57 | 58 | const getCustomLocale = (o: any, m: any) => { 59 | if (!o || typeof o !== 'object' || o.constructor !== Object || !Object.keys(o).length) { 60 | console.error('wrong structure'); 61 | return false; 62 | } 63 | Object.keys(o).map(i => { 64 | if (!m[i]) { 65 | m[i] = o[i]; 66 | } else { 67 | if (Object.keys(o[i]).length) { 68 | Object.keys(o[i]).map(j => { 69 | m[i][j] = o[i][j]; 70 | }); 71 | } 72 | } 73 | }); 74 | return m; 75 | }; 76 | 77 | declare global { 78 | interface Window { 79 | REACT_MINIMAL_DATETIME_RANGE: any; 80 | } 81 | } 82 | 83 | const handleCustomLocale = (locale: any, w: Window) => { 84 | let res; 85 | if (typeof w !== 'undefined') { 86 | if (w.REACT_MINIMAL_DATETIME_RANGE && w.REACT_MINIMAL_DATETIME_RANGE['customLocale']) { 87 | res = getCustomLocale(w.REACT_MINIMAL_DATETIME_RANGE['customLocale'], locale); 88 | } 89 | } 90 | if (typeof res === 'undefined' || res === false) { 91 | return locale; 92 | } 93 | return res; 94 | }; 95 | 96 | if (typeof window !== 'undefined') { 97 | window.REACT_MINIMAL_DATETIME_RANGE = window.REACT_MINIMAL_DATETIME_RANGE || {}; 98 | locale = handleCustomLocale(locale, window); 99 | } 100 | 101 | export default locale; 102 | -------------------------------------------------------------------------------- /src/js/component/react-minimal-datetime-range.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --item-width: 25px; 3 | --item-height: 25px; 4 | } 5 | 6 | .react-minimal-datetime-range-calendar--range { 7 | display: inline-block; 8 | vertical-align: top; 9 | } 10 | 11 | .react-minimal-datetime-range { 12 | opacity: 0; 13 | position: relative; 14 | box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.1), 0 0 4px 0 rgba(0, 0, 0, 0.08); 15 | display: inline-block; 16 | transition: all 0.3s; 17 | transform: translateY(-1em) perspective(600px) rotateX(10deg); 18 | padding: 20px; 19 | background-color: #fff; 20 | } 21 | 22 | .react-minimal-datetime-range.visible { 23 | z-index: 1; 24 | opacity: 1; 25 | transform: translateY(0) perspective(600px) rotateX(0); 26 | } 27 | 28 | .react-minimal-datetime-range__calendar { 29 | display: inline-block; 30 | vertical-align: top; 31 | margin-bottom: 40px; 32 | position: relative; 33 | } 34 | 35 | .react-minimal-datetime-range__calendar:before { 36 | content: ''; 37 | display: inline-block; 38 | height: 100%; 39 | vertical-align: middle; 40 | } 41 | 42 | .react-minimal-datetime-range__calendar { 43 | } 44 | 45 | .react-minimal-datetime-range__close { 46 | cursor: pointer; 47 | position: absolute; 48 | top: 10px; 49 | right: 10px; 50 | color: #adb5bd; 51 | } 52 | 53 | .react-minimal-datetime-range__clear.disabled { 54 | cursor: not-allowed; 55 | } 56 | 57 | .react-minimal-datetime-range__clear { 58 | cursor: pointer; 59 | position: absolute; 60 | right: 2%; 61 | top: 50%; 62 | transform: translateY(-50%); 63 | color: #adb5bd; 64 | } 65 | 66 | @media only screen and (max-width: 900px) { 67 | .react-minimal-datetime-range-calendar--range { 68 | display: block; 69 | } 70 | .react-minimal-datetime-date-piker__divider { 71 | display: block; 72 | } 73 | } 74 | 75 | /* dropdown section */ 76 | 77 | .react-minimal-datetime-range-dropdown { 78 | position: relative; 79 | } 80 | 81 | .react-minimal-datetime-range-dropdown.visible .react-minimal-datetime-range-dropdown-calendar__menu { 82 | transform: translate3d(-50%, 0, 0) scale3d(1, 1, 1); 83 | transform: translate(-50%, 0) scale(1, 1) \9; 84 | opacity: 1; 85 | padding: 10px; 86 | z-index: 1000; 87 | } 88 | 89 | .react-minimal-datetime-range-dropdown.visible .react-minimal-datetime-range-dropdown-calendar__menu-no-effect { 90 | display: block; 91 | } 92 | 93 | .react-minimal-datetime-range-dropdown .react-minimal-datetime-range-dropdown-calendar__menu { 94 | will-change: transform, opacity; 95 | transform: translate3d(-50%, 0, 0) scale3d(1, 0, 1); 96 | transform: translate(-50%, 0) scale(1, 0) \9; 97 | opacity: 0; 98 | left: 50%; 99 | width: 280px; 100 | margin-top: 30px; 101 | text-align: center; 102 | transform-origin: 0 0; 103 | transition: transform 0.4s, opacity 0.2s; 104 | position: absolute; 105 | box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.3), 0 0 1px 0 rgba(0, 0, 0, 0.12); 106 | /*z-index: -1;*/ 107 | background-color: #fff; 108 | } 109 | 110 | .react-minimal-datetime-range-dropdown .react-minimal-datetime-range-dropdown-calendar__container { 111 | border-radius: 3px; 112 | overflow: hidden; 113 | margin-top: 10px; 114 | } 115 | 116 | .react-minimal-datetime-range-dropdown .react-minimal-datetime-range-dropdown-calendar__item { 117 | padding: 1px 4px; 118 | line-height: 20px; 119 | transition: background-color 0.4s; 120 | cursor: pointer; 121 | display: block; 122 | } 123 | 124 | .react-minimal-datetime-range-dropdown-calendar__month { 125 | background-color: #fff; 126 | *zoom: 1; 127 | 128 | &:after { 129 | content: '\200B'; 130 | display: block; 131 | height: 0; 132 | clear: both; 133 | } 134 | } 135 | 136 | .react-minimal-datetime-range-dropdown-calendar__month-item { 137 | background-color: #fff; 138 | cursor: pointer; 139 | float: left; 140 | width: 33.3%; 141 | } 142 | 143 | .react-minimal-datetime-range-dropdown-calendar__month-item > div { 144 | padding: 10px 0; 145 | padding: 10px 2px; 146 | font-size: 12px; 147 | margin: 5px; 148 | background-color: #fff; 149 | transition: all 0.3s; 150 | 151 | &:hover { 152 | background-color: #74c0fc; 153 | color: #fff; 154 | } 155 | } 156 | 157 | .react-minimal-datetime-range-dropdown-calendar__month-item.active > div { 158 | background-color: #339af0; 159 | color: #fff; 160 | } 161 | 162 | .react-minimal-datetime-range-calendar__previous, 163 | .react-minimal-datetime-range-calendar__next { 164 | } 165 | 166 | .react-minimal-datetime-range-dropdown .react-minimal-datetime-range-dropdown-calendar__item:hover { 167 | background-color: #eee; 168 | } 169 | 170 | .react-minimal-datetime-range-dropdown-calendar__year { 171 | position: absolute; 172 | overflow: hidden; 173 | width: 100%; 174 | height: 100%; 175 | background-color: #fff; 176 | } 177 | 178 | .react-minimal-datetime-range-dropdown-calendar__year-item { 179 | background-color: #fff; 180 | cursor: pointer; 181 | float: left; 182 | height: 100%; 183 | display: table; 184 | width: 20%; 185 | } 186 | 187 | .react-minimal-datetime-range-dropdown-calendar__year-item > span { 188 | height: 100%; 189 | display: table-cell; 190 | vertical-align: middle; 191 | font-size: 12px; 192 | margin: 2px; 193 | font-size: 14px; 194 | background-color: #fff; 195 | transition: all 0.3s; 196 | 197 | &:hover { 198 | background-color: #74c0fc; 199 | color: #fff; 200 | } 201 | } 202 | 203 | .react-minimal-datetime-range-dropdown-calendar__year-item.active > span { 204 | background-color: #339af0; 205 | color: #fff; 206 | } 207 | 208 | /* end of dropdown section */ 209 | 210 | .react-minimal-datetime-range-calendar__default-day, 211 | .react-minimal-datetime-range-calendar__today { 212 | font-size: 12px; 213 | margin-top: 10px; 214 | } 215 | 216 | .react-minimal-datetime-range-calendar__today { 217 | left: 0; 218 | } 219 | 220 | .react-minimal-datetime-range-calendar__default-day { 221 | right: 0; 222 | } 223 | 224 | .react-minimal-datetime-range-calendar__default-day .react-minimal-datetime-range-calendar__icon, 225 | .react-minimal-datetime-range-calendar__today .react-minimal-datetime-range-calendar__icon { 226 | font-size: 15px; 227 | } 228 | 229 | .react-minimal-datetime-range-calendar__clicker { 230 | cursor: pointer; 231 | } 232 | 233 | .react-minimal-datetime-range__col { 234 | display: inline-block; 235 | vertical-align: middle; 236 | } 237 | 238 | .react-minimal-datetime-range-calendar__title { 239 | cursor: pointer; 240 | width: 100%; 241 | position: absolute; 242 | color: var(--oc-gray-8); 243 | line-height: 17px; 244 | 245 | &:hover { 246 | color: #74c0fc; 247 | } 248 | } 249 | 250 | .react-minimal-datetime-range-calendar__inline-span span { 251 | display: inline-block; 252 | vertical-align: middle; 253 | } 254 | 255 | .react-minimal-datetime-range-calendar__inline-span:before { 256 | content: ''; 257 | display: inline-block; 258 | height: 100%; 259 | vertical-align: middle; 260 | } 261 | 262 | .react-minimal-datetime-range-calendar__content { 263 | } 264 | 265 | .react-minimal-datetime-range-calendar__icon { 266 | cursor: pointer; 267 | font-size: 20px; 268 | } 269 | 270 | .react-minimal-datetime-range__col-0-5 { 271 | width: 5%; 272 | } 273 | 274 | .react-minimal-datetime-range__col-9 { 275 | width: 90%; 276 | } 277 | 278 | .react-minimal-datetime-range__col-3 { 279 | width: 25%; 280 | } 281 | 282 | .react-minimal-datetime-range__col-6 { 283 | width: 50%; 284 | } 285 | 286 | .react-minimal-datetime-range-calendar__header { 287 | text-align: center; 288 | } 289 | 290 | .react-minimal-datetime-range--inline-block { 291 | display: inline-block; 292 | vertical-align: middle; 293 | } 294 | 295 | .react-minimal-datetime-range-calendar__table { 296 | display: table; 297 | border-collapse: collapse; 298 | border-collapse: initial !important\9; 299 | margin: 0 auto; 300 | } 301 | 302 | @media all and (-ms-high-contrast: none) { 303 | .react-minimal-datetime-range-calendar__table { 304 | border-collapse: initial; 305 | } 306 | } 307 | 308 | @supports (-ms-ime-align: auto) { 309 | .react-minimal-datetime-range-calendar__table { 310 | border-collapse: initial; 311 | } 312 | } 313 | 314 | .react-minimal-datetime-range-calendar__table-row { 315 | display: table-row; 316 | } 317 | 318 | .react-minimal-datetime-range-calendar__table-cel { 319 | font-size: 12px; 320 | display: table-cell; 321 | text-align: center; 322 | vertical-align: middle; 323 | padding: 10px; 324 | cursor: default; 325 | transition: all 0.3s; 326 | background-color: #fff; 327 | color: var(--oc-gray-7); 328 | padding: 6px; 329 | width: var(--item-width); 330 | height: var(--item-height); 331 | 332 | &.disabled { 333 | color: #adb5bd; 334 | } 335 | 336 | &.today { 337 | color: #fc7474; 338 | } 339 | &.marked { 340 | position: relative; 341 | &:after { 342 | position: absolute; 343 | content: ''; 344 | width: 5px; 345 | height: 5px; 346 | background-color: #ced4da; 347 | border-radius: 50%; 348 | left: 50%; 349 | bottom: 3px; 350 | transform: translateX(-50%); 351 | } 352 | } 353 | &.active { 354 | &:not(.today) { 355 | color: #fff; 356 | background-color: #74c0fc; 357 | } 358 | 359 | &.range { 360 | &.today { 361 | color: #fff; 362 | background-color: #74c0fc; 363 | text-decoration: underline; 364 | } 365 | } 366 | } 367 | 368 | &.highlight { 369 | background-color: #d0ebff; 370 | } 371 | 372 | &.no-border { 373 | border: 1px solid transparent; 374 | } 375 | 376 | &.react-minimal-datetime-range-calendar__date-item { 377 | position: relative; 378 | 379 | &:not(.disabled) { 380 | cursor: pointer; 381 | 382 | &:hover { 383 | color: #fff; 384 | background-color: #74c0fc; 385 | } 386 | } 387 | } 388 | 389 | &.react-minimal-datetime-range-calendar__date-item .react-minimal-datetime-range-calendar__icon { 390 | position: absolute; 391 | right: 0; 392 | bottom: 0; 393 | font-size: 12px; 394 | } 395 | } 396 | 397 | .react-minimal-datetime-range-calendar__table-caption { 398 | color: var(--oc-gray-7); 399 | } 400 | 401 | .react-minimal-datetime-range-calendar__mask { 402 | opacity: 0; 403 | filter: alpha(opacity=0); 404 | position: absolute; 405 | top: 0; 406 | left: 0; 407 | z-index: -1; 408 | width: 100%; 409 | height: 100%; 410 | background-color: rgba(0, 0, 0, 0.3); 411 | 412 | &.visible { 413 | opacity: 1 !important; 414 | filter: alpha(opacity=100); 415 | background-color: rgba(0, 0, 0, 0.3) !important; 416 | z-index: 1 !important; 417 | } 418 | } 419 | 420 | .react-minimal-datetime-range-check { 421 | position: absolute; 422 | right: 0px; 423 | bottom: 0px; 424 | font-size: 12px; 425 | } 426 | 427 | .react-minimal-datetime-range__icon-fill { 428 | fill: var(--oc-gray-8); 429 | } 430 | 431 | .today.active .react-minimal-datetime-range-check__path { 432 | fill: #fc7474; 433 | } 434 | 435 | .active .react-minimal-datetime-range-check__path, 436 | .today:hover .react-minimal-datetime-range-check__path { 437 | fill: #fff; 438 | } 439 | 440 | .react-minimal-datetime-range-calendar__button { 441 | position: absolute; 442 | bottom: -40px; 443 | display: inline-block; 444 | color: var(--oc-gray-6); 445 | cursor: pointer; 446 | padding: 5px 10px; 447 | border: 1px solid #ced4da; 448 | transition: all 0.3s; 449 | background-color: #fff; 450 | 451 | &:hover { 452 | border: 1px solid #4dabf7; 453 | background-color: #4dabf7; 454 | color: #fff; 455 | } 456 | } 457 | 458 | .forwardEnter { 459 | will-change: transform; 460 | transition: opacity 0.5s ease-in, transform 0.3s; 461 | opacity: 1; 462 | transform: translate3d(100%, 0, 0); 463 | 464 | &.forwardEnterActive { 465 | transform: translate3d(0, 0, 0); 466 | } 467 | } 468 | 469 | .forwardLeave { 470 | opacity: 1; 471 | transition: opacity 0.5s ease-in; 472 | 473 | &.forwardLeaveActive { 474 | opacity: 0; 475 | } 476 | } 477 | 478 | .backwardEnter { 479 | &.backwardEnterActive { 480 | } 481 | } 482 | 483 | .backwardLeave { 484 | will-change: transform, opacity; 485 | transition: transform 0.3s ease-in; 486 | transform: translate3d(100%, 0, 0); 487 | 488 | &.backwardLeaveActive { 489 | } 490 | } 491 | 492 | .react-minimal-datetime-range-calendar__title-container { 493 | position: relative; 494 | display: block; 495 | height: 18px; 496 | overflow: hidden; 497 | width: 100%; 498 | text-align: center; 499 | } 500 | 501 | .react-minimal-datetime-range-calendar__selector-panel-year-set-container { 502 | position: relative; 503 | display: block; 504 | height: 24px; 505 | overflow: hidden; 506 | text-align: center; 507 | width: 100%; 508 | margin: 0 auto; 509 | } 510 | 511 | .react-minimal-datetime-range-calendar__body-container { 512 | position: relative; 513 | display: block; 514 | transition: height 0.3s; 515 | overflow: hidden; 516 | text-align: center; 517 | } 518 | 519 | .slide { 520 | position: absolute; 521 | } 522 | 523 | .slide-enter { 524 | transform: translateX(100%); 525 | transition: 0.3s transform ease-in-out; 526 | } 527 | 528 | .slide-enter-active { 529 | transform: translateX(0); 530 | } 531 | 532 | .slide-exit { 533 | transform: translateX(0); 534 | transition: 0.3s transform ease-in-out; 535 | } 536 | 537 | .slide-exit-active { 538 | transform: translateX(-100%); 539 | } 540 | 541 | .forward-enter { 542 | transform: translateX(100%); 543 | transition: 0.3s transform ease-in-out; 544 | } 545 | 546 | .forward-enter-active { 547 | transform: translateX(0); 548 | } 549 | 550 | .forward-exit { 551 | transform: translateX(0); 552 | transition: 0.3s transform ease-in-out; 553 | } 554 | 555 | .forward-exit-active { 556 | transform: translateX(-100%); 557 | } 558 | 559 | .backward-enter { 560 | transform: translateX(-100%); 561 | transition: 0.3s transform ease-in-out; 562 | } 563 | 564 | .backward-enter-active { 565 | transform: translateX(0); 566 | } 567 | 568 | .backward-exit { 569 | transform: translateX(0); 570 | transition: 0.3s transform ease-in-out; 571 | } 572 | 573 | .backward-exit-active { 574 | transform: translateX(100%); 575 | } 576 | 577 | .react-minimal-datetime-range__range-input-wrapper { 578 | width: 100%; 579 | height: 32px; 580 | position: relative; 581 | display: inline-block; 582 | padding: 4px 0; 583 | color: rgba(0, 0, 0, 0.65); 584 | background-color: #fff; 585 | border: 1px solid #d9d9d9; 586 | border-radius: 4px; 587 | transition: all 0.3s; 588 | } 589 | 590 | .react-minimal-datetime-range__range-input-wrapper.disabled { 591 | border: 1px solid #ccc; 592 | color: #ccc; 593 | background: #f8f8f8; 594 | cursor: not-allowed; 595 | } 596 | 597 | .react-minimal-datetime-range__range-input-wrapper input.react-minimal-datetime-range__range-input.disabled { 598 | cursor: not-allowed; 599 | } 600 | 601 | .react-minimal-datetime-range__range-input-wrapper input.react-minimal-datetime-range__range-input { 602 | border: none; 603 | width: 44%; 604 | height: 95%; 605 | text-align: center; 606 | background-color: transparent; 607 | outline: 0; 608 | } 609 | 610 | .react-minimal-datetime-range__range-input-wrapper .react-minimal-datetime-range__range-input-separator { 611 | display: inline-block; 612 | min-width: 10px; 613 | white-space: nowrap; 614 | text-align: center; 615 | pointer-events: none; 616 | vertical-align: middle; 617 | } 618 | 619 | .react-minimal-datetime-range__range .react-minimal-datetime-range { 620 | position: absolute; 621 | } 622 | 623 | .react-minimal-datetime-range__button-wrapper { 624 | text-align: right; 625 | } 626 | 627 | .react-minimal-datetime-range__button { 628 | font-size: 12px; 629 | cursor: pointer; 630 | display: inline-block; 631 | margin-right: 10px; 632 | padding: 2px 5px; 633 | border-radius: 4px; 634 | } 635 | 636 | .react-minimal-datetime-range__button--type { 637 | color: #74c0fc; 638 | 639 | &.disabled { 640 | color: #adb5bd; 641 | cursor: not-allowed; 642 | } 643 | } 644 | 645 | .react-minimal-datetime-range__button--confirm { 646 | background-color: #74c0fc; 647 | color: #fff; 648 | border: 1px solid #4dabf7; 649 | 650 | &.disabled { 651 | background-color: #f1f3f5; 652 | color: #adb5bd; 653 | cursor: not-allowed; 654 | border: 1px solid #ced4da; 655 | } 656 | } 657 | 658 | .react-minimal-datetime-date-piker { 659 | position: relative; 660 | } 661 | 662 | .react-minimal-datetime-date-piker__divider { 663 | display: inline-block; 664 | width: 20px; 665 | } 666 | 667 | .react-minimal-datetime-range__time-piker { 668 | position: absolute; 669 | left: 0; 670 | right: 0; 671 | top: 0; 672 | bottom: 0; 673 | background-color: #fff; 674 | } 675 | 676 | .react-minimal-datetime-range__time-select-wrapper { 677 | height: 100%; 678 | } 679 | 680 | .react-minimal-datetime-range__date { 681 | display: inline-block; 682 | width: 50%; 683 | text-align: center; 684 | margin-bottom: 10px; 685 | } 686 | 687 | .react-minimal-datetime-range__time-select-options-wrapper { 688 | overflow-y: auto; 689 | height: 85%; 690 | display: inline-block; 691 | width: 25%; 692 | } 693 | 694 | .react-minimal-datetime-range__time-select-wrapper--single { 695 | text-align: center; 696 | } 697 | 698 | .react-minimal-datetime-range__time-select-wrapper--single .react-minimal-datetime-range__time-select-options-wrapper { 699 | width: 50%; 700 | } 701 | 702 | .react-minimal-datetime-range__time-select-option { 703 | padding: 2 5px; 704 | cursor: pointer; 705 | 706 | &:hover { 707 | background-color: #d0ebff; 708 | } 709 | 710 | &.active { 711 | color: #fff; 712 | background-color: #74c0fc; 713 | } 714 | } 715 | -------------------------------------------------------------------------------- /src/js/component/utils.ts: -------------------------------------------------------------------------------- 1 | export const cx = (...params: Array) => { 2 | const classes = []; 3 | for (let i = 0; i < params.length; i += 1) { 4 | const arg = params[i]; 5 | if (!arg) continue; 6 | const argType = typeof arg; 7 | if (argType === 'string' || argType === 'number') { 8 | classes.push(arg); 9 | } else if (Array.isArray(arg) && arg.length) { 10 | const inner: string = cx.apply(null, arg); 11 | if (inner) { 12 | classes.push(inner); 13 | } 14 | } else if (argType === 'object') { 15 | for (const key in arg) { 16 | if ({}.hasOwnProperty.call(arg, key) && arg[key]) { 17 | classes.push(key); 18 | } 19 | } 20 | } 21 | } 22 | return classes.join(' '); 23 | }; 24 | export const isValidDate = (str: string) => { 25 | try { 26 | const d = new Date(str); 27 | if (!isNaN(d.getTime())) { 28 | return true; 29 | } 30 | return false; 31 | } catch (e) { 32 | return false; 33 | } 34 | }; 35 | export const isValidDates = (arr: Array) => { 36 | let isValid = false; 37 | if (arr.length === 2) { 38 | isValid = true; 39 | arr.forEach(v => { 40 | if (!isValidDate(v)) { 41 | isValid = false; 42 | } 43 | }); 44 | } 45 | return isValid; 46 | }; 47 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "stylelint-config-standard", 3 | rules: { 4 | 'block-no-empty': true, 5 | 'color-hex-case': 'lower', 6 | 'color-hex-length': null, 7 | 'color-no-invalid-hex': true, 8 | 'length-zero-no-unit': true, 9 | 'comment-empty-line-before': ['always', { 10 | 'except': ['first-nested'], 11 | 'ignore': ['stylelint-commands', 'between-comments'], 12 | }], 13 | 'declaration-colon-space-after': 'always', 14 | 'max-empty-lines': 2, 15 | 'unit-whitelist': ['em', 'rem', '%', 's', 'ms', 'px', 'deg', 'vw', 'vh', 'dpi', 'dppx'], 16 | 'selector-combinator-space-after': null, 17 | 'selector-pseudo-element-colon-notation': null, 18 | 'selector-list-comma-newline-after': null, 19 | 'comment-empty-line-before': null, 20 | 'block-closing-brace-newline-before': null, 21 | 'number-leading-zero': null, 22 | 'rule-empty-line-before': null, 23 | 'declaration-block-trailing-semicolon': null 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0xb60FCefB862640cbfF058e04CD64B4ed8EaFCA1F' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib/", 4 | "sourceMap": true, 5 | "strictNullChecks": false, 6 | "declaration": true, 7 | "noImplicitAny": true, 8 | "module": "commonjs", 9 | "target": "es5", 10 | "downlevelIteration": true, 11 | "lib": ["es2016", "dom"], 12 | "jsx": "react", 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "strict": true, 16 | }, 17 | "include": ["./src/**/*"] 18 | } 19 | -------------------------------------------------------------------------------- /webpack/base.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import path from 'path'; 3 | import PATH from './build_path'; 4 | import WebpackAssetsManifest from 'webpack-assets-manifest'; 5 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 6 | export default { 7 | context: PATH.ROOT_PATH, 8 | entry: { 9 | index: PATH.ROOT_PATH + 'example/index.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 | -------------------------------------------------------------------------------- /webpack/build_path.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ROOT_PATH = path.join(__dirname, '../'); 3 | const ASSET_PATH = path.join(ROOT_PATH, 'dist'); 4 | const NODE_MODULES_PATH = path.join(ROOT_PATH, './node_modules'); 5 | const HTML_PATH = path.join(ROOT_PATH, './src/html'); 6 | const SOURCE_PATH = path.join(ROOT_PATH, './src'); 7 | 8 | module.exports = { 9 | ROOT_PATH: ROOT_PATH, 10 | ASSET_PATH: ASSET_PATH, 11 | NODE_MODULES_PATH: NODE_MODULES_PATH, 12 | HTML_PATH: HTML_PATH, 13 | SOURCE_PATH: SOURCE_PATH 14 | }; 15 | -------------------------------------------------------------------------------- /webpack/development.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | import ESLintPlugin from 'eslint-webpack-plugin'; 5 | import base from './base.babel.js'; 6 | const PATH = require('./build_path'); 7 | const config = { 8 | ...base, 9 | mode: 'development', 10 | output: { 11 | publicPath: '/', 12 | filename: '[name].js', 13 | }, 14 | }; 15 | config.plugins.push( 16 | new ESLintPlugin({ 17 | context: 'src', 18 | emitWarning: true, 19 | failOnError: false, 20 | exclude: ['data', 'locales'], 21 | extensions: ['airbnb-typescript'], 22 | }), 23 | ); 24 | config.plugins.push( 25 | new MiniCssExtractPlugin({ filename: 'css/[name].css' }), 26 | new HtmlWebpackPlugin({ 27 | template: PATH.HTML_PATH + '/layout.html', 28 | title: 'react-minimal-datetime-range', 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-minimal-datetime-range/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-minimal-datetime-range', 64 | page: 'index', 65 | filename: '../index.html', 66 | hash: false, 67 | chunksSortMode: function(chunk1, chunk2) { 68 | var orders = ['index']; 69 | var order1 = orders.indexOf(chunk1.names[0]); 70 | var order2 = orders.indexOf(chunk2.names[0]); 71 | if (order1 > order2) { 72 | return 1; 73 | } else if (order1 < order2) { 74 | return -1; 75 | } else { 76 | return 0; 77 | } 78 | }, 79 | }), 80 | ); 81 | module.exports = config; 82 | -------------------------------------------------------------------------------- /webpack/umd.base.config.babel.js: -------------------------------------------------------------------------------- 1 | const env = require('yargs').argv.env; // use --env with webpack 2 2 | const path = require('path'); 3 | const PATH = require('./build_path'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | 6 | let libraryName = 'react-minimal-datetime-range'; 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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------