UNPKG

@blueprintjs/datetime

Version:

Components for interacting with dates and times

299 lines 13.9 kB
/* * Copyright 2015 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import classNames from "classnames"; import * as React from "react"; import DayPicker from "react-day-picker"; import { AbstractPureComponent2, Button, DISPLAYNAME_PREFIX, Divider } from "@blueprintjs/core"; import * as Classes from "./common/classes"; import * as DateUtils from "./common/dateUtils"; import * as Errors from "./common/errors"; import { DatePickerCaption } from "./datePickerCaption"; import { getDefaultMaxDate, getDefaultMinDate } from "./datePickerCore"; import { DatePickerNavbar } from "./datePickerNavbar"; import { Shortcuts } from "./shortcuts"; import { TimePicker } from "./timePicker"; /** * Date picker component. * * @see https://blueprintjs.com/docs/#datetime/datepicker */ export class DatePicker extends AbstractPureComponent2 { static defaultProps = { canClearSelection: true, clearButtonText: "Clear", dayPickerProps: {}, highlightCurrentDay: false, maxDate: getDefaultMaxDate(), minDate: getDefaultMinDate(), reverseMonthAndYearMenus: false, shortcuts: false, showActionsBar: false, todayButtonText: "Today", }; static displayName = `${DISPLAYNAME_PREFIX}.DatePicker`; ignoreNextMonthChange = false; constructor(props, context) { super(props, context); const value = getInitialValue(props); const initialMonth = getInitialMonth(props, value); this.state = { displayMonth: initialMonth.getMonth(), displayYear: initialMonth.getFullYear(), selectedDay: value == null ? null : value.getDate(), selectedShortcutIndex: this.props.selectedShortcutIndex !== undefined ? this.props.selectedShortcutIndex : -1, value, }; } render() { const { className, dayPickerProps, footerElement, locale, localeUtils, maxDate, minDate, showActionsBar } = this.props; const { displayMonth, displayYear } = this.state; return (React.createElement("div", { className: classNames(Classes.DATEPICKER, className) }, this.maybeRenderShortcuts(), React.createElement("div", { className: Classes.DATEPICKER_CONTENT }, 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 }), this.maybeRenderTimePicker(), showActionsBar && this.renderOptionsBar(), footerElement))); } componentDidUpdate(prevProps, prevState) { super.componentDidUpdate(prevProps, prevState); const { value } = this.props; if (value === prevProps.value) { // no action needed return; } else if (value == null) { // clear the value this.setState({ value }); } else { this.setState({ displayMonth: value.getMonth(), displayYear: value.getFullYear(), selectedDay: value.getDate(), value, }); } if (this.props.selectedShortcutIndex !== prevProps.selectedShortcutIndex) { this.setState({ selectedShortcutIndex: this.props.selectedShortcutIndex }); } } validateProps(props) { const { defaultValue, initialMonth, maxDate, minDate, value } = props; if (defaultValue != null && !DateUtils.isDayInRange(defaultValue, [minDate, maxDate])) { console.error(Errors.DATEPICKER_DEFAULT_VALUE_INVALID); } if (initialMonth != null && !DateUtils.isMonthInRange(initialMonth, [minDate, maxDate])) { console.error(Errors.DATEPICKER_INITIAL_MONTH_INVALID); } if (maxDate != null && minDate != null && maxDate < minDate && !DateUtils.areSameDay(maxDate, minDate)) { console.error(Errors.DATEPICKER_MAX_DATE_INVALID); } if (value != null && !DateUtils.isDayInRange(value, [minDate, maxDate])) { console.error(Errors.DATEPICKER_VALUE_INVALID); } } shouldHighlightCurrentDay = (date) => { const { highlightCurrentDay } = this.props; return highlightCurrentDay && DateUtils.isToday(date); }; getDatePickerModifiers = () => { const { modifiers } = this.props; return { isToday: this.shouldHighlightCurrentDay, ...modifiers, }; }; renderDay = (day) => { const date = day.getDate(); return React.createElement("div", { className: Classes.DATEPICKER_DAY_WRAPPER }, date); }; disabledDays = (day) => !DateUtils.isDayInRange(day, [this.props.minDate, this.props.maxDate]); getDisabledDaysModifier = () => { const { dayPickerProps: { disabledDays }, } = this.props; return Array.isArray(disabledDays) ? [this.disabledDays, ...disabledDays] : [this.disabledDays, disabledDays]; }; renderCaption = (props) => (React.createElement(DatePickerCaption, { ...props, maxDate: this.props.maxDate, minDate: this.props.minDate, onDateChange: this.handleMonthChange, reverseMonthAndYearMenus: this.props.reverseMonthAndYearMenus })); renderNavbar = (props) => (React.createElement(DatePickerNavbar, { ...props, maxDate: this.props.maxDate, minDate: this.props.minDate })); renderOptionsBar() { const { clearButtonText, todayButtonText, minDate, maxDate, canClearSelection } = this.props; const todayEnabled = isTodayEnabled(minDate, maxDate); return [ React.createElement(Divider, { key: "div" }), React.createElement("div", { className: Classes.DATEPICKER_FOOTER, key: "footer" }, React.createElement(Button, { minimal: true, disabled: !todayEnabled, onClick: this.handleTodayClick, text: todayButtonText }), React.createElement(Button, { disabled: !canClearSelection, minimal: true, onClick: this.handleClearClick, text: clearButtonText })), ]; } maybeRenderTimePicker() { const { timePrecision, timePickerProps, minDate, maxDate } = this.props; if (timePrecision == null && timePickerProps === undefined) { return null; } const applyMin = DateUtils.areSameDay(this.state.value, minDate); const applyMax = DateUtils.areSameDay(this.state.value, maxDate); return (React.createElement("div", { className: Classes.DATEPICKER_TIMEPICKER_WRAPPER }, React.createElement(TimePicker, { precision: timePrecision, minTime: applyMin ? minDate : undefined, maxTime: applyMax ? maxDate : undefined, ...timePickerProps, onChange: this.handleTimeChange, value: this.state.value }))); } maybeRenderShortcuts() { const { shortcuts } = this.props; if (shortcuts == null || shortcuts === false) { return null; } const { selectedShortcutIndex } = this.state; const { maxDate, minDate, timePrecision } = this.props; // Reuse the existing date range shortcuts and only care about start date const dateRangeShortcuts = shortcuts === true ? true : shortcuts.map(shortcut => ({ ...shortcut, dateRange: [shortcut.date, undefined], })); return [ React.createElement(Shortcuts, { key: "shortcuts", ...{ allowSingleDayRange: true, maxDate, minDate, selectedShortcutIndex, shortcuts: dateRangeShortcuts, timePrecision, }, onShortcutClick: this.handleShortcutClick, useSingleDateShortcuts: true }), React.createElement(Divider, { key: "div" }), ]; } handleDayClick = (day, modifiers, e) => { this.props.dayPickerProps.onDayClick?.(day, modifiers, e); if (modifiers.disabled) { return; } this.updateDay(day); // allow toggling selected date by clicking it again (if prop enabled) const newValue = this.props.canClearSelection && modifiers.selected ? null : DateUtils.getDateTime(day, this.state.value); this.updateValue(newValue, true); }; handleShortcutClick = (shortcut, selectedShortcutIndex) => { const { onShortcutChange, selectedShortcutIndex: currentShortcutIndex } = this.props; const { dateRange, includeTime } = shortcut; const newDate = dateRange[0]; const newValue = includeTime ? newDate : DateUtils.getDateTime(newDate, this.state.value); this.updateDay(newDate); this.updateValue(newValue, true); if (currentShortcutIndex === undefined) { this.setState({ selectedShortcutIndex }); } const datePickerShortcut = { ...shortcut, date: shortcut.dateRange[0] }; onShortcutChange?.(datePickerShortcut, selectedShortcutIndex); }; updateDay = (day) => { if (this.props.value === undefined) { // set now if uncontrolled, otherwise they'll be updated in `componentDidUpdate` this.setState({ displayMonth: day.getMonth(), displayYear: day.getFullYear(), selectedDay: day.getDate(), }); } if (this.state.value != null && this.state.value.getMonth() !== day.getMonth()) { this.ignoreNextMonthChange = true; } }; computeValidDateInSpecifiedMonthYear(displayYear, displayMonth) { const { minDate, maxDate } = this.props; const { selectedDay } = this.state; // month is 0-based, date is 1-based. date 0 is last day of previous month. const maxDaysInMonth = new Date(displayYear, displayMonth + 1, 0).getDate(); const displayDate = selectedDay == null ? 1 : Math.min(selectedDay, maxDaysInMonth); // 12:00 matches the underlying react-day-picker timestamp behavior const value = DateUtils.getDateTime(new Date(displayYear, displayMonth, displayDate, 12), this.state.value); // clamp between min and max dates if (value < minDate) { return minDate; } else if (value > maxDate) { return maxDate; } return value; } handleClearClick = () => this.updateValue(null, true); handleMonthChange = (newDate) => { const date = this.computeValidDateInSpecifiedMonthYear(newDate.getFullYear(), newDate.getMonth()); this.setState({ displayMonth: date.getMonth(), displayYear: date.getFullYear() }); if (this.state.value !== null) { // if handleDayClick just got run (so this flag is set), then the // user selected a date in a new month, so don't invoke onChange a // second time this.updateValue(date, false, this.ignoreNextMonthChange); this.ignoreNextMonthChange = false; } this.props.dayPickerProps.onMonthChange?.(date); }; handleTodayClick = () => { const value = new Date(); const displayMonth = value.getMonth(); const displayYear = value.getFullYear(); const selectedDay = value.getDate(); this.setState({ displayMonth, displayYear, selectedDay }); this.updateValue(value, true); }; handleTimeChange = (time) => { this.props.timePickerProps?.onChange?.(time); const { value } = this.state; const newValue = DateUtils.getDateTime(value != null ? value : new Date(), time); this.updateValue(newValue, true); }; /** * Update `value` by invoking `onChange` (always) and setting state (if uncontrolled). */ updateValue(value, isUserChange, skipOnChange = false) { if (!skipOnChange) { this.props.onChange?.(value, isUserChange); } if (this.props.value === undefined) { this.setState({ value }); } } } function getInitialValue(props) { // !== because `null` is a valid value (no date) if (props.value !== undefined) { return props.value; } if (props.defaultValue !== undefined) { return props.defaultValue; } return null; } function getInitialMonth(props, value) { const today = new Date(); // != because we must have a real `Date` to begin the calendar on. if (props.initialMonth != null) { return props.initialMonth; } else if (value != null) { return value; } else if (DateUtils.isDayInRange(today, [props.minDate, props.maxDate])) { return today; } else { return DateUtils.getDateBetween([props.minDate, props.maxDate]); } } function isTodayEnabled(minDate, maxDate) { const today = new Date(); return DateUtils.isDayInRange(today, [minDate, maxDate]); } //# sourceMappingURL=datePicker.js.map