1 | import React from 'react';
|
2 | import PropTypes from 'prop-types';
|
3 | import momentPropTypes from 'react-moment-proptypes';
|
4 | import { forbidExtraProps, mutuallyExclusiveProps, nonNegativeInteger } from 'airbnb-prop-types';
|
5 | import { withStyles, withStylesPropTypes } from 'react-with-styles';
|
6 | import moment from 'moment';
|
7 | import { addEventListener } from 'consolidated-events';
|
8 |
|
9 | import { CalendarDayPhrases } from '../defaultPhrases';
|
10 | import getPhrasePropTypes from '../utils/getPhrasePropTypes';
|
11 | import noflip from '../utils/noflip';
|
12 |
|
13 | import CalendarMonth from './CalendarMonth';
|
14 |
|
15 | import isTransitionEndSupported from '../utils/isTransitionEndSupported';
|
16 | import getTransformStyles from '../utils/getTransformStyles';
|
17 | import getCalendarMonthWidth from '../utils/getCalendarMonthWidth';
|
18 | import toISOMonthString from '../utils/toISOMonthString';
|
19 | import isPrevMonth from '../utils/isPrevMonth';
|
20 | import isNextMonth from '../utils/isNextMonth';
|
21 |
|
22 | import ModifiersShape from '../shapes/ModifiersShape';
|
23 | import ScrollableOrientationShape from '../shapes/ScrollableOrientationShape';
|
24 | import DayOfWeekShape from '../shapes/DayOfWeekShape';
|
25 |
|
26 |
|
27 | import {
|
28 | HORIZONTAL_ORIENTATION,
|
29 | VERTICAL_ORIENTATION,
|
30 | VERTICAL_SCROLLABLE,
|
31 | DAY_SIZE,
|
32 | } from '../constants';
|
33 |
|
34 | const propTypes = forbidExtraProps({
|
35 | ...withStylesPropTypes,
|
36 | enableOutsideDays: PropTypes.bool,
|
37 | firstVisibleMonthIndex: PropTypes.number,
|
38 | horizontalMonthPadding: nonNegativeInteger,
|
39 | initialMonth: momentPropTypes.momentObj,
|
40 | isAnimating: PropTypes.bool,
|
41 | numberOfMonths: PropTypes.number,
|
42 | modifiers: PropTypes.objectOf(PropTypes.objectOf(ModifiersShape)),
|
43 | orientation: ScrollableOrientationShape,
|
44 | onDayClick: PropTypes.func,
|
45 | onDayMouseEnter: PropTypes.func,
|
46 | onDayMouseLeave: PropTypes.func,
|
47 | onMonthTransitionEnd: PropTypes.func,
|
48 | onMonthChange: PropTypes.func,
|
49 | onYearChange: PropTypes.func,
|
50 | renderMonthText: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'),
|
51 | renderCalendarDay: PropTypes.func,
|
52 | renderDayContents: PropTypes.func,
|
53 | translationValue: PropTypes.number,
|
54 | renderMonthElement: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'),
|
55 | daySize: nonNegativeInteger,
|
56 | focusedDate: momentPropTypes.momentObj,
|
57 | isFocused: PropTypes.bool,
|
58 | firstDayOfWeek: DayOfWeekShape,
|
59 | setMonthTitleHeight: PropTypes.func,
|
60 | isRTL: PropTypes.bool,
|
61 | transitionDuration: nonNegativeInteger,
|
62 | verticalBorderSpacing: nonNegativeInteger,
|
63 |
|
64 |
|
65 | monthFormat: PropTypes.string,
|
66 | phrases: PropTypes.shape(getPhrasePropTypes(CalendarDayPhrases)),
|
67 | dayAriaLabelFormat: PropTypes.string,
|
68 | });
|
69 |
|
70 | const defaultProps = {
|
71 | enableOutsideDays: false,
|
72 | firstVisibleMonthIndex: 0,
|
73 | horizontalMonthPadding: 13,
|
74 | initialMonth: moment(),
|
75 | isAnimating: false,
|
76 | numberOfMonths: 1,
|
77 | modifiers: {},
|
78 | orientation: HORIZONTAL_ORIENTATION,
|
79 | onDayClick() {},
|
80 | onDayMouseEnter() {},
|
81 | onDayMouseLeave() {},
|
82 | onMonthChange() {},
|
83 | onYearChange() {},
|
84 | onMonthTransitionEnd() {},
|
85 | renderMonthText: null,
|
86 | renderCalendarDay: undefined,
|
87 | renderDayContents: null,
|
88 | translationValue: null,
|
89 | renderMonthElement: null,
|
90 | daySize: DAY_SIZE,
|
91 | focusedDate: null,
|
92 | isFocused: false,
|
93 | firstDayOfWeek: null,
|
94 | setMonthTitleHeight: null,
|
95 | isRTL: false,
|
96 | transitionDuration: 200,
|
97 | verticalBorderSpacing: undefined,
|
98 |
|
99 |
|
100 | monthFormat: 'MMMM YYYY',
|
101 | phrases: CalendarDayPhrases,
|
102 | dayAriaLabelFormat: undefined,
|
103 | };
|
104 |
|
105 | function getMonths(initialMonth, numberOfMonths, withoutTransitionMonths) {
|
106 | let month = initialMonth.clone();
|
107 | if (!withoutTransitionMonths) month = month.subtract(1, 'month');
|
108 |
|
109 | const months = [];
|
110 | for (let i = 0; i < (withoutTransitionMonths ? numberOfMonths : numberOfMonths + 2); i += 1) {
|
111 | months.push(month);
|
112 | month = month.clone().add(1, 'month');
|
113 | }
|
114 |
|
115 | return months;
|
116 | }
|
117 |
|
118 | class CalendarMonthGrid extends React.PureComponent {
|
119 | constructor(props) {
|
120 | super(props);
|
121 | const withoutTransitionMonths = props.orientation === VERTICAL_SCROLLABLE;
|
122 | this.state = {
|
123 | months: getMonths(props.initialMonth, props.numberOfMonths, withoutTransitionMonths),
|
124 | };
|
125 |
|
126 | this.isTransitionEndSupported = isTransitionEndSupported();
|
127 | this.onTransitionEnd = this.onTransitionEnd.bind(this);
|
128 | this.setContainerRef = this.setContainerRef.bind(this);
|
129 |
|
130 | this.locale = moment.locale();
|
131 | this.onMonthSelect = this.onMonthSelect.bind(this);
|
132 | this.onYearSelect = this.onYearSelect.bind(this);
|
133 | }
|
134 |
|
135 | componentDidMount() {
|
136 | this.removeEventListener = addEventListener(
|
137 | this.container,
|
138 | 'transitionend',
|
139 | this.onTransitionEnd,
|
140 | );
|
141 | }
|
142 |
|
143 | componentWillReceiveProps(nextProps) {
|
144 | const { initialMonth, numberOfMonths, orientation } = nextProps;
|
145 | const { months } = this.state;
|
146 |
|
147 | const {
|
148 | initialMonth: prevInitialMonth,
|
149 | numberOfMonths: prevNumberOfMonths,
|
150 | } = this.props;
|
151 | const hasMonthChanged = !prevInitialMonth.isSame(initialMonth, 'month');
|
152 | const hasNumberOfMonthsChanged = prevNumberOfMonths !== numberOfMonths;
|
153 | let newMonths = months;
|
154 |
|
155 | if (hasMonthChanged && !hasNumberOfMonthsChanged) {
|
156 | if (isNextMonth(prevInitialMonth, initialMonth)) {
|
157 | newMonths = months.slice(1);
|
158 | newMonths.push(months[months.length - 1].clone().add(1, 'month'));
|
159 | } else if (isPrevMonth(prevInitialMonth, initialMonth)) {
|
160 | newMonths = months.slice(0, months.length - 1);
|
161 | newMonths.unshift(months[0].clone().subtract(1, 'month'));
|
162 | } else {
|
163 | const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
|
164 | newMonths = getMonths(initialMonth, numberOfMonths, withoutTransitionMonths);
|
165 | }
|
166 | }
|
167 |
|
168 | if (hasNumberOfMonthsChanged) {
|
169 | const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
|
170 | newMonths = getMonths(initialMonth, numberOfMonths, withoutTransitionMonths);
|
171 | }
|
172 |
|
173 | const momentLocale = moment.locale();
|
174 | if (this.locale !== momentLocale) {
|
175 | this.locale = momentLocale;
|
176 | newMonths = newMonths.map((m) => m.locale(this.locale));
|
177 | }
|
178 |
|
179 | this.setState({
|
180 | months: newMonths,
|
181 | });
|
182 | }
|
183 |
|
184 | componentDidUpdate() {
|
185 | const {
|
186 | isAnimating,
|
187 | transitionDuration,
|
188 | onMonthTransitionEnd,
|
189 | } = this.props;
|
190 |
|
191 |
|
192 |
|
193 |
|
194 | if ((!this.isTransitionEndSupported || !transitionDuration) && isAnimating) {
|
195 | onMonthTransitionEnd();
|
196 | }
|
197 | }
|
198 |
|
199 | componentWillUnmount() {
|
200 | if (this.removeEventListener) this.removeEventListener();
|
201 | }
|
202 |
|
203 | onTransitionEnd() {
|
204 | const { onMonthTransitionEnd } = this.props;
|
205 | onMonthTransitionEnd();
|
206 | }
|
207 |
|
208 | onMonthSelect(currentMonth, newMonthVal) {
|
209 | const newMonth = currentMonth.clone();
|
210 | const { onMonthChange, orientation } = this.props;
|
211 | const { months } = this.state;
|
212 | const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
|
213 | let initialMonthSubtraction = months.indexOf(currentMonth);
|
214 | if (!withoutTransitionMonths) {
|
215 | initialMonthSubtraction -= 1;
|
216 | }
|
217 | newMonth.set('month', newMonthVal).subtract(initialMonthSubtraction, 'months');
|
218 | onMonthChange(newMonth);
|
219 | }
|
220 |
|
221 | onYearSelect(currentMonth, newYearVal) {
|
222 | const newMonth = currentMonth.clone();
|
223 | const { onYearChange, orientation } = this.props;
|
224 | const { months } = this.state;
|
225 | const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
|
226 | let initialMonthSubtraction = months.indexOf(currentMonth);
|
227 | if (!withoutTransitionMonths) {
|
228 | initialMonthSubtraction -= 1;
|
229 | }
|
230 | newMonth.set('year', newYearVal).subtract(initialMonthSubtraction, 'months');
|
231 | onYearChange(newMonth);
|
232 | }
|
233 |
|
234 | setContainerRef(ref) {
|
235 | this.container = ref;
|
236 | }
|
237 |
|
238 | render() {
|
239 | const {
|
240 | enableOutsideDays,
|
241 | firstVisibleMonthIndex,
|
242 | horizontalMonthPadding,
|
243 | isAnimating,
|
244 | modifiers,
|
245 | numberOfMonths,
|
246 | monthFormat,
|
247 | orientation,
|
248 | translationValue,
|
249 | daySize,
|
250 | onDayMouseEnter,
|
251 | onDayMouseLeave,
|
252 | onDayClick,
|
253 | renderMonthText,
|
254 | renderCalendarDay,
|
255 | renderDayContents,
|
256 | renderMonthElement,
|
257 | onMonthTransitionEnd,
|
258 | firstDayOfWeek,
|
259 | focusedDate,
|
260 | isFocused,
|
261 | isRTL,
|
262 | styles,
|
263 | phrases,
|
264 | dayAriaLabelFormat,
|
265 | transitionDuration,
|
266 | verticalBorderSpacing,
|
267 | setMonthTitleHeight,
|
268 | css,
|
269 | } = this.props;
|
270 |
|
271 | const { months } = this.state;
|
272 | const isVertical = orientation === VERTICAL_ORIENTATION;
|
273 | const isVerticalScrollable = orientation === VERTICAL_SCROLLABLE;
|
274 | const isHorizontal = orientation === HORIZONTAL_ORIENTATION;
|
275 |
|
276 | const calendarMonthWidth = getCalendarMonthWidth(
|
277 | daySize,
|
278 | horizontalMonthPadding,
|
279 | );
|
280 |
|
281 | const width = isVertical || isVerticalScrollable
|
282 | ? calendarMonthWidth
|
283 | : (numberOfMonths + 2) * calendarMonthWidth;
|
284 |
|
285 | const transformType = (isVertical || isVerticalScrollable) ? 'translateY' : 'translateX';
|
286 | const transformValue = `${transformType}(${translationValue}px)`;
|
287 |
|
288 | return (
|
289 | <div
|
290 | {...css(
|
291 | styles.CalendarMonthGrid,
|
292 | isHorizontal && styles.CalendarMonthGrid__horizontal,
|
293 | isVertical && styles.CalendarMonthGrid__vertical,
|
294 | isVerticalScrollable && styles.CalendarMonthGrid__vertical_scrollable,
|
295 | isAnimating && styles.CalendarMonthGrid__animating,
|
296 | isAnimating && transitionDuration && {
|
297 | transition: `transform ${transitionDuration}ms ease-in-out`,
|
298 | },
|
299 | {
|
300 | ...getTransformStyles(transformValue),
|
301 | width,
|
302 | },
|
303 | )}
|
304 | ref={this.setContainerRef}
|
305 | onTransitionEnd={onMonthTransitionEnd}
|
306 | >
|
307 | {months.map((month, i) => {
|
308 | const isVisible = (i >= firstVisibleMonthIndex)
|
309 | && (i < firstVisibleMonthIndex + numberOfMonths);
|
310 | const hideForAnimation = i === 0 && !isVisible;
|
311 | const showForAnimation = i === 0 && isAnimating && isVisible;
|
312 | const monthString = toISOMonthString(month);
|
313 | return (
|
314 | <div
|
315 | key={monthString}
|
316 | {...css(
|
317 | isHorizontal && styles.CalendarMonthGrid_month__horizontal,
|
318 | hideForAnimation && styles.CalendarMonthGrid_month__hideForAnimation,
|
319 | showForAnimation && !isVertical && !isRTL && {
|
320 | position: 'absolute',
|
321 | left: -calendarMonthWidth,
|
322 | },
|
323 | showForAnimation && !isVertical && isRTL && {
|
324 | position: 'absolute',
|
325 | right: 0,
|
326 | },
|
327 | showForAnimation && isVertical && {
|
328 | position: 'absolute',
|
329 | top: -translationValue,
|
330 | },
|
331 | !isVisible && !isAnimating && styles.CalendarMonthGrid_month__hidden,
|
332 | )}
|
333 | >
|
334 | <CalendarMonth
|
335 | month={month}
|
336 | isVisible={isVisible}
|
337 | enableOutsideDays={enableOutsideDays}
|
338 | modifiers={modifiers[monthString]}
|
339 | monthFormat={monthFormat}
|
340 | orientation={orientation}
|
341 | onDayMouseEnter={onDayMouseEnter}
|
342 | onDayMouseLeave={onDayMouseLeave}
|
343 | onDayClick={onDayClick}
|
344 | onMonthSelect={this.onMonthSelect}
|
345 | onYearSelect={this.onYearSelect}
|
346 | renderMonthText={renderMonthText}
|
347 | renderCalendarDay={renderCalendarDay}
|
348 | renderDayContents={renderDayContents}
|
349 | renderMonthElement={renderMonthElement}
|
350 | firstDayOfWeek={firstDayOfWeek}
|
351 | daySize={daySize}
|
352 | focusedDate={isVisible ? focusedDate : null}
|
353 | isFocused={isFocused}
|
354 | phrases={phrases}
|
355 | setMonthTitleHeight={setMonthTitleHeight}
|
356 | dayAriaLabelFormat={dayAriaLabelFormat}
|
357 | verticalBorderSpacing={verticalBorderSpacing}
|
358 | horizontalMonthPadding={horizontalMonthPadding}
|
359 | />
|
360 | </div>
|
361 | );
|
362 | })}
|
363 | </div>
|
364 | );
|
365 | }
|
366 | }
|
367 |
|
368 | CalendarMonthGrid.propTypes = propTypes;
|
369 | CalendarMonthGrid.defaultProps = defaultProps;
|
370 |
|
371 | export default withStyles(({
|
372 | reactDates: {
|
373 | color,
|
374 | noScrollBarOnVerticalScrollable,
|
375 | spacing,
|
376 | zIndex,
|
377 | },
|
378 | }) => ({
|
379 | CalendarMonthGrid: {
|
380 | background: color.background,
|
381 | textAlign: noflip('left'),
|
382 | zIndex,
|
383 | },
|
384 |
|
385 | CalendarMonthGrid__animating: {
|
386 | zIndex: zIndex + 1,
|
387 | },
|
388 |
|
389 | CalendarMonthGrid__horizontal: {
|
390 | position: 'absolute',
|
391 | left: noflip(spacing.dayPickerHorizontalPadding),
|
392 | },
|
393 |
|
394 | CalendarMonthGrid__vertical: {
|
395 | margin: '0 auto',
|
396 | },
|
397 |
|
398 | CalendarMonthGrid__vertical_scrollable: {
|
399 | margin: '0 auto',
|
400 | overflowY: 'scroll',
|
401 | ...(noScrollBarOnVerticalScrollable && {
|
402 | '-webkitOverflowScrolling': 'touch',
|
403 | '::-webkit-scrollbar': {
|
404 | '-webkit-appearance': 'none',
|
405 | display: 'none',
|
406 | },
|
407 | }),
|
408 | },
|
409 |
|
410 | CalendarMonthGrid_month__horizontal: {
|
411 | display: 'inline-block',
|
412 | verticalAlign: 'top',
|
413 | minHeight: '100%',
|
414 | },
|
415 |
|
416 | CalendarMonthGrid_month__hideForAnimation: {
|
417 | position: 'absolute',
|
418 | zIndex: zIndex - 1,
|
419 | opacity: 0,
|
420 | pointerEvents: 'none',
|
421 | },
|
422 |
|
423 | CalendarMonthGrid_month__hidden: {
|
424 | visibility: 'hidden',
|
425 | },
|
426 | }), { pureComponent: typeof React.PureComponent !== 'undefined' })(CalendarMonthGrid);
|