foundation.ts 52 KB

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