├── .babelrc ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── doc └── index.md ├── examples ├── .gitignore ├── index.html └── src │ ├── components │ ├── App.css │ ├── App.js │ ├── Example.css │ ├── Example.js │ └── MyTimePicker.js │ ├── main.css │ └── main.js ├── package.json ├── src ├── components │ ├── Calendar.js │ ├── DatePicker.js │ ├── Day.js │ ├── DaysOfWeek.js │ ├── DaysViewHeading.js │ ├── MonthSelector.js │ └── MonthsViewHeading.js ├── index.js ├── styles │ └── basic.css └── utils │ ├── assets.js │ ├── moment-helper.js │ └── persian.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0", 5 | "react" 6 | ] 7 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [*.md] 9 | max_line_length = 0 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directories 29 | node_modules 30 | jspm_packages 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional REPL history 36 | .node_repl_history 37 | 38 | .idea 39 | lib -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mohamad Mohebifar 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Persian Date Picker :calendar: 2 | 3 | [![gitcheese.com](https://s3.amazonaws.com/gitcheese-ui-master/images/badge.svg)](https://www.gitcheese.com/donate/users/6104558/repos/43522564) 4 | 5 | [Persian calendar](https://en.wikipedia.org/wiki/Solar_Hijri_calendar) and date picker components for [React](https://facebook.github.io/react/). This is an open source project made in [@evandhq](https://github.com/evandhq) team. 6 | 7 | [See the demo.](https://evandhq.github.io/react-persian-datepicker) 8 | 9 |

10 | React Persian Date Picker مجموعه‌ای از کامپوننت‌های ReactJS مورد نیاز جهت ایجاد ورود و نمایش تاریخ هجری شمسی است. این نرم‌افزار متن‌باز در ایوند توسعه یافته‌است. 11 |

12 | 13 |

14 | نسخه نمایشی را اینجا ببینید. 15 |

