monthsGridFoundation.ts 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942
  1. /* eslint-disable max-len */
  2. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  3. import { strings } from './constants';
  4. import {
  5. format,
  6. set,
  7. addMonths,
  8. subMonths,
  9. subYears,
  10. addYears,
  11. differenceInCalendarMonths,
  12. differenceInCalendarYears,
  13. isSameDay,
  14. parseISO
  15. } from 'date-fns';
  16. import { isBefore, isValidDate, getDefaultFormatToken, getFullDateOffset } from './_utils/index';
  17. import { formatFullDate, WeekStartNumber } from './_utils/getMonthTable';
  18. import { compatiableParse } from './_utils/parser';
  19. import { includes, isSet, isEqual, isFunction } from 'lodash';
  20. import { zonedTimeToUtc } from '../utils/date-fns-extra';
  21. import { getDefaultFormatTokenByType } from './_utils/getDefaultFormatToken';
  22. import isNullOrUndefined from '../utils/isNullOrUndefined';
  23. import { BaseValueType, ValueType } from './foundation';
  24. import { MonthDayInfo } from './monthFoundation';
  25. import { ArrayElement } from '../utils/type';
  26. const dateDiffFns = {
  27. month: differenceInCalendarMonths,
  28. year: differenceInCalendarYears,
  29. };
  30. const dateCalcFns = {
  31. prevMonth: subMonths,
  32. nextMonth: addMonths,
  33. prevYear: subYears,
  34. nextYear: addYears,
  35. };
  36. type Type = ArrayElement<typeof strings.TYPE_SET>;
  37. interface MonthsGridElementProps {
  38. navPrev?: React.ReactNode;
  39. navNext?: React.ReactNode;
  40. renderDate?: () => React.ReactNode;
  41. renderFullDate?: () => React.ReactNode;
  42. }
  43. export type PanelType = 'left' | 'right';
  44. export type YearMonthChangeType = 'prevMonth' | 'nextMonth' | 'prevYear' | 'nextYear';
  45. export interface MonthsGridFoundationProps extends MonthsGridElementProps {
  46. type?: Type;
  47. defaultValue?: ValueType;
  48. defaultPickerValue?: ValueType;
  49. multiple?: boolean;
  50. max?: number;
  51. splitPanels?: boolean;
  52. weekStartsOn?: WeekStartNumber;
  53. disabledDate?: (date: Date, options?: { rangeStart: string; rangeEnd: string }) => boolean;
  54. disabledTime?: (date: Date | Date[], panelType: PanelType) => void;
  55. disabledTimePicker?: boolean;
  56. hideDisabledOptions?: boolean;
  57. onMaxSelect?: (v?: any) => void;
  58. timePickerOpts?: any;
  59. isControlledComponent?: boolean;
  60. rangeStart?: string;
  61. rangeInputFocus?: boolean | string;
  62. locale?: any;
  63. localeCode?: string;
  64. format?: string;
  65. startDateOffset?: () => void;
  66. endDateOffset?: () => void;
  67. autoSwitchDate?: boolean;
  68. motionEnd?: boolean;
  69. density?: string;
  70. dateFnsLocale?: any;
  71. timeZone?: string | number;
  72. syncSwitchMonth?: boolean;
  73. onChange?: (
  74. value: [Date] | [Date, Date],
  75. options?: { closePanel?: boolean; needCheckFocusRecord?: boolean }
  76. ) => void;
  77. onPanelChange?: (date: Date | Date[], dateString: string | string[]) => void;
  78. setRangeInputFocus?: (rangeInputFocus: 'rangeStart' | 'rangeEnd') => void;
  79. isAnotherPanelHasOpened?: (currentRangeInput: 'rangeStart' | 'rangeEnd') => boolean;
  80. focusRecordsRef?: any;
  81. triggerRender?: (props: Record<string, any>) => any;
  82. }
  83. export interface MonthInfo {
  84. pickerDate: Date;
  85. showDate: Date;
  86. isTimePickerOpen: boolean;
  87. isYearPickerOpen: boolean;
  88. }
  89. export interface MonthsGridFoundationState {
  90. selected: Set<string>;
  91. monthLeft: MonthInfo;
  92. monthRight: MonthInfo;
  93. maxWeekNum: number; // Maximum number of weeks left and right for manual height adjustment
  94. hoverDay: string; // Real-time hover date
  95. rangeStart: string; // Start date for range selection
  96. rangeEnd: string; // End date of range selection
  97. currentPanelHeight: number; // current month panel height,
  98. offsetRangeStart: string;
  99. offsetRangeEnd: string;
  100. weeksRowNum?: number;
  101. }
  102. export interface MonthsGridDateAdapter {
  103. updateDaySelected: (selected: Set<string>) => void;
  104. }
  105. export interface MonthsGridRangeAdapter {
  106. setRangeStart: (rangeStart: string) => void;
  107. setRangeEnd: (rangeEnd: string) => void;
  108. setHoverDay: (hoverDay: string) => void;
  109. setWeeksHeight: (maxWeekNum: number) => void;
  110. setOffsetRangeStart: (offsetRangeStart: string) => void;
  111. setOffsetRangeEnd: (offsetRangeEnd: string) => void;
  112. }
  113. export interface MonthsGridAdapter extends DefaultAdapter<MonthsGridFoundationProps, MonthsGridFoundationState>, MonthsGridRangeAdapter, MonthsGridDateAdapter {
  114. updateMonthOnLeft: (v: MonthInfo) => void;
  115. updateMonthOnRight: (v: MonthInfo) => void;
  116. notifySelectedChange: MonthsGridFoundationProps['onChange'];
  117. notifyMaxLimit: MonthsGridFoundationProps['onMaxSelect'];
  118. notifyPanelChange: MonthsGridFoundationProps['onPanelChange'];
  119. setRangeInputFocus: MonthsGridFoundationProps['setRangeInputFocus'];
  120. isAnotherPanelHasOpened: MonthsGridFoundationProps['isAnotherPanelHasOpened'];
  121. }
  122. export default class MonthsGridFoundation extends BaseFoundation<MonthsGridAdapter> {
  123. newBiMonthPanelDate: [Date, Date];
  124. constructor(adapter: MonthsGridAdapter) {
  125. super({ ...adapter });
  126. // Date change data when double panels
  127. this.newBiMonthPanelDate = [this.getState('monthLeft').pickerDate, this.getState('monthRight').pickerDate];
  128. }
  129. init() {
  130. const defaultValue = this.getProp('defaultValue');
  131. this.initDefaultPickerValue();
  132. this.updateSelectedFromProps(defaultValue);
  133. }
  134. initDefaultPickerValue() {
  135. const defaultPickerValue = compatiableParse(this.getProp('defaultPickerValue'));
  136. if (defaultPickerValue && isValidDate(defaultPickerValue)) {
  137. this._updatePanelDetail(strings.PANEL_TYPE_LEFT, {
  138. pickerDate: defaultPickerValue,
  139. });
  140. this._updatePanelDetail(strings.PANEL_TYPE_RIGHT, {
  141. pickerDate: addMonths(defaultPickerValue, 1),
  142. });
  143. }
  144. }
  145. updateSelectedFromProps(values: ValueType, refreshPicker = true) {
  146. const type: Type = this.getProp('type');
  147. const { selected, rangeStart, rangeEnd } = this.getStates();
  148. if (values && (values as BaseValueType[]).length) {
  149. switch (type) {
  150. case 'date':
  151. this._initDatePickerFromValue(values as BaseValueType[], refreshPicker);
  152. break;
  153. case 'dateRange':
  154. this._initDateRangePickerFromValue(values as BaseValueType[]);
  155. break;
  156. case 'dateTime':
  157. this._initDateTimePickerFromValue(values as BaseValueType[]);
  158. break;
  159. case 'dateTimeRange':
  160. this._initDateTimeRangePickerFormValue(values as BaseValueType[]);
  161. break;
  162. default:
  163. break;
  164. }
  165. } else if (Array.isArray(values) && !values.length || !values) {
  166. // Empty panel when value is empty Select date
  167. if (isSet(selected) && selected.size) {
  168. this._adapter.updateDaySelected(new Set());
  169. }
  170. if (rangeStart) {
  171. this._adapter.setRangeStart('');
  172. }
  173. if (rangeEnd) {
  174. this._adapter.setRangeEnd('');
  175. }
  176. }
  177. }
  178. calcDisabledTime(panelType: PanelType) {
  179. const { disabledTime, type } = this.getProps();
  180. if (typeof disabledTime === 'function' && panelType && ['dateTime', 'dateTimeRange'].includes(type)) {
  181. const { rangeStart, rangeEnd, monthLeft } = this.getStates();
  182. const selected = [];
  183. if (type === 'dateTimeRange') {
  184. if (rangeStart) {
  185. selected.push(rangeStart);
  186. }
  187. if (rangeStart && rangeEnd) {
  188. selected.push(rangeEnd);
  189. }
  190. } else if (monthLeft && monthLeft.showDate) {
  191. selected.push(monthLeft.showDate);
  192. }
  193. const selectedDates = selected.map(str => (str instanceof Date ? str : parseISO(str)));
  194. const cbDates = type === 'dateTimeRange' ? selectedDates : selectedDates[0];
  195. return disabledTime(cbDates, panelType);
  196. }
  197. }
  198. _initDatePickerFromValue(values: BaseValueType[], refreshPicker = true) {
  199. const monthLeft = this.getState('monthLeft');
  200. const newMonthLeft = { ...monthLeft };
  201. this._adapter.updateMonthOnLeft(newMonthLeft);
  202. const newSelected = new Set<string>();
  203. if (!this._isMultiple()) {
  204. newSelected.add(format(values[0] as Date, strings.FORMAT_FULL_DATE));
  205. } else {
  206. values.forEach(date => {
  207. newSelected.add(format(date as Date, strings.FORMAT_FULL_DATE));
  208. });
  209. }
  210. if (refreshPicker) {
  211. this.handleShowDateAndTime(strings.PANEL_TYPE_LEFT, values[0] || newMonthLeft.pickerDate);
  212. } else {
  213. this.handleShowDateAndTime(strings.PANEL_TYPE_LEFT, newMonthLeft.pickerDate);
  214. }
  215. this._adapter.updateDaySelected(newSelected);
  216. }
  217. _initDateRangePickerFromValue(values: BaseValueType[], withTime = false) {
  218. // init month panel
  219. const monthLeft = this.getState('monthLeft');
  220. const monthRight = this.getState('monthRight');
  221. const adjustResult = this._autoAdjustMonth(
  222. { ...monthLeft, pickerDate: values[0] || monthLeft.pickerDate },
  223. { ...monthRight, pickerDate: values[1] || monthRight.pickerDate }
  224. );
  225. this.handleShowDateAndTime(strings.PANEL_TYPE_LEFT, adjustResult.monthLeft.pickerDate);
  226. this.handleShowDateAndTime(strings.PANEL_TYPE_RIGHT, adjustResult.monthRight.pickerDate);
  227. // init range
  228. const formatToken = withTime ? strings.FORMAT_DATE_TIME : strings.FORMAT_FULL_DATE;
  229. let rangeStart = values[0] && format(values[0] as Date, formatToken);
  230. let rangeEnd = values[1] && format(values[1] as Date, formatToken);
  231. if (this._isNeedSwap(rangeStart, rangeEnd)) {
  232. [rangeStart, rangeEnd] = [rangeEnd, rangeStart];
  233. }
  234. this._adapter.setRangeStart(rangeStart);
  235. this._adapter.setRangeEnd(rangeEnd);
  236. this._adapter.setHoverDay(rangeEnd);
  237. }
  238. _initDateTimePickerFromValue(values: BaseValueType[]) {
  239. this._initDatePickerFromValue(values);
  240. }
  241. _initDateTimeRangePickerFormValue(values: BaseValueType[]) {
  242. this._initDateRangePickerFromValue(values, true);
  243. }
  244. // eslint-disable-next-line @typescript-eslint/no-empty-function
  245. destroy() { }
  246. /**
  247. * sync change another panel month when change months from the else yam panel
  248. * call it when
  249. * - current change panel targe date month is same with another panel date
  250. *
  251. * @example
  252. * - panelType=right, target=new Date('2022-09-01') and left panel is in '2022-09' => call it, left panel minus one month to '2022-08'
  253. * - panelType=left, target=new Date('2021-12-01') and right panel is in '2021-12' => call it, right panel add one month to '2021-01'
  254. */
  255. handleSyncChangeMonths(options: { panelType: PanelType, target: Date }) {
  256. const { panelType, target } = options;
  257. const { type } = this._adapter.getProps();
  258. const { monthLeft, monthRight } = this._adapter.getStates();
  259. if (this.isRangeType(type)) {
  260. if (panelType === 'right' && differenceInCalendarMonths(target, monthLeft.pickerDate) === 0) {
  261. this.handleYearOrMonthChange('prevMonth', 'left', 1, true);
  262. } else if (panelType === 'left' && differenceInCalendarMonths(monthRight.pickerDate, target) === 0) {
  263. this.handleYearOrMonthChange('nextMonth', 'right', 1, true);
  264. }
  265. }
  266. }
  267. /**
  268. * Get the target date based on the panel type and switch type
  269. */
  270. getTargetChangeDate(options: { panelType: PanelType, switchType: YearMonthChangeType }) {
  271. const { panelType, switchType } = options;
  272. const { monthRight, monthLeft } = this._adapter.getStates();
  273. const currentDate = panelType === 'left' ? monthLeft.pickerDate : monthRight.pickerDate;
  274. let target: Date;
  275. switch (switchType) {
  276. case 'prevMonth':
  277. target = addMonths(currentDate, -1);
  278. break;
  279. case 'nextMonth':
  280. target = addMonths(currentDate, 1);
  281. break;
  282. case 'prevYear':
  283. target = addYears(currentDate, -1);
  284. break;
  285. case 'nextYear':
  286. target = addYears(currentDate, 1);
  287. break;
  288. }
  289. return target;
  290. }
  291. /**
  292. * Change month by yam panel
  293. */
  294. toMonth(panelType: PanelType, target: Date) {
  295. const { type } = this._adapter.getProps();
  296. const diff = this._getDiff('month', target, panelType);
  297. this.handleYearOrMonthChange(diff < 0 ? 'prevMonth' : 'nextMonth', panelType, Math.abs(diff), false);
  298. if (this.isRangeType(type)) {
  299. this.handleSyncChangeMonths({ panelType, target });
  300. }
  301. }
  302. toYear(panelType: PanelType, target: Date) {
  303. const diff = this._getDiff('year', target, panelType);
  304. this.handleYearOrMonthChange(diff < 0 ? 'prevYear' : 'nextYear', panelType, Math.abs(diff), false);
  305. }
  306. toYearMonth(panelType: PanelType, target: Date) {
  307. this.toYear(panelType, target);
  308. this.toMonth(panelType, target);
  309. }
  310. isRangeType(type?: Type) {
  311. const { type: typeFromProp } = this.getProps();
  312. const realType = type ? type : typeFromProp;
  313. return typeof realType === 'string' && /range/i.test(realType);
  314. }
  315. handleSwitchMonthOrYear(switchType: YearMonthChangeType, panelType: PanelType) {
  316. const { type, syncSwitchMonth } = this.getProps();
  317. const rangeType = this.isRangeType(type);
  318. // range type and syncSwitchMonth, we should change panels at same time
  319. if (rangeType && syncSwitchMonth) {
  320. this.handleYearOrMonthChange(switchType, 'left', 1, true);
  321. this.handleYearOrMonthChange(switchType, 'right', 1, true);
  322. } else {
  323. this.handleYearOrMonthChange(switchType, panelType);
  324. /**
  325. * default behavior (v2.2.0)
  326. * In order to prevent the two panels from being the same month, this will confuse the user when selecting the range
  327. * https://github.com/DouyinFE/semi-design/issues/260
  328. */
  329. if (rangeType) {
  330. const target = this.getTargetChangeDate({ panelType, switchType });
  331. this.handleSyncChangeMonths({ panelType, target });
  332. }
  333. }
  334. }
  335. prevMonth(panelType: PanelType) {
  336. this.handleSwitchMonthOrYear('prevMonth', panelType);
  337. }
  338. nextMonth(panelType: PanelType) {
  339. this.handleSwitchMonthOrYear('nextMonth', panelType);
  340. }
  341. prevYear(panelType: PanelType) {
  342. this.handleSwitchMonthOrYear('prevYear', panelType);
  343. }
  344. nextYear(panelType: PanelType) {
  345. this.handleSwitchMonthOrYear('nextYear', panelType);
  346. }
  347. /**
  348. * Calculate the year and month difference
  349. */
  350. _getDiff(type: 'month' | 'year', target: Date, panelType: PanelType) {
  351. const panelDetail = this._getPanelDetail(panelType);
  352. const diff = dateDiffFns[type] && dateDiffFns[type](target, panelDetail.pickerDate);
  353. return diff;
  354. }
  355. _getPanelDetail(panelType: PanelType) {
  356. return panelType === strings.PANEL_TYPE_RIGHT ? this.getState('monthRight') : this.getState('monthLeft');
  357. }
  358. /**
  359. * Format locale date
  360. * locale get from LocaleProvider
  361. * @param {Date} date
  362. * @param {String} token
  363. * @returns
  364. */
  365. localeFormat(date: Date, token: string) {
  366. const dateFnsLocale = this._adapter.getProp('dateFnsLocale');
  367. return format(date, token, { locale: dateFnsLocale });
  368. }
  369. isValidTimeZone(timeZone?: string | number) {
  370. const propTimeZone = this.getProp('timeZone');
  371. const _timeZone = isNullOrUndefined(timeZone) ? propTimeZone : timeZone;
  372. return ['string', 'number'].includes(typeof _timeZone) && _timeZone !== '';
  373. }
  374. /**
  375. * 根据 type 处理 onChange 返回的参数
  376. *
  377. * - 返回的日期需要把用户时间转换为设置的时区时间
  378. * - 用户时间:用户计算机系统时间
  379. * - 时区时间:通过 ConfigProvider 设置的 timeZone
  380. * - 例子:用户设置时区为+9,计算机所在时区为+8区,然后用户选择了22:00
  381. * - DatePicker 内部保存日期 state 为 +8 的 22:00 => a = new Date("2021-05-25 22:00:00")
  382. * - 传出去时,需要把 +8 的 22:00 => +9 的 22:00 => b = zonedTimeToUtc(a, "+09:00");
  383. *
  384. * The parameters returned by onChange are processed according to type
  385. *
  386. * -The returned date needs to convert the user time to the set time zone time
  387. * -User time: user computer system time
  388. * -Time zone: timeZone set by ConfigProvider
  389. * -Example: The user sets the time zone to + 9, and the time zone where the computer is located is + 8, and then the user selects 22:00
  390. * -DatePicker internal save date state is + 8 22:00 = > a = new Date ("2021-05-25 22:00:00")
  391. * -When passing out, you need to put + 8's 22:00 = > + 9's 22:00 = > b = zonedTimeToUtc (a, "+ 09:00");
  392. *
  393. * e.g.
  394. * let a = new Date ("2021-05-25 22:00:00");
  395. * = > Tue May 25 2021 22:00:00 GMT + 0800 (China Standard Time)
  396. * let b = zonedTimeToUtc (a, "+ 09:00");
  397. * = > Tue May 25 2021 21:00:00 GMT + 0800 (China Standard Time)
  398. *
  399. * @param {Date|Date[]} value
  400. */
  401. disposeCallbackArgs(value: Date | Date[]) {
  402. let _value = Array.isArray(value) ? value : (value && [value]) || [];
  403. if (this.isValidTimeZone()) {
  404. const timeZone = this.getProp('timeZone');
  405. _value = _value.map(date => zonedTimeToUtc(date, timeZone));
  406. }
  407. const type = this.getProp('type');
  408. const formatToken = this.getProp('format') || getDefaultFormatTokenByType(type);
  409. let notifyValue,
  410. notifyDate;
  411. switch (type) {
  412. case 'date':
  413. case 'dateTime':
  414. case 'month':
  415. if (!this._isMultiple()) {
  416. notifyValue = _value[0] && this.localeFormat(_value[0], formatToken);
  417. [notifyDate] = _value;
  418. } else {
  419. notifyValue = _value.map(v => v && this.localeFormat(v, formatToken));
  420. notifyDate = [..._value];
  421. }
  422. break;
  423. case 'dateRange':
  424. case 'dateTimeRange':
  425. notifyValue = _value.map(v => v && this.localeFormat(v, formatToken));
  426. notifyDate = [..._value];
  427. break;
  428. default:
  429. break;
  430. }
  431. return {
  432. notifyValue,
  433. notifyDate,
  434. };
  435. }
  436. handleYearOrMonthChange(
  437. type: YearMonthChangeType,
  438. panelType: PanelType = strings.PANEL_TYPE_LEFT,
  439. step = 1,
  440. notSeparateInRange = false
  441. ) {
  442. const { autoSwitchDate, type: datePanelType } = this.getProps();
  443. const { monthLeft, monthRight } = this.getStates();
  444. const isRangeType = this.isRangeType(datePanelType);
  445. const isLeftPanelInRange = isRangeType && panelType === strings.PANEL_TYPE_LEFT;
  446. const panelDetail = this._getPanelDetail(panelType);
  447. const { pickerDate } = panelDetail;
  448. const fn = dateCalcFns[type];
  449. const targetMonth = fn(pickerDate, step);
  450. // Determine if the date has changed
  451. const panelDateHasUpdate = (panelType === strings.PANEL_TYPE_LEFT && !isEqual(targetMonth, monthLeft.pickerDate)) ||
  452. (panelType === strings.PANEL_TYPE_RIGHT && !isEqual(targetMonth, monthRight.pickerDate));
  453. this._updatePanelDetail(panelType, { pickerDate: targetMonth });
  454. if (panelDateHasUpdate) { // When the date changes
  455. if (!isRangeType) { // Single Panel Type
  456. const { notifyValue, notifyDate } = this.disposeCallbackArgs(targetMonth);
  457. this._adapter.notifyPanelChange(notifyDate, notifyValue);
  458. } else { // Double Panel Type
  459. if (isLeftPanelInRange) { // Left panel
  460. this.newBiMonthPanelDate[0] = targetMonth;
  461. } else { // Right panel
  462. this.newBiMonthPanelDate[1] = targetMonth;
  463. }
  464. if (!(isLeftPanelInRange && notSeparateInRange)) { // Not synchronously switching the left panel in the scene
  465. const { notifyValue, notifyDate } = this.disposeCallbackArgs(this.newBiMonthPanelDate);
  466. this._adapter.notifyPanelChange(notifyDate, notifyValue);
  467. }
  468. }
  469. }
  470. if (autoSwitchDate) {
  471. this.updateDateAfterChangeYM(type, targetMonth);
  472. }
  473. }
  474. /**
  475. * You have chosen to switch the year and month in the future to directly update the Date without closing the date panel
  476. * @param {*} type
  477. * @param {*} targetDate
  478. */
  479. updateDateAfterChangeYM(
  480. type: YearMonthChangeType,
  481. targetDate: Date
  482. ) {
  483. const { multiple, disabledDate } = this.getProps();
  484. const { selected: selectedSet, rangeStart, rangeEnd } = this.getStates();
  485. const includeRange = ['dateRange', 'dateTimeRange'].includes(type);
  486. const options = { closePanel: false };
  487. if (!multiple && !includeRange && selectedSet.size) {
  488. const selectedStr = Array.from(selectedSet)[0] as string;
  489. const selectedDate = new Date(selectedStr);
  490. const year = targetDate.getFullYear();
  491. const month = targetDate.getMonth();
  492. const fullDate = set(selectedDate, { year, month });
  493. if (disabledDate(fullDate, { rangeStart, rangeEnd })) {
  494. return;
  495. }
  496. this._adapter.notifySelectedChange([fullDate], options);
  497. }
  498. }
  499. _isMultiple() {
  500. return Boolean(this.getProp('multiple')) && this.getProp('type') === 'date';
  501. }
  502. _isRange() {
  503. // return this._adapter.getProp('type') === dateRangeTypeKey;
  504. }
  505. handleDayClick(day: MonthDayInfo, panelType: PanelType) {
  506. const type = this.getProp('type');
  507. switch (true) {
  508. case type === 'date' || type === 'dateTime':
  509. this.handleDateSelected(day, panelType);
  510. break;
  511. case type === 'dateRange' || type === 'dateTimeRange':
  512. this.handleRangeSelected(day);
  513. break;
  514. default:
  515. break;
  516. }
  517. }
  518. handleDateSelected(day: { fullDate: string; fullValidDate?: Date }, panelType: PanelType) {
  519. const { max, type, isControlledComponent, dateFnsLocale } = this.getProps();
  520. const multiple = this._isMultiple();
  521. const { selected } = this.getStates();
  522. const monthDetail = this._getPanelDetail(panelType);
  523. const newSelected = new Set(multiple ? [...selected] : []);
  524. const { fullDate } = day;
  525. const time = monthDetail.pickerDate;
  526. const dateStr = type === 'dateTime' ? this._mergeDateAndTime(fullDate, time) : fullDate;
  527. if (!multiple) {
  528. newSelected.add(dateStr);
  529. } else {
  530. if (newSelected.has(dateStr)) {
  531. newSelected.delete(dateStr);
  532. } else if (max && newSelected.size === max) {
  533. this._adapter.notifyMaxLimit();
  534. } else {
  535. newSelected.add(dateStr);
  536. }
  537. }
  538. const dateFormat = this.getValidDateFormat();
  539. // When passed to the upper layer, it is converted into a Date object to ensure that the input parameter format of initFormDefaultValue is consistent
  540. const newSelectedDates = [...newSelected].map(_dateStr => compatiableParse(_dateStr, dateFormat, undefined, dateFnsLocale));
  541. this.handleShowDateAndTime(panelType, time);
  542. if (!isControlledComponent) {
  543. // Uncontrolled components, update internal values when operating, and notify external
  544. // MonthGrid internally uses string to represent fullDate for easy rendering
  545. this._adapter.updateDaySelected(newSelected);
  546. }
  547. this._adapter.notifySelectedChange(newSelectedDates as [Date]);
  548. }
  549. handleShowDateAndTime(panelType: PanelType, pickerDate: number | Date, showDate?: Date) {
  550. const _showDate = showDate || pickerDate;
  551. this._updatePanelDetail(panelType, { showDate: _showDate, pickerDate });
  552. }
  553. /**
  554. * link date and time
  555. *
  556. * @param {Date|string} date
  557. * @param {Date|string} time
  558. * @returns {Date}
  559. */
  560. _mergeDateAndTime(date: Date | string, time: Date | string) {
  561. const dateFnsLocale = this._adapter.getProp('dateFnsLocale');
  562. const dateStr = format(
  563. isValidDate(date) ? date as Date : compatiableParse(date as string, strings.FORMAT_FULL_DATE, undefined, dateFnsLocale),
  564. strings.FORMAT_FULL_DATE
  565. );
  566. const timeStr = format(
  567. isValidDate(time) ? time as Date : compatiableParse(time as string, strings.FORMAT_TIME_PICKER, undefined, dateFnsLocale),
  568. strings.FORMAT_TIME_PICKER
  569. );
  570. const timeFormat = this.getValidTimeFormat();
  571. return compatiableParse(`${dateStr} ${timeStr}`, timeFormat, undefined, dateFnsLocale);
  572. }
  573. handleRangeSelected(day: MonthDayInfo) {
  574. let { rangeStart, rangeEnd } = this.getStates();
  575. const { startDateOffset, endDateOffset, type, dateFnsLocale, rangeInputFocus, triggerRender } = this._adapter.getProps();
  576. const { fullDate } = day;
  577. let rangeStartReset = false;
  578. let rangeEndReset = false;
  579. const isDateRangeAndHasOffset = (startDateOffset || endDateOffset) && type === 'dateRange';
  580. if (isDateRangeAndHasOffset) {
  581. rangeStart = getFullDateOffset(startDateOffset, fullDate);
  582. rangeEnd = getFullDateOffset(endDateOffset, fullDate);
  583. } else {
  584. if (rangeInputFocus === 'rangeEnd') {
  585. rangeEnd = fullDate;
  586. // rangStart Parten in dateTime: 'yyyy-MM-dd HH:MM:SS', rangeEnd parten: 'yyyy-MM-dd'
  587. if ((rangeStart && rangeEnd) && isBefore(rangeEnd, rangeStart.trim().split(/\s+/)[0])) {
  588. rangeStart = null;
  589. rangeStartReset = true;
  590. }
  591. // Compatible to select date after opening the panel without click input
  592. } else if (rangeInputFocus === 'rangeStart' || !rangeInputFocus) {
  593. rangeStart = fullDate;
  594. // rangEnd Parten in dateTime: 'yyyy-MM-dd HH:MM:SS', rangeStart parten: 'yyyy-MM-dd'
  595. if ((rangeStart && rangeEnd) && isBefore(rangeEnd.trim().split(/\s+/)[0], rangeStart)) {
  596. rangeEnd = null;
  597. rangeEndReset = true;
  598. }
  599. }
  600. }
  601. // next focus logic
  602. const isRangeType = /range/i.test(type);
  603. if (isRangeType) {
  604. if (isDateRangeAndHasOffset) {
  605. this._adapter.setRangeStart(rangeStart);
  606. this._adapter.setRangeEnd(rangeEnd);
  607. } else {
  608. if (rangeInputFocus === 'rangeEnd') {
  609. this._adapter.setRangeEnd(rangeEnd);
  610. if (rangeStartReset) {
  611. this._adapter.setRangeStart(rangeStart);
  612. }
  613. if (!this._adapter.isAnotherPanelHasOpened('rangeEnd') || !rangeStart) {
  614. this._adapter.setRangeInputFocus('rangeStart');
  615. }
  616. } else if (rangeInputFocus === 'rangeStart' || !rangeInputFocus) {
  617. this._adapter.setRangeStart(rangeStart);
  618. if (rangeEndReset) {
  619. this._adapter.setRangeEnd(rangeEnd);
  620. }
  621. if (!this._adapter.isAnotherPanelHasOpened('rangeStart') || !rangeEnd) {
  622. this._adapter.setRangeInputFocus('rangeEnd');
  623. }
  624. }
  625. }
  626. }
  627. const dateFormat = this.getValidDateFormat();
  628. // only notify when choose completed
  629. if (rangeStart || rangeEnd) {
  630. const [startDate, endDate] = [
  631. compatiableParse(rangeStart, dateFormat, undefined, dateFnsLocale),
  632. compatiableParse(rangeEnd, dateFormat, undefined, dateFnsLocale),
  633. ];
  634. let date: [Date, Date] = [startDate, endDate];
  635. // If the type is dateRangeTime, add the value of time
  636. if (type === 'dateTimeRange') {
  637. const startTime = this.getState('monthLeft').pickerDate;
  638. const endTime = this.getState('monthRight').pickerDate;
  639. const start = rangeStart ? this._mergeDateAndTime(rangeStart, startTime) : null;
  640. const end = rangeEnd ? this._mergeDateAndTime(rangeEnd, endTime) : null;
  641. if (isSameDay(startDate, endDate) && isBefore(end, start)) {
  642. date = [start, start];
  643. } else {
  644. date = [start, end];
  645. }
  646. }
  647. /**
  648. * no need to check focus then
  649. * - dateRange and isDateRangeAndHasOffset
  650. * - dateRange and triggerRender
  651. */
  652. const needCheckFocusRecord = !(type === 'dateRange' && (isDateRangeAndHasOffset || isFunction(triggerRender)));
  653. this._adapter.notifySelectedChange(date, { needCheckFocusRecord });
  654. }
  655. }
  656. _isNeedSwap(rangeStart: Date | string, rangeEnd: Date | string) {
  657. // Check whether the start and end are reasonable and whether they need to be reversed
  658. return rangeStart && rangeEnd && isBefore(rangeEnd, rangeStart);
  659. }
  660. /**
  661. * Day may be empty, this is unhover state
  662. * @param {*} day
  663. */
  664. handleDayHover(day = { fullDate: '' }, panelType?: PanelType) {
  665. const { fullDate } = day;
  666. const { startDateOffset, endDateOffset, type } = this.getProps();
  667. this._adapter.setHoverDay(fullDate);
  668. if ((startDateOffset || endDateOffset) && type === 'dateRange') {
  669. const offsetRangeStart = getFullDateOffset(startDateOffset, fullDate);
  670. const offsetRangeEnd = getFullDateOffset(endDateOffset, fullDate);
  671. this._adapter.setOffsetRangeStart(offsetRangeStart);
  672. this._adapter.setOffsetRangeEnd(offsetRangeEnd);
  673. }
  674. }
  675. // Guarantee that monthLeft, monthRight will not appear in the same month or monthLeft is greater than MonthRight
  676. _autoAdjustMonth(monthLeft: MonthInfo, monthRight: MonthInfo) {
  677. let newMonthLeft = monthLeft;
  678. let newMonthRight = monthRight;
  679. const difference = differenceInCalendarMonths(monthLeft.pickerDate, monthRight.pickerDate);
  680. if (difference > 0) {
  681. // The month on the left is larger than the month on the right, swap
  682. newMonthLeft = { ...monthRight };
  683. newMonthRight = { ...monthLeft };
  684. } else if (difference === 0) {
  685. // Around the same month, the number of months on the right + 1
  686. newMonthLeft = monthLeft;
  687. newMonthRight = { ...monthRight, pickerDate: addMonths(monthRight.pickerDate, 1) };
  688. }
  689. return { monthLeft: newMonthLeft, monthRight: newMonthRight };
  690. }
  691. getValidTimeFormat() {
  692. const formatProp = this.getProp('format') || strings.FORMAT_TIME_PICKER;
  693. const timeFormatTokens = [];
  694. if (includes(formatProp, 'h') || includes(formatProp, 'H')) {
  695. timeFormatTokens.push('HH');
  696. }
  697. if (includes(formatProp, 'm')) {
  698. timeFormatTokens.push('mm');
  699. }
  700. if (includes(formatProp, 's')) {
  701. timeFormatTokens.push('ss');
  702. }
  703. return timeFormatTokens.join(':');
  704. }
  705. getValidDateFormat() {
  706. return this.getProp('format') || getDefaultFormatToken(this.getProp('type'));
  707. }
  708. handleTimeChange(newTime: { timeStampValue: number }, panelType: PanelType) {
  709. const { rangeEnd, rangeStart } = this.getStates();
  710. const dateFnsLocale = this.getProp('dateFnsLocale');
  711. const ts = newTime.timeStampValue;
  712. const type = this.getProp('type');
  713. const panelDetail = this._getPanelDetail(panelType);
  714. const { showDate } = panelDetail;
  715. const timeDate = new Date(ts);
  716. const dateFormat = this.getValidDateFormat();
  717. const destRange = panelType === strings.PANEL_TYPE_RIGHT ? rangeEnd : rangeStart;
  718. let year,
  719. monthNo,
  720. date;
  721. // if (pickerDate && isValidDate(pickerDate)) {
  722. // year = pickerDate.getFullYear();
  723. // monthNo = pickerDate.getMonth();
  724. // date = pickerDate.getDate();
  725. // } else
  726. if (type === 'dateTimeRange' && destRange) {
  727. const rangeDate = compatiableParse(destRange, dateFormat, undefined, dateFnsLocale);
  728. year = rangeDate.getFullYear();
  729. monthNo = rangeDate.getMonth();
  730. date = rangeDate.getDate();
  731. } else {
  732. year = showDate.getFullYear();
  733. monthNo = showDate.getMonth();
  734. date = showDate.getDate();
  735. }
  736. const hours = timeDate.getHours();
  737. const minutes = timeDate.getMinutes();
  738. const seconds = timeDate.getSeconds();
  739. const milSeconds = timeDate.getMilliseconds();
  740. const dateArgs = [year, monthNo, date, hours, minutes, seconds, milSeconds] as const;
  741. const fullValidDate = new Date(...dateArgs);
  742. if (type === 'dateTimeRange') {
  743. this.handleShowDateAndTime(panelType, fullValidDate, showDate);
  744. this._updateTimeInDateRange(panelType, fullValidDate);
  745. } else {
  746. const fullDate = formatFullDate(year, monthNo + 1, date);
  747. this.handleDateSelected(
  748. {
  749. fullDate,
  750. fullValidDate,
  751. },
  752. panelType
  753. );
  754. this.handleShowDateAndTime(panelType, fullValidDate);
  755. this._adapter.notifySelectedChange([fullValidDate]);
  756. }
  757. }
  758. /**
  759. * Update the time part in the range
  760. * @param {string} panelType
  761. * @param {Date} timeDate
  762. */
  763. _updateTimeInDateRange(panelType: PanelType, timeDate: Date) {
  764. const { isControlledComponent, dateFnsLocale } = this.getProps();
  765. let rangeStart = this.getState('rangeStart');
  766. let rangeEnd = this.getState('rangeEnd');
  767. const dateFormat = this.getValidDateFormat();
  768. // TODO: Modify a time individually
  769. if (rangeStart && rangeEnd) {
  770. let startDate = compatiableParse(rangeStart, dateFormat, undefined, dateFnsLocale);
  771. let endDate = compatiableParse(rangeEnd, dateFormat, undefined, dateFnsLocale);
  772. // console.log('_updateTimeInDateRange()', rangeStart, rangeEnd, startDate, endDate);
  773. if (panelType === strings.PANEL_TYPE_RIGHT) {
  774. endDate = this._mergeDateAndTime(timeDate, timeDate);
  775. rangeEnd = format(endDate, strings.FORMAT_DATE_TIME);
  776. if (this._isNeedSwap(rangeStart, rangeEnd)) {
  777. [rangeStart, rangeEnd] = [rangeEnd, rangeStart];
  778. [startDate, endDate] = [endDate, startDate];
  779. }
  780. if (!isControlledComponent) {
  781. this._adapter.setRangeEnd(rangeEnd);
  782. }
  783. } else {
  784. startDate = this._mergeDateAndTime(timeDate, timeDate);
  785. rangeStart = format(startDate, strings.FORMAT_DATE_TIME);
  786. if (this._isNeedSwap(rangeStart, rangeEnd)) {
  787. [rangeStart, rangeEnd] = [rangeEnd, rangeStart];
  788. [startDate, endDate] = [endDate, startDate];
  789. }
  790. if (!isControlledComponent) {
  791. this._adapter.setRangeStart(rangeStart);
  792. }
  793. }
  794. // console.log('_updateTimeInDateRange()', rangeStart, rangeEnd, startDate, endDate);
  795. this._adapter.notifySelectedChange([startDate, endDate]);
  796. }
  797. }
  798. _updatePanelDetail(
  799. panelType: PanelType,
  800. kvs: {
  801. showDate?: number | Date;
  802. pickerDate?: number | Date;
  803. isTimePickerOpen?: boolean;
  804. isYearPickerOpen?: boolean;
  805. }
  806. ) {
  807. const { monthLeft, monthRight } = this.getStates();
  808. if (panelType === strings.PANEL_TYPE_RIGHT) {
  809. this._adapter.updateMonthOnRight({ ...monthRight, ...kvs });
  810. } else {
  811. this._adapter.updateMonthOnLeft({ ...monthLeft, ...kvs });
  812. }
  813. }
  814. showYearPicker(panelType: PanelType) {
  815. this._updatePanelDetail(panelType, { isTimePickerOpen: false, isYearPickerOpen: true });
  816. }
  817. showTimePicker(panelType: PanelType, opt?: boolean) {
  818. if (this.getProp('disabledTimePicker')) {
  819. return;
  820. }
  821. this._updatePanelDetail(panelType, { isTimePickerOpen: true, isYearPickerOpen: false });
  822. }
  823. showDatePanel(panelType: PanelType) {
  824. this._updatePanelDetail(panelType, { isTimePickerOpen: false, isYearPickerOpen: false });
  825. }
  826. /**
  827. * Get year and month panel open type
  828. *
  829. * It is useful info to set minHeight of weeks.
  830. * - When yam open type is 'left' or 'right', weeks minHeight should be set
  831. * If the minHeight is not set, the change of the number of weeks will cause the scrollList to be unstable
  832. */
  833. getYAMOpenType() {
  834. const { monthLeft, monthRight } = this._adapter.getStates();
  835. const leftYearPickerOpen = monthLeft.isYearPickerOpen;
  836. const rightYearPickerOpen = monthRight.isYearPickerOpen;
  837. if (leftYearPickerOpen && rightYearPickerOpen) {
  838. return 'both';
  839. } else if (leftYearPickerOpen) {
  840. return 'left';
  841. } else if (rightYearPickerOpen) {
  842. return 'right';
  843. } else {
  844. return 'none';
  845. }
  846. }
  847. }