foundation.ts 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124
  1. /* eslint-disable no-nested-ternary */
  2. /* eslint-disable max-len, max-depth, */
  3. import { format, isValid, isSameSecond, isEqual as isDateEqual, isDate } from 'date-fns';
  4. import { get, isObject, isString, isEqual } from 'lodash';
  5. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  6. import { isValidDate, isTimestamp } from './_utils/index';
  7. import isNullOrUndefined from '../utils/isNullOrUndefined';
  8. import { utcToZonedTime, zonedTimeToUtc } from '../utils/date-fns-extra';
  9. import { compatiableParse } from './_utils/parser';
  10. import { getDefaultFormatTokenByType } from './_utils/getDefaultFormatToken';
  11. import { strings } from './constants';
  12. import { strings as inputStrings } from '../input/constants';
  13. import { Type, DateInputFoundationProps } from './inputFoundation';
  14. import { MonthsGridFoundationProps } from './monthsGridFoundation';
  15. import { WeekStartNumber } from './_utils/getMonthTable';
  16. import { ArrayElement, Motion } from '../utils/type';
  17. export type ValidateStatus = ArrayElement<typeof strings.STATUS>;
  18. export type InputSize = ArrayElement<typeof strings.SIZE_SET>;
  19. export type Position = ArrayElement<typeof strings.POSITION_SET>;
  20. export type BaseValueType = string | number | Date;
  21. export type DayStatusType = {
  22. isToday?: boolean; // Current day
  23. isSelected?: boolean; // Selected
  24. isDisabled?: boolean; // Disabled
  25. isSelectedStart?: boolean; // Select Start
  26. isSelectedEnd?: boolean; // End of selection
  27. isInRange?: boolean; // Range within the selected date
  28. isHover?: boolean; // Date between selection and hover date
  29. isOffsetRangeStart?: boolean; // Week selection start
  30. isOffsetRangeEnd?: boolean; // End of week selection
  31. isHoverInOffsetRange?: boolean; // Hover in the week selection
  32. };
  33. export type DisabledDateOptions = {
  34. rangeStart?: string;
  35. rangeEnd?: string;
  36. };
  37. export type PresetType = {
  38. start?: string | Date | number;
  39. end?: string | Date | number;
  40. text?: string;
  41. };
  42. export type TriggerRenderProps = {
  43. [x: string]: any;
  44. value?: ValueType;
  45. inputValue?: string;
  46. placeholder?: string | string[];
  47. autoFocus?: boolean;
  48. size?: InputSize;
  49. disabled?: boolean;
  50. inputReadOnly?: boolean;
  51. componentProps?: DatePickerFoundationProps;
  52. };
  53. export type DateOffsetType = (selectedDate?: Date) => Date;
  54. export type DensityType = 'default' | 'compact';
  55. export type DisabledDateType = (date?: Date, options?: DisabledDateOptions) => boolean;
  56. export type DisabledTimeType = (date?: Date | Date[], panelType?: string) => ({
  57. disabledHours?: () => number[];
  58. disabledMinutes?: (hour: number) => number[];
  59. disabledSeconds?: (hour: number, minute: number) => number[];
  60. });
  61. export type OnCancelType = (date: Date | Date[], dateStr: string | string[]) => void;
  62. export type OnPanelChangeType = (date: Date | Date[], dateStr: string | string[]) => void;
  63. export type OnChangeType = (date?: Date | Date[] | string | string[], dateStr?: string | string[] | Date | Date[]) => void;
  64. export type OnConfirmType = (date: Date | Date[], dateStr: string | string[]) => void;
  65. // type OnPresetClickType = (item: PresetType, e: React.MouseEvent<HTMLDivElement>) => void;
  66. export type OnPresetClickType = (item: PresetType, e: any) => void;
  67. export type PresetsType = Array<PresetType | (() => PresetType)>;
  68. // type RenderDateType = (dayNumber?: number, fullDate?: string) => React.ReactNode;
  69. export type RenderDateType = (dayNumber?: number, fullDate?: string) => any;
  70. // type RenderFullDateType = (dayNumber?: number, fullDate?: string, dayStatus?: DayStatusType) => React.ReactNode;
  71. export type RenderFullDateType = (dayNumber?: number, fullDate?: string, dayStatus?: DayStatusType) => any;
  72. // type TriggerRenderType = (props: TriggerRenderProps) => React.ReactNode;
  73. export type TriggerRenderType = (props: TriggerRenderProps) => any;
  74. export type ValueType = BaseValueType | BaseValueType[];
  75. export interface ElementProps {
  76. bottomSlot?: any;
  77. insetLabel?: any;
  78. prefix?: any;
  79. topSlot?: any;
  80. }
  81. export interface RenderProps {
  82. renderDate?: RenderDateType;
  83. renderFullDate?: RenderFullDateType;
  84. triggerRender?: TriggerRenderType;
  85. }
  86. export interface EventHandlerProps {
  87. onCancel?: OnCancelType;
  88. onChange?: OnChangeType;
  89. onOpenChange?: (status: boolean) => void;
  90. onPanelChange?: OnPanelChangeType;
  91. onConfirm?: OnConfirmType;
  92. // properties below need overwrite
  93. // onBlur?: React.MouseEventHandler<HTMLInputElement>;
  94. onBlur?: (e: any) => void;
  95. // onClear?: React.MouseEventHandler<HTMLDivElement>;
  96. onClear?: (e: any) => void;
  97. // onFocus?: React.MouseEventHandler<HTMLInputElement>;
  98. onFocus?: (e: any, rangType: 'rangeStart' | 'rangeEnd') => void;
  99. onPresetClick?: OnPresetClickType;
  100. }
  101. export interface DatePickerFoundationProps extends ElementProps, RenderProps, EventHandlerProps {
  102. autoAdjustOverflow?: boolean;
  103. autoFocus?: boolean;
  104. autoSwitchDate?: boolean;
  105. className?: string;
  106. defaultOpen?: boolean;
  107. defaultPickerValue?: ValueType;
  108. defaultValue?: ValueType;
  109. density?: DensityType;
  110. disabled?: boolean;
  111. disabledDate?: DisabledDateType;
  112. disabledTime?: DisabledTimeType;
  113. dropdownClassName?: string;
  114. dropdownStyle?: React.CSSProperties;
  115. endDateOffset?: DateOffsetType;
  116. format?: string;
  117. getPopupContainer?: () => HTMLElement;
  118. inputReadOnly?: boolean;
  119. inputStyle?: React.CSSProperties;
  120. max?: number;
  121. motion?: Motion;
  122. multiple?: boolean;
  123. needConfirm?: boolean;
  124. onChangeWithDateFirst?: boolean;
  125. open?: boolean;
  126. placeholder?: string | string[];
  127. position?: Position;
  128. prefixCls?: string;
  129. presets?: PresetsType;
  130. showClear?: boolean;
  131. size?: InputSize;
  132. spacing?: number;
  133. startDateOffset?: DateOffsetType;
  134. stopPropagation?: boolean | string;
  135. style?: React.CSSProperties;
  136. timePickerOpts?: any; // TODO import timePicker props
  137. timeZone?: string | number;
  138. type?: Type;
  139. validateStatus?: ValidateStatus;
  140. value?: ValueType;
  141. weekStartsOn?: WeekStartNumber;
  142. zIndex?: number;
  143. syncSwitchMonth?: boolean;
  144. hideDisabledOptions?: MonthsGridFoundationProps['hideDisabledOptions'];
  145. disabledTimePicker?: MonthsGridFoundationProps['disabledTimePicker'];
  146. locale?: any;
  147. dateFnsLocale?: any;
  148. localeCode?: string;
  149. rangeSeparator?: string;
  150. }
  151. export interface DatePickerFoundationState {
  152. panelShow: boolean;
  153. isRange: boolean;
  154. inputValue: string;
  155. value: Date[];
  156. cachedSelectedValue: Date[];
  157. prevTimeZone: string | number;
  158. motionEnd: boolean;
  159. rangeInputFocus: 'rangeStart' | 'rangeEnd' | boolean;
  160. autofocus: boolean;
  161. }
  162. export { Type, DateInputFoundationProps };
  163. export interface DatePickerAdapter extends DefaultAdapter<DatePickerFoundationProps, DatePickerFoundationState> {
  164. togglePanel: (panelShow: boolean) => void;
  165. registerClickOutSide: () => void;
  166. unregisterClickOutSide: () => void;
  167. notifyBlur: DatePickerFoundationProps['onBlur'];
  168. notifyFocus: DatePickerFoundationProps['onFocus'];
  169. notifyClear: DatePickerFoundationProps['onClear'];
  170. notifyChange: DatePickerFoundationProps['onChange'];
  171. notifyCancel: DatePickerFoundationProps['onCancel'];
  172. notifyConfirm: DatePickerFoundationProps['onConfirm'];
  173. notifyOpenChange: DatePickerFoundationProps['onOpenChange'];
  174. notifyPresetsClick: DatePickerFoundationProps['onPresetClick'];
  175. updateValue: (value: Date[]) => void;
  176. updatePrevTimezone: (prevTimeZone: string | number) => void;
  177. updateCachedSelectedValue: (cachedSelectedValue: Date[]) => void;
  178. updateInputValue: (inputValue: string) => void;
  179. needConfirm: () => boolean;
  180. typeIsYearOrMonth: () => boolean;
  181. setMotionEnd: (motionEnd: boolean) => void;
  182. setRangeInputFocus: (rangeInputFocus: DatePickerFoundationState['rangeInputFocus']) => void;
  183. couldPanelClosed: () => boolean;
  184. isEventTarget: (e: any) => boolean;
  185. }
  186. /**
  187. * The datePicker foundation.js is responsible for maintaining the date value and the input box value, as well as the callback of both
  188. * task 1. Accept the selected date change, update the date value, and update the input box value according to the date = > Notify the change
  189. * task 2. When the input box changes, update the date value = > Notify the change
  190. */
  191. export default class DatePickerFoundation extends BaseFoundation<DatePickerAdapter> {
  192. constructor(adapter: DatePickerAdapter) {
  193. super({ ...adapter });
  194. }
  195. init() {
  196. const timeZone = this.getProp('timeZone');
  197. if (this._isControlledComponent()) {
  198. this.initFromProps({ timeZone, value: this.getProp('value') });
  199. } else if (this._isInProps('defaultValue')) {
  200. this.initFromProps({ timeZone, value: this.getProp('defaultValue') });
  201. }
  202. this.initPanelOpenStatus(this.getProp('defaultOpen'));
  203. }
  204. isValidTimeZone(timeZone?: string | number) {
  205. const propTimeZone = this.getProp('timeZone');
  206. const _timeZone = isNullOrUndefined(timeZone) ? propTimeZone : timeZone;
  207. return ['string', 'number'].includes(typeof _timeZone) && _timeZone !== '';
  208. }
  209. initFromProps({ value, timeZone, prevTimeZone }: Pick<DatePickerFoundationProps, 'value' | 'timeZone'> & { prevTimeZone?: string | number }) {
  210. const _value = (Array.isArray(value) ? [...value] : (value || value === 0) && [value]) || [];
  211. const result = this.parseWithTimezone(_value, timeZone, prevTimeZone);
  212. this._adapter.updatePrevTimezone(prevTimeZone);
  213. this._adapter.updateInputValue(null);
  214. this._adapter.updateValue(result);
  215. if (this._adapter.needConfirm()) {
  216. this._adapter.updateCachedSelectedValue(result);
  217. }
  218. }
  219. parseWithTimezone(value: ValueType, timeZone: string | number, prevTimeZone: string | number) {
  220. const result: Date[] = [];
  221. if (Array.isArray(value) && value.length) {
  222. for (const v of value) {
  223. let parsedV = (v || v === 0) && this._parseValue(v);
  224. if (parsedV) {
  225. if (this.isValidTimeZone(prevTimeZone)) {
  226. parsedV = zonedTimeToUtc(parsedV, prevTimeZone as string);
  227. }
  228. result.push(this.isValidTimeZone(timeZone) ? utcToZonedTime(parsedV, timeZone as string) : parsedV);
  229. }
  230. }
  231. }
  232. return result;
  233. }
  234. _isMultiple() {
  235. return Boolean(this.getProp('multiple'));
  236. }
  237. /**
  238. *
  239. * Verify and parse the following three format inputs
  240. *
  241. 1. Date object
  242. 2. ISO 9601-compliant string
  243. 3. ts timestamp
  244. Unified here to format the incoming value and output it as a Date object
  245. *
  246. */
  247. _parseValue(value: BaseValueType): Date {
  248. const dateFnsLocale = this._adapter.getProp('dateFnsLocale');
  249. let dateObj: Date;
  250. if (!value && value !== 0) {
  251. return new Date();
  252. }
  253. if (isValidDate(value)) {
  254. dateObj = value as Date;
  255. } else if (isString(value)) {
  256. dateObj = compatiableParse(value as string, this.getProp('format'), undefined, dateFnsLocale);
  257. } else if (isTimestamp(value)) {
  258. dateObj = new Date(value);
  259. } else {
  260. throw new TypeError('defaultValue should be valid Date object/timestamp or string');
  261. }
  262. return dateObj;
  263. }
  264. destroy() {
  265. // Ensure that event listeners will be uninstalled and users may not trigger closePanel
  266. // this._adapter.togglePanel(false);
  267. this._adapter.unregisterClickOutSide();
  268. }
  269. initPanelOpenStatus(defaultOpen?: boolean) {
  270. if ((this.getProp('open') || defaultOpen) && !this.getProp('disabled')) {
  271. this._adapter.togglePanel(true);
  272. this._adapter.registerClickOutSide();
  273. } else {
  274. this._adapter.togglePanel(false);
  275. this._adapter.unregisterClickOutSide();
  276. }
  277. }
  278. openPanel() {
  279. if (!this.getProp('disabled')) {
  280. if (!this._isControlledComponent('open')) {
  281. this._adapter.togglePanel(true);
  282. this._adapter.registerClickOutSide();
  283. }
  284. this._adapter.notifyOpenChange(true);
  285. }
  286. }
  287. /**
  288. * do these side effects when type is dateRange or dateTimeRange
  289. * 1. trigger input blur, if input value is invalid, set input value and state value to previous status
  290. * 2. set cachedSelectedValue using given dates(in needConfirm mode)
  291. * - directly closePanel without click confirm will set cachedSelectedValue to state value
  292. * - select one date(which means that the selection value is incomplete) and click confirm also set cachedSelectedValue to state value
  293. */
  294. rangeTypeSideEffectsWhenClosePanel(inputValue: string, willUpdateDates: Date[]) {
  295. if (this._isRangeType()) {
  296. this._adapter.setRangeInputFocus(false);
  297. /**
  298. * inputValue is string when it is not disabled or can't parsed
  299. * when inputValue is null, picker value will back to last selected value
  300. */
  301. this.handleInputBlur(inputValue);
  302. this.resetCachedSelectedValue(willUpdateDates);
  303. }
  304. }
  305. /**
  306. * clear input value when selected date is not confirmed
  307. */
  308. needConfirmSideEffectsWhenClosePanel(willUpdateDates: Date[] | null | undefined) {
  309. if (this._adapter.needConfirm() && !this._isRangeType()) {
  310. /**
  311. * if `null` input element will show `cachedSelectedValue` formatted value(format in DateInput render)
  312. * if `` input element will show `` directly
  313. */
  314. this._adapter.updateInputValue(null);
  315. this.resetCachedSelectedValue(willUpdateDates);
  316. }
  317. }
  318. resetCachedSelectedValue(willUpdateDates?: Date[]) {
  319. const { value, cachedSelectedValue } = this._adapter.getStates();
  320. const newCachedSelectedValue = Array.isArray(willUpdateDates) ? willUpdateDates : value;
  321. if (!isEqual(newCachedSelectedValue, cachedSelectedValue)) {
  322. this._adapter.updateCachedSelectedValue(newCachedSelectedValue);
  323. }
  324. }
  325. /**
  326. * timing to call closePanel
  327. * 1. click confirm button
  328. * 2. click cancel button
  329. * 3. select date, time, year, month
  330. * - date type and not multiple, close panel after select date
  331. * - dateRange type, close panel after select rangeStart and rangeEnd
  332. * 4. click outside
  333. * @param {Event} e
  334. * @param {String} inputValue
  335. * @param {Date[]} dates
  336. */
  337. closePanel(e?: any, inputValue: string = null, dates?: Date[]) {
  338. const { value, cachedSelectedValue } = this._adapter.getStates();
  339. const willUpdateDates = isNullOrUndefined(dates) ? this._adapter.needConfirm() ? value : cachedSelectedValue : dates;
  340. if (!this._isControlledComponent('open')) {
  341. this._adapter.togglePanel(false);
  342. this._adapter.unregisterClickOutSide();
  343. }
  344. // range type picker, closing panel requires the following side effects
  345. this.rangeTypeSideEffectsWhenClosePanel(inputValue, willUpdateDates as Date[]);
  346. this.needConfirmSideEffectsWhenClosePanel(willUpdateDates as Date[]);
  347. this._adapter.notifyOpenChange(false);
  348. this._adapter.notifyBlur(e);
  349. }
  350. /**
  351. * clear range input focus when open is controlled
  352. * fixed github 1375
  353. */
  354. clearRangeInputFocus = () => {
  355. const { type } = this._adapter.getProps();
  356. const { rangeInputFocus } = this._adapter.getStates();
  357. if (type === 'dateTimeRange' && rangeInputFocus) {
  358. this._adapter.setRangeInputFocus(false);
  359. }
  360. }
  361. /**
  362. * Callback when the content of the input box changes
  363. * Update the date panel if the changed value is a legal date, otherwise only update the input box
  364. * @param {String} input The value of the input box after the change
  365. * @param {Event} e
  366. */
  367. handleInputChange(input: string, e: any) {
  368. const result = this._isMultiple() ? this.parseMultipleInput(input) : this.parseInput(input);
  369. const { value: stateValue } = this.getStates();
  370. // Enter a valid date or empty
  371. if ((result && result.length) || input === '') {
  372. // If you click the clear button
  373. if (get(e, inputStrings.CLEARBTN_CLICKED_EVENT_FLAG) && this._isControlledComponent('value')) {
  374. this._notifyChange(result);
  375. return;
  376. }
  377. this._updateValueAndInput(result, input === '', input);
  378. // Updates the selected value when entering a valid date
  379. const changedDates = this._getChangedDates(result);
  380. if (!this._someDateDisabled(changedDates)) {
  381. if (this._adapter.needConfirm()) {
  382. this._adapter.updateCachedSelectedValue(result);
  383. }
  384. if (!isEqual(result, stateValue)) {
  385. this._notifyChange(result);
  386. }
  387. }
  388. } else {
  389. this._adapter.updateInputValue(input);
  390. }
  391. }
  392. /**
  393. * Input box blur
  394. * @param {String} input
  395. * @param {Event} e
  396. */
  397. handleInputBlur(input = '', e?: any) {
  398. const parsedResult = input ?
  399. this._isMultiple() ?
  400. this.parseMultipleInput(input, ',', true) :
  401. this.parseInput(input) :
  402. [];
  403. const stateValue = this.getState('value');
  404. // console.log(input, parsedResult);
  405. if (parsedResult && parsedResult.length) {
  406. this._updateValueAndInput(parsedResult, input === '');
  407. } else if (input === '') {
  408. // if clear input, set input to `''`
  409. this._updateValueAndInput('' as any, true, '');
  410. } else {
  411. this._updateValueAndInput(stateValue);
  412. }
  413. }
  414. /**
  415. * called when range type rangeEnd input tab press
  416. * @param {Event} e
  417. */
  418. handleRangeEndTabPress(e: any) {
  419. this._adapter.setRangeInputFocus(false);
  420. }
  421. /**
  422. * called when the input box is focused
  423. * @param {Event} e input focus event
  424. * @param {String} range 'rangeStart' or 'rangeEnd', use when type is range
  425. */
  426. handleInputFocus(e: any, range: 'rangeStart' | 'rangeEnd') {
  427. const rangeInputFocus = this._adapter.getState('rangeInputFocus');
  428. range && this._adapter.setRangeInputFocus(range);
  429. /**
  430. * rangeType: only notify when range is false
  431. * not rangeType: notify when focus
  432. */
  433. if (!range || !['rangeStart', 'rangeEnd'].includes(rangeInputFocus)) {
  434. this._adapter.notifyFocus(e, range);
  435. }
  436. }
  437. handleSetRangeFocus(rangeInputFocus: boolean | 'rangeStart' | 'rangeEnd') {
  438. this._adapter.setRangeInputFocus(rangeInputFocus);
  439. }
  440. handleInputClear(e: any) {
  441. this._adapter.notifyClear(e);
  442. }
  443. /**
  444. * 范围选择清除按钮回调
  445. * 因为清除按钮没有集成在Input内,因此需要手动清除 value、inputValue、cachedValue
  446. *
  447. * callback of range input clear button
  448. * Since the clear button is not integrated in Input, you need to manually clear value, inputValue, cachedValue
  449. */
  450. handleRangeInputClear(e: any) {
  451. const value: Date[] = [];
  452. const inputValue = '';
  453. if (!this._isControlledComponent('value')) {
  454. this._updateValueAndInput(value, true, inputValue);
  455. if (this._adapter.needConfirm()) {
  456. this._adapter.updateCachedSelectedValue(value);
  457. }
  458. }
  459. this._notifyChange(value);
  460. this._adapter.notifyClear(e);
  461. }
  462. // eslint-disable-next-line @typescript-eslint/no-empty-function
  463. handleRangeInputBlur(value: any, e: any) {
  464. }
  465. // Parses input only after user returns
  466. handleInputComplete(input: any = '') {
  467. // console.log(input);
  468. let parsedResult = input ?
  469. this._isMultiple() ?
  470. this.parseMultipleInput(input, ',', true) :
  471. this.parseInput(input) :
  472. [];
  473. parsedResult = parsedResult && parsedResult.length ? parsedResult : this.getState('value');
  474. // Use the current date as the value when the current input is empty and the last input is also empty
  475. if (!parsedResult || !parsedResult.length) {
  476. const nowDate = new Date();
  477. if (this._isRangeType()) {
  478. parsedResult = [nowDate, nowDate];
  479. } else {
  480. parsedResult = [nowDate];
  481. }
  482. }
  483. this._updateValueAndInput(parsedResult);
  484. const { value: stateValue } = this.getStates();
  485. const changedDates = this._getChangedDates(parsedResult);
  486. if (!this._someDateDisabled(changedDates) && !isEqual(parsedResult, stateValue)) {
  487. this._notifyChange(parsedResult);
  488. }
  489. }
  490. /**
  491. * Parse the input, return the time object if it is valid,
  492. * otherwise return "
  493. *
  494. * @param {string} input
  495. * @returns {Date [] | '}
  496. */
  497. parseInput(input = '') {
  498. let result: Date[] = [];
  499. // console.log(input);
  500. const { dateFnsLocale, rangeSeparator } = this.getProps();
  501. if (input && input.length) {
  502. const type = this.getProp('type');
  503. const formatToken = this.getProp('format') || getDefaultFormatTokenByType(type);
  504. let parsedResult,
  505. formatedInput;
  506. const nowDate = new Date();
  507. switch (type) {
  508. case 'date':
  509. case 'dateTime':
  510. case 'month':
  511. parsedResult = input ? compatiableParse(input, formatToken, nowDate, dateFnsLocale) : '';
  512. formatedInput = parsedResult && isValid(parsedResult) && this.localeFormat(parsedResult as Date, formatToken);
  513. if (parsedResult && formatedInput === input) {
  514. result = [parsedResult as Date];
  515. }
  516. break;
  517. case 'dateRange':
  518. case 'dateTimeRange':
  519. const separator = rangeSeparator;
  520. const values = input.split(separator);
  521. parsedResult =
  522. values &&
  523. values.reduce((arr, cur) => {
  524. const parsedVal = cur && compatiableParse(cur, formatToken, nowDate, dateFnsLocale);
  525. parsedVal && arr.push(parsedVal);
  526. return arr;
  527. }, []);
  528. formatedInput =
  529. parsedResult &&
  530. parsedResult.map(v => v && isValid(v) && this.localeFormat(v, formatToken)).join(separator);
  531. if (parsedResult && formatedInput === input) {
  532. parsedResult.sort((d1, d2) => d1.getTime() - d2.getTime());
  533. result = parsedResult;
  534. }
  535. break;
  536. default:
  537. break;
  538. }
  539. }
  540. return result;
  541. }
  542. /**
  543. * Parses the input when multiple is true, if valid,
  544. * returns a list of time objects, otherwise returns an array
  545. *
  546. * @param {string} [input='']
  547. * @param {string} [separator=',']
  548. * @param {boolean} [needDedupe=false]
  549. * @returns {Date[]}
  550. */
  551. parseMultipleInput(input = '', separator: string = strings.DEFAULT_SEPARATOR_MULTIPLE, needDedupe = false) {
  552. const max = this.getProp('max');
  553. const inputArr = input.split(separator);
  554. const result: Date[] = [];
  555. for (const curInput of inputArr) {
  556. let tmpParsed = curInput && this.parseInput(curInput);
  557. tmpParsed = Array.isArray(tmpParsed) ? tmpParsed : tmpParsed && [tmpParsed];
  558. if (tmpParsed && tmpParsed.length) {
  559. if (needDedupe) {
  560. // 20190519 TODO: needs to determine the case where multiple is true and range
  561. !result.filter(r => Boolean(tmpParsed.find(tp => isSameSecond(r, tp)))) && result.push(...tmpParsed);
  562. } else {
  563. result.push(...tmpParsed);
  564. }
  565. } else {
  566. return [];
  567. }
  568. if (max && max > 0 && result.length > max) {
  569. return [];
  570. }
  571. }
  572. return result;
  573. }
  574. /**
  575. * dates[] => string
  576. *
  577. * @param {Date[]} dates
  578. * @returns {string}
  579. */
  580. formatDates(dates: Date[] = []) {
  581. let str = '';
  582. const rangeSeparator = this.getProp('rangeSeparator');
  583. if (Array.isArray(dates) && dates.length) {
  584. const type = this.getProp('type');
  585. const formatToken = this.getProp('format') || getDefaultFormatTokenByType(type);
  586. switch (type) {
  587. case 'date':
  588. case 'dateTime':
  589. case 'month':
  590. str = this.localeFormat(dates[0], formatToken);
  591. break;
  592. case 'dateRange':
  593. case 'dateTimeRange':
  594. const startIsTruthy = !isNullOrUndefined(dates[0]);
  595. const endIsTruthy = !isNullOrUndefined(dates[1]);
  596. if (startIsTruthy && endIsTruthy) {
  597. str = `${this.localeFormat(dates[0], formatToken)}${rangeSeparator}${this.localeFormat(dates[1], formatToken)}`;
  598. } else {
  599. if (startIsTruthy) {
  600. str = `${this.localeFormat(dates[0], formatToken)}${rangeSeparator}`;
  601. } else if (endIsTruthy) {
  602. str = `${rangeSeparator}${this.localeFormat(dates[1], formatToken)}`;
  603. }
  604. }
  605. break;
  606. default:
  607. break;
  608. }
  609. }
  610. return str;
  611. }
  612. /**
  613. * dates[] => string
  614. *
  615. * @param {Date[]} dates
  616. * @returns {string}
  617. */
  618. formatMultipleDates(dates: Date[] = [], separator: string = strings.DEFAULT_SEPARATOR_MULTIPLE) {
  619. const strs = [];
  620. if (Array.isArray(dates) && dates.length) {
  621. const type = this.getProp('type');
  622. switch (type) {
  623. case 'date':
  624. case 'dateTime':
  625. case 'month':
  626. dates.forEach(date => strs.push(this.formatDates([date])));
  627. break;
  628. case 'dateRange':
  629. case 'dateTimeRange':
  630. for (let i = 0; i < dates.length; i += 2) {
  631. strs.push(this.formatDates(dates.slice(i, i + 2)));
  632. }
  633. break;
  634. default:
  635. break;
  636. }
  637. }
  638. return strs.join(separator);
  639. }
  640. /**
  641. * Update date value and the value of the input box
  642. * 1. Select Update
  643. * 2. Input Update
  644. * @param {Date|''} value
  645. * @param {Boolean} forceUpdateValue
  646. * @param {String} input
  647. */
  648. _updateValueAndInput(value: Date | Array<Date>, forceUpdateValue?: boolean, input?: string) {
  649. let _value: Array<Date>;
  650. if (forceUpdateValue || value) {
  651. if (!Array.isArray(value)) {
  652. _value = value ? [value] : [];
  653. } else {
  654. _value = value;
  655. }
  656. const changedDates = this._getChangedDates(_value);
  657. // You cannot update the value directly when needConfirm, you can only change the value through handleConfirm
  658. if (!this._isControlledComponent() && !this._someDateDisabled(changedDates) && !this._adapter.needConfirm()) {
  659. this._adapter.updateValue(_value);
  660. }
  661. }
  662. this._adapter.updateInputValue(input);
  663. }
  664. /**
  665. * when changing the selected value through the date panel
  666. * @param {*} value
  667. * @param {*} options
  668. */
  669. handleSelectedChange(value: Date[], options?: { fromPreset?: boolean; needCheckFocusRecord?: boolean }) {
  670. const type = this.getProp('type');
  671. const { value: stateValue } = this.getStates();
  672. const controlled = this._isControlledComponent();
  673. const fromPreset = isObject(options) ? options.fromPreset : options;
  674. const closePanel = get(options, 'closePanel', true);
  675. /**
  676. * It is used to determine whether the panel can be stowed. In a Range type component, it is necessary to select both starting Time and endTime before stowing.
  677. * To determine whether both starting Time and endTime have been selected, it is used to judge whether the two inputs have been Focused.
  678. * This variable is used to indicate whether such a judgment is required. In the scene with shortcut operations, it is not required.
  679. */
  680. const needCheckFocusRecord = get(options, 'needCheckFocusRecord', true);
  681. if (this._adapter.needConfirm()) {
  682. this._adapter.updateCachedSelectedValue(value);
  683. }
  684. const dates = Array.isArray(value) ? [...value] : value ? [value] : [];
  685. const changedDates = this._getChangedDates(dates);
  686. let inputValue;
  687. if (!this._someDateDisabled(changedDates)) {
  688. inputValue = this._isMultiple() ? this.formatMultipleDates(dates) : this.formatDates(dates);
  689. const isRangeTypeAndInputIncomplete = this._isRangeType() && !this._isRangeValueComplete(dates);
  690. /**
  691. * If the input is incomplete when under control, the notifyChange is not triggered because
  692. * You need to update the value of the input box, otherwise there will be a problem that a date is selected but the input box does not show the date #1357
  693. *
  694. * 受控时如果输入不完整,由于没有触发 notifyChange
  695. * 需要组件内更新一下输入框的值,否则会出现选了一个日期但是输入框没有回显日期的问题 #1357
  696. */
  697. if (!this._adapter.needConfirm() || fromPreset) {
  698. if (isRangeTypeAndInputIncomplete) {
  699. // do not change value when selected value is incomplete
  700. this._adapter.updateInputValue(inputValue);
  701. return;
  702. } else {
  703. (!controlled || fromPreset) && this._updateValueAndInput(dates, true, inputValue);
  704. }
  705. }
  706. if (!controlled && this._adapter.needConfirm()) {
  707. // select date only change inputValue when needConfirm is true
  708. this._adapter.updateInputValue(inputValue);
  709. // if inputValue is not complete, don't notifyChange
  710. if (isRangeTypeAndInputIncomplete) {
  711. return;
  712. }
  713. }
  714. if (!isEqual(value, stateValue)) {
  715. this._notifyChange(value);
  716. }
  717. }
  718. const focusRecordChecked = !needCheckFocusRecord || (needCheckFocusRecord && this._adapter.couldPanelClosed());
  719. if ((type === 'date' && !this._isMultiple() && closePanel) || (type === 'dateRange' && this._isRangeValueComplete(dates) && closePanel && focusRecordChecked)) {
  720. this.closePanel(undefined, inputValue, dates);
  721. }
  722. }
  723. /**
  724. * when changing the year and month through the panel when the type is year or month
  725. * @param {*} item
  726. */
  727. handleYMSelectedChange(item: { currentMonth?: number; currentYear?: number } = {}) {
  728. // console.log(item);
  729. const { currentMonth, currentYear } = item;
  730. if (typeof currentMonth === 'number' && typeof currentYear === 'number') {
  731. // Strings with only dates (e.g. "1970-01-01") will be treated as UTC instead of local time #1460
  732. const date = new Date(currentYear, currentMonth - 1);
  733. this.handleSelectedChange([date]);
  734. }
  735. }
  736. handleConfirm() {
  737. const { cachedSelectedValue, value } = this.getStates();
  738. const isRangeValueComplete = this._isRangeValueComplete(cachedSelectedValue);
  739. const newValue = isRangeValueComplete ? cachedSelectedValue : value;
  740. if (this._adapter.needConfirm() && !this._isControlledComponent()) {
  741. this._adapter.updateValue(newValue);
  742. }
  743. // If the input is incomplete, the legal date of the last input is used
  744. this.closePanel(undefined, undefined, newValue);
  745. if (isRangeValueComplete) {
  746. const { notifyValue, notifyDate } = this.disposeCallbackArgs(cachedSelectedValue);
  747. this._adapter.notifyConfirm(notifyDate, notifyValue);
  748. }
  749. }
  750. handleCancel() {
  751. this.closePanel();
  752. const value = this.getState('value');
  753. const { notifyValue, notifyDate } = this.disposeCallbackArgs(value);
  754. this._adapter.notifyCancel(notifyDate, notifyValue);
  755. }
  756. handlePresetClick(item: PresetType, e: any) {
  757. const { type, timeZone } = this.getProps();
  758. const prevTimeZone = this.getState('prevTimezone');
  759. let value;
  760. switch (type) {
  761. case 'month':
  762. case 'dateTime':
  763. case 'date':
  764. value = this.parseWithTimezone([item.start], timeZone, prevTimeZone);
  765. this.handleSelectedChange(value);
  766. break;
  767. case 'dateTimeRange':
  768. case 'dateRange':
  769. value = this.parseWithTimezone([item.start, item.end], timeZone, prevTimeZone);
  770. this.handleSelectedChange(value, { needCheckFocusRecord: false });
  771. break;
  772. default:
  773. break;
  774. }
  775. this._adapter.notifyPresetsClick(item, e);
  776. }
  777. /**
  778. * 根据 type 处理 onChange 返回的参数
  779. *
  780. * - 返回的日期需要把用户时间转换为设置的时区时间
  781. * - 用户时间:用户计算机系统时间
  782. * - 时区时间:通过 ConfigProvider 设置的 timeZone
  783. * - 例子:用户设置时区为+9,计算机所在时区为+8区,然后用户选择了22:00
  784. * - DatePicker 内部保存日期 state 为 +8 的 22:00 => a = new Date("2021-05-25 22:00:00")
  785. * - 传出去时,需要把 +8 的 22:00 => +9 的 22:00 => b = zonedTimeToUtc(a, "+09:00");
  786. *
  787. * According to the type processing onChange returned parameters
  788. *
  789. * - the returned date needs to convert the user time to the set time zone time
  790. * - user time: user computer system time
  791. * - time zone time: timeZone set by ConfigProvider
  792. * - example: the user sets the time zone to + 9, the computer's time zone is + 8 zone, and then the user selects 22:00
  793. * - DatePicker internal save date state is + 8 22:00 = > a = new Date ("2021-05-25 22:00:00")
  794. * - when passed out, you need to + 8 22:00 = > + 9 22:00 = > b = zonedTimeToUtc (a, "+ 09:00");
  795. *
  796. * e.g.
  797. * let a = new Date ("2021-05-25 22:00:00");
  798. * = > Tue May 25 2021 22:00:00 GMT + 0800 (China Standard Time)
  799. * let b = zonedTimeToUtc (a, "+ 09:00");
  800. * = > Tue May 25 2021 21:00:00 GMT + 0800 (China Standard Time)
  801. *
  802. * @param {Date|Date[]} value
  803. * @return {{ notifyDate: Date|Date[], notifyValue: string|string[]}}
  804. */
  805. disposeCallbackArgs(value: Date | Date[]) {
  806. let _value = Array.isArray(value) ? value : (value && [value]) || [];
  807. if (this.isValidTimeZone()) {
  808. const timeZone = this.getProp('timeZone');
  809. _value = _value.map(date => zonedTimeToUtc(date, timeZone));
  810. }
  811. const type = this.getProp('type');
  812. const formatToken = this.getProp('format') || getDefaultFormatTokenByType(type);
  813. let notifyValue,
  814. notifyDate;
  815. switch (type) {
  816. case 'date':
  817. case 'dateTime':
  818. case 'month':
  819. if (!this._isMultiple()) {
  820. notifyValue = _value[0] && this.localeFormat(_value[0], formatToken);
  821. [notifyDate] = _value;
  822. } else {
  823. notifyValue = _value.map(v => v && this.localeFormat(v, formatToken));
  824. notifyDate = [..._value];
  825. }
  826. break;
  827. case 'dateRange':
  828. case 'dateTimeRange':
  829. notifyValue = _value.map(v => v && this.localeFormat(v, formatToken));
  830. notifyDate = [..._value];
  831. break;
  832. default:
  833. break;
  834. }
  835. return {
  836. notifyValue,
  837. notifyDate,
  838. };
  839. }
  840. /**
  841. * Notice: Check whether the date is the same as the state value before calling
  842. * @param {Date[]} value
  843. */
  844. _notifyChange(value: Date[]) {
  845. if (this._isRangeType() && !this._isRangeValueComplete(value)) {
  846. return;
  847. }
  848. const { onChangeWithDateFirst } = this.getProps();
  849. const { notifyValue, notifyDate } = this.disposeCallbackArgs(value);
  850. if (onChangeWithDateFirst) {
  851. this._adapter.notifyChange(notifyDate, notifyValue);
  852. } else {
  853. this._adapter.notifyChange(notifyValue, notifyDate);
  854. }
  855. }
  856. /**
  857. * Get the date changed through the date panel or enter
  858. * @param {Date[]} dates
  859. * @returns {Date[]}
  860. */
  861. _getChangedDates(dates: Date[]) {
  862. const type = this._adapter.getProp('type');
  863. const stateValue: Date[] = this._adapter.getState('value');
  864. const changedDates = [];
  865. switch (type) {
  866. case 'dateRange':
  867. case 'dateTimeRange':
  868. const [stateStart, stateEnd] = stateValue;
  869. const [start, end] = dates;
  870. if (!isDateEqual(start, stateStart)) {
  871. changedDates.push(start);
  872. }
  873. if (!isDateEqual(end, stateEnd)) {
  874. changedDates.push(end);
  875. }
  876. break;
  877. default:
  878. const stateValueSet = new Set<number>();
  879. stateValue.forEach(value => stateValueSet.add(isDate(value) && value.valueOf()));
  880. for (const date of dates) {
  881. if (!stateValueSet.has(isDate(date) && date.valueOf())) {
  882. changedDates.push(date);
  883. }
  884. }
  885. }
  886. return changedDates;
  887. }
  888. /**
  889. * Whether a date is disabled
  890. * @param {Array} value
  891. */
  892. _someDateDisabled(value: Date[]) {
  893. const stateValue = this.getState('value');
  894. const disabledOptions = { rangeStart: '', rangeEnd: '' };
  895. // DisabledDate needs to pass the second parameter
  896. if (this._isRangeType() && Array.isArray(stateValue)) {
  897. if (isValid(stateValue[0])) {
  898. const rangeStart = format(stateValue[0], 'yyyy-MM-dd');
  899. disabledOptions.rangeStart = rangeStart;
  900. }
  901. if (isValid(stateValue[1])) {
  902. const rangeEnd = format(stateValue[1], 'yyyy-MM-dd');
  903. disabledOptions.rangeEnd = rangeEnd;
  904. }
  905. }
  906. let isSomeDateDisabled = false;
  907. for (const date of value) {
  908. // skip check if date is null
  909. if (!isNullOrUndefined(date) && this.disabledDisposeDate(date, disabledOptions)) {
  910. isSomeDateDisabled = true;
  911. break;
  912. }
  913. }
  914. return isSomeDateDisabled;
  915. }
  916. getMergedMotion = (motion: any) => {
  917. const mergedMotion = typeof motion === 'undefined' || motion ? {
  918. ...motion,
  919. didEnter: () => {
  920. this._adapter.setMotionEnd(true);
  921. },
  922. didLeave: () => {
  923. this._adapter.setMotionEnd(false);
  924. }
  925. } : false;
  926. return mergedMotion;
  927. };
  928. /**
  929. * Format locale date
  930. * locale get from LocaleProvider
  931. * @param {Date} date
  932. * @param {String} token
  933. */
  934. localeFormat(date: Date, token: string) {
  935. const dateFnsLocale = this._adapter.getProp('dateFnsLocale');
  936. return format(date, token, { locale: dateFnsLocale });
  937. }
  938. _isRangeType = () => {
  939. const type = this._adapter.getProp('type');
  940. return /range/i.test(type);
  941. };
  942. _isRangeValueComplete = (value: Date[] | Date) => {
  943. let result = false;
  944. if (Array.isArray(value)) {
  945. result = !value.some(date => isNullOrUndefined(date));
  946. }
  947. return result;
  948. };
  949. /**
  950. * Convert computer date to UTC date
  951. * Before passing the date to the user, you need to convert the date to UTC time
  952. * dispose date from computer date to utc date
  953. * When given timeZone prop, you should convert computer date to utc date before passing to user
  954. * @param {(date: Date) => Boolean} fn
  955. * @param {Date|Date[]} date
  956. * @returns {Boolean}
  957. */
  958. disposeDateFn(fn: (date: Date, ...rest: any) => boolean, date: Date | Date[], ...rest: any[]) {
  959. const { notifyDate } = this.disposeCallbackArgs(date);
  960. const dateIsArray = Array.isArray(date);
  961. const notifyDateIsArray = Array.isArray(notifyDate);
  962. let disposeDate;
  963. if (dateIsArray === notifyDateIsArray) {
  964. disposeDate = notifyDate;
  965. } else {
  966. disposeDate = dateIsArray ? [notifyDate] : notifyDate[0];
  967. }
  968. return fn(disposeDate, ...rest);
  969. }
  970. /**
  971. * Determine whether the date is disabled
  972. * Whether the date is disabled
  973. * @param {Date} date
  974. * @returns {Boolean}
  975. */
  976. disabledDisposeDate(date: Date, ...rest: any[]) {
  977. const { disabledDate } = this.getProps();
  978. return this.disposeDateFn(disabledDate, date, ...rest);
  979. }
  980. /**
  981. * Determine whether the date is disabled
  982. * Whether the date time is disabled
  983. * @param {Date|Date[]} date
  984. * @returns {Object}
  985. */
  986. disabledDisposeTime(date: Date | Date[], ...rest: any[]) {
  987. const { disabledTime } = this.getProps();
  988. return this.disposeDateFn(disabledTime, date, ...rest);
  989. }
  990. /**
  991. * Trigger wrapper needs to do two things:
  992. * 1. Open Panel when clicking trigger;
  993. * 2. When clicking on a child but the child does not listen to the focus event, manually trigger focus
  994. *
  995. * @param {Event} e
  996. * @returns
  997. */
  998. handleTriggerWrapperClick(e: any) {
  999. const { disabled } = this._adapter.getProps();
  1000. const { rangeInputFocus } = this._adapter.getStates();
  1001. if (disabled) {
  1002. return;
  1003. }
  1004. /**
  1005. * - 非范围选择时,trigger 为原生输入框,已在组件内处理了 focus 逻辑
  1006. * - isEventTarget 函数用于判断触发事件的是否为 input wrapper。如果是冒泡上来的不用处理,因为在子级已经处理了 focus 逻辑。
  1007. *
  1008. * - When type is not range type, Input component will automatically focus in the same case
  1009. * - isEventTarget is used to judge whether the event is a bubbling event
  1010. */
  1011. if (this._isRangeType() && !rangeInputFocus && this._adapter.isEventTarget(e)) {
  1012. setTimeout(() => {
  1013. // using setTimeout get correct state value 'rangeInputFocus'
  1014. this.handleInputFocus(e, 'rangeStart');
  1015. this.openPanel();
  1016. }, 0);
  1017. } else {
  1018. this.openPanel();
  1019. }
  1020. }
  1021. }