├── .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 | [](#contributors-)
4 |
5 | [](https://badge.fury.io/js/react-minimal-datetime-range)   [](https://raw.githubusercontent.com/edwardfxiao/react-minimal-datetime-range/master/LICENSE) [](https://github.com/996icu/996.ICU/blob/master/LICENSE) [](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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------