I have a field that I would like to contain just a date (with no specific time). When I configure the datetime widget with sensible options for a date-only field, dates are stored properly in the saved markdown, but when I load those dates in the UI, I see the date before. This is happening because the DateTime component from the react-datetime library uses local timezones. It loads the date as the start of day UTC and then converts to the local timezone, which is going to be the previous day in any timezone with a negative UTC offset, including all of the Americas. This change adds a pickerUtc option to the datetime widget so that users can specify when they would like the datetime picker to display times in UTC rather than in the local timezone. By setting this new option to true on date-only fields, users can ensure that everyone sees the same date in the picker regardless of local timezones.
164 lines
4.4 KiB
JavaScript
164 lines
4.4 KiB
JavaScript
/** @jsx jsx */
|
|
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { jsx, css } from '@emotion/core';
|
|
import reactDateTimeStyles from 'react-datetime/css/react-datetime.css';
|
|
import DateTime from 'react-datetime';
|
|
import moment from 'moment';
|
|
import { buttons } from 'netlify-cms-ui-default';
|
|
|
|
export default class DateTimeControl extends React.Component {
|
|
static propTypes = {
|
|
field: PropTypes.object.isRequired,
|
|
forID: PropTypes.string,
|
|
onChange: PropTypes.func.isRequired,
|
|
classNameWrapper: PropTypes.string.isRequired,
|
|
setActiveStyle: PropTypes.func.isRequired,
|
|
setInactiveStyle: PropTypes.func.isRequired,
|
|
value: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
|
};
|
|
|
|
getFormats() {
|
|
const { field } = this.props;
|
|
const format = field.get('format');
|
|
|
|
// dateFormat and timeFormat are strictly for modifying
|
|
// input field with the date/time pickers
|
|
const dateFormat = field.get('dateFormat');
|
|
// show time-picker? false hides it, true shows it using default format
|
|
let timeFormat = field.get('timeFormat');
|
|
if (typeof timeFormat === 'undefined') {
|
|
timeFormat = true;
|
|
}
|
|
|
|
return {
|
|
format,
|
|
dateFormat,
|
|
timeFormat,
|
|
};
|
|
}
|
|
|
|
getDefaultValue() {
|
|
const { field } = this.props;
|
|
const defaultValue = field.get('default');
|
|
return defaultValue;
|
|
}
|
|
|
|
getPickerUtc() {
|
|
const { field } = this.props;
|
|
const pickerUtc = field.get('pickerUtc');
|
|
return pickerUtc;
|
|
}
|
|
|
|
formats = this.getFormats();
|
|
defaultValue = this.getDefaultValue();
|
|
pickerUtc = this.getPickerUtc();
|
|
|
|
componentDidMount() {
|
|
const { value } = this.props;
|
|
|
|
/**
|
|
* Set the current date as default value if no value is provided and default is absent. An
|
|
* empty default string means the value is intentionally blank.
|
|
*/
|
|
if (value === undefined) {
|
|
setTimeout(() => {
|
|
this.handleChange(this.defaultValue === undefined ? new Date() : this.defaultValue);
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
// Date is valid if datetime is a moment or Date object otherwise it's a string.
|
|
// Handle the empty case, if the user wants to empty the field.
|
|
isValidDate = datetime =>
|
|
moment.isMoment(datetime) || datetime instanceof Date || datetime === '';
|
|
|
|
handleChange = datetime => {
|
|
/**
|
|
* Set the date only if it is valid.
|
|
*/
|
|
if (!this.isValidDate(datetime)) {
|
|
return;
|
|
}
|
|
|
|
const { onChange } = this.props;
|
|
const { format } = this.formats;
|
|
|
|
/**
|
|
* Produce a formatted string only if a format is set in the config.
|
|
* Otherwise produce a date object.
|
|
*/
|
|
if (format) {
|
|
const formattedValue = datetime ? moment(datetime).format(format) : '';
|
|
onChange(formattedValue);
|
|
} else {
|
|
const value = moment.isMoment(datetime) ? datetime.toDate() : datetime;
|
|
onChange(value);
|
|
}
|
|
};
|
|
|
|
onBlur = datetime => {
|
|
const { setInactiveStyle } = this.props;
|
|
|
|
if (!this.isValidDate(datetime)) {
|
|
const parsedDate = moment(datetime);
|
|
|
|
if (parsedDate.isValid()) {
|
|
this.handleChange(datetime);
|
|
} else {
|
|
window.alert('The date you entered is invalid.');
|
|
}
|
|
}
|
|
|
|
setInactiveStyle();
|
|
};
|
|
|
|
render() {
|
|
const { forID, value, classNameWrapper, setActiveStyle, t } = this.props;
|
|
const { format, dateFormat, timeFormat } = this.formats;
|
|
|
|
return (
|
|
<div
|
|
css={css`
|
|
${reactDateTimeStyles};
|
|
position: relative;
|
|
`}
|
|
>
|
|
<DateTime
|
|
dateFormat={dateFormat}
|
|
timeFormat={timeFormat}
|
|
value={moment(value, format)}
|
|
onChange={this.handleChange}
|
|
onFocus={setActiveStyle}
|
|
onBlur={this.onBlur}
|
|
inputProps={{ className: classNameWrapper, id: forID }}
|
|
utc={this.pickerUtc}
|
|
/>
|
|
<div
|
|
css={css`
|
|
position: absolute;
|
|
right: 20px;
|
|
transform: translateY(-40px);
|
|
width: fit-content;
|
|
z-index: 1;
|
|
`}
|
|
>
|
|
<button
|
|
css={css`
|
|
${buttons.button}
|
|
${buttons.default}
|
|
${buttons.lightBlue}
|
|
${buttons.small}
|
|
`}
|
|
onClick={() => {
|
|
this.handleChange(moment());
|
|
}}
|
|
>
|
|
{t('editor.editorWidgets.datetime.now')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|