import React from 'react';
import { defineMessages, InjectedIntlProps, injectIntl } from 'react-intl';
import { DispatchAnalyticsEvent } from '../../../analytics';
import styled from 'styled-components';
import { DateType } from '../../types';
import {
findDateSegmentByPosition,
adjustDate,
isDatePossiblyValid,
} from '../../utils/internal';
import { parseDateType, formatDateType } from '../../utils/formatParse';
import {
EVENT_TYPE,
ACTION,
ACTION_SUBJECT,
ACTION_SUBJECT_ID,
} from '../../../analytics/types/enums';
import TextField from '@atlaskit/textfield';
import { ErrorMessage } from '@atlaskit/form';
import { FormEvent } from 'react';
const DateTextFieldWrapper = styled.div`
padding: 22px;
padding-bottom: 12px;
`;
export interface InputProps {
/** Locale code string (eg. "en-AU") */
locale: string;
date: DateType;
dispatchAnalyticsEvent?: DispatchAnalyticsEvent;
onNewDate: (date: DateType) => void;
onSubmitDate: (date: DateType | null) => void;
onEmptySubmit: () => void;
/** Automatically focus the text field */
autoFocus?: boolean;
/** Automatically select all text in the field. Requires autoFocus to be true. */
autoSelectAll?: boolean;
}
export interface InputState {
inputText: string;
}
const messages = defineMessages({
invalidDateError: {
id: 'fabric.editor.invalidDateError',
defaultMessage: 'Enter a valid date',
description:
'Error message when the date typed in is invalid, requesting they inputs a new date',
},
});
class DatePickerInput extends React.Component<
InputProps & InjectedIntlProps,
InputState
> {
private inputRef: any;
private setInputSelectionPos: number | undefined;
private autofocusTimeout: any | undefined;
private autoSelectAllTimeout: any | undefined;
constructor(props: InputProps & InjectedIntlProps) {
super(props);
const { date } = props;
this.setInputSelectionPos = undefined;
const inputText = formatDateType(date, this.props.locale);
this.state = {
inputText,
};
}
render() {
const {
locale,
intl: { formatMessage },
} = this.props;
const { inputText } = this.state;
const possiblyValid = isDatePossiblyValid(inputText);
const attemptedDateParse = parseDateType(inputText, locale);
// Don't display an error for an empty input.
const displayError: boolean =
(attemptedDateParse === null || !possiblyValid) && inputText !== '';
return (
{displayError && (
{formatMessage(messages.invalidDateError)}
)}
);
}
componentDidUpdate() {
const setInputSelectionPos = this.setInputSelectionPos;
if (setInputSelectionPos !== undefined) {
this.inputRef.setSelectionRange(
setInputSelectionPos,
setInputSelectionPos,
);
this.setInputSelectionPos = undefined;
}
if (this.inputRef && this.props.autoFocus) {
// TODO: Check if input already has focus
this.focusInput();
}
// Don't select all text here - would seleect text on each keystroke
}
/**
* Focus the input textfield
*/
private focusInput = (): void => {
if (!this.inputRef) {
return;
}
// Defer to prevent editor scrolling to top (See FS-3227, also ED-2992)
this.autofocusTimeout = setTimeout(() => {
this.inputRef.focus();
});
};
/**
* Select all the input text
*/
private selectInput = (): void => {
if (!this.inputRef) {
return;
}
// Defer to prevent editor scrolling to top (See FS-3227, also ED-2992)
this.autoSelectAllTimeout = setTimeout(() => {
this.inputRef.select();
});
};
private handleInputRef = (ref?: HTMLInputElement) => {
const { autoFocus, autoSelectAll } = this.props;
if (ref) {
this.inputRef = ref;
}
if (ref && autoFocus) {
this.focusInput();
}
if (autoSelectAll) {
this.selectInput();
}
};
componentWillUnmount() {
if (this.autofocusTimeout !== undefined) {
clearTimeout(this.autofocusTimeout);
}
if (this.autoSelectAllTimeout !== undefined) {
clearTimeout(this.autoSelectAllTimeout);
}
}
private handleChange = (evt: FormEvent) => {
const textFieldValue: string = (evt.target as HTMLInputElement).value;
const { locale, dispatchAnalyticsEvent } = this.props;
const newDate = parseDateType(textFieldValue, locale);
if (newDate !== undefined && newDate !== null) {
this.setState({
inputText: textFieldValue,
});
this.props.onNewDate(newDate);
if (dispatchAnalyticsEvent) {
dispatchAnalyticsEvent({
eventType: EVENT_TYPE.TRACK,
action: ACTION.TYPING_FINISHED,
actionSubject: ACTION_SUBJECT.DATE,
});
}
} else {
// if invalid, just update state text (to rerender textfield)
this.setState({
inputText: textFieldValue,
});
}
};
private handleKeyPress = (event: React.KeyboardEvent) => {
const { locale, dispatchAnalyticsEvent } = this.props;
const textFieldValue: string = (event.target as HTMLInputElement).value;
// Fire event on every keypress (textfield not necessarily empty)
if (
dispatchAnalyticsEvent &&
event.key !== 'Enter' &&
event.key !== 'Backspace'
) {
dispatchAnalyticsEvent({
eventType: EVENT_TYPE.TRACK,
action: ACTION.TYPING_STARTED,
actionSubject: ACTION_SUBJECT.DATE,
});
}
if (event.key !== 'Enter') {
return;
}
if (textFieldValue === '') {
this.props.onEmptySubmit();
return;
}
const newDate = parseDateType(textFieldValue, locale);
this.props.onSubmitDate(newDate);
};
// arrow keys are only triggered by onKeyDown, not onKeyPress
private handleKeyDown = (event: React.KeyboardEvent) => {
const dateString: string = (event.target as HTMLInputElement).value;
const { locale } = this.props;
let adjustment: number | undefined;
if (event.key === 'ArrowUp') {
adjustment = 1;
} else if (event.key === 'ArrowDown') {
adjustment = -1;
}
if (adjustment === undefined) {
return;
}
const { dispatchAnalyticsEvent } = this.props;
const cursorPos = this.inputRef.selectionStart;
const activeSegment = findDateSegmentByPosition(
cursorPos,
dateString,
locale,
);
if (activeSegment === undefined) {
return;
}
let dateSegment:
| ACTION_SUBJECT_ID.DATE_DAY
| ACTION_SUBJECT_ID.DATE_MONTH
| ACTION_SUBJECT_ID.DATE_YEAR;
switch (activeSegment) {
case 'day':
dateSegment = ACTION_SUBJECT_ID.DATE_DAY;
break;
case 'month':
dateSegment = ACTION_SUBJECT_ID.DATE_MONTH;
break;
default:
dateSegment = ACTION_SUBJECT_ID.DATE_YEAR;
}
if (dispatchAnalyticsEvent) {
dispatchAnalyticsEvent({
eventType: EVENT_TYPE.TRACK,
action: adjustment > 0 ? ACTION.INCREMENTED : ACTION.DECREMENTED,
actionSubject: ACTION_SUBJECT.DATE_SEGMENT,
attributes: { dateSegment },
});
}
const oldDateType = parseDateType(dateString, locale);
if (oldDateType === undefined || oldDateType === null) {
return;
}
const newDateType = adjustDate(oldDateType, activeSegment, adjustment);
this.setState({
inputText: formatDateType(newDateType, locale),
});
this.props.onNewDate(newDateType);
this.setInputSelectionPos = Math.min(cursorPos, dateString.length);
event.preventDefault();
};
}
export default injectIntl(DatePickerInput);