├── .babelrc.js ├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ └── bug-report.md ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── calendar-example.css ├── calendar.tsx ├── index.html ├── semantic.min.css └── themes │ └── default │ └── assets │ ├── fonts │ ├── brand-icons.eot │ ├── brand-icons.svg │ ├── brand-icons.ttf │ ├── brand-icons.woff │ ├── brand-icons.woff2 │ ├── icons.eot │ ├── icons.otf │ ├── icons.svg │ ├── icons.ttf │ ├── icons.woff │ ├── icons.woff2 │ ├── outline-icons.eot │ ├── outline-icons.svg │ ├── outline-icons.ttf │ ├── outline-icons.woff │ └── outline-icons.woff2 │ └── images │ └── flags.png ├── index.d.ts ├── package.json ├── src ├── index.ts ├── inputs │ ├── BaseInput.tsx │ ├── DateInput.tsx │ ├── DateTimeInput.tsx │ ├── DatesRangeInput.tsx │ ├── MonthInput.tsx │ ├── MonthRangeInput.tsx │ ├── TimeInput.tsx │ ├── YearInput.tsx │ ├── index.ts │ ├── parse.ts │ └── shared.ts ├── lib │ ├── CustomPropTypes.ts │ ├── checkIE.ts │ ├── checkMobile.ts │ ├── findHTMLElement.ts │ ├── index.ts │ └── tick.ts ├── pickers │ ├── BasePicker.tsx │ ├── YearPicker.tsx │ ├── dayPicker │ │ ├── DatesRangePicker.tsx │ │ ├── DayPicker.tsx │ │ └── sharedFunctions.ts │ ├── monthPicker │ │ ├── MonthPicker.tsx │ │ ├── MonthRangePicker.tsx │ │ ├── const.ts │ │ └── sharedFunctions.ts │ └── timePicker │ │ ├── HourPicker.tsx │ │ ├── MinutePicker.tsx │ │ └── sharedFunctions.ts └── views │ ├── BaseCalendarView.ts │ ├── Calendar.tsx │ ├── CalendarBody │ ├── Body.tsx │ ├── Cell.tsx │ └── index.ts │ ├── CalendarHeader │ ├── Header.tsx │ ├── HeaderRange.tsx │ ├── HeaderWeeks.tsx │ └── index.ts │ ├── DatesRangeView.tsx │ ├── DayView.tsx │ ├── HourView.tsx │ ├── InputView.tsx │ ├── MinuteView.tsx │ ├── MonthRangeView.tsx │ ├── MonthView.tsx │ └── YearView.tsx ├── test ├── .env ├── inputs │ ├── testDateInput.js │ ├── testMonthInput.js │ ├── testMonthRangeInput.js │ ├── testParse.js │ └── testYearInput.js ├── pickers │ ├── dayPicker │ │ ├── testDatesRangePicker.js │ │ ├── testDayPicker.js │ │ └── testSharedFunctions.js │ ├── testMonthPicker.js │ ├── testMonthRangePicker.js │ ├── testYearPicker.js │ └── timePicker │ │ ├── testHourPicker.js │ │ └── testMinutePicker.js ├── setup.js └── views │ └── testHeader.js ├── tsconfig.json ├── tslint.json ├── webpack.config.js ├── webpack.umd.config.js ├── yarn-error.log └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | const browsers = [ 2 | 'last 8 versions', 3 | 'safari > 8', 4 | 'firefox > 23', 5 | 'chrome > 24', 6 | 'opera > 15', 7 | 'not ie < 11', 8 | 'not ie_mob <= 11', 9 | ] 10 | 11 | module.exports = { 12 | presets: [ 13 | [ 14 | "@babel/env", 15 | { targets: browsers }, 16 | ], 17 | "@babel/react", 18 | ], 19 | plugins: [ 20 | "transform-react-handled-props", 21 | "@babel/plugin-proposal-class-properties", 22 | ], 23 | env: { 24 | production: { 25 | plugins: [ 26 | "transform-react-remove-prop-types", 27 | ], 28 | }, 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug. 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Dependencies versions** 24 | Provide version numbers of following packages: 25 | 26 | * semantic-ui-react 27 | * semantic-ui-css (or any alternative) 28 | * semantic-ui-calendar-react 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | calendar.bundle.js 4 | calendar.bundle.js.map 5 | # rely on yarn.lock instead 6 | package-lock.json 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | nbproject/private/ 24 | build/ 25 | nbbuild/ 26 | dist/ 27 | nbdist/ 28 | .nb-gradle/ 29 | 30 | ### OS X ### 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tslint.enable": true, 3 | "tslint.jsEnable": true, 4 | "tslint.run": "onSave" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.15.0 2019-05-04 4 | 5 | - feat: add ``hideMobileKeyboard`` prop [`#143`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/143) 6 | 7 | - feat: add ``className``'s to headers and wrap table cell content in span [`#139`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/139) 8 | 9 | - fix: Esc and Arrow keys fixes for Internet Explorer. And removed prevent default on arrow left/right events [`#137`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/137) 10 | 11 | - fix(DatesRangeInput): error when allowed range doesn't contain today [`#140`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/140) 12 | 13 | - fix (DateInput, DateTimeInput): update internal *Input date when value changed [`#142`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/142) 14 | 15 | ## v0.14.4 2019-03-30 16 | 17 | - fix(package): set proper types path [`#115`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/115) 18 | 19 | ## v0.14.3 2019-03-17 20 | 21 | - feat(DatesRangeInput): allow same start/end date selection when using [`#104`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/104) 22 | 23 | - fix: readOnly prop in InputView [`fe63e3d`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/fe63e3d35c22b62ef23511afba47f56346d03187) 24 | 25 | - fix: #95 #55 can't change years and months in IE [`#110`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/110) 26 | 27 | - chore: build for multiple module systems [`ef32ca7`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/ef32ca7b900a6d83245f84a6be06c1eb84c4a13f) 28 | 29 | - chore: add umd build [`#111`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/111) 30 | 31 | ## v0.14.2 2019-02-27 32 | 33 | - fix: can mark any date [`#98`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/98) 34 | 35 | - fix(InputView): don't wrap field in extra div [`#99`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/99) 36 | 37 | - fix: use "ui icon input" for correct styling [`#100`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/100) 38 | 39 | - fix: can remove icon from input [`#101`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/101) 40 | 41 | ## v0.14.1 2019-02-19 42 | 43 | - feat: allow to mark specific dates [`#77`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/77) 44 | 45 | - feat: MonthRangeInput [`#79`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/79) 46 | 47 | - feat: adds localization prop on each component [`#85`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/85) 48 | 49 | - fix(MinutePicker): #78 minDate disables minutes in each hour in the day [`93972b3`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/93972b3077b2957fb3e4d1f9ecd2e087e3fa4b3d) 50 | 51 | - fix: #83 pass popup mount node to inputView [`#89`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/86) 52 | 53 | - fix: #84 initialize picker state with input value [`#88`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/88) 54 | 55 | - fix(DateInput/DateTimeInput): #93 able to change year, month, date ... [`#96`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/96) 56 | 57 | ## v0.13.0 2019-01-12 58 | 59 | - feat: added transitions for popup [`65`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/65) 60 | 61 | - feat: pickerWidth and pickerStyle props on top level component [`6ed8e76`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/6ed8e76207012c11eae705c6d79de14e4b42623b) 62 | 63 | - fix: BasePicker SyntheticEvent generic type [`69`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/69) 64 | 65 | ## v0.12.2 2018-12-31 66 | 67 | - feat: add clearable props to Input [`#60`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/60) 68 | 69 | - fix: do not select disabled cells after page switch [`b536d89`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/b536d89e8af52e533c97735a0301a0c4dfd04963) 70 | 71 | - fix: not jump over 0th cell on ArrowLeft press [`394470c`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/394470c1105400ca3f62858dc0856da4125c047b) 72 | 73 | - fix(MinutePicker): getInitialDatePosition handles disabled positions [`c1ad726`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/c1ad72661e8d5a88efeacf5573ecfd2e9104bff8) 74 | 75 | - fix: #59 prevent selecting disabled values [`bab7718`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/bab7718df3f969e4deb6001517c14b8ac6bb6137) 76 | 77 | ## v0.12.1 2018-11-24 78 | 79 | - fix: stale input node reference [`32b56c3`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/32b56c381891bd716efb3a93e1ef8ef1ac0400a6) 80 | 81 | - fix: jump over disabled cell when keyboard navigating [`9c15bb1`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/9c15bb17505ea536c71df8d351a9c01441c635c6) 82 | 83 | - fix: if date in month/year disabled the whole month/year disabled [`ee9b673`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/ee9b673a981c436550f7fd3216d7129f2b9fd707) 84 | 85 | - fix: string value in `disable` prop doesn't work [`7ce6c73`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/7ce6c73b017fddd35534c2cb4b3b8433895074ec) 86 | 87 | ## v0.12.0 2018-11-19 88 | 89 | - feat: add disableMinute prop to TimeInput (#49) [`#49`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/49) 90 | 91 | - feat: keyboard shortcuts support [`0033d62`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/0033d62a8061c3cd1d2d9ff0fad7b0e17b0167a2) 92 | 93 | - fix: popup closes on selection [`e3d1807`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/e3d1807d810c06ff32936ab5c4f3ea4aedf12f53) 94 | 95 | - fix: extra Tab needed to navigate inside calendar [`5acc549`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/5acc5491de046b80fb3b444b3a664f327a1e15f2) 96 | 97 | - fix: remove on focus outline from poped up picker [`550f1a4`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/550f1a494b904811707459932314ad864dd815e8) 98 | 99 | ## v0.11.0 2018-11-03 100 | 101 | - feat: add dateTimeFormat prop to DateTimePicker (#42) [`#42`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/42) 102 | 103 | - fix(yearPicker): initialize page with selected value [`6c639aa`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/6c639aa70b53a8c7a56e83c24fdcab8c4aec2aff) 104 | 105 | - fix: #28 popup blured when inside Modal [`036a95f`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/036a95f052aefacfaf97afa66cdf09a8598c969a) 106 | 107 | - fix: initialDate prevent clearing input field [`8c51722`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/8c51722c670bf0b2a8beedb68550a2ec9b797e2d) 108 | 109 | ## v0.10.0 2018-10-18 110 | 111 | - feat: allow passthrough of mountNode to InputView Popup (#38) [`#38`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/38) 112 | 113 | - fix: #39 invalid date when first week in Jan starts with day from prev month (#40) [`#40`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/40) 114 | 115 | ## v0.9.1 2018-09-30 116 | 117 | - feat(preserveViewMode): allow preserveViewMode to reset mode onFocus [`#36`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/36) 118 | 119 | ## v0.9.0 2018-09-18 120 | 121 | - fix: #31 min/maxDate params not working [`b9f335f`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/b9f335f3b8e234549a9c2a144ba277b50bd5a5fe) 122 | - fix: #34 calendar popup unexpectedly closes [`5edea86`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/5edea86ccc9ac27e5af4aa9fb37b95b59a61e95b) 123 | - fix: #33 initialDate doesn't work [`d15f374`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/d15f374b15a181e092561bf959e1986188bda3c1) 124 | - fix: delay handle change on one tick [`4e012f4`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/4e012f4dfdf93d3767b1a84116985a08458ec6a6) 125 | - fix: weeks labels dont change locale [`24b0632`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/24b0632ac2b96bc0db864eb9f285bfb99ac2df6e) 126 | 127 | - feat(DateInput): `enable` attribute #30 [`53c19c3`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/53c19c351a3a867ef8f7a0e50bb92c407543cf28) 128 | 129 | ## v0.8.0 2018-08-04 130 | 131 | - feat: `closeOnMouseLeave` prop [`#23`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/23) 132 | 133 | - fix: #20 onClick prop got false instead of undefined [`#21`](https://github.com/arfedulov/semantic-ui-calendar-react/pull/21) 134 | 135 | - breaking: use Form.Input instead of Input [`abda4fb`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/abda4fb9059dc68ec09da3072e3e1d86463d58b1) 136 | 137 | 138 | ## v0.7.1 2018-07-22 139 | 140 | - feat: `disable`, `minDate`, `maxDate` attributes [`af0d3a9`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/af0d3a91933903f5fc82fee83e5a0499f44f544f) 141 | - feat: add `initialDate` attribute [`23e8008`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/23e800851716e0645451c99f2e0084937747a4c6) 142 | 143 | - fix(DatesRangeInput): clear selected range if `value` is empty [`3b57013`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/3b57013f3f8bd56092c7612f965894f4efc5109e) 144 | - fix(DateInput): accidental import couses error [`45b9811`](https://github.com/arfedulov/semantic-ui-calendar-react/commit/45b9811e6f780d4df4170bc0aca3ab3171f4539f) 145 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Artem Fedulov 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 | -------------------------------------------------------------------------------- /example/calendar-example.css: -------------------------------------------------------------------------------- 1 | .example-calendar-container { 2 | padding: 0 25px; 3 | max-width: 500px; 4 | margin: 50px auto; 5 | } 6 | 7 | .example-calendar-input { 8 | margin: 15px 0; 9 | } -------------------------------------------------------------------------------- /example/calendar.tsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | // import 'moment/locale/ru'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { 6 | Checkbox, 7 | Form, 8 | Header, 9 | Icon, 10 | } from 'semantic-ui-react'; 11 | 12 | import { 13 | DateInput, 14 | DateInputOnChangeData, 15 | DatesRangeInput, 16 | DatesRangeInputOnChangeData, 17 | DateTimeInput, 18 | DateTimeInputOnChangeData, 19 | MonthInput, 20 | MonthInputOnChangeData, 21 | MonthRangeInput, 22 | MonthRangeInputOnChangeData, 23 | TimeInput, 24 | TimeInputOnChangeData, 25 | YearInput, 26 | YearInputOnChangeData, 27 | } from '../src/inputs'; 28 | 29 | type DateTimeFormHandleChangeData = DateInputOnChangeData 30 | | DatesRangeInputOnChangeData 31 | | DateTimeInputOnChangeData 32 | | MonthInputOnChangeData 33 | | MonthRangeInputOnChangeData 34 | | TimeInputOnChangeData 35 | | YearInputOnChangeData; 36 | 37 | class App extends React.Component { 38 | constructor(props) { 39 | super(props); 40 | 41 | this.state = { 42 | clearable: false, 43 | }; 44 | } 45 | 46 | public render() { 47 | return ( 48 |
49 |
50 | As text fields 51 | 52 | 57 | 58 |
59 | 60 | 62 |

Inline

63 | 64 |
65 | ); 66 | } 67 | 68 | private handleCheckboxChange() { 69 | this.setState(() => ({ 70 | clearable: !this.state.clearable, 71 | })); 72 | } 73 | } 74 | 75 | class DateTimeForm extends React.Component { 76 | constructor(props) { 77 | super(props); 78 | 79 | this.state = { 80 | year: '', 81 | date: '', 82 | dateStartYear: '', 83 | time: '', 84 | dateTime: '', 85 | datesRange: '', 86 | month: '', 87 | monthRange: '', 88 | }; 89 | } 90 | 91 | public render() { 92 | const { clearable } = this.props; 93 | 94 | return ( 95 |
96 | )} 103 | clearable={clearable} 104 | animation='scale' 105 | duration={200} 106 | hideMobileKeyboard 107 | value={this.state.date} 108 | iconPosition='left' 109 | preserveViewMode={false} 110 | autoComplete='off' 111 | onChange={this.handleChange} 112 | /> 113 |
114 | 131 |
132 | 147 |
148 | 162 |
163 | 177 |
178 | 191 |
192 | 205 |
206 | 218 | 219 | ); 220 | } 221 | 222 | private handleChange = (event: React.SyntheticEvent, { name, value }: DateTimeFormHandleChangeData) => { 223 | if (this.state.hasOwnProperty(name)) { 224 | this.setState({ [name]: value }); 225 | } 226 | } 227 | } 228 | 229 | class DateTimeFormInline extends React.Component { 230 | constructor(props) { 231 | super(props); 232 | 233 | this.state = { 234 | year: '', 235 | month: '', 236 | date: '', 237 | time: '', 238 | dateTime: '', 239 | datesRange: '', 240 | monthRange: '', 241 | }; 242 | } 243 | 244 | public render() { 245 | return ( 246 |
247 | 256 |
257 | 264 |
265 | 272 |
273 | 280 |
281 | 288 |
289 | 296 |
297 | 304 | 305 | ); 306 | } 307 | 308 | private handleChange = (event: React.SyntheticEvent, { name, value }: DateTimeFormHandleChangeData) => { 309 | if (this.state.hasOwnProperty(name)) { 310 | this.setState({ [name]: value }); 311 | } 312 | } 313 | } 314 | 315 | ReactDOM.render( 316 | , 317 | document.getElementById('root'), 318 | ); 319 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/brand-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/brand-icons.eot -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/brand-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/brand-icons.ttf -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/brand-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/brand-icons.woff -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/brand-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/brand-icons.woff2 -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/icons.eot -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/icons.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/icons.otf -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/icons.woff -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/icons.woff2 -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/outline-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/outline-icons.eot -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/outline-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/outline-icons.ttf -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/outline-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/outline-icons.woff -------------------------------------------------------------------------------- /example/themes/default/assets/fonts/outline-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/fonts/outline-icons.woff2 -------------------------------------------------------------------------------- /example/themes/default/assets/images/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arfedulov/semantic-ui-calendar-react/b76efe860d1e7f0fab7af469a94ab366d621899e/example/themes/default/assets/images/flags.png -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as DateInput, 3 | DateInputProps, 4 | DateInputOnChangeData 5 | } from './dist/types/inputs/DateInput'; 6 | export { 7 | default as DateTimeInput, 8 | DateTimeInputProps, 9 | DateTimeInputOnChangeData 10 | } from './dist/types/inputs/DateTimeInput'; 11 | export { 12 | default as DatesRangeInput, 13 | DatesRangeInputProps, 14 | DatesRangeInputOnChangeData 15 | } from './dist/types/inputs/DatesRangeInput'; 16 | export { 17 | default as TimeInput, 18 | TimeInputProps, 19 | TimeInputOnChangeData 20 | } from './dist/types/inputs/TimeInput'; 21 | export { 22 | default as YearInput, 23 | YearInputProps, 24 | YearInputOnChangeData 25 | } from './dist/types/inputs/YearInput'; 26 | export { 27 | default as MonthInput, 28 | MonthInputProps, 29 | MonthInputOnChangeData 30 | } from './dist/types/inputs/MonthInput'; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semantic-ui-calendar-react", 3 | "sideEffects": false, 4 | "version": "0.15.3", 5 | "description": "date/time picker built from semantic-ui elements", 6 | "main": "dist/commonjs/index.js", 7 | "scripts": { 8 | "test": "yarn env-cmd ./test/.env yarn mocha -r ts-node/register ./test/setup.js ./test/**/*.{js,jsx,ts,tsx}", 9 | "start": "yarn webpack-dev-server", 10 | "prebuild": "yarn test && yarn lint && yarn rimraf dist/*", 11 | "build": "yarn build:commonjs && yarn build:es6 && yarn build:amd && yarn build:umd && yarn build:declarations", 12 | "build:commonjs": "yarn tsc --module commonjs --outDir ./dist/commonjs", 13 | "build:es6": "yarn tsc --module es6 --outDir ./dist/es6", 14 | "build:amd": "yarn tsc --module amd --outDir ./dist/amd", 15 | "build:umd": "yarn webpack --config webpack.umd.config.js", 16 | "build:declarations": "yarn tsc --declaration --emitDeclarationOnly --outDir ./dist/types", 17 | "publish-npm": "yarn test && yarn build && npm publish", 18 | "build:example": "yarn test && yarn lint && yarn webpack --production", 19 | "lint": "yarn tslint src/**" 20 | }, 21 | "keywords": [ 22 | "semantic", 23 | "react", 24 | "calendar", 25 | "datepicker" 26 | ], 27 | "types": "./dist/types/index.d.ts", 28 | "author": "Artem Fedulov ", 29 | "homepage": "https://github.com/arfedulov/semantic-ui-calendar-react#readme", 30 | "bugs": { 31 | "url": "https://github.com/arfedulov/semantic-ui-calendar-react/issues" 32 | }, 33 | "files": [ 34 | "src", 35 | "dist" 36 | ], 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/arfedulov/semantic-ui-calendar-react.git" 40 | }, 41 | "license": "MIT", 42 | "peerDependencies": { 43 | "react": "^16.6.0", 44 | "react-dom": "^16.6.0", 45 | "semantic-ui-react": ">=0.84.0" 46 | }, 47 | "devDependencies": { 48 | "@babel/cli": "^7.1.2", 49 | "@babel/core": "^7.1.2", 50 | "@babel/plugin-proposal-class-properties": "^7.1.0", 51 | "@babel/plugin-proposal-export-default-from": "^7.0.0", 52 | "@babel/plugin-proposal-export-namespace-from": "^7.0.0", 53 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 54 | "@babel/preset-env": "^7.1.0", 55 | "@babel/preset-react": "^7.0.0", 56 | "@babel/register": "^7.0.0", 57 | "@types/lodash": "^4.14.119", 58 | "@types/prop-types": "^15.7.1", 59 | "@types/react": "^16.7.18", 60 | "@types/react-dom": "^16.0.11", 61 | "babel-loader": "^8.0.4", 62 | "babel-plugin-transform-react-handled-props": "^1.0.0", 63 | "babel-plugin-transform-react-remove-prop-types": "^0.4.14", 64 | "babel-preset-airbnb": "^3.2.0", 65 | "chai": "^4.1.2", 66 | "cpy-cli": "^2.0.0", 67 | "env-cmd": "^8.0.2", 68 | "enzyme": "^3.3.0", 69 | "enzyme-adapter-react-16": "^1.1.1", 70 | "jsdom": "^13.1.0", 71 | "mkdirp": "^0.5.1", 72 | "mocha": "^5.2.0", 73 | "mockdate": "^2.0.2", 74 | "react": "^16.6.0", 75 | "react-dom": "^16.6.0", 76 | "rimraf": "^2.6.2", 77 | "semantic-ui-react": "^0.86.0", 78 | "sinon": "^7.2.2", 79 | "ts-loader": "^5.3.1", 80 | "ts-node": "^7.0.1", 81 | "tslint": "^5.11.0", 82 | "typescript": "^3.2.2", 83 | "webpack": "^4.28.3", 84 | "webpack-cli": "^3.1.2", 85 | "webpack-dev-server": "^3.1.10" 86 | }, 87 | "dependencies": { 88 | "keyboard-key": "^1.0.2", 89 | "lodash": "^4.17.15", 90 | "moment": "^2.22.2", 91 | "prop-types": "^15.6.2" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as DateInput, 3 | DateInputProps, 4 | DateInputOnChangeData, 5 | } from './inputs/DateInput'; 6 | export { 7 | default as DateTimeInput, 8 | DateTimeInputProps, 9 | DateTimeInputOnChangeData, 10 | } from './inputs/DateTimeInput'; 11 | export { 12 | default as DatesRangeInput, 13 | DatesRangeInputProps, 14 | DatesRangeInputOnChangeData, 15 | } from './inputs/DatesRangeInput'; 16 | export { 17 | default as TimeInput, 18 | TimeInputProps, 19 | TimeInputOnChangeData, 20 | } from './inputs/TimeInput'; 21 | export { 22 | default as YearInput, 23 | YearInputProps, 24 | YearInputOnChangeData, 25 | } from './inputs/YearInput'; 26 | export { 27 | default as MonthInput, 28 | MonthInputProps, 29 | MonthInputOnChangeData, 30 | } from './inputs/MonthInput'; 31 | export { 32 | default as MonthRangeInput, 33 | MonthRangeInputProps, 34 | MonthRangeInputOnChangeData, 35 | } from './inputs/MonthRangeInput'; 36 | -------------------------------------------------------------------------------- /src/inputs/DateInput.tsx: -------------------------------------------------------------------------------- 1 | import isNil from 'lodash/isNil'; 2 | import invoke from 'lodash/invoke'; 3 | import moment, { Moment } from 'moment'; 4 | import * as PropTypes from 'prop-types'; 5 | import * as React from 'react'; 6 | 7 | import { 8 | BasePickerOnChangeData, 9 | } from '../pickers/BasePicker'; 10 | import DayPicker from '../pickers/dayPicker/DayPicker'; 11 | import MonthPicker from '../pickers/monthPicker/MonthPicker'; 12 | import YearPicker from '../pickers/YearPicker'; 13 | import InputView from '../views/InputView'; 14 | import BaseInput, { 15 | BaseInputProps, 16 | BaseInputPropTypes, 17 | BaseInputState, 18 | DateRelatedProps, 19 | DateRelatedPropTypes, 20 | DisableValuesProps, 21 | DisableValuesPropTypes, 22 | EnableValuesProps, 23 | EnableValuesPropTypes, 24 | MinMaxValueProps, 25 | MinMaxValuePropTypes, 26 | MultimodeProps, 27 | MultimodePropTypes, 28 | MarkedValuesProps, 29 | MarkedValuesPropTypes, 30 | } from './BaseInput'; 31 | 32 | import { 33 | tick, 34 | } from '../lib'; 35 | import { 36 | buildValue, 37 | parseArrayOrValue, 38 | parseValue, 39 | dateValueToString, 40 | } from './parse'; 41 | import { 42 | getDisabledMonths, getDisabledYears, 43 | } from './shared'; 44 | 45 | type CalendarMode = 'year' | 'month' | 'day'; 46 | 47 | function getNextMode(currentMode: CalendarMode) { 48 | if (currentMode === 'year') { 49 | return 'month'; 50 | } 51 | if (currentMode === 'month') { 52 | return 'day'; 53 | } 54 | 55 | return 'year'; 56 | } 57 | 58 | function getPrevMode(currentMode: CalendarMode) { 59 | if (currentMode === 'day') { 60 | return 'month'; 61 | } 62 | if (currentMode === 'month') { 63 | return 'year'; 64 | } 65 | 66 | return 'day'; 67 | } 68 | 69 | export interface DateInputProps extends 70 | BaseInputProps, 71 | DateRelatedProps, 72 | MultimodeProps, 73 | DisableValuesProps, 74 | EnableValuesProps, 75 | MarkedValuesProps, 76 | MinMaxValueProps { 77 | /** Display mode to start. */ 78 | startMode?: CalendarMode; 79 | } 80 | 81 | export type DateInputOnChangeData = DateInputProps; 82 | 83 | interface DateInputState extends BaseInputState { 84 | mode: CalendarMode; 85 | year: number; 86 | month: number; 87 | date: number; 88 | } 89 | 90 | class DateInput extends BaseInput { 91 | /** 92 | * Component responsibility: 93 | * - parse input value 94 | * - handle underlying picker change 95 | */ 96 | public static readonly defaultProps = { 97 | ...BaseInput.defaultProps, 98 | dateFormat: 'DD-MM-YYYY', 99 | startMode: 'day', 100 | preserveViewMode: true, 101 | icon: 'calendar', 102 | }; 103 | 104 | public static readonly propTypes = { 105 | ...BaseInputPropTypes, 106 | ...DateRelatedPropTypes, 107 | ...MultimodePropTypes, 108 | ...DisableValuesPropTypes, 109 | ...EnableValuesPropTypes, 110 | ...MarkedValuesPropTypes, 111 | ...MinMaxValuePropTypes, 112 | ...{ 113 | /** Display mode to start. */ 114 | startMode: PropTypes.oneOf([ 'year', 'month', 'day' ]), 115 | }, 116 | }; 117 | 118 | constructor(props: DateInputProps) { 119 | super(props); 120 | const parsedValue = parseValue(props.value, props.dateFormat, props.localization); 121 | this.state = { 122 | mode: props.startMode, 123 | popupIsClosed: true, 124 | year: parsedValue ? parsedValue.year() : undefined, 125 | month: parsedValue ? parsedValue.month() : undefined, 126 | date: parsedValue ? parsedValue.date() : undefined, 127 | }; 128 | } 129 | 130 | public componentDidUpdate = (prevProps: DateInputProps) => { 131 | // update internal date if ``value`` prop changed and successuffly parsed 132 | if (prevProps.value !== this.props.value) { 133 | const parsed = parseValue(this.props.value, this.props.dateFormat, this.props.localization); 134 | if (parsed) { 135 | this.setState({ 136 | year: parsed.year(), 137 | month: parsed.month(), 138 | date: parsed.date(), 139 | }); 140 | } 141 | } 142 | } 143 | 144 | public render() { 145 | const { 146 | value, 147 | dateFormat, 148 | initialDate, 149 | disable, 150 | enable, 151 | maxDate, 152 | minDate, 153 | preserveViewMode, 154 | startMode, 155 | closable, 156 | markColor, 157 | marked, 158 | localization, 159 | onChange, 160 | ...rest 161 | } = this.props; 162 | 163 | return ( 164 | this.getPicker()} 173 | value={dateValueToString(value, dateFormat, localization)} 174 | /> 175 | ); 176 | } 177 | 178 | private parseInternalValue(): Moment { 179 | /* 180 | Creates moment instance from values stored in component's state 181 | (year, month, date) in order to pass this moment instance to 182 | underlying picker. 183 | Return undefined if none of these state fields has value. 184 | */ 185 | const { 186 | year, 187 | month, 188 | date, 189 | } = this.state; 190 | if (!isNil(year) || !isNil(month) || !isNil(date)) { 191 | return moment({ year, month, date }); 192 | } 193 | } 194 | 195 | private getPicker = () => { 196 | const { 197 | value, 198 | initialDate, 199 | dateFormat, 200 | disable, 201 | minDate, 202 | maxDate, 203 | enable, 204 | inline, 205 | marked, 206 | markColor, 207 | localization, 208 | tabIndex, 209 | pickerWidth, 210 | pickerStyle, 211 | } = this.props; 212 | const pickerProps = { 213 | isPickerInFocus: this.isPickerInFocus, 214 | isTriggerInFocus: this.isTriggerInFocus, 215 | inline, 216 | onCalendarViewMount: this.onCalendarViewMount, 217 | closePopup: this.closePopup, 218 | tabIndex, 219 | pickerWidth, 220 | pickerStyle, 221 | onChange: this.handleSelect, 222 | onHeaderClick: this.switchToPrevMode, 223 | initializeWith: buildValue(this.parseInternalValue(), initialDate, localization, dateFormat), 224 | value: buildValue(value, null, localization, dateFormat, null), 225 | enable: parseArrayOrValue(enable, dateFormat, localization), 226 | minDate: parseValue(minDate, dateFormat, localization), 227 | maxDate: parseValue(maxDate, dateFormat, localization), 228 | localization, 229 | }; 230 | const disableParsed = parseArrayOrValue(disable, dateFormat, localization); 231 | const markedParsed = parseArrayOrValue(marked, dateFormat, localization); 232 | const { mode } = this.state; 233 | if (mode === 'year') { 234 | return ( 235 | 239 | ); 240 | } 241 | if (mode === 'month') { 242 | return ( 243 | 248 | ); 249 | } 250 | 251 | return ; 252 | } 253 | 254 | private switchToNextModeUndelayed = (): void => { 255 | this.setState(({ mode }) => { 256 | return { mode: getNextMode(mode) }; 257 | }, this.onModeSwitch); 258 | } 259 | 260 | private switchToNextMode = (): void => { 261 | tick(this.switchToNextModeUndelayed); 262 | } 263 | 264 | private switchToPrevModeUndelayed = (): void => { 265 | this.setState(({ mode }) => { 266 | return { mode: getPrevMode(mode) }; 267 | }, this.onModeSwitch); 268 | } 269 | 270 | private switchToPrevMode = (): void => { 271 | tick(this.switchToPrevModeUndelayed); 272 | } 273 | 274 | private onFocus = (): void => { 275 | if (!this.props.preserveViewMode) { 276 | this.setState({ mode: this.props.startMode }); 277 | } 278 | } 279 | 280 | private handleSelect = (e, { value }: BasePickerOnChangeData) => { 281 | if (this.state.mode === 'day' && this.props.closable) { 282 | this.closePopup(); 283 | } 284 | this.setState((prevState) => { 285 | const { 286 | mode, 287 | } = prevState; 288 | if (mode === 'day') { 289 | const outValue = moment(value).format(this.props.dateFormat); 290 | invoke(this.props, 'onChange', e, { ...this.props, value: outValue }); 291 | } 292 | 293 | return { 294 | year: value.year, 295 | month: value.month, 296 | date: value.date, 297 | }; 298 | }, () => this.state.mode !== 'day' && this.switchToNextMode()); 299 | } 300 | 301 | /** Keeps internal state in sync with input field value. */ 302 | private onInputValueChange = (e, { value }) => { 303 | const parsedValue = moment(value, this.props.dateFormat); 304 | if (parsedValue.isValid()) { 305 | this.setState({ 306 | year: parsedValue.year(), 307 | month: parsedValue.month(), 308 | date: parsedValue.date(), 309 | }); 310 | } 311 | invoke(this.props, 'onChange', e, { ...this.props, value }); 312 | } 313 | } 314 | 315 | export default DateInput; 316 | -------------------------------------------------------------------------------- /src/inputs/DatesRangeInput.tsx: -------------------------------------------------------------------------------- 1 | import invoke from 'lodash/invoke'; 2 | import * as React from 'react'; 3 | 4 | import InputView from '../views/InputView'; 5 | import { 6 | parseDatesRange, 7 | parseValue, 8 | parseArrayOrValue, 9 | buildValue, 10 | } from './parse'; 11 | 12 | import DatesRangePicker, { 13 | DatesRangePickerOnChangeData, 14 | } from '../pickers/dayPicker/DatesRangePicker'; 15 | import BaseInput, { 16 | BaseInputProps, 17 | BaseInputPropTypes, 18 | BaseInputState, 19 | DateRelatedProps, 20 | DateRelatedPropTypes, 21 | MinMaxValueProps, 22 | MinMaxValuePropTypes, 23 | MarkedValuesProps, 24 | MarkedValuesPropTypes, 25 | RangeRelatedProps, 26 | RangeRelatedPropTypes, 27 | } from './BaseInput'; 28 | 29 | const DATES_SEPARATOR = ' - '; 30 | 31 | export type DatesRangeInputProps = 32 | & BaseInputProps 33 | & DateRelatedProps 34 | & MarkedValuesProps 35 | & MinMaxValueProps 36 | & RangeRelatedProps; 37 | 38 | export type DatesRangeInputOnChangeData = DatesRangeInputProps; 39 | 40 | class DatesRangeInput extends BaseInput { 41 | /** 42 | * Component responsibility: 43 | * - parse input value (start: Moment, end: Moment) 44 | * - handle DayPicker change (format {start: Moment, end: Moment} into 45 | * string 'start - end') 46 | */ 47 | public static readonly defaultProps = { 48 | ...BaseInput.defaultProps, 49 | dateFormat: 'DD-MM-YYYY', 50 | icon: 'calendar', 51 | }; 52 | 53 | public static readonly propTypes = { 54 | ...BaseInputPropTypes, 55 | ...DateRelatedPropTypes, 56 | ...MarkedValuesPropTypes, 57 | ...MinMaxValuePropTypes, 58 | ...RangeRelatedPropTypes, 59 | }; 60 | 61 | constructor(props) { 62 | super(props); 63 | this.state = { 64 | popupIsClosed: true, 65 | }; 66 | } 67 | 68 | public render() { 69 | const { 70 | value, 71 | dateFormat, 72 | initialDate, 73 | maxDate, 74 | minDate, 75 | closable, 76 | marked, 77 | markColor, 78 | localization, 79 | allowSameEndDate, 80 | ...rest 81 | } = this.props; 82 | 83 | return ( 84 | 93 | ); 94 | } 95 | 96 | private getPicker = () => { 97 | const { 98 | value, 99 | dateFormat, 100 | markColor, 101 | marked, 102 | initialDate, 103 | localization, 104 | minDate, 105 | maxDate, 106 | tabIndex, 107 | pickerWidth, 108 | pickerStyle, 109 | allowSameEndDate, 110 | } = this.props; 111 | const { 112 | start, 113 | end, 114 | } = parseDatesRange(value, dateFormat); 115 | 116 | const markedParsed = parseArrayOrValue(marked, dateFormat, localization); 117 | const minDateParsed = parseValue(minDate, dateFormat, localization); 118 | const maxDateParsed = parseValue(maxDate, dateFormat, localization); 119 | 120 | let initializeWith; 121 | 122 | if (!initialDate && minDateParsed || maxDateParsed) { 123 | initializeWith = minDateParsed || maxDateParsed; 124 | } else { 125 | initializeWith = buildValue(start, initialDate, localization, dateFormat); 126 | } 127 | 128 | return ( 129 | undefined} 146 | tabIndex={tabIndex} 147 | pickerWidth={pickerWidth} 148 | pickerStyle={pickerStyle} 149 | allowSameEndDate={allowSameEndDate} 150 | /> 151 | ); 152 | } 153 | 154 | private handleSelect = (e: React.SyntheticEvent, 155 | { value }: DatesRangePickerOnChangeData) => { 156 | const { dateFormat } = this.props; 157 | const { 158 | start, 159 | end, 160 | } = value; 161 | let outputString = ''; 162 | if (start && end) { 163 | outputString = `${start.format(dateFormat)}${DATES_SEPARATOR}${end.format(dateFormat)}`; 164 | } else if (start) { 165 | outputString = `${start.format(dateFormat)}${DATES_SEPARATOR}`; 166 | } 167 | invoke(this.props, 'onChange', e, { ...this.props, value: outputString }); 168 | if (this.props.closable && start && end) { 169 | this.closePopup(); 170 | } 171 | } 172 | } 173 | 174 | export default DatesRangeInput; 175 | -------------------------------------------------------------------------------- /src/inputs/MonthInput.tsx: -------------------------------------------------------------------------------- 1 | import invoke from 'lodash/invoke'; 2 | import moment from 'moment'; 3 | import * as React from 'react'; 4 | 5 | import MonthPicker, { 6 | MonthPickerOnChangeData, 7 | } from '../pickers/monthPicker/MonthPicker'; 8 | import InputView from '../views/InputView'; 9 | import BaseInput, { 10 | BaseInputProps, 11 | BaseInputPropTypes, 12 | BaseInputState, 13 | DateRelatedProps, 14 | DateRelatedPropTypes, 15 | DisableValuesProps, 16 | DisableValuesPropTypes, 17 | MinMaxValueProps, 18 | MinMaxValuePropTypes, 19 | } from './BaseInput'; 20 | import { 21 | parseArrayOrValue, 22 | parseValue, 23 | buildValue, 24 | dateValueToString, 25 | } from './parse'; 26 | 27 | export type MonthInputProps = 28 | & BaseInputProps 29 | & DateRelatedProps 30 | & DisableValuesProps 31 | & MinMaxValueProps; 32 | 33 | export type MonthInputOnChangeData = MonthInputProps; 34 | 35 | class MonthInput extends BaseInput { 36 | public static readonly defaultProps = { 37 | ...BaseInput.defaultProps, 38 | dateFormat: 'MMM', 39 | icon: 'calendar', 40 | }; 41 | 42 | public static readonly propTypes = { 43 | ...BaseInputPropTypes, 44 | ...DateRelatedPropTypes, 45 | ...DisableValuesPropTypes, 46 | ...MinMaxValuePropTypes, 47 | }; 48 | 49 | constructor(props) { 50 | super(props); 51 | this.state = { 52 | popupIsClosed: true, 53 | }; 54 | } 55 | 56 | public render() { 57 | const { 58 | value, 59 | dateFormat, 60 | initialDate, 61 | disable, 62 | maxDate, 63 | minDate, 64 | closable, 65 | localization, 66 | ...rest 67 | } = this.props; 68 | 69 | return ( 70 | 79 | ); 80 | } 81 | 82 | private getPicker = () => { 83 | const { 84 | value, 85 | dateFormat, 86 | disable, 87 | maxDate, 88 | minDate, 89 | localization, 90 | initialDate, 91 | } = this.props; 92 | 93 | return ( 94 | undefined} 109 | /> 110 | ); 111 | } 112 | 113 | private handleSelect = (e: React.SyntheticEvent, 114 | { value }: MonthPickerOnChangeData) => { 115 | const { localization } = this.props; 116 | const date = localization ? moment({ month: value.month }).locale(localization) : moment({ month: value.month }); 117 | let output = ''; 118 | if (date.isValid()) { 119 | output = date.format(this.props.dateFormat); 120 | } 121 | invoke( 122 | this.props, 123 | 'onChange', 124 | e, { ...this.props, value: output }); 125 | if (this.props.closable) { 126 | this.closePopup(); 127 | } 128 | } 129 | } 130 | 131 | export default MonthInput; 132 | -------------------------------------------------------------------------------- /src/inputs/MonthRangeInput.tsx: -------------------------------------------------------------------------------- 1 | import invoke from 'lodash/invoke'; 2 | import * as React from 'react'; 3 | import BaseInput, { 4 | BaseInputProps, 5 | BaseInputPropTypes, 6 | BaseInputState, 7 | DateRelatedProps, 8 | DateRelatedPropTypes, 9 | MinMaxValueProps, 10 | MinMaxValuePropTypes, 11 | } from './BaseInput'; 12 | 13 | import MonthRangePicker from '../pickers/monthPicker/MonthRangePicker'; 14 | import InputView from '../views/InputView'; 15 | import { 16 | parseDatesRange, 17 | parseValue, 18 | buildValue, 19 | } from './parse'; 20 | import { BasePickerOnChangeData } from 'src/pickers/BasePicker'; 21 | 22 | const DATES_SEPARATOR = ' - '; 23 | 24 | export type MonthRangeInputProps = 25 | & BaseInputProps 26 | & DateRelatedProps 27 | & MinMaxValueProps; 28 | 29 | export type MonthRangeInputOnChangeData = MonthRangeInputProps; 30 | 31 | class MonthRangeInput extends BaseInput { 32 | public static readonly defaultProps = { 33 | ...BaseInput.defaultProps, 34 | dateFormat: 'MM-YYYY', 35 | icon: 'calendar', 36 | }; 37 | 38 | public static readonly propTypes = { 39 | ...BaseInputPropTypes, 40 | ...DateRelatedPropTypes, 41 | ...MinMaxValuePropTypes, 42 | }; 43 | 44 | constructor(props) { 45 | super(props); 46 | this.state = { 47 | popupIsClosed: true, 48 | }; 49 | } 50 | 51 | public render() { 52 | const { 53 | value, 54 | dateFormat, 55 | initialDate, 56 | maxDate, 57 | minDate, 58 | closable, 59 | localization, 60 | ...rest 61 | } = this.props; 62 | 63 | return ( 64 | 73 | ); 74 | } 75 | 76 | private getPicker = () => { 77 | const { 78 | value, 79 | dateFormat, 80 | initialDate, 81 | maxDate, 82 | minDate, 83 | localization, 84 | } = this.props; 85 | const { 86 | start, 87 | end, 88 | } = parseDatesRange(value, dateFormat); 89 | 90 | return ( 91 | undefined} 106 | /> 107 | ); 108 | } 109 | 110 | private handleSelect = (e: React.SyntheticEvent, 111 | {value}: BasePickerOnChangeData) => { 112 | const {dateFormat} = this.props; 113 | const { 114 | start, 115 | end, 116 | } = value; 117 | let outputString = ''; 118 | if (start && end) { 119 | outputString = `${start.format(dateFormat)}${DATES_SEPARATOR}${end.format(dateFormat)}`; 120 | } else if (start) { 121 | outputString = `${start.format(dateFormat)}${DATES_SEPARATOR}`; 122 | } 123 | 124 | invoke(this.props, 'onChange', e, {...this.props, value: outputString, date: value}); 125 | if (this.props.closable && start && end) { 126 | this.closePopup(); 127 | } 128 | } 129 | } 130 | 131 | export default MonthRangeInput; 132 | -------------------------------------------------------------------------------- /src/inputs/TimeInput.tsx: -------------------------------------------------------------------------------- 1 | import isNil from 'lodash/isNil'; 2 | import invoke from 'lodash/invoke'; 3 | 4 | import moment from 'moment'; 5 | import * as React from 'react'; 6 | 7 | import { tick } from '../lib'; 8 | import { 9 | BasePickerOnChangeData, 10 | } from '../pickers/BasePicker'; 11 | import HourPicker from '../pickers/timePicker/HourPicker'; 12 | import MinutePicker from '../pickers/timePicker/MinutePicker'; 13 | import InputView from '../views/InputView'; 14 | import BaseInput, { 15 | BaseInputProps, 16 | BaseInputPropTypes, 17 | BaseInputState, 18 | MultimodeProps, 19 | MultimodePropTypes, 20 | TimeRelatedProps, 21 | TimeRelatedPropTypes, 22 | } from './BaseInput'; 23 | import { 24 | parseValue, 25 | TIME_FORMAT, 26 | buildValue, 27 | } from './parse'; 28 | 29 | function getNextMode(currentMode) { 30 | if (currentMode === 'hour') { 31 | return 'minute'; 32 | } 33 | 34 | return 'hour'; 35 | } 36 | 37 | type CalendarMode = 'hour' | 'minute'; 38 | 39 | export type TimeInputProps = 40 | & BaseInputProps 41 | & MultimodeProps 42 | & TimeRelatedProps; 43 | 44 | export type TimeInputOnChangeData = TimeInputProps; 45 | 46 | interface TimeInputState extends BaseInputState { 47 | mode: CalendarMode; 48 | } 49 | 50 | class TimeInput extends BaseInput { 51 | /** 52 | * Component responsibility: 53 | * - parse time input string 54 | * - switch between modes ['hour', 'minute'] 55 | * - handle HourPicker/MinutePicker change (format { hour: number, minute: number } into output time string) 56 | */ 57 | public static readonly defaultProps = { 58 | ...BaseInput.defaultProps, 59 | icon: 'time', 60 | timeFormat: '24', 61 | disableMinute: false, 62 | }; 63 | 64 | public static readonly propTypes = { 65 | ...BaseInputPropTypes, 66 | ...MultimodePropTypes, 67 | ...TimeRelatedPropTypes, 68 | }; 69 | 70 | constructor(props) { 71 | super(props); 72 | this.state = { 73 | mode: 'hour', 74 | popupIsClosed: true, 75 | }; 76 | } 77 | 78 | public render() { 79 | const { 80 | value, 81 | timeFormat, 82 | closable, 83 | disableMinute, 84 | ...rest 85 | } = this.props; 86 | 87 | return ( 88 | this.getPicker()} 96 | /> 97 | ); 98 | } 99 | 100 | private handleSelect = ( 101 | e: React.SyntheticEvent, 102 | { value }: BasePickerOnChangeData, 103 | ) => { 104 | 105 | tick(this.handleSelectUndelayed, e, { value }); 106 | } 107 | 108 | private handleSelectUndelayed = ( 109 | e: React.SyntheticEvent, 110 | { value }: BasePickerOnChangeData, 111 | ) => { 112 | 113 | const { 114 | hour, 115 | minute, 116 | } = value; 117 | const { 118 | timeFormat, 119 | disableMinute, 120 | } = this.props; 121 | 122 | let outputTimeString = ''; 123 | if (this.state.mode === 'hour' && !isNil(hour)) { 124 | outputTimeString = moment({ hour }).format(TIME_FORMAT[timeFormat]); 125 | } else if (!isNil(hour) && !isNil(minute)) { 126 | outputTimeString = moment({ hour, minute }).format(TIME_FORMAT[timeFormat]); 127 | } 128 | invoke(this.props, 'onChange', e, { ...this.props, value: outputTimeString }); 129 | if (this.props.closable && (this.state.mode === 'minute' || this.props.disableMinute)) { 130 | this.closePopup(); 131 | } 132 | if (!disableMinute) { 133 | this.switchToNextMode(); 134 | } 135 | } 136 | 137 | private switchToNextMode = () => { 138 | this.setState(({ mode }) => { 139 | return { mode: getNextMode(mode) }; 140 | }, this.onModeSwitch); 141 | } 142 | 143 | private getPicker() { 144 | const { 145 | value, 146 | timeFormat, 147 | inline, 148 | localization, 149 | tabIndex, 150 | pickerStyle, 151 | pickerWidth, 152 | } = this.props; 153 | const currentValue = parseValue(value, TIME_FORMAT[timeFormat], localization); 154 | const pickerProps = { 155 | inline, 156 | onCalendarViewMount: this.onCalendarViewMount, 157 | isPickerInFocus: this.isPickerInFocus, 158 | isTriggerInFocus: this.isTriggerInFocus, 159 | hasHeader: false, 160 | pickerWidth, 161 | pickerStyle, 162 | onHeaderClick: () => undefined, 163 | closePopup: this.closePopup, 164 | initializeWith: buildValue(currentValue, null, localization, TIME_FORMAT[timeFormat]), 165 | value: buildValue(currentValue, null, TIME_FORMAT[timeFormat], localization, null), 166 | onChange: this.handleSelect, 167 | timeFormat, 168 | tabIndex, 169 | localization, 170 | }; 171 | if (this.state.mode === 'hour') { 172 | return ; 173 | } 174 | 175 | return ; 176 | } 177 | } 178 | 179 | export default TimeInput; 180 | -------------------------------------------------------------------------------- /src/inputs/YearInput.tsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import * as React from 'react'; 3 | 4 | import YearPicker, { 5 | YearPickerOnChangeData, 6 | } from '../pickers/YearPicker'; 7 | import InputView from '../views/InputView'; 8 | import BaseInput, { 9 | BaseInputProps, 10 | BaseInputPropTypes, 11 | BaseInputState, 12 | DateRelatedProps, 13 | DateRelatedPropTypes, 14 | DisableValuesProps, 15 | DisableValuesPropTypes, 16 | MinMaxValueProps, 17 | MinMaxValuePropTypes, 18 | } from './BaseInput'; 19 | import { 20 | parseArrayOrValue, 21 | parseValue, 22 | buildValue, 23 | } from './parse'; 24 | 25 | export type YearInputProps = 26 | & BaseInputProps 27 | & DateRelatedProps 28 | & MinMaxValueProps 29 | & DisableValuesProps; 30 | 31 | export type YearInputOnChangeData = YearInputProps; 32 | 33 | class YearInput extends BaseInput { 34 | public static readonly defaultProps = { 35 | ...BaseInput.defaultProps, 36 | dateFormat: 'YYYY', 37 | icon: 'calendar', 38 | }; 39 | 40 | public static readonly propTypes = { 41 | ...BaseInputPropTypes, 42 | ...DateRelatedPropTypes, 43 | ...MinMaxValuePropTypes, 44 | ...DisableValuesPropTypes, 45 | }; 46 | 47 | constructor(props) { 48 | super(props); 49 | this.state = { 50 | popupIsClosed: true, 51 | }; 52 | } 53 | 54 | public render() { 55 | const { 56 | value, 57 | disable, 58 | maxDate, 59 | minDate, 60 | initialDate, 61 | dateFormat, 62 | closable, 63 | localization, 64 | ...rest 65 | } = this.props; 66 | 67 | return ( 68 | 77 | ); 78 | } 79 | 80 | private getPicker = () => { 81 | const { 82 | value, 83 | disable, 84 | maxDate, 85 | minDate, 86 | initialDate, 87 | dateFormat, 88 | localization, 89 | } = this.props; 90 | 91 | return ( 92 | undefined} 105 | /> 106 | ); 107 | } 108 | 109 | private handleSelect = (e: React.SyntheticEvent, 110 | { value }: YearPickerOnChangeData) => { 111 | const { localization } = this.props; 112 | const date = localization ? moment({ year: value.year }).locale(localization) : moment({ year: value.year }); 113 | let output = ''; 114 | if (date.isValid()) { 115 | output = date.format(this.props.dateFormat); 116 | } 117 | const data = { 118 | ...this.props, 119 | value: output, 120 | }; 121 | this.props.onChange(e, data); 122 | if (this.props.closable) { 123 | this.closePopup(); 124 | } 125 | } 126 | } 127 | 128 | export default YearInput; 129 | -------------------------------------------------------------------------------- /src/inputs/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as DateInput, 3 | DateInputProps, 4 | DateInputOnChangeData, 5 | } from './DateInput'; 6 | export { 7 | default as DateTimeInput, 8 | DateTimeInputProps, 9 | DateTimeInputOnChangeData, 10 | } from './DateTimeInput'; 11 | export { 12 | default as DatesRangeInput, 13 | DatesRangeInputProps, 14 | DatesRangeInputOnChangeData, 15 | } from './DatesRangeInput'; 16 | export { 17 | default as TimeInput, 18 | TimeInputProps, 19 | TimeInputOnChangeData, 20 | } from './TimeInput'; 21 | export { 22 | default as YearInput, 23 | YearInputProps, 24 | YearInputOnChangeData, 25 | } from './YearInput'; 26 | export { 27 | default as MonthInput, 28 | MonthInputProps, 29 | MonthInputOnChangeData, 30 | } from './MonthInput'; 31 | export { 32 | default as MonthRangeInput, 33 | MonthRangeInputProps, 34 | MonthRangeInputOnChangeData, 35 | } from './MonthRangeInput'; 36 | -------------------------------------------------------------------------------- /src/inputs/parse.ts: -------------------------------------------------------------------------------- 1 | import isNil from 'lodash/isNil'; 2 | import isArray from 'lodash/isArray'; 3 | import isString from 'lodash/isString'; 4 | import compact from 'lodash/compact'; 5 | 6 | import moment, { Moment } from 'moment'; 7 | 8 | export const TIME_FORMAT = { 9 | 24: 'HH:mm', 10 | AMPM: 'hh:mm A', 11 | ampm: 'hh:mm a', 12 | }; 13 | 14 | type ParseValueData = 15 | | string 16 | | moment.Moment 17 | | Date; 18 | 19 | /** Parse string, moment, Date. 20 | * 21 | * Return unedfined on invalid input. 22 | */ 23 | export function parseValue(value: ParseValueData, dateFormat: string, localization: string): moment.Moment { 24 | if (!isNil(value) && !isNil(dateFormat)) { 25 | const date = moment(value, dateFormat); 26 | if (date.isValid()) { 27 | date.locale(localization); 28 | 29 | return date; 30 | } 31 | } 32 | } 33 | 34 | type ParseArrayOrValueData = 35 | | ParseValueData 36 | | ParseValueData[]; 37 | 38 | /** Parse string, moment, Date, string[], moment[], Date[]. 39 | * 40 | * Return array of moments. Returned value contains only valid moments. 41 | * Return undefined if none of the input values are valid. 42 | */ 43 | export function parseArrayOrValue(data: ParseArrayOrValueData, dateFormat: string, localization: string) { 44 | if (isArray(data)) { 45 | const parsed = compact((data as ParseValueData[]).map((item) => parseValue(item, dateFormat, localization))); 46 | if (parsed.length > 0) { 47 | return parsed; 48 | } 49 | } 50 | const parsedValue = parseValue((data as ParseValueData), dateFormat, localization); 51 | 52 | return parsedValue && [parsedValue]; 53 | } 54 | 55 | interface DateParams { 56 | year?: number; 57 | month?: number; 58 | date?: number; 59 | hour?: number; 60 | minute?: number; 61 | } 62 | 63 | interface GetInitializerParams { 64 | initialDate?: ParseValueData; 65 | dateFormat?: string; 66 | dateParams?: DateParams; 67 | localization?: string; 68 | } 69 | 70 | /** Create moment. 71 | * 72 | * Creates moment using `dateParams` or `initialDate` arguments (if provided). 73 | * Precedense order: dateParams -> initialDate -> default value 74 | */ 75 | export function getInitializer(context: GetInitializerParams): moment.Moment { 76 | const { 77 | dateParams, 78 | initialDate, 79 | dateFormat, 80 | localization, 81 | } = context; 82 | if (dateParams) { 83 | const parsedParams = localization ? moment(dateParams).locale(localization) : moment(dateParams); 84 | if (parsedParams.isValid()) { 85 | return parsedParams; 86 | } 87 | } 88 | const parsedInitialDate = parseValue(initialDate, dateFormat, localization); 89 | if (parsedInitialDate) { 90 | return parsedInitialDate; 91 | } 92 | 93 | return localization ? moment().locale(localization) : moment(); 94 | } 95 | 96 | type InitialDate = string | moment.Moment | Date; 97 | type DateValue = InitialDate; 98 | 99 | /** Creates moment instance from provided value or initialDate. 100 | * Creates today by default. 101 | */ 102 | export function buildValue(value: ParseValueData, 103 | initialDate: InitialDate, 104 | localization: string, 105 | dateFormat: string, 106 | defaultVal = moment()): Moment { 107 | const valueParsed = parseValue(value, dateFormat, localization); 108 | if (valueParsed) { 109 | return valueParsed; 110 | } 111 | const initialDateParsed = parseValue(initialDate, dateFormat, localization); 112 | if (initialDateParsed) { 113 | return initialDateParsed; 114 | } 115 | const _defaultVal = defaultVal ? defaultVal.clone() : defaultVal; 116 | if (_defaultVal) { 117 | _defaultVal.locale(localization); 118 | } 119 | 120 | return _defaultVal; 121 | } 122 | 123 | export function dateValueToString(value: DateValue, dateFormat: string, locale: string): string { 124 | if (isString(value)) { 125 | return value; 126 | } 127 | if (moment.isMoment(value)) { 128 | const _value = value.clone(); 129 | _value.locale(locale); 130 | 131 | return _value.format(dateFormat); 132 | } 133 | 134 | const date = moment(value, dateFormat); 135 | if (date.isValid()) { 136 | date.locale(locale); 137 | 138 | return date.format(dateFormat); 139 | } 140 | 141 | return ''; 142 | } 143 | 144 | function cleanDate(inputString: string, dateFormat: string): string { 145 | const formattedDateLength = moment().format(dateFormat).length; 146 | 147 | return inputString.trim().slice(0, formattedDateLength); 148 | } 149 | 150 | interface Range { 151 | start?: moment.Moment; 152 | end?: moment.Moment; 153 | } 154 | 155 | /** 156 | * Extract start and end dates from input string. 157 | * Return { start: Moment|undefined, end: Moment|undefined } 158 | * @param {string} inputString Row input string from user 159 | * @param {string} dateFormat Moment formatting string 160 | * @param {string} inputSeparator Separator for split inputString 161 | */ 162 | export function parseDatesRange( 163 | inputString: string = '', 164 | dateFormat: string = '', 165 | inputSeparator: string = ' - ', 166 | ): Range { 167 | const dates = inputString.split(inputSeparator) 168 | .map((date) => cleanDate(date, dateFormat)); 169 | const result: Range = {}; 170 | let start; 171 | let end; 172 | 173 | start = moment(dates[0], dateFormat); 174 | if (dates.length === 2) { 175 | end = moment(dates[1], dateFormat); 176 | } 177 | if (start && start.isValid()) { 178 | result.start = start; 179 | } 180 | if (end && end.isValid()) { 181 | result.end = end; 182 | } 183 | 184 | return result; 185 | } 186 | -------------------------------------------------------------------------------- /src/inputs/shared.ts: -------------------------------------------------------------------------------- 1 | import { Moment } from 'moment'; 2 | 3 | /** 4 | * Filter out all moments that don't have 5 | * all dates in month disabled. 6 | * @param {*} moments 7 | * @return An array of moments; each of these moments 8 | * doesn't have any selectable date in month. 9 | */ 10 | export function getDisabledMonths(moments: Moment[]): Moment[] { 11 | if (!moments) { 12 | return; 13 | } 14 | const disabledMonths = []; 15 | const checkedMonths = []; 16 | for (const m of moments) { 17 | if (checkedMonths.indexOf(m.month()) < 0) { 18 | const momentsForMonth = moments.filter((mForMonth) => mForMonth.month() === m.month()); 19 | const momentsForMonthUniq = []; 20 | for (const mForMonth of momentsForMonth) { 21 | if (momentsForMonthUniq.indexOf(mForMonth) < 0) { 22 | momentsForMonthUniq.push(mForMonth); 23 | } 24 | } 25 | if (momentsForMonthUniq.length === m.daysInMonth()) { 26 | disabledMonths.push(m); 27 | } 28 | checkedMonths.push(m); 29 | } 30 | } 31 | 32 | return disabledMonths; 33 | } 34 | 35 | /** 36 | * Filter out all moments that don't have 37 | * all months in year disabled. 38 | * @param {*} moments 39 | * @return An array of moments; each of these moments 40 | * doesn't have any selectable month in year. 41 | */ 42 | export function getDisabledYears(moments: Moment[]): Moment[] { 43 | if (!moments) { 44 | return; 45 | } 46 | const disabledYears = []; 47 | const checkedYears = []; 48 | for (const y of moments) { 49 | if (checkedYears.indexOf(y.year()) < 0) { 50 | const momentsForYear = getDisabledMonths(moments.filter((mForYear) => mForYear.year() === y.year())); 51 | const momentsForYearUniq = []; 52 | for (const mForYear of momentsForYear) { 53 | if (momentsForYearUniq.indexOf(mForYear) < 0) { 54 | momentsForYearUniq.push(mForYear); 55 | } 56 | } 57 | if (momentsForYearUniq.length === 12) { 58 | disabledYears.push(y); 59 | } 60 | checkedYears.push(y); 61 | } 62 | } 63 | 64 | return disabledYears; 65 | } 66 | -------------------------------------------------------------------------------- /src/lib/CustomPropTypes.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export function momentObj(props, propName, componentName) { 4 | if (props[propName]) { 5 | const value = props[propName]; 6 | 7 | if (moment.isMoment(value)) { 8 | if (!value.isValid()) { 9 | return new Error(`${propName} in ${componentName} is invalid 'moment' object`); 10 | } 11 | } else { 12 | return new Error(`${propName} in ${componentName} is not 'moment' object`); 13 | } 14 | } 15 | 16 | return null; 17 | } 18 | 19 | export function dateObject(props, propName, componentName) { 20 | if (props[propName]) { 21 | const value = props[propName]; 22 | if (value && value.constructor && value.constructor.name) { 23 | if (value.constructor.name !== 'Date') { 24 | return new Error(`${propName} in ${componentName} is not 'Date' object`); 25 | } 26 | } 27 | } 28 | 29 | return null; 30 | } 31 | 32 | export default { 33 | momentObj, 34 | dateObject, 35 | }; 36 | -------------------------------------------------------------------------------- /src/lib/checkIE.ts: -------------------------------------------------------------------------------- 1 | /** Return true if run on Internet Explorer. */ 2 | const checkIE = () => { 3 | if (typeof window === `undefined`) { 4 | return false; 5 | } 6 | const navigator: Navigator = window.navigator; 7 | if (!navigator) { 8 | return false; 9 | } 10 | if (navigator.appName === 'Microsoft Internet Explorer' 11 | || !!(navigator.userAgent.match(/Trident/) 12 | || navigator.userAgent.match(/rv:11/)) 13 | ) { 14 | return true; 15 | } 16 | 17 | return false; 18 | }; 19 | 20 | export default checkIE; 21 | -------------------------------------------------------------------------------- /src/lib/checkMobile.ts: -------------------------------------------------------------------------------- 1 | /** Return true if run on mobile browser. */ 2 | const checkMobile = () => { 3 | if (typeof window === `undefined`) { 4 | return false; 5 | } 6 | const navigator: Navigator = window.navigator; 7 | if (!navigator) { 8 | return false; 9 | } 10 | if (navigator.userAgent.match(/Android/i) 11 | || navigator.userAgent.match(/webOS/i) 12 | || navigator.userAgent.match(/iPhone/i) 13 | || navigator.userAgent.match(/iPad/i) 14 | || navigator.userAgent.match(/iPod/i) 15 | || navigator.userAgent.match(/BlackBerry/i) 16 | || navigator.userAgent.match(/Windows Phone/i) 17 | ) { 18 | return true; 19 | } 20 | 21 | return false; 22 | }; 23 | 24 | export default checkMobile; 25 | -------------------------------------------------------------------------------- /src/lib/findHTMLElement.ts: -------------------------------------------------------------------------------- 1 | import { ReactInstance } from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | export default function findHTMLElement(e: ReactInstance): HTMLElement | undefined { 5 | const el = ReactDOM.findDOMNode(e); 6 | if (el && (el as HTMLElement).focus) { 7 | return el as HTMLElement; 8 | } 9 | 10 | return undefined; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { default as tick } from './tick'; 2 | export { default as findHTMLElement } from './findHTMLElement'; 3 | export { default as checkMobile } from './checkMobile'; 4 | -------------------------------------------------------------------------------- /src/lib/tick.ts: -------------------------------------------------------------------------------- 1 | /** Set zero timeout. 2 | * 3 | * Sometimes we need to delay rerendering components 4 | * on one tick (if they are inside `Popup` and rerendering could 5 | * change `Popup`'s content sizes). 6 | * Because it races with Popup's onclick handler. 7 | * `Popup` relies on it's content sizes when computing 8 | * should popup stay open or be closed. So we need 9 | * to wait until `Popup`'s onclick handler done its job. 10 | */ 11 | const tick = (leadToRerendering, ...args) => { 12 | setTimeout(leadToRerendering, 0, ...args); 13 | }; 14 | 15 | export default tick; 16 | -------------------------------------------------------------------------------- /src/pickers/YearPicker.tsx: -------------------------------------------------------------------------------- 1 | import range from 'lodash/range'; 2 | import includes from 'lodash/includes'; 3 | import isNil from 'lodash/isNil'; 4 | import isArray from 'lodash/isArray'; 5 | import concat from 'lodash/concat'; 6 | import uniq from 'lodash/uniq'; 7 | import filter from 'lodash/filter'; 8 | import last from 'lodash/last'; 9 | import first from 'lodash/first'; 10 | import some from 'lodash/some'; 11 | 12 | import * as React from 'react'; 13 | 14 | import YearView from '../views/YearView'; 15 | import { 16 | BasePickerOnChangeData, 17 | BasePickerProps, 18 | DisableValuesProps, 19 | EnableValuesProps, 20 | MinMaxValueProps, 21 | SingleSelectionPicker, 22 | } from './BasePicker'; 23 | 24 | const PAGE_WIDTH = 3; 25 | const PAGE_HEIGHT = 4; 26 | const YEARS_ON_PAGE = PAGE_WIDTH * PAGE_HEIGHT; 27 | 28 | type YearPickerProps = BasePickerProps 29 | & DisableValuesProps 30 | & EnableValuesProps 31 | & MinMaxValueProps; 32 | 33 | export interface YearPickerOnChangeData extends BasePickerOnChangeData { 34 | value: { 35 | year: number, 36 | }; 37 | } 38 | 39 | class YearPicker extends SingleSelectionPicker { 40 | /* 41 | Note: 42 | use it like this 43 | to make react create new instance when input value changes 44 | */ 45 | constructor(props) { 46 | super(props); 47 | this.PAGE_WIDTH = PAGE_WIDTH; 48 | } 49 | 50 | public render() { 51 | const { 52 | onChange, 53 | value, 54 | initializeWith, 55 | closePopup, 56 | inline, 57 | isPickerInFocus, 58 | isTriggerInFocus, 59 | onCalendarViewMount, 60 | disable, 61 | enable, 62 | minDate, 63 | maxDate, 64 | localization, 65 | ...rest 66 | } = this.props; 67 | 68 | return ( 69 | 85 | ); 86 | } 87 | 88 | protected buildCalendarValues(): string[] { 89 | /* 90 | Return array of years (strings) like ['2012', '2013', ...] 91 | that used to populate calendar's page. 92 | */ 93 | const years = []; 94 | const date = this.state.date; 95 | const padd = date.year() % YEARS_ON_PAGE; 96 | const firstYear = date.year() - padd; 97 | for (let i = 0; i < YEARS_ON_PAGE; i++) { 98 | years[i] = (firstYear + i).toString(); 99 | } 100 | 101 | return years; 102 | } 103 | 104 | protected getInitialDatePosition(): number { 105 | const selectable = this.getSelectableCellPositions(); 106 | const values = this.buildCalendarValues(); 107 | const currentYearIndex = values.indexOf(this.state.date.year().toString()); 108 | if (selectable.indexOf(currentYearIndex) < 0) { 109 | return selectable[0]; 110 | } 111 | 112 | return currentYearIndex; 113 | } 114 | 115 | protected getActiveCellPosition(): number { 116 | /* 117 | Return position of a year that should be displayed as active 118 | (position in array returned by `this.buildCalendarValues`). 119 | */ 120 | if (!isNil(this.props.value)) { 121 | const years = this.buildCalendarValues(); 122 | const yearIndex = years.indexOf(this.props.value.year().toString()); 123 | if (yearIndex >= 0) { 124 | return yearIndex; 125 | } 126 | } 127 | } 128 | 129 | protected getSelectableCellPositions(): number[] { 130 | return filter( 131 | range(0, YEARS_ON_PAGE), 132 | (y) => !includes(this.getDisabledPositions(), y), 133 | ); 134 | } 135 | 136 | protected getDisabledPositions(): number[] { 137 | /* 138 | Return position numbers of years that should be displayed as disabled 139 | (position in array returned by `this.buildCalendarValues`). 140 | */ 141 | let disabled = []; 142 | const years = this.buildCalendarValues(); 143 | if (isArray(this.props.enable)) { 144 | const enabledYears = this.props.enable.map((yearMoment) => yearMoment.year().toString()); 145 | disabled = concat(disabled, 146 | years 147 | .filter((year) => !includes(enabledYears, year)) 148 | .map((year) => years.indexOf(year))); 149 | } 150 | if (isArray(this.props.disable)) { 151 | disabled = concat(disabled, 152 | this.props.disable 153 | .filter((yearMoment) => includes(years, yearMoment.year().toString())) 154 | .map((yearMoment) => years.indexOf(yearMoment.year().toString()))); 155 | } 156 | if (!isNil(this.props.maxDate)) { 157 | if (parseInt(first(years), 10) > this.props.maxDate.year()) { 158 | disabled = range(0, years.length); 159 | } else if (includes(years, this.props.maxDate.year().toString())) { 160 | disabled = concat( 161 | disabled, 162 | range(years.indexOf(this.props.maxDate.year().toString()) + 1, years.length)); 163 | } 164 | } 165 | if (!isNil(this.props.minDate)) { 166 | if (parseInt(last(years), 10) < this.props.minDate.year()) { 167 | disabled = range(0, years.length); 168 | } else if (includes(years, this.props.minDate.year().toString())) { 169 | disabled = concat( 170 | disabled, 171 | range(0, years.indexOf(this.props.minDate.year().toString()))); 172 | } 173 | } 174 | if (disabled.length > 0) { 175 | return uniq(disabled); 176 | } 177 | } 178 | 179 | protected isNextPageAvailable(): boolean { 180 | const { 181 | maxDate, 182 | enable, 183 | } = this.props; 184 | const lastOnPage = parseInt(last(this.buildCalendarValues()), 10); 185 | 186 | if (isArray(enable)) { 187 | return some(enable, (enabledYear) => enabledYear.year() > lastOnPage); 188 | } 189 | if (isNil(maxDate)) { 190 | return true; 191 | } 192 | 193 | return lastOnPage < maxDate.year(); 194 | } 195 | 196 | protected isPrevPageAvailable(): boolean { 197 | const { 198 | minDate, 199 | enable, 200 | } = this.props; 201 | const firstOnPage = parseInt(first(this.buildCalendarValues()), 10); 202 | 203 | if (isArray(enable)) { 204 | return some(enable, (enabledYear) => enabledYear.year() < firstOnPage); 205 | } 206 | if (isNil(minDate)) { 207 | return true; 208 | } 209 | 210 | return firstOnPage > minDate.year(); 211 | } 212 | 213 | protected handleChange = (e: React.SyntheticEvent, { value }): void => { 214 | const data: YearPickerOnChangeData = { 215 | ...this.props, 216 | value: { year: parseInt(value, 10) }, 217 | }; 218 | this.props.onChange(e, data); 219 | } 220 | 221 | protected switchToNextPage = (e: React.SyntheticEvent, 222 | data: any, 223 | callback: () => void): void => { 224 | this.setState(({ date }) => { 225 | const nextDate = date.clone(); 226 | nextDate.add(YEARS_ON_PAGE, 'year'); 227 | 228 | return { date: nextDate }; 229 | }, callback); 230 | } 231 | 232 | protected switchToPrevPage = (e: React.SyntheticEvent, 233 | data: any, 234 | callback: () => void): void => { 235 | this.setState(({ date }) => { 236 | const prevDate = date.clone(); 237 | prevDate.subtract(YEARS_ON_PAGE, 'year'); 238 | 239 | return { date: prevDate }; 240 | }, callback); 241 | } 242 | } 243 | 244 | export default YearPicker; 245 | -------------------------------------------------------------------------------- /src/pickers/dayPicker/DayPicker.tsx: -------------------------------------------------------------------------------- 1 | import filter from 'lodash/filter'; 2 | import range from 'lodash/range'; 3 | import includes from 'lodash/includes'; 4 | import isArray from 'lodash/isArray'; 5 | import some from 'lodash/some'; 6 | 7 | import * as React from 'react'; 8 | 9 | import DayView from '../../views/DayView'; 10 | import { WEEKS_TO_DISPLAY } from '../../views/DayView'; 11 | import { 12 | BasePickerOnChangeData, 13 | BasePickerProps, 14 | DisableValuesProps, 15 | EnableValuesProps, 16 | MinMaxValueProps, 17 | MarkedValuesProps, 18 | ProvideHeadingValue, 19 | SingleSelectionPicker, 20 | } from '../BasePicker'; 21 | import { 22 | buildDays, 23 | getDisabledDays, 24 | getMarkedDays, 25 | getInitialDatePosition, 26 | isNextPageAvailable, 27 | isPrevPageAvailable, 28 | } from './sharedFunctions'; 29 | 30 | const PAGE_WIDTH = 7; 31 | export const DAYS_ON_PAGE = WEEKS_TO_DISPLAY * PAGE_WIDTH; 32 | 33 | export interface DayPickerOnChangeData extends BasePickerOnChangeData { 34 | value: { 35 | year: number; 36 | month: number; 37 | date: number; 38 | }; 39 | } 40 | 41 | type DayPickerProps = BasePickerProps 42 | & DisableValuesProps 43 | & EnableValuesProps 44 | & MinMaxValueProps 45 | & MarkedValuesProps; 46 | 47 | class DayPicker 48 | extends SingleSelectionPicker 49 | implements ProvideHeadingValue { 50 | constructor(props) { 51 | super(props); 52 | this.PAGE_WIDTH = PAGE_WIDTH; 53 | } 54 | 55 | public render() { 56 | const { 57 | onChange, 58 | value, 59 | initializeWith, 60 | closePopup, 61 | inline, 62 | isPickerInFocus, 63 | isTriggerInFocus, 64 | onCalendarViewMount, 65 | disable, 66 | enable, 67 | minDate, 68 | maxDate, 69 | marked, 70 | markColor, 71 | localization, 72 | ...rest 73 | } = this.props; 74 | 75 | return ( 76 | 95 | ); 96 | } 97 | 98 | public getCurrentDate(): string { 99 | /* Return currently selected year and month(string) to display in calendar header. */ 100 | return this.state.date.format('MMMM YYYY'); 101 | } 102 | 103 | protected buildCalendarValues(): string[] { 104 | /* 105 | Return array of dates (strings) like ['31', '1', ...] 106 | that used to populate calendar's page. 107 | */ 108 | return buildDays(this.state.date, DAYS_ON_PAGE); 109 | } 110 | 111 | protected getSelectableCellPositions(): number[] { 112 | return filter( 113 | range(0, DAYS_ON_PAGE), 114 | (d) => !includes(this.getDisabledPositions(), d), 115 | ); 116 | } 117 | 118 | protected getInitialDatePosition(): number { 119 | return getInitialDatePosition(this.state.date.date().toString(), 120 | this.buildCalendarValues(), 121 | this.getSelectableCellPositions()); 122 | } 123 | 124 | protected getActiveCellPosition(): number { 125 | /* 126 | Return position of a date that should be displayed as active 127 | (position in array returned by `this.buildCalendarValues`). 128 | */ 129 | if (this.props.value && this.props.value.isSame(this.state.date, 'month')) { 130 | const disabledPositions = this.getDisabledPositions(); 131 | const active = this.buildCalendarValues() 132 | .map((day, i) => includes(disabledPositions, i) ? undefined : day) 133 | .indexOf(this.props.value.date().toString()); 134 | if (active >= 0) { 135 | return active; 136 | } 137 | } 138 | } 139 | 140 | protected getDisabledPositions(): number[] { 141 | /* 142 | Return position numbers of dates that should be displayed as disabled 143 | (position in array returned by `this.buildCalendarValues`). 144 | */ 145 | const { 146 | disable, 147 | maxDate, 148 | minDate, 149 | enable, 150 | } = this.props; 151 | 152 | return getDisabledDays(disable, maxDate, minDate, this.state.date, DAYS_ON_PAGE, enable); 153 | } 154 | 155 | protected getMarkedPositions(): number[] { 156 | /* 157 | Return position numbers of dates that should be displayed as marked 158 | (position in array returned by `this.buildCalendarValues`). 159 | */ 160 | const { 161 | marked, 162 | } = this.props; 163 | 164 | if (marked) { 165 | return getMarkedDays(marked, this.state.date, DAYS_ON_PAGE); 166 | } else { 167 | return []; 168 | } 169 | } 170 | 171 | protected isNextPageAvailable = (): boolean => { 172 | const { 173 | maxDate, 174 | enable, 175 | } = this.props; 176 | if (isArray(enable)) { 177 | return some(enable, (enabledDate) => enabledDate.isAfter(this.state.date, 'month')); 178 | } 179 | 180 | return isNextPageAvailable(this.state.date, maxDate); 181 | } 182 | 183 | protected isPrevPageAvailable = (): boolean => { 184 | const { 185 | minDate, 186 | enable, 187 | } = this.props; 188 | if (isArray(enable)) { 189 | return some(enable, (enabledDate) => enabledDate.isBefore(this.state.date, 'month')); 190 | } 191 | 192 | return isPrevPageAvailable(this.state.date, minDate); 193 | } 194 | 195 | protected handleChange = (e: React.SyntheticEvent, { value }): void => { 196 | // `value` is selected date(string) like '31' or '1' 197 | const data: DayPickerOnChangeData = { 198 | ...this.props, 199 | value: { 200 | year: this.state.date.year(), 201 | month: this.state.date.month(), 202 | date: parseInt(value, 10), 203 | }, 204 | }; 205 | 206 | this.props.onChange(e, data); 207 | } 208 | 209 | protected switchToNextPage = (e: React.SyntheticEvent, 210 | data: any, 211 | callback: () => void): void => { 212 | this.setState(({ date }) => { 213 | const nextDate = date.clone(); 214 | nextDate.add(1, 'month'); 215 | 216 | return { date: nextDate }; 217 | }, callback); 218 | } 219 | 220 | protected switchToPrevPage = (e: React.SyntheticEvent, 221 | data: any, 222 | callback: () => void): void => { 223 | this.setState(({ date }) => { 224 | const prevDate = date.clone(); 225 | prevDate.subtract(1, 'month'); 226 | 227 | return { date: prevDate }; 228 | }, callback); 229 | } 230 | } 231 | 232 | export default DayPicker; 233 | -------------------------------------------------------------------------------- /src/pickers/dayPicker/sharedFunctions.ts: -------------------------------------------------------------------------------- 1 | import indexOf from 'lodash/indexOf'; 2 | import lastIndexOf from 'lodash/lastIndexOf'; 3 | import range from 'lodash/range'; 4 | import includes from 'lodash/includes'; 5 | import isNil from 'lodash/isNil'; 6 | import isArray from 'lodash/isArray'; 7 | import concat from 'lodash/concat'; 8 | import uniq from 'lodash/uniq'; 9 | import first from 'lodash/first'; 10 | import sortBy from 'lodash/sortBy'; 11 | import slice from 'lodash/slice'; 12 | import find from 'lodash/find'; 13 | 14 | import { Moment } from 'moment'; 15 | 16 | /** Build days to fill page. */ 17 | export function buildDays(date: Moment, daysOnPage: number) { 18 | const start = date.clone().startOf('month').startOf('week'); 19 | 20 | return getDaysArray( 21 | start.date(), 22 | getBrakepoints(date), 23 | daysOnPage).map((d) => d.toString()); 24 | } 25 | 26 | /** Return dates from ends of months. 27 | * 28 | * On one datepicker's page not only days from current month are displayed 29 | * but also some days from adjacent months. This function returns days 30 | * that separate one month from other (last day in month). 31 | * Return array of one or two numbers. 32 | */ 33 | function getBrakepoints(referenceDate: Moment): number[] { 34 | const dateInCurrentMonth = referenceDate.clone(); 35 | const currentMonth = dateInCurrentMonth.month(); 36 | const brakepoints = []; 37 | 38 | const firstDateOnPage = dateInCurrentMonth.clone().startOf('month').startOf('week'); 39 | if (firstDateOnPage.month() !== currentMonth) { 40 | brakepoints.push(firstDateOnPage.clone().endOf('month').date()); 41 | } 42 | brakepoints.push(dateInCurrentMonth.clone().endOf('month').date()); 43 | 44 | return brakepoints; 45 | } 46 | 47 | /* Return array of day positions that are not disabled by default. */ 48 | export function getDefaultEnabledDayPositions(allDays: string[], date: Moment): number[] { 49 | const dateClone = date.clone(); 50 | const brakepoints = getBrakepoints(dateClone); 51 | if (brakepoints.length === 1) { 52 | return range(0, indexOf(allDays, brakepoints[0].toString()) + 1); 53 | } else { 54 | return range(indexOf(allDays, brakepoints[0].toString()) + 1, 55 | lastIndexOf(allDays, brakepoints[1].toString()) + 1); 56 | } 57 | } 58 | 59 | /** Return day positions that shoud be displayed as disabled. */ 60 | export function getDisabledDays( 61 | disable: Moment[], 62 | maxDate: Moment, 63 | minDate: Moment, 64 | currentDate: Moment, 65 | daysOnPage: number, 66 | enable: Moment[]): number[] { 67 | const dayPositions = range(daysOnPage); 68 | const daysInCurrentMonthPositions = getDefaultEnabledDayPositions(buildDays(currentDate, daysOnPage), currentDate); 69 | let disabledDays = dayPositions.filter((dayPosition) => !includes(daysInCurrentMonthPositions, dayPosition)); 70 | if (isArray(enable)) { 71 | const enabledDaysPositions = enable 72 | .filter((date) => date.isSame(currentDate, 'month')) 73 | .map((date) => date.date()) 74 | .map((date) => daysInCurrentMonthPositions[date - 1]); 75 | disabledDays = concat(disabledDays, 76 | dayPositions.filter((position) => { 77 | return !includes(enabledDaysPositions, position); 78 | })); 79 | } 80 | if (isArray(disable)) { 81 | disabledDays = concat(disabledDays, 82 | disable 83 | .filter((date) => date.isSame(currentDate, 'month')) 84 | .map((date) => date.date()) 85 | .map((date) => daysInCurrentMonthPositions[date - 1])); 86 | } 87 | if (!isNil(maxDate)) { 88 | if (maxDate.isBefore(currentDate, 'month')) { 89 | disabledDays = dayPositions; 90 | } 91 | if (maxDate.isSame(currentDate, 'month')) { 92 | disabledDays = concat(disabledDays, 93 | range(1, daysInCurrentMonthPositions.length + 1) 94 | .filter((date) => date > maxDate.date()) 95 | .map((date) => daysInCurrentMonthPositions[date - 1])); 96 | } 97 | } 98 | if (!isNil(minDate)) { 99 | if (minDate.isAfter(currentDate, 'month')) { 100 | disabledDays = dayPositions; 101 | } 102 | if (minDate.isSame(currentDate, 'month')) { 103 | disabledDays = concat(disabledDays, 104 | range(1, daysInCurrentMonthPositions.length + 1) 105 | .filter((date) => date < minDate.date()) 106 | .map((date) => daysInCurrentMonthPositions[date - 1])); 107 | } 108 | } 109 | 110 | return sortBy(uniq(disabledDays).filter((day) => !isNil(day))); 111 | } 112 | 113 | /** Return day positions that should be displayed as marked. */ 114 | export function getMarkedDays( 115 | marked: Moment[], 116 | currentDate: Moment, 117 | daysOnPage: number): number[] { 118 | if (marked.length === 0) { 119 | return []; 120 | } 121 | const allDates = buildDays(currentDate, daysOnPage); 122 | const activeDayPositions = getDefaultEnabledDayPositions(allDates, currentDate); 123 | const allDatesNumb = allDates.map((date) => parseInt(date, 10)); 124 | 125 | /* 126 | * The following will clear all dates before the 1st of the current month. 127 | * This is to prevent marking days before the 1st, that shouldn't be marked. 128 | * If the incorrect dates are marked, instead of the legitimate ones, the legitimate dates 129 | * will not be marked at all. 130 | */ 131 | const fillTo = allDatesNumb.indexOf(1); 132 | for (let i = 0; i < fillTo; i++) { 133 | allDatesNumb[i] = 0; 134 | } 135 | 136 | const markedIndexes = marked 137 | .filter((date) => date.isSame(currentDate, 'month')) 138 | .map((date) => date.date()) 139 | .map((date) => allDatesNumb.indexOf(date)); 140 | 141 | return markedIndexes.filter((index) => includes(activeDayPositions, index)); 142 | } 143 | 144 | export function isNextPageAvailable(date: Moment, maxDate: Moment): boolean { 145 | if (isNil(maxDate)) { 146 | return true; 147 | } 148 | if (date.isSameOrAfter(maxDate, 'month')) { 149 | return false; 150 | } 151 | 152 | return true; 153 | } 154 | 155 | export function isPrevPageAvailable(date: Moment, minDate: Moment): boolean { 156 | if (isNil(minDate)) { 157 | return true; 158 | } 159 | if (date.isSameOrBefore(minDate, 'month')) { 160 | return false; 161 | } 162 | 163 | return true; 164 | } 165 | 166 | // helper 167 | function getDaysArray(start: number, brakepoints: number[], length: number): number[] { 168 | let currentDay = start; 169 | const days = []; 170 | let brakepointsLeft = brakepoints.slice(); 171 | 172 | while (! (days.length === length)) { 173 | days.push(currentDay); 174 | const bp = first(brakepointsLeft); 175 | if (currentDay === bp) { 176 | currentDay = 1; 177 | brakepointsLeft = slice(brakepointsLeft, 1); 178 | } else { 179 | currentDay = currentDay + 1; 180 | } 181 | } 182 | 183 | return days; 184 | } 185 | 186 | export const testExport = { 187 | buildDays, 188 | getBrakepoints, 189 | getDisabledDays, 190 | isNextPageAvailable, 191 | isPrevPageAvailable, 192 | getDaysArray, 193 | getDefaultEnabledDayPositions, 194 | }; 195 | 196 | export function getInitialDatePosition(initDate: string, 197 | values: string[], 198 | selectablePositions: number[]): number { 199 | const selectable = selectablePositions.reduce((acc, pos) => { 200 | acc.push({ value: values[pos], position: pos }); 201 | 202 | return acc; 203 | }, []); 204 | const res = find(selectable, (item) => item.value === initDate); 205 | if (res) { 206 | return res.position; 207 | } 208 | 209 | return selectable[0].position; 210 | } 211 | -------------------------------------------------------------------------------- /src/pickers/monthPicker/MonthPicker.tsx: -------------------------------------------------------------------------------- 1 | import filter from 'lodash/filter'; 2 | import range from 'lodash/range'; 3 | import includes from 'lodash/includes'; 4 | import isNil from 'lodash/isNil'; 5 | 6 | import * as React from 'react'; 7 | 8 | import MonthView from '../../views/MonthView'; 9 | import { 10 | BasePickerOnChangeData, 11 | BasePickerProps, 12 | DisableValuesProps, 13 | EnableValuesProps, 14 | MinMaxValueProps, 15 | OptionalHeaderProps, 16 | ProvideHeadingValue, 17 | SingleSelectionPicker, 18 | } from '../BasePicker'; 19 | import { 20 | MONTH_PAGE_WIDTH, 21 | MONTHS_IN_YEAR, 22 | } from './const'; 23 | import { 24 | buildCalendarValues, 25 | getDisabledPositions, 26 | getInitialDatePosition, 27 | isNextPageAvailable, 28 | isPrevPageAvailable, 29 | } from './sharedFunctions'; 30 | 31 | type MonthPickerProps = BasePickerProps 32 | & DisableValuesProps 33 | & EnableValuesProps 34 | & MinMaxValueProps 35 | & OptionalHeaderProps; 36 | 37 | export interface MonthPickerOnChangeData extends BasePickerOnChangeData { 38 | value: { 39 | year: number, 40 | month: number, 41 | }; 42 | } 43 | 44 | class MonthPicker 45 | extends SingleSelectionPicker 46 | implements ProvideHeadingValue { 47 | /* 48 | Note: 49 | use it like this 50 | to make react create new instance when input value changes 51 | */ 52 | constructor(props) { 53 | super(props); 54 | this.PAGE_WIDTH = MONTH_PAGE_WIDTH; 55 | } 56 | 57 | public render() { 58 | const { 59 | onChange, 60 | value, 61 | initializeWith, 62 | closePopup, 63 | inline, 64 | isPickerInFocus, 65 | isTriggerInFocus, 66 | onCalendarViewMount, 67 | disable, 68 | enable, 69 | minDate, 70 | maxDate, 71 | localization, 72 | ...rest 73 | } = this.props; 74 | 75 | return ( 76 | 93 | ); 94 | } 95 | 96 | public getCurrentDate(): string { 97 | /* Return current year(string) to display in calendar header. */ 98 | return this.state.date.year().toString(); 99 | } 100 | 101 | protected buildCalendarValues(): string[] { 102 | const { localization } = this.props; 103 | 104 | return buildCalendarValues(localization); 105 | } 106 | 107 | protected getSelectableCellPositions(): number[] { 108 | return filter( 109 | range(0, MONTHS_IN_YEAR), 110 | (m) => !includes(this.getDisabledPositions(), m), 111 | ); 112 | } 113 | 114 | protected getInitialDatePosition(): number { 115 | const selectable = this.getSelectableCellPositions(); 116 | 117 | return getInitialDatePosition(selectable, this.state.date); 118 | } 119 | 120 | protected getActiveCellPosition(): number { 121 | /* 122 | Return position of a month that should be displayed as active 123 | (position in array returned by `this.buildCalendarValues`). 124 | */ 125 | if (!isNil(this.props.value)) { 126 | if (this.props.value.year() === this.state.date.year()) { 127 | return this.props.value.month(); 128 | } 129 | } 130 | } 131 | 132 | protected getDisabledPositions(): number[] { 133 | const { 134 | maxDate, 135 | minDate, 136 | enable, 137 | disable, 138 | } = this.props; 139 | 140 | return getDisabledPositions(enable, disable, maxDate, minDate, this.state.date); 141 | } 142 | 143 | protected isNextPageAvailable(): boolean { 144 | const { 145 | maxDate, 146 | enable, 147 | } = this.props; 148 | 149 | return isNextPageAvailable(maxDate, enable, this.state.date); 150 | } 151 | 152 | protected isPrevPageAvailable(): boolean { 153 | const { 154 | minDate, 155 | enable, 156 | } = this.props; 157 | 158 | return isPrevPageAvailable(minDate, enable, this.state.date); 159 | } 160 | 161 | protected handleChange = (e: React.SyntheticEvent, { value }): void => { 162 | const data: MonthPickerOnChangeData = { 163 | ...this.props, 164 | value: { 165 | year: parseInt(this.getCurrentDate(), 10), 166 | month: this.buildCalendarValues().indexOf(value), 167 | }, 168 | }; 169 | this.props.onChange(e, data); 170 | } 171 | 172 | protected switchToNextPage = (e: React.SyntheticEvent, 173 | data: any, 174 | callback: () => void): void => { 175 | this.setState(({ date }) => { 176 | const nextDate = date.clone(); 177 | nextDate.add(1, 'year'); 178 | 179 | return { date: nextDate }; 180 | }, callback); 181 | } 182 | 183 | protected switchToPrevPage = (e: React.SyntheticEvent, 184 | data: any, 185 | callback: () => void): void => { 186 | this.setState(({ date }) => { 187 | const prevDate = date.clone(); 188 | prevDate.subtract(1, 'year'); 189 | 190 | return { date: prevDate }; 191 | }, callback); 192 | } 193 | } 194 | 195 | export default MonthPicker; 196 | -------------------------------------------------------------------------------- /src/pickers/monthPicker/MonthRangePicker.tsx: -------------------------------------------------------------------------------- 1 | import filter from 'lodash/filter'; 2 | import range from 'lodash/range'; 3 | import includes from 'lodash/includes'; 4 | import isNil from 'lodash/isNil'; 5 | 6 | import {Moment} from 'moment'; 7 | import moment from 'moment'; 8 | import * as React from 'react'; 9 | 10 | import {RangeIndexes} from '../../views/BaseCalendarView'; 11 | import MonthRangeView from '../../views/MonthRangeView'; 12 | import { 13 | BasePickerOnChangeData, 14 | BasePickerProps, 15 | MinMaxValueProps, 16 | ProvideHeadingValue, 17 | RangeSelectionPicker, 18 | } from '../BasePicker'; 19 | import { 20 | MONTH_PAGE_WIDTH, 21 | MONTHS_IN_YEAR, 22 | } from './const'; 23 | import { 24 | buildCalendarValues, 25 | getDisabledPositions, 26 | getInitialDatePosition, 27 | isNextPageAvailable, 28 | isPrevPageAvailable, 29 | } from './sharedFunctions'; 30 | 31 | interface MonthRangePickerProps extends BasePickerProps, MinMaxValueProps { 32 | /** Moment date formatting string. */ 33 | dateFormat: string; 34 | /** Start of currently selected dates range. */ 35 | start: Moment; 36 | /** End of currently selected dates range. */ 37 | end: Moment; 38 | } 39 | 40 | export type MonthRangePickerOnChangeData = BasePickerOnChangeData; 41 | 42 | class MonthRangePicker 43 | extends RangeSelectionPicker 44 | implements ProvideHeadingValue { 45 | constructor(props) { 46 | super(props); 47 | this.PAGE_WIDTH = MONTH_PAGE_WIDTH; 48 | } 49 | 50 | public render() { 51 | const { 52 | onChange, 53 | initializeWith, 54 | closePopup, 55 | inline, 56 | isPickerInFocus, 57 | isTriggerInFocus, 58 | onCalendarViewMount, 59 | dateFormat, 60 | start, 61 | end, 62 | minDate, 63 | maxDate, 64 | localization, 65 | ...rest 66 | } = this.props; 67 | 68 | return ( 69 | 87 | ); 88 | } 89 | 90 | public getCurrentDate(): string { 91 | /* Return currently selected year and month(string) to display in calendar header. */ 92 | return this.state.date.format('YYYY'); 93 | } 94 | 95 | protected buildCalendarValues(): string[] { 96 | const { localization } = this.props; 97 | 98 | return buildCalendarValues(localization); 99 | } 100 | 101 | protected getSelectableCellPositions(): number[] { 102 | return filter( 103 | range(0, MONTHS_IN_YEAR), 104 | (d) => !includes(this.getDisabledPositions(), d), 105 | ); 106 | } 107 | 108 | protected getActiveCellsPositions(): RangeIndexes { 109 | /* 110 | Return starting and ending positions of month range that should be displayed as active 111 | { start: number, end: number } 112 | */ 113 | const { 114 | start, 115 | end, 116 | } = this.props; 117 | const currentYear = this.state.date.year(); 118 | const result = { 119 | start: undefined, 120 | end: undefined, 121 | }; 122 | 123 | if (start && end) { 124 | if (currentYear < start.year() || currentYear > end.year()) { 125 | return result; 126 | } 127 | 128 | result.start = currentYear === start.year() ? start.month() : 0; 129 | result.end = currentYear === end.year() ? end.month() : MONTHS_IN_YEAR - 1; 130 | } 131 | if (start && !end) { 132 | result.start = currentYear === start.year() ? start.month() : undefined; 133 | } 134 | 135 | return result; 136 | } 137 | 138 | protected getDisabledPositions(): number[] { 139 | /* 140 | Return position numbers of dates that should be displayed as disabled 141 | (position in array returned by `this.buildCalendarValues`). 142 | */ 143 | const { 144 | maxDate, 145 | minDate, 146 | } = this.props; 147 | 148 | return getDisabledPositions(undefined, undefined, maxDate, minDate, this.state.date); 149 | } 150 | 151 | protected isNextPageAvailable(): boolean { 152 | const {maxDate} = this.props; 153 | 154 | return isNextPageAvailable(maxDate, undefined, this.state.date); 155 | } 156 | 157 | protected isPrevPageAvailable(): boolean { 158 | const {minDate} = this.props; 159 | 160 | return isPrevPageAvailable(minDate, undefined, this.state.date); 161 | } 162 | 163 | protected getSelectedRange(): string { 164 | /* Return currently selected dates range(string) to display in calendar header. */ 165 | const { 166 | start, 167 | end, 168 | dateFormat, 169 | } = this.props; 170 | 171 | return `${start ? start.format(dateFormat) : '- - -'} - ${end ? end.format(dateFormat) : '- - -'}`; 172 | } 173 | 174 | protected handleChange = (e: React.SyntheticEvent, {itemPosition}) => { 175 | // call `onChange` with value: { start: moment, end: moment } 176 | const { 177 | start, 178 | end, 179 | localization, 180 | } = this.props; 181 | const data: MonthRangePickerOnChangeData = { 182 | ...this.props, 183 | value: {}, 184 | }; 185 | 186 | if (isNil(start) && isNil(end)) { 187 | data.value = 188 | localization 189 | ? {start: moment({year: this.state.date.year(), month: itemPosition, date: 1}).locale(localization)} 190 | : {start: moment({year: this.state.date.year(), month: itemPosition, date: 1})}; 191 | } else if (!isNil(start) && isNil(end)) { 192 | data.value = 193 | localization 194 | ? { 195 | start, 196 | end: moment({year: this.state.date.year(), month: itemPosition, date: 1}).locale(localization).endOf('month'), 197 | } 198 | : { 199 | start, 200 | end: moment({year: this.state.date.year(), month: itemPosition, date: 1}).endOf('month'), 201 | }; 202 | } 203 | 204 | this.props.onChange(e, data); 205 | } 206 | 207 | protected switchToNextPage = (e: React.SyntheticEvent, 208 | data: any, 209 | callback: () => void): void => { 210 | this.setState(({date}) => { 211 | const nextDate = date.clone(); 212 | nextDate.add(1, 'year'); 213 | 214 | return {date: nextDate}; 215 | }, callback); 216 | } 217 | 218 | protected switchToPrevPage = (e: React.SyntheticEvent, 219 | data: any, 220 | callback: () => void): void => { 221 | this.setState(({date}) => { 222 | const prevDate = date.clone(); 223 | prevDate.subtract(1, 'year'); 224 | 225 | return {date: prevDate}; 226 | }, callback); 227 | } 228 | 229 | protected getInitialDatePosition = (): number => { 230 | const selectable = this.getSelectableCellPositions(); 231 | 232 | return getInitialDatePosition(selectable, this.state.date); 233 | } 234 | } 235 | 236 | export default MonthRangePicker; 237 | -------------------------------------------------------------------------------- /src/pickers/monthPicker/const.ts: -------------------------------------------------------------------------------- 1 | 2 | export const MONTHS_IN_YEAR = 12; 3 | /** How much months to place in row on calendar page. */ 4 | export const MONTH_PAGE_WIDTH = 3; 5 | -------------------------------------------------------------------------------- /src/pickers/monthPicker/sharedFunctions.ts: -------------------------------------------------------------------------------- 1 | import range from 'lodash/range'; 2 | import includes from 'lodash/includes'; 3 | import isNil from 'lodash/isNil'; 4 | import isArray from 'lodash/isArray'; 5 | import uniq from 'lodash/uniq'; 6 | import some from 'lodash/some'; 7 | 8 | import moment from 'moment'; 9 | import { MONTHS_IN_YEAR } from './const'; 10 | 11 | const buildCalendarValues = (localization?: string): string[] => { 12 | /* 13 | Return array of months (strings) like ['Aug', 'Sep', ...] 14 | that used to populate calendar's page. 15 | */ 16 | const localLocale = localization ? moment.localeData(localization) : undefined; 17 | 18 | return localLocale ? localLocale.monthsShort() : moment.monthsShort(); 19 | }; 20 | 21 | const getInitialDatePosition = ( 22 | selectable: number[], 23 | currentDate: moment.Moment, 24 | ): number => { 25 | if (selectable.indexOf(currentDate.month()) < 0) { 26 | return selectable[0]; 27 | } 28 | 29 | return currentDate.month(); 30 | }; 31 | 32 | const getDisabledPositions = ( 33 | enable: moment.Moment[], 34 | disable: moment.Moment[], 35 | maxDate: moment.Moment, 36 | minDate: moment.Moment, 37 | currentDate: moment.Moment, 38 | ): number[] => { 39 | /* 40 | Return position numbers of months that should be displayed as disabled 41 | (position in array returned by `this.buildCalendarValues`). 42 | */ 43 | let disabled = []; 44 | if (isArray(enable)) { 45 | const enabledMonthPositions = enable 46 | .filter((monthMoment) => monthMoment.isSame(currentDate, 'year')) 47 | .map((monthMoment) => monthMoment.month()); 48 | disabled = disabled.concat(range(0, MONTHS_IN_YEAR) 49 | .filter((monthPosition) => !includes(enabledMonthPositions, monthPosition))); 50 | } 51 | if (isArray(disable)) { 52 | disabled = disabled.concat(disable 53 | .filter((monthMoment) => monthMoment.year() === currentDate.year()) 54 | .map((monthMoment) => monthMoment.month())); 55 | } 56 | if (!isNil(maxDate)) { 57 | if (maxDate.year() === currentDate.year()) { 58 | disabled = disabled.concat( 59 | range(maxDate.month() + 1, MONTHS_IN_YEAR)); 60 | } 61 | if (maxDate.year() < currentDate.year()) { 62 | disabled = range(0, MONTHS_IN_YEAR); 63 | } 64 | } 65 | if (!isNil(minDate)) { 66 | if (minDate.year() === currentDate.year()) { 67 | disabled = disabled.concat(range(0, minDate.month())); 68 | } 69 | if (minDate.year() > currentDate.year()) { 70 | disabled = range(0, MONTHS_IN_YEAR); 71 | } 72 | } 73 | if (disabled.length > 0) { 74 | return uniq(disabled); 75 | } 76 | }; 77 | 78 | const isNextPageAvailable = ( 79 | maxDate: moment.Moment, 80 | enable: moment.Moment[], 81 | currentDate: moment.Moment, 82 | ): boolean => { 83 | if (isArray(enable)) { 84 | return some(enable, (enabledMonth) => enabledMonth.isAfter(currentDate, 'year')); 85 | } 86 | if (isNil(maxDate)) { 87 | return true; 88 | } 89 | 90 | return currentDate.year() < maxDate.year(); 91 | }; 92 | 93 | const isPrevPageAvailable = ( 94 | minDate: moment.Moment, 95 | enable: moment.Moment[], 96 | currentDate: moment.Moment, 97 | ): boolean => { 98 | if (isArray(enable)) { 99 | return some(enable, (enabledMonth) => enabledMonth.isBefore(currentDate, 'year')); 100 | } 101 | if (isNil(minDate)) { 102 | return true; 103 | } 104 | 105 | return currentDate.year() > minDate.year(); 106 | }; 107 | 108 | export { 109 | buildCalendarValues, 110 | getInitialDatePosition, 111 | getDisabledPositions, 112 | isNextPageAvailable, 113 | isPrevPageAvailable, 114 | }; 115 | -------------------------------------------------------------------------------- /src/pickers/timePicker/HourPicker.tsx: -------------------------------------------------------------------------------- 1 | import filter from 'lodash/filter'; 2 | import range from 'lodash/range'; 3 | import includes from 'lodash/includes'; 4 | import isArray from 'lodash/isArray'; 5 | import concat from 'lodash/concat'; 6 | import uniq from 'lodash/uniq'; 7 | import sortBy from 'lodash/sortBy'; 8 | 9 | import * as React from 'react'; 10 | 11 | import HourView from '../../views/HourView'; 12 | import { 13 | BasePickerOnChangeData, 14 | BasePickerProps, 15 | DisableValuesProps, 16 | MinMaxValueProps, 17 | OptionalHeaderProps, 18 | ProvideHeadingValue, 19 | SingleSelectionPicker, 20 | TimeFormat, 21 | TimePickerProps, 22 | } from '../BasePicker'; 23 | import { 24 | buildTimeStringWithSuffix, 25 | getCurrentDate, 26 | isNextPageAvailable, 27 | isPrevPageAvailable, 28 | } from './sharedFunctions'; 29 | 30 | const HOURS_ON_PAGE = 24; 31 | const PAGE_WIDTH = 4; 32 | 33 | type HourPickerProps = BasePickerProps 34 | & MinMaxValueProps 35 | & DisableValuesProps 36 | & TimePickerProps 37 | & OptionalHeaderProps; 38 | 39 | export interface HourPickerOnChangeData extends BasePickerOnChangeData { 40 | value: { 41 | year: number, 42 | month: number, 43 | date: number, 44 | hour: number, 45 | }; 46 | } 47 | 48 | class HourPicker 49 | extends SingleSelectionPicker 50 | implements ProvideHeadingValue { 51 | public static readonly defaultProps: { timeFormat: TimeFormat } = { 52 | timeFormat: '24', 53 | }; 54 | 55 | constructor(props) { 56 | super(props); 57 | this.PAGE_WIDTH = PAGE_WIDTH; 58 | } 59 | 60 | public render() { 61 | const { 62 | onChange, 63 | value, 64 | initializeWith, 65 | closePopup, 66 | inline, 67 | isPickerInFocus, 68 | isTriggerInFocus, 69 | onCalendarViewMount, 70 | minDate, 71 | maxDate, 72 | disable, 73 | timeFormat, 74 | localization, 75 | ...rest 76 | } = this.props; 77 | 78 | return ( 79 | 96 | ); 97 | } 98 | 99 | public getCurrentDate(): string { 100 | /* Return currently selected month, date and year(string) to display in calendar header. */ 101 | return getCurrentDate(this.state.date); 102 | } 103 | 104 | protected buildCalendarValues(): string[] { 105 | /* 106 | Return array of hours (strings) like ['16:00', '17:00', ...] 107 | that used to populate calendar's page. 108 | */ 109 | return range(0, 24).map((h) => { 110 | return `${h < 10 ? '0' : ''}${h}`; 111 | }).map((hour) => buildTimeStringWithSuffix(hour, '00', this.props.timeFormat)); 112 | } 113 | 114 | protected getSelectableCellPositions(): number[] { 115 | return filter( 116 | range(0, HOURS_ON_PAGE), 117 | (h) => !includes(this.getDisabledPositions(), h), 118 | ); 119 | } 120 | 121 | protected getInitialDatePosition(): number { 122 | const selectable = this.getSelectableCellPositions(); 123 | if (selectable.indexOf(this.state.date.hour()) < 0) { 124 | return selectable[0]; 125 | } 126 | 127 | return this.state.date.hour(); 128 | } 129 | 130 | protected getActiveCellPosition(): number { 131 | /* 132 | Return position of an hour that should be displayed as active 133 | (position in array returned by `this.buildCalendarValues`). 134 | */ 135 | const { value } = this.props; 136 | if (value && value.isSame(this.state.date, 'date')) { 137 | return this.props.value.hour(); 138 | } 139 | } 140 | 141 | protected isNextPageAvailable(): boolean { 142 | return isNextPageAvailable(this.state.date, this.props.maxDate); 143 | } 144 | 145 | protected isPrevPageAvailable(): boolean { 146 | return isPrevPageAvailable(this.state.date, this.props.minDate); 147 | } 148 | 149 | protected getDisabledPositions(): number[] { 150 | /* 151 | Return position numbers of hours that should be displayed as disabled 152 | (position in array returned by `this.buildCalendarValues`). 153 | */ 154 | const { 155 | disable, 156 | minDate, 157 | maxDate, 158 | } = this.props; 159 | let disabledByDisable = []; 160 | let disabledByMaxDate = []; 161 | let disabledByMinDate = []; 162 | 163 | if (isArray(disable)) { 164 | disabledByDisable = concat( 165 | disabledByDisable, 166 | disable.filter((date) => date.isSame(this.state.date, 'day')) 167 | .map((date) => date.hour())); 168 | } 169 | if (minDate) { 170 | if (minDate.isSame(this.state.date, 'day')) { 171 | disabledByMinDate = concat( 172 | disabledByMinDate, 173 | range(0 , minDate.hour())); 174 | } 175 | } 176 | if (maxDate) { 177 | if (maxDate.isSame(this.state.date, 'day')) { 178 | disabledByMaxDate = concat( 179 | disabledByMaxDate, 180 | range(maxDate.hour() + 1, 24)); 181 | } 182 | } 183 | const result = sortBy( 184 | uniq( 185 | concat(disabledByDisable, disabledByMaxDate, disabledByMinDate))); 186 | if (result.length > 0) { 187 | return result; 188 | } 189 | } 190 | 191 | protected handleChange = (e: React.SyntheticEvent, { value }): void => { 192 | const data: HourPickerOnChangeData = { 193 | ...this.props, 194 | value: { 195 | year: this.state.date.year(), 196 | month: this.state.date.month(), 197 | date: this.state.date.date(), 198 | hour: this.buildCalendarValues().indexOf(value), 199 | }, 200 | }; 201 | this.props.onChange(e, data); 202 | } 203 | 204 | protected switchToNextPage = (e: React.SyntheticEvent, 205 | data: any, 206 | callback: () => void): void => { 207 | this.setState(({ date }) => { 208 | const nextDate = date.clone(); 209 | nextDate.add(1, 'day'); 210 | 211 | return { date: nextDate }; 212 | }, callback); 213 | } 214 | 215 | protected switchToPrevPage = (e: React.SyntheticEvent, 216 | data: any, 217 | callback: () => void): void => { 218 | this.setState(({ date }) => { 219 | const prevDate = date.clone(); 220 | prevDate.subtract(1, 'day'); 221 | 222 | return { date: prevDate }; 223 | }, callback); 224 | } 225 | } 226 | 227 | export default HourPicker; 228 | -------------------------------------------------------------------------------- /src/pickers/timePicker/MinutePicker.tsx: -------------------------------------------------------------------------------- 1 | import range from 'lodash/range'; 2 | import isArray from 'lodash/isArray'; 3 | import concat from 'lodash/concat'; 4 | import uniq from 'lodash/uniq'; 5 | import sortBy from 'lodash/sortBy'; 6 | 7 | import * as React from 'react'; 8 | 9 | import MinuteView from '../../views/MinuteView'; 10 | import { 11 | BasePickerOnChangeData, 12 | BasePickerProps, 13 | DisableValuesProps, 14 | MinMaxValueProps, 15 | OptionalHeaderProps, 16 | ProvideHeadingValue, 17 | SingleSelectionPicker, 18 | TimeFormat, 19 | TimePickerProps, 20 | } from '../BasePicker'; 21 | import { 22 | buildTimeStringWithSuffix, 23 | getCurrentDate, 24 | isNextPageAvailable, 25 | isPrevPageAvailable, 26 | } from './sharedFunctions'; 27 | 28 | const MINUTES_STEP = 5; 29 | const MINUTES_ON_PAGE = 12; 30 | const PAGE_WIDTH = 3; 31 | 32 | type MinutePickerProps = BasePickerProps 33 | & MinMaxValueProps 34 | & DisableValuesProps 35 | & TimePickerProps 36 | & OptionalHeaderProps; 37 | 38 | export interface MinutePickerOnChangeData extends BasePickerOnChangeData { 39 | value: { 40 | year: number, 41 | month: number, 42 | date: number, 43 | hour: number, 44 | minute: number, 45 | }; 46 | } 47 | 48 | class MinutePicker 49 | extends SingleSelectionPicker 50 | implements ProvideHeadingValue { 51 | public static readonly defaultProps: { timeFormat: TimeFormat } = { 52 | timeFormat: '24', 53 | }; 54 | 55 | constructor(props) { 56 | super(props); 57 | this.PAGE_WIDTH = PAGE_WIDTH; 58 | } 59 | 60 | public render() { 61 | const { 62 | onChange, 63 | value, 64 | initializeWith, 65 | closePopup, 66 | inline, 67 | isPickerInFocus, 68 | isTriggerInFocus, 69 | onCalendarViewMount, 70 | minDate, 71 | maxDate, 72 | disable, 73 | timeFormat, 74 | localization, 75 | ...rest 76 | } = this.props; 77 | 78 | return ( 79 | 96 | ); 97 | } 98 | 99 | public getCurrentDate(): string { 100 | /* Return currently selected month, date and year(string) to display in calendar header. */ 101 | return getCurrentDate(this.state.date); 102 | } 103 | 104 | protected buildCalendarValues(): string[] { 105 | /* 106 | Return array of minutes (strings) like ['16:15', '16:20', ...] 107 | that used to populate calendar's page. 108 | */ 109 | const hour = this.state.date.hour() < 10 110 | ? '0' + this.state.date.hour().toString() 111 | : this.state.date.hour().toString(); 112 | 113 | return range(0, 60, MINUTES_STEP) 114 | .map((minute) => `${minute < 10 ? '0' : ''}${minute}`) 115 | .map((minute) => buildTimeStringWithSuffix(hour, minute, this.props.timeFormat)); 116 | } 117 | 118 | protected getSelectableCellPositions(): number[] { 119 | const disabled = this.getDisabledPositions(); 120 | const all = range(0, MINUTES_ON_PAGE); 121 | if (disabled) { 122 | return all.filter((pos) => { 123 | return disabled.indexOf(pos) < 0; 124 | }); 125 | } 126 | 127 | return all; 128 | } 129 | 130 | protected getInitialDatePosition(): number { 131 | const selectable = this.getSelectableCellPositions(); 132 | if (selectable.indexOf(getMinuteCellPosition(this.state.date.minute())) < 0) { 133 | return selectable[0]; 134 | } 135 | 136 | return getMinuteCellPosition(this.state.date.minute()); 137 | } 138 | 139 | protected getDisabledPositions(): number[] { 140 | const { 141 | disable, 142 | minDate, 143 | maxDate, 144 | } = this.props; 145 | let disabledByDisable = []; 146 | let disabledByMaxDate = []; 147 | let disabledByMinDate = []; 148 | 149 | if (isArray(disable)) { 150 | disabledByDisable = concat( 151 | disabledByDisable, 152 | disable.filter((date) => date.isSame(this.state.date, 'day')) 153 | .map((date) => getMinuteCellPosition(date.minute()))); 154 | } 155 | if (minDate) { 156 | if (minDate.isSame(this.state.date, 'hour')) { 157 | disabledByMinDate = concat( 158 | disabledByMinDate, 159 | range(0 , minDate.minute()).map((m) => getMinuteCellPosition(m))); 160 | } 161 | } 162 | if (maxDate) { 163 | if (maxDate.isSame(this.state.date, 'hour')) { 164 | disabledByMaxDate = concat( 165 | disabledByMaxDate, 166 | range(maxDate.minute() + MINUTES_STEP, 60).map((m) => getMinuteCellPosition(m))); 167 | } 168 | } 169 | const result = sortBy( 170 | uniq( 171 | concat(disabledByDisable, disabledByMaxDate, disabledByMinDate))); 172 | if (result.length > 0) { 173 | return result; 174 | } 175 | } 176 | 177 | protected getActiveCellPosition(): number { 178 | /* 179 | Return position of a minute that should be displayed as active 180 | (position in array returned by `this.buildCalendarValues`). 181 | */ 182 | const { value } = this.props; 183 | if (value && value.isSame(this.state.date, 'date')) { 184 | return Math.floor(this.props.value.minutes() / MINUTES_STEP); 185 | } 186 | } 187 | 188 | protected isNextPageAvailable(): boolean { 189 | return isNextPageAvailable(this.state.date, this.props.maxDate); 190 | } 191 | 192 | protected isPrevPageAvailable(): boolean { 193 | return isPrevPageAvailable(this.state.date, this.props.minDate); 194 | } 195 | 196 | protected handleChange = (e: React.SyntheticEvent, { value }): void => { 197 | const data: MinutePickerOnChangeData = { 198 | ...this.props, 199 | value: { 200 | year: this.state.date.year(), 201 | month: this.state.date.month(), 202 | date: this.state.date.date(), 203 | hour: this.state.date.hour(), 204 | minute: this.buildCalendarValues().indexOf(value) * MINUTES_STEP, 205 | }, 206 | }; 207 | this.props.onChange(e, data); 208 | } 209 | 210 | protected switchToNextPage = (e: React.SyntheticEvent, 211 | data: any, 212 | callback: () => void): void => { 213 | this.setState(({ date }) => { 214 | const nextDate = date.clone(); 215 | nextDate.add(1, 'day'); 216 | 217 | return { date: nextDate }; 218 | }, callback); 219 | } 220 | 221 | protected switchToPrevPage = (e: React.SyntheticEvent, 222 | data: any, 223 | callback: () => void): void => { 224 | this.setState(({ date }) => { 225 | const prevDate = date.clone(); 226 | prevDate.subtract(1, 'day'); 227 | 228 | return { date: prevDate }; 229 | }, callback); 230 | } 231 | } 232 | 233 | function getMinuteCellPosition(minute: number): number { 234 | return Math.floor(minute / MINUTES_STEP); 235 | } 236 | 237 | export default MinutePicker; 238 | -------------------------------------------------------------------------------- /src/pickers/timePicker/sharedFunctions.ts: -------------------------------------------------------------------------------- 1 | import { Moment } from 'moment'; 2 | 3 | import { TimeFormat } from '../BasePicker'; 4 | 5 | export function buildTimeStringWithSuffix( 6 | hour: string, 7 | minute: string, 8 | timeFormat: TimeFormat): string { 9 | if (timeFormat === 'ampm') { 10 | if (parseInt(hour, 10) < 12) { 11 | return `${convertHourTo_12_Format(hour)}:${minute} am`; 12 | } 13 | 14 | return `${convertHourTo_12_Format(hour)}:${minute} pm`; 15 | } 16 | if (timeFormat === 'AMPM') { 17 | if (parseInt(hour, 10) < 12) { 18 | return `${convertHourTo_12_Format(hour)}:${minute} AM`; 19 | } 20 | 21 | return `${convertHourTo_12_Format(hour)}:${minute} PM`; 22 | } 23 | 24 | return `${hour}:${minute}`; 25 | } 26 | 27 | function convertHourTo_12_Format(hour: string): string { 28 | if (hour === '00' || hour === '12') { 29 | return '12'; 30 | } 31 | if (parseInt(hour, 10) < 12) { 32 | return hour; 33 | } 34 | const h = (parseInt(hour, 10) - 12).toString(); 35 | if (h.length === 1) { 36 | return '0' + h; 37 | } 38 | 39 | return h; 40 | } 41 | 42 | export function isNextPageAvailable(date: Moment, maxDate: Moment): boolean { 43 | if (maxDate) { 44 | return maxDate.isAfter(date, 'day'); 45 | } 46 | 47 | return true; 48 | } 49 | 50 | export function isPrevPageAvailable(date: Moment, minDate: Moment): boolean { 51 | if (minDate) { 52 | return minDate.isBefore(date, 'day'); 53 | } 54 | 55 | return true; 56 | } 57 | 58 | export function getCurrentDate(date: Moment): string { 59 | return date.format('MMMM DD, YYYY'); 60 | } 61 | -------------------------------------------------------------------------------- /src/views/BaseCalendarView.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { SemanticCOLORS } from 'semantic-ui-react'; 3 | 4 | export interface BaseCalendarViewProps { 5 | /** Used for passing calendar dom element to parent component. */ 6 | onMount: (e: HTMLElement) => void; 7 | /** Called on calendar blur. */ 8 | onBlur: () => void; 9 | /** Whether a calendar is inside a popup or inline. */ 10 | inline: boolean; 11 | /** An array of values to fill a calendar with (dates, or years, or anything like that). */ 12 | values: string[]; 13 | /** Called after clicking on particular value (date, year or anything like that). */ 14 | onValueClick: (e: React.SyntheticEvent, data: OnValueClickData) => void; 15 | /** Called on calendar cell hover. */ 16 | onCellHover: (e: React.SyntheticEvent, data: any) => void; 17 | /** Index of a cell that should be displayed as hovered. */ 18 | hoveredItemIndex?: number; 19 | /** An array of cell positions to display as disabled. */ 20 | disabledItemIndexes?: number[]; 21 | /** An array of cell positions to display as marked. */ 22 | markedItemIndexes?: number[]; 23 | /** An array of cell positions to display as marked. */ 24 | markColor?: SemanticCOLORS; 25 | /** Moment date localization */ 26 | localization?: string; 27 | } 28 | 29 | export interface SingleSelectionCalendarViewProps { 30 | /** Position of a cell to display as active. */ 31 | activeItemIndex?: number; 32 | } 33 | 34 | export interface RangeIndexes { 35 | start: number | undefined; 36 | end: number | undefined; 37 | } 38 | 39 | export interface RangeSelectionCalendarViewProps { 40 | /** Currently selected range value (from - to) that is displayed in calendar header. */ 41 | currentRangeHeadingValue: string; 42 | /** Indexes of start and end values of currently selected range (to display as active). */ 43 | activeRange: RangeIndexes; 44 | } 45 | 46 | export interface CalendarWithHeaderViewProps { 47 | /** Called after click on next page button. */ 48 | onNextPageBtnClick: (e?: React.SyntheticEvent, data?: any, cb?: () => void) => void; 49 | /** Called after click on previous page button. */ 50 | onPrevPageBtnClick: (e?: React.SyntheticEvent, data?: any, cb?: () => void) => void; 51 | /** Whether to display previous page button as active or disabled. */ 52 | hasPrevPage: boolean; 53 | /** Whether to display next page button as active or disabled. */ 54 | hasNextPage: boolean; 55 | /** Called after click on calendar header. */ 56 | onHeaderClick: () => void; 57 | } 58 | 59 | export interface HeadingValueProps { 60 | /** A value (date, year or anything like that) that is displayed in calendar header. */ 61 | currentHeadingValue: string; 62 | } 63 | 64 | // export interface CalendarWithHeaderViewProps extends CalendarWithHeaderViewPropsBase { 65 | // /** A value (date, year or anything like that) that is displayed in calendar header. */ 66 | // currentHeadingValue: string; 67 | // } 68 | 69 | export interface CalendarWithOptionalHeaderViewProps { 70 | /** Whether a calendar has header. */ 71 | hasHeader: boolean; 72 | /** Called after click on next page button. */ 73 | onNextPageBtnClick?: (e?: React.SyntheticEvent, data?: any, cb?: () => void) => void; 74 | /** Called after click on previous page button. */ 75 | onPrevPageBtnClick?: (e?: React.SyntheticEvent, data?: any, cb?: () => void) => void; 76 | /** Whether to display previous page button as active or disabled. */ 77 | hasPrevPage?: boolean; 78 | /** Whether to display next page button as active or disabled. */ 79 | hasNextPage?: boolean; 80 | /** A value (date, year or anything like that) that is displayed in calendar header. */ 81 | currentHeadingValue?: string; 82 | /** Called after click on calendar header. */ 83 | onHeaderClick?: () => void; 84 | } 85 | 86 | export interface OnValueClickData { 87 | [key: string]: any; 88 | /** Position of the clicked cell. */ 89 | itemPosition: number; 90 | /** Text content of the clicked cell. */ 91 | value: string; 92 | } 93 | 94 | /** Base class for picker view components. */ 95 | class BaseCalendarView

extends React.Component { 96 | protected calendarNode: HTMLElement | undefined; 97 | 98 | public componentDidMount() { 99 | if (this.props.onMount) { 100 | this.props.onMount(this.calendarNode); 101 | } 102 | } 103 | } 104 | 105 | export default BaseCalendarView; 106 | -------------------------------------------------------------------------------- /src/views/Calendar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Table } from 'semantic-ui-react'; 3 | 4 | // interface PickerStyle { 5 | // [key: string]: any; 6 | // width?: string; 7 | // minWidth?: string; 8 | // } 9 | 10 | interface CalendarProps { 11 | /** Table content. */ 12 | children: React.ReactNode[]; 13 | /** Whether to outline the calendar on focus. */ 14 | outlineOnFocus: boolean; 15 | /** Picker width (any value that `style.width` can take). */ 16 | pickerWidth?: string; 17 | /** Style object for picker. */ 18 | pickerStyle?: object; 19 | } 20 | 21 | class Calendar extends React.Component { 22 | public static readonly propTypes: object; 23 | public static readonly defaultProps = { 24 | pickerWidth: '100%', 25 | }; 26 | 27 | public render() { 28 | const { 29 | children, 30 | outlineOnFocus, 31 | pickerWidth, 32 | pickerStyle, 33 | ...rest 34 | } = this.props; 35 | const style = { 36 | width: pickerWidth, 37 | minWidth: '22em', 38 | // Prevent poped up picker from beeing outlined on focus. 39 | // Inline picker should be outlined when in focus. 40 | outline: outlineOnFocus ? undefined : 'none', 41 | ...pickerStyle, 42 | }; 43 | 44 | return ( 45 | 51 | { children } 52 |
53 | ); 54 | } 55 | } 56 | 57 | export default Calendar; 58 | -------------------------------------------------------------------------------- /src/views/CalendarBody/Body.tsx: -------------------------------------------------------------------------------- 1 | import isNil from 'lodash/isNil'; 2 | import isArray from 'lodash/isArray'; 3 | 4 | import * as React from 'react'; 5 | import { Table } from 'semantic-ui-react'; 6 | 7 | import { OnValueClickData } from '../BaseCalendarView'; 8 | import Cell from './Cell'; 9 | import { 10 | cellStyleWidth3, 11 | cellStyleWidth4, 12 | cellStyleWidth7, 13 | CellWidthStyle, 14 | } from './Cell'; 15 | 16 | export type BodyWidth = 3 | 4 | 7; 17 | 18 | interface BodyProps { 19 | /** A number of columns in a row. */ 20 | width: BodyWidth; 21 | /** Data that is used to fill a calendar. */ 22 | data: string[]; 23 | /** Called after a click on calendar's cell. */ 24 | onCellClick: (e: React.SyntheticEvent, data: OnValueClickData) => void; 25 | /** Called on cell hover. */ 26 | onCellHover: (e: React.SyntheticEvent, data: any) => void; 27 | /** Index of an element in `data` array that should be displayed as hovered. */ 28 | hovered?: number; 29 | /** Index of an element (or array of indexes) in `data` array that should be displayed as active. */ 30 | active?: number | number[]; 31 | /** Array of element indexes in `data` array that should be displayed as disabled. */ 32 | disabled?: number[]; 33 | /** Array of element indexes in `data` array that should be displayed as marked. */ 34 | marked?: number[]; 35 | /** The color of the mark that will be displayed on the calendar. */ 36 | markColor?: string; 37 | } 38 | 39 | function Body(props: BodyProps) { 40 | const { 41 | data, 42 | width, 43 | onCellClick, 44 | active, 45 | disabled, 46 | hovered, 47 | onCellHover, 48 | marked, 49 | markColor, 50 | } = props; 51 | const content = buildRows(data, width).map((row, rowIndex) => ( 52 | 53 | { row.map((item, itemIndex) => ( 54 | 66 | )) } 67 | 68 | )); 69 | 70 | return ( 71 | 72 | { content } 73 | 74 | ); 75 | } 76 | 77 | function buildRows(data: string[], width: number): string[][] { 78 | const height = data.length / width; 79 | const rows = []; 80 | for (let i = 0; i < height; i++) { 81 | rows.push(data.slice((i * width), (i * width) + width)); 82 | } 83 | 84 | return rows; 85 | } 86 | 87 | function isActive(rowIndex: number, 88 | rowWidth: number, 89 | colIndex: number, 90 | active: number | number[]): boolean { 91 | if (isNil(active)) { 92 | return false; 93 | } 94 | if (isArray(active)) { 95 | for (const activeIndex of (active as number[])) { 96 | if (rowIndex * rowWidth + colIndex === activeIndex) { 97 | return true; 98 | } 99 | } 100 | } 101 | 102 | return rowIndex * rowWidth + colIndex === active; 103 | } 104 | 105 | function isHovered(rowIndex: number, 106 | rowWidth: number, 107 | colIndex: number, 108 | hovered: number): boolean { 109 | if (isNil(hovered)) { 110 | return false; 111 | } 112 | 113 | return rowIndex * rowWidth + colIndex === hovered; 114 | } 115 | 116 | function isDisabled(rowIndex: number, 117 | rowWidth: number, 118 | colIndex: number, 119 | disabledIndexes: number[]): boolean { 120 | if (isNil(disabledIndexes) || disabledIndexes.length === 0) { 121 | return false; 122 | } 123 | for (const disabledIndex of disabledIndexes) { 124 | if (rowIndex * rowWidth + colIndex === disabledIndex) { 125 | return true; 126 | } 127 | } 128 | 129 | return false; 130 | } 131 | 132 | function getCellStyle(width: BodyWidth): CellWidthStyle { 133 | switch (width) { 134 | case 3: 135 | return cellStyleWidth3; 136 | case 4: 137 | return cellStyleWidth4; 138 | case 7: 139 | return cellStyleWidth7; 140 | default: 141 | break; 142 | } 143 | } 144 | 145 | function isMarked(rowIndex: number, 146 | rowWidth: number, 147 | colIndex: number, 148 | markedIndexes: number[]): boolean { 149 | if (isNil(markedIndexes) || markedIndexes.length === 0) { 150 | return false; 151 | } 152 | for (const markedIndex of markedIndexes) { 153 | if (rowIndex * rowWidth + colIndex === markedIndex) { 154 | return true; 155 | } 156 | } 157 | 158 | return false; 159 | } 160 | 161 | export default Body; 162 | -------------------------------------------------------------------------------- /src/views/CalendarBody/Cell.tsx: -------------------------------------------------------------------------------- 1 | import invoke from 'lodash/invoke'; 2 | 3 | import * as React from 'react'; 4 | import { Table, Label } from 'semantic-ui-react'; 5 | 6 | import { OnValueClickData } from '../BaseCalendarView'; 7 | 8 | const hoverCellStyles = { 9 | outline: '1px solid #85b7d9', 10 | cursor: 'pointer', 11 | }; 12 | 13 | export interface CellWidthStyle { 14 | width: string; 15 | } 16 | 17 | export const cellStyleWidth3: CellWidthStyle = { 18 | width: '33.333333%', 19 | }; 20 | 21 | export const cellStyleWidth4: CellWidthStyle = { 22 | width: '25%', 23 | }; 24 | 25 | export const cellStyleWidth7: CellWidthStyle = { 26 | width: '14.285714%', 27 | }; 28 | 29 | interface CellProps { 30 | /** Position of a cell on the page. (Used by parent component) */ 31 | itemPosition: number; 32 | /** Cell's content. */ 33 | content: string; 34 | /** Styles for cell width. */ 35 | style: CellWidthStyle; 36 | /** Called after click on a cell. */ 37 | onClick: (e: React.SyntheticEvent, data: OnValueClickData) => void; 38 | /** Called on cell hover. */ 39 | onHover: (e: React.SyntheticEvent, data: OnValueClickData) => void; 40 | /** Is cell is hovered. */ 41 | hovered?: boolean; 42 | /** Is cell active. */ 43 | active?: boolean; 44 | /** Is cell disabled. */ 45 | disabled?: boolean; 46 | /** Is cell marked. */ 47 | marked?: boolean; 48 | /** Color of the mark. */ 49 | markColor?: any; 50 | } 51 | 52 | class Cell extends React.Component { 53 | public render() { 54 | const { 55 | itemPosition, 56 | content, 57 | style, 58 | onClick, 59 | onHover, 60 | hovered, 61 | marked, 62 | markColor, 63 | ...rest 64 | } = this.props; 65 | 66 | const cellStyle = { 67 | ...style, 68 | ...(hovered ? hoverCellStyles : {}), 69 | }; 70 | 71 | return ( 72 | 77 | { (marked && !rest.disabled) ? 78 | : {content} } 79 | 80 | ); 81 | } 82 | 83 | private onCellClick = (event) => { 84 | const { 85 | itemPosition, 86 | content, 87 | } = this.props; 88 | invoke(this.props, 'onClick', event, { ...this.props, itemPosition, value: content }); 89 | } 90 | 91 | private onCellHover = (event) => { 92 | const { 93 | itemPosition, 94 | } = this.props; 95 | invoke(this.props, 'onHover', event, { ...this.props, itemPosition }); 96 | } 97 | } 98 | 99 | export default Cell; 100 | -------------------------------------------------------------------------------- /src/views/CalendarBody/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Body } from './Body'; 2 | export { default as Cell } from './Cell'; 3 | -------------------------------------------------------------------------------- /src/views/CalendarHeader/Header.tsx: -------------------------------------------------------------------------------- 1 | import isNil from 'lodash/isNil'; 2 | 3 | import * as React from 'react'; 4 | import { 5 | Icon, 6 | Table, 7 | } from 'semantic-ui-react'; 8 | 9 | import { BodyWidth } from '../CalendarBody/Body'; 10 | import HeaderRange from './HeaderRange'; 11 | import HeaderWeeks from './HeaderWeeks'; 12 | 13 | export interface HeaderProps { 14 | /** Header text content. */ 15 | title: string; 16 | /** Called after click on next page button. */ 17 | onNextPageBtnClick: () => void; 18 | /** Called after click on previous page button. */ 19 | onPrevPageBtnClick: () => void; 20 | /** Whether to display previous page button as active or disabled. */ 21 | hasPrevPage: boolean; 22 | /** Whether to display next page button as active or disabled. */ 23 | hasNextPage: boolean; 24 | /** Whether to display weeks row or not. */ 25 | displayWeeks: boolean; 26 | /** Header width. */ 27 | width: BodyWidth; 28 | /** Text content to display in dates-range row. */ 29 | rangeRowContent?: string; 30 | /** Called after click on calendar header. */ 31 | onHeaderClick?: () => void; 32 | /** Moment date localization */ 33 | localization?: string; 34 | className?: string; 35 | } 36 | 37 | function Header(props: HeaderProps) { 38 | const { 39 | rangeRowContent, 40 | displayWeeks, 41 | onNextPageBtnClick, 42 | onPrevPageBtnClick, 43 | hasPrevPage, 44 | hasNextPage, 45 | onHeaderClick, 46 | width, 47 | title, 48 | localization, 49 | className, 50 | } = props; 51 | 52 | const cellStyle = { 53 | border: 'none', 54 | borderBottom: displayWeeks ? 'none' : '1px solid rgba(34,36,38,.1)', 55 | }; 56 | const prevPageBtnStyle = { 57 | cursor: hasPrevPage ? 'pointer' : 'auto', 58 | }; 59 | const nextPageBtnStyle = { 60 | cursor: hasNextPage ? 'pointer' : 'auto', 61 | }; 62 | const headerTitleStyle = { 63 | cursor: onHeaderClick ? 'pointer' : 'default', 64 | }; 65 | 66 | return ( 67 | 68 | { !isNil(rangeRowContent) && } 69 | 70 | 71 | 77 | 78 | 79 | 83 | { title } 84 | 85 | 86 | 87 | 93 | 94 | 95 | { displayWeeks && } 96 | 97 | ); 98 | } 99 | 100 | export default Header; 101 | -------------------------------------------------------------------------------- /src/views/CalendarHeader/HeaderRange.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Table } from 'semantic-ui-react'; 3 | 4 | const cellStyle = { 5 | border: 'none', 6 | }; 7 | 8 | interface HeaderRangeProps { 9 | /** Selected dates range. */ 10 | content: string; 11 | } 12 | 13 | function HeaderRange(props: HeaderRangeProps) { 14 | const { 15 | content, 16 | } = props; 17 | 18 | return ( 19 | 20 | 21 | { content } 22 | 23 | 24 | ); 25 | } 26 | 27 | export default HeaderRange; 28 | -------------------------------------------------------------------------------- /src/views/CalendarHeader/HeaderWeeks.tsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import * as React from 'react'; 3 | import { Table } from 'semantic-ui-react'; 4 | 5 | /** Return array of week day names. 6 | * 7 | * getWeekDays() --> ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Su'] 8 | */ 9 | const getWeekDays = (m, localization) => { 10 | const weekDays = []; 11 | const day = localization ? m().locale(localization).startOf('week') : m().startOf('week'); 12 | for (let i = 0; i < 7; i++) { 13 | weekDays[i] = day.format('dd'); 14 | day.add(1, 'd'); 15 | } 16 | 17 | return weekDays; 18 | }; 19 | 20 | const cellStyle = { 21 | border: 'none', 22 | borderBottom: '1px solid rgba(34,36,38,.1)', 23 | }; 24 | 25 | const getWeekDayCells = (m, localization) => getWeekDays(m, localization).map((weekDay) => ( 26 | 30 | {weekDay} 31 | 32 | )); 33 | 34 | export interface HeaderWeeksProps { 35 | /** Moment date localization */ 36 | localization?: string; 37 | } 38 | 39 | function HeaderWeeks(props: HeaderWeeksProps) { 40 | const { 41 | localization, 42 | } = props; 43 | 44 | return ( 45 | 46 | { getWeekDayCells(moment, localization) } 47 | 48 | ); 49 | } 50 | 51 | export default HeaderWeeks; 52 | -------------------------------------------------------------------------------- /src/views/CalendarHeader/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from './Header'; 2 | export { default as HeaderRange } from './HeaderRange'; 3 | export { default as HeaderWeeks } from './HeaderWeeks'; 4 | -------------------------------------------------------------------------------- /src/views/DatesRangeView.tsx: -------------------------------------------------------------------------------- 1 | import range from 'lodash/range'; 2 | import isNil from 'lodash/isNil'; 3 | 4 | import * as React from 'react'; 5 | 6 | import BaseCalendarView, { 7 | BaseCalendarViewProps, 8 | CalendarWithHeaderViewProps, 9 | HeadingValueProps, 10 | RangeSelectionCalendarViewProps, 11 | } from './BaseCalendarView'; 12 | import Calendar from './Calendar'; 13 | import Body from './CalendarBody/Body'; 14 | import Header from './CalendarHeader/Header'; 15 | import { 16 | DAY_CALENDAR_ROW_WIDTH, 17 | WEEKS_TO_DISPLAY, 18 | } from './DayView'; 19 | 20 | import { findHTMLElement } from '../lib'; 21 | 22 | const DAY_POSITIONS = range(WEEKS_TO_DISPLAY * 7); 23 | 24 | function getActive(start: number, end: number): number | number[] | undefined { 25 | if (isNil(start) && isNil(end)) { 26 | return; 27 | } 28 | if (!isNil(start) && isNil(end)) { 29 | return start; 30 | } 31 | if (!isNil(start) && !isNil(end)) { 32 | return DAY_POSITIONS.slice(start, end + 1); 33 | } 34 | } 35 | 36 | type DatesRangeViewProps = 37 | BaseCalendarViewProps 38 | & HeadingValueProps 39 | & RangeSelectionCalendarViewProps 40 | & CalendarWithHeaderViewProps; 41 | 42 | class DatesRangeView extends BaseCalendarView { 43 | public static defaultProps = { 44 | active: { 45 | start: undefined, 46 | end: undefined, 47 | }, 48 | }; 49 | 50 | public render() { 51 | const { 52 | values, 53 | onNextPageBtnClick, 54 | onPrevPageBtnClick, 55 | onValueClick, 56 | hasPrevPage, 57 | hasNextPage, 58 | currentHeadingValue, 59 | onHeaderClick, 60 | activeRange, 61 | disabledItemIndexes, 62 | currentRangeHeadingValue, 63 | hoveredItemIndex, 64 | onCellHover, 65 | onMount, 66 | inline, 67 | markColor, 68 | markedItemIndexes, 69 | localization, 70 | ...rest 71 | } = this.props; 72 | const { 73 | start, 74 | end, 75 | } = activeRange; 76 | 77 | return ( 78 | this.calendarNode = findHTMLElement(e)} outlineOnFocus={inline} {...rest}> 79 |

91 | 101 | 102 | ); 103 | } 104 | } 105 | 106 | export default DatesRangeView; 107 | -------------------------------------------------------------------------------- /src/views/DayView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import BaseCalendarView, { 4 | BaseCalendarViewProps, 5 | CalendarWithHeaderViewProps, 6 | HeadingValueProps, 7 | SingleSelectionCalendarViewProps, 8 | } from './BaseCalendarView'; 9 | import Calendar from './Calendar'; 10 | import Body from './CalendarBody/Body'; 11 | import Header from './CalendarHeader/Header'; 12 | 13 | import { findHTMLElement } from '../lib'; 14 | 15 | export const DAY_CALENDAR_ROW_WIDTH = 7; 16 | export const WEEKS_TO_DISPLAY = 6; 17 | 18 | type DayViewProps = 19 | BaseCalendarViewProps 20 | & HeadingValueProps 21 | & SingleSelectionCalendarViewProps 22 | & CalendarWithHeaderViewProps; 23 | 24 | class DayView extends BaseCalendarView { 25 | public render() { 26 | const { 27 | values, 28 | onNextPageBtnClick, 29 | onPrevPageBtnClick, 30 | onValueClick, 31 | hasNextPage, 32 | hasPrevPage, 33 | currentHeadingValue, 34 | onHeaderClick, 35 | disabledItemIndexes, 36 | activeItemIndex, 37 | hoveredItemIndex, 38 | onCellHover, 39 | onMount, 40 | inline, 41 | markedItemIndexes, 42 | markColor, 43 | localization, 44 | ...rest 45 | } = this.props; 46 | 47 | return ( 48 | this.calendarNode = findHTMLElement(e)} outlineOnFocus={inline} {...rest}> 49 |
60 | 70 | 71 | ); 72 | } 73 | } 74 | 75 | export default DayView; 76 | -------------------------------------------------------------------------------- /src/views/HourView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import BaseCalendarView, { 4 | BaseCalendarViewProps, 5 | CalendarWithOptionalHeaderViewProps, 6 | SingleSelectionCalendarViewProps, 7 | } from './BaseCalendarView'; 8 | import Calendar from './Calendar'; 9 | import Body from './CalendarBody/Body'; 10 | import Header, { HeaderProps } from './CalendarHeader/Header'; 11 | 12 | import { findHTMLElement } from '../lib'; 13 | 14 | const HOUR_CALENDAR_ROW_WIDTH = 4; 15 | 16 | type HourViewProps = 17 | BaseCalendarViewProps 18 | & SingleSelectionCalendarViewProps 19 | & CalendarWithOptionalHeaderViewProps; 20 | 21 | class HourView extends BaseCalendarView { 22 | public render() { 23 | const { 24 | values, 25 | hasHeader, 26 | onValueClick, 27 | onNextPageBtnClick, 28 | onPrevPageBtnClick, 29 | hasPrevPage, 30 | hasNextPage, 31 | onHeaderClick, 32 | disabledItemIndexes, 33 | activeItemIndex, 34 | currentHeadingValue, 35 | hoveredItemIndex, 36 | onCellHover, 37 | onMount, 38 | inline, 39 | localization, 40 | ...rest 41 | } = this.props; 42 | const headerProps: HeaderProps = { 43 | className: 'suicr-hour-view-header', 44 | onNextPageBtnClick, 45 | onPrevPageBtnClick, 46 | hasPrevPage, 47 | hasNextPage, 48 | onHeaderClick, 49 | title: currentHeadingValue, 50 | width: HOUR_CALENDAR_ROW_WIDTH, 51 | displayWeeks: false, 52 | localization, 53 | }; 54 | 55 | return ( 56 | this.calendarNode = findHTMLElement(e)} outlineOnFocus={inline} {...rest}> 57 | { hasHeader &&
} 58 | 66 | 67 | ); 68 | } 69 | } 70 | 71 | export default HourView; 72 | -------------------------------------------------------------------------------- /src/views/InputView.tsx: -------------------------------------------------------------------------------- 1 | import isString from 'lodash/isString'; 2 | import invoke from 'lodash/invoke'; 3 | 4 | import * as React from 'react'; 5 | import { 6 | Form, 7 | FormInputProps, 8 | Icon, 9 | Popup, 10 | SemanticICONS, 11 | SemanticTRANSITIONS, 12 | Transition, 13 | } from 'semantic-ui-react'; 14 | import checkIE from '../lib/checkIE'; 15 | import checkMobile from '../lib/checkMobile'; 16 | 17 | const popupStyle = { 18 | padding: '0', 19 | filter: 'none', // prevents bluring popup when used inside Modal with dimmer="bluring" #28 #26 20 | }; 21 | 22 | class FormInputWithRef extends React.Component { 23 | 24 | public render() { 25 | 26 | const { 27 | value, 28 | clearable, 29 | icon, 30 | clearIcon, 31 | onClear, 32 | innerRef, 33 | onFocus, 34 | onBlur, 35 | onMouseEnter, 36 | ...rest 37 | } = this.props; 38 | 39 | const ClearIcon = isString(clearIcon) ? 40 | : 41 | 42 | ; 43 | 44 | return ( 45 | 53 | {value && clearable ? 54 | ClearIcon 55 | : 56 | 57 | } 58 | 59 | 63 | 64 | 65 | ); 66 | } 67 | } 68 | 69 | interface InputViewProps { 70 | /** Used for passing input dom node (input field or inline calendar) to parent component. */ 71 | onMount: (e: HTMLElement) => void; 72 | /** Called after input field value has changed. */ 73 | onChange: (e: React.SyntheticEvent, data: any) => void; 74 | /** Called when component looses focus. */ 75 | onBlur?: (e: React.SyntheticEvent) => void; 76 | closePopup: () => void; 77 | openPopup: () => void; 78 | /** Called on input focus. */ 79 | onFocus?: () => void; 80 | /** Function for rendering picker. */ 81 | renderPicker: () => React.ReactNode; 82 | /** Called after clear icon has clicked. */ 83 | onClear?: (e: React.SyntheticEvent, data: any) => void; 84 | /** Whether to close a popup when cursor leaves it. */ 85 | closeOnMouseLeave?: boolean; 86 | /** A field can have its label next to instead of above it. */ 87 | inlineLabel?: boolean; 88 | /** Using the clearable setting will let users remove their selection from a calendar. */ 89 | clearable?: boolean; 90 | /** Optional Icon to display inside the Input. */ 91 | icon?: SemanticICONS | boolean; 92 | /** Icon position. Default: 'right'. */ 93 | iconPosition?: 'left' | 'right'; 94 | /** Optional Icon to display inside the clearable Input. */ 95 | clearIcon?: any; 96 | /** Whether popup is closed. */ 97 | popupIsClosed?: boolean; 98 | /** The node where the picker should mount. */ 99 | mountNode?: HTMLElement; 100 | /** Input element tabindex. */ 101 | tabIndex?: string | number; 102 | /** Whether to display inline picker or picker inside a popup. */ 103 | inline?: boolean; 104 | /** Duration of the CSS transition animation in milliseconds. */ 105 | duration?: number; 106 | /** Named animation event to used. Must be defined in CSS. */ 107 | animation?: SemanticTRANSITIONS; 108 | /** Where to display popup. */ 109 | popupPosition?: 110 | | 'top left' 111 | | 'top right' 112 | | 'bottom right' 113 | | 'bottom left' 114 | | 'right center' 115 | | 'left center' 116 | | 'top center' 117 | | 'bottom center'; 118 | /** Currently selected value. */ 119 | value?: string; 120 | /** Picker width (any value that `style.width` can take). */ 121 | pickerWidth?: string; 122 | /** Style object for picker. */ 123 | pickerStyle?: object; 124 | /** Do not display popup if true. */ 125 | readOnly?: boolean; 126 | /** Try to prevent mobile keyboard appearing. */ 127 | hideMobileKeyboard?: boolean; 128 | } 129 | 130 | class InputView extends React.Component { 131 | public static defaultProps = { 132 | inline: false, 133 | closeOnMouseLeave: true, 134 | tabIndex: '0', 135 | clearable: false, 136 | clearIcon: 'remove', 137 | animation: 'scale', 138 | duration: 200, 139 | iconPosition: 'right', 140 | }; 141 | 142 | private inputNode: HTMLElement | undefined; 143 | private popupNode: HTMLElement | undefined; 144 | private mouseLeaveTimeout: number | null; 145 | 146 | public onBlur = (e, ...args) => { 147 | const { 148 | closePopup, 149 | } = this.props; 150 | 151 | if ( 152 | e.relatedTarget !== this.popupNode 153 | && e.relatedTarget !== this.inputNode 154 | && !checkIE() 155 | ) { 156 | invoke(this.props, 'onBlur', e, ...args); 157 | closePopup(); 158 | } 159 | } 160 | 161 | public onMouseLeave = (e, ...args) => { 162 | const { closeOnMouseLeave, closePopup } = this.props; 163 | 164 | if (e.relatedTarget !== this.popupNode && e.relatedTarget !== this.inputNode) { 165 | if (closeOnMouseLeave) { 166 | invoke(this.props, 'onMouseLeave', e, ...args); 167 | this.mouseLeaveTimeout = window.setTimeout(() => { 168 | if (this.mouseLeaveTimeout) { 169 | closePopup(); 170 | } 171 | }, 500); 172 | } 173 | } 174 | } 175 | 176 | public onMouseEnter = (e, ...args) => { 177 | const { closeOnMouseLeave } = this.props; 178 | 179 | invoke(this.props, 'onMouseEnter', e, ...args); 180 | if (e.currentTarget === this.popupNode || e.currentTarget === this.inputNode) { 181 | if (closeOnMouseLeave) { 182 | clearTimeout(this.mouseLeaveTimeout); 183 | this.mouseLeaveTimeout = null; 184 | } 185 | } 186 | } 187 | 188 | public render() { 189 | const { 190 | renderPicker, 191 | popupPosition, 192 | inline, 193 | value, 194 | closeOnMouseLeave, 195 | onChange, 196 | onClear, 197 | children, 198 | inlineLabel, 199 | popupIsClosed, 200 | mountNode, 201 | tabIndex, 202 | onMount, 203 | closePopup, 204 | openPopup, 205 | animation, 206 | duration, 207 | pickerWidth, 208 | pickerStyle, 209 | iconPosition, 210 | icon, 211 | readOnly, 212 | hideMobileKeyboard, 213 | ...rest 214 | } = this.props; 215 | 216 | const inputElement = ( 217 | { this.inputNode = e; onMount(e); }} 224 | value={value} 225 | tabIndex={tabIndex} 226 | inline={inlineLabel} 227 | onClear={(e) => (onClear || onChange)(e, { ...rest, value: '' })} 228 | onFocus={(e) => { 229 | invoke(this.props, 'onFocus', e, this.props); 230 | openPopup(); 231 | }} 232 | onBlur={this.onBlur} 233 | onMouseEnter={this.onMouseEnter} 234 | onChange={onChange} /> 235 | ); 236 | 237 | if (inline) { 238 | return renderPicker(); 239 | } 240 | 241 | return (<> 242 | {inputElement} 243 | { 244 | !readOnly 245 | && 246 | { 253 | if (popupIsClosed) { 254 | this.unsetScrollListener(); 255 | // TODO: for some reason sometimes transition component 256 | // doesn't hide even though `popupIsClosed === true` 257 | // To hide it we need to rerender component 258 | this.forceUpdate(); 259 | } else { 260 | this.setScrollListener(); 261 | } 262 | }} 263 | > 264 | 274 |
this.popupNode = ref} 281 | > 282 | {renderPicker()} 283 |
284 |
285 |
286 | } 287 | 288 | ); 289 | } 290 | 291 | public scrollListener = () => { 292 | const { closePopup } = this.props; 293 | closePopup(); 294 | } 295 | 296 | private setScrollListener() { 297 | window.addEventListener('scroll', this.scrollListener); 298 | } 299 | 300 | private unsetScrollListener() { 301 | window.removeEventListener('scroll', this.scrollListener); 302 | } 303 | } 304 | 305 | export default InputView; 306 | -------------------------------------------------------------------------------- /src/views/MinuteView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import BaseCalendarView, { 4 | BaseCalendarViewProps, 5 | CalendarWithOptionalHeaderViewProps, 6 | SingleSelectionCalendarViewProps, 7 | } from './BaseCalendarView'; 8 | import Calendar from './Calendar'; 9 | import Body from './CalendarBody/Body'; 10 | import Header, { HeaderProps } from './CalendarHeader/Header'; 11 | 12 | import { findHTMLElement } from '../lib'; 13 | 14 | const MINUTE_CALENDAR_ROW_WIDTH = 3; 15 | 16 | type MinuteViewProps = 17 | BaseCalendarViewProps 18 | & SingleSelectionCalendarViewProps 19 | & CalendarWithOptionalHeaderViewProps; 20 | 21 | class MinuteView extends BaseCalendarView { 22 | public render() { 23 | const { 24 | values, 25 | hasHeader, 26 | onValueClick, 27 | onNextPageBtnClick, 28 | onPrevPageBtnClick, 29 | hasNextPage, 30 | hasPrevPage, 31 | onHeaderClick, 32 | activeItemIndex, 33 | currentHeadingValue, 34 | hoveredItemIndex, 35 | disabledItemIndexes, 36 | onCellHover, 37 | onMount, 38 | inline, 39 | localization, 40 | ...rest 41 | } = this.props; 42 | const headerProps: HeaderProps = { 43 | className: 'suicr-minute-view-header', 44 | onHeaderClick, 45 | onNextPageBtnClick, 46 | onPrevPageBtnClick, 47 | hasNextPage, 48 | hasPrevPage, 49 | title: currentHeadingValue, 50 | width: MINUTE_CALENDAR_ROW_WIDTH, 51 | displayWeeks: false, 52 | localization, 53 | }; 54 | 55 | return ( 56 | this.calendarNode = findHTMLElement(e)} outlineOnFocus={inline} {...rest}> 57 | { hasHeader &&
} 58 | 66 | 67 | ); 68 | } 69 | } 70 | 71 | export default MinuteView; 72 | -------------------------------------------------------------------------------- /src/views/MonthRangeView.tsx: -------------------------------------------------------------------------------- 1 | import range from 'lodash/range'; 2 | import isNil from 'lodash/isNil'; 3 | 4 | import * as React from 'react'; 5 | import {findHTMLElement} from '../lib'; 6 | 7 | import BaseCalendarView, { 8 | BaseCalendarViewProps, 9 | CalendarWithHeaderViewProps, 10 | HeadingValueProps, 11 | RangeSelectionCalendarViewProps, 12 | } from './BaseCalendarView'; 13 | 14 | import Calendar from './Calendar'; 15 | import Body from './CalendarBody/Body'; 16 | import Header from './CalendarHeader/Header'; 17 | 18 | import { MONTH_CALENDAR_ROW_WIDTH } from './MonthView'; 19 | 20 | type MonthRangeViewProps = 21 | BaseCalendarViewProps 22 | & HeadingValueProps 23 | & RangeSelectionCalendarViewProps 24 | & CalendarWithHeaderViewProps; 25 | 26 | const MONTH_POSITIONS = range(12); 27 | 28 | function getActive(start: number, end: number): number | number[] | undefined { 29 | if (isNil(start) && isNil(end)) { 30 | return; 31 | } 32 | if (!isNil(start) && isNil(end)) { 33 | return start; 34 | } 35 | if (!isNil(start) && !isNil(end)) { 36 | return MONTH_POSITIONS.slice(start, end + 1); 37 | } 38 | } 39 | 40 | class MonthRangeView extends BaseCalendarView { 41 | public static defaultProps = { 42 | active: { 43 | start: undefined, 44 | end: undefined, 45 | }, 46 | }; 47 | 48 | public render() { 49 | const { 50 | values, 51 | onNextPageBtnClick, 52 | onPrevPageBtnClick, 53 | onValueClick, 54 | hasPrevPage, 55 | hasNextPage, 56 | currentHeadingValue, 57 | onHeaderClick, 58 | activeRange, 59 | disabledItemIndexes, 60 | currentRangeHeadingValue, 61 | hoveredItemIndex, 62 | onCellHover, 63 | onMount, 64 | inline, 65 | localization, 66 | ...rest 67 | } = this.props; 68 | const { 69 | start, 70 | end, 71 | } = activeRange; 72 | 73 | return ( 74 | this.calendarNode = findHTMLElement(e)} outlineOnFocus={inline} {...rest}> 75 |
87 | 95 | 96 | ); 97 | } 98 | } 99 | 100 | export default MonthRangeView; 101 | -------------------------------------------------------------------------------- /src/views/MonthView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import BaseCalendarView, { 4 | BaseCalendarViewProps, 5 | CalendarWithOptionalHeaderViewProps, 6 | SingleSelectionCalendarViewProps, 7 | } from './BaseCalendarView'; 8 | import Calendar from './Calendar'; 9 | import Body from './CalendarBody/Body'; 10 | import Header, { HeaderProps } from './CalendarHeader/Header'; 11 | 12 | import { findHTMLElement } from '../lib'; 13 | 14 | export const MONTH_CALENDAR_ROW_WIDTH = 3; 15 | 16 | type MonthViewProps = 17 | BaseCalendarViewProps 18 | & SingleSelectionCalendarViewProps 19 | & CalendarWithOptionalHeaderViewProps; 20 | 21 | class MonthView extends BaseCalendarView { 22 | public render() { 23 | const { 24 | values, 25 | hasHeader, 26 | onValueClick, 27 | onNextPageBtnClick, 28 | onPrevPageBtnClick, 29 | hasPrevPage, 30 | hasNextPage, 31 | onHeaderClick, 32 | disabledItemIndexes, 33 | activeItemIndex, 34 | currentHeadingValue, 35 | onCellHover, 36 | hoveredItemIndex, 37 | onMount, 38 | inline, 39 | localization, 40 | ...rest 41 | } = this.props; 42 | const headerProps: HeaderProps = { 43 | className: 'suicr-month-view-header', 44 | onNextPageBtnClick, 45 | onPrevPageBtnClick, 46 | hasPrevPage, 47 | hasNextPage, 48 | onHeaderClick, 49 | title: currentHeadingValue, 50 | displayWeeks: false, 51 | width: MONTH_CALENDAR_ROW_WIDTH, 52 | localization, 53 | }; 54 | 55 | return ( 56 | this.calendarNode = findHTMLElement(e)} outlineOnFocus={inline} {...rest}> 57 | { hasHeader &&
} 58 | 66 | 67 | ); 68 | } 69 | } 70 | 71 | export default MonthView; 72 | -------------------------------------------------------------------------------- /src/views/YearView.tsx: -------------------------------------------------------------------------------- 1 | import last from 'lodash/last'; 2 | import first from 'lodash/first'; 3 | 4 | import * as React from 'react'; 5 | 6 | import BaseCalendarView, { 7 | BaseCalendarViewProps, 8 | CalendarWithHeaderViewProps, 9 | SingleSelectionCalendarViewProps, 10 | } from './BaseCalendarView'; 11 | import Calendar from './Calendar'; 12 | import Body from './CalendarBody/Body'; 13 | import Header from './CalendarHeader/Header'; 14 | 15 | import { findHTMLElement } from '../lib'; 16 | 17 | const YEAR_CALENDAR_ROW_WIDTH = 3; 18 | 19 | type YearViewProps = 20 | BaseCalendarViewProps 21 | & SingleSelectionCalendarViewProps 22 | & CalendarWithHeaderViewProps; 23 | 24 | class YearView extends BaseCalendarView { 25 | public render() { 26 | const { 27 | values, 28 | onNextPageBtnClick, 29 | onPrevPageBtnClick, 30 | onValueClick, 31 | hasNextPage, 32 | hasPrevPage, 33 | onHeaderClick, 34 | disabledItemIndexes, 35 | activeItemIndex, 36 | hoveredItemIndex, 37 | onCellHover, 38 | onMount, 39 | inline, 40 | localization, 41 | ...rest 42 | } = this.props; 43 | const headerTitle = `${first(values)} - ${last(values)}`; 44 | 45 | return ( 46 | this.calendarNode = findHTMLElement(e)} outlineOnFocus={inline} {...rest}> 47 |
58 | 66 | 67 | ); 68 | } 69 | } 70 | 71 | export default YearView; 72 | -------------------------------------------------------------------------------- /test/.env: -------------------------------------------------------------------------------- 1 | TS_NODE_COMPILER_OPTIONS={"allowJs":true} 2 | -------------------------------------------------------------------------------- /test/inputs/testDateInput.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { 3 | mount 4 | } from 'enzyme'; 5 | import sinon from 'sinon'; 6 | import React from 'react'; 7 | import _ from 'lodash'; 8 | 9 | import DateInput from '../../src/inputs/DateInput'; 10 | 11 | describe(': handleSelect', () => { 12 | it('call `onChange` when in `day` mode (default)', () => { 13 | const onChangeFake = sinon.fake(); 14 | const wrapper = mount(); 17 | 18 | wrapper.instance().handleSelect('click', { value: { year: 2030, month: 4, date: 3 } }); 19 | const calledWithArgs = onChangeFake.args[0]; 20 | 21 | assert(onChangeFake.calledOnce, '`onChange` callback called once'); 22 | assert.equal(calledWithArgs[0], 'click', 'correct first argument'); 23 | assert(_.isString(calledWithArgs[1].value), 'value is string'); 24 | assert.equal(calledWithArgs[1].value, '2030-05-03', 'correct value'); 25 | }); 26 | 27 | // TODO: skipped because now mode switches in callback 28 | it.skip('switch to next mode if not in day mode', () => { 29 | const onChangeFake = sinon.fake(); 30 | const wrapper = mount(); 34 | 35 | assert.equal(wrapper.state('mode'), 'year', 'mode not switched yet'); 36 | wrapper.instance().handleSelect('click', { value: { year: 2030 } }); 37 | assert.equal(wrapper.state('mode'), 'month', 'switched to next mode'); 38 | }); 39 | 40 | // TODO: skipped because now mode switches in callback 41 | it.skip('does not switch to next mode if in day mode', () => { 42 | const onChangeFake = sinon.fake(); 43 | const wrapper = mount(); 47 | 48 | assert.equal(wrapper.state('mode'), 'day', 'mode not switched yet'); 49 | wrapper.instance().handleSelect('click', { value: { year: 2030 } }); 50 | assert.equal(wrapper.state('mode'), 'day', 'mode still not switched'); 51 | }); 52 | 53 | it('does not call `onChange` when not in `day` mode', () => { 54 | const onChangeFake = sinon.fake(); 55 | const wrapper = mount(); 59 | 60 | wrapper.instance().handleSelect('click', { value: { year: 2030 } }); 61 | 62 | assert.isFalse(onChangeFake.calledOnce, '`onChange` callback is not called'); 63 | }); 64 | }); 65 | 66 | describe(': switchToPrevMode', () => { 67 | const wrapper = mount(); 68 | 69 | beforeEach(function(done) { 70 | setTimeout(done); 71 | }, 0); 72 | 73 | it('not yet switched to previous mode', () => { 74 | assert.equal(wrapper.state('mode'), 'day', 'mode is not changed yet'); 75 | wrapper.instance().switchToPrevMode(); 76 | }); 77 | 78 | it('switched to prev mode', () => { 79 | assert.equal(wrapper.state('mode'), 'month', 'mode changed to previous'); 80 | }).timeout(0); 81 | }); 82 | 83 | describe(': switchToNextMode', () => { 84 | const wrapper = mount(); 85 | 86 | beforeEach(function(done) { 87 | setTimeout(done); 88 | }, 0); 89 | 90 | it('not yet switched to next mode', () => { 91 | assert.equal(wrapper.state('mode'), 'day', 'mode is not changed yet'); 92 | wrapper.instance().switchToNextMode(); 93 | }); 94 | 95 | it('switched to next mode', () => { 96 | assert.equal(wrapper.state('mode'), 'year', 'mode changed to next'); 97 | }).timeout(0); 98 | }); 99 | -------------------------------------------------------------------------------- /test/inputs/testMonthInput.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { 3 | mount, 4 | } from 'enzyme'; 5 | import * as sinon from 'sinon'; 6 | import * as React from 'react'; 7 | import * as _ from 'lodash'; 8 | import MonthInput from '../../src/inputs/MonthInput'; 9 | 10 | describe(': handleSelect', () => { 11 | it('call `onChange`', () => { 12 | const onChangeFake = sinon.fake(); 13 | const wrapper = mount(); 15 | 16 | wrapper.instance().handleSelect('click', { value: { month: 5 } }); 17 | const calledWithArgs = onChangeFake.args[0]; 18 | 19 | assert(onChangeFake.calledOnce, '`onChange` callback called once'); 20 | assert.equal(calledWithArgs[0], 'click', 'correct first argument'); 21 | assert(_.isString(calledWithArgs[1].value), 'value is string'); 22 | assert.equal(calledWithArgs[1].value, 'Jun', 'correct value'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/inputs/testMonthRangeInput.js: -------------------------------------------------------------------------------- 1 | import {assert} from "chai"; 2 | import { 3 | mount 4 | } from "enzyme"; 5 | import moment from "moment"; 6 | import * as sinon from "sinon"; 7 | import * as React from "react"; 8 | import * as _ from "lodash"; 9 | import MonthRangeInput from "../../src/inputs/MonthRangeInput"; 10 | 11 | describe(": handleSelect_from", () => { 12 | it("call `onChange`", () => { 13 | const onChangeFake = sinon.fake(); 14 | const wrapper = mount(); 15 | const currentDate = new Date(); 16 | const currentYear = currentDate.getFullYear(); 17 | 18 | wrapper.instance().handleSelect("click", {value: {start: moment().set("month", 2)}}); 19 | const firstCalledWithArgs = onChangeFake.args[0]; 20 | 21 | assert(onChangeFake.calledOnce, "`onChange` callback called once"); 22 | assert.equal(firstCalledWithArgs[0], "click", "correct first argument"); 23 | assert(_.isString(firstCalledWithArgs[1].value), "value is string"); 24 | assert.equal(firstCalledWithArgs[1].value, `03-${currentYear} - `, "correct value"); 25 | }); 26 | }); 27 | 28 | describe(": handleSelect_from_to", ()=>{ 29 | it("call `onChange`", () => { 30 | const onChangeFake = sinon.fake(); 31 | const wrapper = mount(); 32 | const currentDate = new Date(); 33 | const currentYear = currentDate.getFullYear(); 34 | 35 | wrapper.instance().handleSelect("click", {value: {start: moment().set("month", 2), end: moment().set("month", 5)}}); 36 | const secondCalledWithArgs = onChangeFake.args[0]; 37 | 38 | assert(onChangeFake.calledOnce, "`onChange` callback called twice"); 39 | assert.equal(secondCalledWithArgs[0], "click", "correct first argument"); 40 | assert(_.isString(secondCalledWithArgs[1].value), "value is string"); 41 | assert.equal(secondCalledWithArgs[1].value, `03-${currentYear} - 06-${currentYear}`, "correct value"); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/inputs/testParse.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import * as _ from 'lodash'; 3 | import moment from 'moment'; 4 | 5 | import { 6 | getInitializer, 7 | parseValue, 8 | dateValueToString, 9 | } from '../../src/inputs/parse'; 10 | 11 | describe('getInitializer', () => { 12 | const dateFormat = 'YYYY-MM-DD HH:mm'; 13 | 14 | describe('`dateParams` param provided', () => { 15 | it('return valid moment created from `dateParams`', () => { 16 | const dateParams = { 17 | year: 2018, 18 | month: 4, 19 | date: 15, 20 | hour: 14, 21 | minute: 12, 22 | }; 23 | assert(moment.isMoment(getInitializer({ dateFormat, dateParams })), 'return moment'); 24 | assert(getInitializer({ dateFormat, dateParams }).isValid(), 'return valid moment'); 25 | assert( 26 | getInitializer({ dateFormat, dateParams }).isSame(moment(dateParams), 'minute'), 27 | 'return correct moment'); 28 | }); 29 | }); 30 | 31 | describe('`initialDate` param provided', () => { 32 | it('return valid moment created from `initialDate`', () => { 33 | const initialDate = '2018-05-15 14:12'; 34 | assert(moment.isMoment(getInitializer({ initialDate, dateFormat })), 'return moment'); 35 | assert(getInitializer({ initialDate, dateFormat }).isValid(), 'return valid moment'); 36 | assert( 37 | getInitializer({ initialDate, dateFormat }).isSame(moment(initialDate, dateFormat), 'minute'), 38 | 'return correct moment'); 39 | }); 40 | }); 41 | 42 | describe('`initialDate`, and `dateParams` params provided', () => { 43 | it('return valid moment created from `value`', () => { 44 | const value = '2018-05-15 14:12'; 45 | const initialDate = '2020-05-15 15:00'; 46 | const dateParams = { 47 | year: 2018, 48 | month: 4, 49 | date: 15, 50 | hour: 14, 51 | minute: 12, 52 | }; 53 | assert(moment.isMoment(getInitializer({ initialDate, dateFormat, dateParams })), 'return moment'); 54 | assert(getInitializer({ initialDate, dateFormat, dateParams }).isValid(), 'return valid moment'); 55 | assert( 56 | getInitializer({ initialDate, dateFormat, dateParams }).isSame(moment(value, dateFormat), 'minute'), 57 | 'return correct moment'); 58 | }); 59 | }); 60 | }); 61 | 62 | describe('parseValue', () => { 63 | describe('`value` param provided', () => { 64 | it('create moment from input string', () => { 65 | const value = 'Sep 2015'; 66 | const dateFormat = 'MMM YYYY'; 67 | const locale = 'en'; 68 | 69 | assert(moment.isMoment(parseValue(value, dateFormat, locale)), 'return moment instance'); 70 | assert(parseValue(value, dateFormat).isValid(), 'return valid moment instance'); 71 | assert(parseValue(value, dateFormat).isSame(moment('Sep 2015', 'MMM YYYY'), 'month'), 'return correct moment'); 72 | }); 73 | 74 | it('create moment from input Date', () => { 75 | const value = new Date('2015-02-15'); 76 | const dateFormat = 'does not matter if value is Date'; 77 | const locale = 'en'; 78 | 79 | const parsed = parseValue(value, dateFormat, locale); 80 | 81 | assert(moment.isMoment(parsed), 'return moment instance'); 82 | assert(parsed.isValid(), 'return valid moment instance'); 83 | assert(parsed.isSame(moment('2015-02-15', 'YYYY-MM-DD'), 'date'), 'return correct moment'); 84 | }); 85 | 86 | it('create moment from input Moment', () => { 87 | const value = moment('2015-02-15', 'YYYY-MM-DD'); 88 | const dateFormat = 'does not matter if value is Moment'; 89 | const locale = 'en'; 90 | 91 | const parsed = parseValue(value, dateFormat, locale); 92 | 93 | assert(moment.isMoment(parsed), 'return moment instance'); 94 | assert(parsed.isValid(), 'return valid moment instance'); 95 | assert(parsed.isSame(moment('2015-02-15', 'YYYY-MM-DD'), 'date'), 'return correct moment'); 96 | }); 97 | }); 98 | 99 | describe('`value` param is not provided', () => { 100 | it('return undefined', () => { 101 | const dateFormat = 'MMM'; 102 | const locale = 'en'; 103 | 104 | assert(_.isUndefined(parseValue(undefined, dateFormat, locale)), 'return undefined'); 105 | }); 106 | }); 107 | }); 108 | 109 | describe('dateValueToString()', () => { 110 | it('handles string input value', () => { 111 | const inputValue = '17-04-2030'; 112 | const dateFormat = 'DD-MM-YYYY'; 113 | const locale = 'en'; 114 | 115 | const producedValue = dateValueToString(inputValue, dateFormat, locale); 116 | 117 | assert(_.isString(producedValue), 'return string value'); 118 | assert.equal(producedValue, inputValue, 'return correct string'); 119 | }); 120 | 121 | it('handles Date input value', () => { 122 | const inputValue = new Date('2015-08-11'); 123 | const dateFormat = 'DD-MM-YYYY'; 124 | const locale = 'en'; 125 | 126 | const producedValue = dateValueToString(inputValue, dateFormat, locale); 127 | 128 | assert(_.isString(producedValue), 'return string value'); 129 | assert.equal(producedValue, '11-08-2015', 'return correct string'); 130 | }); 131 | 132 | it('handles Moment input value', () => { 133 | const inputValue = moment('2015-08-11', 'YYYY-MM-DD'); 134 | const dateFormat = 'DD-MM-YYYY'; 135 | const locale = 'en'; 136 | 137 | const producedValue = dateValueToString(inputValue, dateFormat, locale); 138 | 139 | assert(_.isString(producedValue), 'return string value'); 140 | assert.equal(producedValue, '11-08-2015', 'return correct string'); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /test/inputs/testYearInput.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { 3 | mount, 4 | } from 'enzyme'; 5 | import * as sinon from 'sinon'; 6 | import * as React from 'react'; 7 | import * as _ from 'lodash'; 8 | import YearInput from '../../src/inputs/YearInput'; 9 | 10 | describe(': handleSelect', () => { 11 | it('call `onChange`', () => { 12 | const onChangeFake = sinon.fake(); 13 | const wrapper = mount(); 15 | 16 | wrapper.instance().handleSelect('click', { value: { year: 2030 } }); 17 | const calledWithArgs = onChangeFake.args[0]; 18 | 19 | assert(onChangeFake.calledOnce, '`onChange` callback called once'); 20 | assert.equal(calledWithArgs[0], 'click', 'correct first argument'); 21 | assert(_.isString(calledWithArgs[1].value), 'value is string'); 22 | assert.equal(calledWithArgs[1].value, '2030', 'correct value'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/pickers/timePicker/testMinutePicker.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { 3 | mount, 4 | } from 'enzyme'; 5 | import * as sinon from 'sinon'; 6 | import * as React from 'react'; 7 | import * as _ from 'lodash'; 8 | import moment from 'moment'; 9 | 10 | import MinutePicker from '../../../src/pickers/timePicker/MinutePicker'; 11 | 12 | describe('', () => { 13 | it('initialized with moment', () => { 14 | const date = moment('2015-05-01'); 15 | const wrapper = mount(); 16 | assert( 17 | moment.isMoment(wrapper.state('date')), 18 | 'has moment instance in `date` state field'); 19 | assert( 20 | wrapper.state('date').isSame(date), 21 | 'initialize `date` state field with moment provided in `initializeWith` prop'); 22 | }); 23 | }); 24 | 25 | describe(': buildCalendarValues', () => { 26 | const date = moment('2018-08-12 15:00'); 27 | 28 | describe('`timeFormat` not provided', () => { 29 | it('return array of strings', () => { 30 | const wrapper = mount(); 31 | const shouldReturn = [ 32 | '15:00', '15:05', '15:10', '15:15', '15:20', '15:25', 33 | '15:30', '15:35', '15:40', '15:45', '15:50', '15:55', 34 | ]; 35 | assert(_.isArray(wrapper.instance().buildCalendarValues()), 'return array'); 36 | assert.equal(wrapper.instance().buildCalendarValues().length, 12, 'return array of length 12'); 37 | wrapper.instance().buildCalendarValues().forEach((minutePosition, i) => { 38 | assert.equal(minutePosition, shouldReturn[i], 'contains corect minute positions'); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('`timeFormat` is ampm', () => { 44 | it('return array of strings', () => { 45 | const wrapper = mount(); 48 | const shouldReturn = [ 49 | '03:00 pm', '03:05 pm', '03:10 pm', '03:15 pm', '03:20 pm', '03:25 pm', 50 | '03:30 pm', '03:35 pm', '03:40 pm', '03:45 pm', '03:50 pm', '03:55 pm', 51 | ]; 52 | assert(_.isArray(wrapper.instance().buildCalendarValues()), 'return array'); 53 | assert.equal(wrapper.instance().buildCalendarValues().length, 12, 'return array of length 12'); 54 | wrapper.instance().buildCalendarValues().forEach((minutePosition, i) => { 55 | assert.equal(minutePosition, shouldReturn[i], 'contains corect minute positions'); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('`timeFormat` is AMPM', () => { 61 | it('return array of strings', () => { 62 | const wrapper = mount(); 65 | const shouldReturn = [ 66 | '03:00 PM', '03:05 PM', '03:10 PM', '03:15 PM', '03:20 PM', '03:25 PM', 67 | '03:30 PM', '03:35 PM', '03:40 PM', '03:45 PM', '03:50 PM', '03:55 PM', 68 | ]; 69 | assert(_.isArray(wrapper.instance().buildCalendarValues()), 'return array'); 70 | assert.equal(wrapper.instance().buildCalendarValues().length, 12, 'return array of length 12'); 71 | wrapper.instance().buildCalendarValues().forEach((minutePosition, i) => { 72 | assert.equal(minutePosition, shouldReturn[i], 'contains corect minute positions'); 73 | }); 74 | }); 75 | }); 76 | }); 77 | 78 | describe(': getActiveCellPosition', () => { 79 | const date = moment('2018-08-12 10:00'); 80 | 81 | it('return active minute position when value is not multiple of 5', () => { 82 | const wrapper = mount(); 85 | /* 86 | [ 87 | '10:00', '10:05', '10:10', '10:15', '10:20', '10:25', 88 | '10:30', '10:35', '10:40', '10:45', '10:50', '10:55', 89 | ] 90 | */ 91 | assert(_.isNumber(wrapper.instance().getActiveCellPosition()), 'return number'); 92 | assert.equal(wrapper.instance().getActiveCellPosition(), 3, 'return active minute position number'); 93 | }); 94 | 95 | it('return active minute position when value is multiple of 5', () => { 96 | const wrapper = mount(); 99 | /* 100 | [ 101 | '10:00', '10:05', '10:10', '10:15', '10:20', '10:25', 102 | '10:30', '10:35', '10:40', '10:45', '10:50', '10:55', 103 | ] 104 | */ 105 | assert(_.isNumber(wrapper.instance().getActiveCellPosition()), 'return number'); 106 | assert.equal(wrapper.instance().getActiveCellPosition(), 4, 'return active minute position number'); 107 | }); 108 | 109 | it('return active minute position when value is 59', () => { 110 | const wrapper = mount(); 113 | /* 114 | [ 115 | '10:00', '10:05', '10:10', '10:15', '10:20', '10:25', 116 | '10:30', '10:35', '10:40', '10:45', '10:50', '10:55', 117 | ] 118 | */ 119 | assert(_.isNumber(wrapper.instance().getActiveCellPosition()), 'return number'); 120 | assert.equal(wrapper.instance().getActiveCellPosition(), 11, 'return active minute position number'); 121 | }); 122 | 123 | it('return undefined when value is not provided', () => { 124 | const wrapper = mount(); 126 | assert(_.isUndefined(wrapper.instance().getActiveCellPosition()), 'return undefined'); 127 | }); 128 | }); 129 | 130 | describe(': isNextPageAvailable', () => { 131 | const date = moment('2018-08-12'); 132 | 133 | describe('is not available by maxDate', () => { 134 | it('return false', () => { 135 | const wrapper = mount(); 138 | 139 | assert(_.isBoolean(wrapper.instance().isNextPageAvailable()), 'return boolean'); 140 | assert.isFalse(wrapper.instance().isNextPageAvailable(), 'return false'); 141 | }); 142 | }); 143 | 144 | describe('available by maxDate', () => { 145 | it('return true', () => { 146 | const wrapper = mount(); 149 | 150 | assert(_.isBoolean(wrapper.instance().isNextPageAvailable()), 'return boolean'); 151 | assert.isTrue(wrapper.instance().isNextPageAvailable(), 'return true'); 152 | }); 153 | }); 154 | }); 155 | 156 | describe(': isPrevPageAvailable', () => { 157 | const date = moment('2018-08-12'); 158 | 159 | describe('is not available by minDate', () => { 160 | it('return false', () => { 161 | const wrapper = mount(); 164 | 165 | assert(_.isBoolean(wrapper.instance().isPrevPageAvailable()), 'return boolean'); 166 | assert.isFalse(wrapper.instance().isPrevPageAvailable(), 'return false'); 167 | }); 168 | }); 169 | 170 | describe('available by minDate', () => { 171 | it('return true', () => { 172 | const wrapper = mount(); 175 | 176 | assert(_.isBoolean(wrapper.instance().isPrevPageAvailable()), 'return boolean'); 177 | assert.isTrue(wrapper.instance().isPrevPageAvailable(), 'return true'); 178 | }); 179 | }); 180 | }); 181 | 182 | describe(': getCurrentDate', () => { 183 | const date = moment('2018-08-12'); 184 | 185 | it('return string in format `MMMM DD, YYYY`', () => { 186 | const wrapper = mount(); 188 | 189 | assert(_.isString(wrapper.instance().getCurrentDate()), 'return string'); 190 | assert.equal(wrapper.instance().getCurrentDate(), date.format('MMMM DD, YYYY'), 'return proper value'); 191 | }); 192 | }); 193 | 194 | describe(': handleChange', () => { 195 | const date = moment('2018-08-12 10:00'); 196 | 197 | it('call onChangeFake with { year: number, month: number, date: number, hour: number }', () => { 198 | const onChangeFake = sinon.fake(); 199 | const wrapper = mount(); 202 | const possibleValues = wrapper.instance().buildCalendarValues(); 203 | /* 204 | [ 205 | '**:00', '**:05', '**:10', '**:15', '**:20', '**:25', 206 | '**:30', '**:35', '**:40', '**:45', '**:50', '**:55', 207 | ] 208 | */ 209 | wrapper.instance().handleChange('click', { value: possibleValues[8]}); 210 | const calledWithArgs = onChangeFake.args[0]; 211 | 212 | assert(onChangeFake.calledOnce, 'onChangeFake called once'); 213 | assert.equal(calledWithArgs[0], 'click', 'correct first argument'); 214 | assert.equal(calledWithArgs[1].value.year, 2018, 'correct year'); 215 | assert.equal(calledWithArgs[1].value.month, 7, 'correct month'); 216 | assert.equal(calledWithArgs[1].value.date, 12, 'correct date'); 217 | assert.equal(calledWithArgs[1].value.hour, 10, 'correct hour'); 218 | assert.equal(calledWithArgs[1].value.minute, 40, 'correct hour'); 219 | }); 220 | }); 221 | 222 | describe(': switchToNextPage', () => { 223 | const date = moment('2018-08-12'); 224 | 225 | it('shift `date` state field one day forward', () => { 226 | const wrapper = mount(); 228 | 229 | assert.equal(wrapper.state('date').date(), 12, 'date not changed yet'); 230 | wrapper.instance().switchToNextPage(); 231 | assert.equal(wrapper.state('date').date(), 12 + 1, 'date shifted one day forward'); 232 | }); 233 | }); 234 | 235 | describe(': switchToPrevPage', () => { 236 | const date = moment('2018-08-12'); 237 | 238 | it('shift `date` state field one day backward', () => { 239 | const wrapper = mount(); 241 | 242 | assert.equal(wrapper.state('date').date(), 12, 'date not changed yet'); 243 | wrapper.instance().switchToPrevPage(); 244 | assert.equal(wrapper.state('date').date(), 12 - 1, 'date shifted one day backward'); 245 | }); 246 | }); 247 | 248 | describe(': getSelectableCellPositions', () => { 249 | const date = moment('2018-08-12 10:00'); 250 | 251 | it('return minutes positions that are >= `minDate`', () => { 252 | const wrapper = mount(); 255 | const expected = [ 3, 4, 5, 6, 7, 8, 9, 10, 11 ]; 256 | const actual = wrapper.instance().getSelectableCellPositions(); 257 | 258 | assert.equal(actual.length, expected.length); 259 | expected.forEach((expectPos, i) => { 260 | assert.equal(expectPos, actual[i]); 261 | }); 262 | }); 263 | }); 264 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | import * as moment from 'moment'; 3 | 4 | import { configure } from 'enzyme'; 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | 7 | configure({ adapter: new Adapter() }); 8 | 9 | moment.locale('en'); 10 | 11 | const { window } = new JSDOM('', { 12 | url: 'https://example.com', 13 | }); 14 | 15 | function copyProps(src, target) { 16 | const props = Object.getOwnPropertyNames(src) 17 | .filter(prop => typeof target[prop] === 'undefined') 18 | .reduce((result, prop) => ({ 19 | ...result, 20 | [prop]: Object.getOwnPropertyDescriptor(src, prop), 21 | }), {}); 22 | Object.defineProperties(target, props); 23 | } 24 | 25 | global.window = window; 26 | global.document = window.document; 27 | copyProps(window, global); 28 | -------------------------------------------------------------------------------- /test/views/testHeader.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import * as Enzyme from 'enzyme'; 3 | import * as Adapter from 'enzyme-adapter-react-16'; 4 | import moment from 'moment'; 5 | import { 6 | shallow, 7 | } from 'enzyme'; 8 | import * as sinon from 'sinon'; 9 | import * as React from 'react'; 10 | import { 11 | Table, 12 | Icon, 13 | } from 'semantic-ui-react'; 14 | import * as _ from 'lodash'; 15 | 16 | import { 17 | Header, 18 | HeaderRange, 19 | HeaderWeeks, 20 | } from '../../src/views/CalendarHeader'; 21 | 22 | describe('', () => { 23 | it('consists of proper elements', () => { 24 | const wrapper = shallow(); 25 | assert(wrapper.is(Table.Row), 'the top node is '); 26 | assert(wrapper.children().every(Table.HeaderCell), 'top node contains nodes: '); 27 | assert.equal(wrapper.children().getElements().length, 7, 'top node contains 7 nodes'); 28 | }); 29 | 30 | it('has proper localization', () => { 31 | const wrapper = shallow(); 32 | const weekArray = wrapper.children().getElements().map(element => moment(element.key, 'ddd dddd', 'ru').isValid()); 33 | assert.notInclude(weekArray, false, 'days of the week parsed correctly'); 34 | assert.equal(weekArray.length, 7, 'top node contains 7 nodes'); 35 | }); 36 | 37 | it('has proper styling', () => { 38 | const wrapper = shallow(); 39 | wrapper.children().forEach((child) => { 40 | const style = child.prop('style'); 41 | assert.equal(_.keys(style).length, 2, 'each child of top node has prop style: { border, borderBottom }'); 42 | assert.equal( 43 | style.border, 44 | 'none', 45 | 'each child of top node has prop style: { border: "none" }'); 46 | assert.equal( 47 | style.borderBottom, 48 | '1px solid rgba(34,36,38,.1)', 49 | 'each child of top node has prop style: { borderBottom: "1px solid rgba(34,36,38,.1)" }'); 50 | }); 51 | }); 52 | }); 53 | 54 | describe('', () => { 55 | it('consists of proper elements and sets its content correctly', () => { 56 | const wrapper = shallow(); 57 | assert(wrapper.is(Table.Row), 'the top node is '); 58 | assert(wrapper.children().first().is(Table.HeaderCell), 'top node contains '); 59 | assert.equal(wrapper.children().getElements().length, 1, 'top node contains just one '); 60 | assert.equal(wrapper.find(Table.HeaderCell).children().first().text(), 'any text', 'uses `content` prop as it should'); 61 | }); 62 | 63 | it('has proper styling', () => { 64 | const wrapper = shallow(); 65 | wrapper.children().forEach((child) => { 66 | const style = child.prop('style'); 67 | assert.equal(_.keys(style).length, 1, 'each child of top node has prop style: { border: "none" }'); 68 | assert.equal(style.border, 'none', 'each child of top node has prop style: { border: "none" }'); 69 | }); 70 | const style = wrapper.children().first().prop('style'); 71 | assert.equal(_.keys(style).length, 1, 'top node\'s child has prop style: { border: "none" }'); 72 | assert.equal(style.border, 'none', 'top node\'s child has prop style: { border: "none" }'); 73 | }); 74 | }); 75 | 76 | describe('
', () => { 77 | it('consists of proper elements', () => { 78 | const wrapper = shallow( 79 |
{}} 86 | onPrevPageBtnClick={() => {}} /> 87 | ); 88 | assert(wrapper.is(Table.Header), 'top node is Table.Header'); 89 | assert.equal(wrapper.find(Table.Row).getElements().length, 1, 'has one '); 90 | assert.equal(wrapper.find(HeaderWeeks).getElements().length, 1, 'has one '); 91 | assert.isFalse(wrapper.find(HeaderRange).exists(), 'does not have '); 92 | }); 93 | 94 | it('sets title properly', () => { 95 | const wrapper = shallow( 96 |
{}} 103 | onPrevPageBtnClick={() => {}} /> 104 | ); 105 | assert.equal(wrapper.find(Table.HeaderCell) 106 | .at(1).children().first().text(), 'any text', 'node contains value from `title` prop'); 107 | }); 108 | 109 | it('does not display weeks row if `displayWeeks` is false', () => { 110 | const wrapper = shallow( 111 |
{}} 118 | onPrevPageBtnClick={() => {}} /> 119 | ); 120 | assert.isFalse(wrapper.find(HeaderWeeks).exists(), 'does not have '); 121 | }); 122 | 123 | it('display range row if `rangeRowContent` provided', () => { 124 | const wrapper = shallow( 125 |
{}} 132 | onPrevPageBtnClick={() => {}} 133 | rangeRowContent="any text" /> 134 | ); 135 | assert(wrapper.find(HeaderRange).exists(), 'has '); 136 | }); 137 | 138 | it('sets central cell colSpan to 5 if `width` 7', () => { 139 | const wrapper = shallow( 140 |
{}} 147 | onPrevPageBtnClick={() => {}} /> 148 | ); 149 | assert.equal(wrapper.find(Table.HeaderCell) 150 | .at(1).prop('colSpan'), (7 - 2).toString(), 'central cell colSpan === (7 - 2)'); 151 | }); 152 | 153 | it('sets central cell colSpan to 2 if `width` 4', () => { 154 | const wrapper = shallow( 155 |
{}} 162 | onPrevPageBtnClick={() => {}} /> 163 | ); 164 | assert.equal(wrapper.find(Table.HeaderCell) 165 | .at(1).prop('colSpan'), (4 - 2).toString(), 'central cell colSpan === (4 - 2)'); 166 | }); 167 | 168 | it('sets central cell colSpan to 1 if `width` 3', () => { 169 | const wrapper = shallow( 170 |
{}} 177 | onPrevPageBtnClick={() => {}} /> 178 | ); 179 | assert.equal(wrapper.find(Table.HeaderCell) 180 | .at(1).prop('colSpan'), (3 - 2).toString(), 'central cell colSpan === (3 - 2)'); 181 | }); 182 | 183 | it('calls onPrevPageBtnClick', () => { 184 | const onPrevPageBtnClick = sinon.fake(); 185 | const onNextPageBtnClick = sinon.fake(); 186 | const wrapper = shallow( 187 |
195 | ); 196 | const prevBtn = wrapper.find(Icon).first(); 197 | prevBtn.simulate('click'); 198 | assert(onPrevPageBtnClick.calledOnce, 'onPrevPageBtnClick is called'); 199 | }); 200 | 201 | it('calls onNextPageBtnClick', () => { 202 | const onPrevPageBtnClick = sinon.fake(); 203 | const onNextPageBtnClick = sinon.fake(); 204 | const wrapper = shallow( 205 |
213 | ); 214 | const nextBtn = wrapper.find(Icon).last(); 215 | nextBtn.simulate('click'); 216 | assert(onNextPageBtnClick.calledOnce, 'onNextPageBtnClick is called'); 217 | }); 218 | 219 | it('does not call onNextPageBtnClick if it has not next page', () => { 220 | const onPrevPageBtnClick = sinon.fake(); 221 | const onNextPageBtnClick = sinon.fake(); 222 | const wrapper = shallow( 223 |
231 | ); 232 | const nextBtn = wrapper.find(Icon).last(); 233 | nextBtn.simulate('click'); 234 | assert(onNextPageBtnClick.notCalled, 'onNextPageBtnClick is not called'); 235 | }); 236 | 237 | it('does not call onPrevPageBtnClick if it has not previous page', () => { 238 | const onPrevPageBtnClick = sinon.fake(); 239 | const onNextPageBtnClick = sinon.fake(); 240 | const wrapper = shallow( 241 |
249 | ); 250 | const prevBtn = wrapper.find(Icon).first(); 251 | prevBtn.simulate('click'); 252 | assert(onPrevPageBtnClick.notCalled, 'onPrevPageBtnClick is not called'); 253 | }); 254 | 255 | it('calls onHeaderClick after clicking on header if `onHeaderClick` prop provided', () => { 256 | const onHeaderClick = sinon.fake(); 257 | const wrapper = shallow( 258 |
{}} 266 | onPrevPageBtnClick={() => {}} /> 267 | ); 268 | wrapper.find(Table.HeaderCell).at(1).simulate('click'); 269 | assert(onHeaderClick.calledOnce, 'onHeaderClick is called once'); 270 | }); 271 | }); 272 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "moduleResolution": "node", 6 | "baseUrl": "./", 7 | "outDir": "./dist", 8 | "allowJs": false, 9 | "target": "es5", 10 | "jsx": "react", 11 | "lib": [ 12 | "es2015", 13 | "dom" 14 | ], 15 | "module": "commonjs" 16 | }, 17 | "include": [ 18 | "./src/**/*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "indent": [true, "spaces", 2], 8 | "newline-before-return": true, 9 | "quotemark": [true, "single"], 10 | "object-literal-sort-keys": false, 11 | "trailing-comma": [ 12 | true, 13 | { 14 | "multiline": "always", 15 | "singleline": "never", 16 | "esSpecCompliant": true 17 | } 18 | ] 19 | }, 20 | "rules": { 21 | "indent": [true, "spaces", 2], 22 | "newline-before-return": true, 23 | "quotemark": [true, "single"], 24 | "object-literal-sort-keys": false, 25 | "max-classes-per-file": false, 26 | "no-trailing-whitespace": [ true, "ignore-blank-lines"], 27 | "trailing-comma": [ 28 | true, 29 | { 30 | "multiline": "always", 31 | "singleline": "never", 32 | "esSpecCompliant": true 33 | } 34 | ], 35 | "interface-name": [true, "never-prefix"], 36 | "ordered-imports": false, 37 | "variable-name": [true, "allow-leading-underscore", "allow-pascal-case"] 38 | }, 39 | "rulesDirectory": [] 40 | } 41 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | const config = { 5 | entry: { 6 | calendar: './example/calendar.tsx', 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, 'example'), 10 | filename: '[name].bundle.js', 11 | }, 12 | mode: 'development', 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.tsx?$/, 17 | exclude: /node-modules/, 18 | loader: 'ts-loader', 19 | }, 20 | { 21 | test: /\.jsx?$/, 22 | exclude: /node-modules/, 23 | loader: 'babel-loader', 24 | }, 25 | ], 26 | }, 27 | resolve: { 28 | extensions: ['.webpack.js', '.web.js', '.ts', '.tsx', '.js', 'jsx'], 29 | }, 30 | devServer: { 31 | contentBase: path.resolve(__dirname, 'example'), 32 | port: 9000, 33 | hot: true, 34 | }, 35 | devtool: 'source-map', 36 | plugins: [ 37 | new webpack.HotModuleReplacementPlugin(), 38 | ], 39 | }; 40 | 41 | module.exports = config; 42 | -------------------------------------------------------------------------------- /webpack.umd.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const config = { 4 | entry: { 5 | index: './src/index.ts', 6 | }, 7 | output: { 8 | path: path.resolve(__dirname, 'dist', 'umd'), 9 | library: 'SemanticUiCalendarReact', 10 | libraryTarget: 'umd', 11 | filename: 'semantic-ui-calendar-react.js', 12 | }, 13 | mode: 'production', 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | exclude: /node-modules/, 19 | loader: 'ts-loader', 20 | }, 21 | { 22 | test: /\.jsx?$/, 23 | exclude: /node-modules/, 24 | loader: 'babel-loader', 25 | }, 26 | ], 27 | }, 28 | resolve: { 29 | extensions: ['.webpack.js', '.web.js', '.ts', '.tsx', '.js', 'jsx'], 30 | }, 31 | devtool: 'source-map', 32 | }; 33 | 34 | module.exports = config; 35 | --------------------------------------------------------------------------------