Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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