foundation.ts 52 KB

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