UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

293 lines (291 loc) 11.8 kB
/* * 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;