foundation.ts 54 KB

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