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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2019 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 CalendarIcon from 'sonar-ui-common/components/icons/CalendarIcon';
  23. import ChevronLeftIcon from 'sonar-ui-common/components/icons/ChevronLeftIcon';
  24. import ChevronRightIcon from 'sonar-ui-common/components/icons/ChevronRightIcon';
  25. import { DayModifiers, Modifier, Modifiers } from 'react-day-picker';
  26. import { InjectedIntlProps, injectIntl } from 'react-intl';
  27. import { range } from 'lodash';
  28. import * as addMonths from 'date-fns/add_months';
  29. import * as setMonth from 'date-fns/set_month';
  30. import * as setYear from 'date-fns/set_year';
  31. import * as subMonths from 'date-fns/sub_months';
  32. import {
  33. getShortMonthName,
  34. getWeekDayName,
  35. getShortWeekDayName
  36. } from 'sonar-ui-common/helpers/l10n';
  37. import { ButtonIcon, ClearButton } from 'sonar-ui-common/components/controls/buttons';
  38. import OutsideClickHandler from 'sonar-ui-common/components/controls/OutsideClickHandler';
  39. import { lazyLoad } from 'sonar-ui-common/components/lazyLoad';
  40. import Select from 'sonar-ui-common/components/controls/Select';
  41. import './DayPicker.css';
  42. import './styles.css';
  43. const DayPicker = lazyLoad(() => import('react-day-picker'));
  44. interface Props {
  45. className?: string;
  46. currentMonth?: Date;
  47. highlightFrom?: Date;
  48. highlightTo?: Date;
  49. inputClassName?: string;
  50. maxDate?: Date;
  51. minDate?: Date;
  52. name?: string;
  53. onChange: (date: Date | undefined) => void;
  54. placeholder: string;
  55. value?: Date;
  56. }
  57. interface State {
  58. currentMonth: Date;
  59. open: boolean;
  60. lastHovered?: Date;
  61. }
  62. type Week = [string, string, string, string, string, string, string];
  63. export default class DateInput extends React.PureComponent<Props, State> {
  64. input?: HTMLInputElement | null;
  65. constructor(props: Props) {
  66. super(props);
  67. this.state = { currentMonth: props.value || props.currentMonth || new Date(), open: false };
  68. }
  69. focus = () => {
  70. if (this.input) {
  71. this.input.focus();
  72. }
  73. this.openCalendar();
  74. };
  75. handleResetClick = () => {
  76. this.closeCalendar();
  77. this.props.onChange(undefined);
  78. };
  79. openCalendar = () => {
  80. this.setState({
  81. currentMonth: this.props.value || this.props.currentMonth || new Date(),
  82. lastHovered: undefined,
  83. open: true
  84. });
  85. };
  86. closeCalendar = () => {
  87. this.setState({ open: false });
  88. };
  89. handleDayClick = (day: Date, modifiers: DayModifiers) => {
  90. if (!modifiers.disabled) {
  91. this.closeCalendar();
  92. this.props.onChange(day);
  93. }
  94. };
  95. handleDayMouseEnter = (day: Date, modifiers: DayModifiers) => {
  96. this.setState({ lastHovered: modifiers.disabled ? undefined : day });
  97. };
  98. handleCurrentMonthChange = ({ value }: { value: number }) => {
  99. this.setState((state: State) => ({ currentMonth: setMonth(state.currentMonth, value) }));
  100. };
  101. handleCurrentYearChange = ({ value }: { value: number }) => {
  102. this.setState(state => ({ currentMonth: setYear(state.currentMonth, value) }));
  103. };
  104. handlePreviousMonthClick = () => {
  105. this.setState(state => ({ currentMonth: subMonths(state.currentMonth, 1) }));
  106. };
  107. handleNextMonthClick = () => {
  108. this.setState(state => ({ currentMonth: addMonths(state.currentMonth, 1) }));
  109. };
  110. render() {
  111. const { highlightFrom, highlightTo, minDate, value } = this.props;
  112. const { lastHovered } = this.state;
  113. const after = this.props.maxDate || new Date();
  114. const months = range(12);
  115. const years = range(new Date().getFullYear() - 10, new Date().getFullYear() + 1);
  116. const selectedDays: Modifier[] = value ? [value] : [];
  117. let modifiers: Partial<Modifiers> | undefined;
  118. const lastHoveredOrValue = lastHovered || value;
  119. if (highlightFrom && lastHoveredOrValue) {
  120. modifiers = { highlighted: { from: highlightFrom, to: lastHoveredOrValue } };
  121. selectedDays.push(highlightFrom);
  122. }
  123. if (highlightTo && lastHoveredOrValue) {
  124. modifiers = { highlighted: { from: lastHoveredOrValue, to: highlightTo } };
  125. selectedDays.push(highlightTo);
  126. }
  127. const weekdaysLong = range(7).map(getWeekDayName) as Week;
  128. const weekdaysShort = range(7).map(getShortWeekDayName) as Week;
  129. return (
  130. <OutsideClickHandler onClickOutside={this.closeCalendar}>
  131. <span className={classNames('date-input-control', this.props.className)}>
  132. <InputWrapper
  133. className={classNames('date-input-control-input', this.props.inputClassName, {
  134. 'is-filled': this.props.value !== undefined
  135. })}
  136. innerRef={node => (this.input = node)}
  137. name={this.props.name}
  138. onFocus={this.openCalendar}
  139. placeholder={this.props.placeholder}
  140. readOnly={true}
  141. type="text"
  142. value={value}
  143. />
  144. <CalendarIcon className="date-input-control-icon" fill="" />
  145. {this.props.value !== undefined && (
  146. <ClearButton
  147. className="button-tiny date-input-control-reset"
  148. iconProps={{ size: 12 }}
  149. onClick={this.handleResetClick}
  150. />
  151. )}
  152. {this.state.open && (
  153. <div className="date-input-calendar">
  154. <nav className="date-input-calendar-nav">
  155. <ButtonIcon className="button-small" onClick={this.handlePreviousMonthClick}>
  156. <ChevronLeftIcon />
  157. </ButtonIcon>
  158. <div className="date-input-calender-month">
  159. <Select
  160. className="date-input-calender-month-select"
  161. onChange={this.handleCurrentMonthChange}
  162. options={months.map(month => ({
  163. label: getShortMonthName(month),
  164. value: month
  165. }))}
  166. value={this.state.currentMonth.getMonth()}
  167. />
  168. <Select
  169. className="date-input-calender-month-select spacer-left"
  170. onChange={this.handleCurrentYearChange}
  171. options={years.map(year => ({ label: String(year), value: year }))}
  172. value={this.state.currentMonth.getFullYear()}
  173. />
  174. </div>
  175. <ButtonIcon className="button-small" onClick={this.handleNextMonthClick}>
  176. <ChevronRightIcon />
  177. </ButtonIcon>
  178. </nav>
  179. <DayPicker
  180. captionElement={<NullComponent />}
  181. disabledDays={{ after, before: minDate }}
  182. firstDayOfWeek={1}
  183. modifiers={modifiers}
  184. month={this.state.currentMonth}
  185. navbarElement={<NullComponent />}
  186. onDayClick={this.handleDayClick}
  187. onDayMouseEnter={this.handleDayMouseEnter}
  188. selectedDays={selectedDays}
  189. weekdaysLong={weekdaysLong}
  190. weekdaysShort={weekdaysShort}
  191. />
  192. </div>
  193. )}
  194. </span>
  195. </OutsideClickHandler>
  196. );
  197. }
  198. }
  199. function NullComponent() {
  200. return null;
  201. }
  202. type InputWrapperProps = T.Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value'> &
  203. InjectedIntlProps & {
  204. innerRef: React.Ref<HTMLInputElement>;
  205. value: Date | undefined;
  206. };
  207. const InputWrapper = injectIntl(({ innerRef, intl, value, ...other }: InputWrapperProps) => {
  208. const formattedValue =
  209. value && intl.formatDate(value, { year: 'numeric', month: 'short', day: 'numeric' });
  210. return <input {...other} ref={innerRef} value={formattedValue || ''} />;
  211. });