16 | 17 | # Installation 18 | 19 | Use npm to install the package: 20 | 21 | ``` 22 | npm install react-persian-datepicker --save 23 | ``` 24 | 25 | # Usage 26 | This package offers two components, `Calendar` and `DatePicker`. The first of which is a simple calendar that you can use in whichever way you want. The second one is an actual input with an input-ish behaviour. 27 | 28 | This package uses [moment-jalaali](https://github.com/jalaali/moment-jalaali) under the hood and all the values are basically moment objects. 29 | 30 | Below is a basic example. 31 | 32 | ```es6 33 | import React from 'react'; 34 | import { Calendar, DatePicker } from 'react-persian-datepicker'; 35 | 36 | const MyComponent = () => ( 37 |
38 |
39 | {/* Calendar Component */} 40 | 41 |
42 | 43 |
44 | {/* Date Picker Component */} 45 | 46 |
47 |
48 | ); 49 | ``` 50 | # API Documentaion 51 | Current API documentation for **v3.0.2** is [available](https://github.com/evandhq/react-persian-datepicker/blob/master/doc/index.md) and for more examples [take a look at here](https://github.com/evandhq/react-persian-datepicker/blob/master/examples/src/components/App.js#L43). 52 | 53 | Note that you need `css-loader` for `/\.css$/` files enabled to have the styles working as we use css modules to put classnames in place. Otherwise, you'll have to pass an object of class names (like a css module) as `styles` prop. This way, you can develop your own stylesheet for the calendar according to the [basic one](https://github.com/evandhq/react-persian-datepicker/blob/master/src/styles/basic.css). The only thing that you will need to do is to require the css file that you made and pass it as `styles` prop to either `Calendar` or `DatePicker`. 54 | -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | # API Documentation 2 | This documentation is for **v3.0.2**. if you are using another version, you may update this file and make a PR. Contributions are totally welcomed ;) 3 | 4 | ## DatePicker 5 | A jalaali date picker component for react. 6 | 7 | ### Example 8 | ``` 9 | import {DatePicker} from 'react-persian-datepicker' 10 | // or const DatePicker = require('react-persian-datepicker').DatePicker 11 | 12 | ... 13 | render () { 14 | return ; 15 | } 16 | ``` 17 | For more examples please visit [github page](https://evandhq.github.io/react-persian-datepicker/) or see `examples/` directory. 18 | 19 | ### Options 20 | 21 | Property | Type | Default | Required | Description 22 | :-------------------|:------:|:--------------:|:--------:|:---------------------------------------- 23 | value | object | null | | usable to create controlled datepicker, if `defaultValue` provided it takes it's value 24 | defaultValue | object | null | | sets default value for datepicker 25 | onChange | func | | | it sends updated `momentjs` object as argument to provided function. By default it sets the datepicker value, if you need to implement this, consider updating `value` accordingly 26 | onFocus | func | | | by default it makes datepicker visible. if you need to implement this, please see `handleFocus` method in the [source code](https://github.com/thg303/react-persian-datepicker/blob/master/src/components/DatePicker.js#L63) 27 | onBlur | func | | | it sends actual `blur` event as argument, by default it handles visibility and the value of the datepicker and then runs this callback. 28 | children | node | | | it is not used in this component, (propbably should be removed in the next version) 29 | min | object | | | accepts a `Date` or `Moment` object as the minimum value for datepicker 30 | max | object | | | accepts a `Date` or `Moment` object as the maximum value for datepicker 31 | defaultMonth| object | | | sets Calendar's default starting month, see `Calendar` documentations below for more details. 32 | inputFormat | string | "jYYYY/jM/jD" | | sets how date should appear in the input field. see [moment-jalaali documentations](https://github.com/jalaali/moment-jalaali) for more details 33 | removable | bool | | | it is not used in this component, (propbably should be removed in the next version) 34 | timePickerComponent | func | | | if provided, it would show up in the datepicker. it should be a React Component which accepts four properties: `min`, `max`, `momentValue`, `setMomentValue`, datepicker will send corrsendponding `min`, `max` and `momentValue` and uses `setMomentValue` which sets the datepicker value internally. for more details see the [source code](https://github.com/thg303/react-persian-datepicker/blob/master/src/components/DatePicker.js), you may also find an implemented TimePicker component at [examples/src/components/MyTimePicker.js](https://github.com/evandhq/react-persian-datepicker/blob/master/examples/src/components/MyTimePicker.js) 35 | calendarStyles| object | [basic.css](https://github.com/thg303/react-persian-datepicker/blob/master/src/styles/basic.css) | | css object which will be used in the `Calendar` component 36 | calendarContainerProps| object | `{}` | | this object will be passed as `containerProps` in the `Calendar` component 37 | 38 | ## Calendar 39 | A jalaali Calendar for react. It uses **Persian** locales by default. 40 | 41 | ### Example 42 | ``` 43 | import {Calendar} from 'react-persian-datepicker' 44 | // or const Calendar = require('react-persian-datepicker').Calendar 45 | 46 | ... 47 | render () { 48 | return ; 49 | } 50 | ``` 51 | For more examples please visit [github page](https://evandhq.github.io/react-persian-datepicker/) or see `examples/` directory. 52 | 53 | ### Options 54 | 55 | Property | Type | Default | Required | Description 56 | :-------------------|:------:|:--------------:|:--------:|:---------------------------------------- 57 | min | object | | | accepts a `Date` or `Moment` object as the minimum day for Calendar 58 | max | object | | | accepts a `Date` or `Moment` object as the maximum day for Calendar 59 | styles | object | [basic.css](https://github.com/thg303/react-persian-datepicker/blob/master/src/styles/basic.css) | | css object which will be used. 60 | selectedDay | object | null | | sets default selected day 61 | defaultMonth| object | | | sets Calendar's default starting month, if not set, the `selectedDay`'s month will be used, if it was not set too, it sets current month. 62 | onSelect | func | | | if provided, it will be called after user clicked on a day. the `selectedDay` (moment object) will be passed as argument to the function. 63 | onClickOutside | func | | | if provided, it will be called after user clicked outside of calendar. it uses [react-onclickoutside package](https://github.com/Pomax/react-onclickoutside) and used by `DatePicker` component 64 | containerProps | object | `{}` | | it is not used in this component, (propbably should be removed in the next version) 65 | 66 | ## outsideClickIgnoreClass 67 | It is a simple string "ignore--click--outside" used as a class flag in CSS. 68 | 69 | --- 70 | Documentation by [thg303](https://github.com/thg303) at 2017/8/9 71 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | build.js -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Persian Datepicker 6 | 7 | 8 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/src/components/App.css: -------------------------------------------------------------------------------- 1 | :local(.jumbotron) { 2 | background-color: #216ba5; 3 | color: #dedede; 4 | text-align: center; 5 | padding: 20px; 6 | } 7 | 8 | :local(.jumbotron) a { 9 | color: #fff; 10 | text-decoration: none; 11 | } 12 | 13 | :local(.examples) { 14 | padding-top: 30px; 15 | } -------------------------------------------------------------------------------- /examples/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Example from './Example'; 3 | import styles from './App.css'; 4 | import { Calendar, DatePicker } from '../../../src/index'; 5 | import MyTimePicker from './MyTimePicker'; 6 | import moment from 'moment-jalali'; 7 | 8 | const generateRandomMoment = () => { 9 | return moment() 10 | .hour(Math.round(Math.random() * 23)) 11 | .minute(Math.round(Math.random() * 60)) 12 | .add(Math.round(Math.random() * 100), 'day'); 13 | } 14 | 15 | export default class App extends Component { 16 | state = { 17 | value: moment(), 18 | someValue: new moment() 19 | }; 20 | 21 | render() { 22 | return ( 23 |
24 | 29 | مشاهده در گیت‌هاب 30 | 31 |
32 |

React Persian Date Picker

33 |

34 | پروژه‌ای از ایوند 35 |

36 |

37 | 38 | دریافت پروژه 39 | 40 |

