monthsGridFoundation.ts 39 KB

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