2019-01-09 15:05:29 +11:00
|
|
|
/** @jsx jsx */
|
2021-05-07 01:04:48 -03:00
|
|
|
import { Component } from 'react';
|
2021-01-29 20:05:07 -05:00
|
|
|
import { jsx } from '@emotion/react';
|
|
|
|
|
import { CSSObject } from '@emotion/serialize';
|
2021-01-27 10:19:14 -05:00
|
|
|
import moment, { Moment } from 'moment';
|
2021-01-29 20:27:04 -05:00
|
|
|
import * as chrono from 'chrono-node';
|
2018-01-30 16:30:27 +11:00
|
|
|
|
2021-01-27 10:19:14 -05:00
|
|
|
import Select, {
|
|
|
|
|
GroupProps,
|
|
|
|
|
OptionProps,
|
|
|
|
|
components as SelectComponents,
|
|
|
|
|
} from 'react-select';
|
2018-01-30 16:30:27 +11:00
|
|
|
|
2021-01-27 10:19:14 -05:00
|
|
|
interface DateOption {
|
|
|
|
|
date: Moment;
|
|
|
|
|
value: Date;
|
|
|
|
|
label: string;
|
|
|
|
|
display?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const createOptionForDate = (d: Moment | Date) => {
|
2018-01-30 23:08:39 +11:00
|
|
|
const date = moment.isMoment(d) ? d : moment(d);
|
2018-01-30 16:30:27 +11:00
|
|
|
return {
|
2018-01-30 23:08:39 +11:00
|
|
|
date,
|
|
|
|
|
value: date.toDate(),
|
|
|
|
|
label: date.calendar(null, {
|
|
|
|
|
sameDay: '[Today] (Do MMM YYYY)',
|
|
|
|
|
nextDay: '[Tomorrow] (Do MMM YYYY)',
|
|
|
|
|
nextWeek: '[Next] dddd (Do MMM YYYY)',
|
|
|
|
|
lastDay: '[Yesterday] (Do MMM YYYY)',
|
|
|
|
|
lastWeek: '[Last] dddd (Do MMM YYYY)',
|
2018-01-30 16:30:27 +11:00
|
|
|
sameElse: 'Do MMMM YYYY',
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2021-01-27 10:19:14 -05:00
|
|
|
interface CalendarGroup {
|
|
|
|
|
label: string;
|
|
|
|
|
options: readonly DateOption[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultOptions: (DateOption | CalendarGroup)[] = [
|
|
|
|
|
'today',
|
|
|
|
|
'tomorrow',
|
|
|
|
|
'yesterday',
|
2021-04-13 21:25:00 -04:00
|
|
|
].map((i) => createOptionForDate(chrono.parseDate(i)));
|
2018-01-30 23:08:39 +11:00
|
|
|
|
|
|
|
|
const createCalendarOptions = (date = new Date()) => {
|
2021-01-27 10:19:14 -05:00
|
|
|
const daysInMonth = Array.apply(null, Array(moment(date).daysInMonth())).map(
|
|
|
|
|
(x, i) => {
|
|
|
|
|
const d = moment(date).date(i + 1);
|
|
|
|
|
return { ...createOptionForDate(d), display: 'calendar' };
|
|
|
|
|
}
|
|
|
|
|
);
|
2018-01-30 23:08:39 +11:00
|
|
|
return {
|
|
|
|
|
label: moment(date).format('MMMM YYYY'),
|
|
|
|
|
options: daysInMonth,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
defaultOptions.push(createCalendarOptions());
|
|
|
|
|
|
2018-01-31 11:26:37 +11:00
|
|
|
const suggestions = [
|
|
|
|
|
'sunday',
|
|
|
|
|
'saturday',
|
|
|
|
|
'friday',
|
|
|
|
|
'thursday',
|
|
|
|
|
'wednesday',
|
|
|
|
|
'tuesday',
|
|
|
|
|
'monday',
|
|
|
|
|
'december',
|
|
|
|
|
'november',
|
|
|
|
|
'october',
|
|
|
|
|
'september',
|
|
|
|
|
'august',
|
|
|
|
|
'july',
|
|
|
|
|
'june',
|
|
|
|
|
'may',
|
|
|
|
|
'april',
|
|
|
|
|
'march',
|
|
|
|
|
'february',
|
|
|
|
|
'january',
|
|
|
|
|
'yesterday',
|
|
|
|
|
'tomorrow',
|
|
|
|
|
'today',
|
2021-01-27 10:19:14 -05:00
|
|
|
].reduce<{ [key: string]: string }>((acc, str) => {
|
2018-01-31 11:26:37 +11:00
|
|
|
for (let i = 1; i < str.length; i++) {
|
|
|
|
|
acc[str.substr(0, i)] = str;
|
|
|
|
|
}
|
|
|
|
|
return acc;
|
|
|
|
|
}, {});
|
|
|
|
|
|
2021-01-27 10:19:14 -05:00
|
|
|
const suggest = (str: string) =>
|
2018-01-31 11:26:37 +11:00
|
|
|
str
|
|
|
|
|
.split(/\b/)
|
2021-04-13 21:25:00 -04:00
|
|
|
.map((i) => suggestions[i] || i)
|
2018-01-31 11:26:37 +11:00
|
|
|
.join('');
|
|
|
|
|
|
2018-01-30 23:08:39 +11:00
|
|
|
const days = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
|
|
|
|
|
2021-01-29 20:05:07 -05:00
|
|
|
const daysHeaderStyles: CSSObject = {
|
2018-01-30 23:08:39 +11:00
|
|
|
marginTop: '5px',
|
|
|
|
|
paddingTop: '5px',
|
|
|
|
|
paddingLeft: '2%',
|
|
|
|
|
borderTop: '1px solid #eee',
|
|
|
|
|
};
|
2021-01-29 20:05:07 -05:00
|
|
|
const daysHeaderItemStyles: CSSObject = {
|
2018-01-30 23:08:39 +11:00
|
|
|
color: '#999',
|
|
|
|
|
cursor: 'default',
|
|
|
|
|
fontSize: '75%',
|
2021-01-29 20:05:07 -05:00
|
|
|
fontWeight: 500,
|
2018-01-30 23:08:39 +11:00
|
|
|
display: 'inline-block',
|
|
|
|
|
width: '12%',
|
|
|
|
|
margin: '0 1%',
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
};
|
2021-01-29 20:05:07 -05:00
|
|
|
const daysContainerStyles: CSSObject = {
|
2018-01-30 23:08:39 +11:00
|
|
|
paddingTop: '5px',
|
|
|
|
|
paddingLeft: '2%',
|
|
|
|
|
};
|
|
|
|
|
|
2021-01-27 10:19:14 -05:00
|
|
|
const Group = (props: GroupProps<DateOption, false>) => {
|
2019-04-29 17:38:33 +10:00
|
|
|
const {
|
|
|
|
|
Heading,
|
|
|
|
|
getStyles,
|
|
|
|
|
children,
|
|
|
|
|
label,
|
|
|
|
|
headingProps,
|
|
|
|
|
cx,
|
|
|
|
|
theme,
|
2021-03-25 09:37:30 -04:00
|
|
|
selectProps,
|
2019-04-29 17:38:33 +10:00
|
|
|
} = props;
|
2018-01-30 23:08:39 +11:00
|
|
|
return (
|
2021-01-29 20:05:07 -05:00
|
|
|
<div aria-label={label as string} css={getStyles('group', props)}>
|
2021-03-25 09:37:30 -04:00
|
|
|
<Heading
|
|
|
|
|
selectProps={selectProps}
|
|
|
|
|
theme={theme}
|
|
|
|
|
getStyles={getStyles}
|
|
|
|
|
cx={cx}
|
|
|
|
|
{...headingProps}
|
|
|
|
|
>
|
2018-04-26 09:57:26 +10:00
|
|
|
{label}
|
|
|
|
|
</Heading>
|
2018-07-23 14:41:15 +10:00
|
|
|
<div css={daysHeaderStyles}>
|
2018-01-30 23:08:39 +11:00
|
|
|
{days.map((day, i) => (
|
2018-07-23 14:41:15 +10:00
|
|
|
<span key={`${i}-${day}`} css={daysHeaderItemStyles}>
|
2018-01-30 23:08:39 +11:00
|
|
|
{day}
|
2018-07-23 14:41:15 +10:00
|
|
|
</span>
|
2018-01-30 23:08:39 +11:00
|
|
|
))}
|
2018-07-23 14:41:15 +10:00
|
|
|
</div>
|
|
|
|
|
<div css={daysContainerStyles}>{children}</div>
|
|
|
|
|
</div>
|
2018-01-30 23:08:39 +11:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2021-03-29 17:40:03 -04:00
|
|
|
const getOptionStyles = (defaultStyles: CSSObject): CSSObject => ({
|
2018-01-30 23:08:39 +11:00
|
|
|
...defaultStyles,
|
|
|
|
|
display: 'inline-block',
|
|
|
|
|
width: '12%',
|
|
|
|
|
margin: '0 1%',
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
});
|
|
|
|
|
|
2021-01-27 10:19:14 -05:00
|
|
|
const Option = (props: OptionProps<DateOption, false>) => {
|
2018-02-01 17:07:48 +11:00
|
|
|
const { data, getStyles, innerRef, innerProps } = props;
|
2018-01-30 23:08:39 +11:00
|
|
|
if (data.display === 'calendar') {
|
|
|
|
|
const defaultStyles = getStyles('option', props);
|
|
|
|
|
const styles = getOptionStyles(defaultStyles);
|
|
|
|
|
if (data.date.date() === 1) {
|
|
|
|
|
const indentBy = data.date.day();
|
|
|
|
|
if (indentBy) {
|
|
|
|
|
styles.marginLeft = `${indentBy * 14 + 1}%`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return (
|
2018-07-23 14:41:15 +10:00
|
|
|
<span {...innerProps} css={styles} ref={innerRef}>
|
2018-01-30 23:08:39 +11:00
|
|
|
{data.date.format('D')}
|
2018-07-23 14:41:15 +10:00
|
|
|
</span>
|
2018-01-30 23:08:39 +11:00
|
|
|
);
|
|
|
|
|
} else return <SelectComponents.Option {...props} />;
|
|
|
|
|
};
|
2018-01-30 16:30:27 +11:00
|
|
|
|
2021-01-27 10:19:14 -05:00
|
|
|
interface DatePickerProps {
|
|
|
|
|
readonly value: DateOption | null;
|
|
|
|
|
readonly onChange: (value: DateOption | null) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DatePickerState {
|
2021-01-29 20:05:07 -05:00
|
|
|
readonly options: readonly (DateOption | CalendarGroup)[];
|
2021-01-27 10:19:14 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class DatePicker extends Component<DatePickerProps, DatePickerState> {
|
|
|
|
|
state: DatePickerState = {
|
2018-01-30 16:30:27 +11:00
|
|
|
options: defaultOptions,
|
|
|
|
|
};
|
2021-01-27 10:19:14 -05:00
|
|
|
handleInputChange = (value: string) => {
|
2018-01-30 16:30:27 +11:00
|
|
|
if (!value) {
|
|
|
|
|
this.setState({ options: defaultOptions });
|
|
|
|
|
return;
|
|
|
|
|
}
|
2018-01-31 11:26:37 +11:00
|
|
|
const date = chrono.parseDate(suggest(value.toLowerCase()));
|
2018-01-30 16:30:27 +11:00
|
|
|
if (date) {
|
|
|
|
|
this.setState({
|
2018-01-30 23:08:39 +11:00
|
|
|
options: [createOptionForDate(date), createCalendarOptions(date)],
|
2018-01-30 16:30:27 +11:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
this.setState({
|
|
|
|
|
options: [],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
render() {
|
|
|
|
|
const { value } = this.props;
|
|
|
|
|
const { options } = this.state;
|
|
|
|
|
return (
|
2021-01-27 10:19:14 -05:00
|
|
|
<Select<DateOption, false>
|
2018-01-30 16:30:27 +11:00
|
|
|
{...this.props}
|
2018-01-30 23:08:39 +11:00
|
|
|
components={{ Group, Option }}
|
2018-01-30 16:30:27 +11:00
|
|
|
filterOption={null}
|
2018-01-30 23:08:39 +11:00
|
|
|
isMulti={false}
|
2021-04-13 21:25:00 -04:00
|
|
|
isOptionSelected={(o, v) => v.some((i) => i.date.isSame(o.date, 'day'))}
|
2018-01-30 23:08:39 +11:00
|
|
|
maxMenuHeight={380}
|
2018-01-30 16:30:27 +11:00
|
|
|
onChange={this.props.onChange}
|
|
|
|
|
onInputChange={this.handleInputChange}
|
|
|
|
|
options={options}
|
|
|
|
|
value={value}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-27 10:19:14 -05:00
|
|
|
interface State {
|
|
|
|
|
readonly value: DateOption | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default class Experimental extends Component<{}, State> {
|
|
|
|
|
state: State = {
|
2021-01-29 20:05:07 -05:00
|
|
|
value: defaultOptions[0] as DateOption,
|
2018-01-30 16:30:27 +11:00
|
|
|
};
|
2021-01-27 10:19:14 -05:00
|
|
|
handleChange = (value: DateOption | null) => {
|
2018-01-30 16:30:27 +11:00
|
|
|
this.setState({ value });
|
|
|
|
|
};
|
|
|
|
|
render() {
|
|
|
|
|
const { value } = this.state;
|
2021-01-27 10:19:14 -05:00
|
|
|
const displayValue =
|
|
|
|
|
value && 'value' in value ? value.value.toString() : 'null';
|
2018-01-30 16:30:27 +11:00
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<pre>Value: {displayValue}</pre>
|
2018-03-02 13:26:44 +11:00
|
|
|
<DatePicker value={value} onChange={this.handleChange} />
|
2018-01-30 16:30:27 +11:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|