@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
293 lines (291 loc) • 11.8 kB
JavaScript
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useMemo, useState } from 'react';
import { makeStyles } from 'tss-react/mui';
import { defineMessages, useIntl } from 'react-intl';
import moment from 'moment-timezone';
import PublicRoundedIcon from '@mui/icons-material/PublicRounded';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import { AdapterMoment as DateAdapter } from '@mui/x-date-pickers/AdapterMoment';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import { asLocalizedDateTime, create8601String, get8601Pieces, getTimezones } from '../../utils/datetime';
import FormControl from '@mui/material/FormControl';
import { nnou } from '../../utils/object';
import { useSpreadState } from '../../hooks/useSpreadState';
import { UNDEFINED } from '../../utils/constants';
const translations = defineMessages({
datePlaceholder: {
id: 'datetimepicker.datePlaceholder',
defaultMessage: 'Date'
},
timePlaceholder: {
id: 'datetimepicker.timePlaceholder',
defaultMessage: 'Time'
},
dateInvalidMessage: {
id: 'datetimepicker.dateInvalidMessage',
defaultMessage: 'Invalid Date.'
},
timeInvalidMessage: {
id: 'datetimepicker.timeInvalidMessage',
defaultMessage: 'Invalid Time.'
}
});
const useStyles = makeStyles()(() => ({
popupIndicator: {
padding: ' 8px',
marginTop: '-6px',
marginRight: '-7px'
}
}));
function DateTimePicker(props) {
var _a;
const {
id,
onChange,
onDateChange,
onTimeChange,
onTimeZoneChange,
onError,
value,
timeZone,
disablePast,
disabled = false,
controls = ['date', 'time', 'timeZone'],
localeCode = 'en-US',
dateTimeFormatOptions
} = props;
// Time picker control seems to always display in function of the time of the
// browser's time zone but we display things in function of the selected time zone.
// This causes some discrepancies between the time displayed on the field and the time
// displayed when the time picker is opened. `internalDate` is a transposed date/time
// used to sync the timepicker with what's displayed to the user.
const internalDate = useMemo(() => {
const date = value ? new Date(value) : new Date();
const localOffset = moment().format().substr(-6);
const dateWithoutOffset = moment(date).tz(timeZone).format().substr(0, 19);
return new Date(`${dateWithoutOffset}${localOffset}`);
}, [value, timeZone]);
const timeZones = getTimezones();
const { classes } = useStyles();
const hour12 =
(_a =
dateTimeFormatOptions === null || dateTimeFormatOptions === void 0 ? void 0 : dateTimeFormatOptions.hour12) !==
null && _a !== void 0
? _a
: true;
const currentTimeZoneDesc = timeZones.find((tz) => tz.name === unescape(timeZone));
const [datePickerOpen, setDatePickerOpen] = useState(false);
const [timePickerOpen, setTimePickerOpen] = useState(false);
const [pickerState, setPickerState] = useSpreadState({
dateValid: true,
timeValid: true,
timezoneValid: true
});
const { formatMessage } = useIntl();
const createOnDateChange =
(name) =>
// Date/time change handler
(newMoment) => {
let newDate = newMoment.toDate();
if (newDate === null) {
setPickerState({ dateValid: false });
onError === null || onError === void 0 ? void 0 : onError();
}
let changes;
const internalDatePieces = get8601Pieces(internalDate);
const pickerDatePieces = get8601Pieces(newDate);
switch (name) {
case 'date': {
// Grab the picker-sent date, keep the time we had
const dateString = create8601String(pickerDatePieces[0], internalDatePieces[1], currentTimeZoneDesc.offset);
changes = { dateString, date: new Date(dateString), timeZoneName: timeZone };
onDateChange === null || onDateChange === void 0 ? void 0 : onDateChange(changes);
break;
}
case 'time': {
// Grab the picker-sent time, keep the date we had
const dateString = create8601String(internalDatePieces[0], pickerDatePieces[1], currentTimeZoneDesc.offset);
changes = { dateString, date: new Date(dateString), timeZoneName: timeZone };
onTimeChange === null || onTimeChange === void 0 ? void 0 : onTimeChange(changes);
break;
}
}
setPickerState({ dateValid: true });
onChange === null || onChange === void 0 ? void 0 : onChange(changes);
};
// The idea is that when time zone is changed, it doesn't convert the
// date to the new timezone but that you can select, date, time and timezone
// individually without one changing the other fields.
const handleTimezoneChange = (event, value) => {
const pieces = get8601Pieces(internalDate);
// Keep date/time we had and apply new offset
const dateString = create8601String(pieces[0], pieces[1], value.offset);
const changes = { date: new Date(dateString), dateString, timeZoneName: value.name };
onTimeZoneChange === null || onTimeZoneChange === void 0 ? void 0 : onTimeZoneChange(value);
onTimeChange === null || onTimeChange === void 0 ? void 0 : onTimeChange(changes);
onDateChange === null || onDateChange === void 0 ? void 0 : onDateChange(changes);
onChange === null || onChange === void 0 ? void 0 : onChange(changes);
};
const handlePopupOnlyInputChange = (event) => {
event.preventDefault();
};
const formControlProps = {};
if (nnou(id)) {
formControlProps['id'] = id;
}
return React.createElement(
FormControl,
Object.assign({}, formControlProps, { fullWidth: true }),
React.createElement(
LocalizationProvider,
{ dateAdapter: DateAdapter },
controls.includes('date') &&
React.createElement(
React.Fragment,
null,
React.createElement(DatePicker, {
open: datePickerOpen,
views: ['year', 'month', 'day'],
renderInput: (props) =>
React.createElement(
TextField,
Object.assign(
{
size: 'small',
margin: 'normal',
placeholder: formatMessage(translations.datePlaceholder),
error: !pickerState.dateValid,
helperText: pickerState.dateValid ? '' : formatMessage(translations.dateInvalidMessage),
onClick: disabled
? null
: () => {
setDatePickerOpen(true);
}
},
props,
{
inputProps: Object.assign(Object.assign({}, props.inputProps), {
value: asLocalizedDateTime(internalDate, localeCode),
onChange: handlePopupOnlyInputChange
})
}
)
),
value: internalDate,
onChange: createOnDateChange('date'),
disabled: disabled,
disablePast: disablePast,
onAccept: () => {
setDatePickerOpen(false);
},
// Both clicking cancel and outside the calendar trigger onClose
onClose: () => {
setDatePickerOpen(false);
}
})
),
controls.includes('time') &&
React.createElement(TimePicker, {
open: timePickerOpen,
value: internalDate,
onChange: createOnDateChange('time'),
disabled: disabled,
ampm: hour12,
onOpen: () => setTimePickerOpen(true),
onAccept: () => setTimePickerOpen(false),
onClose: () => setTimePickerOpen(false),
renderInput: (props) =>
React.createElement(
TextField,
Object.assign(
{
size: 'small',
margin: 'normal',
helperText: pickerState.timeValid ? '' : formatMessage(translations.timeInvalidMessage),
placeholder: formatMessage(translations.timePlaceholder),
onClick: disabled
? null
: () => {
setTimePickerOpen(true);
}
},
props,
{
inputProps: Object.assign(Object.assign({}, props.inputProps), {
onChange: handlePopupOnlyInputChange,
value: asLocalizedDateTime(internalDate, localeCode, {
hour12,
hour:
(dateTimeFormatOptions === null || dateTimeFormatOptions === void 0
? void 0
: dateTimeFormatOptions.hour) || '2-digit',
minute:
(dateTimeFormatOptions === null || dateTimeFormatOptions === void 0
? void 0
: dateTimeFormatOptions.minute) || '2-digit',
// If the timezone control isn't displayed, the time displayed may
// be misleading/unexpected to the user, so if timezone isn't displayed,
// display timezone here.
timeZoneName: controls.includes('timeZone') ? UNDEFINED : 'short'
})
})
}
)
)
})
),
controls.includes('timeZone') &&
React.createElement(Autocomplete, {
options: timeZones,
getOptionLabel: (timezone) =>
typeof timezone === 'string' ? timezone : `${timezone.name} (GMT${timezone.offset})`,
value: currentTimeZoneDesc,
onChange: handleTimezoneChange,
size: 'small',
classes: {
popupIndicator: classes.popupIndicator
},
popupIcon: React.createElement(PublicRoundedIcon, null),
disableClearable: true,
renderInput: (params) =>
React.createElement(
TextField,
Object.assign({ size: 'small', margin: 'normal' }, params, { variant: 'outlined', fullWidth: true })
),
disabled: disabled
})
);
}
export default DateTimePicker;