foundation.ts 53 KB

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