diff --git a/app/assets/javascripts/components/common/date_picker.jsx b/app/assets/javascripts/components/common/date_picker.jsx index 31946cbe37..f930b20a69 100644 --- a/app/assets/javascripts/components/common/date_picker.jsx +++ b/app/assets/javascripts/components/common/date_picker.jsx @@ -1,8 +1,7 @@ -import React from 'react'; -import createReactClass from 'create-react-class'; +import React, { useState, useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; import DayPicker from 'react-day-picker'; -import OnClickOutside from 'react-onclickoutside'; +import { useOnClickOutside } from 'react-onclickoutside'; import { range, includes } from 'lodash-es'; import { startOfDay, endOfDay, isValid, isAfter, parseISO, getHours, getMinutes, setHours, setMinutes, formatISO } from 'date-fns'; import InputHOC from '../high_order/input_hoc.jsx'; @@ -10,351 +9,302 @@ import Conditional from '../high_order/conditional.jsx'; import CourseDateUtils from '../../utils/course_date_utils.js'; import { formatDateWithoutTime, toDate } from '../../utils/date_utils.js'; -const DatePicker = createReactClass({ - displayName: 'DatePicker', - - propTypes: { - id: PropTypes.string, - value: PropTypes.string, - value_key: PropTypes.string, - spacer: PropTypes.string, - label: PropTypes.string, - timeLabel: PropTypes.string, - valueClass: PropTypes.string, - editable: PropTypes.bool, - enabled: PropTypes.bool, - focus: PropTypes.bool, - inline: PropTypes.bool, - isClearable: PropTypes.bool, - placeholder: PropTypes.string, - p_tag_classname: PropTypes.string, - onBlur: PropTypes.func, - onFocus: PropTypes.func, - onChange: PropTypes.func, - onClick: PropTypes.func, - append: PropTypes.string, - date_props: PropTypes.object, - showTime: PropTypes.bool - }, - - getDefaultProps() { - return { - invalidMessage: I18n.t('application.field_invalid_date') - }; - }, - - getInitialState() { - if (this.props.value) { - const dateObj = toDate(this.props.value); +const DatePicker = (props) => { + const { + id, + value, + value_key, + spacer = ': ', + label, + timeLabel, + valueClass, + editable, + enabled, + focus, + inline, + isClearable, + placeholder, + p_tag_classname, + onBlur, + onFocus, + onChange, + onClick, + append, + date_props, + showTime, + invalidMessage = I18n.t('application.field_invalid_date') + } = props; + + const getInitialState = () => { + if (value) { + const dateObj = toDate(value); return { value: formatDateWithoutTime(dateObj), hour: getHours(dateObj), minute: getMinutes(dateObj), - datePickerVisible: false }; } return { value: '', hour: 0, minute: 0, - datePickerVisible: false }; - }, - - /** - * Update parent component with new date value. - * Used instead of onChange() in InputMixin because we need to - * call this.props.onChange with the full date string, not just YYYY-MM-DD - * @return {null} - */ - onChangeHandler() { - const e = { target: { value: formatISO(this.getDate()) } }; - this.props.onChange(e); - }, - - /** - * Get date object of currently select date, hour and minute - * @return {Date} - */ - getDate() { - let dateObj = toDate(this.state.value); - dateObj = setHours(dateObj, this.state.hour); - return setMinutes(dateObj, this.state.minute); - }, - - getFormattedDate() { - return formatDateWithoutTime(this.getDate()); - }, - - /** - * Get formatted date to be displayed as text, - * based on whether or not to include the time - * @return {String} formatted date - */ - getFormattedDateTime() { - return CourseDateUtils.formattedDateTime(this.getDate(), this.props.showTime); - }, - - getTimeDropdownOptions(type) { - return range(0, type === 'hour' ? 24 : 60).map((value) => { - return ( - - ); - }); - }, - - handleDatePickerChange(selectedDate) { + }; + + const [state, setState] = useState(getInitialState()); + const [datePickerVisible, setDatePickerVisible] = useState(false); + + const dateFieldRef = useRef(null); + const dayPickerRef = useRef(null); + + useOnClickOutside(dateFieldRef, () => { + if (datePickerVisible) { + setDatePickerVisible(false); + } + }); + + const getDate = () => { + let dateObj = toDate(state.value); + dateObj = setHours(dateObj, state.hour); + return setMinutes(dateObj, state.minute); + }; + + const getFormattedDate = () => formatDateWithoutTime(getDate()); + + const getFormattedDateTime = () => CourseDateUtils.formattedDateTime(getDate(), showTime); + + const onChangeHandler = () => { + const e = { target: { value: formatISO(getDate()) } }; + onChange(e); + }; + + const handleDatePickerChange = (selectedDate) => { const date = toDate(selectedDate); - if (this.isDayDisabled(date)) { + if (isDayDisabled(date)) { return; } - this.refs.datefield.focus(); - this.setState({ + dateFieldRef.current.focus(); + setState(prevState => ({ + ...prevState, value: formatDateWithoutTime(date), - datePickerVisible: false - }, this.onChangeHandler); - }, - - /** - * Update value of date input field. - * Does not issue callbacks to parent component. - * @param {Event} e - input change event - * @return {null} - */ - handleDateFieldChange(e) { + })); + setDatePickerVisible(false); + onChangeHandler(); + }; + + const handleDateFieldChange = (e) => { const { value } = e.target; - if (value !== this.state.value) { - this.setState({ value }); + if (value !== state.value) { + setState(prevState => ({ ...prevState, value })); } - }, - - /** - * When they blur out of the date input field, - * update the state if valid or revert back to last valid value - * @param {Event} e - blur event - * @return {null} - */ - handleDateFieldBlur(e) { + }; + + const handleDateFieldBlur = (e) => { const { value } = e.target; - if (this.isValidDate(value) && !this.isDayDisabled(parseISO(value))) { - this.setState({ value }, () => { - this.onChangeHandler(); - }); + if (isValidDate(value) && !isDayDisabled(parseISO(value))) { + setState(prevState => ({ ...prevState, value })); + onChangeHandler(); } else { - this.setState({ value: this.getInitialState().value }); + setState(getInitialState()); } - }, + }; - handleHourFieldChange(e) { - if (this.state.value === '') { - this.handleDatePickerChange(new Date()); - } - this.setState({ - hour: e.target.value - }, this.onChangeHandler); - }, - - handleMinuteFieldChange(e) { - if (this.state.value === '') { - this.handleDatePickerChange(new Date()); + const handleHourFieldChange = (e) => { + if (state.value === '') { + handleDatePickerChange(new Date()); } - this.setState({ - minute: e.target.value - }, this.onChangeHandler); - }, - - handleClickOutside() { - if (this.state.datePickerVisible) { - this.setState({ datePickerVisible: false }); + setState(prevState => ({ ...prevState, hour: e.target.value })); + onChangeHandler(); + }; + + const handleMinuteFieldChange = (e) => { + if (state.value === '') { + handleDatePickerChange(new Date()); } - }, + setState(prevState => ({ ...prevState, minute: e.target.value })); + onChangeHandler(); + }; - handleDateFieldClick() { - if (!this.state.datePickerVisible) { - this.setState({ datePickerVisible: true }); + const handleDateFieldClick = () => { + if (!datePickerVisible) { + setDatePickerVisible(true); } - }, + }; - handleDateFieldFocus() { - this.setState({ datePickerVisible: true }); - }, + const handleDateFieldFocus = () => { + setDatePickerVisible(true); + }; - handleDateFieldKeyDown(e) { - // Close picker if tab, enter, or escape + const handleDateFieldKeyDown = (e) => { if (includes([9, 13, 27], e.keyCode)) { - this.setState({ datePickerVisible: false }); + setDatePickerVisible(false); } - }, + }; - isDaySelected(date) { + const isDaySelected = (date) => { const currentDate = formatDateWithoutTime(date); - return currentDate === this.state.value; - }, + return currentDate === state.value; + }; - isDayDisabled(currentDate) { - if (this.props.date_props) { - const minDate = startOfDay(this.props.date_props.minDate); + const isDayDisabled = (currentDate) => { + if (date_props) { + const minDate = startOfDay(date_props.minDate); if (isValid(minDate) && isAfter(minDate, currentDate)) { return true; } - const maxDate = endOfDay(this.props.date_props.maxDate); + const maxDate = endOfDay(date_props.maxDate); if (isValid(maxDate) && isAfter(currentDate, maxDate)) { return true; } } - }, - - /** - * Validates given date string (should be similar to YYYY-MM-DD). - * This is implemented here to be self-contained within DatePicker. - * @param {String} value - date string - * @return {Boolean} valid or not - */ - isValidDate(value) { + return false; + }; + + const isValidDate = (value) => { const validationRegex = /^20\d\d-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])/; return validationRegex.test(value) && isValid(toDate(value)); - }, - - showCurrentDate() { - return this.refs.daypicker.showMonth(this.state.month); - }, - - render() { - const spacer = this.props.spacer || ': '; - let label; - let timeLabel; - let currentMonth; - - if (this.props.label) { - label = this.props.label; - label += spacer; + }; + + const showCurrentDate = () => { + return dayPickerRef.current.showMonth(state.month); + }; + + const getTimeDropdownOptions = (type) => { + return range(0, type === 'hour' ? 24 : 60).map((value) => ( + + )); + }; + + if (editable) { + let labelClass = ''; + let inputClass = (inline !== null) && inline ? ' inline' : ''; + + if (props.invalid) { + labelClass += 'red'; + inputClass += 'invalid'; } - if (this.props.timeLabel) { - timeLabel = this.props.timeLabel; - timeLabel += spacer; - } else { - // use unicode for to account for spacing when there is no label - timeLabel = '\u00A0'; - } - - let valueClass = 'text-input-component__value '; - if (this.props.valueClass) { valueClass += this.props.valueClass; } - - if (this.props.editable) { - let labelClass = ''; - let inputClass = (this.props.inline !== null) && this.props.inline ? ' inline' : ''; - - if (this.props.invalid) { - labelClass += 'red'; - inputClass += 'invalid'; + let minDate; + if (date_props && date_props.minDate) { + if (isValid(date_props.minDate)) { + minDate = date_props.minDate; } + } - let minDate; - if (this.props.date_props && this.props.date_props.minDate) { - if (isValid(this.props.date_props.minDate)) { - minDate = this.props.date_props.minDate; - } - } + const date = parseISO(state.value); + const currentMonth = isValid(date) ? date : (minDate || new Date()); - // don't validate YYYY-MM-DD format so we can update the daypicker as they type - const date = parseISO(this.state.value); - if (isValid(date)) { - currentMonth = date; - } else if (minDate) { - currentMonth = minDate; - } else { - currentMonth = new Date(); - } + const modifiers = { + selected: isDaySelected, + disabled: isDayDisabled + }; - const modifiers = { - selected: this.isDaySelected, - disabled: this.isDayDisabled - }; + const dateInput = ( +
- {label} - {(this.props.value !== null || this.props.editable) && !this.props.label ? spacer : null} - - {this.getFormattedDateTime()} - - {this.props.append} -
- ); - } + + ); return ( - {this.getFormattedDateTime()} ++ {label} + {(value !== null || editable) && !label ? spacer : null} + + {getFormattedDateTime()} + + {append} +
); } -}); -export default Conditional(InputHOC(OnClickOutside(DatePicker))); + return ( + {getFormattedDateTime()} + ); +}; + +DatePicker.propTypes = { + id: PropTypes.string, + value: PropTypes.string, + value_key: PropTypes.string, + spacer: PropTypes.string, + label: PropTypes.string, + timeLabel: PropTypes.string, + valueClass: PropTypes.string, + editable: PropTypes.bool, + enabled: PropTypes.bool, + focus: PropTypes.bool, + inline: PropTypes.bool, + isClearable: PropTypes.bool, + placeholder: PropTypes.string, + p_tag_classname: PropTypes.string, + onBlur: PropTypes.func, + onFocus: PropTypes.func, + onChange: PropTypes.func, + onClick: PropTypes.func, + append: PropTypes.string, + date_props: PropTypes.object, + showTime: PropTypes.bool +}; + +export default Conditional(InputHOC(DatePicker));