├── examples
├── index.styl
├── DateTimePicker
│ ├── index.js
│ ├── Controlled.jsx
│ └── Uncontrolled.jsx
├── DatePicker
│ ├── index.js
│ ├── Controlled.jsx
│ ├── Uncontrolled.jsx
│ ├── Selectable.jsx
│ └── Dropdown.jsx
├── DateTimeRangePicker
│ ├── index.js
│ ├── DateTimeRangePicker.styl
│ ├── Uncontrolled.jsx
│ ├── DateTimeRangePicker.jsx
│ ├── Controlled.jsx
│ ├── DropdownRight.jsx
│ └── Dropdown.jsx
├── Section.jsx
├── Section.styl
├── index.html
├── .stylintrc
├── webpack.config.js
├── Navbar.jsx
├── index.jsx
└── Navbar.styl
├── .npmignore
├── test
└── index.js
├── .gitignore
├── src
├── TimeInput
│ ├── lib
│ │ ├── get-groups.js
│ │ ├── is-twelve-hour-time.js
│ │ ├── zero-pad.js
│ │ ├── validate.js
│ │ ├── replace-char-at.js
│ │ ├── toggle-24-hour.js
│ │ ├── get-base.js
│ │ ├── stringify.js
│ │ ├── caret.js
│ │ ├── get-group-id.js
│ │ └── time-string-adder.js
│ ├── Clock.jsx
│ ├── index.styl
│ └── index.jsx
├── index.js
├── reset-context.styl
├── angle-right.svg
├── angle-left.svg
├── index.styl
├── DateInput
│ ├── Calendar.jsx
│ ├── index.styl
│ └── index.jsx
├── react-datepicker.styl
└── DatePicker.jsx
├── dist
├── react-datepicker.css.map
├── react-datepicker.min.css
└── react-datepicker.css
├── .babelrc
├── .eslintrc
├── docs
├── 35071d00819547a959ef3450c129d77e.eot
├── 3eff5a4e9fb92ca96cbf2fa77649b8c1.ttf
├── 81436770636b45508203b3022075ae73.ttf
├── 8a53d21a4d9aa1aac2bf15093bd748c4.woff
├── d6c7e9d3e5adb7a5261c5ad9f7d3caaa.woff
├── d9c6d360d27eac625da0405245ec9f0d.eot
├── 5ce48c51b8e3b5d666156ff739655a38.svg
├── 1dada8ea59a31dad3dbb76472a3de6ba.svg
├── index.html
└── 37f4597594857b017901209aae0a60e1.svg
├── .travis.yml
├── bower.json
├── scripts
└── bowersync
├── .stylintrc
├── LICENSE
├── webpack.config.js
├── package.json
└── README.md
/examples/index.styl:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | /.nyc_output
3 | /coverage
4 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import { test } from 'tap';
2 |
3 | test('noop', (t) => {
4 | t.end();
5 | });
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | package-lock.json
4 | /.nyc_output
5 | /coverage
6 | /lib
7 |
--------------------------------------------------------------------------------
/examples/DateTimePicker/index.js:
--------------------------------------------------------------------------------
1 | export Controlled from './Controlled';
2 | export Uncontrolled from './Uncontrolled';
3 |
--------------------------------------------------------------------------------
/src/TimeInput/lib/get-groups.js:
--------------------------------------------------------------------------------
1 | module.exports = function getGroups(str) {
2 | return str.split(/[:\s+]/);
3 | };
4 |
--------------------------------------------------------------------------------
/dist/react-datepicker.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"../dist/react-datepicker.css","sourceRoot":""}
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "react"],
3 | "plugins": [
4 | "transform-decorators-legacy"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "trendmicro",
3 | "parser": "babel-eslint",
4 | "env": {
5 | "browser": true,
6 | "node": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs/35071d00819547a959ef3450c129d77e.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trendmicro-frontend/react-datepicker/HEAD/docs/35071d00819547a959ef3450c129d77e.eot
--------------------------------------------------------------------------------
/docs/3eff5a4e9fb92ca96cbf2fa77649b8c1.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trendmicro-frontend/react-datepicker/HEAD/docs/3eff5a4e9fb92ca96cbf2fa77649b8c1.ttf
--------------------------------------------------------------------------------
/docs/81436770636b45508203b3022075ae73.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trendmicro-frontend/react-datepicker/HEAD/docs/81436770636b45508203b3022075ae73.ttf
--------------------------------------------------------------------------------
/docs/8a53d21a4d9aa1aac2bf15093bd748c4.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trendmicro-frontend/react-datepicker/HEAD/docs/8a53d21a4d9aa1aac2bf15093bd748c4.woff
--------------------------------------------------------------------------------
/docs/d6c7e9d3e5adb7a5261c5ad9f7d3caaa.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trendmicro-frontend/react-datepicker/HEAD/docs/d6c7e9d3e5adb7a5261c5ad9f7d3caaa.woff
--------------------------------------------------------------------------------
/docs/d9c6d360d27eac625da0405245ec9f0d.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trendmicro-frontend/react-datepicker/HEAD/docs/d9c6d360d27eac625da0405245ec9f0d.eot
--------------------------------------------------------------------------------
/src/TimeInput/lib/is-twelve-hour-time.js:
--------------------------------------------------------------------------------
1 | module.exports = function isTwelveHourTime(groups) {
2 | return /[a-z]/i.test(groups[groups.length - 1]);
3 | };
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import DatePicker from './DatePicker';
2 |
3 | export DateInput from './DateInput';
4 | export TimeInput from './TimeInput';
5 |
6 | export default DatePicker;
7 |
--------------------------------------------------------------------------------
/src/TimeInput/lib/zero-pad.js:
--------------------------------------------------------------------------------
1 | module.exports = function zeroPad(val, digits) {
2 | while (val.length < digits) {
3 | val = '0' + val;
4 | }
5 | return val;
6 | };
7 |
--------------------------------------------------------------------------------
/src/TimeInput/lib/validate.js:
--------------------------------------------------------------------------------
1 | module.exports = function validate(val) {
2 | return /^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9](:[0-9][0-9][0-9])?)?(\s+[ap]m)?$/i.test(val);
3 | };
4 |
--------------------------------------------------------------------------------
/examples/DatePicker/index.js:
--------------------------------------------------------------------------------
1 | export Controlled from './Controlled';
2 | export Uncontrolled from './Uncontrolled';
3 | export Selectable from './Selectable';
4 | export Dropdown from './Dropdown';
5 |
--------------------------------------------------------------------------------
/src/TimeInput/lib/replace-char-at.js:
--------------------------------------------------------------------------------
1 | module.exports = function replaceCharAt(str, index, replacement) {
2 | str = str.split('');
3 | str[index] = replacement;
4 | return str.join('');
5 | };
6 |
--------------------------------------------------------------------------------
/examples/DateTimeRangePicker/index.js:
--------------------------------------------------------------------------------
1 | export Controlled from './Controlled';
2 | export Uncontrolled from './Uncontrolled';
3 | export Dropdown from './Dropdown';
4 | export DropdownRight from './DropdownRight';
5 |
--------------------------------------------------------------------------------
/src/TimeInput/lib/toggle-24-hour.js:
--------------------------------------------------------------------------------
1 | module.exports = function toggle24Hr(groups) {
2 | const m = groups[groups.length - 1].toUpperCase();
3 | groups[groups.length - 1] = (m === 'AM') ? 'PM' : 'AM';
4 | return groups;
5 | };
6 |
--------------------------------------------------------------------------------
/src/reset-context.styl:
--------------------------------------------------------------------------------
1 | reset-context() {
2 | // https://www.paulirish.com/2012/box-sizing-border-box-ftw/
3 | box-sizing: border-box;
4 | *, *:before, *:after {
5 | box-sizing: inherit;
6 | }
7 |
8 | line-height: 20px;
9 | }
10 |
--------------------------------------------------------------------------------
/src/TimeInput/lib/get-base.js:
--------------------------------------------------------------------------------
1 | module.exports = function getBase(groupId, twelveHourTime) {
2 | if (!groupId) {
3 | return twelveHourTime ? 12 : 24;
4 | }
5 | if (groupId < 3) {
6 | return 60;
7 | }
8 | return 1000;
9 | };
10 |
--------------------------------------------------------------------------------
/src/TimeInput/lib/stringify.js:
--------------------------------------------------------------------------------
1 | import isTwelveHourTime from './is-twelve-hour-time';
2 |
3 | module.exports = function stringify(groups) {
4 | if (isTwelveHourTime(groups)) {
5 | return groups.slice(0, -1).join(':') + ' ' + groups[groups.length - 1];
6 | }
7 | return groups.join(':');
8 | };
9 |
--------------------------------------------------------------------------------
/src/angle-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/TimeInput/lib/caret.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | start: function(el) {
3 | return el.selectionStart;
4 | },
5 | end: function(el) {
6 | return el.selectionEnd;
7 | },
8 | set: function(el, start, end) {
9 | el.setSelectionRange(start, end || start);
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/angle-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 | group: edge
4 |
5 | language: node_js
6 |
7 | os:
8 | - linux
9 |
10 | node_js:
11 | - '8'
12 | - '6'
13 |
14 | before_install:
15 | - npm install -g npm
16 | - npm --version
17 |
18 | after_success:
19 | - npm run coveralls
20 | - npm run coverage-clean
21 |
--------------------------------------------------------------------------------
/docs/5ce48c51b8e3b5d666156ff739655a38.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/1dada8ea59a31dad3dbb76472a3de6ba.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/TimeInput/lib/get-group-id.js:
--------------------------------------------------------------------------------
1 | module.exports = function getGroupId(index) {
2 | if (index < 3) {
3 | return 0;
4 | }
5 | if (index < 6) {
6 | return 1;
7 | }
8 | if (index < 9) {
9 | return 2;
10 | }
11 | if (index < 13) {
12 | return 3;
13 | }
14 | return 4;
15 | };
16 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@trendmicro/react-datepicker",
3 | "description": "React DatePicker Component",
4 | "homepage": "https://github.com/trendmicro-frontend/react-datepicker",
5 | "snapshots": [],
6 | "keywords": [
7 | "react",
8 | "date",
9 | "picker",
10 | "react-datepicker"
11 | ],
12 | "license": "MIT"
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.styl:
--------------------------------------------------------------------------------
1 | @import "reset-context";
2 |
3 | .date-picker-container {
4 | reset-context();
5 |
6 | border: none;
7 | border-radius: 0;
8 | box-shadow: none;
9 | padding: 0 5px;
10 |
11 | position: relative;
12 | display: inline-block;
13 |
14 | :global {
15 | @import "react-datepicker";
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/examples/Section.jsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import React from 'react';
3 | import styles from './Section.styl';
4 |
5 | export default (props) => (
6 |
7 |
8 | {props.children}
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/examples/Section.styl:
--------------------------------------------------------------------------------
1 | .section {
2 | background: #fff;
3 | border: 1px solid #d6d6d6;
4 | position: relative;
5 | z-index: 1;
6 | transition: height .3s ease;
7 | }
8 | .section:last-child {
9 | margin-bottom: 20px;
10 | }
11 | .section-content {
12 | padding: 0 16px 16px;
13 | position: absolute;
14 | top: 0;
15 | bottom: 0;
16 | left: 0;
17 | right: 0;
18 | }
19 | @media screen and (max-width: 1023px) {
20 | .section-content {
21 | position: static;
22 | height: 100%;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React DatePicker
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/scripts/bowersync:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var fs = require('fs');
4 | var path = require('path');
5 | var pkg = require('../package.json');
6 | var bower = require('../bower.json');
7 |
8 | // Update bower.json
9 | Object.keys(bower).forEach((key) => {
10 | bower[key] = pkg[key] || bower[key];
11 | });
12 | bower.authors = pkg.contributors.map(author => {
13 | return {
14 | name: author.name,
15 | email: author.email,
16 | homepage: author.url
17 | };
18 | });
19 |
20 | var content = JSON.stringify(bower, null, 2);
21 | fs.writeFileSync(path.join(__dirname, '../bower.json'), content + '\n', 'utf8');
22 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React DatePicker
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/TimeInput/Clock.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 |
4 | const Clock = ({ width = 16, height = 16, ...props }) => (
5 |
15 | );
16 |
17 | Clock.propTypes = {
18 | width: PropTypes.number,
19 | height: PropTypes.number
20 | };
21 |
22 | export default Clock;
23 |
--------------------------------------------------------------------------------
/docs/37f4597594857b017901209aae0a60e1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
--------------------------------------------------------------------------------
/src/DateInput/Calendar.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 |
4 | const Calendar = ({ width = 16, height = 16, ...props }) => (
5 |
15 | );
16 |
17 | Calendar.propTypes = {
18 | width: PropTypes.number,
19 | height: PropTypes.number
20 | };
21 |
22 | export default Calendar;
23 |
--------------------------------------------------------------------------------
/src/DateInput/index.styl:
--------------------------------------------------------------------------------
1 | @import "../reset-context";
2 |
3 | @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
4 | .date-input > input {
5 | height: 32px;
6 | }
7 | }
8 |
9 | .date-input-container {
10 | reset-context();
11 | position: relative;
12 | }
13 |
14 | .date-input {
15 | width: 120px;
16 |
17 | > input {
18 | display: block;
19 | width: 100%;
20 | height: auto;
21 | line-height: inherit;
22 | padding: 5px 12px;
23 | padding-left: 30px;
24 | font-size: 13px;
25 | color: #222222;
26 | border: 1px solid #ccc;
27 | border-radius: 3px;
28 | outline: none;
29 |
30 | &:focus {
31 | border-color: #0096cc;
32 | }
33 | }
34 | }
35 |
36 | .date-input-icon {
37 | position: absolute;
38 | left: 9px;
39 | top: 8px;
40 | color: #666;
41 | width: 14px;
42 | height: 14px;
43 | }
44 |
--------------------------------------------------------------------------------
/src/TimeInput/index.styl:
--------------------------------------------------------------------------------
1 | @import "../reset-context";
2 |
3 | @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
4 | .time-input > input {
5 | height: 32px;
6 | }
7 | }
8 |
9 | .time-input-container {
10 | reset-context();
11 | position: relative;
12 | }
13 |
14 | .time-input {
15 | width: 120px;
16 |
17 | > input {
18 | display: block;
19 | width: 100%;
20 | height: auto;
21 | line-height: inherit;
22 | padding: 5px 12px;
23 | padding-left: 30px;
24 | font-size: 13px;
25 | color: #222222;
26 | border: 1px solid #ccc;
27 | border-radius: 3px;
28 | outline: none;
29 |
30 | &:focus {
31 | border-color: #0096cc;
32 | }
33 | }
34 | }
35 |
36 | .time-input-icon {
37 | position: absolute;
38 | left: 9px;
39 | top: 8px;
40 | color: #666;
41 | width: 14px;
42 | height: 14px;
43 | }
44 |
--------------------------------------------------------------------------------
/.stylintrc:
--------------------------------------------------------------------------------
1 | //
2 | // https://github.com/rossPatton/stylint
3 | //
4 | {
5 | "blocks": false,
6 | "brackets": "always",
7 | "colons": "always",
8 | "colors": false,
9 | "commaSpace": "always",
10 | "commentSpace": false,
11 | "cssLiteral": "never",
12 | "depthLimit": false,
13 | "duplicates": false,
14 | "efficient": "always",
15 | "extendPref": false,
16 | "globalDupe": false,
17 | "indentPref": false,
18 | "leadingZero": "never",
19 | "maxErrors": false,
20 | "maxWarnings": false,
21 | "mixed": false,
22 | "namingConvention": false,
23 | "namingConventionStrict": false,
24 | "none": "never",
25 | "noImportant": true,
26 | "parenSpace": false,
27 | "placeholders": "always",
28 | "prefixVarsWithDollar": "always",
29 | "quotePref": false,
30 | "semicolons": "always",
31 | "sortOrder": false,
32 | "stackedProperties": "never",
33 | "trailingWhitespace": "never",
34 | "universal": false,
35 | "valid": true,
36 | "zeroUnits": "never",
37 | "zIndexNormalize": false
38 | }
39 |
--------------------------------------------------------------------------------
/examples/.stylintrc:
--------------------------------------------------------------------------------
1 | //
2 | // https://github.com/rossPatton/stylint
3 | //
4 | {
5 | "blocks": false,
6 | "brackets": "always",
7 | "colons": "always",
8 | "colors": false,
9 | "commaSpace": "always",
10 | "commentSpace": false,
11 | "cssLiteral": "never",
12 | "depthLimit": false,
13 | "duplicates": false,
14 | "efficient": "always",
15 | "extendPref": false,
16 | "globalDupe": false,
17 | "indentPref": false,
18 | "leadingZero": "never",
19 | "maxErrors": false,
20 | "maxWarnings": false,
21 | "mixed": false,
22 | "namingConvention": false,
23 | "namingConventionStrict": false,
24 | "none": "never",
25 | "noImportant": false,
26 | "parenSpace": false,
27 | "placeholders": "always",
28 | "prefixVarsWithDollar": "always",
29 | "quotePref": false,
30 | "semicolons": "always",
31 | "sortOrder": false,
32 | "stackedProperties": "never",
33 | "trailingWhitespace": "never",
34 | "universal": false,
35 | "valid": true,
36 | "zeroUnits": "never",
37 | "zIndexNormalize": false
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Trend Micro Inc.
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 |
--------------------------------------------------------------------------------
/examples/DatePicker/Controlled.jsx:
--------------------------------------------------------------------------------
1 | import Anchor from '@trendmicro/react-anchor';
2 | import moment from 'moment';
3 | import PropTypes from 'prop-types';
4 | import React, { PureComponent } from 'react';
5 | import DatePicker from '../../src';
6 |
7 | export default class extends PureComponent {
8 | static propTypes = {
9 | locale: PropTypes.string
10 | };
11 |
12 | state = this.getInitialState();
13 |
14 | getInitialState() {
15 | const now = moment();
16 |
17 | return {
18 | date: moment(now).format('YYYY-MM-DD')
19 | };
20 | }
21 | render() {
22 | const { locale } = this.props;
23 | const { date } = this.state;
24 |
25 | return (
26 |
27 |
Controlled Component
28 |
Selected: {date}
29 |
{
33 | this.setState(state => ({ date: date }));
34 | }}
35 | />
36 |
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/DatePicker/Uncontrolled.jsx:
--------------------------------------------------------------------------------
1 | import Anchor from '@trendmicro/react-anchor';
2 | import moment from 'moment';
3 | import PropTypes from 'prop-types';
4 | import React, { PureComponent } from 'react';
5 | import DatePicker from '../../src';
6 |
7 | export default class extends PureComponent {
8 | static propTypes = {
9 | locale: PropTypes.string
10 | };
11 |
12 | state = this.getInitialState();
13 |
14 | getInitialState() {
15 | const now = moment();
16 |
17 | return {
18 | date: moment(now).format('YYYY-MM-DD')
19 | };
20 | }
21 | render() {
22 | const { locale } = this.props;
23 | const { date } = this.state;
24 |
25 | return (
26 |
27 |
Uncontrolled Component
28 |
Selected: {date}
29 |
{
33 | this.setState(state => ({ date: date }));
34 | }}
35 | />
36 |
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/DateTimeRangePicker/DateTimeRangePicker.styl:
--------------------------------------------------------------------------------
1 | .date-picker-pane {
2 | display: inline-block;
3 |
4 | .date-picker-pane-header,
5 | .date-picker-pane-body,
6 | .date-picker-pane-footer {
7 | white-space: nowrap;
8 | }
9 |
10 | .date-picker-pane-header {
11 | .tilde {
12 | display: inline-block;
13 | line-height: 32px;
14 | text-align: center;
15 | width: 24px;
16 | vertical-align: top;
17 | }
18 | }
19 |
20 | .date-picker-pane-body {
21 | margin-top: 8px;
22 |
23 | .date-picker-pane-container {
24 | display: inline-block;
25 | }
26 |
27 | .date-picker-pane-container + .date-picker-pane-container {
28 | margin-left: 24px;
29 | }
30 | .date-picker-pane-container + .date-picker-pane-container:before {
31 | content: ' ';
32 | display: block;
33 | height: 260px;
34 | vertical-align: middle;
35 | margin-left: -12px;
36 | float: left;
37 | margin-right: 12px;
38 | margin-top: 16px;
39 | border-left: 1px solid #E6E6E6;
40 | }
41 | }
42 |
43 | .date-picker-pane-footer {
44 | margin-top: 12px;
45 | }
46 | }
47 |
48 | .input-icon-group {
49 | position: relative;
50 | display: inline-block;
51 | vertical-align: top;
52 |
53 | + .input-icon-group {
54 | margin-left: 8px;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/examples/DatePicker/Selectable.jsx:
--------------------------------------------------------------------------------
1 | import Anchor from '@trendmicro/react-anchor';
2 | import moment from 'moment';
3 | import PropTypes from 'prop-types';
4 | import React, { PureComponent } from 'react';
5 | import DatePicker from '../../src';
6 |
7 | export default class extends PureComponent {
8 | static propTypes = {
9 | locale: PropTypes.string
10 | };
11 |
12 | state = this.getInitialState();
13 |
14 | getInitialState() {
15 | const now = moment();
16 |
17 | return {
18 | date: moment(now).format('YYYY-MM-DD'),
19 | minDate: moment(now).subtract(7, 'days').format('YYYY-MM-DD'),
20 | maxDate: moment(now).add(7, 'days').format('YYYY-MM-DD')
21 | };
22 | }
23 | render() {
24 | const { locale } = this.props;
25 | const { date, minDate, maxDate } = this.state;
26 |
27 | return (
28 |
29 |
Selectable Date ({minDate} ~ {maxDate})
30 |
Selected: {date}
31 |
{
37 | this.setState(state => ({ date: date }));
38 | }}
39 | />
40 |
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/TimeInput/lib/time-string-adder.js:
--------------------------------------------------------------------------------
1 | import zeroPad from './zero-pad';
2 | import getGroups from './get-groups';
3 | import getBase from './get-base';
4 | import stringify from './stringify';
5 | import toggle24Hr from './toggle-24-hour';
6 | import isTwelveHourTime from './is-twelve-hour-time';
7 |
8 | const add = (groups, groupId, amount, twelveHourTime) => {
9 | const base = getBase(groupId, twelveHourTime);
10 | if (!groupId && groups[groupId] === '12' && twelveHourTime) {
11 | groups[groupId] = '00';
12 | }
13 | const val = Number(groups[groupId]) + amount;
14 | groups = replace(groups, groupId, (val + base) % base);
15 | if (groupId && val >= base) {
16 | return add(groups, groupId - 1, 1, twelveHourTime);
17 | }
18 | if (groupId && val < 0) {
19 | return add(groups, groupId - 1, -1, twelveHourTime);
20 | }
21 | if (!groupId && twelveHourTime) {
22 | if (val >= base || val < 0) {
23 | toggle24Hr(groups);
24 | }
25 | if (groups[0] === '00') {
26 | groups[0] = '12';
27 | }
28 | }
29 |
30 | return groups;
31 | };
32 |
33 | const replace = (groups, groupId, amount) => {
34 | const digits = groups[groupId].length;
35 | groups[groupId] = zeroPad(String(amount), digits);
36 | return groups;
37 | };
38 |
39 | module.exports = function adder(str, groupId, amount) {
40 | const groups = getGroups(str);
41 | const twelveHourTime = isTwelveHourTime(groups);
42 | if (twelveHourTime && (groupId === (groups.length - 1))) {
43 | return stringify(toggle24Hr(groups));
44 | }
45 | return stringify(add(groups, groupId, amount, twelveHourTime));
46 | };
47 |
--------------------------------------------------------------------------------
/examples/DateTimeRangePicker/Uncontrolled.jsx:
--------------------------------------------------------------------------------
1 | import Anchor from '@trendmicro/react-anchor';
2 | import moment from 'moment';
3 | import PropTypes from 'prop-types';
4 | import React, { PureComponent } from 'react';
5 | import DateTimeRangePicker from './DateTimeRangePicker';
6 |
7 | export default class extends PureComponent {
8 | static propTypes = {
9 | locale: PropTypes.string
10 | };
11 |
12 | state = this.getInitialState();
13 |
14 | getInitialState() {
15 | const now = moment();
16 |
17 | return {
18 | minDate: '2000-01-01',
19 | maxDate: moment(now).format('YYYY-MM-DD'),
20 | startDate: moment(now).format('YYYY-MM-DD'),
21 | startTime: moment(now).format('hh:mm:ss'),
22 | endDate: moment(now).add(7, 'days').format('YYYY-MM-DD'),
23 | endTime: moment(now).add(7, 'days').format('hh:mm:ss')
24 | };
25 | }
26 |
27 | render() {
28 | const { locale } = this.props;
29 | const { minDate, maxDate, startDate, startTime, endDate, endTime } = this.state;
30 |
31 | return (
32 |
33 |
Uncontrolled Component
34 |
Note: This example will not update invalid date/time range.
35 |
36 | - Minimum: {minDate}
37 | - Maximum: {maxDate}
38 | - Range: {startDate} {startTime} ~ {endDate} {endTime}
39 |
40 |
49 |
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/examples/DatePicker/Dropdown.jsx:
--------------------------------------------------------------------------------
1 | import '@trendmicro/react-dropdown/dist/react-dropdown.css';
2 | import Anchor from '@trendmicro/react-anchor';
3 | import Dropdown from '@trendmicro/react-dropdown';
4 | import moment from 'moment';
5 | import PropTypes from 'prop-types';
6 | import React, { PureComponent } from 'react';
7 | import DatePicker, { DateInput } from '../../src';
8 |
9 | export default class extends PureComponent {
10 | static propTypes = {
11 | locale: PropTypes.string
12 | };
13 |
14 | state = this.getInitialState();
15 |
16 | getInitialState() {
17 | const now = moment();
18 |
19 | return {
20 | date: moment(now).format('YYYY-MM-DD')
21 | };
22 | }
23 | render() {
24 | const { locale } = this.props;
25 | const { date } = this.state;
26 |
27 | return (
28 |
29 |
Dropdown
30 |
Selected: {date}
31 |
32 |
37 | {
40 | this.setState(state => ({ date: value }));
41 | }}
42 | />
43 |
44 |
45 | {
49 | this.setState(state => ({ date: date }));
50 | }}
51 | />
52 |
53 |
54 |
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/examples/DateTimePicker/Controlled.jsx:
--------------------------------------------------------------------------------
1 | import Anchor from '@trendmicro/react-anchor';
2 | import moment from 'moment';
3 | import PropTypes from 'prop-types';
4 | import React, { PureComponent } from 'react';
5 | import DatePicker, { TimeInput, DateInput } from '../../src';
6 |
7 | export default class extends PureComponent {
8 | static propTypes = {
9 | locale: PropTypes.string
10 | };
11 |
12 | state = this.getInitialState();
13 |
14 | getInitialState() {
15 | const now = moment();
16 |
17 | return {
18 | date: moment(now).format('YYYY-MM-DD'),
19 | time: moment(now).format('hh:mm:ss')
20 | };
21 | }
22 | render() {
23 | const { locale } = this.props;
24 | const { date, time } = this.state;
25 |
26 | return (
27 |
28 |
Controlled Component
29 |
Selected: {date} {time}
30 |
31 |
32 | {
35 | this.setState(state => ({ date: value }));
36 | }}
37 | />
38 |
39 |
40 | {
43 | this.setState(state => ({ time: value }));
44 | }}
45 | />
46 |
47 |
48 |
49 | {
53 | this.setState(state => ({ date: date }));
54 | }}
55 | />
56 |
57 |
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/examples/DateTimePicker/Uncontrolled.jsx:
--------------------------------------------------------------------------------
1 | import Anchor from '@trendmicro/react-anchor';
2 | import moment from 'moment';
3 | import PropTypes from 'prop-types';
4 | import React, { PureComponent } from 'react';
5 | import DatePicker, { TimeInput, DateInput } from '../../src';
6 |
7 | export default class extends PureComponent {
8 | static propTypes = {
9 | locale: PropTypes.string
10 | };
11 |
12 | state = this.getInitialState();
13 |
14 | getInitialState() {
15 | const now = moment();
16 |
17 | return {
18 | date: moment(now).format('YYYY-MM-DD'),
19 | time: moment(now).format('hh:mm:ss')
20 | };
21 | }
22 | render() {
23 | const { locale } = this.props;
24 | const { date, time } = this.state;
25 |
26 | return (
27 |
28 |
Uncontrolled Component
29 |
Selected: {date} {time}
30 |
31 |
32 | {
35 | this.setState(state => ({ date: value }));
36 | }}
37 | />
38 |
39 |
40 | {
43 | this.setState(state => ({ time: value }));
44 | }}
45 | />
46 |
47 |
48 |
49 | {
53 | this.setState(state => ({ date: date }));
54 | }}
55 | />
56 |
57 |
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/react-datepicker.styl:
--------------------------------------------------------------------------------
1 | .react-datepicker {
2 | }
3 |
4 | .react-datepicker__header {
5 | text-align: center;
6 | background-color: #fff;
7 | border: none;
8 | position: relative;
9 | padding: 0;
10 | }
11 |
12 | .react-datepicker__month {
13 | margin: 0;
14 | text-align: center;
15 | }
16 |
17 | .react-datepicker__current-month {
18 | color: #222;
19 | font-weight: bold;
20 | font-size: 13px;
21 | height: 20px;
22 | margin: 8px 0;
23 | }
24 |
25 | .react-datepicker__navigation {
26 | background: none;
27 | line-height: 20px;
28 | text-align: center;
29 | cursor: pointer;
30 | position: absolute;
31 | top: 3px;
32 | padding: 5px;
33 | border: none;
34 | z-index: 1;
35 | outline: 0;
36 | width: 30px;
37 | height: 30px;
38 | background-color: transparent;
39 | background-position: center center;
40 | background-repeat: no-repeat;
41 |
42 | &:hover {
43 | border-radius: 3px;
44 | background-color: #eee;
45 | }
46 | }
47 |
48 | .react-datepicker__navigation--previous {
49 | left: 8px;
50 | background-image: url("./angle-left.svg");
51 | }
52 |
53 | .react-datepicker__navigation--next {
54 | right: 8px;
55 | background-image: url("./angle-right.svg");
56 | }
57 |
58 | .react-datepicker__day,
59 | .react-datepicker__day-name {
60 | color: #222;
61 | display: inline-block;
62 | text-align: center;
63 | width: 30px;
64 | line-height: 20px;
65 | border: 0;
66 | padding: 5px;
67 | margin: 2px;
68 | }
69 |
70 | .react-datepicker__day {
71 | cursor: pointer;
72 | font-size: 13px;
73 |
74 | &:hover {
75 | background: #eee;
76 | cursor: pointer;
77 | border-radius: 50%;
78 | }
79 |
80 | &.react-datepicker__day--disabled,
81 | &:hover.react-datepicker__day--disabled {
82 | background: inherit;
83 | cursor: default;
84 | color: #bbb;
85 | }
86 | }
87 |
88 | .react-datepicker__day-name {
89 | padding-top: 9px;
90 | font-size: 12px;
91 | font-weight: bold;
92 | }
93 |
94 | .react-datepicker__day-names {
95 | white-space: nowrap;
96 | }
97 |
98 | .react-datepicker__day--outside-month {
99 | color: #bbb;
100 | }
101 |
102 | .react-datepicker__day--today {
103 | color: #db3d44;
104 | }
105 |
106 | .react-datepicker__day--selected {
107 | color: #fff;
108 | font-weight: bold;
109 | background-color: #db3d44;
110 | border-radius: 50%;
111 |
112 | &:hover {
113 | background-color: #db3d44;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/DatePicker.jsx:
--------------------------------------------------------------------------------
1 | import cx from 'classnames';
2 | import moment from 'moment';
3 | import PropTypes from 'prop-types';
4 | import React, { PureComponent } from 'react';
5 | import DatePicker from 'react-datepicker';
6 | import { uncontrollable } from 'uncontrollable';
7 | import styles from './index.styl';
8 |
9 | class DatePickerWrapper extends PureComponent {
10 | static propTypes = {
11 | locale: PropTypes.string,
12 |
13 | date: PropTypes.oneOfType([
14 | PropTypes.object,
15 | PropTypes.string
16 | ]),
17 |
18 | // The minimum selectable date. When set to null, there is no minimum.
19 | // Types supported:
20 | // * Date: A date object containing the minimum date.
21 | // * String: A date string in ISO 8601 format (i.e. YYYY-MM-DD).
22 | minDate: PropTypes.oneOfType([
23 | PropTypes.object,
24 | PropTypes.string
25 | ]),
26 |
27 | // The maximum selectable date. When set to null, there is no maximum.
28 | // Types supported:
29 | // * Date: A date object containing the maximum date.
30 | // * String: A date string in ISO 8601 format (i.e. YYYY-MM-DD).
31 | maxDate: PropTypes.oneOfType([
32 | PropTypes.object,
33 | PropTypes.string
34 | ]),
35 |
36 | // Called when a date is selected.
37 | onSelect: PropTypes.func
38 | };
39 | static defaultProps = {
40 | date: null,
41 | minDate: null,
42 | maxDate: null,
43 | onSelect: () => {}
44 | };
45 |
46 | handleSelect = (selected) => {
47 | const date = moment(selected).format('YYYY-MM-DD');
48 | this.props.onSelect && this.props.onSelect(date);
49 | };
50 |
51 | render() {
52 | const {
53 | locale,
54 | date,
55 | minDate,
56 | maxDate,
57 | onSelect, // eslint-disable-line
58 | className,
59 | ...props
60 | } = this.props;
61 |
62 | return (
63 |
74 | );
75 | }
76 | }
77 |
78 | export default uncontrollable(DatePickerWrapper, {
79 | // Define the pairs of prop/handlers you want to be uncontrollable
80 | date: 'onSelect'
81 | });
82 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var pkg = require('./package.json');
2 | var path = require('path');
3 | var webpack = require('webpack');
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 | var findImports = require('find-imports');
6 | var stylusLoader = require('stylus-loader');
7 | var nib = require('nib');
8 | var publicname = pkg.name.replace(/^@\w+\//, ''); // Strip out "@trendmicro/" from package name
9 | var banner = [
10 | publicname + ' v' + pkg.version,
11 | '(c) ' + new Date().getFullYear() + ' Trend Micro Inc.',
12 | pkg.license,
13 | pkg.homepage
14 | ].join(' | ');
15 | var localClassPrefix = publicname.replace(/^react-/, ''); // Strip out "react-" from publicname
16 |
17 | module.exports = {
18 | devtool: 'source-map',
19 | entry: path.resolve(__dirname, 'src/index.js'),
20 | output: {
21 | path: path.join(__dirname, 'lib'),
22 | filename: 'index.js',
23 | libraryTarget: 'commonjs2'
24 | },
25 | externals: []
26 | .concat(findImports(['src/**/*.{js,jsx}'], { flatten: true }))
27 | .concat(Object.keys(pkg.peerDependencies))
28 | .concat(Object.keys(pkg.dependencies)),
29 | module: {
30 | rules: [
31 | // http://survivejs.com/webpack_react/linting_in_webpack/
32 | {
33 | test: /\.jsx?$/,
34 | loader: 'eslint-loader',
35 | enforce: 'pre',
36 | exclude: /node_modules/
37 | },
38 | {
39 | test: /\.styl$/,
40 | loader: 'stylint-loader',
41 | enforce: 'pre'
42 | },
43 | {
44 | test: /\.jsx?$/,
45 | loader: 'babel-loader',
46 | exclude: /(node_modules|bower_components)/
47 | },
48 | {
49 | test: /\.styl$/,
50 | use: ExtractTextPlugin.extract({
51 | fallback: 'style-loader',
52 | use: 'css-loader?camelCase&modules&importLoaders=1&localIdentName=' + localClassPrefix + '---[local]---[hash:base64:5]!stylus-loader'
53 | })
54 | },
55 | {
56 | test: /\.css$/,
57 | loader: 'style-loader!css-loader'
58 | },
59 | {
60 | test: /\.(png|jpg|svg)$/,
61 | loader: 'url-loader'
62 | }
63 | ]
64 | },
65 | plugins: [
66 | new webpack.DefinePlugin({
67 | 'process.env': {
68 | // This has effect on the react lib size
69 | NODE_ENV: JSON.stringify('production')
70 | }
71 | }),
72 | new webpack.NoEmitOnErrorsPlugin(),
73 | new stylusLoader.OptionsPlugin({
74 | default: {
75 | // nib - CSS3 extensions for Stylus
76 | use: [nib()],
77 | // no need to have a '@import "nib"' in the stylesheet
78 | import: ['~nib/lib/nib/index.styl']
79 | }
80 | }),
81 | new ExtractTextPlugin('../dist/' + publicname + '.css'),
82 | new webpack.BannerPlugin(banner)
83 | ],
84 | resolve: {
85 | extensions: ['.js', '.json', '.jsx']
86 | }
87 | };
88 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@trendmicro/react-datepicker",
3 | "version": "1.0.0-alpha.7",
4 | "description": "React DatePicker Component",
5 | "main": "lib/index.js",
6 | "files": [
7 | "dist",
8 | "lib"
9 | ],
10 | "scripts": {
11 | "prepublish": "npm run eslint && npm test && npm run clean && npm run bowersync && npm run build && npm run build-examples",
12 | "bowersync": "./scripts/bowersync",
13 | "build": "webpack && npm run cleancss",
14 | "build-examples": "cd examples; webpack",
15 | "clean": "rm -f {lib,dist}/*",
16 | "cleancss": "cleancss -o dist/react-datepicker.min.css dist/react-datepicker.css",
17 | "demo": "http-server -p 8000 docs/",
18 | "eslint": "eslint --ext .js --ext .jsx examples src test",
19 | "test": "tap test/*.js --node-arg=--require --node-arg=babel-register --node-arg=--require --node-arg=babel-polyfill",
20 | "coveralls": "tap test/*.js --coverage --coverage-report=text-lcov --nyc-arg=--require --nyc-arg=babel-register --nyc-arg=--require --nyc-arg=babel-polyfill | coveralls",
21 | "dev": "cd examples; webpack-dev-server --hot --inline --host 0.0.0.0 --port 8000 --content-base ../docs"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/trendmicro-frontend/react-datepicker.git"
26 | },
27 | "author": "Cheton Wu",
28 | "license": "MIT",
29 | "bugs": {
30 | "url": "https://github.com/trendmicro-frontend/react-datepicker/issues"
31 | },
32 | "homepage": "https://github.com/trendmicro-frontend/react-datepicker",
33 | "keywords": [
34 | "react",
35 | "date",
36 | "picker",
37 | "react-datepicker"
38 | ],
39 | "peerDependencies": {
40 | "moment": ">=2.8.0",
41 | "react": "^0.14.0 || >=15.0.0"
42 | },
43 | "dependencies": {
44 | "classnames": "^2.2.5",
45 | "prop-types": "^15.6.0",
46 | "react-datepicker": "~1.5.0",
47 | "uncontrollable": "^7.0.0"
48 | },
49 | "devDependencies": {
50 | "@trendmicro/react-anchor": "~0.5.6",
51 | "@trendmicro/react-buttons": "~1.3.1",
52 | "@trendmicro/react-dropdown": "~1.4.0",
53 | "babel-cli": "~6.26.0",
54 | "babel-core": "~6.26.0",
55 | "babel-eslint": "~8.2.2",
56 | "babel-loader": "~7.1.4",
57 | "babel-plugin-transform-decorators-legacy": "~1.3.4",
58 | "babel-preset-es2015": "~6.24.1",
59 | "babel-preset-react": "~6.24.1",
60 | "babel-preset-stage-0": "~6.24.1",
61 | "clean-css": "~4.1.11",
62 | "clean-css-cli": "~4.1.11",
63 | "coveralls": "~3.0.0",
64 | "css-loader": "~0.28.0",
65 | "eslint": "~4.18.2",
66 | "eslint-config-trendmicro": "~1.3.0",
67 | "eslint-loader": "~1.7.1",
68 | "eslint-plugin-import": "~2.9.0",
69 | "eslint-plugin-jsx-a11y": "~6.0.3",
70 | "eslint-plugin-react": "~7.7.0",
71 | "extract-text-webpack-plugin": "~3.0.0",
72 | "file-loader": "~0.11.1",
73 | "find-imports": "~0.5.2",
74 | "html-webpack-plugin": "~2.30.1",
75 | "http-server": "~0.11.1",
76 | "moment": "~2.21.0",
77 | "nib": "~1.1.2",
78 | "qs": "~6.5.1",
79 | "react": "^0.14.0 || >=15.0.0",
80 | "react-dom": "^0.14.0 || >=15.0.0",
81 | "recompose": "^0.30.0",
82 | "style-loader": "~0.18.2",
83 | "stylint": "~1.5.9",
84 | "stylint-loader": "~1.0.0",
85 | "stylus-loader": "~3.0.1",
86 | "tap": "~11.1.1",
87 | "trendmicro-ui": "~0.5.1",
88 | "url-loader": "~0.5.8",
89 | "webpack": "~3.4.1",
90 | "webpack-dev-server": "~2.6.1",
91 | "which": "~1.3.0"
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/examples/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const stylusLoader = require('stylus-loader');
5 | const nib = require('nib');
6 |
7 | module.exports = {
8 | devtool: 'source-map',
9 | entry: path.resolve(__dirname, 'index.jsx'),
10 | output: {
11 | path: path.join(__dirname, '../docs'),
12 | filename: 'bundle.js?[hash]'
13 | },
14 | module: {
15 | rules: [
16 | // http://survivejs.com/webpack_react/linting_in_webpack/
17 | {
18 | test: /\.jsx?$/,
19 | loader: 'eslint-loader',
20 | enforce: 'pre',
21 | exclude: /(node_modules)/
22 | },
23 | {
24 | test: /\.styl$/,
25 | loader: 'stylint-loader',
26 | enforce: 'pre'
27 | },
28 | {
29 | test: /\.jsx?$/,
30 | loader: 'babel-loader',
31 | exclude: /(node_modules|bower_components)/
32 | },
33 | {
34 | test: /\.styl$/,
35 | use: [
36 | 'style-loader',
37 | 'css-loader?camelCase&modules&importLoaders=1&localIdentName=[local]---[hash:base64:5]',
38 | 'stylus-loader'
39 | ]
40 | },
41 | {
42 | test: /\.css$/,
43 | loader: 'style-loader!css-loader'
44 | },
45 | {
46 | test: /\.(png|jpg)$/,
47 | loader: 'url-loader',
48 | options: {
49 | limit: 8192
50 | }
51 | },
52 | {
53 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
54 | loader: 'url-loader',
55 | options: {
56 | limit: 10000,
57 | mimetype: 'application/font-woff'
58 | }
59 | },
60 | {
61 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
62 | loader: 'file-loader'
63 | }
64 | ]
65 | },
66 | plugins: [
67 | new webpack.LoaderOptionsPlugin({
68 | debug: true
69 | }),
70 | new webpack.NamedModulesPlugin(),
71 | new webpack.NoEmitOnErrorsPlugin(),
72 | new stylusLoader.OptionsPlugin({
73 | default: {
74 | // nib - CSS3 extensions for Stylus
75 | use: [nib()],
76 | // no need to have a '@import "nib"' in the stylesheet
77 | import: ['~nib/lib/nib/index.styl']
78 | }
79 | }),
80 | new HtmlWebpackPlugin({
81 | filename: '../docs/index.html',
82 | template: 'index.html'
83 | })
84 | ],
85 | resolve: {
86 | extensions: ['.js', '.json', '.jsx']
87 | },
88 | // https://webpack.github.io/docs/webpack-dev-server.html#additional-configuration-options
89 | devServer: {
90 | disableHostCheck: true,
91 | noInfo: false,
92 | lazy: false,
93 | // https://webpack.github.io/docs/node.js-api.html#compiler
94 | watchOptions: {
95 | poll: true, // use polling instead of native watchers
96 | ignored: /node_modules/
97 | }
98 | }
99 | };
100 |
--------------------------------------------------------------------------------
/examples/DateTimeRangePicker/DateTimeRangePicker.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { PureComponent } from 'react';
3 | import { uncontrollable } from 'uncontrollable';
4 | import DatePicker, { TimeInput, DateInput } from '../../src';
5 | import styles from './DateTimeRangePicker.styl';
6 |
7 | class DateTimeRangePicker extends PureComponent {
8 | static propTypes = {
9 | locale: PropTypes.string,
10 | minDate: PropTypes.oneOfType([
11 | PropTypes.object,
12 | PropTypes.string
13 | ]),
14 | maxDate: PropTypes.oneOfType([
15 | PropTypes.object,
16 | PropTypes.string
17 | ]),
18 | startDate: PropTypes.string,
19 | startTime: PropTypes.string,
20 | endDate: PropTypes.string,
21 | endTime: PropTypes.string,
22 | onChangeStartDate: PropTypes.func,
23 | onChangeStartTime: PropTypes.func,
24 | onChangeEndDate: PropTypes.func,
25 | onChangeEndTime: PropTypes.func
26 | };
27 |
28 | render() {
29 | const {
30 | locale,
31 | minDate,
32 | maxDate,
33 | startDate,
34 | startTime,
35 | endDate,
36 | endTime,
37 | onChangeStartDate,
38 | onChangeStartTime,
39 | onChangeEndDate,
40 | onChangeEndTime
41 | } = this.props;
42 |
43 | return (
44 |
45 |
46 |
47 |
53 |
54 |
55 |
59 |
60 |
~
61 |
62 |
68 |
69 |
70 |
74 |
75 |
76 |
77 |
78 |
85 |
86 |
87 |
94 |
95 |
96 |
97 | );
98 | }
99 | }
100 |
101 | export default uncontrollable(DateTimeRangePicker, {
102 | // Define the pairs of prop/handlers you want to be uncontrollable
103 | startDate: 'onChangeStartDate',
104 | startTime: 'onChangeStartTime',
105 | endDate: 'onChangeEndDate',
106 | endTime: 'onChangeEndTime'
107 | });
108 |
--------------------------------------------------------------------------------
/examples/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import PropTypes from 'prop-types';
3 | import React, { Component } from 'react';
4 | import Anchor from '@trendmicro/react-anchor';
5 | import { Button } from '@trendmicro/react-buttons';
6 | import Dropdown, { MenuItem } from '@trendmicro/react-dropdown';
7 | import styles from './Navbar.styl';
8 |
9 | const locales = [
10 | 'en',
11 | 'cs',
12 | 'de',
13 | 'es',
14 | 'fr',
15 | 'it',
16 | 'ja',
17 | 'pt-br',
18 | 'ru',
19 | 'zh-cn',
20 | 'zh-tw'
21 | ];
22 |
23 | const mapLocaleToString = (locale) => {
24 | return {
25 | 'en': 'English',
26 | 'cs': 'Czech',
27 | 'de': 'German',
28 | 'es': 'Spanish',
29 | 'fr': 'French',
30 | 'it': 'Italian',
31 | 'ja': 'Japanese',
32 | 'pt-br': 'Portuguese',
33 | 'ru': 'Russian',
34 | 'zh-cn': 'Simplified Chinese',
35 | 'zh-tw': 'Traditional Chinese'
36 | }[locale];
37 | };
38 |
39 | export default class extends Component {
40 | static propTypes = {
41 | name: PropTypes.string,
42 | url: PropTypes.string,
43 | locale: PropTypes.string,
44 | changeLocale: PropTypes.func
45 | };
46 |
47 | state = {
48 | collapseIn: false
49 | };
50 |
51 | render() {
52 | const { name, url } = this.props;
53 |
54 | return (
55 |
123 | );
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/dist/react-datepicker.min.css:
--------------------------------------------------------------------------------
1 | /*! react-datepicker v1.0.0-alpha.7 | (c) 2020 Trend Micro Inc. | MIT | https://github.com/trendmicro-frontend/react-datepicker */.datepicker---date-picker-container---1kNBY{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;line-height:20px;border:none;border-radius:0;-webkit-box-shadow:none;box-shadow:none;padding:0 5px;position:relative;display:inline-block}.datepicker---date-picker-container---1kNBY *,.datepicker---date-picker-container---1kNBY :after,.datepicker---date-picker-container---1kNBY :before{-webkit-box-sizing:inherit;-moz-box-sizing:inherit;box-sizing:inherit}.datepicker---date-picker-container---1kNBY .react-datepicker__header{text-align:center;background-color:#fff;border:none;position:relative;padding:0}.datepicker---date-picker-container---1kNBY .react-datepicker__month{margin:0;text-align:center}.datepicker---date-picker-container---1kNBY .react-datepicker__current-month{color:#222;font-weight:700;font-size:13px;height:20px;margin:8px 0}.datepicker---date-picker-container---1kNBY .react-datepicker__navigation{background:0 0;line-height:20px;text-align:center;cursor:pointer;position:absolute;top:3px;padding:5px;border:none;z-index:1;outline:0;width:30px;height:30px;background-color:transparent;background-position:center center;background-repeat:no-repeat}.datepicker---date-picker-container---1kNBY .react-datepicker__navigation:hover{border-radius:3px;background-color:#eee}.datepicker---date-picker-container---1kNBY .react-datepicker__navigation--previous{left:8px;background-image:url(data:image/svg+xml;base64,PCEtLSBHZW5lcmF0ZWQgYnkgVHJlbmQgTWljcm8gU3R5bGUgUG9ydGFsIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiPg0KICA8dGl0bGU+YW5nbGUtbGVmdDwvdGl0bGU+DQogIDxwYXRoIGZpbGw9InJnYigzNCwzNCwzNCkiIGQ9Ik05LjUwMSAxMi41MDZsLTQuNDk5LTQuNTA2IDQuNDg4LTQuNDk0IDEgMS0zLjQ5IDMuNDk0IDMuNTAxIDMuNTA2eiI+PC9wYXRoPg0KPC9zdmc+)}.datepicker---date-picker-container---1kNBY .react-datepicker__navigation--next{right:8px;background-image:url(data:image/svg+xml;base64,PCEtLSBHZW5lcmF0ZWQgYnkgVHJlbmQgTWljcm8gU3R5bGUgUG9ydGFsIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiPg0KICA8dGl0bGU+YW5nbGUtcmlnaHQ8L3RpdGxlPg0KICA8cGF0aCBmaWxsPSJyZ2IoMzQsMzQsMzQpIiBkPSJNNi41MDEgMTIuNWwtMS0xIDMuNTAxLTMuNTA2LTMuNDktMy40OTQgMS0xIDQuNDg4IDQuNDk0eiI+PC9wYXRoPg0KPC9zdmc+)}.datepicker---date-picker-container---1kNBY .react-datepicker__day,.datepicker---date-picker-container---1kNBY .react-datepicker__day-name{color:#222;display:inline-block;text-align:center;width:30px;line-height:20px;border:0;padding:5px;margin:2px}.datepicker---date-picker-container---1kNBY .react-datepicker__day{cursor:pointer;font-size:13px}.datepicker---date-picker-container---1kNBY .react-datepicker__day:hover{background:#eee;cursor:pointer;border-radius:50%}.datepicker---date-picker-container---1kNBY .react-datepicker__day.react-datepicker__day--disabled,.datepicker---date-picker-container---1kNBY .react-datepicker__day:hover.react-datepicker__day--disabled{background:inherit;cursor:default;color:#bbb}.datepicker---date-picker-container---1kNBY .react-datepicker__day-name{padding-top:9px;font-size:12px;font-weight:700}.datepicker---date-picker-container---1kNBY .react-datepicker__day-names{white-space:nowrap}.datepicker---date-picker-container---1kNBY .react-datepicker__day--outside-month{color:#bbb}.datepicker---date-picker-container---1kNBY .react-datepicker__day--today{color:#db3d44}.datepicker---date-picker-container---1kNBY .react-datepicker__day--selected{color:#fff;font-weight:700;background-color:#db3d44;border-radius:50%}.datepicker---date-picker-container---1kNBY .react-datepicker__day--selected:hover{background-color:#db3d44}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.datepicker---date-input---1vOyn>input{height:32px}}.datepicker---date-input-container---2zD9B{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;line-height:20px;position:relative}.datepicker---date-input-container---2zD9B *,.datepicker---date-input-container---2zD9B :after,.datepicker---date-input-container---2zD9B :before{-webkit-box-sizing:inherit;-moz-box-sizing:inherit;box-sizing:inherit}.datepicker---date-input---1vOyn{width:120px}.datepicker---date-input---1vOyn>input{display:block;width:100%;height:auto;line-height:inherit;padding:5px 12px;padding-left:30px;font-size:13px;color:#222;border:1px solid #ccc;border-radius:3px;outline:0}.datepicker---date-input---1vOyn>input:focus{border-color:#0096cc}.datepicker---date-input-icon---1UeQu{position:absolute;left:9px;top:8px;color:#666;width:14px;height:14px}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.datepicker---time-input---2ICRB>input{height:32px}}.datepicker---time-input-container---s4Ikh{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;line-height:20px;position:relative}.datepicker---time-input-container---s4Ikh *,.datepicker---time-input-container---s4Ikh :after,.datepicker---time-input-container---s4Ikh :before{-webkit-box-sizing:inherit;-moz-box-sizing:inherit;box-sizing:inherit}.datepicker---time-input---2ICRB{width:120px}.datepicker---time-input---2ICRB>input{display:block;width:100%;height:auto;line-height:inherit;padding:5px 12px;padding-left:30px;font-size:13px;color:#222;border:1px solid #ccc;border-radius:3px;outline:0}.datepicker---time-input---2ICRB>input:focus{border-color:#0096cc}.datepicker---time-input-icon---3TeZ8{position:absolute;left:9px;top:8px;color:#666;width:14px;height:14px}
--------------------------------------------------------------------------------
/examples/index.jsx:
--------------------------------------------------------------------------------
1 | import 'trendmicro-ui/dist/css/trendmicro-ui.css';
2 | import qs from 'qs';
3 | import React, { PureComponent } from 'react';
4 | import ReactDOM from 'react-dom';
5 | import toRenderProps from 'recompose/toRenderProps';
6 | import withStateHandlers from 'recompose/withStateHandlers';
7 | import Navbar from './Navbar';
8 | import Section from './Section';
9 | import * as DatePickerExample from './DatePicker';
10 | import * as DateTimePickerExample from './DateTimePicker';
11 | import * as DateTimeRangePickerExample from './DateTimeRangePicker';
12 |
13 | const q = qs.parse(window.location.search, { ignoreQueryPrefix: true });
14 | const locale = q.locale || 'en';
15 |
16 | const Enhanced = toRenderProps(withStateHandlers(
17 | { // state
18 | period: '7d'
19 | },
20 | { // handlers
21 | onSelect: () => ({ period }) => ({
22 | period
23 | })
24 | }
25 | ));
26 |
27 | class App extends PureComponent {
28 | state = {
29 | locale: locale
30 | };
31 |
32 | changeLocale = (locale) => {
33 | window.location.search = `locale=${locale}`;
34 | };
35 |
36 | render() {
37 | const name = 'React DatePicker';
38 | const url = 'https://github.com/trendmicro-frontend/react-datepicker';
39 | const { locale } = this.state;
40 |
41 | return (
42 |
43 |
49 |
50 |
51 |
52 |
53 | DatePicker
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | DateTimePicker
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | DateTimeRangePicker
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {({ locale, period, onSelect }) => (
96 |
101 | )}
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | );
114 | }
115 | }
116 |
117 | ReactDOM.render(
118 | ,
119 | document.getElementById('container')
120 | );
121 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-datepicker [](https://travis-ci.org/trendmicro-frontend/react-datepicker) [](https://coveralls.io/github/trendmicro-frontend/react-datepicker?branch=master)
2 |
3 | [](https://nodei.co/npm/@trendmicro/react-datepicker/)
4 |
5 | React DatePicker
6 |
7 | [](https://trendmicro-frontend.github.io/react-datepicker)
8 |
9 | Demo: https://trendmicro-frontend.github.io/react-datepicker
10 |
11 | ## Installation
12 |
13 | 1. Install the latest version of [react](https://github.com/facebook/react), [moment](https://github.com/moment/moment) and [react-datepicker](https://github.com/trendmicro-frontend/react-datepicker):
14 |
15 | ```
16 | npm install --save react moment @trendmicro/react-datepicker
17 | ```
18 |
19 | 2. At this point you can import `@trendmicro/react-datepicker` and its styles in your application as follows:
20 |
21 | ```js
22 | import DatePicker, { DateInput, TimeInput } from '@trendmicro/react-datepicker';
23 |
24 | // Be sure to include styles at some point, probably during your bootstraping
25 | import '@trendmicro/react-datepicker/dist/react-datepicker.css';
26 | ```
27 |
28 | ## Usage
29 |
30 | ### DatePicker
31 |
32 | Initialize state in your React component:
33 | ```js
34 | state = {
35 | date: moment().format('YYYY-MM-DD')
36 | };
37 | ```
38 |
39 | #### Controlled
40 |
41 | ```js
42 | {
45 | this.setState(state => ({ date: date }));
46 | }}
47 | />
48 | ```
49 |
50 | #### Uncontrolled
51 |
52 | ```js
53 | {
56 | // Optional
57 | }}
58 | />
59 | ```
60 |
61 | ### DateInput
62 |
63 | Initialize state in your React component:
64 | ```js
65 | state = {
66 | // 2017-08-01
67 | value: moment().format('YYYY-MM-DD')
68 | };
69 | ```
70 |
71 | #### Controlled
72 |
73 | ```js
74 | {
77 | this.setState(state => ({ value: value }));
78 | }}
79 | />
80 | ```
81 |
82 | #### Uncontrolled
83 |
84 | ```js
85 | {
88 | // Optional
89 | }}
90 | />
91 | ```
92 |
93 | ### TimeInput
94 |
95 | Initialize state in your React component:
96 | ```js
97 | state = {
98 | // 08:00:00
99 | value: moment().format('hh:mm:ss')
100 | };
101 | ```
102 |
103 | #### Controlled
104 |
105 | ```js
106 | {
109 | this.setState(state => ({ value: value }));
110 | }}
111 | />
112 | ```
113 |
114 | #### Uncontrolled
115 |
116 | ```js
117 | {
120 | // Optional
121 | }}
122 | />
123 | ```
124 |
125 | ## Examples
126 |
127 | ### [DatePicker](https://github.com/trendmicro-frontend/react-datepicker/tree/master/examples/DatePicker)
128 |
129 | [](https://trendmicro-frontend.github.io/react-datepicker)
130 |
131 | #### Sources
132 | * [Controlled](https://github.com/trendmicro-frontend/react-datepicker/tree/master/examples/DatePicker/Controlled.jsx)
133 | * [Uncontrolled](https://github.com/trendmicro-frontend/react-datepicker/tree/master/examples/DatePicker/Uncontrolled.jsx)
134 | * [Selectable](https://github.com/trendmicro-frontend/react-datepicker/tree/master/examples/DatePicker/Selectable.jsx)
135 | * [Dropdown](https://github.com/trendmicro-frontend/react-datepicker/tree/master/examples/DatePicker/Dropdown.jsx)
136 |
137 | ### [DateTimePicker](https://github.com/trendmicro-frontend/react-datepicker/tree/master/examples/DateTimePicker)
138 |
139 | [](https://trendmicro-frontend.github.io/react-datepicker)
140 |
141 | #### Sources
142 | * [Controlled](https://github.com/trendmicro-frontend/react-datepicker/tree/master/examples/DateTimePicker/Controlled.jsx)
143 | * [Uncontrolled](https://github.com/trendmicro-frontend/react-datepicker/tree/master/examples/DateTimePicker/Uncontrolled.jsx)
144 |
145 | ### [DateTimeRangePicker](https://github.com/trendmicro-frontend/react-datepicker/tree/master/examples/DateTimeRangePicker)
146 |
147 | [](https://trendmicro-frontend.github.io/react-datepicker)
148 |
149 | #### Sources
150 | * [Controlled](https://github.com/trendmicro-frontend/react-datepicker/tree/master/examples/DateTimeRangePicker/Controlled.jsx)
151 | * [Uncontrolled](https://github.com/trendmicro-frontend/react-datepicker/tree/master/examples/DateTimeRangePicker/Uncontrolled.jsx)
152 | * [Dropdown](https://github.com/trendmicro-frontend/react-datepicker/tree/master/examples/DateTimeRangePicker/Dropdown.jsx)
153 | - @trendmicro/react-dropdown@0.7.0 or above is required to use `Dropdown.MenuWrapper`
154 |
155 | ## API
156 |
157 | ### Properties
158 |
159 | #### DatePicker
160 |
161 | Name | Type | Default | Description
162 | :--- | :--- | :------ | :----------
163 | locale | string | 'en' |
164 | date | object or string | null |
165 | defaultDate | object or string | null |
166 | minDate | object or string | null | The minimum selectable date. When set to null, there is no minimum.
167 | maxDate | object or string | null | The maximum selectable date. When set to null, there is no maximum.
168 | onSelect | function(date) | | Called when a date is selected.
169 |
170 | #### DateInput
171 |
172 | Name | Type | Default | Description
173 | :--- | :--- | :------ | :----------
174 | value | object or string | null |
175 | defaultValue | object or string | null |
176 | minDate | object or string | null | The minimum date. When set to null, there is no minimum.
177 | maxDate | object or string | null | The maximum date. When set to null, there is no maximum.
178 | onChange | function(value) | | Called when the value changes.
179 |
180 | #### TimeInput
181 |
182 | Name | Type | Default | Description
183 | :--- | :--- | :------ | :----------
184 | value | string | '00:00:00' |
185 | defaultValue | string | '00:00:00' |
186 | onChange | function(value) | | Called when the value changes.
187 |
188 | ## License
189 |
190 | MIT
191 |
--------------------------------------------------------------------------------
/examples/DateTimeRangePicker/Controlled.jsx:
--------------------------------------------------------------------------------
1 | import Anchor from '@trendmicro/react-anchor';
2 | import moment from 'moment';
3 | import PropTypes from 'prop-types';
4 | import React, { PureComponent } from 'react';
5 | import DateTimeRangePicker from './DateTimeRangePicker';
6 |
7 | const normalizeDateString = (dateString) => {
8 | let m = moment(dateString);
9 | if (!m.isValid()) {
10 | m = moment();
11 | }
12 | return m.format('YYYY-MM-DD');
13 | };
14 |
15 | const normalizeTimeString = (timeString) => {
16 | let [hh = '00', mm = '00', ss = '00'] = timeString.split(':');
17 | hh = Number(hh) || 0;
18 | mm = Number(mm) || 0;
19 | ss = Number(ss) || 0;
20 | hh = (hh < 0 || hh > 23) ? '00' : ('0' + hh).slice(-2);
21 | mm = (mm < 0 || mm > 59) ? '00' : ('0' + mm).slice(-2);
22 | ss = (ss < 0 || ss > 59) ? '00' : ('0' + ss).slice(-2);
23 | return `${hh}:${mm}:${ss}`;
24 | };
25 |
26 | export default class extends PureComponent {
27 | static propTypes = {
28 | locale: PropTypes.string
29 | };
30 |
31 | state = this.getInitialState();
32 |
33 | changeStartDate = (date) => {
34 | if (!date) {
35 | return;
36 | }
37 |
38 | this.setState(state => {
39 | const startDate = normalizeDateString(date);
40 | const endDate = normalizeDateString(state.endDate);
41 | const startTime = normalizeTimeString(state.startTime);
42 | const endTime = normalizeTimeString(state.endTime);
43 | const isoStartDateTime = `${startDate}T${startTime}`;
44 | const isoEndDateTime = `${endDate}T${endTime}`;
45 | const isEndDateAfterMaxDate = state.maxDate && moment(endDate).isAfter(moment(state.maxDate).endOf('day'));
46 | const isSameOrAfterEnd = moment(isoStartDateTime).isSameOrAfter(isoEndDateTime);
47 |
48 | let nextEndDate = endDate;
49 | if (isEndDateAfterMaxDate) {
50 | nextEndDate = normalizeDateString(state.maxDate);
51 | } else if (isSameOrAfterEnd) {
52 | nextEndDate = startDate;
53 | }
54 |
55 | return {
56 | startDate: startDate,
57 | endDate: nextEndDate,
58 | startTime: startTime,
59 | endTime: isSameOrAfterEnd ? startTime : endTime
60 | };
61 | });
62 | };
63 |
64 | changeEndDate = (date) => {
65 | if (!date) {
66 | return;
67 | }
68 |
69 | this.setState(state => {
70 | const startDate = normalizeDateString(state.startDate);
71 | const endDate = normalizeDateString(date);
72 | const startTime = normalizeTimeString(state.startTime);
73 | const endTime = normalizeTimeString(state.endTime);
74 | const isoStartDateTime = `${startDate}T${startTime}`;
75 | const isoEndDateTime = `${endDate}T${endTime}`;
76 | const isStartDateBeforeMinDate = state.minDate && moment(startDate).isBefore(moment(state.minDate).startOf('day'));
77 | const isSameOrBeforeStart = moment(isoEndDateTime).isSameOrBefore(isoStartDateTime);
78 |
79 | let nextStartDate = startDate;
80 | if (isStartDateBeforeMinDate) {
81 | nextStartDate = normalizeDateString(state.minDate);
82 | } else if (isSameOrBeforeStart) {
83 | nextStartDate = endDate;
84 | }
85 |
86 | return {
87 | startDate: nextStartDate,
88 | endDate: endDate,
89 | startTime: isSameOrBeforeStart ? endTime : startTime,
90 | endTime: endTime
91 | };
92 | });
93 | };
94 |
95 | changeStartTime = (time = '00:00:00') => {
96 | this.setState(state => {
97 | const startDate = normalizeDateString(state.startDate);
98 | const endDate = normalizeDateString(state.endDate);
99 | const startTime = normalizeTimeString(time);
100 | const endTime = normalizeTimeString(state.endTime);
101 | const isoStartDateTime = `${startDate}T${startTime}`;
102 | const isoEndDateTime = `${endDate}T${endTime}`;
103 | const isSameOrAfterEnd = moment(isoStartDateTime).isSameOrAfter(isoEndDateTime);
104 |
105 | return {
106 | startTime: startTime,
107 | endTime: isSameOrAfterEnd ? startTime : endTime
108 | };
109 | });
110 | };
111 |
112 | changeEndTime = (time = '00:00:00') => {
113 | this.setState(state => {
114 | const startDate = normalizeDateString(state.startDate);
115 | const endDate = normalizeDateString(state.endDate);
116 | const startTime = normalizeTimeString(state.startTime);
117 | const endTime = normalizeTimeString(time);
118 | const isoStartDateTime = `${startDate}T${startTime}`;
119 | const isoEndDateTime = `${endDate}T${endTime}`;
120 | const isSameOrBeforeStart = moment(isoEndDateTime).isSameOrBefore(isoStartDateTime);
121 |
122 | return {
123 | startTime: isSameOrBeforeStart ? endTime : startTime,
124 | endTime: endTime
125 | };
126 | });
127 | };
128 |
129 | getInitialState() {
130 | const now = moment();
131 |
132 | return {
133 | minDate: '2000-01-01',
134 | maxDate: moment(now).format('YYYY-MM-DD'),
135 | startDate: moment(now).format('YYYY-MM-DD'),
136 | startTime: moment(now).format('hh:mm:ss'),
137 | endDate: moment(now).add(7, 'days').format('YYYY-MM-DD'),
138 | endTime: moment(now).add(7, 'days').format('hh:mm:ss')
139 | };
140 | }
141 | render() {
142 | const { locale } = this.props;
143 | const { minDate, maxDate, startDate, startTime, endDate, endTime } = this.state;
144 |
145 | return (
146 |
147 |
Controlled Component
148 |
Note: This example will update invalid date/time range.
149 |
150 | - Minimum: {minDate}
151 | - Maximum: {maxDate}
152 | - Range: {startDate} {startTime} ~ {endDate} {endTime}
153 |
154 |
167 |
168 | );
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/dist/react-datepicker.css:
--------------------------------------------------------------------------------
1 | /*! react-datepicker v1.0.0-alpha.7 | (c) 2020 Trend Micro Inc. | MIT | https://github.com/trendmicro-frontend/react-datepicker */
2 | .datepicker---date-picker-container---1kNBY {
3 | -webkit-box-sizing: border-box;
4 | -moz-box-sizing: border-box;
5 | box-sizing: border-box;
6 | line-height: 20px;
7 | border: none;
8 | border-radius: 0;
9 | -webkit-box-shadow: none;
10 | box-shadow: none;
11 | padding: 0 5px;
12 | position: relative;
13 | display: inline-block;
14 | }
15 | .datepicker---date-picker-container---1kNBY *,
16 | .datepicker---date-picker-container---1kNBY *:before,
17 | .datepicker---date-picker-container---1kNBY *:after {
18 | -webkit-box-sizing: inherit;
19 | -moz-box-sizing: inherit;
20 | box-sizing: inherit;
21 | }
22 | .datepicker---date-picker-container---1kNBY .react-datepicker__header {
23 | text-align: center;
24 | background-color: #fff;
25 | border: none;
26 | position: relative;
27 | padding: 0;
28 | }
29 | .datepicker---date-picker-container---1kNBY .react-datepicker__month {
30 | margin: 0;
31 | text-align: center;
32 | }
33 | .datepicker---date-picker-container---1kNBY .react-datepicker__current-month {
34 | color: #222;
35 | font-weight: bold;
36 | font-size: 13px;
37 | height: 20px;
38 | margin: 8px 0;
39 | }
40 | .datepicker---date-picker-container---1kNBY .react-datepicker__navigation {
41 | background: none;
42 | line-height: 20px;
43 | text-align: center;
44 | cursor: pointer;
45 | position: absolute;
46 | top: 3px;
47 | padding: 5px;
48 | border: none;
49 | z-index: 1;
50 | outline: 0;
51 | width: 30px;
52 | height: 30px;
53 | background-color: transparent;
54 | background-position: center center;
55 | background-repeat: no-repeat;
56 | }
57 | .datepicker---date-picker-container---1kNBY .react-datepicker__navigation:hover {
58 | border-radius: 3px;
59 | background-color: #eee;
60 | }
61 | .datepicker---date-picker-container---1kNBY .react-datepicker__navigation--previous {
62 | left: 8px;
63 | background-image: url(data:image/svg+xml;base64,PCEtLSBHZW5lcmF0ZWQgYnkgVHJlbmQgTWljcm8gU3R5bGUgUG9ydGFsIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiPg0KICA8dGl0bGU+YW5nbGUtbGVmdDwvdGl0bGU+DQogIDxwYXRoIGZpbGw9InJnYigzNCwzNCwzNCkiIGQ9Ik05LjUwMSAxMi41MDZsLTQuNDk5LTQuNTA2IDQuNDg4LTQuNDk0IDEgMS0zLjQ5IDMuNDk0IDMuNTAxIDMuNTA2eiI+PC9wYXRoPg0KPC9zdmc+);
64 | }
65 | .datepicker---date-picker-container---1kNBY .react-datepicker__navigation--next {
66 | right: 8px;
67 | background-image: url(data:image/svg+xml;base64,PCEtLSBHZW5lcmF0ZWQgYnkgVHJlbmQgTWljcm8gU3R5bGUgUG9ydGFsIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiPg0KICA8dGl0bGU+YW5nbGUtcmlnaHQ8L3RpdGxlPg0KICA8cGF0aCBmaWxsPSJyZ2IoMzQsMzQsMzQpIiBkPSJNNi41MDEgMTIuNWwtMS0xIDMuNTAxLTMuNTA2LTMuNDktMy40OTQgMS0xIDQuNDg4IDQuNDk0eiI+PC9wYXRoPg0KPC9zdmc+);
68 | }
69 | .datepicker---date-picker-container---1kNBY .react-datepicker__day,
70 | .datepicker---date-picker-container---1kNBY .react-datepicker__day-name {
71 | color: #222;
72 | display: inline-block;
73 | text-align: center;
74 | width: 30px;
75 | line-height: 20px;
76 | border: 0;
77 | padding: 5px;
78 | margin: 2px;
79 | }
80 | .datepicker---date-picker-container---1kNBY .react-datepicker__day {
81 | cursor: pointer;
82 | font-size: 13px;
83 | }
84 | .datepicker---date-picker-container---1kNBY .react-datepicker__day:hover {
85 | background: #eee;
86 | cursor: pointer;
87 | border-radius: 50%;
88 | }
89 | .datepicker---date-picker-container---1kNBY .react-datepicker__day.react-datepicker__day--disabled,
90 | .datepicker---date-picker-container---1kNBY .react-datepicker__day:hover.react-datepicker__day--disabled {
91 | background: inherit;
92 | cursor: default;
93 | color: #bbb;
94 | }
95 | .datepicker---date-picker-container---1kNBY .react-datepicker__day-name {
96 | padding-top: 9px;
97 | font-size: 12px;
98 | font-weight: bold;
99 | }
100 | .datepicker---date-picker-container---1kNBY .react-datepicker__day-names {
101 | white-space: nowrap;
102 | }
103 | .datepicker---date-picker-container---1kNBY .react-datepicker__day--outside-month {
104 | color: #bbb;
105 | }
106 | .datepicker---date-picker-container---1kNBY .react-datepicker__day--today {
107 | color: #db3d44;
108 | }
109 | .datepicker---date-picker-container---1kNBY .react-datepicker__day--selected {
110 | color: #fff;
111 | font-weight: bold;
112 | background-color: #db3d44;
113 | border-radius: 50%;
114 | }
115 | .datepicker---date-picker-container---1kNBY .react-datepicker__day--selected:hover {
116 | background-color: #db3d44;
117 | }
118 | @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
119 | .datepicker---date-input---1vOyn > input {
120 | height: 32px;
121 | }
122 | }
123 | .datepicker---date-input-container---2zD9B {
124 | -webkit-box-sizing: border-box;
125 | -moz-box-sizing: border-box;
126 | box-sizing: border-box;
127 | line-height: 20px;
128 | position: relative;
129 | }
130 | .datepicker---date-input-container---2zD9B *,
131 | .datepicker---date-input-container---2zD9B *:before,
132 | .datepicker---date-input-container---2zD9B *:after {
133 | -webkit-box-sizing: inherit;
134 | -moz-box-sizing: inherit;
135 | box-sizing: inherit;
136 | }
137 | .datepicker---date-input---1vOyn {
138 | width: 120px;
139 | }
140 | .datepicker---date-input---1vOyn > input {
141 | display: block;
142 | width: 100%;
143 | height: auto;
144 | line-height: inherit;
145 | padding: 5px 12px;
146 | padding-left: 30px;
147 | font-size: 13px;
148 | color: #222;
149 | border: 1px solid #ccc;
150 | border-radius: 3px;
151 | outline: none;
152 | }
153 | .datepicker---date-input---1vOyn > input:focus {
154 | border-color: #0096cc;
155 | }
156 | .datepicker---date-input-icon---1UeQu {
157 | position: absolute;
158 | left: 9px;
159 | top: 8px;
160 | color: #666;
161 | width: 14px;
162 | height: 14px;
163 | }
164 | @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
165 | .datepicker---time-input---2ICRB > input {
166 | height: 32px;
167 | }
168 | }
169 | .datepicker---time-input-container---s4Ikh {
170 | -webkit-box-sizing: border-box;
171 | -moz-box-sizing: border-box;
172 | box-sizing: border-box;
173 | line-height: 20px;
174 | position: relative;
175 | }
176 | .datepicker---time-input-container---s4Ikh *,
177 | .datepicker---time-input-container---s4Ikh *:before,
178 | .datepicker---time-input-container---s4Ikh *:after {
179 | -webkit-box-sizing: inherit;
180 | -moz-box-sizing: inherit;
181 | box-sizing: inherit;
182 | }
183 | .datepicker---time-input---2ICRB {
184 | width: 120px;
185 | }
186 | .datepicker---time-input---2ICRB > input {
187 | display: block;
188 | width: 100%;
189 | height: auto;
190 | line-height: inherit;
191 | padding: 5px 12px;
192 | padding-left: 30px;
193 | font-size: 13px;
194 | color: #222;
195 | border: 1px solid #ccc;
196 | border-radius: 3px;
197 | outline: none;
198 | }
199 | .datepicker---time-input---2ICRB > input:focus {
200 | border-color: #0096cc;
201 | }
202 | .datepicker---time-input-icon---3TeZ8 {
203 | position: absolute;
204 | left: 9px;
205 | top: 8px;
206 | color: #666;
207 | width: 14px;
208 | height: 14px;
209 | }
210 |
211 | /*# sourceMappingURL=react-datepicker.css.map*/
--------------------------------------------------------------------------------
/src/TimeInput/index.jsx:
--------------------------------------------------------------------------------
1 | import cx from 'classnames';
2 | import PropTypes from 'prop-types';
3 | import React, { PureComponent } from 'react';
4 | import { uncontrollable } from 'uncontrollable';
5 | import Clock from './Clock';
6 | import isTwelveHourTime from './lib/is-twelve-hour-time';
7 | import replaceCharAt from './lib/replace-char-at';
8 | import getGroupId from './lib/get-group-id';
9 | import getGroups from './lib/get-groups';
10 | import adder from './lib/time-string-adder';
11 | import caret from './lib/caret';
12 | import validate from './lib/validate';
13 | import styles from './index.styl';
14 |
15 | const SILHOUETTE = '00:00:00:000 AM';
16 |
17 | class TimeInput extends PureComponent {
18 | static propTypes = {
19 | className: PropTypes.string,
20 | value: PropTypes.string,
21 | onChange: PropTypes.func
22 | };
23 | static defaultProps = {
24 | value: '00:00:00:000 AM'
25 | };
26 |
27 | input = null;
28 | mounted = false;
29 | state = {
30 | focused: false,
31 | caretIndex: null
32 | };
33 |
34 | handleFocus = (event) => {
35 | if (this.mounted) {
36 | this.setState({ focused: true });
37 | }
38 | };
39 |
40 | handleBlur = (event) => {
41 | if (this.mounted) {
42 | this.setState({ caretIndex: null, focused: false });
43 | }
44 | };
45 |
46 | handleChange = (event) => {
47 | let value = this.props.value;
48 | let newValue = this.input.value;
49 | let diff = newValue.length - value.length;
50 | let end = caret.start(this.input);
51 | let insertion;
52 | let start = end - Math.abs(diff);
53 |
54 | event.preventDefault();
55 |
56 | if (diff > 0) {
57 | insertion = newValue.slice(end - diff, end);
58 | while (diff--) {
59 | const oldChar = value.charAt(start);
60 | const newChar = insertion.charAt(0);
61 | if (this.isSeparator(oldChar)) {
62 | if (this.isSeparator(newChar)) {
63 | insertion = insertion.slice(1);
64 | start++;
65 | } else {
66 | start++;
67 | diff++;
68 | end++;
69 | }
70 | } else {
71 | value = replaceCharAt(value, start, newChar);
72 | insertion = insertion.slice(1);
73 | start++;
74 | }
75 | }
76 | newValue = value;
77 | } else {
78 | if (newValue.charAt(start) === ':') {
79 | start++;
80 | }
81 | // apply default to selection
82 | let result = value;
83 | for (let i = start; i < end; i++) {
84 | result = replaceCharAt(result, i, newValue.charAt(i));
85 | }
86 | newValue = result;
87 | }
88 |
89 | if (validate(newValue)) {
90 | if (newValue.charAt(end) === ':') {
91 | end++;
92 | }
93 | this.onChange(newValue, end);
94 | } else {
95 | const caretIndex = this.props.value.length - (newValue.length - end);
96 | if (this.mounted) {
97 | this.setState({ caretIndex: caretIndex });
98 | }
99 | }
100 | };
101 |
102 | handleKeyDown = (event) => {
103 | event.stopPropagation();
104 |
105 | if (event.which === 9) {
106 | this.handleTab(event);
107 | return;
108 | }
109 | if (event.which === 38 || event.which === 40) {
110 | this.handleArrows(event);
111 | return;
112 | }
113 | if (event.which === 8) {
114 | this.handleBackspace(event);
115 | return;
116 | }
117 | if (event.which === 32 || event.which === 46) {
118 | this.handleForwardspace(event);
119 | return;
120 | }
121 | if (event.which === 27) {
122 | this.handleEscape(event);
123 | return;
124 | }
125 | };
126 |
127 | handleEscape = () => {
128 | if (this.mounted) {
129 | this.input.blur();
130 | }
131 | };
132 |
133 | handleTab = (event) => {
134 | const start = caret.start(this.input);
135 | const value = this.props.value;
136 | const groups = getGroups(value);
137 | let groupId = getGroupId(start);
138 | if (event.shiftKey) {
139 | if (!groupId) {
140 | return;
141 | }
142 | groupId--;
143 | } else {
144 | if (groupId >= (groups.length - 1)) {
145 | return;
146 | }
147 | groupId++;
148 | }
149 |
150 | event.preventDefault();
151 |
152 | let index = groupId * 3;
153 | if (this.props.value.charAt(index) === ' ') {
154 | index++;
155 | }
156 | if (this.mounted) {
157 | this.setState({ caretIndex: index });
158 | }
159 | };
160 |
161 | handleArrows = (event) => {
162 | event.preventDefault();
163 | const start = caret.start(this.input);
164 | const amount = event.which === 38 ? 1 : -1;
165 | const value = adder(this.props.value, getGroupId(start), amount);
166 | this.onChange(value, start);
167 | };
168 |
169 | handleBackspace = (event) => {
170 | event.preventDefault();
171 |
172 | let start = caret.start(this.input);
173 | let value = this.props.value;
174 | let end = caret.end(this.input);
175 |
176 | if (!start && !end) {
177 | return;
178 | }
179 |
180 | let diff = end - start;
181 | const silhouette = this.silhouette();
182 |
183 | if (!diff) {
184 | if (value[start - 1] === ':') {
185 | start--;
186 | }
187 | value = replaceCharAt(value, start - 1, silhouette.charAt(start - 1));
188 | start--;
189 | } else {
190 | while (diff--) {
191 | if (value[end - 1] !== ':') {
192 | value = replaceCharAt(value, end - 1, silhouette.charAt(end - 1));
193 | }
194 | end--;
195 | }
196 | if (value.charAt(start - 1) === ':') {
197 | start--;
198 | }
199 | }
200 |
201 | this.onChange(value, start);
202 | };
203 |
204 | handleForwardspace = (event) => {
205 | event.preventDefault();
206 |
207 | let start = caret.start(this.input);
208 | let value = this.props.value;
209 | let end = caret.end(this.input);
210 |
211 | if (start === end === (value.length - 1)) {
212 | return;
213 | }
214 |
215 | let diff = end - start;
216 | const silhouette = this.silhouette();
217 |
218 | if (!diff) {
219 | if (value[start] === ':') {
220 | start++;
221 | }
222 | value = replaceCharAt(value, start, silhouette.charAt(start));
223 | start++;
224 | } else {
225 | while (diff--) {
226 | if (value[end - 1] !== ':') {
227 | value = replaceCharAt(value, start, silhouette.charAt(start));
228 | }
229 | start++;
230 | }
231 | }
232 | if (value.charAt(start) === ':') {
233 | start++;
234 | }
235 |
236 | this.onChange(value, start);
237 | };
238 |
239 | isSeparator = (char) => {
240 | return /[:\s]/.test(char);
241 | };
242 |
243 | format = (val) => {
244 | if (isTwelveHourTime(val)) {
245 | val = val.replace(/^00/, '12');
246 | }
247 | return val.toUpperCase();
248 | };
249 |
250 | onChange = (str, caretIndex) => {
251 | if (this.props.onChange) {
252 | this.props.onChange(this.format(str));
253 | }
254 | if (this.mounted && typeof caretIndex === 'number') {
255 | this.setState({ caretIndex: caretIndex });
256 | }
257 | };
258 |
259 | silhouette = () => {
260 | return this.props.value.replace(/\d/g, (val, i) => {
261 | return SILHOUETTE.charAt(i);
262 | });
263 | };
264 |
265 | componentDidMount () {
266 | this.mounted = true;
267 | }
268 | componentWillUnmount () {
269 | this.mounted = false;
270 | }
271 | componentDidUpdate() {
272 | const index = this.state.caretIndex;
273 | if (index || index === 0) {
274 | caret.set(this.input, index);
275 | }
276 | }
277 | render() {
278 | const value = this.format(this.props.value);
279 | const icon = (
280 |
281 | );
282 |
283 | return (
284 |
288 |
289 | {
291 | this.input = node;
292 | }}
293 | type="text"
294 | value={value}
295 | onChange={this.handleChange}
296 | onFocus={this.handleFocus}
297 | onBlur={this.handleBlur}
298 | onKeyDown={this.handleKeyDown}
299 | />
300 |
301 | {icon}
302 |
303 | );
304 | }
305 | }
306 |
307 | export default uncontrollable(TimeInput, {
308 | // Define the pairs of prop/handlers you want to be uncontrollable
309 | value: 'onChange'
310 | });
311 |
--------------------------------------------------------------------------------
/examples/DateTimeRangePicker/DropdownRight.jsx:
--------------------------------------------------------------------------------
1 | import '@trendmicro/react-buttons/dist/react-buttons.css';
2 | import '@trendmicro/react-dropdown/dist/react-dropdown.css';
3 | import Anchor from '@trendmicro/react-anchor';
4 | import { Button } from '@trendmicro/react-buttons';
5 | import Dropdown, { MenuItem } from '@trendmicro/react-dropdown'; // @trendmicro/react-dropdown@0.7.0 or above is required
6 | import moment from 'moment';
7 | import PropTypes from 'prop-types';
8 | import React, { PureComponent } from 'react';
9 | import DateTimeRangePicker from './DateTimeRangePicker';
10 |
11 | const normalizeDateString = (dateString) => {
12 | let m = moment(dateString);
13 | if (!m.isValid()) {
14 | m = moment();
15 | }
16 | return m.format('YYYY-MM-DD');
17 | };
18 |
19 | const normalizeTimeString = (timeString) => {
20 | let [hh = '00', mm = '00', ss = '00'] = timeString.split(':');
21 | hh = Number(hh) || 0;
22 | mm = Number(mm) || 0;
23 | ss = Number(ss) || 0;
24 | hh = (hh < 0 || hh > 23) ? '00' : ('0' + hh).slice(-2);
25 | mm = (mm < 0 || mm > 59) ? '00' : ('0' + mm).slice(-2);
26 | ss = (ss < 0 || ss > 59) ? '00' : ('0' + ss).slice(-2);
27 | return `${hh}:${mm}:${ss}`;
28 | };
29 |
30 | const mapPeriodToString = (period) => {
31 | return {
32 | '1d': 'Last 24 hours',
33 | '7d': 'Last 7 days',
34 | '30d': 'Last 30 days',
35 | '90d': 'Last 90 days',
36 | 'custom': 'Custom range'
37 | }[period];
38 | };
39 |
40 | export default class extends PureComponent {
41 | static propTypes = {
42 | locale: PropTypes.string
43 | };
44 |
45 | state = this.getInitialState();
46 |
47 | changeStartDate = (date) => {
48 | if (!date) {
49 | return;
50 | }
51 | const startDate = normalizeDateString(date);
52 | const endDate = normalizeDateString(this.state.nextEndDate);
53 | const startTime = normalizeTimeString(this.state.nextStartTime);
54 | const endTime = normalizeTimeString(this.state.nextEndTime);
55 | const isoStartDateTime = `${startDate}T${startTime}`;
56 | const isoEndDateTime = `${endDate}T${endTime}`;
57 | const isSameOrAfterEnd = moment(isoStartDateTime).isSameOrAfter(isoEndDateTime);
58 |
59 | this.setState({
60 | nextStartDate: startDate,
61 | nextEndDate: isSameOrAfterEnd ? startDate : endDate,
62 | nextStartTime: startTime,
63 | nextEndTime: isSameOrAfterEnd ? startTime : endTime
64 | });
65 | };
66 | changeEndDate = (date) => {
67 | if (!date) {
68 | return;
69 | }
70 | const startDate = normalizeDateString(this.state.nextStartDate);
71 | const endDate = normalizeDateString(date);
72 | const startTime = normalizeTimeString(this.state.nextStartTime);
73 | const endTime = normalizeTimeString(this.state.nextEndTime);
74 | const isoStartDateTime = `${startDate}T${startTime}`;
75 | const isoEndDateTime = `${endDate}T${endTime}`;
76 | const isSameOrBeforeStart = moment(isoEndDateTime).isSameOrBefore(isoStartDateTime);
77 |
78 | this.setState({
79 | nextStartDate: isSameOrBeforeStart ? endDate : startDate,
80 | nextEndDate: endDate,
81 | nextStartTime: isSameOrBeforeStart ? endTime : startTime,
82 | nextEndTime: endTime
83 | });
84 | };
85 | changeStartTime = (time = '00:00:00') => {
86 | const startDate = normalizeDateString(this.state.nextStartDate);
87 | const endDate = normalizeDateString(this.state.nextEndDate);
88 | const startTime = normalizeTimeString(time);
89 | const endTime = normalizeTimeString(this.state.nextEndTime);
90 | const isoStartDateTime = `${startDate}T${startTime}`;
91 | const isoEndDateTime = `${endDate}T${endTime}`;
92 | const isSameOrAfterEnd = moment(isoStartDateTime).isSameOrAfter(isoEndDateTime);
93 |
94 | this.setState({
95 | nextStartTime: startTime,
96 | nextEndTime: isSameOrAfterEnd ? startTime : endTime
97 | });
98 | };
99 | changeEndTime = (time = '00:00:00') => {
100 | const startDate = normalizeDateString(this.state.nextStartDate);
101 | const endDate = normalizeDateString(this.state.nextEndDate);
102 | const startTime = normalizeTimeString(this.state.nextStartTime);
103 | const endTime = normalizeTimeString(time);
104 | const isoStartDateTime = `${startDate}T${startTime}`;
105 | const isoEndDateTime = `${endDate}T${endTime}`;
106 | const isSameOrBeforeStart = moment(isoEndDateTime).isSameOrBefore(isoStartDateTime);
107 |
108 | this.setState({
109 | nextStartTime: isSameOrBeforeStart ? endTime : startTime,
110 | nextEndTime: endTime
111 | });
112 | };
113 |
114 | getInitialState() {
115 | const now = moment();
116 | const startDate = moment(now).format('YYYY-MM-DD');
117 | const startTime = moment(now).format('hh:mm:ss');
118 | const endDate = moment(now).add(7, 'days').format('YYYY-MM-DD');
119 | const endTime = moment(now).add(7, 'days').format('hh:mm:ss');
120 |
121 | return {
122 | // prev
123 | startDate: startDate,
124 | startTime: startTime,
125 | endDate: endDate,
126 | endTime: endTime,
127 |
128 | // next
129 | nextStartDate: startDate,
130 | nextStartTime: startTime,
131 | nextEndDate: endDate,
132 | nextEndTime: endTime,
133 |
134 | // Dropdown
135 | open: false,
136 | period: '1d',
137 | showDateTimeRangePicker: false
138 | };
139 | }
140 | render() {
141 | const { locale } = this.props;
142 | const {
143 | startDate, startTime, endDate, endTime,
144 | nextStartDate, nextStartTime, nextEndDate, nextEndTime,
145 | period
146 | } = this.state;
147 |
148 | return (
149 |
150 |
Right Align Dropdown
151 | {period !== 'custom' &&
152 |
Selected: {mapPeriodToString(period)}
153 | }
154 | {period === 'custom' &&
155 |
Selected: {startDate} {startTime} - {endDate} {endTime}
156 | }
157 |
{
162 | this.setState(state => ({
163 | period: eventKey,
164 | showDateTimeRangePicker: eventKey === 'custom',
165 | nextStartDate: state.startDate,
166 | nextStartTime: state.startTime,
167 | nextEndDate: state.endDate,
168 | nextEndTime: state.endTime
169 | }));
170 | }}
171 | onClose={() => {
172 | this.setState(state => ({
173 | open: false,
174 | showDateTimeRangePicker: false
175 | }));
176 | }}
177 | onToggle={open => {
178 | this.setState(state => {
179 | const { period } = state;
180 | return {
181 | open: open || period === 'custom'
182 | };
183 | });
184 | }}
185 | >
186 |
187 | {mapPeriodToString(period)}
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
198 |
199 | {(this.state.showDateTimeRangePicker || this.state.period === 'custom') &&
200 |
207 |
218 |
219 |
237 |
247 |
248 |
249 | }
250 |
251 |
252 |
253 | );
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/examples/Navbar.styl:
--------------------------------------------------------------------------------
1 | .container-fluid {
2 | padding-right: 15px;
3 | padding-left: 15px;
4 | margin-right: auto;
5 | margin-left: auto;
6 | }
7 | .sr-only {
8 | position: absolute;
9 | width: 1px;
10 | height: 1px;
11 | padding: 0;
12 | margin: -1px;
13 | overflow: hidden;
14 | clip: rect(0, 0, 0, 0);
15 | border: 0;
16 | }
17 | .collapse {
18 | display: none;
19 | &.in { display: block; }
20 | tr&.in { display: table-row; }
21 | tbody&.in { display: table-row-group; }
22 | }
23 | .nav:before,
24 | .nav:after,
25 | .navbar:before,
26 | .navbar:after,
27 | .navbar-header:before,
28 | .navbar-header:after,
29 | .navbar-collapse:before,
30 | .navbar-collapse:after {
31 | display: table;
32 | content: "";
33 | }
34 | .nav:after,
35 | .navbar:after,
36 | .navbar-header:after,
37 | .navbar-collapse:after {
38 | clear: both;
39 | }
40 | .nav {
41 | padding-left: 0;
42 | margin-bottom: 0;
43 | list-style: none;
44 | }
45 | .navbar {
46 | position: relative;
47 | min-height: 50px;
48 | border: 1px solid transparent;
49 | }
50 | @media (min-width: 768px) {
51 | .navbar {
52 | border-radius: 4px;
53 | }
54 | }
55 | @media (min-width: 768px) {
56 | .navbar-header {
57 | float: left;
58 | }
59 | }
60 | .navbar-collapse {
61 | padding-right: 15px;
62 | padding-left: 15px;
63 | overflow-x: visible;
64 | -webkit-overflow-scrolling: touch;
65 | border-top: 1px solid transparent;
66 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1);
67 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1);
68 | }
69 | .navbar-collapse.in {
70 | overflow-y: auto;
71 | }
72 | @media (min-width: 768px) {
73 | .navbar-collapse {
74 | width: auto;
75 | border-top: 0;
76 | -webkit-box-shadow: none;
77 | box-shadow: none;
78 | }
79 | .navbar-collapse.collapse {
80 | display: block !important;
81 | height: auto !important;
82 | padding-bottom: 0;
83 | overflow: visible !important;
84 | }
85 | .navbar-collapse.in {
86 | overflow-y: visible;
87 | }
88 | .navbar-fixed-top .navbar-collapse,
89 | .navbar-static-top .navbar-collapse,
90 | .navbar-fixed-bottom .navbar-collapse {
91 | padding-right: 0;
92 | padding-left: 0;
93 | }
94 | }
95 | .navbar-fixed-top .navbar-collapse,
96 | .navbar-fixed-bottom .navbar-collapse {
97 | max-height: 340px;
98 | }
99 | @media (max-device-width: 480px) and (orientation: landscape) {
100 | .navbar-fixed-top .navbar-collapse,
101 | .navbar-fixed-bottom .navbar-collapse {
102 | max-height: 200px;
103 | }
104 | }
105 | .container > .navbar-header,
106 | .container-fluid > .navbar-header,
107 | .container > .navbar-collapse,
108 | .container-fluid > .navbar-collapse {
109 | margin-right: -15px;
110 | margin-left: -15px;
111 | }
112 | @media (min-width: 768px) {
113 | .container > .navbar-header,
114 | .container-fluid > .navbar-header,
115 | .container > .navbar-collapse,
116 | .container-fluid > .navbar-collapse {
117 | margin-right: 0;
118 | margin-left: 0;
119 | }
120 | }
121 | .navbar-static-top {
122 | z-index: 1000;
123 | border-width: 0 0 1px;
124 | }
125 | @media (min-width: 768px) {
126 | .navbar-static-top {
127 | border-radius: 0;
128 | }
129 | }
130 | .navbar-fixed-top,
131 | .navbar-fixed-bottom {
132 | position: fixed;
133 | right: 0;
134 | left: 0;
135 | z-index: 1030;
136 | }
137 | @media (min-width: 768px) {
138 | .navbar-fixed-top,
139 | .navbar-fixed-bottom {
140 | border-radius: 0;
141 | }
142 | }
143 | .navbar-fixed-top {
144 | top: 0;
145 | border-width: 0 0 1px;
146 | }
147 | .navbar-fixed-bottom {
148 | bottom: 0;
149 | margin-bottom: 0;
150 | border-width: 1px 0 0;
151 | }
152 | .navbar-brand {
153 | float: left;
154 | height: 50px;
155 | padding: 15px 15px;
156 | font-size: 18px;
157 | line-height: 20px;
158 | }
159 | .navbar-brand,
160 | .navbar-brand:hover,
161 | .navbar-brand:focus {
162 | text-decoration: none;
163 | }
164 | .navbar-brand > img {
165 | display: block;
166 | }
167 | @media (min-width: 768px) {
168 | .navbar > .container .navbar-brand,
169 | .navbar > .container-fluid .navbar-brand {
170 | margin-left: -15px;
171 | }
172 | }
173 | .navbar-toggle {
174 | position: relative;
175 | float: right;
176 | padding: 9px 10px;
177 | margin-top: 8px;
178 | margin-right: 15px;
179 | margin-bottom: 8px;
180 | background-color: transparent;
181 | background-image: none;
182 | border: 1px solid transparent;
183 | border-radius: 4px;
184 | cursor: pointer;
185 | }
186 | .navbar-toggle:focus {
187 | outline: 0;
188 | }
189 | .navbar-toggle .icon-bar {
190 | display: block;
191 | width: 22px;
192 | height: 2px;
193 | border-radius: 1px;
194 | }
195 | .navbar-toggle .icon-bar + .icon-bar {
196 | margin-top: 4px;
197 | }
198 | @media (min-width: 768px) {
199 | .navbar-toggle {
200 | display: none;
201 | }
202 | }
203 | .navbar-nav {
204 | margin: 7.5px -15px;
205 | }
206 | .navbar-nav > li > a {
207 | padding-top: 10px;
208 | padding-bottom: 10px;
209 | line-height: 20px;
210 | }
211 | @media (max-width: 767px) {
212 | .navbar-nav .open .dropdown-menu {
213 | position: static;
214 | float: none;
215 | width: auto;
216 | margin-top: 0;
217 | background-color: transparent;
218 | border: 0;
219 | -webkit-box-shadow: none;
220 | box-shadow: none;
221 | }
222 | .navbar-nav .open .dropdown-menu > li > a,
223 | .navbar-nav .open .dropdown-menu .dropdown-header {
224 | padding: 5px 15px 5px 25px;
225 | }
226 | .navbar-nav .open .dropdown-menu > li > a {
227 | line-height: 20px;
228 | }
229 | .navbar-nav .open .dropdown-menu > li > a:hover,
230 | .navbar-nav .open .dropdown-menu > li > a:focus {
231 | background-image: none;
232 | }
233 | }
234 | @media (min-width: 768px) {
235 | .navbar-nav {
236 | float: left;
237 | margin: 0;
238 | }
239 | .navbar-nav > li {
240 | float: left;
241 | }
242 | .navbar-nav > li > a {
243 | padding-top: 15px;
244 | padding-bottom: 15px;
245 | }
246 | }
247 | .navbar-form {
248 | padding: 10px 15px;
249 | margin-top: 8px;
250 | margin-right: -15px;
251 | margin-bottom: 8px;
252 | margin-left: -15px;
253 | border-top: 1px solid transparent;
254 | border-bottom: 1px solid transparent;
255 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1);
256 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1);
257 | }
258 | @media (min-width: 768px) {
259 | .navbar-form .form-group {
260 | display: inline-block;
261 | margin-bottom: 0;
262 | vertical-align: middle;
263 | }
264 | .navbar-form .form-control {
265 | display: inline-block;
266 | width: auto;
267 | vertical-align: middle;
268 | }
269 | .navbar-form .form-control-static {
270 | display: inline-block;
271 | }
272 | .navbar-form .input-group {
273 | display: inline-table;
274 | vertical-align: middle;
275 | }
276 | .navbar-form .input-group .input-group-addon,
277 | .navbar-form .input-group .input-group-btn,
278 | .navbar-form .input-group .form-control {
279 | width: auto;
280 | }
281 | .navbar-form .input-group > .form-control {
282 | width: 100%;
283 | }
284 | .navbar-form .control-label {
285 | margin-bottom: 0;
286 | vertical-align: middle;
287 | }
288 | .navbar-form .radio,
289 | .navbar-form .checkbox {
290 | display: inline-block;
291 | margin-top: 0;
292 | margin-bottom: 0;
293 | vertical-align: middle;
294 | }
295 | .navbar-form .radio label,
296 | .navbar-form .checkbox label {
297 | padding-left: 0;
298 | }
299 | .navbar-form .radio input[type="radio"],
300 | .navbar-form .checkbox input[type="checkbox"] {
301 | position: relative;
302 | margin-left: 0;
303 | }
304 | .navbar-form .has-feedback .form-control-feedback {
305 | top: 0;
306 | }
307 | }
308 | @media (max-width: 767px) {
309 | .navbar-form .form-group {
310 | margin-bottom: 5px;
311 | }
312 | .navbar-form .form-group:last-child {
313 | margin-bottom: 0;
314 | }
315 | }
316 | @media (min-width: 768px) {
317 | .navbar-form {
318 | width: auto;
319 | padding-top: 0;
320 | padding-bottom: 0;
321 | margin-right: 0;
322 | margin-left: 0;
323 | border: 0;
324 | -webkit-box-shadow: none;
325 | box-shadow: none;
326 | }
327 | }
328 | .navbar-nav > li > .dropdown-menu {
329 | margin-top: 0;
330 | border-top-left-radius: 0;
331 | border-top-right-radius: 0;
332 | }
333 | .navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {
334 | margin-bottom: 0;
335 | border-top-left-radius: 4px;
336 | border-top-right-radius: 4px;
337 | border-bottom-right-radius: 0;
338 | border-bottom-left-radius: 0;
339 | }
340 | .navbar-btn {
341 | margin-top: 8px;
342 | margin-bottom: 8px;
343 | }
344 | .navbar-btn.btn-sm {
345 | margin-top: 10px;
346 | margin-bottom: 10px;
347 | }
348 | .navbar-btn.btn-xs {
349 | margin-top: 14px;
350 | margin-bottom: 14px;
351 | }
352 | .navbar-text {
353 | margin-top: 15px;
354 | margin-bottom: 15px;
355 | }
356 | @media (min-width: 768px) {
357 | .navbar-text {
358 | float: left;
359 | margin-right: 15px;
360 | margin-left: 15px;
361 | }
362 | }
363 | @media (min-width: 768px) {
364 | .navbar-left {
365 | float: left !important;
366 | }
367 | .navbar-right {
368 | float: right !important;
369 | margin-right: -15px;
370 | }
371 | .navbar-right ~ .navbar-right {
372 | margin-right: 0;
373 | }
374 | }
375 | .navbar-default {
376 | background-color: #f8f8f8;
377 | border-color: #e7e7e7;
378 | }
379 | .navbar-default .navbar-brand {
380 | color: #777;
381 | }
382 | .navbar-default .navbar-brand:hover,
383 | .navbar-default .navbar-brand:focus {
384 | color: #5e5e5e;
385 | background-color: transparent;
386 | }
387 | .navbar-default .navbar-text {
388 | color: #777;
389 | }
390 | .navbar-default .navbar-nav > li > a {
391 | color: #777;
392 | }
393 | .navbar-default .navbar-nav > li > a:hover,
394 | .navbar-default .navbar-nav > li > a:focus {
395 | color: #333;
396 | background-color: transparent;
397 | }
398 | .navbar-default .navbar-nav > .active > a,
399 | .navbar-default .navbar-nav > .active > a:hover,
400 | .navbar-default .navbar-nav > .active > a:focus {
401 | color: #555;
402 | background-color: #e7e7e7;
403 | }
404 | .navbar-default .navbar-nav > .disabled > a,
405 | .navbar-default .navbar-nav > .disabled > a:hover,
406 | .navbar-default .navbar-nav > .disabled > a:focus {
407 | color: #ccc;
408 | background-color: transparent;
409 | }
410 | .navbar-default .navbar-toggle {
411 | border-color: #ddd;
412 | }
413 | .navbar-default .navbar-toggle:hover,
414 | .navbar-default .navbar-toggle:focus {
415 | background-color: #ddd;
416 | }
417 | .navbar-default .navbar-toggle .icon-bar {
418 | background-color: #888;
419 | }
420 | .navbar-default .navbar-collapse,
421 | .navbar-default .navbar-form {
422 | border-color: #e7e7e7;
423 | }
424 | .navbar-default .navbar-nav > .open > a,
425 | .navbar-default .navbar-nav > .open > a:hover,
426 | .navbar-default .navbar-nav > .open > a:focus {
427 | color: #555;
428 | background-color: #e7e7e7;
429 | }
430 | @media (max-width: 767px) {
431 | .navbar-default .navbar-nav .open .dropdown-menu > li > a {
432 | color: #777;
433 | }
434 | .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,
435 | .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
436 | color: #333;
437 | background-color: transparent;
438 | }
439 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a,
440 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,
441 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
442 | color: #555;
443 | background-color: #e7e7e7;
444 | }
445 | .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,
446 | .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,
447 | .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {
448 | color: #ccc;
449 | background-color: transparent;
450 | }
451 | }
452 | .navbar-default .navbar-link {
453 | color: #777;
454 | }
455 | .navbar-default .navbar-link:hover {
456 | color: #333;
457 | }
458 | .navbar-default .btn-link {
459 | color: #777;
460 | }
461 | .navbar-default .btn-link:hover,
462 | .navbar-default .btn-link:focus {
463 | color: #333;
464 | }
465 |
--------------------------------------------------------------------------------
/src/DateInput/index.jsx:
--------------------------------------------------------------------------------
1 | import cx from 'classnames';
2 | import moment from 'moment';
3 | import PropTypes from 'prop-types';
4 | import React, { PureComponent } from 'react';
5 | import { uncontrollable } from 'uncontrollable';
6 | import Calendar from './Calendar';
7 | import styles from './index.styl';
8 |
9 | const KEYCODE_BACKSPACE = 8;
10 | const KEYCODE_TAB = 9;
11 | const KEYCODE_ESCAPE = 27;
12 | const KEYCODE_PAGE_UP = 33;
13 | const KEYCODE_PAGE_DOWN = 34;
14 | const KEYCODE_UP_ARROW = 38;
15 | const KEYCODE_DOWN_ARROW = 40;
16 | const KEYCODE_SPACE = 32;
17 | const KEYCODE_DELETE = 46;
18 |
19 | const SILHOUETTE = '0001-01-01';
20 |
21 | const getGroups = (str) => {
22 | return str.split(/[\-\s+]/);
23 | };
24 |
25 | const getGroupId = (index) => {
26 | if (index < 5) {
27 | return 0;
28 | }
29 | if (index < 8) {
30 | return 1;
31 | }
32 | return 2;
33 | };
34 |
35 | const replaceCharAt = (string, index, replace) => {
36 | return string.substring(0, index) + replace + string.substring(index + 1);
37 | };
38 |
39 | const isValidDate = (date) => {
40 | if (!date) {
41 | return false;
42 | }
43 |
44 | return moment(date).isValid();
45 | };
46 |
47 | class DateInput extends PureComponent {
48 | static propTypes = {
49 | value: PropTypes.string,
50 |
51 | // The minimum date. When set to null, there is no minimum.
52 | // Types supported:
53 | // * Date: A date object containing the minimum date.
54 | // * String: A date string in ISO 8601 format (i.e. YYYY-MM-DD).
55 | minDate: PropTypes.oneOfType([
56 | PropTypes.object,
57 | PropTypes.string
58 | ]),
59 |
60 | // The maximum date. When set to null, there is no maximum.
61 | // Types supported:
62 | // * Date: A date object containing the maximum date.
63 | // * String: A date string in ISO 8601 format (i.e. YYYY-MM-DD).
64 | maxDate: PropTypes.oneOfType([
65 | PropTypes.object,
66 | PropTypes.string
67 | ]),
68 |
69 | // Called when the value changes.
70 | onChange: PropTypes.func
71 | };
72 | static defaultProps = {
73 | value: '0000-00-00'
74 | };
75 |
76 | input = null;
77 | mounted = false;
78 | state = {
79 | focused: false,
80 | caretIndex: null
81 | };
82 |
83 | handleDateOutOfRange = () => {
84 | if (typeof this.props.onChange !== 'function') {
85 | return;
86 | }
87 |
88 | const date = moment(this.props.value);
89 |
90 | if (isValidDate(this.props.minDate)) {
91 | const minDate = moment(this.props.minDate).startOf('day');
92 | if (date.isBefore(minDate)) {
93 | this.props.onChange(minDate.format('YYYY-MM-DD'));
94 | }
95 | }
96 |
97 | if (isValidDate(this.props.maxDate)) {
98 | const maxDate = moment(this.props.maxDate).endOf('day');
99 | if (date.isAfter(maxDate)) {
100 | this.props.onChange(maxDate.format('YYYY-MM-DD'));
101 | }
102 | }
103 | };
104 |
105 | handleFocus = (event) => {
106 | if (this.mounted) {
107 | this.setState({ focused: true });
108 | }
109 | };
110 |
111 | handleBlur = (event) => {
112 | if (this.mounted) {
113 | this.setState({ caretIndex: null, focused: false });
114 | this.handleDateOutOfRange();
115 | }
116 | };
117 |
118 | handleChange = (event) => {
119 | let value = this.props.value;
120 | let newValue = this.input.value;
121 | let diff = newValue.length - value.length;
122 | let end = this.input.selectionStart;
123 | let insertion;
124 | let start = end - Math.abs(diff);
125 |
126 | event.preventDefault();
127 |
128 | if (diff > 0) {
129 | insertion = newValue.slice(end - diff, end);
130 | while (diff--) {
131 | const oldChar = value.charAt(start);
132 | const newChar = insertion.charAt(0);
133 | if (this.isSeparator(oldChar)) {
134 | if (this.isSeparator(newChar)) {
135 | insertion = insertion.slice(1);
136 | start++;
137 | } else {
138 | start++;
139 | diff++;
140 | end++;
141 | }
142 | } else {
143 | value = replaceCharAt(value, start, newChar);
144 | insertion = insertion.slice(1);
145 | start++;
146 | }
147 | }
148 | newValue = value;
149 | } else {
150 | if (newValue.charAt(start) === '-') {
151 | start++;
152 | }
153 | // apply default to selection
154 | let result = value;
155 | for (let i = start; i < end; i++) {
156 | result = replaceCharAt(result, i, newValue.charAt(i));
157 | }
158 | newValue = result;
159 | }
160 |
161 | if (newValue.length > SILHOUETTE.length) {
162 | return;
163 | }
164 |
165 | const m = moment(newValue);
166 | if (m.isValid()) {
167 | if (newValue.charAt(end) === '-') {
168 | end++;
169 | }
170 | this.onChange(newValue, end);
171 | } else {
172 | const caretIndex = this.props.value.length - (newValue.length - end);
173 | if (this.mounted) {
174 | this.setState({ caretIndex: caretIndex });
175 | }
176 | }
177 | };
178 |
179 | handleKeyDown = (event) => {
180 | event.stopPropagation();
181 |
182 | if (event.which === KEYCODE_BACKSPACE) {
183 | this.handleBackspace(event);
184 | return;
185 | }
186 | if (event.which === KEYCODE_TAB) {
187 | this.handleTab(event);
188 | return;
189 | }
190 | if (event.which === KEYCODE_ESCAPE) {
191 | this.handleEscape(event);
192 | return;
193 | }
194 | if (event.which === KEYCODE_SPACE || event.which === KEYCODE_DELETE) {
195 | this.handleForwardspace(event);
196 | return;
197 | }
198 | if (event.which === KEYCODE_PAGE_UP) {
199 | this.handlePageUp(event);
200 | return;
201 | }
202 | if (event.which === KEYCODE_PAGE_DOWN) {
203 | this.handlePageDown(event);
204 | return;
205 | }
206 | if (event.which === KEYCODE_UP_ARROW) {
207 | this.handleUpArrow(event);
208 | return;
209 | }
210 | if (event.which === KEYCODE_DOWN_ARROW) {
211 | this.handleDownArrow(event);
212 | return;
213 | }
214 | };
215 |
216 | handleEscape = () => {
217 | if (this.mounted) {
218 | this.input.blur();
219 | }
220 | };
221 |
222 | handleTab = (event) => {
223 | const start = this.input.selectionStart;
224 | const value = this.props.value;
225 | const groups = getGroups(value);
226 | let groupId = getGroupId(start);
227 | if (event.shiftKey) {
228 | if (!groupId) {
229 | return;
230 | }
231 | groupId--;
232 | } else {
233 | if (groupId >= (groups.length - 1)) {
234 | return;
235 | }
236 | groupId++;
237 | }
238 |
239 | event.preventDefault();
240 |
241 | let index = 0; // YYYY-MM-DD
242 | if (groupId === 1) {
243 | index = (4 + 1);
244 | }
245 | if (groupId === 2) {
246 | index = (4 + 1) + (2 + 1);
247 | }
248 | if (this.props.value.charAt(index) === ' ') {
249 | index++;
250 | }
251 | if (this.mounted) {
252 | this.setState({ caretIndex: index });
253 | }
254 | };
255 |
256 | handlePageUp = (event) => {
257 | event.preventDefault();
258 |
259 | const m = moment(this.props.value);
260 | if (!m.isValid()) {
261 | return;
262 | }
263 |
264 | const value = m.subtract(1, 'months').format('YYYY-MM-DD');
265 | const start = this.input.selectionStart;
266 | this.onChange(value, start);
267 | };
268 |
269 | handlePageDown = (event) => {
270 | event.preventDefault();
271 |
272 | const m = moment(this.props.value);
273 | if (!m.isValid()) {
274 | return;
275 | }
276 |
277 | const value = m.add(1, 'months').format('YYYY-MM-DD');
278 | const start = this.input.selectionStart;
279 | this.onChange(value, start);
280 | };
281 |
282 | handleUpArrow = (event) => {
283 | event.preventDefault();
284 |
285 | const start = this.input.selectionStart;
286 | const groupId = getGroupId(start);
287 | const unit = {
288 | 0: 'years',
289 | 1: 'months',
290 | 2: 'days'
291 | }[groupId];
292 |
293 | if (!unit) {
294 | return;
295 | }
296 |
297 | const m = moment(this.props.value);
298 | if (!m.isValid()) {
299 | return;
300 | }
301 |
302 | const value = m.add(1, unit).format('YYYY-MM-DD');
303 | this.onChange(value, start);
304 | };
305 |
306 | handleDownArrow = (event) => {
307 | event.preventDefault();
308 |
309 | const start = this.input.selectionStart;
310 | const groupId = getGroupId(start);
311 | const unit = {
312 | 0: 'years',
313 | 1: 'months',
314 | 2: 'days'
315 | }[groupId];
316 |
317 | if (!unit) {
318 | return;
319 | }
320 |
321 | const m = moment(this.props.value);
322 | if (!m.isValid()) {
323 | return;
324 | }
325 |
326 | const value = m.subtract(1, unit).format('YYYY-MM-DD');
327 | this.onChange(value, start);
328 | };
329 |
330 | handleBackspace = (event) => {
331 | event.preventDefault();
332 |
333 | let value = this.props.value;
334 | let start = this.input.selectionStart;
335 | let end = this.input.selectionEnd;
336 |
337 | if (!start && !end) {
338 | return;
339 | }
340 |
341 | let diff = end - start;
342 | const silhouette = this.silhouette();
343 |
344 | if (!diff) {
345 | if (value[start - 1] === '-') {
346 | start--;
347 | }
348 | value = replaceCharAt(value, start - 1, silhouette.charAt(start - 1));
349 | start--;
350 | } else {
351 | while (diff--) {
352 | if (value[end - 1] !== '-') {
353 | value = replaceCharAt(value, end - 1, silhouette.charAt(end - 1));
354 | }
355 | end--;
356 | }
357 | if (value.charAt(start - 1) === '-') {
358 | start--;
359 | }
360 | }
361 |
362 | this.onChange(value, start);
363 | };
364 |
365 | handleForwardspace = (event) => {
366 | event.preventDefault();
367 |
368 | let start = this.input.selectionStart;
369 | let value = this.props.value;
370 | let end = this.input.selectionEnd;
371 |
372 | if (start === end === (value.length - 1)) {
373 | return;
374 | }
375 |
376 | let diff = end - start;
377 | const silhouette = this.silhouette();
378 |
379 | if (!diff) {
380 | if (value[start] === '-') {
381 | start++;
382 | }
383 | value = replaceCharAt(value, start, silhouette.charAt(start));
384 | start++;
385 | } else {
386 | while (diff--) {
387 | if (value[end - 1] !== '-') {
388 | value = replaceCharAt(value, start, silhouette.charAt(start));
389 | }
390 | start++;
391 | }
392 | }
393 |
394 | if (value.charAt(start) === '-') {
395 | start++;
396 | }
397 |
398 | this.onChange(value, start);
399 | };
400 |
401 | isSeparator = (char) => {
402 | return /[:\s]/.test(char);
403 | };
404 |
405 | onChange = (str, caretIndex) => {
406 | const m = moment(str);
407 | if (m.isValid()) {
408 | this.props.onChange && this.props.onChange(str);
409 | }
410 | if (this.mounted && typeof caretIndex === 'number') {
411 | this.setState({ caretIndex: caretIndex });
412 | }
413 | };
414 |
415 | silhouette = () => {
416 | return this.props.value.replace(/\d/g, (val, i) => {
417 | return SILHOUETTE.charAt(i);
418 | });
419 | };
420 |
421 | componentDidMount () {
422 | this.mounted = true;
423 | this.handleDateOutOfRange();
424 | }
425 | componentWillUnmount () {
426 | this.mounted = false;
427 | }
428 | componentDidUpdate() {
429 | const index = this.state.caretIndex;
430 | if (index || index === 0) {
431 | const selectionStart = index;
432 | const selectionEnd = index;
433 | this.input.setSelectionRange(selectionStart, selectionEnd);
434 | }
435 | }
436 | render() {
437 | const icon = (
438 |
439 | );
440 |
441 | return (
442 |
446 |
447 | {
449 | this.input = node;
450 | }}
451 | type="text"
452 | value={this.props.value}
453 | onChange={this.handleChange}
454 | onFocus={this.handleFocus}
455 | onBlur={this.handleBlur}
456 | onKeyDown={this.handleKeyDown}
457 | />
458 |
459 | {icon}
460 |
461 | );
462 | }
463 | }
464 |
465 | export default uncontrollable(DateInput, {
466 | // Define the pairs of prop/handlers you want to be uncontrollable
467 | value: 'onChange'
468 | });
469 |
--------------------------------------------------------------------------------
/examples/DateTimeRangePicker/Dropdown.jsx:
--------------------------------------------------------------------------------
1 | import _isInteger from 'lodash/isInteger';
2 | import moment from 'moment';
3 | import PropTypes from 'prop-types';
4 | import React, { PureComponent } from 'react';
5 | import Anchor from '@trendmicro/react-anchor';
6 | import { Button } from '@trendmicro/react-buttons';
7 | import Dropdown, { MenuItem } from '@trendmicro/react-dropdown';
8 | import DateTimeRangePicker from './DateTimeRangePicker';
9 |
10 | const normalizeDateString = (dateString) => {
11 | let m = moment(dateString);
12 | if (!m.isValid()) {
13 | m = moment();
14 | }
15 | return m.format('YYYY-MM-DD');
16 | };
17 |
18 | const normalizeTimeString = (timeString) => {
19 | let [hh = '00', mm = '00', ss = '00'] = timeString.split(':');
20 | hh = Number(hh) || 0;
21 | mm = Number(mm) || 0;
22 | ss = Number(ss) || 0;
23 | hh = (hh < 0 || hh > 23) ? '00' : ('0' + hh).slice(-2);
24 | mm = (mm < 0 || mm > 59) ? '00' : ('0' + mm).slice(-2);
25 | ss = (ss < 0 || ss > 59) ? '00' : ('0' + ss).slice(-2);
26 | return `${hh}:${mm}:${ss}`;
27 | };
28 |
29 | const mapPeriodToString = (period) => {
30 | if (period === 'custom') {
31 | return 'Custom range...';
32 | }
33 |
34 | // Only days are supported (e.g. 1, 7, '1d', or '7d')
35 | if (_isInteger(period) || period.match(/^\d+d$/)) {
36 | const days = parseInt(period, 10);
37 | if (days === 1) {
38 | return 'Today';
39 | }
40 | if (days > 1) {
41 | return `Last ${days} days`;
42 | }
43 | }
44 |
45 | return '';
46 | };
47 |
48 | class DateTimeRangePickerDropdown extends PureComponent {
49 | static propTypes = {
50 | locale: PropTypes.string,
51 | startDate: PropTypes.string,
52 | startTime: PropTypes.string,
53 | endDate: PropTypes.string,
54 | endTime: PropTypes.string,
55 | minDate: PropTypes.string,
56 | maxDate: PropTypes.string,
57 | periods: PropTypes.arrayOf(
58 | PropTypes.oneOfType([
59 | PropTypes.number,
60 | PropTypes.string
61 | ])
62 | ),
63 | period: PropTypes.oneOfType([
64 | PropTypes.number,
65 | PropTypes.string
66 | ]),
67 | defaultPeriod: PropTypes.oneOfType([
68 | PropTypes.number,
69 | PropTypes.string
70 | ]),
71 | onSelect: PropTypes.func
72 | };
73 | static defaultProps = {
74 | periods: ['1d', '7d', '14d', '30d', '60d'],
75 | defaultPeriod: '7d'
76 | };
77 |
78 | state = this.getInitialState();
79 |
80 | changeStartDate = (date) => {
81 | if (!date) {
82 | return;
83 | }
84 |
85 | this.setState(state => {
86 | const startDate = normalizeDateString(date);
87 | const endDate = normalizeDateString(state.nextEndDate);
88 | const startTime = normalizeTimeString(state.nextStartTime);
89 | const endTime = normalizeTimeString(state.nextEndTime);
90 | const isoStartDateTime = `${startDate}T${startTime}`;
91 | const isoEndDateTime = `${endDate}T${endTime}`;
92 | const isEndDateAfterMaxDate = state.maxDate && moment(endDate).isAfter(moment(state.maxDate).endOf('day'));
93 | const isSameOrAfterEnd = moment(isoStartDateTime).isSameOrAfter(isoEndDateTime);
94 |
95 | let nextEndDate = endDate;
96 | if (isEndDateAfterMaxDate) {
97 | nextEndDate = normalizeDateString(state.maxDate);
98 | } else if (isSameOrAfterEnd) {
99 | nextEndDate = startDate;
100 | }
101 |
102 | return {
103 | nextStartDate: startDate,
104 | nextEndDate: nextEndDate,
105 | nextStartTime: startTime,
106 | nextEndTime: isSameOrAfterEnd ? startTime : endTime
107 | };
108 | });
109 | };
110 |
111 | changeEndDate = (date) => {
112 | if (!date) {
113 | return;
114 | }
115 |
116 | this.setState(state => {
117 | const startDate = normalizeDateString(state.nextStartDate);
118 | const endDate = normalizeDateString(date);
119 | const startTime = normalizeTimeString(state.nextStartTime);
120 | const endTime = normalizeTimeString(state.nextEndTime);
121 | const isoStartDateTime = `${startDate}T${startTime}`;
122 | const isoEndDateTime = `${endDate}T${endTime}`;
123 | const isStartDateBeforeMinDate = state.minDate && moment(startDate).isBefore(moment(state.minDate).startOf('day'));
124 | const isSameOrBeforeStart = moment(isoEndDateTime).isSameOrBefore(isoStartDateTime);
125 |
126 | let nextStartDate = startDate;
127 | if (isStartDateBeforeMinDate) {
128 | nextStartDate = normalizeDateString(state.minDate);
129 | } else if (isSameOrBeforeStart) {
130 | nextStartDate = endDate;
131 | }
132 |
133 | return {
134 | nextStartDate: nextStartDate,
135 | nextEndDate: endDate,
136 | nextStartTime: isSameOrBeforeStart ? endTime : startTime,
137 | nextEndTime: endTime
138 | };
139 | });
140 | };
141 |
142 | changeStartTime = (time = '00:00:00') => {
143 | this.setState(state => {
144 | const startDate = normalizeDateString(state.nextStartDate);
145 | const endDate = normalizeDateString(state.nextEndDate);
146 | const startTime = normalizeTimeString(time);
147 | const endTime = normalizeTimeString(state.nextEndTime);
148 | const isoStartDateTime = `${startDate}T${startTime}`;
149 | const isoEndDateTime = `${endDate}T${endTime}`;
150 | const isSameOrAfterEnd = moment(isoStartDateTime).isSameOrAfter(isoEndDateTime);
151 |
152 | return {
153 | nextStartTime: startTime,
154 | nextEndTime: isSameOrAfterEnd ? startTime : endTime
155 | };
156 | });
157 | };
158 |
159 | changeEndTime = (time = '00:00:00') => {
160 | this.setState(state => {
161 | const startDate = normalizeDateString(state.nextStartDate);
162 | const endDate = normalizeDateString(state.nextEndDate);
163 | const startTime = normalizeTimeString(state.nextStartTime);
164 | const endTime = normalizeTimeString(time);
165 | const isoStartDateTime = `${startDate}T${startTime}`;
166 | const isoEndDateTime = `${endDate}T${endTime}`;
167 | const isSameOrBeforeStart = moment(isoEndDateTime).isSameOrBefore(isoStartDateTime);
168 |
169 | return {
170 | nextStartTime: isSameOrBeforeStart ? endTime : startTime,
171 | nextEndTime: endTime
172 | };
173 | });
174 | };
175 |
176 | handleDropdownSelect = (eventKey) => {
177 | const period = eventKey;
178 |
179 | this.setState(state => ({
180 | period: period,
181 |
182 | // Restore to previous state
183 | nextStartDate: state.startDate,
184 | nextStartTime: state.startTime,
185 | nextEndDate: state.endDate,
186 | nextEndTime: state.endTime
187 | }), () => {
188 | const { onSelect } = this.props;
189 |
190 | if (typeof onSelect !== 'function') {
191 | return;
192 | }
193 |
194 | if (period !== 'custom') {
195 | onSelect({ period });
196 | }
197 | });
198 | };
199 |
200 | handleDropdownClose = () => {
201 | this.setState(state => ({
202 | open: false,
203 |
204 | // Restore to previous state
205 | period: this.props.period !== undefined ? this.props.period : state.period,
206 | nextStartDate: state.startDate,
207 | nextStartTime: state.startTime,
208 | nextEndDate: state.endDate,
209 | nextEndTime: state.endTime
210 | }));
211 | };
212 |
213 | handleDropdownToggle = (open) => {
214 | this.setState(state => {
215 | const { period } = state;
216 | return {
217 | open: open || period === 'custom'
218 | };
219 | });
220 | };
221 |
222 | handleClickApplyForCustomRange = () => {
223 | this.setState(state => ({
224 | open: false,
225 |
226 | // Apply specified range
227 | startDate: state.nextStartDate,
228 | startTime: state.nextStartTime,
229 | endDate: state.nextEndDate,
230 | endTime: state.nextEndTime
231 | }), () => {
232 | const { onSelect } = this.props;
233 |
234 | if (typeof onSelect !== 'function') {
235 | return;
236 | }
237 |
238 | const { period, startDate, startTime, endDate, endTime } = this.state;
239 | if (period === 'custom') {
240 | onSelect({ period, startDate, startTime, endDate, endTime });
241 | } else {
242 | onSelect({ period });
243 | }
244 | });
245 | };
246 |
247 | handleClickCancelForCustomRange = () => {
248 | this.setState(state => ({
249 | open: false,
250 |
251 | // Restore to previous state
252 | nextStartDate: state.startDate,
253 | nextStartTime: state.startTime,
254 | nextEndDate: state.endDate,
255 | nextEndTime: state.endTime
256 | }));
257 | };
258 |
259 | getInitialState() {
260 | const now = moment().seconds(0);
261 | const days = parseInt(this.props.defaultPeriod, 10) || parseInt(DateTimeRangePickerDropdown.defaultProps.defaultPeriod, 10) || 0;
262 | const startOfDay = moment(now).startOf('day');
263 | const endOfDay = moment(now).endOf('day');
264 | const {
265 | startDate = moment(startOfDay).subtract((days > 0) ? (days - 1) : 0, 'days').format('YYYY-MM-DD'),
266 | startTime = moment(startOfDay).subtract((days > 0) ? (days - 1) : 0, 'days').format('HH:mm:ss'),
267 | endDate = moment(endOfDay).format('YYYY-MM-DD'),
268 | endTime = moment(endOfDay).format('HH:mm:ss'),
269 | minDate = null, // Defaults to no minimum
270 | maxDate = null, // Defaults to no maximum
271 | } = this.props;
272 |
273 | return {
274 | minDate: minDate,
275 | maxDate: maxDate,
276 |
277 | // prev
278 | startDate: startDate,
279 | startTime: startTime,
280 | endDate: endDate,
281 | endTime: endTime,
282 |
283 | // next
284 | nextStartDate: startDate,
285 | nextStartTime: startTime,
286 | nextEndDate: endDate,
287 | nextEndTime: endTime,
288 |
289 | // Dropdown
290 | open: false,
291 | period: this.props.period
292 | };
293 | }
294 |
295 | componentDidUpdate(prevProps, prevState) {
296 | const { period: prevPeriod } = prevProps;
297 | const { period: nextPeriod } = this.props;
298 | if (prevPeriod !== nextPeriod) {
299 | this.handleDropdownSelect(nextPeriod);
300 | }
301 | }
302 |
303 | render() {
304 | const { locale, periods } = this.props;
305 | const {
306 | minDate, maxDate,
307 | startDate, startTime, endDate, endTime,
308 | nextStartDate, nextStartTime, nextEndDate, nextEndTime
309 | } = this.state;
310 | const period = this.state.period !== undefined ? this.state.period : this.props.defaultPeriod;
311 | const showDateTimeRangePicker = this.state.open && (period === 'custom');
312 |
313 | return (
314 |
315 |
Dropdown
316 |
Selected: {mapPeriodToString(period)}
317 | {period === 'custom' &&
318 |
319 | - Minimum: {minDate}
320 | - Maximum: {maxDate}
321 | - Range: {startDate} {startTime} - {endDate} {endTime}
322 |
323 | }
324 |
330 |
331 | {mapPeriodToString(period)}
332 |
333 |
338 |
339 | {periods.map(period => (
340 |
343 | ))}
344 |
345 |
348 |
349 | {showDateTimeRangePicker &&
350 |
357 |
370 |
371 |
372 |
379 |
382 |
383 |
384 |
385 | }
386 |
387 |
388 |
389 | );
390 | }
391 | }
392 |
393 | export default DateTimeRangePickerDropdown;
394 |
--------------------------------------------------------------------------------