UNPKG

13.7 kBJavaScriptView Raw
1/*
2 * Copyright 2015 Palantir Technologies, Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16import classNames from "classnames";
17import * as React from "react";
18import DayPicker from "react-day-picker";
19import { AbstractPureComponent2, Button, DISPLAYNAME_PREFIX, Divider } from "@blueprintjs/core";
20import * as Classes from "./common/classes";
21import * as DateUtils from "./common/dateUtils";
22import * as Errors from "./common/errors";
23import { DatePickerCaption } from "./datePickerCaption";
24import { getDefaultMaxDate, getDefaultMinDate } from "./datePickerCore";
25import { DatePickerNavbar } from "./datePickerNavbar";
26import { Shortcuts } from "./shortcuts";
27import { TimePicker } from "./timePicker";
28export class DatePicker extends AbstractPureComponent2 {
29 static defaultProps = {
30 canClearSelection: true,
31 clearButtonText: "Clear",
32 dayPickerProps: {},
33 highlightCurrentDay: false,
34 maxDate: getDefaultMaxDate(),
35 minDate: getDefaultMinDate(),
36 reverseMonthAndYearMenus: false,
37 shortcuts: false,
38 showActionsBar: false,
39 todayButtonText: "Today",
40 };
41 static displayName = `${DISPLAYNAME_PREFIX}.DatePicker`;
42 ignoreNextMonthChange = false;
43 constructor(props, context) {
44 super(props, context);
45 const value = getInitialValue(props);
46 const initialMonth = getInitialMonth(props, value);
47 this.state = {
48 displayMonth: initialMonth.getMonth(),
49 displayYear: initialMonth.getFullYear(),
50 selectedDay: value == null ? null : value.getDate(),
51 selectedShortcutIndex: this.props.selectedShortcutIndex !== undefined ? this.props.selectedShortcutIndex : -1,
52 value,
53 };
54 }
55 render() {
56 const { className, dayPickerProps, locale, localeUtils, maxDate, minDate, showActionsBar } = this.props;
57 const { displayMonth, displayYear } = this.state;
58 return (React.createElement("div", { className: classNames(Classes.DATEPICKER, className) },
59 this.maybeRenderShortcuts(),
60 React.createElement("div", null,
61 React.createElement(DayPicker, { showOutsideDays: true, locale: locale, localeUtils: localeUtils, modifiers: this.getDatePickerModifiers(), ...dayPickerProps, canChangeMonth: true, captionElement: this.renderCaption, navbarElement: this.renderNavbar, disabledDays: this.getDisabledDaysModifier(), fromMonth: minDate, month: new Date(displayYear, displayMonth), onDayClick: this.handleDayClick, onMonthChange: this.handleMonthChange, selectedDays: this.state.value, toMonth: maxDate, renderDay: dayPickerProps?.renderDay ?? this.renderDay }),
62 this.maybeRenderTimePicker(),
63 showActionsBar && this.renderOptionsBar())));
64 }
65 componentDidUpdate(prevProps, prevState) {
66 super.componentDidUpdate(prevProps, prevState);
67 const { value } = this.props;
68 if (value === prevProps.value) {
69 // no action needed
70 return;
71 }
72 else if (value == null) {
73 // clear the value
74 this.setState({ value });
75 }
76 else {
77 this.setState({
78 displayMonth: value.getMonth(),
79 displayYear: value.getFullYear(),
80 selectedDay: value.getDate(),
81 value,
82 });
83 }
84 if (this.props.selectedShortcutIndex !== prevProps.selectedShortcutIndex) {
85 this.setState({ selectedShortcutIndex: this.props.selectedShortcutIndex });
86 }
87 }
88 validateProps(props) {
89 const { defaultValue, initialMonth, maxDate, minDate, value } = props;
90 if (defaultValue != null && !DateUtils.isDayInRange(defaultValue, [minDate, maxDate])) {
91 console.error(Errors.DATEPICKER_DEFAULT_VALUE_INVALID);
92 }
93 if (initialMonth != null && !DateUtils.isMonthInRange(initialMonth, [minDate, maxDate])) {
94 console.error(Errors.DATEPICKER_INITIAL_MONTH_INVALID);
95 }
96 if (maxDate != null && minDate != null && maxDate < minDate && !DateUtils.areSameDay(maxDate, minDate)) {
97 console.error(Errors.DATEPICKER_MAX_DATE_INVALID);
98 }
99 if (value != null && !DateUtils.isDayInRange(value, [minDate, maxDate])) {
100 console.error(Errors.DATEPICKER_VALUE_INVALID);
101 }
102 }
103 shouldHighlightCurrentDay = (date) => {
104 const { highlightCurrentDay } = this.props;
105 return highlightCurrentDay && DateUtils.isToday(date);
106 };
107 getDatePickerModifiers = () => {
108 const { modifiers } = this.props;
109 return {
110 isToday: this.shouldHighlightCurrentDay,
111 ...modifiers,
112 };
113 };
114 renderDay = (day) => {
115 const date = day.getDate();
116 return React.createElement("div", { className: Classes.DATEPICKER_DAY_WRAPPER }, date);
117 };
118 disabledDays = (day) => !DateUtils.isDayInRange(day, [this.props.minDate, this.props.maxDate]);
119 getDisabledDaysModifier = () => {
120 const { dayPickerProps: { disabledDays }, } = this.props;
121 return Array.isArray(disabledDays) ? [this.disabledDays, ...disabledDays] : [this.disabledDays, disabledDays];
122 };
123 renderCaption = (props) => (React.createElement(DatePickerCaption, { ...props, maxDate: this.props.maxDate, minDate: this.props.minDate, onDateChange: this.handleMonthChange, reverseMonthAndYearMenus: this.props.reverseMonthAndYearMenus }));
124 renderNavbar = (props) => (React.createElement(DatePickerNavbar, { ...props, maxDate: this.props.maxDate, minDate: this.props.minDate }));
125 renderOptionsBar() {
126 const { clearButtonText, todayButtonText, minDate, maxDate, canClearSelection } = this.props;
127 const todayEnabled = isTodayEnabled(minDate, maxDate);
128 return [
129 React.createElement(Divider, { key: "div" }),
130 React.createElement("div", { className: Classes.DATEPICKER_FOOTER, key: "footer" },
131 React.createElement(Button, { minimal: true, disabled: !todayEnabled, onClick: this.handleTodayClick, text: todayButtonText }),
132 React.createElement(Button, { disabled: !canClearSelection, minimal: true, onClick: this.handleClearClick, text: clearButtonText })),
133 ];
134 }
135 maybeRenderTimePicker() {
136 const { timePrecision, timePickerProps, minDate, maxDate } = this.props;
137 if (timePrecision == null && timePickerProps === undefined) {
138 return null;
139 }
140 const applyMin = DateUtils.areSameDay(this.state.value, minDate);
141 const applyMax = DateUtils.areSameDay(this.state.value, maxDate);
142 return (React.createElement("div", { className: Classes.DATEPICKER_TIMEPICKER_WRAPPER },
143 React.createElement(TimePicker, { precision: timePrecision, minTime: applyMin ? minDate : undefined, maxTime: applyMax ? maxDate : undefined, ...timePickerProps, onChange: this.handleTimeChange, value: this.state.value })));
144 }
145 maybeRenderShortcuts() {
146 const { shortcuts } = this.props;
147 if (shortcuts == null || shortcuts === false) {
148 return null;
149 }
150 const { selectedShortcutIndex } = this.state;
151 const { maxDate, minDate, timePrecision } = this.props;
152 // Reuse the existing date range shortcuts and only care about start date
153 const dateRangeShortcuts = shortcuts === true
154 ? true
155 : shortcuts.map(shortcut => ({
156 ...shortcut,
157 dateRange: [shortcut.date, undefined],
158 }));
159 return [
160 React.createElement(Shortcuts, { key: "shortcuts", ...{
161 allowSingleDayRange: true,
162 maxDate,
163 minDate,
164 selectedShortcutIndex,
165 shortcuts: dateRangeShortcuts,
166 timePrecision,
167 }, onShortcutClick: this.handleShortcutClick, useSingleDateShortcuts: true }),
168 React.createElement(Divider, { key: "div" }),
169 ];
170 }
171 handleDayClick = (day, modifiers, e) => {
172 this.props.dayPickerProps.onDayClick?.(day, modifiers, e);
173 if (modifiers.disabled) {
174 return;
175 }
176 this.updateDay(day);
177 // allow toggling selected date by clicking it again (if prop enabled)
178 const newValue = this.props.canClearSelection && modifiers.selected ? null : DateUtils.getDateTime(day, this.state.value);
179 this.updateValue(newValue, true);
180 };
181 handleShortcutClick = (shortcut, selectedShortcutIndex) => {
182 const { onShortcutChange, selectedShortcutIndex: currentShortcutIndex } = this.props;
183 const { dateRange, includeTime } = shortcut;
184 const newDate = dateRange[0];
185 const newValue = includeTime ? newDate : DateUtils.getDateTime(newDate, this.state.value);
186 this.updateDay(newDate);
187 this.updateValue(newValue, true);
188 if (currentShortcutIndex === undefined) {
189 this.setState({ selectedShortcutIndex });
190 }
191 const datePickerShortcut = { ...shortcut, date: shortcut.dateRange[0] };
192 onShortcutChange?.(datePickerShortcut, selectedShortcutIndex);
193 };
194 updateDay = (day) => {
195 if (this.props.value === undefined) {
196 // set now if uncontrolled, otherwise they'll be updated in `componentDidUpdate`
197 this.setState({
198 displayMonth: day.getMonth(),
199 displayYear: day.getFullYear(),
200 selectedDay: day.getDate(),
201 });
202 }
203 if (this.state.value != null && this.state.value.getMonth() !== day.getMonth()) {
204 this.ignoreNextMonthChange = true;
205 }
206 };
207 computeValidDateInSpecifiedMonthYear(displayYear, displayMonth) {
208 const { minDate, maxDate } = this.props;
209 const { selectedDay } = this.state;
210 // month is 0-based, date is 1-based. date 0 is last day of previous month.
211 const maxDaysInMonth = new Date(displayYear, displayMonth + 1, 0).getDate();
212 const displayDate = selectedDay == null ? 1 : Math.min(selectedDay, maxDaysInMonth);
213 // 12:00 matches the underlying react-day-picker timestamp behavior
214 const value = DateUtils.getDateTime(new Date(displayYear, displayMonth, displayDate, 12), this.state.value);
215 // clamp between min and max dates
216 if (value < minDate) {
217 return minDate;
218 }
219 else if (value > maxDate) {
220 return maxDate;
221 }
222 return value;
223 }
224 handleClearClick = () => this.updateValue(null, true);
225 handleMonthChange = (newDate) => {
226 const date = this.computeValidDateInSpecifiedMonthYear(newDate.getFullYear(), newDate.getMonth());
227 this.setState({ displayMonth: date.getMonth(), displayYear: date.getFullYear() });
228 if (this.state.value !== null) {
229 // if handleDayClick just got run (so this flag is set), then the
230 // user selected a date in a new month, so don't invoke onChange a
231 // second time
232 this.updateValue(date, false, this.ignoreNextMonthChange);
233 this.ignoreNextMonthChange = false;
234 }
235 this.props.dayPickerProps.onMonthChange?.(date);
236 };
237 handleTodayClick = () => {
238 const value = new Date();
239 const displayMonth = value.getMonth();
240 const displayYear = value.getFullYear();
241 const selectedDay = value.getDate();
242 this.setState({ displayMonth, displayYear, selectedDay });
243 this.updateValue(value, true);
244 };
245 handleTimeChange = (time) => {
246 this.props.timePickerProps?.onChange?.(time);
247 const { value } = this.state;
248 const newValue = DateUtils.getDateTime(value != null ? value : new Date(), time);
249 this.updateValue(newValue, true);
250 };
251 /**
252 * Update `value` by invoking `onChange` (always) and setting state (if uncontrolled).
253 */
254 updateValue(value, isUserChange, skipOnChange = false) {
255 if (!skipOnChange) {
256 this.props.onChange?.(value, isUserChange);
257 }
258 if (this.props.value === undefined) {
259 this.setState({ value });
260 }
261 }
262}
263function getInitialValue(props) {
264 // !== because `null` is a valid value (no date)
265 if (props.value !== undefined) {
266 return props.value;
267 }
268 if (props.defaultValue !== undefined) {
269 return props.defaultValue;
270 }
271 return null;
272}
273function getInitialMonth(props, value) {
274 const today = new Date();
275 // != because we must have a real `Date` to begin the calendar on.
276 if (props.initialMonth != null) {
277 return props.initialMonth;
278 }
279 else if (value != null) {
280 return value;
281 }
282 else if (DateUtils.isDayInRange(today, [props.minDate, props.maxDate])) {
283 return today;
284 }
285 else {
286 return DateUtils.getDateBetween([props.minDate, props.maxDate]);
287 }
288}
289function isTodayEnabled(minDate, maxDate) {
290 const today = new Date();
291 return DateUtils.isDayInRange(today, [minDate, maxDate]);
292}
293//# sourceMappingURL=datePicker.js.map
\No newline at end of file