foundation.ts 52 KB

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