41 |
42 |
43 |
44 | 48 | 49 | 50 |
51 |
52 | 56 | this.setState({ someValue: value })} 59 | /> 60 |
61 | 64 |
65 |
66 |
67 |
68 | 72 | this.setState({ value })} 75 | /> 76 |
77 | 80 |
81 |
82 |
83 |
84 | 89 | this.setState({ value })} 96 | /> 97 |
98 | 101 |
102 |
103 |
104 |
105 | 109 | 110 | 111 |
112 |
113 |
114 | ); 115 | } 116 | } 117 | 118 | const basicDatePickerCode = `render() { 119 | return ; 120 | }`; 121 | 122 | 123 | const clearDatePickerCode = `render() { 124 | return ( 125 |
126 | this.setState({ value })} 129 | /> 130 | 133 |
134 | ); 135 | }`; 136 | 137 | const controlledDatePickerCode = `render() { 138 | return ( 139 |
140 | this.setState({ value })} 143 | /> 144 | 147 |
148 | ); 149 | }`; 150 | 151 | const dateTimePickerCode = `render() { 152 | return ( 153 |
154 | this.setState({ value })} 159 | /> 160 | 163 |
164 | ); 165 | };`; 166 | 167 | const basicExampleCode = `render() { 168 | return ; 169 | };`; -------------------------------------------------------------------------------- /examples/src/components/Example.css: -------------------------------------------------------------------------------- 1 | :local(.example) { 2 | background-color: #fff; 3 | border-radius: 4px; 4 | border: 1px solid #e4e4e4; 5 | padding: 20px; 6 | margin-bottom: 20px; 7 | } 8 | 9 | :local(.example) h1, :local(.example) h2, :local(.example) h3 { 10 | border-bottom: 1px dashed rgba(238, 238, 238, 0.44); 11 | margin: 0 0 20px; 12 | color: #216ba5; 13 | padding-bottom: 10px; 14 | } 15 | 16 | :local(.row):after { 17 | content: ''; 18 | clear: both; 19 | display: block; 20 | } 21 | 22 | :local(.row) > div { 23 | width: 50%; 24 | padding: 13px 0; 25 | float: right; 26 | } 27 | 28 | :local(.row) > :local(.description) { 29 | padding-left: 30px; 30 | } 31 | 32 | pre { 33 | background-color: #F5F8FB !important; 34 | padding: 20px 10px !important; 35 | margin: 0 0 20px -20px; 36 | border-radius: 0 3px 3px 0; 37 | border-top: 1px solid #d8e4ef; 38 | border-bottom: 1px solid #d8e4ef; 39 | border-right: 1px solid #d8e4ef; 40 | } -------------------------------------------------------------------------------- /examples/src/components/Example.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import styles from './Example.css'; 3 | import Highlight from 'react-syntax-highlight'; 4 | require('highlight.js/styles/github.css'); 5 | 6 | export default class Example extends Component { 7 | render() { 8 | const { title, code, description, children } = this.props; 9 | return ( 10 |
11 |
12 |

{title}

13 |
14 |
15 | { 16 | description ? ( 17 |
18 | {description} 19 |
20 | ) : null 21 | } 22 | 23 |
{children}
24 |
25 |
26 | 27 |
28 |
29 |
30 |
31 | ) 32 | } 33 | } 34 | 35 | Example.propTypes = { 36 | title: PropTypes.string.isRequired, 37 | code: PropTypes.string.isRequired, 38 | description: PropTypes.string 39 | }; -------------------------------------------------------------------------------- /examples/src/components/MyTimePicker.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import moment from 'moment-jalali'; 3 | import TimePicker from 'rc-time-picker-evand'; 4 | import { outsideClickIgnoreClass } from '../../../src/index'; 5 | import { persianNumber } from '../../../src/utils/persian'; 6 | import 'rc-time-picker-evand/assets/index.css'; 7 | 8 | const disabledMinutes = () => { 9 | return [...Array(60)].map((v, i) => i).filter(v => v % 5 !== 0); 10 | }; 11 | 12 | export default class MyTimePicker extends Component { 13 | static propTypes = { 14 | momentValue: PropTypes.object, 15 | setMomentValue: PropTypes.func 16 | }; 17 | 18 | handleChange(value) { 19 | const { momentValue, min } = this.props; 20 | let newValue; 21 | 22 | if (momentValue) { 23 | newValue = momentValue.clone(); 24 | } else if (min && min.isAfter(moment())) { 25 | newValue = min.clone(); 26 | } else { 27 | newValue = moment(value); 28 | } 29 | 30 | newValue.hour(value.hour()); 31 | newValue.minute(value.minute()); 32 | 33 | this.props.setMomentValue(newValue); 34 | } 35 | 36 | render() { 37 | const { momentValue } = this.props; 38 | return momentValue ? ( 39 |
40 |
ساعت:
41 |
42 | persianNumber(value)} 53 | hideDisabledOptions 54 | /> 55 |
56 |
57 |
58 | ) : null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/src/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: TestFont, IRANSans, Tahoma, "Dejavu Sans", sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | direction: rtl; 6 | } 7 | 8 | * { 9 | box-sizing: border-box; 10 | } 11 | 12 | pre { 13 | direction: ltr; 14 | } 15 | 16 | :global(.container) { 17 | max-width: 900px; 18 | margin: auto; 19 | } 20 | 21 | button, :global(.github-fork-ribbon):after { 22 | font-family: TestFont, IRANSans, Tahoma, "Dejavu Sans", sans-serif; 23 | } 24 | 25 | input { 26 | font-family: TestFont, IRANSans, Tahoma, "Dejavu Sans", sans-serif; 27 | text-align: left; 28 | direction: ltr; 29 | width: 100%; 30 | height: 35px; 31 | padding: 6px 12px; 32 | background-color: #fff; 33 | background-image: none; 34 | border: 1px solid #eee; 35 | border-radius: 3px; 36 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); 37 | -webkit-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out; 38 | transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out; 39 | } 40 | 41 | :global .time-picker-panel { 42 | direction: ltr; 43 | } -------------------------------------------------------------------------------- /examples/src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App'; 4 | 5 | ReactDOM.render(, document.getElementById('content')); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-persian-datepicker", 3 | "version": "3.0.2", 4 | "description": "Persian calendar and date picker components for React", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "NODE_ENV=development webpack-dev-server --hot --inline --content-base ./examples", 9 | "webpack": "NODE_ENV=production webpack", 10 | "babel": "babel src --out-dir lib --ignore /examples && cp src/styles lib -rf", 11 | "prepublish": "npm run babel" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/mohebifar/react-persian-datepicker.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "component", 20 | "jalali", 21 | "persian", 22 | "calendar", 23 | "datepicker", 24 | "timepicker" 25 | ], 26 | "author": "Mohamad Mohebifar", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/mohebifar/react-persian-datepicker/issues" 30 | }, 31 | "files": [ 32 | "lib" 33 | ], 34 | "homepage": "https://github.com/mohebifar/react-persian-datepicker#readme", 35 | "devDependencies": { 36 | "babel": "^6.5.2", 37 | "babel-cli": "^6.10.1", 38 | "babel-loader": "^6.2.4", 39 | "babel-preset-es2015": "^6.9.0", 40 | "babel-preset-es2016": "^6.11.3", 41 | "babel-preset-react": "^6.11.1", 42 | "babel-preset-stage-0": "^6.5.0", 43 | "css-loader": "^0.23.1", 44 | "rc-time-picker-evand": "^3.1.0", 45 | "react-syntax-highlight": "0.0.2", 46 | "style-loader": "^0.13.1", 47 | "webpack": "^1.13.1", 48 | "webpack-dev-server": "^1.14.1" 49 | }, 50 | "peerDependencies": { 51 | "react": "^15.2.1", 52 | "react-dom": "^15.2.1" 53 | }, 54 | "dependencies": { 55 | "classnames": "^2.2.5", 56 | "moment-jalali": "^0.3.9", 57 | "prop-types": "^15.5.10", 58 | "rc-trigger": "^1.7.2", 59 | "react-onclickoutside": "^5.3.3", 60 | "react-tether": "^0.5.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/Calendar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import DaysViewHeading from './DaysViewHeading'; 4 | import DaysOfWeek from './DaysOfWeek'; 5 | import MonthSelector from './MonthSelector'; 6 | import Day from './Day'; 7 | import { getDaysOfMonth } from '../utils/moment-helper'; 8 | import moment from 'moment-jalali'; 9 | import onClickOutside from 'react-onclickoutside'; 10 | 11 | // Load Persian localisation 12 | moment.loadPersian(); 13 | 14 | export class Calendar extends Component { 15 | static propTypes = { 16 | min: PropTypes.object, 17 | max: PropTypes.object, 18 | styles: PropTypes.object, 19 | selectedDay: PropTypes.object, 20 | defaultMonth: PropTypes.object, 21 | onSelect: PropTypes.func, 22 | onClickOutside: PropTypes.func, 23 | containerProps: PropTypes.object 24 | }; 25 | 26 | static childContextTypes = { 27 | nextMonth: PropTypes.func.isRequired, 28 | prevMonth: PropTypes.func.isRequired, 29 | setCalendarMode: PropTypes.func.isRequired, 30 | setMonth: PropTypes.func.isRequired 31 | }; 32 | 33 | static defaultProps = { 34 | styles: require('../styles/basic.css'), 35 | containerProps: {} 36 | }; 37 | 38 | state = { 39 | month: this.props.defaultMonth || this.props.selectedDay || moment(this.props.min), 40 | selectedDay: this.props.selectedDay || null, 41 | mode: 'days' 42 | }; 43 | 44 | getChildContext() { 45 | return { 46 | nextMonth: this.nextMonth.bind(this), 47 | prevMonth: this.prevMonth.bind(this), 48 | setCalendarMode: this.setMode.bind(this), 49 | setMonth: this.setMonth.bind(this) 50 | }; 51 | } 52 | 53 | componentWillReceiveProps({ selectedDay, defaultMonth, min }) { 54 | if (this.props.selectedDay !== selectedDay) { 55 | this.selectDay(selectedDay); 56 | } else if (defaultMonth && this.props.defaultMonth !== defaultMonth && this.state.month === this.props.defaultMonth) { 57 | this.setMonth(defaultMonth); 58 | } else if (min && this.props.min !== min && this.state.month.isSame(this.props.min)) { 59 | this.setMonth(min.clone()); 60 | } 61 | } 62 | 63 | setMode(mode) { 64 | this.setState({ mode }); 65 | } 66 | 67 | setMonth(month) { 68 | this.setState({ month }); 69 | } 70 | 71 | nextMonth() { 72 | this.setState({ 73 | month: this.state.month.clone().add(1, 'jMonth') 74 | }); 75 | } 76 | 77 | prevMonth() { 78 | this.setState({ 79 | month: this.state.month.clone().subtract(1, 'jMonth') 80 | }); 81 | } 82 | 83 | selectDay(selectedDay) { 84 | const { month } = this.state; 85 | 86 | // Because there's no `m1.isSame(m2, 'jMonth')` 87 | if (selectedDay.format('jYYYYjMM') !== month.format('jYYYYjMM')) { 88 | this.setState({ month: selectedDay }); 89 | } 90 | 91 | this.setState({ selectedDay }); 92 | } 93 | 94 | handleClickOnDay = selectedDay => { 95 | const { onSelect } = this.props; 96 | this.selectDay(selectedDay); 97 | if (onSelect) { 98 | onSelect(selectedDay); 99 | } 100 | }; 101 | 102 | handleClickOutside(event) { 103 | if (this.props.onClickOutside) { 104 | this.props.onClickOutside(event); 105 | } 106 | } 107 | 108 | days = null; 109 | lastRenderedMonth = null; 110 | 111 | renderMonthSelector() { 112 | const { month } = this.state; 113 | const { styles } = this.props; 114 | return (); 115 | } 116 | 117 | renderDays() { 118 | const { month, selectedDay } = this.state; 119 | const { children, min, max, styles, outsideClickIgnoreClass } = this.props; 120 | 121 | let days; 122 | 123 | if (this.lastRenderedMonth === month) { 124 | days = this.days; 125 | } else { 126 | days = getDaysOfMonth(month); 127 | this.days = days; 128 | this.lastRenderedMonth = month; 129 | } 130 | 131 | return ( 132 |
133 | {children} 134 | 135 | 136 |
137 | { 138 | days.map(day => { 139 | const isCurrentMonth = day.format('jMM') === month.format('jMM'); 140 | const disabled = (min ? day.isBefore(min) : false) || (max ? day.isAfter(max) : false); 141 | const selected = selectedDay ? selectedDay.isSame(day, 'day') : false; 142 | 143 | return ( 144 | 153 | ); 154 | }) 155 | } 156 |
157 |
158 | ); 159 | } 160 | 161 | render() { 162 | const { 163 | selectedDay, 164 | min, 165 | max, 166 | onClickOutside, 167 | outsideClickIgnoreClass, 168 | styles, 169 | className 170 | } = this.props; 171 | const { mode } = this.state; 172 | 173 | return ( 174 |
175 | { mode === 'monthSelector' ? this.renderMonthSelector() : this.renderDays() } 176 |
177 | ); 178 | } 179 | } 180 | 181 | export default onClickOutside(Calendar); 182 | -------------------------------------------------------------------------------- /src/components/DatePicker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import moment from 'moment-jalali'; 4 | import TetherComponent from 'react-tether'; 5 | import Calendar from './Calendar'; 6 | import classnames from 'classnames'; 7 | 8 | export const outsideClickIgnoreClass = 'ignore--click--outside'; 9 | 10 | export default class DatePicker extends Component { 11 | static propTypes = { 12 | value: PropTypes.object, 13 | defaultValue: PropTypes.object, 14 | onChange: PropTypes.func, 15 | onFocus: PropTypes.func, 16 | onBlur: PropTypes.func, 17 | children: PropTypes.node, 18 | min: PropTypes.object, 19 | max: PropTypes.object, 20 | defaultMonth: PropTypes.object, 21 | inputFormat: PropTypes.string, 22 | removable: PropTypes.bool, 23 | timePickerComponent: PropTypes.func, 24 | calendarStyles: PropTypes.object, 25 | calendarContainerProps: PropTypes.object 26 | }; 27 | 28 | static defaultProps = { 29 | inputFormat: 'jYYYY/jM/jD', 30 | calendarStyles: require('../styles/basic.css'), 31 | calendarContainerProps: {} 32 | }; 33 | 34 | state = { 35 | isOpen: false, 36 | momentValue: this.props.defaultValue || null, 37 | inputValue: this.props.defaultValue ? 38 | this.props.defaultValue.format(this.props.inputFormat) : '' 39 | }; 40 | 41 | setOpen(isOpen) { 42 | this.setState({ isOpen }); 43 | } 44 | 45 | componentWillReceiveProps(nextProps) { 46 | if ('value' in nextProps && nextProps.value !== this.props.value) { 47 | this.setMomentValue(nextProps.value); 48 | } 49 | } 50 | 51 | setMomentValue(momentValue) { 52 | const { inputFormat } = this.props; 53 | 54 | if (this.props.onChange) { 55 | this.props.onChange(momentValue); 56 | } 57 | 58 | let inputValue = ""; 59 | if (momentValue) 60 | inputValue = momentValue.format(inputFormat); 61 | this.setState({ momentValue, inputValue }); 62 | } 63 | 64 | handleFocus() { 65 | this.setOpen(true); 66 | } 67 | 68 | handleBlur(event) { 69 | const { onBlur, inputFormat } = this.props; 70 | const { isOpen, momentValue } = this.state; 71 | 72 | if (isOpen) { 73 | this.refs.input.focus(); 74 | } else if (onBlur) { 75 | onBlur(event); 76 | } 77 | 78 | if (momentValue) { 79 | const inputValue = momentValue.format(inputFormat); 80 | this.setState({ inputValue }); 81 | } 82 | } 83 | 84 | handleClickOutsideCalendar() { 85 | this.setOpen(false); 86 | } 87 | 88 | handleSelectDay(selectedDay) { 89 | const { momentValue: oldValue } = this.state; 90 | let momentValue = selectedDay.clone(); 91 | 92 | if (oldValue) { 93 | momentValue = momentValue 94 | .set({ 95 | hour: oldValue.hours(), 96 | minute: oldValue.minutes(), 97 | second: oldValue.seconds() 98 | }); 99 | } 100 | 101 | this.setMomentValue(momentValue); 102 | } 103 | 104 | handleInputChange(event) { 105 | const { inputFormat } = this.props; 106 | const inputValue = event.target.value; 107 | const momentValue = moment(inputValue, inputFormat); 108 | 109 | if (momentValue.isValid()) { 110 | this.setState({ momentValue }); 111 | } 112 | 113 | this.setState({ inputValue }); 114 | } 115 | 116 | handleInputClick() { 117 | if (!this.props.disabled) { 118 | this.setOpen(true) 119 | } 120 | } 121 | 122 | renderInput() { 123 | let { isOpen, inputValue } = this.state; 124 | 125 | if (this.props.value) { 126 | let value = this.props.value; 127 | let inputFormat = this.props.inputFormat; 128 | inputValue = value.format(inputFormat); 129 | } 130 | 131 | const className = classnames(this.props.className, { 132 | [outsideClickIgnoreClass]: isOpen 133 | }); 134 | 135 | return ( 136 |
137 | 147 |
148 | ); 149 | } 150 | 151 | renderCalendar() { 152 | const { momentValue } = this.state; 153 | const { timePickerComponent: TimePicker, onChange, min, max, defaultMonth, calendarStyles, calendarContainerProps } = this.props; 154 | 155 | return ( 156 |
157 | 168 | { 169 | TimePicker ? ( 170 | 176 | ) : null 177 | } 178 | 179 |
180 | ); 181 | } 182 | 183 | removeDate() { 184 | const {onChange} = this.props; 185 | if (onChange) { 186 | onChange(''); 187 | } 188 | this.setState({ 189 | input: '', 190 | inputValue: '' 191 | }); 192 | } 193 | 194 | render() { 195 | const { isOpen } = this.state; 196 | 197 | return ( 198 | 199 | { this.renderInput() } 200 | { isOpen ? this.renderCalendar() : null } 201 | 202 | ); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/components/Day.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | import { persianNumber } from '../utils/persian'; 5 | 6 | const styles = { 7 | wrapper: {}, 8 | button: { 9 | outline: 'none', 10 | cursor: 'pointer' 11 | } 12 | }; 13 | 14 | export default class Day extends Component { 15 | static propTypes = { 16 | day: PropTypes.object.isRequired, 17 | isCurrentMonth: PropTypes.bool, 18 | disabled: PropTypes.bool, 19 | selected: PropTypes.bool, 20 | onClick: PropTypes.func 21 | }; 22 | 23 | shouldComponentUpdate(nextProps) { 24 | return nextProps.selected !== this.props.selected || 25 | nextProps.disabled !== this.props.disabled || 26 | nextProps.isCurrentMonth !== this.props.isCurrentMonth; 27 | } 28 | 29 | handleClick(event) { 30 | event.preventDefault(); 31 | event.stopPropagation(); 32 | event.nativeEvent.stopImmediatePropagation(); 33 | const { onClick, day } = this.props; 34 | 35 | if (onClick) { 36 | onClick(day); 37 | } 38 | } 39 | 40 | render() { 41 | const { day, disabled, selected, isCurrentMonth, onClick, styles, ...rest } = this.props; 42 | 43 | const className = classnames(styles.dayWrapper, { 44 | [styles.selected]: selected, 45 | [styles.currentMonth]: isCurrentMonth 46 | }); 47 | 48 | return ( 49 |
50 | 58 |
59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/DaysOfWeek.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | // Day of week names for use in date-picker heading 5 | const dayOfWeekNames = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج']; 6 | 7 | export default class DaysOfWeek extends Component { 8 | static propTypes = { 9 | styles: PropTypes.object 10 | }; 11 | 12 | render() { 13 | const { styles } = this.props; 14 | 15 | return ( 16 |
17 | { dayOfWeekNames.map((name, key) =>
{name}
) } 18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/DaysViewHeading.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { persianNumber } from '../utils/persian'; 4 | import { leftArrow, rightArrow } from '../utils/assets'; 5 | 6 | export default class Heading extends Component { 7 | static propTypes = { 8 | month: PropTypes.object.isRequired 9 | }; 10 | 11 | static contextTypes = { 12 | styles: PropTypes.object, 13 | nextMonth: PropTypes.func.isRequired, 14 | prevMonth: PropTypes.func.isRequired, 15 | setCalendarMode: PropTypes.func.isRequired 16 | }; 17 | 18 | handleMonthClick(event) { 19 | const { setCalendarMode } = this.context; 20 | event.preventDefault(); 21 | setCalendarMode('monthSelector'); 22 | } 23 | 24 | render() { 25 | const { nextMonth, prevMonth } = this.context; 26 | const { month, styles } = this.props; 27 | 28 | return ( 29 |
30 | 33 |
48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/MonthSelector.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import moment from 'moment-jalali'; 4 | import classnames from 'classnames'; 5 | import MonthsViewHeading from './MonthsViewHeading'; 6 | import { persianNumber } from '../utils/persian'; 7 | import { leftArrow, rightArrow } from '../utils/assets'; 8 | 9 | // List of months 10 | const months = [ 11 | 'فروردین', 12 | 'اردیبهشت', 13 | 'خرداد', 14 | 'تیر', 15 | 'مرداد', 16 | 'شهریور', 17 | 'مهر', 18 | 'آبان', 19 | 'آذر', 20 | 'دی', 21 | 'بهمن', 22 | 'اسفند' 23 | ]; 24 | 25 | export default class MonthSelector extends Component { 26 | static propTypes = { 27 | styles: PropTypes.object, 28 | selectedMonth: PropTypes.object.isRequired 29 | }; 30 | 31 | static contextTypes = { 32 | setCalendarMode: PropTypes.func.isRequired, 33 | setMonth: PropTypes.func.isRequired 34 | }; 35 | 36 | state = { 37 | year: this.props.selectedMonth 38 | }; 39 | 40 | nextYear() { 41 | this.setState({ 42 | year: this.state.year.clone().add(1, 'year') 43 | }); 44 | } 45 | 46 | prevYear() { 47 | this.setState({ 48 | year: this.state.year.clone().subtract(1, 'year') 49 | }); 50 | } 51 | 52 | handleClick(key) { 53 | const { setMonth, setCalendarMode } = this.context; 54 | setMonth(moment(key, 'jM-jYYYY')); 55 | setCalendarMode('days'); 56 | } 57 | 58 | render() { 59 | const { year } = this.state; 60 | const { selectedMonth, styles } = this.props; 61 | 62 | return ( 63 |
64 | 70 |
71 | { 72 | months.map((name, key) => { 73 | const buttonFingerprint = (key + 1) + '-' + year.format('jYYYY'); 74 | const selectedMonthFingerprint = selectedMonth.format('jM-jYYYY'); 75 | const isCurrent = selectedMonthFingerprint === buttonFingerprint; 76 | 77 | const className = classnames(styles.monthWrapper, { 78 | [styles.selected]: isCurrent 79 | }); 80 | 81 | return ( 82 |
83 | 86 |
87 | ); 88 | }) 89 | } 90 |
91 |
); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/components/MonthsViewHeading.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { persianNumber } from '../utils/persian'; 4 | import { leftArrow, rightArrow } from '../utils/assets'; 5 | 6 | export default class MonthsViewHeading extends Component { 7 | static propTypes = { 8 | year: PropTypes.object.isRequired, 9 | onNextYear: PropTypes.func.isRequired, 10 | onPrevYear: PropTypes.func.isRequired 11 | }; 12 | 13 | static contextTypes = { 14 | styles: PropTypes.object 15 | }; 16 | 17 | render() { 18 | const { year, styles } = this.props; 19 | 20 | return ( 21 |
22 | 23 | { persianNumber(year.format('jYYYY')) } 24 | 25 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export Calendar from './components/Calendar'; 2 | export DatePicker, { outsideClickIgnoreClass } from './components/DatePicker'; 3 | -------------------------------------------------------------------------------- /src/styles/basic.css: -------------------------------------------------------------------------------- 1 | .calendarContainer { 2 | border-radius: 3px; 3 | box-shadow: 0 3px 10px #dbdbdb; 4 | border: 1px solid #cccccc; 5 | width: 300px; 6 | margin: auto; 7 | text-align: center; 8 | padding: 10px; 9 | background-color: #fff; 10 | } 11 | 12 | .calendarContainer * { 13 | box-sizing: border-box; 14 | } 15 | 16 | .calendarContainer .dayPickerContainer:after, 17 | .calendarContainer .monthsList:after, 18 | .calendarContainer .daysOfWeek:after { 19 | content: ''; 20 | display: block; 21 | clear: both; 22 | } 23 | 24 | /* Heading */ 25 | .calendarContainer .heading { 26 | height: 42px; 27 | font-weight: bold; 28 | margin-bottom: 10px; 29 | } 30 | 31 | .calendarContainer .heading > button { 32 | border-radius: 3px; 33 | background: none; 34 | margin: 5px 0; 35 | border: 1px solid #F7F7F7; 36 | text-align: center; 37 | line-height: 30px; 38 | width: 36px; 39 | height: 32px; 40 | cursor: pointer; 41 | } 42 | 43 | .calendarContainer .heading > button:hover { 44 | background-color: #f2f2f2; 45 | } 46 | 47 | .calendarContainer .heading > span { 48 | line-height: 35px; 49 | } 50 | 51 | .calendarContainer .heading svg { 52 | width: 10px; 53 | } 54 | 55 | .calendarContainer .heading .prev { 56 | float: right; 57 | } 58 | 59 | .calendarContainer .heading .next { 60 | float: left; 61 | } 62 | 63 | .calendarContainer .heading .title { 64 | line-height: 32px; 65 | width: 120px; 66 | height: 32px; 67 | font-size: 1em; 68 | margin: 5px 0; 69 | border: 1px solid #F7F7F7; 70 | text-align: center; 71 | display: inline-block; 72 | font-weight: normal; 73 | } 74 | 75 | /* Day wrapper styles */ 76 | .calendarContainer .dayWrapper { 77 | padding: 5; 78 | float: right; 79 | width: 14.28571429%; 80 | } 81 | 82 | /* Day wrapper button styles */ 83 | .calendarContainer .dayWrapper button { 84 | border: none; 85 | background: none; 86 | outline: none; 87 | width: 100%; 88 | height: 36px; 89 | border-radius: 3px; 90 | cursor: pointer; 91 | } 92 | 93 | .calendarContainer .dayWrapper button:hover { 94 | background-color: #eeeeff; 95 | } 96 | 97 | .calendarContainer .dayWrapper button[disabled] { 98 | color: #aaa; 99 | cursor: not-allowed; 100 | background-color: #ebebeb; 101 | } 102 | 103 | .calendarContainer .dayWrapper button.selected { 104 | background-color: #337ab7; 105 | color: #ffffff; 106 | } 107 | 108 | .calendarContainer .dayWrapper:not(.currentMonth) button { 109 | opacity: .5; 110 | } 111 | 112 | /* Days of week row */ 113 | .calendarContainer .daysOfWeek { 114 | border-bottom: 1px solid #EEE; 115 | margin-bottom: 5px; 116 | padding-bottom: 5px; 117 | display: flex; 118 | width: 100%; 119 | } 120 | 121 | .calendarContainer .daysOfWeek > div { 122 | flex-grow: 1; 123 | justify-content: space-between; 124 | } 125 | 126 | /* Month selector portion */ 127 | .calendarContainer .monthsList { 128 | clear: both; 129 | width: 100%; 130 | } 131 | 132 | .calendarContainer .monthsList button { 133 | width: 33.33333332%; 134 | height: 25%; 135 | float: right; 136 | border: 1px solid #F9F9F9; 137 | outline: none; 138 | font-size: 1em; 139 | background: #fff; 140 | padding: 10px 0; 141 | cursor: pointer; 142 | } 143 | 144 | .calendarContainer .monthsList button:hover { 145 | background: #eeeeee; 146 | cursor: pointer; 147 | } 148 | 149 | /* Selected state of buttons */ 150 | .calendarContainer .selected button, 151 | .calendarContainer .selected button:hover, 152 | .calendarContainer .selected button:active, 153 | .calendarContainer .selected button:focus { 154 | background-color: #337ab7; 155 | color: #ffffff; 156 | } 157 | -------------------------------------------------------------------------------- /src/utils/assets.js: -------------------------------------------------------------------------------- 1 | export const leftArrow = { 2 | __html: '' 3 | }; 4 | 5 | export const rightArrow = { 6 | __html: '' 7 | }; 8 | 9 | export const remove = { 10 | __html: '' 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/moment-helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get days of a month that should be shown on a month page 3 | * 4 | * @param month A moment object 5 | * @returns {Array} 6 | */ 7 | export function getDaysOfMonth(month) { 8 | const days = []; 9 | 10 | const current = month.clone().startOf('jMonth'); 11 | const end = month.clone().endOf('jMonth'); 12 | 13 | // Set start to the first day of week in the last month 14 | current.subtract((current.day() + 1) % 7, 'days'); 15 | 16 | // Set end to the last day of week in the next month 17 | end.add(6 - (end.day() + 1) % 7, 'days'); 18 | 19 | while (current.isBefore(end)) { 20 | days.push(current.clone()); 21 | current.add(1, 'days'); 22 | } 23 | 24 | return days; 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/persian.js: -------------------------------------------------------------------------------- 1 | const latinToPersianMap = ['۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹', '۰']; 2 | const latinNumbers = [/1/g, /2/g, /3/g, /4/g, /5/g, /6/g, /7/g, /8/g, /9/g, /0/g]; 3 | 4 | function prepareNumber(input) { 5 | let string; 6 | if (typeof input === 'number') { 7 | string = input.toString(); 8 | } else if (typeof input === 'undefined') { 9 | string = ''; 10 | } else { 11 | string = input; 12 | } 13 | 14 | return string; 15 | } 16 | 17 | function latinToPersian(string) { 18 | let result = string; 19 | 20 | for (let index = 0; index < 10; index++) { 21 | result = result.replace(latinNumbers[index], latinToPersianMap[index]); 22 | } 23 | 24 | return result; 25 | } 26 | 27 | export function persianNumber(input) { 28 | return latinToPersian(prepareNumber(input)); 29 | } 30 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var webpack = require('webpack'); 3 | var babelrc = fs.readFileSync('./.babelrc'); 4 | var babelLoaderQuery = JSON.parse(babelrc); 5 | 6 | var plugins = [ 7 | new webpack.DefinePlugin({ 8 | 'process.env': { 9 | NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'production') 10 | } 11 | }) 12 | ]; 13 | 14 | if (process.env.NODE_ENV !== 'development') { 15 | plugins.push(new webpack.optimize.UglifyJsPlugin()); 16 | } 17 | 18 | module.exports = { 19 | context: __dirname, 20 | entry: [ 21 | './examples/src/main.js', 22 | './examples/src/main.css' 23 | ], 24 | resolve: { 25 | modulesDirectories: [ 26 | 'node_modules', 27 | 'examples' 28 | ], 29 | extensions: ['', '.json', '.js', '.jsx'] 30 | }, 31 | module: { 32 | loaders: [ 33 | { 34 | test: /\.jsx?$/, 35 | exclude: /node_modules/, 36 | loaders: ['babel-loader?' + JSON.stringify(babelLoaderQuery)] 37 | }, 38 | { 39 | test: /\.css?$/, 40 | exclude: /node_modules/, 41 | loader: 'style!css?modules&importLoaders=1' 42 | }, 43 | { 44 | test: /\.css?$/, 45 | include: /node_modules/, 46 | loader: 'style!css' 47 | } 48 | ] 49 | }, 50 | output: { 51 | filename: 'build.js', 52 | path: __dirname + '/examples' 53 | }, 54 | plugins: plugins 55 | }; --------------------------------------------------------------------------------