month.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. /* eslint-disable max-len */
  2. import React from 'react';
  3. import classNames from 'classnames';
  4. import PropTypes from 'prop-types';
  5. import MonthFoundation, { MonthAdapter, MonthDayInfo, MonthFoundationProps, MonthFoundationState } from '@douyinfe/semi-foundation/datePicker/monthFoundation';
  6. import { cssClasses, numbers } from '@douyinfe/semi-foundation/datePicker/constants';
  7. import BaseComponent, { BaseProps } from '../_base/baseComponent';
  8. import { isBefore, isAfter, isBetween, isSameDay } from '@douyinfe/semi-foundation/datePicker/_utils/index';
  9. import { noop, stubFalse, isFunction } from 'lodash';
  10. import { parseISO } from 'date-fns';
  11. import { Locale } from '../locale/interface';
  12. const prefixCls = cssClasses.PREFIX;
  13. export interface MonthProps extends MonthFoundationProps, BaseProps {
  14. forwardRef: React.Ref<any>;
  15. locale: Locale['DatePicker'];
  16. focusRecordsRef: React.RefObject<{ rangeStart: boolean; rangeEnd: boolean }>;
  17. }
  18. export type MonthState = MonthFoundationState;
  19. export default class Month extends BaseComponent<MonthProps, MonthState> {
  20. static propTypes = {
  21. month: PropTypes.object,
  22. selected: PropTypes.object,
  23. rangeStart: PropTypes.string,
  24. rangeEnd: PropTypes.string,
  25. offsetRangeStart: PropTypes.string,
  26. offsetRangeEnd: PropTypes.string,
  27. onDayClick: PropTypes.func,
  28. onDayHover: PropTypes.func,
  29. weekStartsOn: PropTypes.number,
  30. disabledDate: PropTypes.func,
  31. weeksRowNum: PropTypes.number,
  32. onWeeksRowNumChange: PropTypes.func,
  33. renderDate: PropTypes.func,
  34. renderFullDate: PropTypes.func,
  35. hoverDay: PropTypes.string, // Real-time hover date
  36. startDateOffset: PropTypes.func,
  37. endDateOffset: PropTypes.func,
  38. rangeInputFocus: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
  39. focusRecordsRef: PropTypes.object
  40. };
  41. static defaultProps = {
  42. month: new Date(),
  43. selected: new Set(),
  44. rangeStart: '',
  45. rangeEnd: '',
  46. onDayClick: noop,
  47. onDayHover: noop,
  48. onWeeksRowNumChange: noop,
  49. weekStartsOn: numbers.WEEK_START_ON,
  50. disabledDate: stubFalse,
  51. weeksRowNum: 0,
  52. };
  53. monthRef: React.RefObject<HTMLDivElement>;
  54. foundation: MonthFoundation;
  55. constructor(props: MonthProps) {
  56. super(props);
  57. this.state = {
  58. weekdays: [],
  59. month: { weeks: [], monthText: '' },
  60. todayText: '',
  61. weeksRowNum: props.weeksRowNum,
  62. };
  63. this.monthRef = React.createRef();
  64. }
  65. get adapter(): MonthAdapter {
  66. return {
  67. ...super.adapter,
  68. updateToday: todayText => this.setState({ todayText }),
  69. setWeekDays: weekdays => this.setState({ weekdays }),
  70. setWeeksRowNum: (weeksRowNum, callback) => this.setState({ weeksRowNum }, callback),
  71. updateMonthTable: month => this.setState({ month }),
  72. notifyDayClick: day => this.props.onDayClick(day),
  73. notifyDayHover: day => this.props.onDayHover(day),
  74. notifyWeeksRowNumChange: weeksRowNum => this.props.onWeeksRowNumChange(weeksRowNum),
  75. };
  76. }
  77. componentDidMount() {
  78. this.foundation = new MonthFoundation(this.adapter);
  79. this.foundation.init();
  80. }
  81. componentWillUnmount() {
  82. this.foundation.destroy();
  83. }
  84. componentDidUpdate(prevProps: MonthProps, prevState: MonthState) {
  85. if (prevProps.month !== this.props.month) {
  86. this.foundation.getMonthTable();
  87. }
  88. }
  89. getSingleDayStatus(options: Partial<MonthProps> & { fullDate: string; todayText: string }) {
  90. const { fullDate, todayText, selected, disabledDate, rangeStart, rangeEnd } = options;
  91. const disabledOptions = { rangeStart, rangeEnd };
  92. const isToday = fullDate === todayText;
  93. const isSelected = selected.has(fullDate);
  94. let isDisabled = disabledDate && disabledDate(parseISO(fullDate), disabledOptions);
  95. if (
  96. !isDisabled &&
  97. this.props.rangeInputFocus === 'rangeStart' &&
  98. rangeEnd &&
  99. this.props.focusRecordsRef &&
  100. this.props.focusRecordsRef.current.rangeEnd
  101. ) {
  102. // The reason for splitting is that the dateRangeTime format: 'yyyy-MM-dd HH:MM:SS'
  103. isDisabled = isAfter(fullDate, rangeEnd.trim().split(/\s+/)[0]);
  104. }
  105. if (
  106. !isDisabled &&
  107. this.props.rangeInputFocus === 'rangeEnd' &&
  108. rangeStart &&
  109. this.props.focusRecordsRef &&
  110. this.props.focusRecordsRef.current.rangeStart
  111. ) {
  112. // The reason for splitting is that the dateRangeTime format: 'yyyy-MM-dd HH:MM:SS'
  113. isDisabled = isBefore(fullDate, rangeStart.trim().split(/\s+/)[0]);
  114. }
  115. return {
  116. isToday, // Today
  117. isSelected, // Selected
  118. isDisabled // Disabled
  119. };
  120. }
  121. getDateRangeStatus(options: Partial<MonthProps> & { fullDate: string }) {
  122. const { rangeStart, rangeEnd, fullDate, hoverDay, offsetRangeStart, offsetRangeEnd, rangeInputFocus } = options;
  123. // If no item is selected, return the empty object directly
  124. const _isDateRangeAnySelected = Boolean(rangeStart || rangeEnd);
  125. const _isDateRangeSelected = Boolean(rangeStart && rangeEnd);
  126. const _isOffsetDateRangeAnyExist = offsetRangeStart || offsetRangeEnd;
  127. if (!_isDateRangeAnySelected) {
  128. return ({});
  129. }
  130. // The range selects the hover date, and the normal hover is .semi-datepicker-main: hover
  131. const _isHoverDay = isSameDay(hoverDay, fullDate);
  132. // When one is selected
  133. // eslint-disable-next-line one-var
  134. let _isHoverAfterStart, _isHoverBeforeEnd, isSelectedStart, isSelectedEnd, isHoverDayAroundOneSelected;
  135. if (rangeStart) {
  136. isSelectedStart = isSameDay(fullDate, rangeStart);
  137. if (rangeInputFocus === 'rangeEnd') {
  138. _isHoverAfterStart = isBetween(fullDate, { start: rangeStart, end: hoverDay });
  139. }
  140. }
  141. if (rangeEnd) {
  142. isSelectedEnd = isSameDay(fullDate, rangeEnd);
  143. if (rangeInputFocus === 'rangeStart') {
  144. _isHoverBeforeEnd = isBetween(fullDate, { start: hoverDay, end: rangeEnd });
  145. }
  146. }
  147. if (!_isDateRangeSelected && _isDateRangeAnySelected) {
  148. isHoverDayAroundOneSelected = _isHoverDay;
  149. }
  150. // eslint-disable-next-line one-var
  151. let isHover;
  152. if (!_isOffsetDateRangeAnyExist) {
  153. isHover = _isHoverAfterStart || _isHoverBeforeEnd || _isHoverDay;
  154. }
  155. // Select all
  156. // eslint-disable-next-line one-var
  157. let isInRange, isSelectedStartAfterHover, isSelectedEndBeforeHover, isHoverDayInStartSelection, isHoverDayInEndSelection, isHoverDayInRange;
  158. if (_isDateRangeSelected) {
  159. isInRange = isBetween(fullDate, { start: rangeStart, end: rangeEnd });
  160. if (!_isOffsetDateRangeAnyExist) {
  161. isSelectedStartAfterHover = isSelectedStart && isAfter(rangeStart, hoverDay);
  162. isSelectedEndBeforeHover = isSelectedEnd && isBefore(rangeEnd, hoverDay);
  163. isHoverDayInStartSelection = _isHoverDay && rangeInputFocus === 'rangeStart';
  164. isHoverDayInEndSelection = _isHoverDay && rangeInputFocus === 'rangeEnd';
  165. isHoverDayInRange = _isHoverDay && isBetween(hoverDay, { start: rangeStart, end: rangeEnd });
  166. }
  167. }
  168. return {
  169. isHoverDay: _isHoverDay, // Is the current hover date
  170. isSelectedStart, // Select Start
  171. isSelectedEnd, // End of selection
  172. isInRange, // Range within the selected date
  173. isHover, // Date between selection and hover date
  174. isSelectedStartAfterHover, // Choose to start behind the hover
  175. isSelectedEndBeforeHover, // Choose to end in front of the hover
  176. isHoverDayInRange, // Hover date within range
  177. isHoverDayInStartSelection, // Hover date when starting Date is selected
  178. isHoverDayInEndSelection, // Hover date when endDate is selected
  179. isHoverDayAroundOneSelected, // Hover date and select a date
  180. };
  181. }
  182. getOffsetDateStatus(options: Partial<MonthProps> & { fullDate: string }) {
  183. const { offsetRangeStart, offsetRangeEnd, rangeStart, rangeEnd, fullDate, hoverDay } = options;
  184. // When there is no offset, return the empty object directly
  185. const _isOffsetDateRangeNull = !(offsetRangeStart || offsetRangeEnd);
  186. if (_isOffsetDateRangeNull) {
  187. return ({});
  188. }
  189. // Range Select base date
  190. const _isInRange = isBetween(fullDate, { start: rangeStart, end: rangeEnd });
  191. const _isHoverDay = isSameDay(hoverDay, fullDate);
  192. const _isSelectedStart = rangeStart && isSameDay(fullDate, rangeStart);
  193. const _isSelectedEnd = rangeEnd && isSameDay(fullDate, rangeEnd);
  194. const _isDateRangeSelected = Boolean(rangeStart && rangeEnd);
  195. // Determine whether it is offsetStart or offsetRangeEnd
  196. const isOffsetRangeStart = isSameDay(fullDate, offsetRangeStart);
  197. const isOffsetRangeEnd = isSameDay(fullDate, offsetRangeEnd);
  198. const isHoverDayOffset = _isHoverDay;
  199. // When selected
  200. let isHoverInOffsetRange, isInOffsetRange;
  201. if (_isDateRangeSelected) {
  202. isHoverInOffsetRange = _isInRange && _isHoverDay;
  203. }
  204. // When there is an offset area
  205. const _isOffsetDateRangeSelected = Boolean(offsetRangeStart && offsetRangeEnd);
  206. if (_isOffsetDateRangeSelected) {
  207. isInOffsetRange = (_isSelectedStart || isBetween(fullDate, { start: offsetRangeStart, end: offsetRangeEnd }) || _isSelectedEnd);
  208. }
  209. return {
  210. isOffsetRangeStart, // Week selection start
  211. isOffsetRangeEnd, // End of week selection
  212. isHoverInOffsetRange, // Hover in the week selection
  213. isHoverDayOffset, // Week selection hover day
  214. isInOffsetRange // Include start and end within the week selection (start and end styles are the same as other dates, so start and end are included)
  215. };
  216. }
  217. /**
  218. * get day current status
  219. * @param {Object} fullDate
  220. * @param {Object} options
  221. * @returns {Object}
  222. */
  223. getDayStatus(currentDay: MonthDayInfo, options: MonthProps & { todayText: string }) {
  224. const { fullDate } = currentDay;
  225. const { hoverDay, rangeStart, rangeEnd, todayText, offsetRangeStart, offsetRangeEnd, disabledDate, selected, rangeInputFocus } = options;
  226. const singleDayStatus = this.getSingleDayStatus({ fullDate, todayText, hoverDay, selected, disabledDate, rangeStart, rangeEnd });
  227. const dateRangeStatus = this.getDateRangeStatus({ fullDate, rangeStart, rangeEnd, hoverDay, offsetRangeStart, offsetRangeEnd, rangeInputFocus, ...singleDayStatus });
  228. const offsetDataStatus = this.getOffsetDateStatus({ offsetRangeStart, offsetRangeEnd, rangeStart, rangeEnd, fullDate, hoverDay, ...singleDayStatus, ...dateRangeStatus });
  229. // this parameter will pass to the user when given renderFullDate function, do not delete or modify its key
  230. const dayStatus = {
  231. ...singleDayStatus,
  232. ...dateRangeStatus,
  233. ...offsetDataStatus,
  234. };
  235. return dayStatus;
  236. }
  237. renderDayOfWeek() {
  238. const { locale } = this.props;
  239. const weekdayCls = classNames(cssClasses.WEEKDAY);
  240. const weekdayItemCls = classNames(`${prefixCls}-weekday-item`);
  241. const { weekdays } = this.state;
  242. // i18n
  243. const weekdaysText = weekdays.map(key => locale.weeks[key]);
  244. return (
  245. <div className={weekdayCls}>
  246. {weekdaysText.map((E, i) => (
  247. <div key={E + i} className={weekdayItemCls}>
  248. {E}
  249. </div>
  250. ))}
  251. </div>
  252. );
  253. }
  254. renderWeeks() {
  255. const { month } = this.state;
  256. const { weeks } = month;
  257. const { weeksRowNum } = this.props;
  258. let style = {};
  259. if (weeksRowNum) {
  260. const height = weeksRowNum * numbers.WEEK_HEIGHT;
  261. style = { height };
  262. }
  263. const weeksCls = classNames(cssClasses.WEEKS);
  264. return (
  265. <div className={weeksCls} style={style}>
  266. {weeks.map((week, weekIndex) => this.renderWeek(week, weekIndex))}
  267. </div>
  268. );
  269. }
  270. renderWeek(week: MonthDayInfo[], weekIndex: number) {
  271. const weekCls = cssClasses.WEEK;
  272. return (
  273. <div className={weekCls} key={weekIndex}>
  274. {week.map((day, dayIndex) => this.renderDay(day, dayIndex))}
  275. </div>
  276. );
  277. }
  278. renderDay(day: MonthDayInfo, dayIndex: number) {
  279. const { todayText } = this.state;
  280. const { renderFullDate, renderDate } = this.props;
  281. const { fullDate, dayNumber } = day;
  282. if (!fullDate) {
  283. return (
  284. <div key={(dayNumber as number) + dayIndex} className={cssClasses.DAY}>
  285. <span />
  286. </div>
  287. );
  288. }
  289. const dayStatus = this.getDayStatus(day, { todayText, ...this.props });
  290. const dayCls = classNames(cssClasses.DAY, {
  291. [cssClasses.DAY_TODAY]: dayStatus.isToday,
  292. [cssClasses.DAY_IN_RANGE]: dayStatus.isInRange,
  293. [cssClasses.DAY_HOVER]: dayStatus.isHover,
  294. [cssClasses.DAY_SELECTED]: dayStatus.isSelected,
  295. [cssClasses.DAY_SELECTED_START]: dayStatus.isSelectedStart,
  296. [cssClasses.DAY_SELECTED_END]: dayStatus.isSelectedEnd,
  297. [cssClasses.DAY_DISABLED]: dayStatus.isDisabled,
  298. // offsetDate class
  299. [cssClasses.DAY_HOVER_DAY]: dayStatus.isHoverDayOffset,
  300. [cssClasses.DAY_IN_OFFSET_RANGE]: dayStatus.isInOffsetRange,
  301. [cssClasses.DAY_SELECTED_RANGE_HOVER]: dayStatus.isHoverInOffsetRange,
  302. [cssClasses.DAY_OFFSET_RANGE_START]: dayStatus.isOffsetRangeStart,
  303. [cssClasses.DAY_OFFSET_RANGE_END]: dayStatus.isOffsetRangeEnd,
  304. // range input class
  305. [cssClasses.DAY_SELECTED_START_AFTER_HOVER]: dayStatus.isSelectedStartAfterHover,
  306. [cssClasses.DAY_SELECTED_END_BEFORE_HOVER]: dayStatus.isSelectedEndBeforeHover,
  307. [cssClasses.DAY_HOVER_DAY_BEFORE_RANGE]: dayStatus.isHoverDayInStartSelection,
  308. [cssClasses.DAY_HOVER_DAY_AFTER_RANGE]: dayStatus.isHoverDayInEndSelection,
  309. [cssClasses.DAY_HOVER_DAY_AROUND_SINGLE_SELECTED]: dayStatus.isHoverDayAroundOneSelected,
  310. });
  311. const dayMainCls = classNames({
  312. [`${cssClasses.DAY}-main`]: true,
  313. });
  314. const fullDateArgs = [dayNumber, fullDate, dayStatus];
  315. const customRender = isFunction(renderFullDate);
  316. return (
  317. <div
  318. className={!customRender ? dayCls : cssClasses.DAY}
  319. title={fullDate}
  320. key={(dayNumber as number) + dayIndex}
  321. onClick={e => !dayStatus.isDisabled && this.foundation.handleClick(day)}
  322. onMouseEnter={() => this.foundation.handleHover(day)}
  323. onMouseLeave={() => this.foundation.handleHover()}
  324. >
  325. {customRender ? renderFullDate(...fullDateArgs) : (
  326. <div className={dayMainCls}>
  327. {isFunction(renderDate) ? renderDate(dayNumber, fullDate) : <span>{dayNumber}</span>}
  328. </div>
  329. )}
  330. </div>
  331. );
  332. }
  333. render() {
  334. const { forwardRef } = this.props;
  335. const weekday = this.renderDayOfWeek();
  336. const weeks = this.renderWeeks();
  337. const monthCls = classNames(cssClasses.MONTH);
  338. const ref = forwardRef || this.monthRef;
  339. return (
  340. <div ref={ref} className={monthCls}>
  341. {weekday}
  342. {weeks}
  343. </div>
  344. );
  345. }
  346. }