You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DateInput.tsx 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2018 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import * as React from 'react';
  21. import * as classNames from 'classnames';
  22. import { DayModifiers, Modifier, Modifiers } from 'react-day-picker';
  23. import { InjectedIntlProps, injectIntl } from 'react-intl';
  24. import { range } from 'lodash';
  25. import * as addMonths from 'date-fns/add_months';
  26. import * as setMonth from 'date-fns/set_month';
  27. import * as setYear from 'date-fns/set_year';
  28. import * as subMonths from 'date-fns/sub_months';
  29. import OutsideClickHandler from './OutsideClickHandler';
  30. import Select from './Select';
  31. import { lazyLoad } from '../lazyLoad';
  32. import * as theme from '../../app/theme';
  33. import CalendarIcon from '../icons-components/CalendarIcon';
  34. import ChevronLeftIcon from '../icons-components/ChevronLeftIcon';
  35. import ChevronRightIcon from '../icons-components/ChevronRightcon';
  36. import ClearIcon from '../icons-components/ClearIcon';
  37. import { ButtonIcon } from '../ui/buttons';
  38. import { getShortMonthName, getWeekDayName, getShortWeekDayName } from '../../helpers/l10n';
  39. import './DayPicker.css';
  40. import './styles.css';
  41. const DayPicker = lazyLoad(() => import('react-day-picker'));
  42. interface Props {
  43. className?: string;
  44. currentMonth?: Date;
  45. highlightFrom?: Date;
  46. highlightTo?: Date;
  47. inputClassName?: string;
  48. maxDate?: Date;
  49. minDate?: Date;
  50. name?: string;
  51. onChange: (date: Date | undefined) => void;
  52. placeholder: string;
  53. value?: Date;
  54. }
  55. interface State {
  56. currentMonth: Date;
  57. open: boolean;
  58. lastHovered?: Date;
  59. }
  60. type Week = [string, string, string, string, string, string, string];
  61. export default class DateInput extends React.PureComponent<Props, State> {
  62. input?: HTMLInputElement | null;
  63. constructor(props: Props) {
  64. super(props);
  65. this.state = { currentMonth: props.value || props.currentMonth || new Date(), open: false };
  66. }
  67. focus = () => {
  68. if (this.input) {
  69. this.input.focus();
  70. }
  71. this.openCalendar();
  72. };
  73. handleResetClick = () => {
  74. this.closeCalendar();
  75. this.props.onChange(undefined);
  76. };
  77. openCalendar = () => {
  78. this.setState({
  79. currentMonth: this.props.value || this.props.currentMonth || new Date(),
  80. lastHovered: undefined,
  81. open: true
  82. });
  83. };
  84. closeCalendar = () => {
  85. this.setState({ open: false });
  86. };
  87. handleDayClick = (day: Date, modifiers: DayModifiers) => {
  88. if (!modifiers.disabled) {
  89. this.closeCalendar();
  90. this.props.onChange(day);
  91. }
  92. };
  93. handleDayMouseEnter = (day: Date, modifiers: DayModifiers) => {
  94. this.setState({ lastHovered: modifiers.disabled ? undefined : day });
  95. };
  96. handleCurrentMonthChange = ({ value }: { value: number }) => {
  97. this.setState((state: State) => ({ currentMonth: setMonth(state.currentMonth, value) }));
  98. };
  99. handleCurrentYearChange = ({ value }: { value: number }) => {
  100. this.setState(state => ({ currentMonth: setYear(state.currentMonth, value) }));
  101. };
  102. handlePreviousMonthClick = () => {
  103. this.setState(state => ({ currentMonth: subMonths(state.currentMonth, 1) }));
  104. };
  105. handleNextMonthClick = () => {
  106. this.setState(state => ({ currentMonth: addMonths(state.currentMonth, 1) }));
  107. };
  108. render() {
  109. const { highlightFrom, highlightTo, minDate, value } = this.props;
  110. const { lastHovered } = this.state;
  111. const after = this.props.maxDate || new Date();
  112. const months = range(12);
  113. const years = range(new Date().getFullYear() - 10, new Date().getFullYear() + 1);
  114. const selectedDays: Modifier[] = value ? [value] : [];
  115. let modifiers: Partial<Modifiers> | undefined;
  116. const lastHoveredOrValue = lastHovered || value;
  117. if (highlightFrom && lastHoveredOrValue) {
  118. modifiers = { highlighted: { from: highlightFrom, to: lastHoveredOrValue } };
  119. selectedDays.push(highlightFrom);
  120. }
  121. if (highlightTo && lastHoveredOrValue) {
  122. modifiers = { highlighted: { from: lastHoveredOrValue, to: highlightTo } };
  123. selectedDays.push(highlightTo);
  124. }
  125. const weekdaysLong = range(7).map(getWeekDayName) as Week;
  126. const weekdaysShort = range(7).map(getShortWeekDayName) as Week;
  127. return (
  128. <OutsideClickHandler onClickOutside={this.closeCalendar}>
  129. <span className={classNames('date-input-control', this.props.className)}>
  130. <InputWrapper
  131. className={classNames('date-input-control-input', this.props.inputClassName, {
  132. 'is-filled': this.props.value !== undefined
  133. })}
  134. innerRef={node => (this.input = node)}
  135. name={this.props.name}
  136. onFocus={this.openCalendar}
  137. placeholder={this.props.placeholder}
  138. readOnly={true}
  139. type="text"
  140. value={value}
  141. />
  142. <CalendarIcon className="date-input-control-icon" fill="" />
  143. {this.props.value !== undefined && (
  144. <ButtonIcon
  145. className="button-tiny date-input-control-reset"
  146. color={theme.gray60}
  147. onClick={this.handleResetClick}>
  148. <ClearIcon size={12} />
  149. </ButtonIcon>
  150. )}
  151. {this.state.open && (
  152. <div className="date-input-calendar">
  153. <nav className="date-input-calendar-nav">
  154. <ButtonIcon className="button-small" onClick={this.handlePreviousMonthClick}>
  155. <ChevronLeftIcon />
  156. </ButtonIcon>
  157. <div className="date-input-calender-month">
  158. <Select
  159. className="date-input-calender-month-select"
  160. onChange={this.handleCurrentMonthChange}
  161. options={months.map(month => ({
  162. label: getShortMonthName(month),
  163. value: month
  164. }))}
  165. value={this.state.currentMonth.getMonth()}
  166. />
  167. <Select
  168. className="date-input-calender-month-select spacer-left"
  169. onChange={this.handleCurrentYearChange}
  170. options={years.map(year => ({ label: String(year), value: year }))}
  171. value={this.state.currentMonth.getFullYear()}
  172. />
  173. </div>
  174. <ButtonIcon className="button-small" onClick={this.handleNextMonthClick}>
  175. <ChevronRightIcon />
  176. </ButtonIcon>
  177. </nav>
  178. <DayPicker
  179. captionElement={<NullComponent />}
  180. disabledDays={{ after, before: minDate }}
  181. firstDayOfWeek={1}
  182. modifiers={modifiers}
  183. month={this.state.currentMonth}
  184. navbarElement={<NullComponent />}
  185. onDayClick={this.handleDayClick}
  186. onDayMouseEnter={this.handleDayMouseEnter}
  187. selectedDays={selectedDays}
  188. weekdaysLong={weekdaysLong}
  189. weekdaysShort={weekdaysShort}
  190. />
  191. </div>
  192. )}
  193. </span>
  194. </OutsideClickHandler>
  195. );
  196. }
  197. }
  198. function NullComponent() {
  199. return null;
  200. }
  201. type InputWrapperProps = T.Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value'> &
  202. InjectedIntlProps & {
  203. innerRef: React.Ref<HTMLInputElement>;
  204. value: Date | undefined;
  205. };
  206. const InputWrapper = injectIntl(({ innerRef, intl, value, ...other }: InputWrapperProps) => {
  207. const formattedValue =
  208. value && intl.formatDate(value, { year: 'numeric', month: 'short', day: 'numeric' });
  209. return <input {...other} ref={innerRef} value={formattedValue || ''} />;
  210. });