foundation.ts 54 KB

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