├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── README.md ├── babel.config.js ├── build ├── MonthPicker.js ├── WeekDay.js ├── YearPicker.js ├── index.js └── locale.js ├── demo ├── css │ └── demo.css ├── index.html ├── js │ └── demo.js ├── jsx │ └── demo.js ├── less │ ├── calendar.less │ └── demo.less └── webpack.config.js ├── package-lock.json ├── package.json ├── src ├── MonthPicker.js ├── WeekDay.js ├── YearPicker.js ├── index.js └── locale.js └── test ├── datepicker.js └── utils ├── jsdom.js └── render.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@detools/eslint-config" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | test 3 | src 4 | coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": true, 10 | "arrowParens": "avoid" 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 10.15.3 5 | branches: 6 | only: 7 | - master 8 | script: npm run travis 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-date-range-picker 2 | 3 | DateRange Picker as React Component 4 | 5 | [![npm version](https://img.shields.io/npm/v/react-date-range-picker.svg?style=flat)](https://www.npmjs.com/package/react-date-range-picker) 6 | [![dependencies](http://img.shields.io/david/isnifer/react-date-range-picker.svg?style=flat)](https://david-dm.org/isnifer/react-date-range-picker) 7 | [![tests](https://travis-ci.org/isnifer/react-date-range-picker.svg?branch=master)](https://travis-ci.org/isnifer/react-date-range-picker) 8 | [![Coverage Status](https://coveralls.io/repos/isnifer/react-date-range-picker/badge.svg?branch=master&service=github)](https://coveralls.io/github/isnifer/react-date-range-picker?branch=master) 9 | 10 | ### Installation 11 | 12 | `npm i react-date-range-picker` 13 | 14 | ### Example 15 | 16 | ```js 17 | import React from 'react' 18 | import { render } from 'react-dom' 19 | import Datepicker from 'react-date-range-picker' 20 | 21 | class Demo extends React.Component { 22 | state = { 23 | date: new Date().toLocaleString(), 24 | } 25 | 26 | onClick = date => { 27 | this.setState({ date: date.toLocaleString() }) 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 | 34 | 35 |
36 | ) 37 | } 38 | } 39 | 40 | render(, document.getElementById('app')) 41 | ``` 42 | 43 | ![2015-08-18 11-30-21 react daterange picker](https://cloud.githubusercontent.com/assets/1788245/9325674/bc1df256-459c-11e5-9bb4-5d113eef9e8e.png) 44 | 45 | ## Demo is here 46 | 47 | https://isnifer.github.io/react-calendar/ 48 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const getRealPaths = plugin => 2 | Array.isArray(plugin) ? [require.resolve(plugin[0]), ...plugin.slice(1)] : require.resolve(plugin) 3 | 4 | module.exports = { 5 | presets: [[require.resolve('@babel/preset-env'), { modules: false }]], 6 | plugins: [ 7 | ['@babel/plugin-proposal-class-properties', { loose: false }], 8 | ['@babel/plugin-proposal-decorators', { legacy: true }], 9 | '@babel/plugin-proposal-export-default-from', 10 | '@babel/plugin-proposal-export-namespace-from', 11 | '@babel/plugin-proposal-object-rest-spread', 12 | '@babel/plugin-syntax-dynamic-import', 13 | '@babel/plugin-syntax-jsx', 14 | '@babel/plugin-transform-runtime', 15 | 'babel-plugin-dynamic-import-node', 16 | 'babel-plugin-lodash', 17 | ].map(getRealPaths), 18 | } 19 | -------------------------------------------------------------------------------- /build/MonthPicker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _classnames = require('classnames'); 14 | 15 | var _classnames2 = _interopRequireDefault(_classnames); 16 | 17 | var _momentRange = require('moment-range'); 18 | 19 | var _momentRange2 = _interopRequireDefault(_momentRange); 20 | 21 | var _locale = require('./locale'); 22 | 23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 24 | 25 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 26 | 27 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 28 | 29 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 30 | 31 | var TODAY_MONTH = new Date().getMonth(); 32 | 33 | var MonthPicker = function (_Component) { 34 | _inherits(MonthPicker, _Component); 35 | 36 | function MonthPicker(props) { 37 | _classCallCheck(this, MonthPicker); 38 | 39 | var _this = _possibleConstructorReturn(this, (MonthPicker.__proto__ || Object.getPrototypeOf(MonthPicker)).call(this, props)); 40 | 41 | _initialiseProps.call(_this); 42 | 43 | _this.state = { 44 | month: parseInt(props.currentMonth, 10) || TODAY_MONTH 45 | }; 46 | return _this; 47 | } 48 | 49 | _createClass(MonthPicker, [{ 50 | key: 'componentWillReceiveProps', 51 | value: function componentWillReceiveProps(_ref) { 52 | var month = _ref.month; 53 | 54 | if (month !== this.props.month) { 55 | this.setState({ month: month }); 56 | } 57 | } 58 | }, { 59 | key: 'render', 60 | value: function render() { 61 | var _this2 = this; 62 | 63 | return _react2.default.createElement( 64 | 'div', 65 | { className: 'calendar__months' }, 66 | _locale.MONTH_NAMES[this.props.locale].map(function (item, i) { 67 | var range = new _momentRange2.default(new Date(_this2.props.year, i, 1), new Date(_this2.props.year, i, 31)); 68 | var disabled = !range.overlaps(_this2.props.range); 69 | return _react2.default.createElement( 70 | 'div', 71 | { 72 | className: (0, _classnames2.default)('calendar__month-item', { 'calendar__month-item_current': i === _this2.state.month }, { 'calendar__month-item_disabled': disabled }), 73 | onClick: _this2.onClick, 74 | id: 'month_' + i + '_' + disabled, 75 | key: i }, 76 | item 77 | ); 78 | }) 79 | ); 80 | } 81 | }]); 82 | 83 | return MonthPicker; 84 | }(_react.Component); 85 | 86 | var _initialiseProps = function _initialiseProps() { 87 | var _this3 = this; 88 | 89 | this.onClick = function (_ref2) { 90 | var target = _ref2.target; 91 | 92 | var props = target.id.split('_'); 93 | if (props[2] === 'true') { 94 | return false; 95 | } 96 | 97 | var id = props[1]; 98 | _this3.props.onClick(parseInt(id, 10)); 99 | }; 100 | }; 101 | 102 | MonthPicker.propTypes = { 103 | locale: _react.PropTypes.string.isRequired, 104 | onClick: _react.PropTypes.func.isRequired, 105 | range: _react.PropTypes.instanceOf(_momentRange2.default).isRequired, 106 | year: _react.PropTypes.oneOfType([_react.PropTypes.string, _react.PropTypes.number]), 107 | currentMonth: _react.PropTypes.oneOfType([_react.PropTypes.string, _react.PropTypes.number]) 108 | }; 109 | 110 | exports.default = MonthPicker; -------------------------------------------------------------------------------- /build/WeekDay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _classnames = require('classnames'); 14 | 15 | var _classnames2 = _interopRequireDefault(_classnames); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | var WeekDay = function (_React$Component) { 26 | _inherits(WeekDay, _React$Component); 27 | 28 | function WeekDay(props) { 29 | _classCallCheck(this, WeekDay); 30 | 31 | var _this = _possibleConstructorReturn(this, (WeekDay.__proto__ || Object.getPrototypeOf(WeekDay)).call(this, props)); 32 | 33 | _this.inRange = _this.inRange.bind(_this); 34 | _this.onClick = _this.onClick.bind(_this); 35 | return _this; 36 | } 37 | 38 | _createClass(WeekDay, [{ 39 | key: 'inRange', 40 | value: function inRange() { 41 | return this.props.range.contains(this.props.date); 42 | } 43 | }, { 44 | key: 'onClick', 45 | value: function onClick(e) { 46 | return this.inRange() ? this.props.onClick(this.props.date) : e.preventDefault(); 47 | } 48 | }, { 49 | key: 'render', 50 | value: function render() { 51 | var inRange = this.inRange(); 52 | var className = (0, _classnames2.default)('calendar__day', { calendar__day_available: inRange }, { calendar__day_disabled: !inRange }, { calendar__day_current: this.props.current }); 53 | return _react2.default.createElement( 54 | 'td', 55 | { className: 'calendar__cell' }, 56 | _react2.default.createElement( 57 | 'span', 58 | { className: className, onClick: this.onClick }, 59 | this.props.date.getDate() 60 | ) 61 | ); 62 | } 63 | }]); 64 | 65 | return WeekDay; 66 | }(_react2.default.Component); 67 | 68 | WeekDay.propTypes = { 69 | date: _react2.default.PropTypes.instanceOf(Date).isRequired, 70 | range: _react2.default.PropTypes.object.isRequired, 71 | onClick: _react2.default.PropTypes.func.isRequired 72 | }; 73 | 74 | exports.default = WeekDay; -------------------------------------------------------------------------------- /build/YearPicker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _classnames = require('classnames'); 14 | 15 | var _classnames2 = _interopRequireDefault(_classnames); 16 | 17 | var _momentRange = require('moment-range'); 18 | 19 | var _momentRange2 = _interopRequireDefault(_momentRange); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 26 | 27 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 28 | 29 | var range = function range(currentYear) { 30 | var year = parseInt(currentYear, 10); 31 | var tmpArray = []; 32 | 33 | for (var i = currentYear - 5; i <= currentYear + 6; i++) { 34 | tmpArray = tmpArray.concat(i); 35 | } 36 | 37 | return tmpArray; 38 | }; 39 | 40 | var TODAY_YEAR = new Date().getFullYear(); 41 | 42 | var YearPicker = function (_Component) { 43 | _inherits(YearPicker, _Component); 44 | 45 | function YearPicker(props) { 46 | _classCallCheck(this, YearPicker); 47 | 48 | var _this = _possibleConstructorReturn(this, (YearPicker.__proto__ || Object.getPrototypeOf(YearPicker)).call(this, props)); 49 | 50 | _initialiseProps.call(_this); 51 | 52 | _this.state = { 53 | year: parseInt(props.currentYear, 10) || TODAY_YEAR 54 | }; 55 | return _this; 56 | } 57 | 58 | _createClass(YearPicker, [{ 59 | key: 'componentWillReceiveProps', 60 | value: function componentWillReceiveProps(_ref) { 61 | var currentYear = _ref.currentYear; 62 | 63 | if (parseInt(currentYear, 10) !== this.props.year) { 64 | this.setState({ year: parseInt(currentYear, 10) }); 65 | } 66 | } 67 | }, { 68 | key: 'render', 69 | value: function render() { 70 | var _this2 = this; 71 | 72 | return _react2.default.createElement( 73 | 'div', 74 | { className: 'calendar__years' }, 75 | range(this.state.year).map(function (item, i) { 76 | var range = new _momentRange2.default(new Date(item, 0, 1), new Date(item, 11, 31)); 77 | var disabled = !range.overlaps(_this2.props.range); 78 | return _react2.default.createElement( 79 | 'div', 80 | { 81 | className: (0, _classnames2.default)('calendar__year', { calendar__year_current: item === _this2.state.year }, { calendar__year_disabled: disabled }), 82 | onClick: _this2.onClick, 83 | id: 'year_' + i + '_' + disabled, 84 | key: item }, 85 | item 86 | ); 87 | }) 88 | ); 89 | } 90 | }]); 91 | 92 | return YearPicker; 93 | }(_react.Component); 94 | 95 | var _initialiseProps = function _initialiseProps() { 96 | var _this3 = this; 97 | 98 | this.onClick = function (_ref2) { 99 | var _ref2$target = _ref2.target, 100 | textContent = _ref2$target.textContent, 101 | id = _ref2$target.id; 102 | 103 | var props = id.split('_'); 104 | if (props[2] === 'true') { 105 | return false; 106 | } 107 | 108 | _this3.props.onClick(parseInt(textContent, 10)); 109 | }; 110 | }; 111 | 112 | YearPicker.propTypes = { 113 | onClick: _react.PropTypes.func.isRequired, 114 | range: _react2.default.PropTypes.instanceOf(_momentRange2.default).isRequired, 115 | currentYear: _react.PropTypes.oneOfType([_react.PropTypes.string, _react.PropTypes.number]) 116 | }; 117 | 118 | exports.default = YearPicker; -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _momentRange = require('moment-range'); 16 | 17 | var _momentRange2 = _interopRequireDefault(_momentRange); 18 | 19 | var _calendar = require('calendar'); 20 | 21 | var _classnames = require('classnames'); 22 | 23 | var _classnames2 = _interopRequireDefault(_classnames); 24 | 25 | var _WeekDay = require('./WeekDay'); 26 | 27 | var _WeekDay2 = _interopRequireDefault(_WeekDay); 28 | 29 | var _YearPicker = require('./YearPicker'); 30 | 31 | var _YearPicker2 = _interopRequireDefault(_YearPicker); 32 | 33 | var _MonthPicker = require('./MonthPicker'); 34 | 35 | var _MonthPicker2 = _interopRequireDefault(_MonthPicker); 36 | 37 | var _locale = require('./locale'); 38 | 39 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 40 | 41 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 42 | 43 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 44 | 45 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 46 | 47 | /** 48 | * Reset time of Date to 00:00:00 49 | * @param {Date} date 50 | * @return {Date} 51 | */ 52 | function resetDate(date) { 53 | return new Date(date.toString().replace(/\d{2}:\d{2}:\d{2}/, '00:00:00')); 54 | } 55 | 56 | var id = 1; 57 | 58 | var Datepicker = function (_Component) { 59 | _inherits(Datepicker, _Component); 60 | 61 | function Datepicker(props) { 62 | _classCallCheck(this, Datepicker); 63 | 64 | var _this = _possibleConstructorReturn(this, (Datepicker.__proto__ || Object.getPrototypeOf(Datepicker)).call(this, props)); 65 | 66 | _this.setOuterDate = function (date) { 67 | _this.props.onClick(date, _this.props.name || 'date_' + _this.id); 68 | }; 69 | 70 | _this.onYearNameClick = function () { 71 | _this.setState({ 72 | dateVisible: false, 73 | monthVisible: false, 74 | yearVisible: true 75 | }); 76 | }; 77 | 78 | _this.onYearClick = function (year) { 79 | _this.restoreDateVisibility({ year: year }); 80 | }; 81 | 82 | _this.onMonthNameClick = function () { 83 | _this.setState({ 84 | dateVisible: false, 85 | monthVisible: true, 86 | yearVisible: false 87 | }); 88 | }; 89 | 90 | _this.onMonthClick = function (month) { 91 | _this.restoreDateVisibility({ month: month }); 92 | }; 93 | 94 | _this.restoreDateVisibility = function (model) { 95 | var month = model.month || _this.state.month; 96 | var year = model.year || _this.state.year; 97 | var day = _this.state.date.getDate(); 98 | var date = new Date(year, month, day, 0, 0, 0); 99 | 100 | _this.setState(_extends({ 101 | date: date, 102 | dateVisible: true, 103 | monthVisible: false, 104 | yearVisible: false 105 | }, model), _this.setOuterDate.bind(_this, date)); 106 | }; 107 | 108 | _this.state = { 109 | date: null, 110 | month: null, 111 | range: null, 112 | year: null, 113 | 114 | // UI: Visibility levels 115 | dateVisible: true, 116 | monthVisible: false, 117 | yearVisible: false 118 | }; 119 | 120 | _this.id = id++; 121 | 122 | _this.calendar = new _calendar.Calendar(props.locale === 'RU' ? 1 : 0); 123 | 124 | _this.onClick = _this.onClick.bind(_this); 125 | _this.changeMonth = _this.changeMonth.bind(_this); 126 | return _this; 127 | } 128 | 129 | // Setting up default state of calendar 130 | 131 | 132 | _createClass(Datepicker, [{ 133 | key: 'componentWillMount', 134 | value: function componentWillMount() { 135 | var TODAY = new Date(); 136 | 137 | // If we have defined 'initialDate' prop 138 | // we will use it also for define month and year. 139 | // If not it will be TODAY 140 | var initialDate = this.props.initialDate || TODAY; 141 | var initialRange = this.props.range || new _momentRange2.default(resetDate(this.props.minimumDate), resetDate(this.props.maximumDate)); 142 | var state = { 143 | date: initialDate, 144 | month: initialDate.getMonth(), 145 | year: initialDate.getFullYear() 146 | }; 147 | 148 | // Before set range to state we should check - 149 | // is range contains our initialDate. 150 | // If not, we will fire Error 151 | if (initialRange.contains(initialDate)) { 152 | state.range = initialRange; 153 | } else { 154 | throw new Error('Initial Range doesn\'t contains Initial Date'); 155 | } 156 | 157 | this.setState(state); 158 | } 159 | }, { 160 | key: 'componentWillReceiveProps', 161 | value: function componentWillReceiveProps(nextProps) { 162 | var range = void 0; 163 | 164 | if (nextProps.range !== this.props.range) { 165 | range = nextProps.range; 166 | } else if (nextProps.minimumDate !== this.props.minimumDate || nextProps.maximumDate !== this.props.maximumDate) { 167 | range = new _momentRange2.default(resetDate(nextProps.minimumDate), resetDate(nextProps.maximumDate)); 168 | } 169 | 170 | if (range) { 171 | this.setState({ range: range }); 172 | } 173 | } 174 | 175 | /** 176 | * Change month handler 177 | * @param {Number} direction - "1" or "-1" 178 | */ 179 | 180 | }, { 181 | key: 'changeMonth', 182 | value: function changeMonth(direction) { 183 | if (this.state.dateVisible) { 184 | var nextMonth = this.state.month + direction; 185 | var isMonthAvailable = nextMonth >= 0 && nextMonth <= 11; 186 | 187 | var model = void 0; 188 | if (!isMonthAvailable) { 189 | model = { 190 | month: direction === 1 ? 0 : 11, 191 | year: this.state.year + direction 192 | }; 193 | } else { 194 | model = { month: this.state.month + direction }; 195 | } 196 | 197 | this.setState(model); 198 | } 199 | 200 | if (this.state.yearVisible) { 201 | this.setState({ year: this.state.year + (direction + 1 ? 12 : -12) }); 202 | } 203 | } 204 | 205 | /** 206 | * Calendar state setter 207 | * @param {Date} date - selected date 208 | */ 209 | 210 | }, { 211 | key: 'onClick', 212 | value: function onClick(date) { 213 | if (date) { 214 | this.setState({ date: date }, this.setOuterDate.bind(this, date)); 215 | } 216 | } 217 | }, { 218 | key: 'renderMonth', 219 | value: function renderMonth() { 220 | var _this2 = this; 221 | 222 | // Array of weeks, which contains arrays of days 223 | // [[Date, Date, ...], [Date, Date, ...], ...] 224 | var month = this.calendar.monthDates(this.state.year, this.state.month); 225 | return month.map(function (week, i) { 226 | return _react2.default.createElement( 227 | 'tr', 228 | { className: 'calendar__week', key: i }, 229 | _this2.renderWeek(week) 230 | ); 231 | }); 232 | } 233 | 234 | /** 235 | * Render week 236 | * @param {Array} weekDays - array of instanceof Date 237 | * @return {Array} - array of Components 238 | */ 239 | 240 | }, { 241 | key: 'renderWeek', 242 | value: function renderWeek(weekDays) { 243 | var _this3 = this; 244 | 245 | return weekDays.map(function (day, i) { 246 | var isCurrent = _this3.state.date.toDateString() === day.toDateString(); 247 | return _react2.default.createElement(_WeekDay2.default, { 248 | key: i, 249 | date: day, 250 | range: _this3.state.range, 251 | current: isCurrent, 252 | onClick: _this3.onClick }); 253 | }); 254 | } 255 | }, { 256 | key: 'renderWeekdayNames', 257 | value: function renderWeekdayNames() { 258 | return _locale.WEEK_NAMES[this.props.locale].map(function (weekname, i) { 259 | return _react2.default.createElement( 260 | 'th', 261 | { className: 'calendar__weekday-name', key: i }, 262 | weekname 263 | ); 264 | }); 265 | } 266 | 267 | // TODO: Fix direction 268 | 269 | }, { 270 | key: 'renderNavigation', 271 | value: function renderNavigation() { 272 | return _react2.default.createElement( 273 | 'span', 274 | { className: 'calendar__arrows' }, 275 | _react2.default.createElement( 276 | 'span', 277 | { 278 | className: 'calendar__arrow calendar__arrow_left', 279 | onClick: this.changeMonth.bind(this, -1) }, 280 | '\u2190' 281 | ), 282 | _react2.default.createElement( 283 | 'span', 284 | { className: 'calendar__month-name' }, 285 | _react2.default.createElement( 286 | 'span', 287 | { className: 'calendar__head-month', onClick: this.onMonthNameClick }, 288 | _locale.MONTH_NAMES[this.props.locale][this.state.month] 289 | ), 290 | ', ', 291 | _react2.default.createElement( 292 | 'span', 293 | { className: 'calendar__head-year', onClick: this.onYearNameClick }, 294 | this.state.year 295 | ) 296 | ), 297 | _react2.default.createElement( 298 | 'span', 299 | { 300 | className: 'calendar__arrow calendar__arrow_right', 301 | onClick: this.changeMonth.bind(this, 1) }, 302 | '\u2192' 303 | ) 304 | ); 305 | } 306 | }, { 307 | key: 'render', 308 | value: function render() { 309 | return _react2.default.createElement( 310 | 'div', 311 | { className: 'calendar' }, 312 | _react2.default.createElement( 313 | 'div', 314 | { className: 'calendar__head' }, 315 | !this.props.disableNavigation && !this.props.outsideNavigation ? this.renderNavigation() : '' 316 | ), 317 | this.state.dateVisible && _react2.default.createElement( 318 | 'table', 319 | { className: 'calendar__month' }, 320 | _react2.default.createElement( 321 | 'thead', 322 | null, 323 | _react2.default.createElement( 324 | 'tr', 325 | { className: 'calendar__week-names' }, 326 | this.renderWeekdayNames() 327 | ) 328 | ), 329 | _react2.default.createElement( 330 | 'tbody', 331 | null, 332 | this.renderMonth() 333 | ) 334 | ), 335 | this.state.monthVisible && _react2.default.createElement(_MonthPicker2.default, { 336 | currentMonth: this.props.month, 337 | onClick: this.onMonthClick, 338 | locale: this.props.locale, 339 | range: this.state.range, 340 | year: this.state.year 341 | }), 342 | this.state.yearVisible && _react2.default.createElement(_YearPicker2.default, { currentYear: this.state.year, onClick: this.onYearClick, range: this.state.range }) 343 | ); 344 | } 345 | }]); 346 | 347 | return Datepicker; 348 | }(_react.Component); 349 | 350 | Datepicker.propTypes = { 351 | onClick: _react.PropTypes.func, 352 | range: _react.PropTypes.instanceOf(_momentRange2.default), 353 | disableNavigation: _react.PropTypes.bool, 354 | outsideNavigation: _react.PropTypes.bool, 355 | initialDate: _react.PropTypes.instanceOf(Date), 356 | locale: _react.PropTypes.string, 357 | minimumDate: _react.PropTypes.instanceOf(Date), 358 | maximumDate: _react.PropTypes.instanceOf(Date), 359 | name: _react.PropTypes.string 360 | }; 361 | 362 | Datepicker.defaultProps = { 363 | // Handler which will be execute when click on day 364 | onClick: function onClick() {}, 365 | 366 | // Instance of DateRange 367 | range: null, 368 | 369 | // If true, navigation will be hidden 370 | disableNavigation: false, 371 | 372 | // If true, navigation will be in root container 373 | outsideNavigation: false, 374 | 375 | // Available locales: RU, EN, DE, FR, IT, POR, ESP 376 | locale: 'EN', 377 | 378 | // Minimum available date 379 | minimumDate: new Date(1970, 0, 1), 380 | 381 | // Maximum available date 382 | maximumDate: new Date(2100, 0, 1) 383 | }; 384 | 385 | exports.default = Datepicker; -------------------------------------------------------------------------------- /build/locale.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var MONTH_NAMES = exports.MONTH_NAMES = { 7 | RU: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'], 8 | EN: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], 9 | DE: ['Januari', 'Februari', 'March', 'April', 'Kan', 'June', 'Juli', 'Augustus', 'September', 'Oktober', 'November', 'December'], 10 | FR: ['Janvier', 'Février', 'Mars', 'Avril', 'Peut', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'], 11 | ITA: ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giu', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'], 12 | POR: ['Janeiro', 'Fevereiro', 'Março', 'April', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'November', 'Dezembro'], 13 | ESP: ['Enero', 'Febrero', 'Marzo', 'Abril', 'Puede', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'] 14 | }; 15 | 16 | var WEEK_NAMES = exports.WEEK_NAMES = { 17 | RU: ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'], 18 | EN: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 19 | DE: ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'], 20 | FR: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'], 21 | IT: ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'], 22 | POR: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'], 23 | ESP: ['Dom.', 'Lun.', 'Mar.', 'Mié.', 'Jue.', 'Vie.', 'Sáb.'] 24 | }; 25 | 26 | var WEEK_NAMES_SHORT = exports.WEEK_NAMES_SHORT = { 27 | RU: ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'], 28 | EN: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], 29 | DE: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'], 30 | FR: ['Di', 'Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa'], 31 | IT: ['D', 'L', 'Ma', 'Me', 'G', 'V', 'S'], 32 | POR: ['Dom', '2ª', '3ª', '4ª', '5ª', '6ª', 'Sáb'], 33 | ESP: ['Do', 'Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sá'] 34 | }; -------------------------------------------------------------------------------- /demo/css/demo.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | font: normal 16px "Arial", Tahoma, Verdana; 10 | } 11 | .demo { 12 | padding: 50px 0 0 50px; 13 | } 14 | .demo__part { 15 | margin-bottom: 50px; 16 | } 17 | .demo__part:before, 18 | .demo__part:after { 19 | display: table; 20 | content: ''; 21 | width: 100%; 22 | } 23 | .demo__inline { 24 | float: left; 25 | margin-right: 20px; 26 | } 27 | .demo__description { 28 | font-size: 18px; 29 | line-height: 1.33333333; 30 | color: #333; 31 | } 32 | .demo__ru, 33 | .demo__en { 34 | position: relative; 35 | } 36 | .demo__ru:before, 37 | .demo__en:before { 38 | content: 'RU'; 39 | display: block; 40 | position: absolute; 41 | color: red; 42 | top: 3px; 43 | left: -18px; 44 | font-size: 10px; 45 | } 46 | .demo__ru { 47 | margin-bottom: 10px; 48 | } 49 | .demo__en { 50 | margin-bottom: 20px; 51 | } 52 | .demo__en:before { 53 | content: 'EN'; 54 | } 55 | .demo__title { 56 | margin-bottom: 5px; 57 | } 58 | .demo__props { 59 | margin-top: 20px; 60 | } 61 | .demo__code { 62 | margin: 0; 63 | display: inline-block; 64 | padding: 16px; 65 | overflow: auto; 66 | font-size: 85%; 67 | line-height: 1.45; 68 | background-color: #f7f7f7; 69 | border-radius: 3px; 70 | } 71 | .demo__input { 72 | margin-bottom: 5px; 73 | width: 300px; 74 | height: 40px; 75 | font-size: 25px; 76 | padding: 0 10px; 77 | } 78 | .demo__fork { 79 | position: absolute; 80 | display: block; 81 | overflow: hidden; 82 | top: 0; 83 | right: 0; 84 | border: 0; 85 | } 86 | .calendar { 87 | width: 300px; 88 | position: relative; 89 | overflow: hidden; 90 | -webkit-user-select: none; 91 | -moz-user-select: none; 92 | user-select: none; 93 | } 94 | .calendar__head { 95 | background: transparent; 96 | text-align: center; 97 | height: 40px; 98 | background: #FFCCBC; 99 | } 100 | .calendar__head-month { 101 | cursor: pointer; 102 | display: inline-block; 103 | border-bottom: 1px dotted rgba(0, 0, 0, 0.3); 104 | } 105 | .calendar__head-year { 106 | cursor: pointer; 107 | display: inline-block; 108 | border-bottom: 1px dotted rgba(0, 0, 0, 0.3); 109 | } 110 | .calendar__arrows { 111 | display: flex; 112 | flex-flow: row nowrap; 113 | justify-content: space-between; 114 | align-items: center; 115 | height: 40px; 116 | } 117 | .calendar__arrow { 118 | width: 40px; 119 | height: 40px; 120 | color: #FF5722; 121 | cursor: pointer; 122 | display: flex; 123 | align-items: center; 124 | justify-content: center; 125 | } 126 | .calendar__arrow:hover { 127 | background: #FF5722; 128 | color: #FFCCBC; 129 | } 130 | .calendar__month { 131 | margin: 0; 132 | border-collapse: collapse; 133 | } 134 | .calendar__weekday-name { 135 | text-align: center; 136 | width: 43px; 137 | height: 40px; 138 | padding: 0; 139 | background: #607D8B; 140 | color: #CFD8DC; 141 | } 142 | .calendar__cell { 143 | width: 43px; 144 | height: 40px; 145 | padding: 0; 146 | background: #CFD8DC; 147 | } 148 | .calendar__day { 149 | width: 43px; 150 | height: 40px; 151 | display: block; 152 | line-height: 40px; 153 | text-align: center; 154 | background: #CFD8DC; 155 | color: #727272; 156 | -webkit-user-select: none; 157 | -moz-user-select: none; 158 | user-select: none; 159 | } 160 | .calendar__day_other-month { 161 | opacity: 0.5; 162 | } 163 | .calendar__day_available { 164 | background: #4CAF50; 165 | color: #fff; 166 | cursor: pointer; 167 | } 168 | .calendar__day_available:hover { 169 | background: #AFB42B; 170 | } 171 | .calendar__day_disabled { 172 | background: #455A64; 173 | color: #B6B6B6; 174 | cursor: not-allowed; 175 | } 176 | .calendar__day_current { 177 | background: #673AB7; 178 | } 179 | .calendar__day_current_available.calendar__day_current:hover { 180 | background: #512DA8; 181 | } 182 | .calendar__years, 183 | .calendar__months { 184 | display: flex; 185 | flex-flow: row wrap; 186 | } 187 | .calendar__year, 188 | .calendar__month-item { 189 | width: 25%; 190 | height: 80px; 191 | display: flex; 192 | justify-content: center; 193 | align-items: center; 194 | background: #4CAF50; 195 | color: #fff; 196 | cursor: pointer; 197 | -webkit-user-select: none; 198 | -moz-user-select: none; 199 | user-select: none; 200 | } 201 | .calendar__year:hover, 202 | .calendar__month-item:hover { 203 | background: #AFB42B; 204 | } 205 | .calendar__year_current, 206 | .calendar__month-item_current, 207 | .calendar__year_current:hover, 208 | .calendar__month-item_current:hover { 209 | background: #673AB7; 210 | } 211 | .calendar__year_disabled, 212 | .calendar__month-item_disabled, 213 | .calendar__year_disabled:hover, 214 | .calendar__month-item_disabled:hover { 215 | background: #455A64; 216 | color: #B6B6B6; 217 | cursor: not-allowed; 218 | pointer-events: none; 219 | } 220 | .calendar__month-item { 221 | font-size: 14px; 222 | } 223 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React DateRange Picker 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/jsx/demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import DateRange from 'moment-range' 4 | import Datepicker from '../../src' 5 | 6 | function transformDate(date) { 7 | const day = date.getDate() 8 | 9 | let month = date.getMonth() + 1 10 | month = month < 10 ? `0${month}` : month 11 | 12 | return [day, month, date.getFullYear()].join('.') 13 | } 14 | 15 | const leftRangeDate = new Date(2012, 7, 20) 16 | const rightRangeDate = new Date(2016, 7, 27) 17 | 18 | class Demo extends React.Component { 19 | constructor(props) { 20 | super(props) 21 | 22 | this.state = { 23 | date_1: transformDate(new Date()), 24 | date_2: transformDate(new Date(2015, 7, 8)), 25 | date_3: transformDate(new Date(2015, 7, 12)), 26 | date_4: leftRangeDate, 27 | date_5: rightRangeDate, 28 | } 29 | 30 | this.onClick = this.onClick.bind(this) 31 | this.onClickTwo = this.onClickTwo.bind(this) 32 | this.onClickThree = this.onClickThree.bind(this) 33 | this.onClickFour = this.onClickFour.bind(this) 34 | this.onClickFive = this.onClickFive.bind(this) 35 | } 36 | 37 | onClick(date, name) { 38 | this.setState({ [name]: transformDate(date) }) 39 | } 40 | 41 | onClickTwo(date, name) { 42 | this.setState({ [name]: transformDate(date) }) 43 | } 44 | 45 | onClickThree(date, name) { 46 | this.setState({ [name]: transformDate(date) }) 47 | } 48 | 49 | onClickFour(date, name) { 50 | this.setState({ [name]: date }) 51 | } 52 | 53 | onClickFive(date, name) { 54 | this.setState({ [name]: date }) 55 | } 56 | 57 | render() { 58 | return ( 59 |
60 |
61 |
62 |
63 | Самый простой вариант календаря. Ему передан только callback,
64 | который в свою очередь будет возвращать дату календаря в качестве
65 | единственного аргумента 66 |
67 |
68 | Very simple calendar example. It got only callback, which will pass
69 | selected date as argument. 70 |
71 |
72 | 79 | 80 |
81 |
Source
82 |
{''}
83 |
84 |
85 |
86 |
87 |
88 | Чуть более сложный вариант календаря. Компонент получил три новых атрибута.
89 | Атрибут range - instanceof `moment-range`. Период календаря,
90 | задает пределы доступности выбора дат в календаре.
91 | Атрибут initialDate - instanceof `Date`. Дата, которая будет выбрана в 92 | календаре
93 | по умолчанию. Важно! Эта дата, должна быть частью периода календаря.
94 | Атрибут locale - instanceof `String`. Несколько встроенных локалей на выбор:{' '} 95 |
96 | RU, EN, DE, FR, ITA, POR, ESP 97 |
98 |
99 | A little more complex version of the calendar. The component has received three new 100 | attribute.
101 | Attribute range - instanceof `moment-range`. Period of the Calendar,
102 | sets limits accessibility to select dates on the calendar.
103 | Attribute initialDate - instanceof `Date`. Date to be selected on the 104 | calendar default.
105 | Important! This date should be part of the period of the calendar.
106 | Attribute locale - instanceof `String`. Several built-in locales to choose 107 | from:
108 | RU, EN, DE, FR, ITA, POR, ESP. 109 |
110 |
111 |
112 | 119 | 125 |
126 |
Source
127 |
128 |                 {''}
133 |               
134 |
135 |
136 |
137 | 144 | 150 |
151 |
Source
152 |
153 |                 {''}
158 |               
159 |
160 |
161 |
162 |
163 |
164 |
165 | Этот же вариант позволяет вам связать два календаря между собой.
166 | Довольно популярный кейс, когда у пользователя есть выбор дат С и ПО.
167 | Все, что необходимо - это зафиксировать изначально крайние даты,
168 | в данном примере это константы leftRangeDate и rightRangeDate.
169 | Передать их в качестве левого ограничения для календаря справа и
170 | правого ограничения для календаря справа соотвественно.
171 | Демо ниже более наглядно. 172 |
173 |
174 | This option allows us to link the two calendars together.
175 | Quite a popular case when the user has a choice of dates FROM and TO.
176 | All that is needed is to fix the original Dates,
177 | in this example leftRangeDate and rightRangeDate are constants.
178 | Send them as the left limit for the calendar on the left and
179 | right limit for the calendar to the right respectively.
180 | The demo below more clearly. 181 |
182 |
183 |
184 | 190 | 197 |
198 |
Source
199 |
200 |                 {'\n' +
205 |                   ''}
211 |               
212 |
213 |
214 |
215 | 221 | 228 |
229 |
Source
230 |
231 |                 {'\n' +
236 |                   ''}
242 |               
243 |
244 |
245 |
246 | 247 | Fork Me on Github 251 | 252 |
253 | ) 254 | } 255 | } 256 | 257 | render(, document.querySelector('.datepicker')) 258 | -------------------------------------------------------------------------------- /demo/less/calendar.less: -------------------------------------------------------------------------------- 1 | .no-select() { 2 | -webkit-user-select: none; 3 | -moz-user-select: none; 4 | user-select: none; 5 | } 6 | 7 | .calendar { 8 | width: 300px; 9 | position: relative; 10 | overflow: hidden; 11 | .no-select(); 12 | 13 | &__head { 14 | background: transparent; 15 | text-align: center; 16 | height: 40px; 17 | background: #FFCCBC; 18 | } 19 | 20 | &__head-month { 21 | cursor: pointer; 22 | display: inline-block; 23 | border-bottom: 1px dotted rgba(0,0,0, .3); 24 | } 25 | 26 | &__head-year { 27 | cursor: pointer; 28 | display: inline-block; 29 | border-bottom: 1px dotted rgba(0,0,0, .3); 30 | } 31 | 32 | &__arrows { 33 | display: flex; 34 | flex-flow: row nowrap; 35 | justify-content: space-between; 36 | align-items: center; 37 | height: 40px; 38 | } 39 | 40 | &__arrow { 41 | width: 40px; 42 | height: 40px; 43 | color: #FF5722; 44 | cursor: pointer; 45 | display: flex; 46 | align-items: center; 47 | justify-content: center; 48 | 49 | &:hover { 50 | background: #FF5722; 51 | color: #FFCCBC; 52 | } 53 | } 54 | 55 | &__month { 56 | margin: 0; 57 | border-collapse: collapse; 58 | } 59 | 60 | &__weekday-name { 61 | text-align: center; 62 | width: 43px; 63 | height: 40px; 64 | padding: 0; 65 | background: #607D8B; 66 | color: #CFD8DC; 67 | } 68 | 69 | &__cell { 70 | width: 43px; 71 | height: 40px; 72 | padding: 0; 73 | background: #CFD8DC; 74 | } 75 | 76 | &__day { 77 | width: 43px; 78 | height: 40px; 79 | display: block; 80 | line-height: 40px; 81 | text-align: center; 82 | background: #CFD8DC; 83 | color: #727272; 84 | .no-select(); 85 | 86 | &_other-month { 87 | opacity: 0.5; 88 | } 89 | 90 | &_available { 91 | background: #4CAF50; 92 | color: #fff; 93 | cursor: pointer; 94 | 95 | &:hover { 96 | background: #AFB42B; 97 | } 98 | } 99 | 100 | &_disabled { 101 | background: #455A64; 102 | color: #B6B6B6; 103 | cursor: not-allowed; 104 | } 105 | 106 | &_current { 107 | background: #673AB7; 108 | 109 | &_available&:hover { 110 | background: #512DA8; 111 | } 112 | } 113 | } 114 | 115 | &__years, 116 | &__months { 117 | display: flex; 118 | flex-flow: row wrap; 119 | } 120 | 121 | &__year, 122 | &__month-item { 123 | width: 25%; 124 | height: 80px; 125 | display: flex; 126 | justify-content: center; 127 | align-items: center; 128 | background: #4CAF50; 129 | color: #fff; 130 | cursor: pointer; 131 | .no-select(); 132 | 133 | &:hover { 134 | background: #AFB42B; 135 | } 136 | 137 | &_current, 138 | &_current:hover { 139 | background: #673AB7; 140 | } 141 | 142 | &_disabled, 143 | &_disabled:hover { 144 | background: #455A64; 145 | color: #B6B6B6; 146 | cursor: not-allowed; 147 | pointer-events: none; 148 | } 149 | } 150 | 151 | &__month-item { 152 | font-size: 14px; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /demo/less/demo.less: -------------------------------------------------------------------------------- 1 | .clearfix() { 2 | &:before, 3 | &:after { 4 | display: table; 5 | content: ''; 6 | width: 100%; 7 | } 8 | } 9 | 10 | * { 11 | -webkit-box-sizing: border-box; 12 | -moz-box-sizing: border-box; 13 | box-sizing: border-box; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | padding: 0; 19 | font: normal 16px "Arial", Tahoma, Verdana; 20 | } 21 | 22 | .demo { 23 | padding: 50px 0 0 50px; 24 | 25 | &__part { 26 | margin-bottom: 50px; 27 | .clearfix(); 28 | } 29 | 30 | &__inline { 31 | float: left; 32 | margin-right: 20px; 33 | } 34 | 35 | &__description { 36 | font-size: 18px; 37 | line-height: 24/18; 38 | color: #333; 39 | } 40 | 41 | &__ru, 42 | &__en { 43 | position: relative; 44 | 45 | &:before { 46 | content: 'RU'; 47 | display: block; 48 | position: absolute; 49 | color: red; 50 | top: 3px; 51 | left: -18px; 52 | font-size: 10px; 53 | } 54 | } 55 | 56 | &__ru { 57 | margin-bottom: 10px; 58 | } 59 | 60 | &__en { 61 | margin-bottom: 20px; 62 | 63 | &:before { 64 | content: 'EN'; 65 | } 66 | } 67 | 68 | &__title { 69 | margin-bottom: 5px; 70 | } 71 | 72 | &__props { 73 | margin-top: 20px; 74 | } 75 | 76 | &__code { 77 | margin: 0; 78 | display: inline-block; 79 | padding: 16px; 80 | overflow: auto; 81 | font-size: 85%; 82 | line-height: 1.45; 83 | background-color: #f7f7f7; 84 | border-radius: 3px; 85 | } 86 | 87 | &__input { 88 | margin-bottom: 5px; 89 | width: 300px; 90 | height: 40px; 91 | font-size: 25px; 92 | padding: 0 10px; 93 | } 94 | 95 | &__fork { 96 | position: absolute; 97 | display: block; 98 | overflow: hidden; 99 | top: 0; 100 | right: 0; 101 | border: 0; 102 | } 103 | } 104 | 105 | @import 'calendar'; 106 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import CleanWebpackPlugin from 'clean-webpack-plugin' 3 | import MiniCssExtractPlugin from 'mini-css-extract-plugin' 4 | 5 | const DEV_SERVER_PORT = 3000 6 | const IS_PRODUCTION = process.env.NODE_ENV === 'production' 7 | const PATH_TO_SRC = path.resolve(__dirname, '..', 'src') 8 | const PATH_TO_DIST = path.resolve(__dirname, 'js') 9 | const PATH_TO_DEV_DIST = path.resolve(__dirname, 'public') 10 | 11 | const CLEAN_OPTIONS = { 12 | // Instead of this ugly hack — we will get "wwwroot is outside of the project root. Skipping..." 13 | root: path.resolve(PATH_TO_DIST, '..'), 14 | exclude: ['index.html', 'favicon.png'], 15 | verbose: true, 16 | dry: false, 17 | } 18 | 19 | const productionPlugins = [ 20 | new CleanWebpackPlugin(PATH_TO_DIST, CLEAN_OPTIONS), 21 | new MiniCssExtractPlugin({ 22 | filename: '[name].css', 23 | chunkFilename: '[id].css', 24 | }), 25 | ] 26 | 27 | export default { 28 | mode: 'development', 29 | 30 | entry: path.resolve(PATH_TO_SRC, 'index.js'), 31 | 32 | output: { 33 | path: !IS_PRODUCTION ? PATH_TO_DEV_DIST : PATH_TO_DIST, 34 | filename: 'bundle.js', 35 | chunkFilename: '[name].bundle.js', 36 | publicPath: '/', 37 | }, 38 | 39 | resolve: { 40 | extensions: ['.js'], 41 | alias: { 42 | '@': PATH_TO_SRC, 43 | }, 44 | }, 45 | 46 | target: 'web', 47 | 48 | module: { 49 | rules: [ 50 | { 51 | test: /\.js$/, 52 | loader: 'babel-loader', 53 | include: [PATH_TO_SRC], 54 | }, 55 | { 56 | test: /\.less$/, 57 | use: [ 58 | !IS_PRODUCTION ? 'style-loader' : MiniCssExtractPlugin.loader, // CommonJS => Style nodes 59 | 'css-loader', // CSS => CommonJS 60 | 'less-loader', // Less => CSS 61 | ], 62 | }, 63 | { 64 | test: /\.(png|jp(e?)g|gif|svg|eot|ttf|woff|woff2)$/, 65 | use: [ 66 | { 67 | loader: 'url-loader', 68 | options: { 69 | limit: 10000, 70 | }, 71 | }, 72 | ], 73 | }, 74 | ], 75 | }, 76 | 77 | optimization: { 78 | splitChunks: { 79 | chunks: 'all', 80 | }, 81 | }, 82 | 83 | plugins: IS_PRODUCTION ? productionPlugins : [], 84 | 85 | devServer: { 86 | port: DEV_SERVER_PORT, 87 | hotOnly: true, 88 | contentBase: [PATH_TO_DEV_DIST], // boolean | string | array, static file location 89 | compress: true, // enable gzip compression 90 | historyApiFallback: true, // true for index.html upon 404, object for multiple paths 91 | https: false, // true for self-signed, object for cert authority 92 | noInfo: false, // only errors & warns on hot reload 93 | }, 94 | } 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@isnifer/react-calendar", 3 | "version": "0.5.2", 4 | "description": "React DateRange Picker", 5 | "keywords": "react, date, range, picker, component, calendar", 6 | "homepage": "https://isnifer.github.io/react-calendar/", 7 | "license": "MIT", 8 | "author": { 9 | "name": "Anton Kuznetsov", 10 | "email": "isnifer@gmail.com", 11 | "url": "https://github.com/isnifer" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/isnifer/react-calendar.git" 16 | }, 17 | "dependencies": { 18 | "calendar": "^0.1.0", 19 | "classnames": "^2.2.5", 20 | "moment-range": "^4.0.2", 21 | "prop-types": "^15.7.2" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.3.4", 25 | "@babel/plugin-proposal-class-properties": "^7.3.4", 26 | "@babel/plugin-proposal-decorators": "^7.3.0", 27 | "@babel/plugin-proposal-export-default-from": "^7.2.0", 28 | "@babel/plugin-proposal-export-namespace-from": "^7.2.0", 29 | "@babel/plugin-proposal-object-rest-spread": "^7.3.4", 30 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 31 | "@babel/plugin-syntax-jsx": "^7.2.0", 32 | "@babel/plugin-transform-runtime": "^7.3.4", 33 | "@babel/preset-env": "^7.3.4", 34 | "@detools/eslint-config": "^1.2.0", 35 | "babel-loader": "^8.0.5", 36 | "babel-plugin-dynamic-import-node": "^2.2.0", 37 | "babel-plugin-lodash": "^3.3.4", 38 | "clean-webpack-plugin": "^2.0.1", 39 | "coveralls": "^3.0.3", 40 | "css-loader": "^2.1.1", 41 | "enzyme": "^3.9.0", 42 | "eslint": "^5.15.3", 43 | "jsdom": "^14.0.0", 44 | "less": "^3.9.0", 45 | "less-loader": "^4.1.0", 46 | "mini-css-extract-plugin": "^0.5.0", 47 | "moment": "^2.13.0", 48 | "nyc": "^13.3.0", 49 | "prettier": "^1.16.4", 50 | "react": "^16.8.4", 51 | "react-addons-test-utils": "^15.0.1", 52 | "react-dom": "^16.8.4", 53 | "rimraf": "^2.4.3", 54 | "style-loader": "^0.23.1", 55 | "tap-spec": "^5.0.0", 56 | "tape": "^4.10.1", 57 | "url-loader": "^1.1.2", 58 | "webpack": "^4.29.6", 59 | "webpack-cli": "^3.3.0", 60 | "webpack-dev-server": "^3.2.1" 61 | }, 62 | "peerDependencies": { 63 | "react": "16.x", 64 | "react-dom": "16.x", 65 | "moment": "^2.13.0" 66 | }, 67 | "main": "build/index.js", 68 | "scripts": { 69 | "build": "rimraf build && babel src --out-dir build", 70 | "watch": "babel src --out-dir build -w", 71 | "demo": "webpack", 72 | "demo-server": "webpack-dev-server", 73 | "test": "babel-node test/datepicker.js | tap-spec", 74 | "travis": "npm run build && npm run coveralls", 75 | "prepare": "npm run build", 76 | "coverage": "babel-node ./node_modules/.bin/nyc cover --dir coverage test/datepicker.js | tap-spec", 77 | "coveralls": "npm run coverage && coveralls < coverage/lcov.info && rimraf ./coverage" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/MonthPicker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import cn from 'classnames' 4 | import DateRange from 'moment-range' 5 | import { MONTH_NAMES } from './locale' 6 | 7 | const TODAY_MONTH = new Date().getMonth() 8 | class MonthPicker extends Component { 9 | state = { 10 | month: parseInt(props.currentMonth, 10) || TODAY_MONTH, 11 | } 12 | 13 | componentWillReceiveProps({ month }) { 14 | if (month !== this.props.month) { 15 | this.setState({ month }) 16 | } 17 | } 18 | 19 | onClick = ({ target }) => { 20 | const props = target.id.split('_') 21 | if (props[2] === 'true') { 22 | return false 23 | } 24 | 25 | const id = props[1] 26 | this.props.onClick(parseInt(id, 10)) 27 | } 28 | 29 | render() { 30 | return ( 31 |
32 | {MONTH_NAMES[this.props.locale].map((item, i) => { 33 | const range = new DateRange( 34 | new Date(this.props.year, i, 1), 35 | new Date(this.props.year, i, 31) 36 | ) 37 | const disabled = !range.overlaps(this.props.range) 38 | return ( 39 |
48 | {item} 49 |
50 | ) 51 | })} 52 |
53 | ) 54 | } 55 | } 56 | 57 | MonthPicker.propTypes = { 58 | locale: PropTypes.string.isRequired, 59 | onClick: PropTypes.func.isRequired, 60 | range: PropTypes.instanceOf(DateRange).isRequired, 61 | year: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 62 | month: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 63 | currentMonth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 64 | } 65 | 66 | export default MonthPicker 67 | -------------------------------------------------------------------------------- /src/WeekDay.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import cn from 'classnames' 4 | 5 | class WeekDay extends Component { 6 | inRange = () => this.props.range.contains(this.props.date) 7 | 8 | onClick = event => (this.inRange() ? this.props.onClick(this.props.date) : event.preventDefault()) 9 | 10 | render() { 11 | const inRange = this.inRange() 12 | const className = cn( 13 | 'calendar__day', 14 | { calendar__day_available: inRange }, 15 | { calendar__day_disabled: !inRange }, 16 | { calendar__day_current: this.props.current } 17 | ) 18 | return ( 19 | 20 | 21 | {this.props.date.getDate()} 22 | 23 | 24 | ) 25 | } 26 | } 27 | 28 | WeekDay.propTypes = { 29 | date: PropTypes.instanceOf(Date).isRequired, 30 | range: PropTypes.object.isRequired, 31 | onClick: PropTypes.func.isRequired, 32 | } 33 | 34 | export default WeekDay 35 | -------------------------------------------------------------------------------- /src/YearPicker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import cn from 'classnames' 4 | import DateRange from 'moment-range' 5 | 6 | const range = currentYear => { 7 | const year = parseInt(currentYear, 10) 8 | let tmpArray = [] 9 | 10 | for (let i = year - 5; i <= year + 6; i += 1) { 11 | tmpArray = tmpArray.concat(i) 12 | } 13 | 14 | return tmpArray 15 | } 16 | 17 | const TODAY_YEAR = new Date().getFullYear() 18 | class YearPicker extends Component { 19 | constructor(props) { 20 | super(props) 21 | 22 | this.state = { 23 | year: parseInt(props.currentYear, 10) || TODAY_YEAR, 24 | } 25 | } 26 | 27 | componentWillReceiveProps({ currentYear }) { 28 | if (parseInt(currentYear, 10) !== this.props.year) { 29 | this.setState({ year: parseInt(currentYear, 10) }) 30 | } 31 | } 32 | 33 | onClick = ({ target: { textContent, id } }) => { 34 | const props = id.split('_') 35 | if (props[2] === 'true') { 36 | return false 37 | } 38 | 39 | this.props.onClick(parseInt(textContent, 10)) 40 | } 41 | 42 | render() { 43 | return ( 44 |
45 | {range(this.state.year).map((item, i) => { 46 | const range = new DateRange(new Date(item, 0, 1), new Date(item, 11, 31)) 47 | const disabled = !range.overlaps(this.props.range) 48 | return ( 49 |
58 | {item} 59 |
60 | ) 61 | })} 62 |
63 | ) 64 | } 65 | } 66 | 67 | YearPicker.propTypes = { 68 | onClick: PropTypes.func.isRequired, 69 | range: React.PropTypes.instanceOf(DateRange).isRequired, 70 | currentYear: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 71 | } 72 | 73 | export default YearPicker 74 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import DateRange from 'moment-range' 4 | import { Calendar } from 'calendar' 5 | import WeekDay from './WeekDay' 6 | import YearPicker from './YearPicker' 7 | import MonthPicker from './MonthPicker' 8 | import { MONTH_NAMES, WEEK_NAMES } from './locale' 9 | 10 | /** 11 | * Reset time of Date to 00:00:00 12 | * @param {Date} date 13 | * @return {Date} 14 | */ 15 | function resetDate(date) { 16 | return new Date(date.toString().replace(/\d{2}:\d{2}:\d{2}/, '00:00:00')) 17 | } 18 | 19 | let id = 1 20 | class Datepicker extends Component { 21 | constructor(props) { 22 | super(props) 23 | 24 | this.state = { 25 | date: null, 26 | month: null, 27 | range: null, 28 | year: null, 29 | 30 | // UI: Visibility levels 31 | dateVisible: true, 32 | monthVisible: false, 33 | yearVisible: false, 34 | } 35 | 36 | id += 1 37 | this.id = id 38 | 39 | this.calendar = new Calendar(props.locale === 'RU' ? 1 : 0) 40 | 41 | this.onClick = this.onClick.bind(this) 42 | this.changeMonth = this.changeMonth.bind(this) 43 | } 44 | 45 | // Setting up default state of calendar 46 | componentWillMount() { 47 | const TODAY = new Date() 48 | 49 | // If we have defined 'initialDate' prop 50 | // we will use it also for define month and year. 51 | // If not it will be TODAY 52 | const initialDate = this.props.initialDate || TODAY 53 | const initialRange = 54 | this.props.range || 55 | new DateRange(resetDate(this.props.minimumDate), resetDate(this.props.maximumDate)) 56 | const state = { 57 | date: initialDate, 58 | month: initialDate.getMonth(), 59 | year: initialDate.getFullYear(), 60 | } 61 | 62 | // Before set range to state we should check - 63 | // is range contains our initialDate. 64 | // If not, we will fire Error 65 | if (initialRange.contains(initialDate)) { 66 | state.range = initialRange 67 | } else { 68 | throw new Error("Initial Range doesn't contains Initial Date") 69 | } 70 | 71 | this.setState(state) 72 | } 73 | 74 | componentWillReceiveProps(nextProps) { 75 | let range 76 | 77 | if (nextProps.range !== this.props.range) { 78 | range = nextProps.range 79 | } else if ( 80 | nextProps.minimumDate !== this.props.minimumDate || 81 | nextProps.maximumDate !== this.props.maximumDate 82 | ) { 83 | range = new DateRange(resetDate(nextProps.minimumDate), resetDate(nextProps.maximumDate)) 84 | } 85 | 86 | if (range) { 87 | this.setState({ range }) 88 | } 89 | } 90 | 91 | /** 92 | * Change month handler 93 | * @param {Number} direction - "1" or "-1" 94 | */ 95 | changeMonth(direction) { 96 | if (this.state.dateVisible) { 97 | const nextMonth = this.state.month + direction 98 | const isMonthAvailable = nextMonth >= 0 && nextMonth <= 11 99 | 100 | let model 101 | if (!isMonthAvailable) { 102 | model = { 103 | month: direction === 1 ? 0 : 11, 104 | year: this.state.year + direction, 105 | } 106 | } else { 107 | model = { month: this.state.month + direction } 108 | } 109 | 110 | this.setState(model) 111 | } 112 | 113 | if (this.state.yearVisible) { 114 | this.setState({ year: this.state.year + (direction + 1 ? 12 : -12) }) 115 | } 116 | } 117 | 118 | /** 119 | * Calendar state setter 120 | * @param {Date} date - selected date 121 | */ 122 | onClick(date) { 123 | if (date) { 124 | this.setState({ date }, this.setOuterDate.bind(this, date)) 125 | } 126 | } 127 | 128 | setOuterDate = date => { 129 | this.props.onClick(date, this.props.name || `date_${this.id}`) 130 | } 131 | 132 | onYearNameClick = () => { 133 | this.setState({ 134 | dateVisible: false, 135 | monthVisible: false, 136 | yearVisible: true, 137 | }) 138 | } 139 | 140 | onYearClick = year => { 141 | this.restoreDateVisibility({ year }) 142 | } 143 | 144 | onMonthNameClick = () => { 145 | this.setState({ 146 | dateVisible: false, 147 | monthVisible: true, 148 | yearVisible: false, 149 | }) 150 | } 151 | 152 | onMonthClick = month => { 153 | this.restoreDateVisibility({ month }) 154 | } 155 | 156 | restoreDateVisibility = model => { 157 | const month = model.month || this.state.month 158 | const year = model.year || this.state.year 159 | const day = this.state.date.getDate() 160 | const date = new Date(year, month, day, 0, 0, 0) 161 | 162 | this.setState( 163 | { 164 | date, 165 | dateVisible: true, 166 | monthVisible: false, 167 | yearVisible: false, 168 | ...model, 169 | }, 170 | this.setOuterDate.bind(this, date) 171 | ) 172 | } 173 | 174 | renderMonth() { 175 | // Array of weeks, which contains arrays of days 176 | // [[Date, Date, ...], [Date, Date, ...], ...] 177 | var month = this.calendar.monthDates(this.state.year, this.state.month) 178 | return month.map((week, i) => { 179 | return ( 180 | 181 | {this.renderWeek(week)} 182 | 183 | ) 184 | }) 185 | } 186 | 187 | /** 188 | * Render week 189 | * @param {Array} weekDays - array of instanceof Date 190 | * @return {Array} - array of Components 191 | */ 192 | renderWeek(weekDays) { 193 | return weekDays.map((day, i) => { 194 | var isCurrent = this.state.date.toDateString() === day.toDateString() 195 | return ( 196 | 203 | ) 204 | }) 205 | } 206 | 207 | renderWeekdayNames() { 208 | return WEEK_NAMES[this.props.locale].map((weekname, i) => { 209 | return ( 210 | 211 | {weekname} 212 | 213 | ) 214 | }) 215 | } 216 | 217 | // TODO: Fix direction 218 | renderNavigation() { 219 | return ( 220 | 221 | 224 | ← 225 | 226 | 227 | 228 | {MONTH_NAMES[this.props.locale][this.state.month]} 229 | 230 | {', '} 231 | 232 | {this.state.year} 233 | 234 | 235 | 238 | → 239 | 240 | 241 | ) 242 | } 243 | 244 | render() { 245 | return ( 246 |
247 |
248 | {!this.props.disableNavigation && !this.props.outsideNavigation 249 | ? this.renderNavigation() 250 | : ''} 251 |
252 | 253 | {this.state.dateVisible && ( 254 | 255 | 256 | {this.renderWeekdayNames()} 257 | 258 | {this.renderMonth()} 259 |
260 | )} 261 | 262 | {this.state.monthVisible && ( 263 | 270 | )} 271 | 272 | {this.state.yearVisible && ( 273 | 278 | )} 279 |
280 | ) 281 | } 282 | } 283 | 284 | Datepicker.propTypes = { 285 | name: PropTypes.string.isRequired, 286 | initialDate: PropTypes.instanceOf(Date).isRequired, 287 | onClick: PropTypes.func, 288 | range: PropTypes.instanceOf(DateRange), 289 | disableNavigation: PropTypes.bool, 290 | outsideNavigation: PropTypes.bool, 291 | locale: PropTypes.string, 292 | minimumDate: PropTypes.instanceOf(Date), 293 | maximumDate: PropTypes.instanceOf(Date), 294 | } 295 | 296 | Datepicker.defaultProps = { 297 | // Handler which will be execute when click on day 298 | onClick: () => {}, 299 | 300 | // Instance of DateRange 301 | range: null, 302 | 303 | // If true, navigation will be hidden 304 | disableNavigation: false, 305 | 306 | // If true, navigation will be in root container 307 | outsideNavigation: false, 308 | 309 | // Available locales: RU, EN, DE, FR, IT, POR, ESP 310 | locale: 'EN', 311 | 312 | // Minimum available date 313 | minimumDate: new Date(1970, 0, 1), 314 | 315 | // Maximum available date 316 | maximumDate: new Date(2100, 0, 1), 317 | } 318 | 319 | export default Datepicker 320 | -------------------------------------------------------------------------------- /src/locale.js: -------------------------------------------------------------------------------- 1 | export const MONTH_NAMES = { 2 | RU: [ 3 | 'Январь', 4 | 'Февраль', 5 | 'Март', 6 | 'Апрель', 7 | 'Май', 8 | 'Июнь', 9 | 'Июль', 10 | 'Август', 11 | 'Сентябрь', 12 | 'Октябрь', 13 | 'Ноябрь', 14 | 'Декабрь', 15 | ], 16 | EN: [ 17 | 'January', 18 | 'February', 19 | 'March', 20 | 'April', 21 | 'May', 22 | 'June', 23 | 'July', 24 | 'August', 25 | 'September', 26 | 'October', 27 | 'November', 28 | 'December', 29 | ], 30 | DE: [ 31 | 'Januari', 32 | 'Februari', 33 | 'March', 34 | 'April', 35 | 'Kan', 36 | 'June', 37 | 'Juli', 38 | 'Augustus', 39 | 'September', 40 | 'Oktober', 41 | 'November', 42 | 'December', 43 | ], 44 | FR: [ 45 | 'Janvier', 46 | 'Février', 47 | 'Mars', 48 | 'Avril', 49 | 'Peut', 50 | 'Juin', 51 | 'Juillet', 52 | 'Août', 53 | 'Septembre', 54 | 'Octobre', 55 | 'Novembre', 56 | 'Décembre', 57 | ], 58 | ITA: [ 59 | 'Gennaio', 60 | 'Febbraio', 61 | 'Marzo', 62 | 'Aprile', 63 | 'Maggio', 64 | 'Giu', 65 | 'Luglio', 66 | 'Agosto', 67 | 'Settembre', 68 | 'Ottobre', 69 | 'Novembre', 70 | 'Dicembre', 71 | ], 72 | POR: [ 73 | 'Janeiro', 74 | 'Fevereiro', 75 | 'Março', 76 | 'April', 77 | 'Maio', 78 | 'Junho', 79 | 'Julho', 80 | 'Agosto', 81 | 'Setembro', 82 | 'Outubro', 83 | 'November', 84 | 'Dezembro', 85 | ], 86 | ESP: [ 87 | 'Enero', 88 | 'Febrero', 89 | 'Marzo', 90 | 'Abril', 91 | 'Puede', 92 | 'Junio', 93 | 'Julio', 94 | 'Agosto', 95 | 'Septiembre', 96 | 'Octubre', 97 | 'Noviembre', 98 | 'Diciembre', 99 | ], 100 | } 101 | 102 | export const WEEK_NAMES = { 103 | RU: ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'], 104 | EN: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 105 | DE: ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'], 106 | FR: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'], 107 | IT: ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'], 108 | POR: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'], 109 | ESP: ['Dom.', 'Lun.', 'Mar.', 'Mié.', 'Jue.', 'Vie.', 'Sáb.'], 110 | } 111 | 112 | export const WEEK_NAMES_SHORT = { 113 | RU: ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'], 114 | EN: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], 115 | DE: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'], 116 | FR: ['Di', 'Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa'], 117 | IT: ['D', 'L', 'Ma', 'Me', 'G', 'V', 'S'], 118 | POR: ['Dom', '2ª', '3ª', '4ª', '5ª', '6ª', 'Sáb'], 119 | ESP: ['Do', 'Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sá'], 120 | } 121 | -------------------------------------------------------------------------------- /test/datepicker.js: -------------------------------------------------------------------------------- 1 | import './utils/jsdom' 2 | import test from 'tape' 3 | import React from 'react' 4 | import TestUtils from 'react-addons-test-utils' 5 | import DateRange from 'moment-range' 6 | import render from './utils/render' 7 | import Datepicker from '../build' 8 | 9 | const c = render(Datepicker) 10 | 11 | test('Calendar inited', assert => { 12 | const initialDate = new Date() 13 | const DOMComponent = TestUtils.renderIntoDocument() 14 | 15 | assert.equal(c.type, 'div', 'Should be div') 16 | assert.equal(c.props.className, 'calendar', 'With className "calendar"') 17 | 18 | assert.equal(DOMComponent.state.date, initialDate, 'Date in state should equal initialDate') 19 | 20 | assert.end() 21 | }) 22 | 23 | test('Should have 3 elements: head, month, placeholder for nav', assert => { 24 | const { children } = c.props 25 | 26 | assert.equal(children.length, 4, 'Count of children is 4') 27 | 28 | assert.equal(children[0].type, 'div', 'Head should be a div') 29 | assert.equal( 30 | children[0].props.className, 31 | 'calendar__head', 32 | 'And should have className calendar__head' 33 | ) 34 | 35 | assert.equal(children[1].type, 'table', 'Month should be a table') 36 | assert.equal( 37 | children[1].props.className, 38 | 'calendar__month', 39 | 'And should have className calendar__month' 40 | ) 41 | 42 | assert.false(children[2], 'Month Placeholder is false') 43 | assert.false(children[3], 'Year Placeholder is false') 44 | 45 | assert.end() 46 | }) 47 | 48 | test('Test componentWillReceiveProps', assert => { 49 | const leftRangeDate = new Date(2015, 7, 4) 50 | const rightRangeDate = new Date(2015, 7, 27) 51 | 52 | const Parent = React.createClass({ 53 | getInitialState() { 54 | return { 55 | date1: leftRangeDate, 56 | date2: rightRangeDate, 57 | } 58 | }, 59 | 60 | render() { 61 | return ( 62 |
63 | { 69 | this.setState({ date1: date }) 70 | }} 71 | /> 72 | { 78 | this.setState({ date2: date }) 79 | }} 80 | /> 81 | 82 | 87 | 92 |
93 | ) 94 | }, 95 | }) 96 | 97 | const parent = TestUtils.renderIntoDocument() 98 | 99 | const datePicker1 = parent.refs.datepicker1 100 | const datePicker2 = parent.refs.datepicker2 101 | 102 | const datePicker3 = parent.refs.datepicker3 103 | const datePicker4 = parent.refs.datepicker4 104 | 105 | let currentDay1 = TestUtils.findRenderedDOMComponentWithClass( 106 | datePicker1, 107 | 'calendar__day_current' 108 | ) 109 | // TEMP HACK 110 | currentDay1 = currentDay1[Object.keys(currentDay1)[0]]._currentElement 111 | 112 | let currentDay2 = TestUtils.findRenderedDOMComponentWithClass( 113 | datePicker2, 114 | 'calendar__day_current' 115 | ) 116 | // TEMP HACK 117 | currentDay2 = currentDay2[Object.keys(currentDay2)[0]]._currentElement 118 | 119 | assert.equal(currentDay1.props.children, 4, 'Initial date is 4 August in first calendar') 120 | assert.equal(currentDay2.props.children, 27, 'Initial date is 5 August in second calendar') 121 | 122 | // 4 August 123 | assert.equal( 124 | datePicker2.state.range.start.toString(), 125 | new DateRange(leftRangeDate, rightRangeDate).start.toString(), 126 | 'Should set start of range 4 August' 127 | ) 128 | 129 | let nextAvialableDate = TestUtils.scryRenderedDOMComponentsWithClass( 130 | datePicker1, 131 | 'calendar__day_available' 132 | )[1] 133 | TestUtils.Simulate.click(nextAvialableDate) 134 | // TEMP HACK 135 | nextAvialableDate = nextAvialableDate[Object.keys(nextAvialableDate)[0]]._currentElement 136 | 137 | assert.equal( 138 | nextAvialableDate.props.children, 139 | parent.state.date1.getDate(), 140 | 'Should set next available date - 5 August' 141 | ) 142 | 143 | // 5 August 144 | assert.equal( 145 | datePicker2.state.range.start.toString(), 146 | new DateRange(parent.state.date1, rightRangeDate).start.toString(), 147 | 'Should change start of range to 5 August' 148 | ) 149 | 150 | assert.equal( 151 | datePicker4.state.range.toString(), 152 | new DateRange(parent.state.date1, rightRangeDate).toString(), 153 | 'New range for second calendar should start from August 5' 154 | ) 155 | 156 | assert.end() 157 | }) 158 | 159 | test('Date is part of Range', assert => { 160 | assert.doesNotThrow( 161 | () => 162 | TestUtils.renderIntoDocument( 163 | 167 | ), 168 | null, 169 | "Shouldn't throws an exception" 170 | ) 171 | 172 | assert.end() 173 | }) 174 | 175 | test('Date is not part of Range', assert => { 176 | assert.throws( 177 | () => 178 | TestUtils.renderIntoDocument( 179 | 183 | ), 184 | null, 185 | 'Should throws an exception' 186 | ) 187 | 188 | assert.end() 189 | }) 190 | 191 | test('Change Month Action', assert => { 192 | const currentMonth = 8 193 | const currentYear = 2015 194 | let c = TestUtils.renderIntoDocument( 195 | 196 | ) 197 | 198 | let currentDay = TestUtils.scryRenderedDOMComponentsWithClass(c, 'calendar__day_current')[0] 199 | // TEMP HACK 200 | currentDay = currentDay[Object.keys(currentDay)[0]]._currentElement 201 | 202 | const prevMonthButtonInstance = TestUtils.scryRenderedDOMComponentsWithClass( 203 | c, 204 | 'calendar__arrow_left' 205 | )[0] 206 | // TEMP HACK 207 | const prevMonthButton = 208 | prevMonthButtonInstance[Object.keys(prevMonthButtonInstance)[0]]._currentElement 209 | 210 | const nextMonthButtonInstance = TestUtils.scryRenderedDOMComponentsWithClass( 211 | c, 212 | 'calendar__arrow_right' 213 | )[0] 214 | // TEMP HACK 215 | const nextMonthButton = 216 | nextMonthButtonInstance[Object.keys(nextMonthButtonInstance)[0]]._currentElement 217 | 218 | assert.equal(currentDay.props.children, 1, 'Should be first of september') 219 | assert.equal(prevMonthButton.props.children, '←', 'Should be left change month button') 220 | assert.equal(nextMonthButton.props.children, '→', 'Should be right change month button') 221 | 222 | assert.equal(c.state.month, currentMonth, 'Should be September') 223 | 224 | TestUtils.Simulate.click(prevMonthButtonInstance) 225 | assert.equal(c.state.month, currentMonth - 1, 'Should be August') 226 | 227 | TestUtils.Simulate.click(nextMonthButtonInstance) 228 | TestUtils.Simulate.click(nextMonthButtonInstance) 229 | assert.equal(c.state.month, currentMonth + 1, 'Should be October') 230 | 231 | TestUtils.Simulate.click(nextMonthButtonInstance) 232 | TestUtils.Simulate.click(nextMonthButtonInstance) 233 | TestUtils.Simulate.click(nextMonthButtonInstance) 234 | assert.equal(c.state.month, 0, 'Should be January') 235 | assert.equal(c.state.year, currentYear + 1, 'Should be 2016') 236 | 237 | assert.end() 238 | }) 239 | -------------------------------------------------------------------------------- /test/utils/jsdom.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom'; 2 | 3 | global.document = jsdom.jsdom(''); 4 | global.window = document.defaultView; 5 | global.navigator = global.window.navigator; 6 | -------------------------------------------------------------------------------- /test/utils/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-addons-test-utils'; 3 | 4 | export default function render (component, props, ...children) { 5 | const shallowRenderer = TestUtils.createRenderer(); 6 | 7 | shallowRenderer.render( 8 | React.createElement( 9 | component, 10 | props, 11 | ...children 12 | ) 13 | ); 14 | 15 | return shallowRenderer.getRenderOutput(); 16 | } 17 | --------------------------------------------------------------------------------