foundation.ts 52 KB

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