Picker.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
  2. import _extends from "@babel/runtime/helpers/esm/extends";
  3. import { resolveDirective as _resolveDirective, createVNode as _createVNode } from "vue";
  4. /**
  5. * Removed:
  6. * - getCalendarContainer: use `getPopupContainer` instead
  7. * - onOk
  8. *
  9. * New Feature:
  10. * - picker
  11. * - allowEmpty
  12. * - selectable
  13. *
  14. * Tips: Should add faq about `datetime` mode with `defaultValue`
  15. */
  16. import PickerPanel from './PickerPanel';
  17. import PickerTrigger from './PickerTrigger';
  18. import PresetPanel from './PresetPanel';
  19. import { formatValue, isEqual, parseValue } from './utils/dateUtil';
  20. import getDataOrAriaProps, { toArray } from './utils/miscUtil';
  21. import { useProvidePanel } from './PanelContext';
  22. import { getDefaultFormat, getInputSize, elementsContains } from './utils/uiUtil';
  23. import usePickerInput from './hooks/usePickerInput';
  24. import useTextValueMapping from './hooks/useTextValueMapping';
  25. import useValueTexts from './hooks/useValueTexts';
  26. import useHoverValue from './hooks/useHoverValue';
  27. import usePresets from './hooks/usePresets';
  28. import { computed, defineComponent, ref, toRef, watch } from 'vue';
  29. import useMergedState from '../_util/hooks/useMergedState';
  30. import { warning } from '../vc-util/warning';
  31. import classNames from '../_util/classNames';
  32. import { legacyPropsWarning } from './utils/warnUtil';
  33. function Picker() {
  34. return defineComponent({
  35. name: 'Picker',
  36. inheritAttrs: false,
  37. props: ['prefixCls', 'id', 'tabindex', 'dropdownClassName', 'dropdownAlign', 'popupStyle', 'transitionName', 'generateConfig', 'locale', 'inputReadOnly', 'allowClear', 'autofocus', 'showTime', 'showNow', 'showHour', 'showMinute', 'showSecond', 'picker', 'format', 'use12Hours', 'value', 'defaultValue', 'open', 'defaultOpen', 'defaultOpenValue', 'suffixIcon', 'presets', 'clearIcon', 'disabled', 'disabledDate', 'placeholder', 'getPopupContainer', 'panelRender', 'inputRender', 'onChange', 'onOpenChange', 'onPanelChange', 'onFocus', 'onBlur', 'onMousedown', 'onMouseup', 'onMouseenter', 'onMouseleave', 'onContextmenu', 'onClick', 'onKeydown', 'onSelect', 'direction', 'autocomplete', 'showToday', 'renderExtraFooter', 'dateRender', 'minuteStep', 'hourStep', 'secondStep', 'hideDisabledOptions'],
  38. setup(props, _ref) {
  39. let {
  40. attrs,
  41. expose
  42. } = _ref;
  43. const inputRef = ref(null);
  44. const presets = computed(() => props.presets);
  45. const presetList = usePresets(presets);
  46. const picker = computed(() => {
  47. var _a;
  48. return (_a = props.picker) !== null && _a !== void 0 ? _a : 'date';
  49. });
  50. const needConfirmButton = computed(() => picker.value === 'date' && !!props.showTime || picker.value === 'time');
  51. // ============================ Warning ============================
  52. if (process.env.NODE_ENV !== 'production') {
  53. legacyPropsWarning(props);
  54. }
  55. // ============================= State =============================
  56. const formatList = computed(() => toArray(getDefaultFormat(props.format, picker.value, props.showTime, props.use12Hours)));
  57. // Panel ref
  58. const panelDivRef = ref(null);
  59. const inputDivRef = ref(null);
  60. const containerRef = ref(null);
  61. // Real value
  62. const [mergedValue, setInnerValue] = useMergedState(null, {
  63. value: toRef(props, 'value'),
  64. defaultValue: props.defaultValue
  65. });
  66. const selectedValue = ref(mergedValue.value);
  67. const setSelectedValue = val => {
  68. selectedValue.value = val;
  69. };
  70. // Operation ref
  71. const operationRef = ref(null);
  72. // Open
  73. const [mergedOpen, triggerInnerOpen] = useMergedState(false, {
  74. value: toRef(props, 'open'),
  75. defaultValue: props.defaultOpen,
  76. postState: postOpen => props.disabled ? false : postOpen,
  77. onChange: newOpen => {
  78. if (props.onOpenChange) {
  79. props.onOpenChange(newOpen);
  80. }
  81. if (!newOpen && operationRef.value && operationRef.value.onClose) {
  82. operationRef.value.onClose();
  83. }
  84. }
  85. });
  86. // ============================= Text ==============================
  87. const [valueTexts, firstValueText] = useValueTexts(selectedValue, {
  88. formatList,
  89. generateConfig: toRef(props, 'generateConfig'),
  90. locale: toRef(props, 'locale')
  91. });
  92. const [text, triggerTextChange, resetText] = useTextValueMapping({
  93. valueTexts,
  94. onTextChange: newText => {
  95. const inputDate = parseValue(newText, {
  96. locale: props.locale,
  97. formatList: formatList.value,
  98. generateConfig: props.generateConfig
  99. });
  100. if (inputDate && (!props.disabledDate || !props.disabledDate(inputDate))) {
  101. setSelectedValue(inputDate);
  102. }
  103. }
  104. });
  105. // ============================ Trigger ============================
  106. const triggerChange = newValue => {
  107. const {
  108. onChange,
  109. generateConfig,
  110. locale
  111. } = props;
  112. setSelectedValue(newValue);
  113. setInnerValue(newValue);
  114. if (onChange && !isEqual(generateConfig, mergedValue.value, newValue)) {
  115. onChange(newValue, newValue ? formatValue(newValue, {
  116. generateConfig,
  117. locale,
  118. format: formatList.value[0]
  119. }) : '');
  120. }
  121. };
  122. const triggerOpen = newOpen => {
  123. if (props.disabled && newOpen) {
  124. return;
  125. }
  126. triggerInnerOpen(newOpen);
  127. };
  128. const forwardKeydown = e => {
  129. if (mergedOpen.value && operationRef.value && operationRef.value.onKeydown) {
  130. // Let popup panel handle keyboard
  131. return operationRef.value.onKeydown(e);
  132. }
  133. /* istanbul ignore next */
  134. /* eslint-disable no-lone-blocks */
  135. {
  136. warning(false, 'Picker not correct forward Keydown operation. Please help to fire issue about this.');
  137. return false;
  138. }
  139. };
  140. const onInternalMouseup = function () {
  141. if (props.onMouseup) {
  142. props.onMouseup(...arguments);
  143. }
  144. if (inputRef.value) {
  145. inputRef.value.focus();
  146. triggerOpen(true);
  147. }
  148. };
  149. // ============================= Input =============================
  150. const [inputProps, {
  151. focused,
  152. typing
  153. }] = usePickerInput({
  154. blurToCancel: needConfirmButton,
  155. open: mergedOpen,
  156. value: text,
  157. triggerOpen,
  158. forwardKeydown,
  159. isClickOutside: target => !elementsContains([panelDivRef.value, inputDivRef.value, containerRef.value], target),
  160. onSubmit: () => {
  161. if (
  162. // When user typing disabledDate with keyboard and enter, this value will be empty
  163. !selectedValue.value ||
  164. // Normal disabled check
  165. props.disabledDate && props.disabledDate(selectedValue.value)) {
  166. return false;
  167. }
  168. triggerChange(selectedValue.value);
  169. triggerOpen(false);
  170. resetText();
  171. return true;
  172. },
  173. onCancel: () => {
  174. triggerOpen(false);
  175. setSelectedValue(mergedValue.value);
  176. resetText();
  177. },
  178. onKeydown: (e, preventDefault) => {
  179. var _a;
  180. (_a = props.onKeydown) === null || _a === void 0 ? void 0 : _a.call(props, e, preventDefault);
  181. },
  182. onFocus: e => {
  183. var _a;
  184. (_a = props.onFocus) === null || _a === void 0 ? void 0 : _a.call(props, e);
  185. },
  186. onBlur: e => {
  187. var _a;
  188. (_a = props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, e);
  189. }
  190. });
  191. // ============================= Sync ==============================
  192. // Close should sync back with text value
  193. watch([mergedOpen, valueTexts], () => {
  194. if (!mergedOpen.value) {
  195. setSelectedValue(mergedValue.value);
  196. if (!valueTexts.value.length || valueTexts.value[0] === '') {
  197. triggerTextChange('');
  198. } else if (firstValueText.value !== text.value) {
  199. resetText();
  200. }
  201. }
  202. });
  203. // Change picker should sync back with text value
  204. watch(picker, () => {
  205. if (!mergedOpen.value) {
  206. resetText();
  207. }
  208. });
  209. // Sync innerValue with control mode
  210. watch(mergedValue, () => {
  211. // Sync select value
  212. setSelectedValue(mergedValue.value);
  213. });
  214. const [hoverValue, onEnter, onLeave] = useHoverValue(text, {
  215. formatList,
  216. generateConfig: toRef(props, 'generateConfig'),
  217. locale: toRef(props, 'locale')
  218. });
  219. const onContextSelect = (date, type) => {
  220. if (type === 'submit' || type !== 'key' && !needConfirmButton.value) {
  221. // triggerChange will also update selected values
  222. triggerChange(date);
  223. triggerOpen(false);
  224. }
  225. };
  226. useProvidePanel({
  227. operationRef,
  228. hideHeader: computed(() => picker.value === 'time'),
  229. onSelect: onContextSelect,
  230. open: mergedOpen,
  231. defaultOpenValue: toRef(props, 'defaultOpenValue'),
  232. onDateMouseenter: onEnter,
  233. onDateMouseleave: onLeave
  234. });
  235. expose({
  236. focus: () => {
  237. if (inputRef.value) {
  238. inputRef.value.focus();
  239. }
  240. },
  241. blur: () => {
  242. if (inputRef.value) {
  243. inputRef.value.blur();
  244. }
  245. }
  246. });
  247. return () => {
  248. const {
  249. prefixCls = 'rc-picker',
  250. id,
  251. tabindex,
  252. dropdownClassName,
  253. dropdownAlign,
  254. popupStyle,
  255. transitionName,
  256. generateConfig,
  257. locale,
  258. inputReadOnly,
  259. allowClear,
  260. autofocus,
  261. picker = 'date',
  262. defaultOpenValue,
  263. suffixIcon,
  264. clearIcon,
  265. disabled,
  266. placeholder,
  267. getPopupContainer,
  268. panelRender,
  269. onMousedown,
  270. onMouseenter,
  271. onMouseleave,
  272. onContextmenu,
  273. onClick,
  274. onSelect,
  275. direction,
  276. autocomplete = 'off'
  277. } = props;
  278. // ============================= Panel =============================
  279. const panelProps = _extends(_extends(_extends({}, props), attrs), {
  280. class: classNames({
  281. [`${prefixCls}-panel-focused`]: !typing.value
  282. }),
  283. style: undefined,
  284. pickerValue: undefined,
  285. onPickerValueChange: undefined,
  286. onChange: null
  287. });
  288. let panelNode = _createVNode("div", {
  289. "class": `${prefixCls}-panel-layout`
  290. }, [_createVNode(PresetPanel, {
  291. "prefixCls": prefixCls,
  292. "presets": presetList.value,
  293. "onClick": nextValue => {
  294. triggerChange(nextValue);
  295. triggerOpen(false);
  296. }
  297. }, null), _createVNode(PickerPanel, _objectSpread(_objectSpread({}, panelProps), {}, {
  298. "generateConfig": generateConfig,
  299. "value": selectedValue.value,
  300. "locale": locale,
  301. "tabindex": -1,
  302. "onSelect": date => {
  303. onSelect === null || onSelect === void 0 ? void 0 : onSelect(date);
  304. setSelectedValue(date);
  305. },
  306. "direction": direction,
  307. "onPanelChange": (viewDate, mode) => {
  308. const {
  309. onPanelChange
  310. } = props;
  311. onLeave(true);
  312. onPanelChange === null || onPanelChange === void 0 ? void 0 : onPanelChange(viewDate, mode);
  313. }
  314. }), null)]);
  315. if (panelRender) {
  316. panelNode = panelRender(panelNode);
  317. }
  318. const panel = _createVNode("div", {
  319. "class": `${prefixCls}-panel-container`,
  320. "ref": panelDivRef,
  321. "onMousedown": e => {
  322. e.preventDefault();
  323. }
  324. }, [panelNode]);
  325. let suffixNode;
  326. if (suffixIcon) {
  327. suffixNode = _createVNode("span", {
  328. "class": `${prefixCls}-suffix`
  329. }, [suffixIcon]);
  330. }
  331. let clearNode;
  332. if (allowClear && mergedValue.value && !disabled) {
  333. clearNode = _createVNode("span", {
  334. "onMousedown": e => {
  335. e.preventDefault();
  336. e.stopPropagation();
  337. },
  338. "onMouseup": e => {
  339. e.preventDefault();
  340. e.stopPropagation();
  341. triggerChange(null);
  342. triggerOpen(false);
  343. },
  344. "class": `${prefixCls}-clear`,
  345. "role": "button"
  346. }, [clearIcon || _createVNode("span", {
  347. "class": `${prefixCls}-clear-btn`
  348. }, null)]);
  349. }
  350. const mergedInputProps = _extends(_extends(_extends(_extends({
  351. id,
  352. tabindex,
  353. disabled,
  354. readonly: inputReadOnly || typeof formatList.value[0] === 'function' || !typing.value,
  355. value: hoverValue.value || text.value,
  356. onInput: e => {
  357. triggerTextChange(e.target.value);
  358. },
  359. autofocus,
  360. placeholder,
  361. ref: inputRef,
  362. title: text.value
  363. }, inputProps.value), {
  364. size: getInputSize(picker, formatList.value[0], generateConfig)
  365. }), getDataOrAriaProps(props)), {
  366. autocomplete
  367. });
  368. const inputNode = props.inputRender ? props.inputRender(mergedInputProps) : _createVNode("input", mergedInputProps, null);
  369. // ============================ Warning ============================
  370. if (process.env.NODE_ENV !== 'production') {
  371. warning(!defaultOpenValue, '`defaultOpenValue` may confuse user for the current value status. Please use `defaultValue` instead.');
  372. }
  373. // ============================ Return =============================
  374. const popupPlacement = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
  375. return _createVNode("div", {
  376. "ref": containerRef,
  377. "class": classNames(prefixCls, attrs.class, {
  378. [`${prefixCls}-disabled`]: disabled,
  379. [`${prefixCls}-focused`]: focused.value,
  380. [`${prefixCls}-rtl`]: direction === 'rtl'
  381. }),
  382. "style": attrs.style,
  383. "onMousedown": onMousedown,
  384. "onMouseup": onInternalMouseup,
  385. "onMouseenter": onMouseenter,
  386. "onMouseleave": onMouseleave,
  387. "onContextmenu": onContextmenu,
  388. "onClick": onClick
  389. }, [_createVNode("div", {
  390. "class": classNames(`${prefixCls}-input`, {
  391. [`${prefixCls}-input-placeholder`]: !!hoverValue.value
  392. }),
  393. "ref": inputDivRef
  394. }, [inputNode, suffixNode, clearNode]), _createVNode(PickerTrigger, {
  395. "visible": mergedOpen.value,
  396. "popupStyle": popupStyle,
  397. "prefixCls": prefixCls,
  398. "dropdownClassName": dropdownClassName,
  399. "dropdownAlign": dropdownAlign,
  400. "getPopupContainer": getPopupContainer,
  401. "transitionName": transitionName,
  402. "popupPlacement": popupPlacement,
  403. "direction": direction
  404. }, {
  405. default: () => [_createVNode("div", {
  406. "style": {
  407. pointerEvents: 'none',
  408. position: 'absolute',
  409. top: 0,
  410. bottom: 0,
  411. left: 0,
  412. right: 0
  413. }
  414. }, null)],
  415. popupElement: () => panel
  416. })]);
  417. };
  418. }
  419. });
  420. }
  421. export default Picker();