diff --git a/packages/discuz-design/README.md b/packages/discuz-design/README.md
index a84123b98c9605db70464e3124915300b4a29dcd..891b6d45c53473c5d86131a390f102d55c55441d 100644
--- a/packages/discuz-design/README.md
+++ b/packages/discuz-design/README.md
@@ -96,6 +96,7 @@ npm run new poster 海报
- [x] [Slider](./components/slider) - 滑动条
- [x] [AudioRecorder](./components/audio-record) - 录音
- [x] [WebPicker](./components/web-picker) - 选择器(H5)
+ - [x] [DatePicker](./components/datepicker) - 日期选择器(PC)
- 交互
- [x] [Animation](./components/animation) - 动画组件
- [x] [BackToTop](./components/back-to-top) - 返回顶部
diff --git a/packages/discuz-design/components/button/adapters/mini.ts b/packages/discuz-design/components/button/adapters/mini.ts
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ee7f04cf4ea50b2754358dd92ed2d3ce56341a8b 100644
--- a/packages/discuz-design/components/button/adapters/mini.ts
+++ b/packages/discuz-design/components/button/adapters/mini.ts
@@ -0,0 +1 @@
+export const ButtonMiniAdapter = {};
diff --git a/packages/discuz-design/components/button/adapters/web.ts b/packages/discuz-design/components/button/adapters/web.ts
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b70f58ea8961b58dba4b2cd08734f12089a7feaa 100644
--- a/packages/discuz-design/components/button/adapters/web.ts
+++ b/packages/discuz-design/components/button/adapters/web.ts
@@ -0,0 +1 @@
+export const ButtonWebAdapter = {};
diff --git a/packages/discuz-design/components/datepicker/README.md b/packages/discuz-design/components/datepicker/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4e51b61c622ad49aa5ddc083974c1e6e5487f247
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/README.md
@@ -0,0 +1,21 @@
+# Datepicker 组件
+
+## 组件说明
+
+基础Datepicker组件
+
+## 示例
+
+### WEB 示例
+
+### 基础用法
+
+[Example: 基础用法](./__examples__/web/index.tsx)
+
+### 小程序示例
+
+
+
+## API 参数
+
+[Interface: DatepickerProps](./interface.ts)
diff --git a/packages/discuz-design/components/datepicker/__examples__/mini/index.tsx b/packages/discuz-design/components/datepicker/__examples__/mini/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9564881fd266d620ff12b54e9c81bafe370b8a22
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/__examples__/mini/index.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { View } from '@tarojs/components';
+import Datepicker from '../../index';
+
+export default function DatepickerExample() {
+ return Datepicker;
+}
diff --git a/packages/discuz-design/components/datepicker/__examples__/web/index.tsx b/packages/discuz-design/components/datepicker/__examples__/web/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..af2dab74c1b770dcf823904f87c56284e6d3e589
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/__examples__/web/index.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import Datepicker from '../../index';
+
+import YearPicker from '../../layouts/web/year-picker';
+import MonthPicker from '../../layouts/web/month-picker';
+
+
+function Title(props) {
+ return
setStartDate(date)}
+ customInput={}
+ renderCustomHeader={renderCustomHeader}
+ locale="zhCN"
+ dateFormat="yyyy-MM-dd HH:mm"
+ showTimeSelect
+ timeFormat="HH:mm:ss"
+ showPopperArrow={false}
+ timeCaption="时间"
+ />
+ );
+};
diff --git a/packages/discuz-design/components/datepicker/layouts/web/jumper.jsx b/packages/discuz-design/components/datepicker/layouts/web/jumper.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/calendar.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/calendar.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..a9ab861907b054ae5235898c2c06941e22da94d0
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/calendar.jsx
@@ -0,0 +1,973 @@
+import YearDropdown from './year_dropdown';
+import MonthDropdown from './month_dropdown';
+import MonthYearDropdown from './month_year_dropdown';
+import Month from './month';
+import Time from './time';
+import Year from './year';
+import InputTime from './inputTime';
+import React from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import CalendarContainer from './calendar_container';
+import {
+ newDate,
+ setMonth,
+ getMonth,
+ addMonths,
+ subMonths,
+ getStartOfWeek,
+ getStartOfToday,
+ addDays,
+ formatDate,
+ setYear,
+ getYear,
+ isBefore,
+ addYears,
+ subYears,
+ isAfter,
+ getFormattedWeekdayInLocale,
+ getWeekdayShortInLocale,
+ getWeekdayMinInLocale,
+ isSameDay,
+ monthDisabledBefore,
+ monthDisabledAfter,
+ yearDisabledBefore,
+ yearDisabledAfter,
+ yearsDisabledAfter,
+ yearsDisabledBefore,
+ getEffectiveMinDate,
+ getEffectiveMaxDate,
+ addZero,
+ isValid,
+ getYearsPeriod,
+ DEFAULT_YEAR_ITEM_NUMBER,
+} from './date_utils';
+
+const DROPDOWN_FOCUS_CLASSNAMES = [
+ 'react-datepicker__year-select',
+ 'react-datepicker__month-select',
+ 'react-datepicker__month-year-select',
+];
+
+const isDropdownSelect = (element = {}) => {
+ const classNames = (element.className || '').split(/\s+/);
+ return DROPDOWN_FOCUS_CLASSNAMES.some(testClassname => classNames.indexOf(testClassname) >= 0);
+};
+
+export default class Calendar extends React.Component {
+ static get defaultProps() {
+ return {
+ onDropdownFocus: () => {},
+ monthsShown: 1,
+ monthSelectedIn: 0,
+ forceShowMonthNavigation: false,
+ timeCaption: 'Time',
+ previousYearButtonLabel: 'Previous Year',
+ nextYearButtonLabel: 'Next Year',
+ previousMonthButtonLabel: 'Previous Month',
+ nextMonthButtonLabel: 'Next Month',
+ customTimeInput: null,
+ yearItemNumber: DEFAULT_YEAR_ITEM_NUMBER,
+ };
+ }
+
+ static propTypes = {
+ adjustDateOnChange: PropTypes.bool,
+ arrowProps: PropTypes.object,
+ chooseDayAriaLabelPrefix: PropTypes.string,
+ className: PropTypes.string,
+ children: PropTypes.node,
+ container: PropTypes.func,
+ dateFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.array])
+ .isRequired,
+ dayClassName: PropTypes.func,
+ weekDayClassName: PropTypes.func,
+ disabledDayAriaLabelPrefix: PropTypes.string,
+ monthClassName: PropTypes.func,
+ timeClassName: PropTypes.func,
+ disabledKeyboardNavigation: PropTypes.bool,
+ calendarStartDay: PropTypes.number,
+ dropdownMode: PropTypes.oneOf(['scroll', 'select']),
+ endDate: PropTypes.instanceOf(Date),
+ excludeDates: PropTypes.array,
+ filterDate: PropTypes.func,
+ fixedHeight: PropTypes.bool,
+ formatWeekNumber: PropTypes.func,
+ highlightDates: PropTypes.instanceOf(Map),
+ includeDates: PropTypes.array,
+ includeTimes: PropTypes.array,
+ injectTimes: PropTypes.array,
+ inline: PropTypes.bool,
+ shouldFocusDayInline: PropTypes.bool,
+ locale: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.shape({ locale: PropTypes.object }),
+ ]),
+ maxDate: PropTypes.instanceOf(Date),
+ minDate: PropTypes.instanceOf(Date),
+ monthsShown: PropTypes.number,
+ monthSelectedIn: PropTypes.number,
+ nextMonthAriaLabel: PropTypes.string,
+ nextYearAriaLabel: PropTypes.string,
+ onClickOutside: PropTypes.func.isRequired,
+ onMonthChange: PropTypes.func,
+ onYearChange: PropTypes.func,
+ forceShowMonthNavigation: PropTypes.bool,
+ onDropdownFocus: PropTypes.func,
+ onSelect: PropTypes.func.isRequired,
+ onWeekSelect: PropTypes.func,
+ showTimeSelect: PropTypes.bool,
+ showTimeInput: PropTypes.bool,
+ showMonthYearPicker: PropTypes.bool,
+ showFullMonthYearPicker: PropTypes.bool,
+ showTwoColumnMonthYearPicker: PropTypes.bool,
+ showFourColumnMonthYearPicker: PropTypes.bool,
+ showYearPicker: PropTypes.bool,
+ showQuarterYearPicker: PropTypes.bool,
+ showTimeSelectOnly: PropTypes.bool,
+ timeFormat: PropTypes.string,
+ timeIntervals: PropTypes.number,
+ onTimeChange: PropTypes.func,
+ timeInputLabel: PropTypes.string,
+ minTime: PropTypes.instanceOf(Date),
+ maxTime: PropTypes.instanceOf(Date),
+ excludeTimes: PropTypes.array,
+ filterTime: PropTypes.func,
+ timeCaption: PropTypes.string,
+ openToDate: PropTypes.instanceOf(Date),
+ peekNextMonth: PropTypes.bool,
+ previousMonthAriaLabel: PropTypes.string,
+ previousYearAriaLabel: PropTypes.string,
+ scrollableYearDropdown: PropTypes.bool,
+ scrollableMonthYearDropdown: PropTypes.bool,
+ preSelection: PropTypes.instanceOf(Date),
+ selected: PropTypes.instanceOf(Date),
+ selectsEnd: PropTypes.bool,
+ selectsStart: PropTypes.bool,
+ selectsRange: PropTypes.bool,
+ showMonthDropdown: PropTypes.bool,
+ showPreviousMonths: PropTypes.bool,
+ showMonthYearDropdown: PropTypes.bool,
+ showWeekNumbers: PropTypes.bool,
+ showYearDropdown: PropTypes.bool,
+ startDate: PropTypes.instanceOf(Date),
+ todayButton: PropTypes.string,
+ useWeekdaysShort: PropTypes.bool,
+ formatWeekDay: PropTypes.func,
+ withPortal: PropTypes.bool,
+ weekLabel: PropTypes.string,
+ yearItemNumber: PropTypes.number,
+ yearDropdownItemNumber: PropTypes.number,
+ setOpen: PropTypes.func,
+ shouldCloseOnSelect: PropTypes.bool,
+ useShortMonthInDropdown: PropTypes.bool,
+ showDisabledMonthNavigation: PropTypes.bool,
+ previousMonthButtonLabel: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.node,
+ ]),
+ nextMonthButtonLabel: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.node,
+ ]),
+ previousYearButtonLabel: PropTypes.string,
+ nextYearButtonLabel: PropTypes.string,
+ renderCustomHeader: PropTypes.func,
+ renderDayContents: PropTypes.func,
+ onDayMouseEnter: PropTypes.func,
+ onMonthMouseLeave: PropTypes.func,
+ showPopperArrow: PropTypes.bool,
+ handleOnKeyDown: PropTypes.func,
+ handleOnDayKeyDown: PropTypes.func,
+ isInputFocused: PropTypes.bool,
+ customTimeInput: PropTypes.element,
+ weekAriaLabelPrefix: PropTypes.string,
+ setPreSelection: PropTypes.func,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.containerRef = React.createRef();
+
+ this.state = {
+ date: this.getDateInView(),
+ selectingDate: null,
+ monthContainer: null,
+ };
+ }
+
+ componentDidMount() {
+ // monthContainer height is needed in time component
+ // to determine the height for the ul in the time component
+ // setState here so height is given after final component
+ // layout is rendered
+ if (this.props.showTimeSelect) {
+ this.assignMonthContainer = (() => {
+ this.setState({ monthContainer: this.monthContainer });
+ })();
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (
+ this.props.preSelection
+ && !isSameDay(this.props.preSelection, prevProps.preSelection)
+ ) {
+ this.setState({
+ date: this.props.preSelection,
+ });
+ } else if (
+ this.props.openToDate
+ && !isSameDay(this.props.openToDate, prevProps.openToDate)
+ ) {
+ this.setState({
+ date: this.props.openToDate,
+ });
+ }
+ }
+
+ handleClickOutside = (event) => {
+ this.props.onClickOutside(event);
+ };
+
+ setClickOutsideRef = () => this.containerRef.current;
+
+ handleDropdownFocus = (event) => {
+ if (isDropdownSelect(event.target)) {
+ this.props.onDropdownFocus();
+ }
+ };
+
+ getDateInView = () => {
+ const { preSelection, selected, openToDate } = this.props;
+ const minDate = getEffectiveMinDate(this.props);
+ const maxDate = getEffectiveMaxDate(this.props);
+ const current = newDate();
+ const initialDate = openToDate || selected || preSelection;
+ if (initialDate) {
+ return initialDate;
+ }
+ if (minDate && isBefore(current, minDate)) {
+ return minDate;
+ } if (maxDate && isAfter(current, maxDate)) {
+ return maxDate;
+ }
+
+ return current;
+ };
+
+ increaseMonth = () => {
+ this.setState(
+ ({ date }) => ({
+ date: addMonths(date, 1),
+ }),
+ () => this.handleMonthChange(this.state.date),
+ );
+ };
+
+ decreaseMonth = () => {
+ this.setState(
+ ({ date }) => ({
+ date: subMonths(date, 1),
+ }),
+ () => this.handleMonthChange(this.state.date),
+ );
+ };
+
+ handleDayClick = (day, event, monthSelectedIn) => {
+ this.props.onSelect(day, event, monthSelectedIn);
+ this.props.setPreSelection && this.props.setPreSelection(day);
+ };
+
+ handleDayMouseEnter = (day) => {
+ this.setState({ selectingDate: day });
+ this.props.onDayMouseEnter && this.props.onDayMouseEnter(day);
+ };
+
+ handleMonthMouseLeave = () => {
+ this.setState({ selectingDate: null });
+ this.props.onMonthMouseLeave && this.props.onMonthMouseLeave();
+ };
+
+ handleYearChange = (date) => {
+ if (this.props.onYearChange) {
+ this.props.onYearChange(date);
+ }
+ if (this.props.adjustDateOnChange) {
+ if (this.props.onSelect) {
+ this.props.onSelect(date);
+ }
+ if (this.props.setOpen) {
+ this.props.setOpen(true);
+ }
+ }
+
+ this.props.setPreSelection && this.props.setPreSelection(date);
+ };
+
+ handleMonthChange = (date) => {
+ if (this.props.onMonthChange) {
+ this.props.onMonthChange(date);
+ }
+ if (this.props.adjustDateOnChange) {
+ if (this.props.onSelect) {
+ this.props.onSelect(date);
+ }
+ if (this.props.setOpen) {
+ this.props.setOpen(true);
+ }
+ }
+
+ this.props.setPreSelection && this.props.setPreSelection(date);
+ };
+
+ handleMonthYearChange = (date) => {
+ this.handleYearChange(date);
+ this.handleMonthChange(date);
+ };
+
+ changeYear = (year) => {
+ this.setState(
+ ({ date }) => ({
+ date: setYear(date, year),
+ }),
+ () => this.handleYearChange(this.state.date),
+ );
+ };
+
+ changeMonth = (month) => {
+ this.setState(
+ ({ date }) => ({
+ date: setMonth(date, month),
+ }),
+ () => this.handleMonthChange(this.state.date),
+ );
+ };
+
+ changeMonthYear = (monthYear) => {
+ this.setState(
+ ({ date }) => ({
+ date: setYear(setMonth(date, getMonth(monthYear)), getYear(monthYear)),
+ }),
+ () => this.handleMonthYearChange(this.state.date),
+ );
+ };
+
+ header = (date = this.state.date) => {
+ const startOfWeek = getStartOfWeek(
+ date,
+ this.props.locale,
+ this.props.calendarStartDay,
+ );
+
+ const dayNames = [];
+ if (this.props.showWeekNumbers) {
+ dayNames.push(
+ {this.props.weekLabel || '#'}
+
);
+ }
+ return dayNames.concat([0, 1, 2, 3, 4, 5, 6].map((offset) => {
+ const day = addDays(startOfWeek, offset);
+ const weekDayName = this.formatWeekday(day, this.props.locale);
+
+ const weekDayClassName = this.props.weekDayClassName
+ ? this.props.weekDayClassName(day)
+ : undefined;
+
+ return (
+
+ {weekDayName}
+
+ );
+ }));
+ };
+
+ formatWeekday = (day, locale) => {
+ if (this.props.formatWeekDay) {
+ return getFormattedWeekdayInLocale(day, this.props.formatWeekDay, locale);
+ }
+ return this.props.useWeekdaysShort
+ ? getWeekdayShortInLocale(day, locale)
+ : getWeekdayMinInLocale(day, locale);
+ };
+
+ decreaseYear = () => {
+ this.setState(
+ ({ date }) => ({
+ date: subYears(
+ date,
+ this.props.showYearPicker ? this.props.yearItemNumber : 1,
+ ),
+ }),
+ () => this.handleYearChange(this.state.date),
+ );
+ };
+
+ renderPreviousButton = () => {
+ if (this.props.renderCustomHeader) {
+ return;
+ }
+
+ let allPrevDaysDisabled;
+ switch (true) {
+ case this.props.showMonthYearPicker:
+ allPrevDaysDisabled = yearDisabledBefore(this.state.date, this.props);
+ break;
+ case this.props.showYearPicker:
+ allPrevDaysDisabled = yearsDisabledBefore(this.state.date, this.props);
+ break;
+ default:
+ allPrevDaysDisabled = monthDisabledBefore(this.state.date, this.props);
+ break;
+ }
+
+ if (
+ (!this.props.forceShowMonthNavigation
+ && !this.props.showDisabledMonthNavigation
+ && allPrevDaysDisabled)
+ || this.props.showTimeSelectOnly
+ ) {
+ return;
+ }
+
+ const iconClasses = [
+ 'react-datepicker__navigation-icon',
+ 'react-datepicker__navigation-icon--previous',
+ ];
+
+ const classes = [
+ 'react-datepicker__navigation',
+ 'react-datepicker__navigation--previous',
+ ];
+
+ let clickHandler = this.decreaseMonth;
+
+ if (
+ this.props.showMonthYearPicker
+ || this.props.showQuarterYearPicker
+ || this.props.showYearPicker
+ ) {
+ clickHandler = this.decreaseYear;
+ }
+
+ if (allPrevDaysDisabled && this.props.showDisabledMonthNavigation) {
+ classes.push('react-datepicker__navigation--previous--disabled');
+ clickHandler = null;
+ }
+
+ const isForYear = this.props.showMonthYearPicker
+ || this.props.showQuarterYearPicker
+ || this.props.showYearPicker;
+
+ const { previousMonthButtonLabel, previousYearButtonLabel } = this.props;
+
+ const {
+ previousMonthAriaLabel = typeof previousMonthButtonLabel === 'string'
+ ? previousMonthButtonLabel
+ : 'Previous Month',
+ previousYearAriaLabel = typeof previousYearButtonLabel === 'string'
+ ? previousYearButtonLabel
+ : 'Previous Year',
+ } = this.props;
+
+ return (
+
+ );
+ };
+
+ increaseYear = () => {
+ this.setState(
+ ({ date }) => ({
+ date: addYears(
+ date,
+ this.props.showYearPicker ? this.props.yearItemNumber : 1,
+ ),
+ }),
+ () => this.handleYearChange(this.state.date),
+ );
+ };
+
+ renderNextButton = () => {
+ if (this.props.renderCustomHeader) {
+ return;
+ }
+
+ let allNextDaysDisabled;
+ switch (true) {
+ case this.props.showMonthYearPicker:
+ allNextDaysDisabled = yearDisabledAfter(this.state.date, this.props);
+ break;
+ case this.props.showYearPicker:
+ allNextDaysDisabled = yearsDisabledAfter(this.state.date, this.props);
+ break;
+ default:
+ allNextDaysDisabled = monthDisabledAfter(this.state.date, this.props);
+ break;
+ }
+
+ if (
+ (!this.props.forceShowMonthNavigation
+ && !this.props.showDisabledMonthNavigation
+ && allNextDaysDisabled)
+ || this.props.showTimeSelectOnly
+ ) {
+ return;
+ }
+
+ const classes = [
+ 'react-datepicker__navigation',
+ 'react-datepicker__navigation--next',
+ ];
+ const iconClasses = [
+ 'react-datepicker__navigation-icon',
+ 'react-datepicker__navigation-icon--next',
+ ];
+ if (this.props.showTimeSelect) {
+ classes.push('react-datepicker__navigation--next--with-time');
+ }
+ if (this.props.todayButton) {
+ classes.push('react-datepicker__navigation--next--with-today-button');
+ }
+
+ let clickHandler = this.increaseMonth;
+
+ if (
+ this.props.showMonthYearPicker
+ || this.props.showQuarterYearPicker
+ || this.props.showYearPicker
+ ) {
+ clickHandler = this.increaseYear;
+ }
+
+ if (allNextDaysDisabled && this.props.showDisabledMonthNavigation) {
+ classes.push('react-datepicker__navigation--next--disabled');
+ clickHandler = null;
+ }
+
+ const isForYear = this.props.showMonthYearPicker
+ || this.props.showQuarterYearPicker
+ || this.props.showYearPicker;
+
+ const { nextMonthButtonLabel, nextYearButtonLabel } = this.props;
+ const {
+ nextMonthAriaLabel = typeof nextMonthButtonLabel === 'string'
+ ? nextMonthButtonLabel
+ : 'Next Month',
+ nextYearAriaLabel = typeof nextYearButtonLabel === 'string'
+ ? nextYearButtonLabel
+ : 'Next Year',
+ } = this.props;
+
+ return (
+
+ );
+ };
+
+ renderCurrentMonth = (date = this.state.date) => {
+ const classes = ['react-datepicker__current-month'];
+
+ if (this.props.showYearDropdown) {
+ classes.push('react-datepicker__current-month--hasYearDropdown');
+ }
+ if (this.props.showMonthDropdown) {
+ classes.push('react-datepicker__current-month--hasMonthDropdown');
+ }
+ if (this.props.showMonthYearDropdown) {
+ classes.push('react-datepicker__current-month--hasMonthYearDropdown');
+ }
+ return (
+
+ {formatDate(date, this.props.dateFormat, this.props.locale)}
+
+ );
+ };
+
+ renderYearDropdown = (overrideHide = false) => {
+ if (!this.props.showYearDropdown || overrideHide) {
+ return;
+ }
+ return (
+
+ );
+ };
+
+ renderMonthDropdown = (overrideHide = false) => {
+ if (!this.props.showMonthDropdown || overrideHide) {
+ return;
+ }
+ return (
+
+ );
+ };
+
+ renderMonthYearDropdown = (overrideHide = false) => {
+ if (!this.props.showMonthYearDropdown || overrideHide) {
+ return;
+ }
+ return (
+
+ );
+ };
+
+ renderTodayButton = () => {
+ if (!this.props.todayButton || this.props.showTimeSelectOnly) {
+ return;
+ }
+ return (
+ this.props.onSelect(getStartOfToday(), e)}
+ >
+ {this.props.todayButton}
+
+ );
+ };
+
+ renderDefaultHeader = ({ monthDate, i }) => (
+
+ {this.renderCurrentMonth(monthDate)}
+
+ {this.renderMonthDropdown(i !== 0)}
+ {this.renderMonthYearDropdown(i !== 0)}
+ {this.renderYearDropdown(i !== 0)}
+
+
+ {this.header(monthDate)}
+
+
+ );
+
+ renderCustomHeader = (headerArgs = {}) => {
+ const { monthDate, i } = headerArgs;
+
+ if (
+ (this.props.showTimeSelect && !this.state.monthContainer)
+ || this.props.showTimeSelectOnly
+ ) {
+ return null;
+ }
+
+ const prevMonthButtonDisabled = monthDisabledBefore(
+ this.state.date,
+ this.props,
+ );
+
+ const nextMonthButtonDisabled = monthDisabledAfter(
+ this.state.date,
+ this.props,
+ );
+
+ const prevYearButtonDisabled = yearDisabledBefore(
+ this.state.date,
+ this.props,
+ );
+
+ const nextYearButtonDisabled = yearDisabledAfter(
+ this.state.date,
+ this.props,
+ );
+
+ const showDayNames = !this.props.showMonthYearPicker
+ && !this.props.showQuarterYearPicker
+ && !this.props.showYearPicker;
+
+ return (
+
+ {this.props.renderCustomHeader({
+ ...this.state,
+ customHeaderCount: i,
+ monthDate,
+ changeMonth: this.changeMonth,
+ changeYear: this.changeYear,
+ decreaseMonth: this.decreaseMonth,
+ increaseMonth: this.increaseMonth,
+ decreaseYear: this.decreaseYear,
+ increaseYear: this.increaseYear,
+ prevMonthButtonDisabled,
+ nextMonthButtonDisabled,
+ prevYearButtonDisabled,
+ nextYearButtonDisabled,
+ })}
+ {showDayNames && (
+
+ {this.header(monthDate)}
+
+ )}
+
+ );
+ };
+
+ renderYearHeader = () => {
+ const { date } = this.state;
+ const { showYearPicker, yearItemNumber } = this.props;
+ const { startPeriod, endPeriod } = getYearsPeriod(date, yearItemNumber);
+ return (
+
+ {showYearPicker ? `${startPeriod} - ${endPeriod}` : getYear(date)}
+
+ );
+ };
+
+ renderHeader = (headerArgs) => {
+ switch (true) {
+ case this.props.renderCustomHeader !== undefined:
+ return this.renderCustomHeader(headerArgs);
+ case this.props.showMonthYearPicker
+ || this.props.showQuarterYearPicker
+ || this.props.showYearPicker:
+ return this.renderYearHeader(headerArgs);
+ default:
+ return this.renderDefaultHeader(headerArgs);
+ }
+ };
+
+ renderMonths = () => {
+ if (this.props.showTimeSelectOnly || this.props.showYearPicker) {
+ return;
+ }
+
+ const monthList = [];
+ const monthsToSubtract = this.props.showPreviousMonths
+ ? this.props.monthsShown - 1
+ : 0;
+ const fromMonthDate = subMonths(this.state.date, monthsToSubtract);
+ for (let i = 0; i < this.props.monthsShown; ++i) {
+ const monthsToAdd = i - this.props.monthSelectedIn;
+ const monthDate = addMonths(fromMonthDate, monthsToAdd);
+ const monthKey = `month-${i}`;
+ const monthShowsDuplicateDaysEnd = i < this.props.monthsShown - 1;
+ const monthShowsDuplicateDaysStart = i > 0;
+ monthList.push( {
+ this.monthContainer = div;
+ }}
+ className="react-datepicker__month-container"
+ >
+ {this.renderHeader({ monthDate, i })}
+
+
);
+ }
+ return monthList;
+ };
+
+ renderYears = () => {
+ if (this.props.showTimeSelectOnly) {
+ return;
+ }
+ if (this.props.showYearPicker) {
+ return (
+
+ {this.renderHeader()}
+
+
+ );
+ }
+ };
+
+ renderTimeSection = () => {
+ if (
+ this.props.showTimeSelect
+ && (this.state.monthContainer || this.props.showTimeSelectOnly)
+ ) {
+ return (
+
+ );
+ }
+ };
+
+ renderInputTimeSection = () => {
+ const time = new Date(this.props.selected);
+ const timeValid = isValid(time) && Boolean(this.props.selected);
+ const timeString = timeValid
+ ? `${addZero(time.getHours())}:${addZero(time.getMinutes())}`
+ : '';
+ if (this.props.showTimeInput) {
+ return (
+
+ );
+ }
+ };
+
+ render() {
+ const Container = this.props.container || CalendarContainer;
+ return (
+
+
+ {this.renderPreviousButton()}
+ {this.renderNextButton()}
+ {this.renderMonths()}
+ {this.renderYears()}
+ {this.renderTodayButton()}
+ {this.renderTimeSection()}
+ {this.renderInputTimeSection()}
+ {this.props.children}
+
+
+ );
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/calendar_container.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/calendar_container.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..3e12b0641cfc9c0169aea47567865f74cd9b3600
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/calendar_container.jsx
@@ -0,0 +1,25 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+export default function CalendarContainer({
+ className,
+ children,
+ showPopperArrow,
+ arrowProps = {},
+}) {
+ return (
+
+ {showPopperArrow && (
+
+ )}
+ {children}
+
+ );
+}
+
+CalendarContainer.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node,
+ arrowProps: PropTypes.object, // react-popper arrow props
+ showPopperArrow: PropTypes.bool,
+};
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/date_utils.js b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/date_utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..e10e247f97afae20726050e4a2260f76007c0390
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/date_utils.js
@@ -0,0 +1,686 @@
+import isDate from 'date-fns/isDate';
+import isValidDate from 'date-fns/isValid';
+import format from 'date-fns/format';
+import addMinutes from 'date-fns/addMinutes';
+import addHours from 'date-fns/addHours';
+import addDays from 'date-fns/addDays';
+import addWeeks from 'date-fns/addWeeks';
+import addMonths from 'date-fns/addMonths';
+import addYears from 'date-fns/addYears';
+import subMinutes from 'date-fns/subMinutes';
+import subHours from 'date-fns/subHours';
+import subDays from 'date-fns/subDays';
+import subWeeks from 'date-fns/subWeeks';
+import subMonths from 'date-fns/subMonths';
+import subYears from 'date-fns/subYears';
+import getSeconds from 'date-fns/getSeconds';
+import getMinutes from 'date-fns/getMinutes';
+import getHours from 'date-fns/getHours';
+import getDay from 'date-fns/getDay';
+import getDate from 'date-fns/getDate';
+import getISOWeek from 'date-fns/getISOWeek';
+import getMonth from 'date-fns/getMonth';
+import getQuarter from 'date-fns/getQuarter';
+import getYear from 'date-fns/getYear';
+import getTime from 'date-fns/getTime';
+import setSeconds from 'date-fns/setSeconds';
+import setMinutes from 'date-fns/setMinutes';
+import setHours from 'date-fns/setHours';
+import setMonth from 'date-fns/setMonth';
+import setQuarter from 'date-fns/setQuarter';
+import setYear from 'date-fns/setYear';
+import min from 'date-fns/min';
+import max from 'date-fns/max';
+import differenceInCalendarDays from 'date-fns/differenceInCalendarDays';
+import differenceInCalendarMonths from 'date-fns/differenceInCalendarMonths';
+import differenceInCalendarWeeks from 'date-fns/differenceInCalendarWeeks';
+import differenceInCalendarYears from 'date-fns/differenceInCalendarYears';
+import startOfDay from 'date-fns/startOfDay';
+import startOfWeek from 'date-fns/startOfWeek';
+import startOfMonth from 'date-fns/startOfMonth';
+import startOfQuarter from 'date-fns/startOfQuarter';
+import startOfYear from 'date-fns/startOfYear';
+import endOfDay from 'date-fns/endOfDay';
+import endOfWeek from 'date-fns/endOfWeek';
+import endOfMonth from 'date-fns/endOfMonth';
+import dfIsEqual from 'date-fns/isEqual';
+import dfIsSameDay from 'date-fns/isSameDay';
+import dfIsSameMonth from 'date-fns/isSameMonth';
+import dfIsSameYear from 'date-fns/isSameYear';
+import dfIsSameQuarter from 'date-fns/isSameQuarter';
+import isAfter from 'date-fns/isAfter';
+import isBefore from 'date-fns/isBefore';
+import isWithinInterval from 'date-fns/isWithinInterval';
+import toDate from 'date-fns/toDate';
+import parse from 'date-fns/parse';
+import parseISO from 'date-fns/parseISO';
+import longFormatters from 'date-fns/_lib/format/longFormatters';
+
+export const DEFAULT_YEAR_ITEM_NUMBER = 12;
+
+// This RegExp catches symbols escaped by quotes, and also
+// sequences of symbols P, p, and the combinations like `PPPPPPPppppp`
+const longFormattingTokensRegExp = /P+p+|P+|p+|''|'(''|[^'])+('|$)|./g;
+
+// ** Date Constructors **
+
+export function newDate(value) {
+ const d = value
+ ? typeof value === 'string' || value instanceof String
+ ? parseISO(value)
+ : toDate(value)
+ : new Date();
+ return isValid(d) ? d : null;
+}
+
+export function parseDate(value, dateFormat, locale, strictParsing, minDate) {
+ let parsedDate = null;
+ const localeObject = getLocaleObject(locale) || getLocaleObject(getDefaultLocale());
+ let strictParsingValueMatch = true;
+ if (Array.isArray(dateFormat)) {
+ dateFormat.forEach((df) => {
+ const tryParseDate = parse(value, df, new Date(), { locale: localeObject });
+ if (strictParsing) {
+ strictParsingValueMatch = isValid(tryParseDate, minDate)
+ && value === format(tryParseDate, df, { awareOfUnicodeTokens: true });
+ }
+ if (isValid(tryParseDate, minDate) && strictParsingValueMatch) {
+ parsedDate = tryParseDate;
+ }
+ });
+ return parsedDate;
+ }
+
+ parsedDate = parse(value, dateFormat, new Date(), { locale: localeObject });
+
+ if (strictParsing) {
+ strictParsingValueMatch = isValid(parsedDate)
+ && value === format(parsedDate, dateFormat, { awareOfUnicodeTokens: true });
+ } else if (!isValid(parsedDate)) {
+ dateFormat = dateFormat
+ .match(longFormattingTokensRegExp)
+ .map((substring) => {
+ const firstCharacter = substring[0];
+ if (firstCharacter === 'p' || firstCharacter === 'P') {
+ const longFormatter = longFormatters[firstCharacter];
+ return localeObject
+ ? longFormatter(substring, localeObject.formatLong)
+ : firstCharacter;
+ }
+ return substring;
+ })
+ .join('');
+
+ if (value.length > 0) {
+ parsedDate = parse(value, dateFormat.slice(0, value.length), new Date());
+ }
+
+ if (!isValid(parsedDate)) {
+ parsedDate = new Date(value);
+ }
+ }
+
+ return isValid(parsedDate) && strictParsingValueMatch ? parsedDate : null;
+}
+
+// ** Date "Reflection" **
+
+export { isDate };
+
+export function isValid(date, minDate) {
+ minDate = minDate ? minDate : new Date('1/1/1000');
+ return isValidDate(date) && isAfter(date, minDate);
+}
+
+// ** Date Formatting **
+
+export function formatDate(date, formatStr, locale) {
+ if (locale === 'en') {
+ return format(date, formatStr, { awareOfUnicodeTokens: true });
+ }
+ let localeObj = getLocaleObject(locale);
+ if (locale && !localeObj) {
+ console.warn(`A locale object was not found for the provided string ["${locale}"].`);
+ }
+ if (
+ !localeObj
+ && !!getDefaultLocale()
+ && !!getLocaleObject(getDefaultLocale())
+ ) {
+ localeObj = getLocaleObject(getDefaultLocale());
+ }
+ return format(date, formatStr, {
+ locale: localeObj ? localeObj : null,
+ awareOfUnicodeTokens: true,
+ });
+}
+
+export function safeDateFormat(date, { dateFormat, locale }) {
+ return (
+ (date
+ && formatDate(
+ date,
+ Array.isArray(dateFormat) ? dateFormat[0] : dateFormat,
+ locale,
+ ))
+ || ''
+ );
+}
+
+export function safeDateRangeFormat(startDate, endDate, props) {
+ if (!startDate) {
+ return '';
+ }
+
+ const formattedStartDate = safeDateFormat(startDate, props);
+ const formattedEndDate = endDate ? safeDateFormat(endDate, props) : '';
+
+ return `${formattedStartDate} - ${formattedEndDate}`;
+}
+
+// ** Date Setters **
+
+export function setTime(date, { hour = 0, minute = 0, second = 0 }) {
+ return setHours(setMinutes(setSeconds(date, second), minute), hour);
+}
+
+export { setMinutes, setHours, setMonth, setQuarter, setYear };
+
+// ** Date Getters **
+
+// getDay Returns day of week, getDate returns day of month
+export {
+ getSeconds,
+ getMinutes,
+ getHours,
+ getMonth,
+ getQuarter,
+ getYear,
+ getDay,
+ getDate,
+ getTime,
+};
+
+export function getWeek(date, locale) {
+ const localeObj = (locale && getLocaleObject(locale))
+ || (getDefaultLocale() && getLocaleObject(getDefaultLocale()));
+ return getISOWeek(date, localeObj ? { locale: localeObj } : null);
+}
+
+export function getDayOfWeekCode(day, locale) {
+ return formatDate(day, 'ddd', locale);
+}
+
+// *** Start of ***
+
+export function getStartOfDay(date) {
+ return startOfDay(date);
+}
+
+export function getStartOfWeek(date, locale, calendarStartDay) {
+ const localeObj = locale
+ ? getLocaleObject(locale)
+ : getLocaleObject(getDefaultLocale());
+ return startOfWeek(date, {
+ locale: localeObj,
+ weekStartsOn: calendarStartDay,
+ });
+}
+
+export function getStartOfMonth(date) {
+ return startOfMonth(date);
+}
+
+export function getStartOfYear(date) {
+ return startOfYear(date);
+}
+
+export function getStartOfQuarter(date) {
+ return startOfQuarter(date);
+}
+
+export function getStartOfToday() {
+ return startOfDay(newDate());
+}
+
+// *** End of ***
+
+export function getEndOfWeek(date) {
+ return endOfWeek(date);
+}
+
+export function getEndOfMonth(date) {
+ return endOfMonth(date);
+}
+
+// ** Date Math **
+
+// *** Addition ***
+
+export { addMinutes, addDays, addWeeks, addMonths, addYears };
+
+// *** Subtraction ***
+
+export {
+ addHours,
+ subMinutes,
+ subHours,
+ subDays,
+ subWeeks,
+ subMonths,
+ subYears,
+};
+
+// ** Date Comparison **
+
+export { isBefore, isAfter };
+
+export function isSameYear(date1, date2) {
+ if (date1 && date2) {
+ return dfIsSameYear(date1, date2);
+ }
+ return !date1 && !date2;
+}
+
+export function isSameMonth(date1, date2) {
+ if (date1 && date2) {
+ return dfIsSameMonth(date1, date2);
+ }
+ return !date1 && !date2;
+}
+
+export function isSameQuarter(date1, date2) {
+ if (date1 && date2) {
+ return dfIsSameQuarter(date1, date2);
+ }
+ return !date1 && !date2;
+}
+
+export function isSameDay(date1, date2) {
+ if (date1 && date2) {
+ return dfIsSameDay(date1, date2);
+ }
+ return !date1 && !date2;
+}
+
+export function isEqual(date1, date2) {
+ if (date1 && date2) {
+ return dfIsEqual(date1, date2);
+ }
+ return !date1 && !date2;
+}
+
+export function isDayInRange(day, startDate, endDate) {
+ let valid;
+ const start = startOfDay(startDate);
+ const end = endOfDay(endDate);
+
+ try {
+ valid = isWithinInterval(day, { start, end });
+ } catch (err) {
+ valid = false;
+ }
+ return valid;
+}
+
+// *** Diffing ***
+
+export function getDaysDiff(date1, date2) {
+ return differenceInCalendarDays(date1, date2);
+}
+
+// ** Date Localization **
+
+export function registerLocale(localeName, localeData) {
+ const scope = typeof window !== 'undefined' ? window : global;
+
+ if (!scope.__localeData__) {
+ scope.__localeData__ = {};
+ }
+ scope.__localeData__[localeName] = localeData;
+}
+
+export function setDefaultLocale(localeName) {
+ const scope = typeof window !== 'undefined' ? window : global;
+
+ scope.__localeId__ = localeName;
+}
+
+export function getDefaultLocale() {
+ const scope = typeof window !== 'undefined' ? window : global;
+
+ return scope.__localeId__;
+}
+
+export function getLocaleObject(localeSpec) {
+ if (typeof localeSpec === 'string') {
+ // Treat it as a locale name registered by registerLocale
+ const scope = typeof window !== 'undefined' ? window : global;
+ return scope.__localeData__ ? scope.__localeData__[localeSpec] : null;
+ }
+ // Treat it as a raw date-fns locale object
+ return localeSpec;
+}
+
+export function getFormattedWeekdayInLocale(date, formatFunc, locale) {
+ return formatFunc(formatDate(date, 'EEEE', locale));
+}
+
+export function getWeekdayMinInLocale(date, locale) {
+ return formatDate(date, 'EEEEEE', locale);
+}
+
+export function getWeekdayShortInLocale(date, locale) {
+ return formatDate(date, 'EEE', locale);
+}
+
+export function getMonthInLocale(month, locale) {
+ return formatDate(setMonth(newDate(), month), 'LLLL', locale);
+}
+
+export function getMonthShortInLocale(month, locale) {
+ return formatDate(setMonth(newDate(), month), 'LLL', locale);
+}
+
+export function getQuarterShortInLocale(quarter, locale) {
+ return formatDate(setQuarter(newDate(), quarter), 'QQQ', locale);
+}
+
+// ** Utils for some components **
+
+export function isDayDisabled(
+ day,
+ { minDate, maxDate, excludeDates, includeDates, filterDate } = {},
+) {
+ return (
+ isOutOfBounds(day, { minDate, maxDate })
+ || (excludeDates
+ && excludeDates.some(excludeDate => isSameDay(day, excludeDate)))
+ || (includeDates
+ && !includeDates.some(includeDate => isSameDay(day, includeDate)))
+ || (filterDate && !filterDate(newDate(day)))
+ || false
+ );
+}
+
+export function isDayExcluded(day, { excludeDates } = {}) {
+ return (
+ (excludeDates
+ && excludeDates.some(excludeDate => isSameDay(day, excludeDate)))
+ || false
+ );
+}
+
+export function isMonthDisabled(
+ month,
+ { minDate, maxDate, excludeDates, includeDates, filterDate } = {},
+) {
+ return (
+ isOutOfBounds(month, { minDate, maxDate })
+ || (excludeDates
+ && excludeDates.some(excludeDate => isSameMonth(month, excludeDate)))
+ || (includeDates
+ && !includeDates.some(includeDate => isSameMonth(month, includeDate)))
+ || (filterDate && !filterDate(newDate(month)))
+ || false
+ );
+}
+
+export function isMonthinRange(startDate, endDate, m, day) {
+ const startDateYear = getYear(startDate);
+ const startDateMonth = getMonth(startDate);
+ const endDateYear = getYear(endDate);
+ const endDateMonth = getMonth(endDate);
+ const dayYear = getYear(day);
+ if (startDateYear === endDateYear && startDateYear === dayYear) {
+ return startDateMonth <= m && m <= endDateMonth;
+ } if (startDateYear < endDateYear) {
+ return (
+ (dayYear === startDateYear && startDateMonth <= m)
+ || (dayYear === endDateYear && endDateMonth >= m)
+ || (dayYear < endDateYear && dayYear > startDateYear)
+ );
+ }
+}
+
+export function isQuarterDisabled(
+ quarter,
+ { minDate, maxDate, excludeDates, includeDates, filterDate } = {},
+) {
+ return (
+ isOutOfBounds(quarter, { minDate, maxDate })
+ || (excludeDates
+ && excludeDates.some(excludeDate => isSameQuarter(quarter, excludeDate)))
+ || (includeDates
+ && !includeDates.some(includeDate => isSameQuarter(quarter, includeDate)))
+ || (filterDate && !filterDate(newDate(quarter)))
+ || false
+ );
+}
+
+export function isYearDisabled(year, { minDate, maxDate } = {}) {
+ const date = new Date(year, 0, 1);
+ return isOutOfBounds(date, { minDate, maxDate }) || false;
+}
+
+export function isQuarterInRange(startDate, endDate, q, day) {
+ const startDateYear = getYear(startDate);
+ const startDateQuarter = getQuarter(startDate);
+ const endDateYear = getYear(endDate);
+ const endDateQuarter = getQuarter(endDate);
+ const dayYear = getYear(day);
+ if (startDateYear === endDateYear && startDateYear === dayYear) {
+ return startDateQuarter <= q && q <= endDateQuarter;
+ } if (startDateYear < endDateYear) {
+ return (
+ (dayYear === startDateYear && startDateQuarter <= q)
+ || (dayYear === endDateYear && endDateQuarter >= q)
+ || (dayYear < endDateYear && dayYear > startDateYear)
+ );
+ }
+}
+
+export function isOutOfBounds(day, { minDate, maxDate } = {}) {
+ return (
+ (minDate && differenceInCalendarDays(day, minDate) < 0)
+ || (maxDate && differenceInCalendarDays(day, maxDate) > 0)
+ );
+}
+
+export function isTimeInList(time, times) {
+ return times.some(listTime => getHours(listTime) === getHours(time)
+ && getMinutes(listTime) === getMinutes(time));
+}
+
+export function isTimeDisabled(
+ time,
+ { excludeTimes, includeTimes, filterTime } = {},
+) {
+ return (
+ (excludeTimes && isTimeInList(time, excludeTimes))
+ || (includeTimes && !isTimeInList(time, includeTimes))
+ || (filterTime && !filterTime(time))
+ || false
+ );
+}
+
+export function isTimeInDisabledRange(time, { minTime, maxTime }) {
+ if (!minTime || !maxTime) {
+ throw new Error('Both minTime and maxTime props required');
+ }
+ const base = newDate();
+ const baseTime = setHours(setMinutes(base, getMinutes(time)), getHours(time));
+ const min = setHours(
+ setMinutes(base, getMinutes(minTime)),
+ getHours(minTime),
+ );
+ const max = setHours(
+ setMinutes(base, getMinutes(maxTime)),
+ getHours(maxTime),
+ );
+
+ let valid;
+ try {
+ valid = !isWithinInterval(baseTime, { start: min, end: max });
+ } catch (err) {
+ valid = false;
+ }
+ return valid;
+}
+
+export function monthDisabledBefore(day, { minDate, includeDates } = {}) {
+ const previousMonth = subMonths(day, 1);
+ return (
+ (minDate && differenceInCalendarMonths(minDate, previousMonth) > 0)
+ || (includeDates
+ && includeDates.every(includeDate => differenceInCalendarMonths(includeDate, previousMonth) > 0))
+ || false
+ );
+}
+
+export function monthDisabledAfter(day, { maxDate, includeDates } = {}) {
+ const nextMonth = addMonths(day, 1);
+ return (
+ (maxDate && differenceInCalendarMonths(nextMonth, maxDate) > 0)
+ || (includeDates
+ && includeDates.every(includeDate => differenceInCalendarMonths(nextMonth, includeDate) > 0))
+ || false
+ );
+}
+
+export function yearDisabledBefore(day, { minDate, includeDates } = {}) {
+ const previousYear = subYears(day, 1);
+ return (
+ (minDate && differenceInCalendarYears(minDate, previousYear) > 0)
+ || (includeDates
+ && includeDates.every(includeDate => differenceInCalendarYears(includeDate, previousYear) > 0))
+ || false
+ );
+}
+
+export function yearsDisabledBefore(
+ day,
+ { minDate, yearItemNumber = DEFAULT_YEAR_ITEM_NUMBER } = {},
+) {
+ const previousYear = getStartOfYear(subYears(day, yearItemNumber));
+ const { endPeriod } = getYearsPeriod(previousYear, yearItemNumber);
+ const minDateYear = minDate && getYear(minDate);
+ return (minDateYear && minDateYear > endPeriod) || false;
+}
+
+export function yearDisabledAfter(day, { maxDate, includeDates } = {}) {
+ const nextYear = addYears(day, 1);
+ return (
+ (maxDate && differenceInCalendarYears(nextYear, maxDate) > 0)
+ || (includeDates
+ && includeDates.every(includeDate => differenceInCalendarYears(nextYear, includeDate) > 0))
+ || false
+ );
+}
+
+export function yearsDisabledAfter(
+ day,
+ { maxDate, yearItemNumber = DEFAULT_YEAR_ITEM_NUMBER } = {},
+) {
+ const nextYear = addYears(day, yearItemNumber);
+ const { startPeriod } = getYearsPeriod(nextYear, yearItemNumber);
+ const maxDateYear = maxDate && getYear(maxDate);
+ return (maxDateYear && maxDateYear < startPeriod) || false;
+}
+
+export function getEffectiveMinDate({ minDate, includeDates }) {
+ if (includeDates && minDate) {
+ const minDates = includeDates.filter(includeDate => differenceInCalendarDays(includeDate, minDate) >= 0);
+ return min(minDates);
+ } if (includeDates) {
+ return min(includeDates);
+ }
+ return minDate;
+}
+
+export function getEffectiveMaxDate({ maxDate, includeDates }) {
+ if (includeDates && maxDate) {
+ const maxDates = includeDates.filter(includeDate => differenceInCalendarDays(includeDate, maxDate) <= 0);
+ return max(maxDates);
+ } if (includeDates) {
+ return max(includeDates);
+ }
+ return maxDate;
+}
+
+export function getHightLightDaysMap(
+ highlightDates = [],
+ defaultClassName = 'react-datepicker__day--highlighted',
+) {
+ const dateClasses = new Map();
+ for (let i = 0, len = highlightDates.length; i < len; i++) {
+ const obj = highlightDates[i];
+ if (isDate(obj)) {
+ const key = formatDate(obj, 'MM.dd.yyyy');
+ const classNamesArr = dateClasses.get(key) || [];
+ if (!classNamesArr.includes(defaultClassName)) {
+ classNamesArr.push(defaultClassName);
+ dateClasses.set(key, classNamesArr);
+ }
+ } else if (typeof obj === 'object') {
+ const keys = Object.keys(obj);
+ const className = keys[0];
+ const arrOfDates = obj[keys[0]];
+ if (typeof className === 'string' && arrOfDates.constructor === Array) {
+ for (let k = 0, len = arrOfDates.length; k < len; k++) {
+ const key = formatDate(arrOfDates[k], 'MM.dd.yyyy');
+ const classNamesArr = dateClasses.get(key) || [];
+ if (!classNamesArr.includes(className)) {
+ classNamesArr.push(className);
+ dateClasses.set(key, classNamesArr);
+ }
+ }
+ }
+ }
+ }
+
+ return dateClasses;
+}
+
+export function timesToInjectAfter(
+ startOfDay,
+ currentTime,
+ currentMultiplier,
+ intervals,
+ injectedTimes,
+) {
+ const l = injectedTimes.length;
+ const times = [];
+ for (let i = 0; i < l; i++) {
+ const injectedTime = addMinutes(
+ addHours(startOfDay, getHours(injectedTimes[i])),
+ getMinutes(injectedTimes[i]),
+ );
+ const nextTime = addMinutes(
+ startOfDay,
+ (currentMultiplier + 1) * intervals,
+ );
+
+ if (
+ isAfter(injectedTime, currentTime)
+ && isBefore(injectedTime, nextTime)
+ ) {
+ times.push(injectedTimes[i]);
+ }
+ }
+
+ return times;
+}
+
+export function addZero(i) {
+ return i < 10 ? `0${i}` : `${i}`;
+}
+
+export function getYearsPeriod(
+ date,
+ yearItemNumber = DEFAULT_YEAR_ITEM_NUMBER,
+) {
+ const endPeriod = Math.ceil(getYear(date) / yearItemNumber) * yearItemNumber;
+ const startPeriod = endPeriod - (yearItemNumber - 1);
+ return { startPeriod, endPeriod };
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/day.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/day.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..94dbe20608df925124b7435dd479aea35477496a
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/day.jsx
@@ -0,0 +1,332 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import {
+ getDay,
+ getMonth,
+ getDate,
+ newDate,
+ isSameDay,
+ isDayDisabled,
+ isDayExcluded,
+ isDayInRange,
+ isEqual,
+ isBefore,
+ isAfter,
+ getDayOfWeekCode,
+ formatDate,
+} from './date_utils';
+
+export default class Day extends React.Component {
+ static propTypes = {
+ ariaLabelPrefixWhenEnabled: PropTypes.string,
+ ariaLabelPrefixWhenDisabled: PropTypes.string,
+ disabledKeyboardNavigation: PropTypes.bool,
+ day: PropTypes.instanceOf(Date).isRequired,
+ dayClassName: PropTypes.func,
+ endDate: PropTypes.instanceOf(Date),
+ highlightDates: PropTypes.instanceOf(Map),
+ inline: PropTypes.bool,
+ shouldFocusDayInline: PropTypes.bool,
+ month: PropTypes.number,
+ onClick: PropTypes.func,
+ onMouseEnter: PropTypes.func,
+ preSelection: PropTypes.instanceOf(Date),
+ selected: PropTypes.object,
+ selectingDate: PropTypes.instanceOf(Date),
+ selectsEnd: PropTypes.bool,
+ selectsStart: PropTypes.bool,
+ selectsRange: PropTypes.bool,
+ startDate: PropTypes.instanceOf(Date),
+ renderDayContents: PropTypes.func,
+ handleOnKeyDown: PropTypes.func,
+ containerRef: PropTypes.oneOfType([
+ PropTypes.func,
+ // PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
+ ]),
+ monthShowsDuplicateDaysEnd: PropTypes.bool,
+ monthShowsDuplicateDaysStart: PropTypes.bool,
+ locale: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.shape({ locale: PropTypes.object }),
+ ]),
+ };
+
+ componentDidMount() {
+ this.handleFocusDay();
+ }
+
+ componentDidUpdate(prevProps) {
+ this.handleFocusDay(prevProps);
+ }
+
+ dayEl = React.createRef();
+
+ handleClick = (event) => {
+ if (!this.isDisabled() && this.props.onClick) {
+ this.props.onClick(event);
+ }
+ };
+
+ handleMouseEnter = (event) => {
+ if (!this.isDisabled() && this.props.onMouseEnter) {
+ this.props.onMouseEnter(event);
+ }
+ };
+
+ handleOnKeyDown = (event) => {
+ const eventKey = event.key;
+ if (eventKey === ' ') {
+ event.preventDefault();
+ event.key = 'Enter';
+ }
+
+ this.props.handleOnKeyDown(event);
+ };
+
+ isSameDay = other => isSameDay(this.props.day, other);
+
+ isKeyboardSelected = () => !this.props.disabledKeyboardNavigation
+ && !this.isSameDay(this.props.selected)
+ && this.isSameDay(this.props.preSelection);
+
+ isDisabled = () => isDayDisabled(this.props.day, this.props);
+
+ isExcluded = () => isDayExcluded(this.props.day, this.props);
+
+ getHighLightedClass = (defaultClassName) => {
+ const { day, highlightDates } = this.props;
+
+ if (!highlightDates) {
+ return false;
+ }
+
+ // Looking for className in the Map of {'day string, 'className'}
+ const dayStr = formatDate(day, 'MM.dd.yyyy');
+ return highlightDates.get(dayStr);
+ };
+
+ isInRange = () => {
+ const { day, startDate, endDate } = this.props;
+ if (!startDate || !endDate) {
+ return false;
+ }
+ return isDayInRange(day, startDate, endDate);
+ };
+
+ isInSelectingRange = () => {
+ const { day, selectsStart, selectsEnd, selectsRange, startDate, endDate } = this.props;
+
+ const selectingDate = this.props.selectingDate ?? this.props.preSelection;
+
+ if (
+ !(selectsStart || selectsEnd || selectsRange)
+ || !selectingDate
+ || this.isDisabled()
+ ) {
+ return false;
+ }
+
+ if (
+ selectsStart
+ && endDate
+ && (isBefore(selectingDate, endDate) || isEqual(selectingDate, endDate))
+ ) {
+ return isDayInRange(day, selectingDate, endDate);
+ }
+
+ if (
+ selectsEnd
+ && startDate
+ && (isAfter(selectingDate, startDate) || isEqual(selectingDate, startDate))
+ ) {
+ return isDayInRange(day, startDate, selectingDate);
+ }
+
+ if (
+ selectsRange
+ && startDate
+ && !endDate
+ && (isAfter(selectingDate, startDate) || isEqual(selectingDate, startDate))
+ ) {
+ return isDayInRange(day, startDate, selectingDate);
+ }
+
+ return false;
+ };
+
+ isSelectingRangeStart = () => {
+ if (!this.isInSelectingRange()) {
+ return false;
+ }
+
+ const { day, startDate, selectsStart } = this.props;
+ const selectingDate = this.props.selectingDate ?? this.props.preSelection;
+
+ if (selectsStart) {
+ return isSameDay(day, selectingDate);
+ }
+ return isSameDay(day, startDate);
+ };
+
+ isSelectingRangeEnd = () => {
+ if (!this.isInSelectingRange()) {
+ return false;
+ }
+
+ const { day, endDate, selectsEnd } = this.props;
+ const selectingDate = this.props.selectingDate ?? this.props.preSelection;
+
+ if (selectsEnd) {
+ return isSameDay(day, selectingDate);
+ }
+ return isSameDay(day, endDate);
+ };
+
+ isRangeStart = () => {
+ const { day, startDate, endDate } = this.props;
+ if (!startDate || !endDate) {
+ return false;
+ }
+ return isSameDay(startDate, day);
+ };
+
+ isRangeEnd = () => {
+ const { day, startDate, endDate } = this.props;
+ if (!startDate || !endDate) {
+ return false;
+ }
+ return isSameDay(endDate, day);
+ };
+
+ isWeekend = () => {
+ const weekday = getDay(this.props.day);
+ return weekday === 0 || weekday === 6;
+ };
+
+ isOutsideMonth = () => (
+ this.props.month !== undefined
+ && this.props.month !== getMonth(this.props.day)
+ );
+
+ getClassNames = (date) => {
+ const dayClassName = this.props.dayClassName
+ ? this.props.dayClassName(date)
+ : undefined;
+ return classnames(
+ 'react-datepicker__day',
+ dayClassName,
+ `react-datepicker__day--${getDayOfWeekCode(this.props.day)}`,
+ {
+ 'react-datepicker__day--disabled': this.isDisabled(),
+ 'react-datepicker__day--excluded': this.isExcluded(),
+ 'react-datepicker__day--selected': this.isSameDay(this.props.selected),
+ // 'react-datepicker__day--keyboard-selected': this.isKeyboardSelected(),
+ 'react-datepicker__day--range-start': this.isRangeStart(),
+ 'react-datepicker__day--range-end': this.isRangeEnd(),
+ 'react-datepicker__day--in-range': this.isInRange(),
+ 'react-datepicker__day--in-selecting-range': this.isInSelectingRange(),
+ 'react-datepicker__day--selecting-range-start':
+ this.isSelectingRangeStart(),
+ 'react-datepicker__day--selecting-range-end':
+ this.isSelectingRangeEnd(),
+ 'react-datepicker__day--today': this.isSameDay(newDate()),
+ 'react-datepicker__day--weekend': this.isWeekend(),
+ 'react-datepicker__day--outside-month': this.isOutsideMonth(),
+ },
+ this.getHighLightedClass('react-datepicker__day--highlighted'),
+ );
+ };
+
+ getAriaLabel = () => {
+ const {
+ day,
+ ariaLabelPrefixWhenEnabled = 'Choose',
+ ariaLabelPrefixWhenDisabled = 'Not available',
+ } = this.props;
+
+ const prefix = this.isDisabled() || this.isExcluded()
+ ? ariaLabelPrefixWhenDisabled
+ : ariaLabelPrefixWhenEnabled;
+
+ return `${prefix} ${formatDate(day, 'PPPP', this.props.locale)}`;
+ };
+
+ getTabIndex = (selected, preSelection) => {
+ const selectedDay = selected || this.props.selected;
+ const preSelectionDay = preSelection || this.props.preSelection;
+
+ const tabIndex = this.isKeyboardSelected()
+ || (this.isSameDay(selectedDay) && isSameDay(preSelectionDay, selectedDay))
+ ? 0
+ : -1;
+
+ return tabIndex;
+ };
+
+ // various cases when we need to apply focus to the preselected day
+ // focus the day on mount/update so that keyboard navigation works while cycling through months with up or down keys (not for prev and next month buttons)
+ // prevent focus for these activeElement cases so we don't pull focus from the input as the calendar opens
+ handleFocusDay = (prevProps = {}) => {
+ let shouldFocusDay = false;
+ // only do this while the input isn't focused
+ // otherwise, typing/backspacing the date manually may steal focus away from the input
+ if (
+ this.getTabIndex() === 0
+ && !prevProps.isInputFocused
+ && this.isSameDay(this.props.preSelection)
+ ) {
+ // there is currently no activeElement and not inline
+ if (!document.activeElement || document.activeElement === document.body) {
+ shouldFocusDay = true;
+ }
+ // inline version:
+ // do not focus on initial render to prevent autoFocus issue
+ // focus after month has changed via keyboard
+ if (this.props.inline && !this.props.shouldFocusDayInline) {
+ shouldFocusDay = false;
+ }
+ // the activeElement is in the container, and it is another instance of Day
+ if (
+ this.props.containerRef
+ && this.props.containerRef.current
+ && this.props.containerRef.current.contains(document.activeElement)
+ && document.activeElement.classList.contains('react-datepicker__day')
+ ) {
+ shouldFocusDay = true;
+ }
+ }
+
+ shouldFocusDay && this.dayEl.current.focus({ preventScroll: true });
+ };
+
+ renderDayContents = () => {
+ if (this.isOutsideMonth()) {
+ if (this.props.monthShowsDuplicateDaysEnd && getDate(this.props.day) < 10) return null;
+ if (
+ this.props.monthShowsDuplicateDaysStart
+ && getDate(this.props.day) > 20
+ ) return null;
+ }
+
+ return this.props.renderDayContents
+ ? this.props.renderDayContents(getDate(this.props.day), this.props.day)
+ : getDate(this.props.day);
+ };
+
+ render = () => (
+
+ {this.renderDayContents()}
+
+ );
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/index.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/index.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..8cdcb853d81187cc9f0280d71346fc270deed82b
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/index.jsx
@@ -0,0 +1,1074 @@
+/* eslint-disable no-nested-ternary */
+import React from 'react';
+import PropTypes from 'prop-types';
+import Calendar from './calendar';
+import Portal from './portal';
+import PopperComponent, { popperPlacementPositions } from './popper_component';
+import classnames from 'classnames';
+import startOfDay from 'date-fns/startOfDay';
+import endOfDay from 'date-fns/endOfDay';
+import {
+ newDate,
+ isDate,
+ isBefore,
+ isAfter,
+ isEqual,
+ setTime,
+ getSeconds,
+ getMinutes,
+ getHours,
+ addDays,
+ addMonths,
+ addWeeks,
+ addYears,
+ subDays,
+ subMonths,
+ subWeeks,
+ subYears,
+ isDayDisabled,
+ isDayInRange,
+ getEffectiveMinDate,
+ getEffectiveMaxDate,
+ parseDate,
+ safeDateFormat,
+ safeDateRangeFormat,
+ getHightLightDaysMap,
+ getYear,
+ getMonth,
+ registerLocale,
+ setDefaultLocale,
+ getDefaultLocale,
+ DEFAULT_YEAR_ITEM_NUMBER,
+} from './date_utils';
+import onClickOutside from 'react-onclickoutside';
+
+export { default as CalendarContainer } from './calendar_container';
+
+export { registerLocale, setDefaultLocale, getDefaultLocale };
+
+const outsideClickIgnoreClass = 'react-datepicker-ignore-onclickoutside';
+const WrappedCalendar = onClickOutside(Calendar);
+
+// Compares dates year+month combinations
+function hasPreSelectionChanged(date1, date2) {
+ if (date1 && date2) {
+ return (
+ getMonth(date1) !== getMonth(date2) || getYear(date1) !== getYear(date2)
+ );
+ }
+
+ return date1 !== date2;
+}
+
+/**
+ * General datepicker component.
+ */
+const INPUT_ERR_1 = 'Date input not valid.';
+
+export default class DatePicker extends React.Component {
+ static get defaultProps() {
+ return {
+ allowSameDay: false,
+ dateFormat: 'MM/dd/yyyy',
+ dateFormatCalendar: 'LLLL yyyy',
+ onChange() {},
+ disabled: false,
+ disabledKeyboardNavigation: false,
+ dropdownMode: 'scroll',
+ onFocus() {},
+ onBlur() {},
+ onKeyDown() {},
+ onInputClick() {},
+ onSelect() {},
+ onClickOutside() {},
+ onMonthChange() {},
+ onCalendarOpen() {},
+ onCalendarClose() {},
+ preventOpenOnFocus: false,
+ onYearChange() {},
+ onInputError() {},
+ monthsShown: 1,
+ readOnly: false,
+ withPortal: false,
+ shouldCloseOnSelect: true,
+ showTimeSelect: false,
+ showTimeInput: false,
+ showPreviousMonths: false,
+ showMonthYearPicker: false,
+ showFullMonthYearPicker: false,
+ showTwoColumnMonthYearPicker: false,
+ showFourColumnMonthYearPicker: false,
+ showYearPicker: false,
+ showQuarterYearPicker: false,
+ strictParsing: false,
+ timeIntervals: 30,
+ timeCaption: 'Time',
+ previousMonthAriaLabel: 'Previous Month',
+ previousMonthButtonLabel: 'Previous Month',
+ nextMonthAriaLabel: 'Next Month',
+ nextMonthButtonLabel: 'Next Month',
+ previousYearAriaLabel: 'Previous Year',
+ previousYearButtonLabel: 'Previous Year',
+ nextYearAriaLabel: 'Next Year',
+ nextYearButtonLabel: 'Next Year',
+ timeInputLabel: 'Time',
+ enableTabLoop: true,
+ yearItemNumber: DEFAULT_YEAR_ITEM_NUMBER,
+
+ renderDayContents(date) {
+ return date;
+ },
+ focusSelectedMonth: false,
+ showPopperArrow: true,
+ excludeScrollbar: true,
+ customTimeInput: null,
+ calendarStartDay: undefined,
+ };
+ }
+
+ static propTypes = {
+ adjustDateOnChange: PropTypes.bool,
+ allowSameDay: PropTypes.bool,
+ ariaDescribedBy: PropTypes.string,
+ ariaInvalid: PropTypes.string,
+ ariaLabelClose: PropTypes.string,
+ ariaLabelledBy: PropTypes.string,
+ ariaRequired: PropTypes.string,
+ autoComplete: PropTypes.string,
+ autoFocus: PropTypes.bool,
+ calendarClassName: PropTypes.string,
+ calendarContainer: PropTypes.func,
+ children: PropTypes.node,
+ chooseDayAriaLabelPrefix: PropTypes.string,
+ closeOnScroll: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
+ className: PropTypes.string,
+ customInput: PropTypes.element,
+ customInputRef: PropTypes.string,
+ calendarStartDay: PropTypes.number,
+ // eslint-disable-next-line react/no-unused-prop-types
+ dateFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
+ dateFormatCalendar: PropTypes.string,
+ dayClassName: PropTypes.func,
+ weekDayClassName: PropTypes.func,
+ disabledDayAriaLabelPrefix: PropTypes.string,
+ monthClassName: PropTypes.func,
+ timeClassName: PropTypes.func,
+ disabled: PropTypes.bool,
+ disabledKeyboardNavigation: PropTypes.bool,
+ dropdownMode: PropTypes.oneOf(['scroll', 'select']).isRequired,
+ endDate: PropTypes.instanceOf(Date),
+ excludeDates: PropTypes.array,
+ filterDate: PropTypes.func,
+ fixedHeight: PropTypes.bool,
+ formatWeekNumber: PropTypes.func,
+ highlightDates: PropTypes.array,
+ id: PropTypes.string,
+ includeDates: PropTypes.array,
+ includeTimes: PropTypes.array,
+ injectTimes: PropTypes.array,
+ inline: PropTypes.bool,
+ isClearable: PropTypes.bool,
+ locale: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.shape({ locale: PropTypes.object }),
+ ]),
+ maxDate: PropTypes.instanceOf(Date),
+ minDate: PropTypes.instanceOf(Date),
+ monthsShown: PropTypes.number,
+ name: PropTypes.string,
+ onBlur: PropTypes.func,
+ onChange: PropTypes.func.isRequired,
+ onSelect: PropTypes.func,
+ onWeekSelect: PropTypes.func,
+ onClickOutside: PropTypes.func,
+ onChangeRaw: PropTypes.func,
+ onFocus: PropTypes.func,
+ onInputClick: PropTypes.func,
+ onKeyDown: PropTypes.func,
+ onMonthChange: PropTypes.func,
+ onYearChange: PropTypes.func,
+ onInputError: PropTypes.func,
+ open: PropTypes.bool,
+ onCalendarOpen: PropTypes.func,
+ onCalendarClose: PropTypes.func,
+ openToDate: PropTypes.instanceOf(Date),
+ peekNextMonth: PropTypes.bool,
+ placeholderText: PropTypes.string,
+ popperContainer: PropTypes.func,
+ popperClassName: PropTypes.string, // props
+ popperModifiers: PropTypes.arrayOf(PropTypes.object), // props
+ popperPlacement: PropTypes.oneOf(popperPlacementPositions), // props
+ popperProps: PropTypes.object,
+ preventOpenOnFocus: PropTypes.bool,
+ readOnly: PropTypes.bool,
+ required: PropTypes.bool,
+ scrollableYearDropdown: PropTypes.bool,
+ scrollableMonthYearDropdown: PropTypes.bool,
+ selected: PropTypes.instanceOf(Date),
+ selectsEnd: PropTypes.bool,
+ selectsStart: PropTypes.bool,
+ selectsRange: PropTypes.bool,
+ showMonthDropdown: PropTypes.bool,
+ showPreviousMonths: PropTypes.bool,
+ showMonthYearDropdown: PropTypes.bool,
+ showWeekNumbers: PropTypes.bool,
+ showYearDropdown: PropTypes.bool,
+ strictParsing: PropTypes.bool,
+ forceShowMonthNavigation: PropTypes.bool,
+ showDisabledMonthNavigation: PropTypes.bool,
+ startDate: PropTypes.instanceOf(Date),
+ startOpen: PropTypes.bool,
+ tabIndex: PropTypes.number,
+ timeCaption: PropTypes.string,
+ title: PropTypes.string,
+ todayButton: PropTypes.node,
+ useWeekdaysShort: PropTypes.bool,
+ formatWeekDay: PropTypes.func,
+ value: PropTypes.string,
+ weekLabel: PropTypes.string,
+ withPortal: PropTypes.bool,
+ portalId: PropTypes.string,
+ yearItemNumber: PropTypes.number,
+ yearDropdownItemNumber: PropTypes.number,
+ shouldCloseOnSelect: PropTypes.bool,
+ showTimeInput: PropTypes.bool,
+ showMonthYearPicker: PropTypes.bool,
+ showFullMonthYearPicker: PropTypes.bool,
+ showTwoColumnMonthYearPicker: PropTypes.bool,
+ showFourColumnMonthYearPicker: PropTypes.bool,
+ showYearPicker: PropTypes.bool,
+ showQuarterYearPicker: PropTypes.bool,
+ showTimeSelect: PropTypes.bool,
+ showTimeSelectOnly: PropTypes.bool,
+ timeFormat: PropTypes.string,
+ timeIntervals: PropTypes.number,
+ minTime: PropTypes.instanceOf(Date),
+ maxTime: PropTypes.instanceOf(Date),
+ excludeTimes: PropTypes.array,
+ filterTime: PropTypes.func,
+ useShortMonthInDropdown: PropTypes.bool,
+ clearButtonTitle: PropTypes.string,
+ clearButtonClassName: PropTypes.string,
+ previousMonthAriaLabel: PropTypes.string,
+ previousMonthButtonLabel: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.node,
+ ]),
+ nextMonthAriaLabel: PropTypes.string,
+ nextMonthButtonLabel: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.node,
+ ]),
+ previousYearAriaLabel: PropTypes.string,
+ previousYearButtonLabel: PropTypes.string,
+ nextYearAriaLabel: PropTypes.string,
+ nextYearButtonLabel: PropTypes.string,
+ timeInputLabel: PropTypes.string,
+ renderCustomHeader: PropTypes.func,
+ renderDayContents: PropTypes.func,
+ wrapperClassName: PropTypes.string,
+ focusSelectedMonth: PropTypes.bool,
+ onDayMouseEnter: PropTypes.func,
+ onMonthMouseLeave: PropTypes.func,
+ showPopperArrow: PropTypes.bool,
+ excludeScrollbar: PropTypes.bool,
+ enableTabLoop: PropTypes.bool,
+ customTimeInput: PropTypes.element,
+ weekAriaLabelPrefix: PropTypes.string,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = this.calcInitialState();
+ }
+
+ componentDidMount() {
+ window.addEventListener('scroll', this.onScroll, true);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (
+ prevProps.inline
+ && hasPreSelectionChanged(prevProps.selected, this.props.selected)
+ ) {
+ this.setPreSelection(this.props.selected);
+ }
+ if (
+ this.state.monthSelectedIn !== undefined
+ && prevProps.monthsShown !== this.props.monthsShown
+ ) {
+ this.setState({ monthSelectedIn: 0 });
+ }
+ if (prevProps.highlightDates !== this.props.highlightDates) {
+ this.setState({
+ highlightDates: getHightLightDaysMap(this.props.highlightDates),
+ });
+ }
+ if (
+ !prevState.focused
+ && !isEqual(prevProps.selected, this.props.selected)
+ ) {
+ this.setState({ inputValue: null });
+ }
+
+ if (prevState.open !== this.state.open) {
+ if (prevState.open === false && this.state.open === true) {
+ this.props.onCalendarOpen();
+ }
+
+ if (prevState.open === true && this.state.open === false) {
+ this.props.onCalendarClose();
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ this.clearPreventFocusTimeout();
+ window.removeEventListener('scroll', this.onScroll, true);
+ }
+
+ getPreSelection = () => (this.props.openToDate
+ ? this.props.openToDate
+ : this.props.selectsEnd && this.props.startDate
+ ? this.props.startDate
+ : this.props.selectsStart && this.props.endDate
+ ? this.props.endDate
+ : newDate());
+
+ calcInitialState = () => {
+ const defaultPreSelection = this.getPreSelection();
+ const minDate = getEffectiveMinDate(this.props);
+ const maxDate = getEffectiveMaxDate(this.props);
+ const boundedPreSelection = minDate && isBefore(defaultPreSelection, startOfDay(minDate))
+ ? minDate
+ : maxDate && isAfter(defaultPreSelection, endOfDay(maxDate))
+ ? maxDate
+ : defaultPreSelection;
+ return {
+ open: this.props.startOpen || false,
+ preventFocus: false,
+ preSelection:
+ (this.props.selectsRange
+ ? this.props.startDate
+ : this.props.selected) ?? boundedPreSelection,
+ // transforming highlighted days (perhaps nested array)
+ // to flat Map for faster access in day.jsx
+ highlightDates: getHightLightDaysMap(this.props.highlightDates),
+ focused: false,
+ // used to focus day in inline version after month has changed, but not on
+ // initial render
+ shouldFocusDayInline: false,
+ };
+ };
+
+ clearPreventFocusTimeout = () => {
+ if (this.preventFocusTimeout) {
+ clearTimeout(this.preventFocusTimeout);
+ }
+ };
+
+ setFocus = () => {
+ if (this.input && this.input.focus) {
+ this.input.focus({ preventScroll: true });
+ }
+ };
+
+ setBlur = () => {
+ if (this.input && this.input.blur) {
+ this.input.blur();
+ }
+
+ this.cancelFocusInput();
+ };
+
+ setOpen = (open, skipSetBlur = false) => {
+ this.setState(
+ {
+ open,
+ preSelection:
+ open && this.state.open
+ ? this.state.preSelection
+ : this.calcInitialState().preSelection,
+ lastPreSelectChange: PRESELECT_CHANGE_VIA_NAVIGATE,
+ },
+ () => {
+ if (!open) {
+ this.setState(
+ prev => ({
+ focused: skipSetBlur ? prev.focused : false,
+ }),
+ () => {
+ !skipSetBlur && this.setBlur();
+
+ this.setState({ inputValue: null });
+ },
+ );
+ }
+ },
+ );
+ };
+ inputOk = () => isDate(this.state.preSelection);
+
+ isCalendarOpen = () => (this.props.open === undefined
+ ? this.state.open && !this.props.disabled && !this.props.readOnly
+ : this.props.open);
+
+ handleFocus = (event) => {
+ if (!this.state.preventFocus) {
+ this.props.onFocus(event);
+ if (!this.props.preventOpenOnFocus && !this.props.readOnly) {
+ this.setOpen(true);
+ }
+ }
+ this.setState({ focused: true });
+ };
+
+ cancelFocusInput = () => {
+ clearTimeout(this.inputFocusTimeout);
+ this.inputFocusTimeout = null;
+ };
+
+ deferFocusInput = () => {
+ this.cancelFocusInput();
+ this.inputFocusTimeout = setTimeout(() => this.setFocus(), 1);
+ };
+
+ handleDropdownFocus = () => {
+ this.cancelFocusInput();
+ };
+
+ handleBlur = (event) => {
+ if (!this.state.open || this.props.withPortal || this.props.showTimeInput) {
+ this.props.onBlur(event);
+ }
+
+ this.setState({ focused: false });
+ };
+
+ handleCalendarClickOutside = (event) => {
+ if (!this.props.inline) {
+ this.setOpen(false);
+ }
+ this.props.onClickOutside(event);
+ if (this.props.withPortal) {
+ event.preventDefault();
+ }
+ };
+
+ handleChange = (...allArgs) => {
+ const event = allArgs[0];
+ if (this.props.onChangeRaw) {
+ this.props.onChangeRaw.apply(this, allArgs);
+ if (
+ typeof event.isDefaultPrevented !== 'function'
+ || event.isDefaultPrevented()
+ ) {
+ return;
+ }
+ }
+ this.setState({
+ inputValue: event.target.value,
+ lastPreSelectChange: PRESELECT_CHANGE_VIA_INPUT,
+ });
+ const date = parseDate(
+ event.target.value,
+ this.props.dateFormat,
+ this.props.locale,
+ this.props.strictParsing,
+ this.props.minDate,
+ );
+ if (date || !event.target.value) {
+ this.setSelected(date, event, true);
+ }
+ };
+
+ handleSelect = (date, event, monthSelectedIn) => {
+ // Preventing onFocus event to fix issue
+ // https://github.com/Hacker0x01/react-datepicker/issues/628
+ this.setState({ preventFocus: true }, () => {
+ this.preventFocusTimeout = setTimeout(
+ () => this.setState({ preventFocus: false }),
+ 50,
+ );
+ return this.preventFocusTimeout;
+ });
+ if (this.props.onChangeRaw) {
+ this.props.onChangeRaw(event);
+ }
+ this.setSelected(date, event, false, monthSelectedIn);
+ if (!this.props.shouldCloseOnSelect || this.props.showTimeSelect) {
+ this.setPreSelection(date);
+ } else if (!this.props.inline) {
+ if (!this.props.selectsRange) {
+ this.setOpen(false);
+ }
+ const { startDate, endDate } = this.props;
+ if (startDate && !endDate && !isBefore(date, startDate)) {
+ this.setOpen(false);
+ }
+ }
+ };
+
+ setSelected = (date, event, keepInput, monthSelectedIn) => {
+ let changedDate = date;
+
+ if (changedDate !== null && isDayDisabled(changedDate, this.props)) {
+ return;
+ }
+ const { onChange, selectsRange, startDate, endDate } = this.props;
+
+ if (
+ !isEqual(this.props.selected, changedDate)
+ || this.props.allowSameDay
+ || selectsRange
+ ) {
+ if (changedDate !== null) {
+ if (
+ this.props.selected
+ && (!keepInput
+ || (!this.props.showTimeSelect
+ && !this.props.showTimeSelectOnly
+ && !this.props.showTimeInput))
+ ) {
+ changedDate = setTime(changedDate, {
+ hour: getHours(this.props.selected),
+ minute: getMinutes(this.props.selected),
+ second: getSeconds(this.props.selected),
+ });
+ }
+ if (!this.props.inline) {
+ this.setState({
+ preSelection: changedDate,
+ });
+ }
+ if (!this.props.focusSelectedMonth) {
+ this.setState({ monthSelectedIn });
+ }
+ }
+ if (selectsRange) {
+ const noRanges = !startDate && !endDate;
+ const hasStartRange = startDate && !endDate;
+ const isRangeFilled = startDate && endDate;
+ if (noRanges) {
+ onChange([changedDate, null], event);
+ } else if (hasStartRange) {
+ if (isBefore(changedDate, startDate)) {
+ onChange([changedDate, null], event);
+ } else {
+ onChange([startDate, changedDate], event);
+ }
+ }
+ if (isRangeFilled) {
+ onChange([changedDate, null], event);
+ }
+ } else {
+ onChange(changedDate, event);
+ }
+ }
+
+ if (!keepInput) {
+ this.props.onSelect(changedDate, event);
+ this.setState({ inputValue: null });
+ }
+ };
+
+ // When checking preSelection via min/maxDate, times need to be manipulated via startOfDay/endOfDay
+ setPreSelection = (date) => {
+ const hasMinDate = typeof this.props.minDate !== 'undefined';
+ const hasMaxDate = typeof this.props.maxDate !== 'undefined';
+ let isValidDateSelection = true;
+ if (date) {
+ const dateStartOfDay = startOfDay(date);
+ if (hasMinDate && hasMaxDate) {
+ // isDayinRange uses startOfDay internally, so not necessary to manipulate times here
+ isValidDateSelection = isDayInRange(
+ date,
+ this.props.minDate,
+ this.props.maxDate,
+ );
+ } else if (hasMinDate) {
+ const minDateStartOfDay = startOfDay(this.props.minDate);
+ isValidDateSelection = isAfter(date, minDateStartOfDay)
+ || isEqual(dateStartOfDay, minDateStartOfDay);
+ } else if (hasMaxDate) {
+ const maxDateEndOfDay = endOfDay(this.props.maxDate);
+ isValidDateSelection = isBefore(date, maxDateEndOfDay)
+ || isEqual(dateStartOfDay, maxDateEndOfDay);
+ }
+ }
+ if (isValidDateSelection) {
+ this.setState({
+ preSelection: date,
+ });
+ }
+ };
+
+ handleTimeChange = (time) => {
+ const selected = this.props.selected
+ ? this.props.selected
+ : this.getPreSelection();
+ const changedDate = setTime(selected, {
+ hour: getHours(time),
+ minute: getMinutes(time),
+ });
+
+ this.setState({
+ preSelection: changedDate,
+ });
+
+ this.props.onChange(changedDate);
+ if (this.props.shouldCloseOnSelect) {
+ this.setOpen(false);
+ }
+ if (this.props.showTimeInput) {
+ this.setOpen(true);
+ }
+ this.setState({ inputValue: null });
+ };
+
+ onInputClick = () => {
+ if (!this.props.disabled && !this.props.readOnly) {
+ this.setOpen(true);
+ }
+
+ this.props.onInputClick();
+ };
+
+ onInputKeyDown = (event) => {
+ this.props.onKeyDown(event);
+ const eventKey = event.key;
+
+ if (
+ !this.state.open
+ && !this.props.inline
+ && !this.props.preventOpenOnFocus
+ ) {
+ if (
+ eventKey === 'ArrowDown'
+ || eventKey === 'ArrowUp'
+ || eventKey === 'Enter'
+ ) {
+ this.onInputClick();
+ }
+ return;
+ }
+
+ // if calendar is open, these keys will focus the selected day
+ if (this.state.open) {
+ if (eventKey === 'ArrowDown' || eventKey === 'ArrowUp') {
+ event.preventDefault();
+ const selectedDay = this.calendar.componentNode
+ && this.calendar.componentNode.querySelector('.react-datepicker__day[tabindex="0"]');
+ selectedDay && selectedDay.focus({ preventScroll: true });
+
+ return;
+ }
+
+ const copy = newDate(this.state.preSelection);
+ if (eventKey === 'Enter') {
+ event.preventDefault();
+ if (
+ this.inputOk()
+ && this.state.lastPreSelectChange === PRESELECT_CHANGE_VIA_NAVIGATE
+ ) {
+ this.handleSelect(copy, event);
+ !this.props.shouldCloseOnSelect && this.setPreSelection(copy);
+ } else {
+ this.setOpen(false);
+ }
+ } else if (eventKey === 'Escape') {
+ event.preventDefault();
+
+ this.setOpen(false);
+ }
+
+ if (!this.inputOk()) {
+ this.props.onInputError({ code: 1, msg: INPUT_ERR_1 });
+ }
+ }
+ };
+
+ // keyDown events passed down to day.jsx
+ onDayKeyDown = (event) => {
+ this.props.onKeyDown(event);
+ const eventKey = event.key;
+
+ const copy = newDate(this.state.preSelection);
+ if (eventKey === 'Enter') {
+ event.preventDefault();
+ this.handleSelect(copy, event);
+ !this.props.shouldCloseOnSelect && this.setPreSelection(copy);
+ } else if (eventKey === 'Escape') {
+ event.preventDefault();
+
+ this.setOpen(false);
+ if (!this.inputOk()) {
+ this.props.onInputError({ code: 1, msg: INPUT_ERR_1 });
+ }
+ } else if (!this.props.disabledKeyboardNavigation) {
+ let newSelection;
+ switch (eventKey) {
+ case 'ArrowLeft':
+ newSelection = subDays(copy, 1);
+ break;
+ case 'ArrowRight':
+ newSelection = addDays(copy, 1);
+ break;
+ case 'ArrowUp':
+ newSelection = subWeeks(copy, 1);
+ break;
+ case 'ArrowDown':
+ newSelection = addWeeks(copy, 1);
+ break;
+ case 'PageUp':
+ newSelection = subMonths(copy, 1);
+ break;
+ case 'PageDown':
+ newSelection = addMonths(copy, 1);
+ break;
+ case 'Home':
+ newSelection = subYears(copy, 1);
+ break;
+ case 'End':
+ newSelection = addYears(copy, 1);
+ break;
+ }
+ if (!newSelection) {
+ if (this.props.onInputError) {
+ this.props.onInputError({ code: 1, msg: INPUT_ERR_1 });
+ }
+ return;
+ }
+ event.preventDefault();
+ this.setState({ lastPreSelectChange: PRESELECT_CHANGE_VIA_NAVIGATE });
+ if (this.props.adjustDateOnChange) {
+ this.setSelected(newSelection);
+ }
+ this.setPreSelection(newSelection);
+ // need to figure out whether month has changed to focus day in inline version
+ if (this.props.inline) {
+ const prevMonth = getMonth(copy);
+ const newMonth = getMonth(newSelection);
+ const prevYear = getYear(copy);
+ const newYear = getYear(newSelection);
+
+ if (prevMonth !== newMonth || prevYear !== newYear) {
+ // month has changed
+ this.setState({ shouldFocusDayInline: true });
+ } else {
+ // month hasn't changed
+ this.setState({ shouldFocusDayInline: false });
+ }
+ }
+ }
+ };
+
+ // handle generic key down events in the popper that do not adjust or select dates
+ // ex: while focusing prev and next month buttons
+ onPopperKeyDown = (event) => {
+ const eventKey = event.key;
+ if (eventKey === 'Escape') {
+ // close the popper and refocus the input
+ // stop the input from auto opening onFocus
+ // close the popper
+ // setFocus to the input
+ // allow input auto opening onFocus
+ event.preventDefault();
+ this.setState(
+ {
+ preventFocus: true,
+ },
+ () => {
+ this.setOpen(false);
+ setTimeout(() => {
+ this.setFocus();
+ this.setState({ preventFocus: false });
+ });
+ },
+ );
+ }
+ };
+
+ onClearClick = (event) => {
+ if (event) {
+ if (event.preventDefault) {
+ event.preventDefault();
+ }
+ }
+ if (this.props.selectsRange) {
+ this.props.onChange([null, null], event);
+ } else {
+ this.props.onChange(null, event);
+ }
+ this.setState({ inputValue: null });
+ };
+
+ clear = () => {
+ this.onClearClick();
+ };
+
+ onScroll = (event) => {
+ if (
+ typeof this.props.closeOnScroll === 'boolean'
+ && this.props.closeOnScroll
+ ) {
+ if (
+ event.target === document
+ || event.target === document.documentElement
+ || event.target === document.body
+ ) {
+ this.setOpen(false);
+ }
+ } else if (typeof this.props.closeOnScroll === 'function') {
+ if (this.props.closeOnScroll(event)) {
+ this.setOpen(false);
+ }
+ }
+ };
+
+ renderCalendar = () => {
+ if (!this.props.inline && !this.isCalendarOpen()) {
+ return null;
+ }
+ return (
+ {
+ this.calendar = elem;
+ }}
+ locale={this.props.locale}
+ calendarStartDay={this.props.calendarStartDay}
+ chooseDayAriaLabelPrefix={this.props.chooseDayAriaLabelPrefix}
+ disabledDayAriaLabelPrefix={this.props.disabledDayAriaLabelPrefix}
+ weekAriaLabelPrefix={this.props.weekAriaLabelPrefix}
+ adjustDateOnChange={this.props.adjustDateOnChange}
+ setOpen={this.setOpen}
+ shouldCloseOnSelect={this.props.shouldCloseOnSelect}
+ dateFormat={this.props.dateFormatCalendar}
+ useWeekdaysShort={this.props.useWeekdaysShort}
+ formatWeekDay={this.props.formatWeekDay}
+ dropdownMode={this.props.dropdownMode}
+ selected={this.props.selected}
+ preSelection={this.state.preSelection}
+ onSelect={this.handleSelect}
+ onWeekSelect={this.props.onWeekSelect}
+ openToDate={this.props.openToDate}
+ minDate={this.props.minDate}
+ maxDate={this.props.maxDate}
+ selectsStart={this.props.selectsStart}
+ selectsEnd={this.props.selectsEnd}
+ selectsRange={this.props.selectsRange}
+ startDate={this.props.startDate}
+ endDate={this.props.endDate}
+ excludeDates={this.props.excludeDates}
+ filterDate={this.props.filterDate}
+ onClickOutside={this.handleCalendarClickOutside}
+ formatWeekNumber={this.props.formatWeekNumber}
+ highlightDates={this.state.highlightDates}
+ includeDates={this.props.includeDates}
+ includeTimes={this.props.includeTimes}
+ injectTimes={this.props.injectTimes}
+ inline={this.props.inline}
+ shouldFocusDayInline={this.state.shouldFocusDayInline}
+ peekNextMonth={this.props.peekNextMonth}
+ showMonthDropdown={this.props.showMonthDropdown}
+ showPreviousMonths={this.props.showPreviousMonths}
+ useShortMonthInDropdown={this.props.useShortMonthInDropdown}
+ showMonthYearDropdown={this.props.showMonthYearDropdown}
+ showWeekNumbers={this.props.showWeekNumbers}
+ showYearDropdown={this.props.showYearDropdown}
+ withPortal={this.props.withPortal}
+ forceShowMonthNavigation={this.props.forceShowMonthNavigation}
+ showDisabledMonthNavigation={this.props.showDisabledMonthNavigation}
+ scrollableYearDropdown={this.props.scrollableYearDropdown}
+ scrollableMonthYearDropdown={this.props.scrollableMonthYearDropdown}
+ todayButton={this.props.todayButton}
+ weekLabel={this.props.weekLabel}
+ outsideClickIgnoreClass={outsideClickIgnoreClass}
+ fixedHeight={this.props.fixedHeight}
+ monthsShown={this.props.monthsShown}
+ monthSelectedIn={this.state.monthSelectedIn}
+ onDropdownFocus={this.handleDropdownFocus}
+ onMonthChange={this.props.onMonthChange}
+ onYearChange={this.props.onYearChange}
+ dayClassName={this.props.dayClassName}
+ weekDayClassName={this.props.weekDayClassName}
+ monthClassName={this.props.monthClassName}
+ timeClassName={this.props.timeClassName}
+ showTimeSelect={this.props.showTimeSelect}
+ showTimeSelectOnly={this.props.showTimeSelectOnly}
+ onTimeChange={this.handleTimeChange}
+ timeFormat={this.props.timeFormat}
+ timeIntervals={this.props.timeIntervals}
+ minTime={this.props.minTime}
+ maxTime={this.props.maxTime}
+ excludeTimes={this.props.excludeTimes}
+ filterTime={this.props.filterTime}
+ timeCaption={this.props.timeCaption}
+ className={this.props.calendarClassName}
+ container={this.props.calendarContainer}
+ yearItemNumber={this.props.yearItemNumber}
+ yearDropdownItemNumber={this.props.yearDropdownItemNumber}
+ previousMonthAriaLabel={this.props.previousMonthAriaLabel}
+ previousMonthButtonLabel={this.props.previousMonthButtonLabel}
+ nextMonthAriaLabel={this.props.nextMonthAriaLabel}
+ nextMonthButtonLabel={this.props.nextMonthButtonLabel}
+ previousYearAriaLabel={this.props.previousYearAriaLabel}
+ previousYearButtonLabel={this.props.previousYearButtonLabel}
+ nextYearAriaLabel={this.props.nextYearAriaLabel}
+ nextYearButtonLabel={this.props.nextYearButtonLabel}
+ timeInputLabel={this.props.timeInputLabel}
+ disabledKeyboardNavigation={this.props.disabledKeyboardNavigation}
+ renderCustomHeader={this.props.renderCustomHeader}
+ popperProps={this.props.popperProps}
+ renderDayContents={this.props.renderDayContents}
+ onDayMouseEnter={this.props.onDayMouseEnter}
+ onMonthMouseLeave={this.props.onMonthMouseLeave}
+ showTimeInput={this.props.showTimeInput}
+ showMonthYearPicker={this.props.showMonthYearPicker}
+ showFullMonthYearPicker={this.props.showFullMonthYearPicker}
+ showTwoColumnMonthYearPicker={this.props.showTwoColumnMonthYearPicker}
+ showFourColumnMonthYearPicker={this.props.showFourColumnMonthYearPicker}
+ showYearPicker={this.props.showYearPicker}
+ showQuarterYearPicker={this.props.showQuarterYearPicker}
+ showPopperArrow={this.props.showPopperArrow}
+ excludeScrollbar={this.props.excludeScrollbar}
+ handleOnKeyDown={this.props.onKeyDown}
+ handleOnDayKeyDown={this.onDayKeyDown}
+ isInputFocused={this.state.focused}
+ customTimeInput={this.props.customTimeInput}
+ setPreSelection={this.setPreSelection}
+ >
+ {this.props.children}
+
+ );
+ };
+
+ renderDateInput = () => {
+ const className = classnames(this.props.className, {
+ [outsideClickIgnoreClass]: this.state.open,
+ });
+
+ const customInput = this.props.customInput || ;
+ const customInputRef = this.props.customInputRef || 'ref';
+ const inputValue = typeof this.props.value === 'string'
+ ? this.props.value
+ : typeof this.state.inputValue === 'string'
+ ? this.state.inputValue
+ : this.props.selectsRange
+ ? safeDateRangeFormat(
+ this.props.startDate,
+ this.props.endDate,
+ this.props,
+ )
+ : safeDateFormat(this.props.selected, this.props);
+
+ return React.cloneElement(customInput, {
+ [customInputRef]: (input) => {
+ this.input = input;
+ },
+ value: inputValue,
+ onBlur: this.handleBlur,
+ onChange: this.handleChange,
+ onClick: this.onInputClick,
+ onFocus: this.handleFocus,
+ onKeyDown: this.onInputKeyDown,
+ id: this.props.id,
+ name: this.props.name,
+ autoFocus: this.props.autoFocus,
+ placeholder: this.props.placeholderText,
+ disabled: this.props.disabled,
+ autoComplete: this.props.autoComplete,
+ className: classnames(customInput.props.className, className),
+ title: this.props.title,
+ readOnly: this.props.readOnly,
+ required: this.props.required,
+ tabIndex: this.props.tabIndex,
+ 'aria-describedby': this.props.ariaDescribedBy,
+ 'aria-invalid': this.props.ariaInvalid,
+ 'aria-labelledby': this.props.ariaLabelledBy,
+ 'aria-required': this.props.ariaRequired,
+ });
+ };
+
+ renderClearButton = () => {
+ const {
+ isClearable,
+ selected,
+ startDate,
+ endDate,
+ clearButtonTitle,
+ clearButtonClassName = '',
+ ariaLabelClose = 'Close',
+ } = this.props;
+ if (
+ isClearable
+ && (selected != null || startDate != null || endDate != null)
+ ) {
+ return (
+
+ );
+ }
+ return null;
+ };
+
+ renderInputContainer() {
+ return (
+
+ {this.renderDateInput()}
+ {this.renderClearButton()}
+
+ );
+ }
+
+ render() {
+ const calendar = this.renderCalendar();
+
+ if (this.props.inline) return calendar;
+
+ if (this.props.withPortal) {
+ let portalContainer = this.state.open ? (
+ {calendar}
+ ) : null;
+
+ if (this.state.open && this.props.portalId) {
+ portalContainer = (
+ {portalContainer}
+ );
+ }
+
+ return (
+
+ {this.renderInputContainer()}
+ {portalContainer}
+
+ );
+ };
+
+ return (
+
+ );
+ }
+}
+
+const PRESELECT_CHANGE_VIA_INPUT = 'input';
+const PRESELECT_CHANGE_VIA_NAVIGATE = 'navigate';
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/inputTime.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/inputTime.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..3b16cd186568aad5a0334ad0cd7bb52863d8a518
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/inputTime.jsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export default class inputTime extends React.Component {
+ static propTypes = {
+ onChange: PropTypes.func,
+ date: PropTypes.instanceOf(Date),
+ timeString: PropTypes.string,
+ timeInputLabel: PropTypes.string,
+ customTimeInput: PropTypes.element,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ time: this.props.timeString,
+ };
+ }
+
+ static getDerivedStateFromProps(props, state) {
+ if (props.timeString !== state.time) {
+ return {
+ time: props.timeString,
+ };
+ }
+
+ // Return null to indicate no change to state.
+ return null;
+ }
+
+ onTimeChange = (time) => {
+ this.setState({ time });
+ const date = new Date();
+ date.setHours(time.split(':')[0]);
+ date.setMinutes(time.split(':')[1]);
+ this.props.onChange(date);
+ };
+
+ renderTimeInput = () => {
+ const { time } = this.state;
+ const { date, timeString, customTimeInput } = this.props;
+
+ if (customTimeInput) {
+ return React.cloneElement(customTimeInput, {
+ date,
+ value: time,
+ onChange: this.onTimeChange,
+ });
+ }
+
+ return (
+ {
+ this.onTimeChange(ev.target.value || timeString);
+ }}
+ />
+ );
+ };
+
+ render() {
+ return (
+
+
+ {this.props.timeInputLabel}
+
+
+
+ {this.renderTimeInput()}
+
+
+
+ );
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..96d5ff3d74dffe09269f060716112170f7829d6b
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month.jsx
@@ -0,0 +1,461 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import Week from './week';
+import * as utils from './date_utils';
+
+const FIXED_HEIGHT_STANDARD_WEEK_COUNT = 6;
+
+export default class Month extends React.Component {
+ static propTypes = {
+ ariaLabelPrefix: PropTypes.string,
+ chooseDayAriaLabelPrefix: PropTypes.string,
+ disabledDayAriaLabelPrefix: PropTypes.string,
+ disabledKeyboardNavigation: PropTypes.bool,
+ day: PropTypes.instanceOf(Date).isRequired,
+ dayClassName: PropTypes.func,
+ monthClassName: PropTypes.func,
+ endDate: PropTypes.instanceOf(Date),
+ orderInDisplay: PropTypes.number,
+ excludeDates: PropTypes.array,
+ filterDate: PropTypes.func,
+ fixedHeight: PropTypes.bool,
+ formatWeekNumber: PropTypes.func,
+ highlightDates: PropTypes.instanceOf(Map),
+ includeDates: PropTypes.array,
+ inline: PropTypes.bool,
+ shouldFocusDayInline: PropTypes.bool,
+ locale: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.shape({ locale: PropTypes.object }),
+ ]),
+ maxDate: PropTypes.instanceOf(Date),
+ minDate: PropTypes.instanceOf(Date),
+ onDayClick: PropTypes.func,
+ onDayMouseEnter: PropTypes.func,
+ onMouseLeave: PropTypes.func,
+ onWeekSelect: PropTypes.func,
+ peekNextMonth: PropTypes.bool,
+ preSelection: PropTypes.instanceOf(Date),
+ setPreSelection: PropTypes.func,
+ selected: PropTypes.instanceOf(Date),
+ selectingDate: PropTypes.instanceOf(Date),
+ calendarStartDay: PropTypes.number,
+ selectsEnd: PropTypes.bool,
+ selectsStart: PropTypes.bool,
+ selectsRange: PropTypes.bool,
+ showWeekNumbers: PropTypes.bool,
+ startDate: PropTypes.instanceOf(Date),
+ setOpen: PropTypes.func,
+ shouldCloseOnSelect: PropTypes.bool,
+ renderDayContents: PropTypes.func,
+ showMonthYearPicker: PropTypes.bool,
+ showFullMonthYearPicker: PropTypes.bool,
+ showTwoColumnMonthYearPicker: PropTypes.bool,
+ showFourColumnMonthYearPicker: PropTypes.bool,
+ showQuarterYearPicker: PropTypes.bool,
+ handleOnKeyDown: PropTypes.func,
+ isInputFocused: PropTypes.bool,
+ weekAriaLabelPrefix: PropTypes.string,
+ containerRef: PropTypes.oneOfType([
+ PropTypes.func,
+ // PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
+ ]),
+ monthShowsDuplicateDaysEnd: PropTypes.bool,
+ monthShowsDuplicateDaysStart: PropTypes.bool,
+ };
+
+ MONTH_REFS = [...Array(12)].map(() => React.createRef());
+
+ isDisabled = date => utils.isDayDisabled(date, this.props);
+
+ isExcluded = date => utils.isDayExcluded(date, this.props);
+
+ handleDayClick = (day, event) => {
+ if (this.props.onDayClick) {
+ this.props.onDayClick(day, event, this.props.orderInDisplay);
+ }
+ };
+
+ handleDayMouseEnter = (day) => {
+ if (this.props.onDayMouseEnter) {
+ this.props.onDayMouseEnter(day);
+ }
+ };
+
+ handleMouseLeave = () => {
+ if (this.props.onMouseLeave) {
+ this.props.onMouseLeave();
+ }
+ };
+
+ isRangeStartMonth = (m) => {
+ const { day, startDate, endDate } = this.props;
+ if (!startDate || !endDate) {
+ return false;
+ }
+ return utils.isSameMonth(utils.setMonth(day, m), startDate);
+ };
+
+ isRangeStartQuarter = (q) => {
+ const { day, startDate, endDate } = this.props;
+ if (!startDate || !endDate) {
+ return false;
+ }
+ return utils.isSameQuarter(utils.setQuarter(day, q), startDate);
+ };
+
+ isRangeEndMonth = (m) => {
+ const { day, startDate, endDate } = this.props;
+ if (!startDate || !endDate) {
+ return false;
+ }
+ return utils.isSameMonth(utils.setMonth(day, m), endDate);
+ };
+
+ isRangeEndQuarter = (q) => {
+ const { day, startDate, endDate } = this.props;
+ if (!startDate || !endDate) {
+ return false;
+ }
+ return utils.isSameQuarter(utils.setQuarter(day, q), endDate);
+ };
+
+ isWeekInMonth = (startOfWeek) => {
+ const { day } = this.props;
+ const endOfWeek = utils.addDays(startOfWeek, 6);
+ return (
+ utils.isSameMonth(startOfWeek, day) || utils.isSameMonth(endOfWeek, day)
+ );
+ };
+
+ renderWeeks = () => {
+ const weeks = [];
+ const isFixedHeight = this.props.fixedHeight;
+
+ let i = 0;
+ let breakAfterNextPush = false;
+ let currentWeekStart = utils.getStartOfWeek(
+ utils.getStartOfMonth(this.props.day),
+ this.props.locale,
+ this.props.calendarStartDay,
+ );
+
+ while (true) {
+ weeks.push();
+
+ if (breakAfterNextPush) break;
+
+ i++;
+ currentWeekStart = utils.addWeeks(currentWeekStart, 1);
+
+ // If one of these conditions is true, we will either break on this week
+ // or break on the next week
+ const isFixedAndFinalWeek = isFixedHeight && i >= FIXED_HEIGHT_STANDARD_WEEK_COUNT;
+ const isNonFixedAndOutOfMonth = !isFixedHeight && !this.isWeekInMonth(currentWeekStart);
+
+ if (isFixedAndFinalWeek || isNonFixedAndOutOfMonth) {
+ if (this.props.peekNextMonth) {
+ breakAfterNextPush = true;
+ } else {
+ break;
+ }
+ }
+ }
+
+ return weeks;
+ };
+
+ onMonthClick = (e, m) => {
+ this.handleDayClick(
+ utils.getStartOfMonth(utils.setMonth(this.props.day, m)),
+ e,
+ );
+ };
+
+ handleMonthNavigation = (newMonth, newDate) => {
+ if (this.isDisabled(newDate) || this.isExcluded(newDate)) return;
+ this.props.setPreSelection(newDate);
+ this.MONTH_REFS[newMonth].current
+ && this.MONTH_REFS[newMonth].current.focus();
+ };
+
+ onMonthKeyDown = (event, month) => {
+ const eventKey = event.key;
+ if (!this.props.disabledKeyboardNavigation) {
+ switch (eventKey) {
+ case 'Enter':
+ this.onMonthClick(event, month);
+ this.props.setPreSelection(this.props.selected);
+ break;
+ case 'ArrowRight':
+ this.handleMonthNavigation(
+ month === 11 ? 0 : month + 1,
+ utils.addMonths(this.props.preSelection, 1),
+ );
+ break;
+ case 'ArrowLeft':
+ this.handleMonthNavigation(
+ month === 0 ? 11 : month - 1,
+ utils.subMonths(this.props.preSelection, 1),
+ );
+ break;
+ }
+ }
+ };
+
+ onQuarterClick = (e, q) => {
+ this.handleDayClick(
+ utils.getStartOfQuarter(utils.setQuarter(this.props.day, q)),
+ e,
+ );
+ };
+
+ getMonthClassNames = (m) => {
+ const {
+ day,
+ startDate,
+ endDate,
+ selected,
+ minDate,
+ maxDate,
+ preSelection,
+ monthClassName,
+ } = this.props;
+ const _monthClassName = monthClassName ? monthClassName(day) : undefined;
+ return classnames(
+ 'react-datepicker__month-text',
+ `react-datepicker__month-${m}`,
+ _monthClassName,
+ {
+ 'react-datepicker__month--disabled':
+ (minDate || maxDate)
+ && utils.isMonthDisabled(utils.setMonth(day, m), this.props),
+ 'react-datepicker__month-text--selected':
+ utils.getMonth(day) === m
+ && utils.getYear(day) === utils.getYear(selected),
+ 'react-datepicker__month--selected':
+ utils.getMonth(day) === m
+ && utils.getYear(day) === utils.getYear(selected),
+ // 'react-datepicker__month-text--keyboard-selected':
+ // utils.getMonth(preSelection) === m,
+ 'react-datepicker__month--in-range': utils.isMonthinRange(
+ startDate,
+ endDate,
+ m,
+ day,
+ ),
+ 'react-datepicker__month--range-start': this.isRangeStartMonth(m),
+ 'react-datepicker__month--range-end': this.isRangeEndMonth(m),
+ 'react-datepicker__month-text--today': m === utils.getMonth(utils.newDate()),
+ },
+ );
+ };
+
+ getTabIndex = (m) => {
+ const preSelectedMonth = utils.getMonth(this.props.preSelection);
+ const tabIndex = !this.props.disabledKeyboardNavigation && m === preSelectedMonth
+ ? '0'
+ : '-1';
+
+ return tabIndex;
+ };
+
+ getAriaLabel = (month) => {
+ const {
+ ariaLabelPrefix = 'Choose',
+ disabledDayAriaLabelPrefix = 'Not available',
+ day,
+ } = this.props;
+
+ const labelDate = utils.setMonth(day, month);
+ const prefix = this.isDisabled(labelDate) || this.isExcluded(labelDate)
+ ? disabledDayAriaLabelPrefix
+ : ariaLabelPrefix;
+
+ return `${prefix} ${utils.formatDate(labelDate, 'MMMM yyyy')}`;
+ };
+
+ getQuarterClassNames = (q) => {
+ const { day, startDate, endDate, selected, minDate, maxDate } = this.props;
+ return classnames(
+ 'react-datepicker__quarter-text',
+ `react-datepicker__quarter-${q}`,
+ {
+ 'react-datepicker__quarter--disabled':
+ (minDate || maxDate)
+ && utils.isQuarterDisabled(utils.setQuarter(day, q), this.props),
+ 'react-datepicker__quarter--selected':
+ utils.getQuarter(day) === q
+ && utils.getYear(day) === utils.getYear(selected),
+ 'react-datepicker__quarter--in-range': utils.isQuarterInRange(
+ startDate,
+ endDate,
+ q,
+ day,
+ ),
+ 'react-datepicker__quarter--range-start': this.isRangeStartQuarter(q),
+ 'react-datepicker__quarter--range-end': this.isRangeEndQuarter(q),
+ },
+ );
+ };
+
+ renderMonths = () => {
+ const {
+ showFullMonthYearPicker,
+ showTwoColumnMonthYearPicker,
+ showFourColumnMonthYearPicker,
+ locale,
+ } = this.props;
+ const monthsFourColumns = [
+ [0, 1, 2, 3],
+ [4, 5, 6, 7],
+ [8, 9, 10, 11],
+ ];
+ const monthsThreeColumns = [
+ [0, 1, 2],
+ [3, 4, 5],
+ [6, 7, 8],
+ [9, 10, 11],
+ ];
+ const monthsTwoColumns = [
+ [0, 1],
+ [2, 3],
+ [4, 5],
+ [6, 7],
+ [8, 9],
+ [10, 11],
+ ];
+ const monthLayout = showFourColumnMonthYearPicker
+ ? monthsFourColumns
+ : showTwoColumnMonthYearPicker
+ ? monthsTwoColumns
+ : monthsThreeColumns;
+ return monthLayout.map((month, i) => (
+
+ {month.map((m, j) => (
+
{
+ this.onMonthClick(ev, m);
+ }}
+ onKeyDown={(ev) => {
+ this.onMonthKeyDown(ev, m);
+ }}
+ tabIndex={this.getTabIndex(m)}
+ className={this.getMonthClassNames(m)}
+ role="button"
+ aria-label={this.getAriaLabel(m)}
+ >
+ {showFullMonthYearPicker
+ ? utils.getMonthInLocale(m, locale)
+ : utils.getMonthShortInLocale(m, locale)}
+
+ ))}
+
+ ));
+ };
+
+ renderQuarters = () => {
+ const quarters = [1, 2, 3, 4];
+ return (
+
+ {quarters.map((q, j) => (
+
{
+ this.onQuarterClick(ev, q);
+ }}
+ className={this.getQuarterClassNames(q)}
+ >
+ {utils.getQuarterShortInLocale(q, this.props.locale)}
+
+ ))}
+
+ );
+ };
+
+ getClassNames = () => {
+ const {
+ day,
+ selectingDate,
+ selectsStart,
+ selectsEnd,
+ showMonthYearPicker,
+ showQuarterYearPicker,
+ } = this.props;
+
+ return classnames(
+ 'react-datepicker__month',
+ {
+ 'react-datepicker__month--selecting-range':
+ selectingDate && (selectsStart || selectsEnd),
+ },
+ { 'react-datepicker__monthPicker': showMonthYearPicker },
+ { 'react-datepicker__quarterPicker': showQuarterYearPicker },
+ );
+ };
+
+ render() {
+ const {
+ showMonthYearPicker,
+ showQuarterYearPicker,
+ day,
+ ariaLabelPrefix = 'month ',
+ } = this.props;
+ return (
+
+ {showMonthYearPicker
+ ? this.renderMonths()
+ : showQuarterYearPicker
+ ? this.renderQuarters()
+ : this.renderWeeks()}
+
+ );
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month_dropdown.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month_dropdown.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e3c1d9f381cc65746b80478ccca8e6935aefa364
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month_dropdown.jsx
@@ -0,0 +1,105 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import MonthDropdownOptions from './month_dropdown_options';
+import onClickOutside from 'react-onclickoutside';
+import * as utils from './date_utils';
+
+const WrappedMonthDropdownOptions = onClickOutside(MonthDropdownOptions);
+
+export default class MonthDropdown extends React.Component {
+ static propTypes = {
+ dropdownMode: PropTypes.oneOf(['scroll', 'select']).isRequired,
+ locale: PropTypes.string,
+ month: PropTypes.number.isRequired,
+ onChange: PropTypes.func.isRequired,
+ useShortMonthInDropdown: PropTypes.bool,
+ };
+
+ state = {
+ dropdownVisible: false,
+ };
+
+ renderSelectOptions = monthNames => monthNames.map((M, i) => (
+
+ ));
+
+ renderSelectMode = monthNames => (
+
+ );
+
+ renderReadView = (visible, monthNames) => (
+
+
+
+ {monthNames[this.props.month]}
+
+
+ );
+
+ renderDropdown = monthNames => (
+
+ );
+
+ renderScrollMode = (monthNames) => {
+ const { dropdownVisible } = this.state;
+ const result = [this.renderReadView(!dropdownVisible, monthNames)];
+ if (dropdownVisible) {
+ result.unshift(this.renderDropdown(monthNames));
+ }
+ return result;
+ };
+
+ onChange = (month) => {
+ this.toggleDropdown();
+ if (month !== this.props.month) {
+ this.props.onChange(month);
+ }
+ };
+
+ toggleDropdown = () => this.setState({
+ dropdownVisible: !this.state.dropdownVisible,
+ });
+
+ render() {
+ const monthNames = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map(this.props.useShortMonthInDropdown
+ ? M => utils.getMonthShortInLocale(M, this.props.locale)
+ : M => utils.getMonthInLocale(M, this.props.locale));
+
+ let renderedDropdown;
+ switch (this.props.dropdownMode) {
+ case 'scroll':
+ renderedDropdown = this.renderScrollMode(monthNames);
+ break;
+ case 'select':
+ renderedDropdown = this.renderSelectMode(monthNames);
+ break;
+ }
+
+ return (
+
+ {renderedDropdown}
+
+ );
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month_dropdown_options.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month_dropdown_options.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..41e968b1edbd6e395809e097dfee05c5ab9e86a0
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month_dropdown_options.jsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export default class MonthDropdownOptions extends React.Component {
+ static propTypes = {
+ onCancel: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ month: PropTypes.number.isRequired,
+ monthNames: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
+ };
+
+ renderOptions = () => this.props.monthNames.map((month, i) => (
+
+ {this.props.month === i ? (
+ ✓
+ ) : (
+ ''
+ )}
+ {month}
+
+ ));
+
+ onChange = month => this.props.onChange(month);
+
+ handleClickOutside = () => this.props.onCancel();
+
+ render() {
+ return (
+
+ {this.renderOptions()}
+
+ );
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month_year_dropdown.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month_year_dropdown.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..2be3a1e4c01ec1d02ec4ea6e29aa518728f6c59c
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month_year_dropdown.jsx
@@ -0,0 +1,148 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import MonthYearDropdownOptions from './month_year_dropdown_options';
+import onClickOutside from 'react-onclickoutside';
+import {
+ addMonths,
+ formatDate,
+ getStartOfMonth,
+ isAfter,
+ isSameMonth,
+ isSameYear,
+ newDate,
+ getTime,
+} from './date_utils';
+
+const WrappedMonthYearDropdownOptions = onClickOutside(MonthYearDropdownOptions);
+
+export default class MonthYearDropdown extends React.Component {
+ static propTypes = {
+ dropdownMode: PropTypes.oneOf(['scroll', 'select']).isRequired,
+ dateFormat: PropTypes.string.isRequired,
+ locale: PropTypes.string,
+ maxDate: PropTypes.instanceOf(Date).isRequired,
+ minDate: PropTypes.instanceOf(Date).isRequired,
+ date: PropTypes.instanceOf(Date).isRequired,
+ onChange: PropTypes.func.isRequired,
+ scrollableMonthYearDropdown: PropTypes.bool,
+ };
+
+ state = {
+ dropdownVisible: false,
+ };
+
+ renderSelectOptions = () => {
+ let currDate = getStartOfMonth(this.props.minDate);
+ const lastDate = getStartOfMonth(this.props.maxDate);
+ const options = [];
+
+ while (!isAfter(currDate, lastDate)) {
+ const timepoint = getTime(currDate);
+ options.push();
+
+ currDate = addMonths(currDate, 1);
+ }
+
+ return options;
+ };
+
+ onSelectChange = (e) => {
+ this.onChange(e.target.value);
+ };
+
+ renderSelectMode = () => (
+
+ );
+
+ renderReadView = (visible) => {
+ const yearMonth = formatDate(
+ this.props.date,
+ this.props.dateFormat,
+ this.props.locale,
+ );
+
+ return (
+ this.toggleDropdown(event)}
+ >
+
+
+ {yearMonth}
+
+
+ );
+ };
+
+ renderDropdown = () => (
+
+ );
+
+ renderScrollMode = () => {
+ const { dropdownVisible } = this.state;
+ const result = [this.renderReadView(!dropdownVisible)];
+ if (dropdownVisible) {
+ result.unshift(this.renderDropdown());
+ }
+ return result;
+ };
+
+ onChange = (monthYearPoint) => {
+ this.toggleDropdown();
+
+ const changedDate = newDate(parseInt(monthYearPoint));
+
+ if (
+ isSameYear(this.props.date, changedDate)
+ && isSameMonth(this.props.date, changedDate)
+ ) {
+ return;
+ }
+
+ this.props.onChange(changedDate);
+ };
+
+ toggleDropdown = () => this.setState({
+ dropdownVisible: !this.state.dropdownVisible,
+ });
+
+ render() {
+ let renderedDropdown;
+ switch (this.props.dropdownMode) {
+ case 'scroll':
+ renderedDropdown = this.renderScrollMode();
+ break;
+ case 'select':
+ renderedDropdown = this.renderSelectMode();
+ break;
+ }
+
+ return (
+
+ {renderedDropdown}
+
+ );
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month_year_dropdown_options.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month_year_dropdown_options.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..237f6b11643052a8182d34986d7afbb914562de5
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/month_year_dropdown_options.jsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import {
+ addMonths,
+ formatDate,
+ getStartOfMonth,
+ newDate,
+ isAfter,
+ isSameMonth,
+ isSameYear,
+ getTime,
+} from './date_utils';
+
+function generateMonthYears(minDate, maxDate) {
+ const list = [];
+
+ let currDate = getStartOfMonth(minDate);
+ const lastDate = getStartOfMonth(maxDate);
+
+ while (!isAfter(currDate, lastDate)) {
+ list.push(newDate(currDate));
+
+ currDate = addMonths(currDate, 1);
+ }
+ return list;
+}
+
+export default class MonthYearDropdownOptions extends React.Component {
+ static propTypes = {
+ minDate: PropTypes.instanceOf(Date).isRequired,
+ maxDate: PropTypes.instanceOf(Date).isRequired,
+ onCancel: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ scrollableMonthYearDropdown: PropTypes.bool,
+ date: PropTypes.instanceOf(Date).isRequired,
+ dateFormat: PropTypes.string.isRequired,
+ locale: PropTypes.string,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ monthYearsList: generateMonthYears(
+ this.props.minDate,
+ this.props.maxDate,
+ ),
+ };
+ }
+
+ renderOptions = () => this.state.monthYearsList.map((monthYear) => {
+ const monthYearPoint = getTime(monthYear);
+ const isSameMonthYear = isSameYear(this.props.date, monthYear)
+ && isSameMonth(this.props.date, monthYear);
+
+ return (
+
+ {isSameMonthYear ? (
+
+ ✓
+
+ ) : (
+ ''
+ )}
+ {formatDate(monthYear, this.props.dateFormat, this.props.locale)}
+
+ );
+ });
+
+ onChange = monthYear => this.props.onChange(monthYear);
+
+ handleClickOutside = () => {
+ this.props.onCancel();
+ };
+
+ render() {
+ const dropdownClass = classNames({
+ 'react-datepicker__month-year-dropdown': true,
+ 'react-datepicker__month-year-dropdown--scrollable':
+ this.props.scrollableMonthYearDropdown,
+ });
+
+ return {this.renderOptions()}
;
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/popper_component.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/popper_component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..1ee92680b20efc388af4ae6dc855689e914abadd
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/popper_component.jsx
@@ -0,0 +1,103 @@
+import classnames from 'classnames';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Manager, Reference, Popper } from 'react-popper';
+import { placements } from '@popperjs/core';
+import TabLoop from './tab_loop';
+import Portal from './portal';
+
+export const popperPlacementPositions = placements;
+
+export default class PopperComponent extends React.Component {
+ static get defaultProps() {
+ return {
+ hidePopper: true,
+ popperModifiers: [],
+ popperProps: {},
+ popperPlacement: 'bottom-start',
+ };
+ }
+
+ static propTypes = {
+ className: PropTypes.string,
+ wrapperClassName: PropTypes.string,
+ hidePopper: PropTypes.bool,
+ popperComponent: PropTypes.element,
+ popperModifiers: PropTypes.arrayOf(PropTypes.object), // props
+ popperPlacement: PropTypes.oneOf(popperPlacementPositions), // props
+ popperContainer: PropTypes.func,
+ popperProps: PropTypes.object,
+ targetComponent: PropTypes.element,
+ enableTabLoop: PropTypes.bool,
+ popperOnKeyDown: PropTypes.func,
+ portalId: PropTypes.string,
+ };
+
+ render() {
+ const {
+ className,
+ wrapperClassName,
+ hidePopper,
+ popperComponent,
+ popperModifiers,
+ popperPlacement,
+ popperProps,
+ targetComponent,
+ enableTabLoop,
+ popperOnKeyDown,
+ portalId,
+ } = this.props;
+
+ let popper;
+
+ if (!hidePopper) {
+ const classes = classnames('react-datepicker-popper', className);
+ popper = (
+
+ {({ ref, style, placement, arrowProps }) => (
+
+
+ {React.cloneElement(popperComponent, { arrowProps })}
+
+
+ )}
+
+ );
+ }
+
+ if (this.props.popperContainer) {
+ popper = React.createElement(this.props.popperContainer, {}, popper);
+ }
+
+ if (portalId && !hidePopper) {
+ popper = {popper};
+ }
+
+ const wrapperClasses = classnames(
+ 'react-datepicker-wrapper',
+ wrapperClassName,
+ );
+
+ return (
+
+
+ {({ ref }) => (
+
+ {targetComponent}
+
+ )}
+
+ {popper}
+
+ );
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/portal.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/portal.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..127ffc6c2ca37a86168bbf50a0a4549961358412
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/portal.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ReactDOM from 'react-dom';
+
+export default class Portal extends React.Component {
+ static propTypes = {
+ children: PropTypes.any,
+ portalId: PropTypes.string,
+ };
+
+ constructor(props) {
+ super(props);
+ this.el = document.createElement('div');
+ }
+
+ componentDidMount() {
+ this.portalRoot = document.getElementById(this.props.portalId);
+ if (!this.portalRoot) {
+ this.portalRoot = document.createElement('div');
+ this.portalRoot.setAttribute('id', this.props.portalId);
+ document.body.appendChild(this.portalRoot);
+ }
+ this.portalRoot.appendChild(this.el);
+ }
+
+ componentWillUnmount() {
+ this.portalRoot.removeChild(this.el);
+ }
+
+ render() {
+ return ReactDOM.createPortal(this.props.children, this.el);
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/tab_loop.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/tab_loop.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..2d51e0569c32399e60cd772186c66bf9c59ae64a
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/tab_loop.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+// TabLoop prevents the user from tabbing outside of the popper
+// It creates a tabindex loop so that "Tab" on the last element will focus the first element
+// and "Shift Tab" on the first element will focus the last element
+
+const focusableElementsSelector = '[tabindex], a, button, input, select, textarea';
+const focusableFilter = node => !node.disabled && node.tabIndex !== -1;
+
+export default class TabLoop extends React.Component {
+ static get defaultProps() {
+ return {
+ enableTabLoop: true,
+ };
+ }
+
+ static propTypes = {
+ children: PropTypes.any,
+ enableTabLoop: PropTypes.bool,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.tabLoopRef = React.createRef();
+ }
+
+ // query all focusable elements
+ // trim first and last because they are the focus guards
+ getTabChildren = () => Array.prototype.slice
+ .call(
+ this.tabLoopRef.current.querySelectorAll(focusableElementsSelector),
+ 1,
+ -1,
+ )
+ .filter(focusableFilter);
+
+ handleFocusStart = (e) => {
+ const tabChildren = this.getTabChildren();
+ tabChildren
+ && tabChildren.length > 1
+ && tabChildren[tabChildren.length - 1].focus();
+ };
+
+ handleFocusEnd = (e) => {
+ const tabChildren = this.getTabChildren();
+ tabChildren && tabChildren.length > 1 && tabChildren[0].focus();
+ };
+
+ render() {
+ if (!this.props.enableTabLoop) {
+ return this.props.children;
+ }
+ return (
+
+
+ {this.props.children}
+
+
+ );
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/time.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/time.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..38da994fb216afc50a65d55fe3e90e8649145692
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/time.jsx
@@ -0,0 +1,230 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ getHours,
+ getMinutes,
+ setHours,
+ setMinutes,
+ newDate,
+ getStartOfDay,
+ addMinutes,
+ formatDate,
+ isBefore,
+ isEqual,
+ isTimeInDisabledRange,
+ isTimeDisabled,
+ timesToInjectAfter,
+} from './date_utils';
+
+export default class Time extends React.Component {
+ static get defaultProps() {
+ return {
+ intervals: 30,
+ onTimeChange: () => {},
+ todayButton: null,
+ timeCaption: 'Time',
+ };
+ }
+
+ static calcCenterPosition = (listHeight, centerLiRef) => (
+ centerLiRef.offsetTop - (listHeight / 2 - centerLiRef.clientHeight / 2)
+ );
+
+ static propTypes = {
+ format: PropTypes.string,
+ includeTimes: PropTypes.array,
+ intervals: PropTypes.number,
+ selected: PropTypes.instanceOf(Date),
+ openToDate: PropTypes.instanceOf(Date),
+ onChange: PropTypes.func,
+ timeClassName: PropTypes.func,
+ todayButton: PropTypes.node,
+ minTime: PropTypes.instanceOf(Date),
+ maxTime: PropTypes.instanceOf(Date),
+ excludeTimes: PropTypes.array,
+ filterTime: PropTypes.func,
+ monthRef: PropTypes.object,
+ timeCaption: PropTypes.string,
+ injectTimes: PropTypes.array,
+ handleOnKeyDown: PropTypes.func,
+ locale: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.shape({ locale: PropTypes.object }),
+ ]),
+ showTimeSelectOnly: PropTypes.bool,
+ };
+
+ state = {
+ height: null,
+ };
+
+ componentDidMount() {
+ // code to ensure selected time will always be in focus within time window when it first appears
+ this.list.scrollTop = Time.calcCenterPosition(
+ this.props.monthRef
+ ? this.props.monthRef.clientHeight - this.header.clientHeight
+ : this.list.clientHeight,
+ this.centerLi,
+ );
+ if (this.props.monthRef && this.header) {
+ this.setState({
+ height: this.props.monthRef.clientHeight - this.header.clientHeight,
+ });
+ }
+ }
+
+ handleClick = (time) => {
+ if (
+ ((this.props.minTime || this.props.maxTime)
+ && isTimeInDisabledRange(time, this.props))
+ || ((this.props.excludeTimes
+ || this.props.includeTimes
+ || this.props.filterTime)
+ && isTimeDisabled(time, this.props))
+ ) {
+ return;
+ }
+ this.props.onChange(time);
+ };
+
+ liClasses = (time, currH, currM) => {
+ const classes = [
+ 'react-datepicker__time-list-item',
+ this.props.timeClassName
+ ? this.props.timeClassName(time, currH, currM)
+ : undefined,
+ ];
+
+ if (
+ this.props.selected
+ && currH === getHours(time)
+ && currM === getMinutes(time)
+ ) {
+ classes.push('react-datepicker__time-list-item--selected');
+ }
+ if (
+ ((this.props.minTime || this.props.maxTime)
+ && isTimeInDisabledRange(time, this.props))
+ || ((this.props.excludeTimes
+ || this.props.includeTimes
+ || this.props.filterTime)
+ && isTimeDisabled(time, this.props))
+ ) {
+ classes.push('react-datepicker__time-list-item--disabled');
+ }
+ if (
+ this.props.injectTimes
+ && (getHours(time) * 60 + getMinutes(time)) % this.props.intervals !== 0
+ ) {
+ classes.push('react-datepicker__time-list-item--injected');
+ }
+
+ return classes.join(' ');
+ };
+
+ handleOnKeyDown = (event, time) => {
+ if (event.key === ' ') {
+ event.preventDefault();
+ event.key = 'Enter';
+ }
+
+ if (event.key === 'Enter') {
+ this.handleClick(time);
+ }
+ this.props.handleOnKeyDown(event);
+ };
+
+ renderTimes = () => {
+ let times = [];
+ const format = this.props.format ? this.props.format : 'p';
+ const { intervals } = this.props;
+
+ const base = getStartOfDay(newDate(this.props.selected));
+ const multiplier = 1440 / intervals;
+ const sortedInjectTimes = this.props.injectTimes
+ && this.props.injectTimes.sort((a, b) => a - b);
+
+ const activeDate = this.props.selected || this.props.openToDate || newDate();
+ const currH = getHours(activeDate);
+ const currM = getMinutes(activeDate);
+ const activeTime = setHours(setMinutes(base, currM), currH);
+
+ for (let i = 0; i < multiplier; i++) {
+ const currentTime = addMinutes(base, i * intervals);
+ times.push(currentTime);
+
+ if (sortedInjectTimes) {
+ const timesToInject = timesToInjectAfter(
+ base,
+ currentTime,
+ i,
+ intervals,
+ sortedInjectTimes,
+ );
+ times = times.concat(timesToInject);
+ }
+ }
+
+ return times.map((time, i) => (
+ {
+ if (isBefore(time, activeTime) || isEqual(time, activeTime)) {
+ this.centerLi = li;
+ }
+ }}
+ onKeyDown={(ev) => {
+ this.handleOnKeyDown(ev, time);
+ }}
+ tabIndex="0"
+ >
+ {formatDate(time, format, this.props.locale)}
+
+ ));
+ };
+
+ render() {
+ const { height } = this.state;
+
+ return (
+
+
{
+ this.header = header;
+ }}
+ >
+
+ {this.props.timeCaption}
+
+
+
+
+
{
+ this.list = list;
+ }}
+ style={height ? { height } : {}}
+ tabIndex="0"
+ >
+ {this.renderTimes()}
+
+
+
+
+ );
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/week.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/week.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..6c6436128b5a4708524202a60fb9a58380b9c69f
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/week.jsx
@@ -0,0 +1,153 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Day from './day';
+import WeekNumber from './week_number';
+import * as utils from './date_utils';
+
+export default class Week extends React.Component {
+ static get defaultProps() {
+ return {
+ shouldCloseOnSelect: true,
+ };
+ }
+ static propTypes = {
+ ariaLabelPrefix: PropTypes.string,
+ disabledKeyboardNavigation: PropTypes.bool,
+ day: PropTypes.instanceOf(Date).isRequired,
+ dayClassName: PropTypes.func,
+ disabledDayAriaLabelPrefix: PropTypes.string,
+ chooseDayAriaLabelPrefix: PropTypes.string,
+ endDate: PropTypes.instanceOf(Date),
+ excludeDates: PropTypes.array,
+ filterDate: PropTypes.func,
+ formatWeekNumber: PropTypes.func,
+ highlightDates: PropTypes.instanceOf(Map),
+ includeDates: PropTypes.array,
+ inline: PropTypes.bool,
+ shouldFocusDayInline: PropTypes.bool,
+ locale: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.shape({ locale: PropTypes.object }),
+ ]),
+ maxDate: PropTypes.instanceOf(Date),
+ calendarStartDay: PropTypes.number,
+ minDate: PropTypes.instanceOf(Date),
+ month: PropTypes.number,
+ onDayClick: PropTypes.func,
+ onDayMouseEnter: PropTypes.func,
+ onWeekSelect: PropTypes.func,
+ preSelection: PropTypes.instanceOf(Date),
+ selected: PropTypes.instanceOf(Date),
+ selectingDate: PropTypes.instanceOf(Date),
+ selectsEnd: PropTypes.bool,
+ selectsStart: PropTypes.bool,
+ selectsRange: PropTypes.bool,
+ showWeekNumber: PropTypes.bool,
+ startDate: PropTypes.instanceOf(Date),
+ setOpen: PropTypes.func,
+ shouldCloseOnSelect: PropTypes.bool,
+ renderDayContents: PropTypes.func,
+ handleOnKeyDown: PropTypes.func,
+ isInputFocused: PropTypes.bool,
+ containerRef: PropTypes.oneOfType([
+ PropTypes.func,
+ // PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
+ ]),
+ monthShowsDuplicateDaysEnd: PropTypes.bool,
+ monthShowsDuplicateDaysStart: PropTypes.bool,
+ };
+
+ handleDayClick = (day, event) => {
+ if (this.props.onDayClick) {
+ this.props.onDayClick(day, event);
+ }
+ };
+
+ handleDayMouseEnter = (day) => {
+ if (this.props.onDayMouseEnter) {
+ this.props.onDayMouseEnter(day);
+ }
+ };
+
+ handleWeekClick = (day, weekNumber, event) => {
+ if (typeof this.props.onWeekSelect === 'function') {
+ this.props.onWeekSelect(day, weekNumber, event);
+ }
+ if (this.props.shouldCloseOnSelect) {
+ this.props.setOpen(false);
+ }
+ };
+
+ formatWeekNumber = (date) => {
+ if (this.props.formatWeekNumber) {
+ return this.props.formatWeekNumber(date);
+ }
+ return utils.getWeek(date);
+ };
+
+ renderDays = () => {
+ const startOfWeek = utils.getStartOfWeek(
+ this.props.day,
+ this.props.locale,
+ this.props.calendarStartDay,
+ );
+ const days = [];
+ const weekNumber = this.formatWeekNumber(startOfWeek);
+ if (this.props.showWeekNumber) {
+ const onClickAction = this.props.onWeekSelect
+ ? this.handleWeekClick.bind(this, startOfWeek, weekNumber)
+ : undefined;
+ days.push();
+ }
+ return days.concat([0, 1, 2, 3, 4, 5, 6].map((offset) => {
+ const day = utils.addDays(startOfWeek, offset);
+ return (
+
+ );
+ }));
+ };
+
+ render() {
+ return {this.renderDays()}
;
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/week_number.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/week_number.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..3570b04f2be90ac745e3ea76151e92dcfba60ebe
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/week_number.jsx
@@ -0,0 +1,34 @@
+// @flow
+import React from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+
+export default class WeekNumber extends React.Component {
+ static propTypes = {
+ weekNumber: PropTypes.number.isRequired,
+ onClick: PropTypes.func,
+ };
+
+ handleClick = (event) => {
+ if (this.props.onClick) {
+ this.props.onClick(event);
+ }
+ };
+
+ render() {
+ const { weekNumber, ariaLabelPrefix = 'week ', onClick } = this.props;
+ const weekNumberClasses = {
+ 'react-datepicker__week-number': true,
+ 'react-datepicker__week-number--clickable': !!onClick,
+ };
+ return (
+
+ {weekNumber}
+
+ );
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/year.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/year.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..5c658c809f1808f2de1074914fd1e6f9f0387800
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/year.jsx
@@ -0,0 +1,150 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { getYear, newDate } from './date_utils';
+import * as utils from './date_utils';
+import classnames from 'classnames';
+
+export default class Year extends React.Component {
+ static propTypes = {
+ date: PropTypes.string,
+ disabledKeyboardNavigation: PropTypes.bool,
+ onDayClick: PropTypes.func,
+ preSelection: PropTypes.instanceOf(Date),
+ setPreSelection: PropTypes.func,
+ selected: PropTypes.object,
+ inline: PropTypes.bool,
+ maxDate: PropTypes.instanceOf(Date),
+ minDate: PropTypes.instanceOf(Date),
+ yearItemNumber: PropTypes.number,
+ };
+
+ constructor(props) {
+ super(props);
+ }
+
+ YEAR_REFS = [...Array(this.props.yearItemNumber)].map(() => React.createRef());
+
+ isDisabled = date => utils.isDayDisabled(date, this.props);
+
+ isExcluded = date => utils.isDayExcluded(date, this.props);
+
+ updateFocusOnPaginate = (refIndex) => {
+ const waitForReRender = function () {
+ this.YEAR_REFS[refIndex].current.focus();
+ }.bind(this);
+
+ window.requestAnimationFrame(waitForReRender);
+ };
+
+ handleYearClick = (day, event) => {
+ if (this.props.onDayClick) {
+ this.props.onDayClick(day, event);
+ }
+ };
+
+ handleYearNavigation = (newYear, newDate) => {
+ const { date, yearItemNumber } = this.props;
+ const { startPeriod } = utils.getYearsPeriod(date, yearItemNumber);
+
+ if (this.isDisabled(newDate) || this.isExcluded(newDate)) return;
+ this.props.setPreSelection(newDate);
+
+ if (newYear - startPeriod === -1) {
+ this.updateFocusOnPaginate(yearItemNumber - 1);
+ } else if (newYear - startPeriod === yearItemNumber) {
+ this.updateFocusOnPaginate(0);
+ } else this.YEAR_REFS[newYear - startPeriod].current.focus();
+ };
+
+ isSameDay = (y, other) => utils.isSameDay(y, other);
+
+ isKeyboardSelected = (y) => {
+ const date = utils.getStartOfYear(utils.setYear(this.props.date, y));
+ return (
+ !this.props.disabledKeyboardNavigation
+ && !this.props.inline
+ && !utils.isSameDay(date, utils.getStartOfYear(this.props.selected))
+ && utils.isSameDay(date, utils.getStartOfYear(this.props.preSelection))
+ );
+ };
+
+ onYearClick = (e, y) => {
+ const { date } = this.props;
+ this.handleYearClick(utils.getStartOfYear(utils.setYear(date, y)), e);
+ };
+
+ onYearKeyDown = (e, y) => {
+ const { key } = e;
+ if (!this.props.disabledKeyboardNavigation) {
+ switch (key) {
+ case 'Enter':
+ this.onYearClick(e, y);
+ this.props.setPreSelection(this.props.selected);
+ break;
+ case 'ArrowRight':
+ this.handleYearNavigation(
+ y + 1,
+ utils.addYears(this.props.preSelection, 1),
+ );
+ break;
+ case 'ArrowLeft':
+ this.handleYearNavigation(
+ y - 1,
+ utils.subYears(this.props.preSelection, 1),
+ );
+ break;
+ }
+ }
+ };
+
+ getYearClassNames = (y) => {
+ const { minDate, maxDate, selected } = this.props;
+ return classnames('react-datepicker__year-text', {
+ 'react-datepicker__year-text--selected': y === getYear(selected),
+ 'react-datepicker__year-text--disabled':
+ (minDate || maxDate) && utils.isYearDisabled(y, this.props),
+ // 'react-datepicker__year-text--keyboard-selected':
+ // this.isKeyboardSelected(y),
+ 'react-datepicker__year-text--today': y === getYear(newDate()),
+ });
+ };
+
+ getYearTabIndex = (y) => {
+ if (this.props.disabledKeyboardNavigation) return '-1';
+ const preSelected = utils.getYear(this.props.preSelection);
+
+ return y === preSelected ? '0' : '-1';
+ };
+
+ render() {
+ const yearsList = [];
+ const { date, yearItemNumber } = this.props;
+ const { startPeriod, endPeriod } = utils.getYearsPeriod(
+ date,
+ yearItemNumber,
+ );
+
+ for (let y = startPeriod; y <= endPeriod; y++) {
+ yearsList.push( {
+ this.onYearClick(ev, y);
+ }}
+ onKeyDown={(ev) => {
+ this.onYearKeyDown(ev, y);
+ }}
+ tabIndex={this.getYearTabIndex(y)}
+ className={this.getYearClassNames(y)}
+ key={y}
+ >
+ {y}
+
);
+ }
+
+ return (
+
+ );
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/year_dropdown.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/year_dropdown.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..b4d10f48bf7eaf608f91c9cd299e657bc4e8da0b
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/year_dropdown.jsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import YearDropdownOptions from './year_dropdown_options';
+import onClickOutside from 'react-onclickoutside';
+import { getYear } from './date_utils';
+
+const WrappedYearDropdownOptions = onClickOutside(YearDropdownOptions);
+
+export default class YearDropdown extends React.Component {
+ static propTypes = {
+ adjustDateOnChange: PropTypes.bool,
+ dropdownMode: PropTypes.oneOf(['scroll', 'select']).isRequired,
+ maxDate: PropTypes.instanceOf(Date),
+ minDate: PropTypes.instanceOf(Date),
+ onChange: PropTypes.func.isRequired,
+ scrollableYearDropdown: PropTypes.bool,
+ year: PropTypes.number.isRequired,
+ yearDropdownItemNumber: PropTypes.number,
+ date: PropTypes.instanceOf(Date),
+ onSelect: PropTypes.func,
+ setOpen: PropTypes.func,
+ };
+
+ state = {
+ dropdownVisible: false,
+ };
+
+ renderSelectOptions = () => {
+ const minYear = this.props.minDate ? getYear(this.props.minDate) : 1900;
+ const maxYear = this.props.maxDate ? getYear(this.props.maxDate) : 2100;
+
+ const options = [];
+ for (let i = minYear; i <= maxYear; i++) {
+ options.push();
+ }
+ return options;
+ };
+
+ onSelectChange = (e) => {
+ this.onChange(e.target.value);
+ };
+
+ renderSelectMode = () => (
+
+ );
+
+ renderReadView = visible => (
+ this.toggleDropdown(event)}
+ >
+
+
+ {this.props.year}
+
+
+ );
+
+ renderDropdown = () => (
+
+ );
+
+ renderScrollMode = () => {
+ const { dropdownVisible } = this.state;
+ const result = [this.renderReadView(!dropdownVisible)];
+ if (dropdownVisible) {
+ result.unshift(this.renderDropdown());
+ }
+ return result;
+ };
+
+ onChange = (year) => {
+ this.toggleDropdown();
+ if (year === this.props.year) return;
+ this.props.onChange(year);
+ };
+
+ toggleDropdown = (event) => {
+ this.setState(
+ {
+ dropdownVisible: !this.state.dropdownVisible,
+ },
+ () => {
+ if (this.props.adjustDateOnChange) {
+ this.handleYearChange(this.props.date, event);
+ }
+ },
+ );
+ };
+
+ handleYearChange = (date, event) => {
+ this.onSelect(date, event);
+ this.setOpen();
+ };
+
+ onSelect = (date, event) => {
+ if (this.props.onSelect) {
+ this.props.onSelect(date, event);
+ }
+ };
+
+ setOpen = () => {
+ if (this.props.setOpen) {
+ this.props.setOpen(true);
+ }
+ };
+
+ render() {
+ let renderedDropdown;
+ switch (this.props.dropdownMode) {
+ case 'scroll':
+ renderedDropdown = this.renderScrollMode();
+ break;
+ case 'select':
+ renderedDropdown = this.renderSelectMode();
+ break;
+ }
+
+ return (
+
+ {renderedDropdown}
+
+ );
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/year_dropdown_options.jsx b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/year_dropdown_options.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..c37a9be26abb515a198c733bfe02bdd4594ca066
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/lib/react-datepicker/year_dropdown_options.jsx
@@ -0,0 +1,130 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { getYear } from './date_utils';
+
+function generateYears(year, noOfYear, minDate, maxDate) {
+ const list = [];
+ for (let i = 0; i < 2 * noOfYear + 1; i++) {
+ const newYear = year + noOfYear - i;
+ let isInRange = true;
+
+ if (minDate) {
+ isInRange = getYear(minDate) <= newYear;
+ }
+
+ if (maxDate && isInRange) {
+ isInRange = getYear(maxDate) >= newYear;
+ }
+
+ if (isInRange) {
+ list.push(newYear);
+ }
+ }
+
+ return list;
+}
+
+export default class YearDropdownOptions extends React.Component {
+ static propTypes = {
+ minDate: PropTypes.instanceOf(Date),
+ maxDate: PropTypes.instanceOf(Date),
+ onCancel: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ scrollableYearDropdown: PropTypes.bool,
+ year: PropTypes.number.isRequired,
+ yearDropdownItemNumber: PropTypes.number,
+ };
+
+ constructor(props) {
+ super(props);
+ const { yearDropdownItemNumber, scrollableYearDropdown } = props;
+ const noOfYear = yearDropdownItemNumber || (scrollableYearDropdown ? 10 : 5);
+
+ this.state = {
+ yearsList: generateYears(
+ this.props.year,
+ noOfYear,
+ this.props.minDate,
+ this.props.maxDate,
+ ),
+ };
+ }
+
+ renderOptions = () => {
+ const selectedYear = this.props.year;
+ const options = this.state.yearsList.map(year => (
+
+ {selectedYear === year ? (
+ ✓
+ ) : (
+ ''
+ )}
+ {year}
+
+ ));
+
+ const minYear = this.props.minDate ? getYear(this.props.minDate) : null;
+ const maxYear = this.props.maxDate ? getYear(this.props.maxDate) : null;
+
+ if (!maxYear || !this.state.yearsList.find(year => year === maxYear)) {
+ options.unshift();
+ }
+
+ if (!minYear || !this.state.yearsList.find(year => year === minYear)) {
+ options.push();
+ }
+
+ return options;
+ };
+
+ onChange = (year) => {
+ this.props.onChange(year);
+ };
+
+ handleClickOutside = () => {
+ this.props.onCancel();
+ };
+
+ shiftYears = (amount) => {
+ const years = this.state.yearsList.map(year => year + amount);
+
+ this.setState({
+ yearsList: years,
+ });
+ };
+
+ incrementYears = () => this.shiftYears(1);
+
+ decrementYears = () => this.shiftYears(-1);
+
+ render() {
+ const dropdownClass = classNames({
+ 'react-datepicker__year-dropdown': true,
+ 'react-datepicker__year-dropdown--scrollable':
+ this.props.scrollableYearDropdown,
+ });
+
+ return {this.renderOptions()}
;
+ }
+}
diff --git a/packages/discuz-design/components/datepicker/layouts/web/month-picker.tsx b/packages/discuz-design/components/datepicker/layouts/web/month-picker.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..70850bb4c463779057b502247a2ba8205a55cda1
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/month-picker.tsx
@@ -0,0 +1,108 @@
+import React, { useState, forwardRef } from 'react';
+import classNames from 'classnames';
+import DatePicker, { registerLocale } from './lib/react-datepicker';
+import { DatepickerProps } from '../../interface';
+import { useConfig } from '../../../../extends/configContext';
+import { getYear, getMonth } from 'date-fns';
+
+import Input from '../../../input';
+import Icon from '../../../icon';
+import Button from '../../../button';
+
+import zhCN from 'date-fns/locale/zh-CN';
+registerLocale('zhCN', zhCN);
+
+/**
+ * 月份选择器
+ */
+export default (props) => {
+ const { clsPrefix } = useConfig();
+
+ const [startDate, setStartDate] = useState(new Date());
+
+ const onNowClick = ({ changeYear, changeMonth }) => {
+ changeYear(getYear(Date.now()));
+ changeMonth(getMonth(Date.now()));
+ };
+
+ const ExampleCustomInput = forwardRef(({ value, isYear, onClick }, ref) => (
+
+ ));
+
+ const renderCustomHeader = ({
+ date,
+ changeYear,
+ changeMonth,
+ decreaseMonth,
+ increaseMonth,
+ prevMonthButtonDisabled,
+ nextMonthButtonDisabled,
+ }) => (
+
+
+ changeYear(getYear(date))}
+ showYearPicker
+ dateFormat="yyyy"
+ locale="zhCN"
+ customInput={}
+ />
+
+
+
+ changeYear(getYear(date) - 1)}
+ >
+
+ changeYear(getYear(date) + 1)}
+ size={12}
+ >
+
+
+ );
+
+ return (
+ setStartDate(date)}
+ customInput={}
+ renderCustomHeader={renderCustomHeader}
+ showMonthYearPicker
+ locale="zhCN"
+ dateFormat="yyyy-MM"
+ showPopperArrow={false}
+ />
+ );
+};
diff --git a/packages/discuz-design/components/datepicker/layouts/web/year-picker.jsx b/packages/discuz-design/components/datepicker/layouts/web/year-picker.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..eaaee53b4e5a08928f1f747c6443d83085bea121
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/layouts/web/year-picker.jsx
@@ -0,0 +1,99 @@
+import React, { useState, forwardRef } from 'react';
+import classNames from 'classnames';
+import DatePicker, { registerLocale } from './lib/react-datepicker';
+import { DatepickerProps } from '../../interface';
+import { useConfig } from '../../../../extends/configContext';
+import { getYear, getMonth } from 'date-fns';
+
+import Input from '../../../input';
+import Icon from '../../../icon';
+import Button from '../../../button';
+
+import zhCN from 'date-fns/locale/zh-CN';
+registerLocale('zhCN', zhCN);
+
+/**
+ * 年份选择器
+ */
+export default (props) => {
+ const { clsPrefix } = useConfig();
+
+ const [startDate, setStartDate] = useState(new Date());
+
+ const onNowClick = ({ changeYear }) => {
+ changeYear(getYear(Date.now()));
+ };
+
+ const ExampleCustomInput = forwardRef(({ value, isYear, onClick }, ref) => (
+
+ ));
+
+ const renderCustomHeader = ({
+ date,
+ changeYear,
+ changeMonth,
+ decreaseMonth,
+ increaseMonth,
+ prevMonthButtonDisabled,
+ nextMonthButtonDisabled,
+ }) => (
+
+
+
+
+
+
+ changeYear(getYear(date) - 1)}
+ >
+
+ changeYear(getYear(date) + 1)}
+ size={12}
+ >
+
+
+ );
+
+ return (
+ setStartDate(date)}
+ customInput={
+
+ }
+ // renderCustomHeader={renderCustomHeader}
+ showYearPicker
+ locale="zhCN"
+ dateFormat="yyyy"
+ showPopperArrow={false}
+ yearItemNumber={12}
+ />
+ );
+};
diff --git a/packages/discuz-design/components/datepicker/styles/index.scss b/packages/discuz-design/components/datepicker/styles/index.scss
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/packages/discuz-design/components/datepicker/styles/index.scss
@@ -0,0 +1 @@
+
diff --git a/packages/discuz-design/components/index.ts b/packages/discuz-design/components/index.ts
index a7fab31add74b4347039a207b046bc3b13bcb232..306290ef5d3e03071e92090ec7b464ee834b56ad 100644
--- a/packages/discuz-design/components/index.ts
+++ b/packages/discuz-design/components/index.ts
@@ -39,3 +39,4 @@ export { default as ActionSheet} from './action-sheet';
export { default as SideNav } from './side-nav';
export { default as Step } from './step';
export { default as Popover} from './popover';
+export * as Datepicker from './datepicker';
diff --git a/packages/discuz-design/package.json b/packages/discuz-design/package.json
index 253f60ff062ab2ef7767e8256145c68d23575271..2c5e92b2d0480c63dd5f755e062078a41c6b2d4a 100644
--- a/packages/discuz-design/package.json
+++ b/packages/discuz-design/package.json
@@ -77,16 +77,23 @@
},
"dependencies": {
"@babel/runtime": "7.12.5",
+ "@popperjs/core": "^2.9.2",
"better-scroll": "1.11.1",
+ "classnames": "^2.2.6",
"core-js": "3.8.3",
+ "date-fns": "^2.25.0",
"eventemitter3": "4.0.7",
"gulp-sass": "4.1.0",
"highlight.js": "^10.7.2",
"hoist-non-react-statics": "3.3.2",
+ "prop-types": "^15.7.2",
"qs": "6.1.1",
"react": "17.0.1",
+ "react-datepicker": "^4.3.0",
"react-dom": "17.0.1",
"react-html-parser": "2.0.2",
+ "react-onclickoutside": "^6.12.0",
+ "react-popper": "^2.2.5",
"react-virtualized": "9.22.3",
"recorder-core": "1.1.21021500",
"video.js": "7.11.4",
diff --git a/packages/discuz-design/site/mini/src/app.config.js b/packages/discuz-design/site/mini/src/app.config.js
index 9cc42995e06780df0970acb81d2e23329f8e87ed..2fa3fbf7dd62d957e6d3a7a4a0f5c47fc060645e 100644
--- a/packages/discuz-design/site/mini/src/app.config.js
+++ b/packages/discuz-design/site/mini/src/app.config.js
@@ -39,9 +39,10 @@ export default {
"pages/badge/index",
"pages/slider/index",
"pages/action-sheet/index",
+ "pages/popover/index",
+ "pages/datepicker/index",
"pages/side-nav/index",
"pages/step/index",
- "pages/popover/index"
],
"window": {
"backgroundTextStyle": "light",
diff --git a/packages/discuz-design/site/mini/src/componentList.json b/packages/discuz-design/site/mini/src/componentList.json
index 9a954ca8b23eae51006e80b2c4d4647fc761c8b4..7877c9d5647a63b5f600c215ba544dc1b8cccac3 100644
--- a/packages/discuz-design/site/mini/src/componentList.json
+++ b/packages/discuz-design/site/mini/src/componentList.json
@@ -113,6 +113,9 @@
},
"popover": {
"url": "popover"
+ },
+ "datepicker": {
+ "url": "datepicker"
}
}
}
diff --git a/packages/discuz-design/site/mini/src/pages/datepicker/index.config.js b/packages/discuz-design/site/mini/src/pages/datepicker/index.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..c01b1dfdf9a177d9992a1106e11e653a67a217ff
--- /dev/null
+++ b/packages/discuz-design/site/mini/src/pages/datepicker/index.config.js
@@ -0,0 +1,3 @@
+export default {
+ navigationBarTitleText: '日期选择'
+}
diff --git a/packages/discuz-design/site/mini/src/pages/datepicker/index.jsx b/packages/discuz-design/site/mini/src/pages/datepicker/index.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..8ee1860a75eac0cc5703558bb62a732add1fdc85
--- /dev/null
+++ b/packages/discuz-design/site/mini/src/pages/datepicker/index.jsx
@@ -0,0 +1,20 @@
+import React, { Component } from "react";
+import Taro from '@tarojs/taro';
+import { View } from "@tarojs/components";
+import DatepickerExamples from "../../../../../components/datepicker/__examples__/mini/index";
+
+export default class Index extends Component {
+ componentDidMount() {}
+
+ componentWillUnmount() {}
+
+ componentDidShow() {}
+
+ componentDidHide() {}
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/packages/discuz-design/site/web/pages/datepicker.js b/packages/discuz-design/site/web/pages/datepicker.js
new file mode 100644
index 0000000000000000000000000000000000000000..1ed26d45c6c7f76c907d0f9a84cc3ac8934ad891
--- /dev/null
+++ b/packages/discuz-design/site/web/pages/datepicker.js
@@ -0,0 +1,8 @@
+import React from "react";
+import DatepickerExamples from "../../../components/datepicker/__examples__/web/index";
+
+function Preview() {
+ return ;
+}
+
+export default Preview;
diff --git a/packages/discuz-doc/webpack.config.js b/packages/discuz-doc/webpack.config.js
index e0903b17a2fe6b242fd4b43c157db82e65411dd7..3372a11ad01b1eb03c2f2e4de0862fc943a160da 100644
--- a/packages/discuz-doc/webpack.config.js
+++ b/packages/discuz-doc/webpack.config.js
@@ -50,6 +50,7 @@ const babelOptions = {
plugins: [
devMode && "react-refresh/babel",
"@babel/plugin-transform-runtime",
+ "@babel/plugin-proposal-class-properties",
["@babel/plugin-proposal-decorators", {
legacy: true
}],
diff --git a/packages/discuz-theme/src/components.scss b/packages/discuz-theme/src/components.scss
index 40780209b4dd0a2ffee0bb3887824ebdd7360d1e..6e1ebde369873109e434374d868e095b3cebb2e5 100644
--- a/packages/discuz-theme/src/components.scss
+++ b/packages/discuz-theme/src/components.scss
@@ -37,4 +37,5 @@
@import 'components/upload.scss';
@import 'components/video.scss';
@import 'components/web-picker.scss';
+@import 'components/date-picker.scss';
@import 'components/step.scss';
diff --git a/packages/discuz-theme/src/components/date-picker.scss b/packages/discuz-theme/src/components/date-picker.scss
new file mode 100644
index 0000000000000000000000000000000000000000..8c3cef38dc1851969a6d35dd3e70f6ff20a64608
--- /dev/null
+++ b/packages/discuz-theme/src/components/date-picker.scss
@@ -0,0 +1,816 @@
+@import "../core";
+
+$datepicker__background-color: $body-bg-color !default;
+$datepicker__border-color: $border-color !default;
+$datepicker__highlighted-color: #3dcc4a !default;
+$datepicker__muted-color: #ccc;
+$datepicker__selected-color: $primary-color !default;
+$datepicker__selected-color--hover: $primary-color20 !default;
+$datepicker__text-color: #000 !default;
+$datepicker__header-color: #000 !default;
+$datepicker__navigation-disabled-color: lighten(#ccc, 10%) !default;
+$datepicker__border-radius: 0.3rem !default;
+$datepicker__day-margin: 0.166rem !default;
+$datepicker__font-size: 0.8rem !default;
+$datepicker__font-family: "Helvetica Neue", helvetica, arial, sans-serif !default;
+$datepicker__item-size: 1.7rem !default;
+$datepicker__margin: 0.4rem !default;
+$datepicker__navigation-button-size: 32px !default;
+$datepicker__triangle-size: 8px !default;
+
+/* ----------mixin-------------- */
+%navigation-chevron {
+ border-color: $datepicker__muted-color;
+ border-style: solid;
+ border-width: 3px 3px 0 0;
+ content: "";
+ display: block;
+ height: 9px;
+ position: absolute;
+ top: 6px;
+ width: 9px;
+
+ &--disabled,
+ &--disabled:hover {
+ border-color: $datepicker__navigation-disabled-color;
+ cursor: default;
+ }
+}
+
+%triangle-arrow {
+ margin-left: -$datepicker__triangle-size * 0.5;
+ position: absolute;
+ width: 0;
+
+ &::before,
+ &::after {
+ box-sizing: content-box;
+ position: absolute;
+ border: $datepicker__triangle-size solid transparent;
+ height: 0;
+ width: 1px;
+ content: "";
+ z-index: -1;
+ border-width: $datepicker__triangle-size;
+ left: -$datepicker__triangle-size;
+ }
+
+ &::before {
+ border-bottom-color: $datepicker__border-color;
+ }
+}
+
+%triangle-arrow-up {
+ @extend %triangle-arrow;
+
+ top: 0;
+ margin-top: -$datepicker__triangle-size;
+
+ &::before,
+ &::after {
+ border-top: none;
+ border-bottom-color: $datepicker__background-color;
+ }
+
+ &::after {
+ top: 0;
+ }
+
+ &::before {
+ top: -1px;
+ border-bottom-color: $datepicker__border-color;
+ }
+}
+
+%triangle-arrow-down {
+ @extend %triangle-arrow;
+
+ bottom: 0;
+ margin-bottom: -$datepicker__triangle-size;
+
+ &::before,
+ &::after {
+ border-bottom: none;
+ border-top-color: #fff;
+ }
+
+ &::after {
+ bottom: 0;
+ }
+
+ &::before {
+ bottom: -1px;
+ border-top-color: $datepicker__border-color;
+ }
+}
+
+.react-datepicker-wrapper {
+ display: inline-block;
+ padding: 0;
+ border: 0;
+ width: 100%;
+}
+
+.react-datepicker {
+ font-family: $datepicker__font-family;
+ font-size: $datepicker__font-size;
+ background-color: #fff;
+ color: $datepicker__text-color;
+ border: 1px solid $datepicker__border-color;
+ border-radius: $datepicker__border-radius;
+ display: inline-block;
+ position: relative;
+}
+
+.react-datepicker--time-only {
+ .react-datepicker__triangle {
+ left: 35px;
+ }
+
+ .react-datepicker__time-container {
+ border-left: 0;
+ }
+
+ .react-datepicker__time,
+ .react-datepicker__time-box {
+ border-bottom-left-radius: 0.3rem;
+ border-bottom-right-radius: 0.3rem;
+ }
+}
+
+.react-datepicker__triangle {
+ position: absolute;
+ left: 50px;
+}
+
+.react-datepicker-popper {
+ z-index: 1;
+
+ &[data-placement^="bottom"] {
+ padding-top: $datepicker__triangle-size + 2px;
+
+ .react-datepicker__triangle {
+ @extend %triangle-arrow-up;
+ }
+ }
+
+ &[data-placement="bottom-end"],
+ &[data-placement="top-end"] {
+ .react-datepicker__triangle {
+ left: auto;
+ right: 50px;
+ }
+ }
+
+ &[data-placement^="top"] {
+ padding-bottom: $datepicker__triangle-size + 2px;
+
+ .react-datepicker__triangle {
+ @extend %triangle-arrow-down;
+ }
+ }
+
+ &[data-placement^="right"] {
+ padding-left: $datepicker__triangle-size;
+
+ .react-datepicker__triangle {
+ left: auto;
+ right: 42px;
+ }
+ }
+
+ &[data-placement^="left"] {
+ padding-right: $datepicker__triangle-size;
+
+ .react-datepicker__triangle {
+ left: 42px;
+ right: auto;
+ }
+ }
+}
+
+.react-datepicker__header {
+ text-align: center;
+ background-color: $datepicker__background-color;
+ border-bottom: 1px solid $datepicker__border-color;
+ border-top-left-radius: $datepicker__border-radius;
+ padding: 8px 0;
+ position: relative;
+
+ &--time {
+ padding-bottom: 8px;
+ padding-left: 5px;
+ padding-right: 5px;
+
+ &:not(&--only) {
+ border-top-left-radius: 0;
+ }
+ }
+
+ &:not(&--has-time-select) {
+ border-top-right-radius: $datepicker__border-radius;
+ }
+}
+
+.react-datepicker__jumper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .jumper {
+ &__left,
+ &__right {
+ &:hover {
+ cursor: pointer;
+ color: $primary-color;
+ }
+ }
+}
+}
+
+.react-datepicker__year-dropdown-container--select,
+.react-datepicker__month-dropdown-container--select,
+.react-datepicker__month-year-dropdown-container--select,
+.react-datepicker__year-dropdown-container--scroll,
+.react-datepicker__month-dropdown-container--scroll,
+.react-datepicker__month-year-dropdown-container--scroll {
+ display: inline-block;
+ margin: 0 2px;
+}
+
+.react-datepicker__current-month,
+.react-datepicker-time__header,
+.react-datepicker-year-header {
+ margin-top: 0;
+ color: $datepicker__header-color;
+ font-weight: bold;
+ font-size: $datepicker__font-size * 1.18;
+}
+
+.react-datepicker-time__header {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.react-datepicker__navigation {
+ align-items: center;
+ background: none;
+ display: flex;
+ justify-content: center;
+ text-align: center;
+ cursor: pointer;
+ position: absolute;
+ top: 2px;
+ padding: 0;
+ border: none;
+ z-index: 1;
+ height: $datepicker__navigation-button-size;
+ width: $datepicker__navigation-button-size;
+ text-indent: -999em;
+ overflow: hidden;
+
+ &--previous {
+ left: 2px;
+ }
+
+ &--next {
+ right: 2px;
+
+ &--with-time:not(&--with-today-button) {
+ right: 85px;
+ }
+ }
+
+ &--years {
+ position: relative;
+ top: 0;
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+
+ &-previous {
+ top: 4px;
+ }
+
+ &-upcoming {
+ top: -4px;
+ }
+ }
+
+ &:hover {
+ *::before {
+ border-color: darken($datepicker__muted-color, 15%);
+ }
+ }
+}
+
+.react-datepicker__navigation-icon {
+ position: relative;
+ top: -1px;
+ font-size: 20px;
+ width: 0;
+
+ &::before {
+ @extend %navigation-chevron;
+ }
+
+ &--next {
+ left: -2px;
+
+ &::before {
+ transform: rotate(45deg);
+ left: -7px;
+ }
+ }
+
+ &--previous {
+ right: -2px;
+
+ &::before {
+ transform: rotate(225deg);
+ right: -7px;
+ }
+ }
+}
+
+.react-datepicker__month-container {
+ float: left;
+}
+
+.react-datepicker__year {
+ margin: $datepicker__margin;
+ text-align: center;
+
+ &-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-wrap: wrap;
+ width: 220px;
+ }
+
+ .react-datepicker__year-text {
+ display: inline-block;
+ width: 4rem;
+ height: 2rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 2px;
+ }
+}
+
+.react-datepicker__month {
+ margin: $datepicker__margin;
+ text-align: center;
+
+ .react-datepicker__month-text,
+ .react-datepicker__quarter-text {
+ display: inline-block;
+ width: 4rem;
+ height: 2rem;
+ line-height: 2rem;
+ margin: 2px;
+ }
+}
+
+.react-datepicker__input-time-container {
+ clear: both;
+ width: 100%;
+ float: left;
+ margin: 5px 0 10px 15px;
+ text-align: left;
+
+ .react-datepicker-time__caption {
+ display: inline-block;
+ }
+
+ .react-datepicker-time__input-container {
+ display: inline-block;
+
+ .react-datepicker-time__input {
+ display: inline-block;
+ margin-left: 10px;
+
+ input {
+ width: auto;
+ }
+
+ input[type="time"]::-webkit-inner-spin-button,
+ input[type="time"]::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ input[type="time"] {
+ -moz-appearance: textfield;
+ }
+ }
+
+ .react-datepicker-time__delimiter {
+ margin-left: 5px;
+ display: inline-block;
+ }
+ }
+}
+
+.react-datepicker__time-container {
+ float: right;
+ border-left: 1px solid $datepicker__border-color;
+ width: 85px;
+
+ &--with-today-button {
+ display: inline;
+ border: 1px solid #aeaeae;
+ border-radius: 0.3rem;
+ position: absolute;
+ right: -72px;
+ top: 0;
+ }
+
+ .react-datepicker__time {
+ position: relative;
+ background: white;
+ border-bottom-right-radius: 0.3rem;
+
+ .react-datepicker__time-box {
+ width: 85px;
+ overflow-x: hidden;
+ margin: 0 auto;
+ text-align: center;
+ border-bottom-right-radius: 0.3rem;
+
+ ul.react-datepicker__time-list {
+ list-style: none;
+ margin: 0;
+ height: calc(195px + (#{$datepicker__item-size} / 2));
+ overflow-y: scroll;
+ padding-right: 0;
+ padding-left: 0;
+ width: 100%;
+ box-sizing: content-box;
+
+ li.react-datepicker__time-list-item {
+ height: 30px;
+ padding: 5px 10px;
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ cursor: pointer;
+ background-color: $datepicker__background-color;
+ }
+
+ &--selected {
+ background-color: $datepicker__selected-color;
+ color: white;
+ font-weight: bold;
+
+ &:hover {
+ background-color: $datepicker__selected-color;
+ }
+ }
+
+ &--disabled {
+ color: $datepicker__muted-color;
+
+ &:hover {
+ cursor: default;
+ background-color: transparent;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+.react-datepicker__week-number {
+ color: $datepicker__muted-color;
+ display: inline-block;
+ width: $datepicker__item-size;
+ line-height: $datepicker__item-size;
+ text-align: center;
+ margin: $datepicker__day-margin;
+
+ &.react-datepicker__week-number--clickable {
+ cursor: pointer;
+
+ &:hover {
+ border-radius: $datepicker__border-radius;
+ background-color: $datepicker__background-color;
+ }
+ }
+}
+
+.react-datepicker__day-names,
+.react-datepicker__week {
+ white-space: nowrap;
+}
+
+.react-datepicker__day-names {
+ margin-bottom: -8px;
+}
+
+.react-datepicker__day-name,
+.react-datepicker__day,
+.react-datepicker__time-name {
+ color: $datepicker__text-color;
+ display: inline-block;
+ width: $datepicker__item-size;
+ line-height: $datepicker__item-size;
+ text-align: center;
+ margin: $datepicker__day-margin;
+}
+
+.react-datepicker__month,
+.react-datepicker__quarter {
+ &--selected,
+ &--in-selecting-range,
+ &--in-range {
+ border-radius: $datepicker__border-radius;
+ background-color: $datepicker__selected-color;
+ color: #fff;
+
+ &:hover {
+ background-color: $datepicker__selected-color--hover;
+ }
+ }
+
+ &--disabled {
+ color: $datepicker__muted-color;
+ pointer-events: none;
+
+ &:hover {
+ cursor: default;
+ background-color: transparent;
+ }
+ }
+}
+
+.react-datepicker__day,
+.react-datepicker__month-text,
+.react-datepicker__quarter-text,
+.react-datepicker__year-text {
+ cursor: pointer;
+
+ &:hover {
+ border-radius: $datepicker__border-radius;
+ background-color: $datepicker__background-color;
+ }
+
+ &--today {
+ font-weight: bold;
+ color: $primary-color;
+ }
+
+ &--highlighted {
+ border-radius: $datepicker__border-radius;
+ background-color: $datepicker__highlighted-color;
+ color: #fff;
+
+ &:hover {
+ background-color: darken($datepicker__highlighted-color, 5%);
+ }
+
+ &-custom-1 {
+ color: magenta;
+ }
+
+ &-custom-2 {
+ color: green;
+ }
+ }
+
+ &--selected,
+ &--in-selecting-range,
+ &--in-range {
+ border-radius: $datepicker__border-radius;
+ background-color: $datepicker__selected-color;
+ color: #fff;
+
+ &:hover {
+ background-color: $datepicker__selected-color--hover;
+ }
+ }
+
+ &--keyboard-selected {
+ border-radius: $datepicker__border-radius;
+ background-color: transparent;
+ color: $datepicker__text-color;
+
+ &:hover {
+ background-color: $datepicker__selected-color--hover;
+ color: #fff;
+ }
+ }
+
+ &--in-selecting-range:not(&--in-range) {
+ background-color: rgba($datepicker__selected-color, 0.5);
+ }
+
+ &--in-range:not(&--in-selecting-range) {
+ .react-datepicker__month--selecting-range & {
+ background-color: $datepicker__background-color;
+ color: $datepicker__text-color;
+ }
+ }
+
+ &--disabled {
+ cursor: default;
+ color: $datepicker__muted-color;
+
+ &:hover {
+ background-color: transparent;
+ }
+ }
+}
+
+.react-datepicker__month-text,
+.react-datepicker__quarter-text {
+ &.react-datepicker__month--selected,
+ &.react-datepicker__month--in-range,
+ &.react-datepicker__quarter--selected,
+ &.react-datepicker__quarter--in-range {
+ &:hover {
+ background-color: $datepicker__selected-color;
+ }
+ }
+
+ &:hover {
+ background-color: $datepicker__background-color;
+ }
+}
+
+.react-datepicker__input-container {
+ position: relative;
+ display: inline-block;
+ width: 100%;
+}
+
+.react-datepicker__year-read-view,
+.react-datepicker__month-read-view,
+.react-datepicker__month-year-read-view {
+ border: 1px solid transparent;
+ border-radius: $datepicker__border-radius;
+ position: relative;
+
+ &:hover {
+ cursor: pointer;
+
+ .react-datepicker__year-read-view--down-arrow,
+ .react-datepicker__month-read-view--down-arrow {
+ border-top-color: darken($datepicker__muted-color, 10%);
+ }
+ }
+
+ &--down-arrow {
+ @extend %navigation-chevron;
+
+ transform: rotate(135deg);
+ right: -16px;
+ top: 0;
+ }
+}
+
+.react-datepicker__year-dropdown,
+.react-datepicker__month-dropdown,
+.react-datepicker__month-year-dropdown {
+ background-color: $datepicker__background-color;
+ position: absolute;
+ width: 50%;
+ left: 25%;
+ top: 30px;
+ z-index: 1;
+ text-align: center;
+ border-radius: $datepicker__border-radius;
+ border: 1px solid $datepicker__border-color;
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ &--scrollable {
+ height: 150px;
+ overflow-y: scroll;
+ }
+}
+
+.react-datepicker__year-option,
+.react-datepicker__month-option,
+.react-datepicker__month-year-option {
+ line-height: 20px;
+ width: 100%;
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+
+ &:first-of-type {
+ border-top-left-radius: $datepicker__border-radius;
+ border-top-right-radius: $datepicker__border-radius;
+ }
+
+ &:last-of-type {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ border-bottom-left-radius: $datepicker__border-radius;
+ border-bottom-right-radius: $datepicker__border-radius;
+ }
+
+ &:hover {
+ background-color: $datepicker__muted-color;
+
+ .react-datepicker__navigation--years-upcoming {
+ border-bottom-color: darken($datepicker__muted-color, 10%);
+ }
+
+ .react-datepicker__navigation--years-previous {
+ border-top-color: darken($datepicker__muted-color, 10%);
+ }
+ }
+
+ &--selected {
+ position: absolute;
+ left: 15px;
+ }
+}
+
+.react-datepicker__close-icon {
+ cursor: pointer;
+ background-color: transparent;
+ border: 0;
+ outline: 0;
+ padding: 0 6px 0 0;
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 100%;
+ display: table-cell;
+ vertical-align: middle;
+
+ &::after {
+ cursor: pointer;
+ background-color: $datepicker__selected-color;
+ color: #fff;
+ border-radius: 50%;
+ height: 16px;
+ width: 16px;
+ padding: 2px;
+ font-size: 12px;
+ line-height: 1;
+ text-align: center;
+ display: table-cell;
+ vertical-align: middle;
+ content: "\00d7";
+ }
+}
+
+.react-datepicker__today-button {
+ background: $datepicker__background-color;
+ border-top: 1px solid $datepicker__border-color;
+ cursor: pointer;
+ text-align: center;
+ font-weight: bold;
+ padding: 5px 0;
+ clear: left;
+}
+
+.react-datepicker__portal {
+ position: fixed;
+ width: 100vw;
+ height: 100vh;
+ background-color: rgba(0, 0, 0, 0.8);
+ left: 0;
+ top: 0;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ z-index: 2147483647;
+
+ .react-datepicker__day-name,
+ .react-datepicker__day,
+ .react-datepicker__time-name {
+ width: 3rem;
+ line-height: 3rem;
+ }
+
+ // Resize for small screens
+ @media (max-width: 400px), (max-height: 550px) {
+ .react-datepicker__day-name,
+ .react-datepicker__day,
+ .react-datepicker__time-name {
+ width: 2rem;
+ line-height: 2rem;
+ }
+ }
+
+ .react-datepicker__current-month,
+ .react-datepicker-time__header {
+ font-size: $datepicker__font-size * 1.8;
+ }
+}