Cascader.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
  2. import _extends from "@babel/runtime/helpers/esm/extends";
  3. import { createVNode as _createVNode, resolveDirective as _resolveDirective } from "vue";
  4. import { computed, defineComponent, ref, toRef, toRefs, watchEffect } from 'vue';
  5. import { baseSelectPropsWithoutPrivate } from '../vc-select/BaseSelect';
  6. import omit from '../_util/omit';
  7. import { objectType } from '../_util/type';
  8. import PropTypes from '../_util/vue-types';
  9. import { initDefaultProps } from '../_util/props-util';
  10. import useId from '../vc-select/hooks/useId';
  11. import useMergedState from '../_util/hooks/useMergedState';
  12. import { fillFieldNames, toPathKey, toPathKeys, SHOW_PARENT, SHOW_CHILD } from './utils/commonUtil';
  13. import useEntities from './hooks/useEntities';
  14. import useSearchConfig from './hooks/useSearchConfig';
  15. import useSearchOptions from './hooks/useSearchOptions';
  16. import useMissingValues from './hooks/useMissingValues';
  17. import { formatStrategyValues, toPathOptions } from './utils/treeUtil';
  18. import { conductCheck } from '../vc-tree/utils/conductUtil';
  19. import useDisplayValues from './hooks/useDisplayValues';
  20. import { useProvideCascader } from './context';
  21. import OptionList from './OptionList';
  22. import { BaseSelect } from '../vc-select';
  23. import devWarning from '../vc-util/devWarning';
  24. import useMaxLevel from '../vc-tree/useMaxLevel';
  25. export { SHOW_PARENT, SHOW_CHILD };
  26. function baseCascaderProps() {
  27. return _extends(_extends({}, omit(baseSelectPropsWithoutPrivate(), ['tokenSeparators', 'mode', 'showSearch'])), {
  28. // MISC
  29. id: String,
  30. prefixCls: String,
  31. fieldNames: objectType(),
  32. children: Array,
  33. // Value
  34. value: {
  35. type: [String, Number, Array]
  36. },
  37. defaultValue: {
  38. type: [String, Number, Array]
  39. },
  40. changeOnSelect: {
  41. type: Boolean,
  42. default: undefined
  43. },
  44. displayRender: Function,
  45. checkable: {
  46. type: Boolean,
  47. default: undefined
  48. },
  49. showCheckedStrategy: {
  50. type: String,
  51. default: SHOW_PARENT
  52. },
  53. // Search
  54. showSearch: {
  55. type: [Boolean, Object],
  56. default: undefined
  57. },
  58. searchValue: String,
  59. onSearch: Function,
  60. // Trigger
  61. expandTrigger: String,
  62. // Options
  63. options: Array,
  64. /** @private Internal usage. Do not use in your production. */
  65. dropdownPrefixCls: String,
  66. loadData: Function,
  67. // Open
  68. /** @deprecated Use `open` instead */
  69. popupVisible: {
  70. type: Boolean,
  71. default: undefined
  72. },
  73. dropdownClassName: String,
  74. dropdownMenuColumnStyle: {
  75. type: Object,
  76. default: undefined
  77. },
  78. /** @deprecated Use `dropdownStyle` instead */
  79. popupStyle: {
  80. type: Object,
  81. default: undefined
  82. },
  83. dropdownStyle: {
  84. type: Object,
  85. default: undefined
  86. },
  87. /** @deprecated Use `placement` instead */
  88. popupPlacement: String,
  89. placement: String,
  90. /** @deprecated Use `onDropdownVisibleChange` instead */
  91. onPopupVisibleChange: Function,
  92. onDropdownVisibleChange: Function,
  93. // Icon
  94. expandIcon: PropTypes.any,
  95. loadingIcon: PropTypes.any
  96. });
  97. }
  98. export function singleCascaderProps() {
  99. return _extends(_extends({}, baseCascaderProps()), {
  100. checkable: Boolean,
  101. onChange: Function
  102. });
  103. }
  104. export function multipleCascaderProps() {
  105. return _extends(_extends({}, baseCascaderProps()), {
  106. checkable: Boolean,
  107. onChange: Function
  108. });
  109. }
  110. export function internalCascaderProps() {
  111. return _extends(_extends({}, baseCascaderProps()), {
  112. onChange: Function,
  113. customSlots: Object
  114. });
  115. }
  116. function isMultipleValue(value) {
  117. return Array.isArray(value) && Array.isArray(value[0]);
  118. }
  119. function toRawValues(value) {
  120. if (!value) {
  121. return [];
  122. }
  123. if (isMultipleValue(value)) {
  124. return value;
  125. }
  126. return (value.length === 0 ? [] : [value]).map(val => Array.isArray(val) ? val : [val]);
  127. }
  128. export default defineComponent({
  129. compatConfig: {
  130. MODE: 3
  131. },
  132. name: 'Cascader',
  133. inheritAttrs: false,
  134. props: initDefaultProps(internalCascaderProps(), {}),
  135. setup(props, _ref) {
  136. let {
  137. attrs,
  138. expose,
  139. slots
  140. } = _ref;
  141. const mergedId = useId(toRef(props, 'id'));
  142. const multiple = computed(() => !!props.checkable);
  143. // =========================== Values ===========================
  144. const [rawValues, setRawValues] = useMergedState(props.defaultValue, {
  145. value: computed(() => props.value),
  146. postState: toRawValues
  147. });
  148. // ========================= FieldNames =========================
  149. const mergedFieldNames = computed(() => fillFieldNames(props.fieldNames));
  150. // =========================== Option ===========================
  151. const mergedOptions = computed(() => props.options || []);
  152. // Only used in multiple mode, this fn will not call in single mode
  153. const pathKeyEntities = useEntities(mergedOptions, mergedFieldNames);
  154. /** Convert path key back to value format */
  155. const getValueByKeyPath = pathKeys => {
  156. const keyPathEntities = pathKeyEntities.value;
  157. return pathKeys.map(pathKey => {
  158. const {
  159. nodes
  160. } = keyPathEntities[pathKey];
  161. return nodes.map(node => node[mergedFieldNames.value.value]);
  162. });
  163. };
  164. // =========================== Search ===========================
  165. const [mergedSearchValue, setSearchValue] = useMergedState('', {
  166. value: computed(() => props.searchValue),
  167. postState: search => search || ''
  168. });
  169. const onInternalSearch = (searchText, info) => {
  170. setSearchValue(searchText);
  171. if (info.source !== 'blur' && props.onSearch) {
  172. props.onSearch(searchText);
  173. }
  174. };
  175. const {
  176. showSearch: mergedShowSearch,
  177. searchConfig: mergedSearchConfig
  178. } = useSearchConfig(toRef(props, 'showSearch'));
  179. const searchOptions = useSearchOptions(mergedSearchValue, mergedOptions, mergedFieldNames, computed(() => props.dropdownPrefixCls || props.prefixCls), mergedSearchConfig, toRef(props, 'changeOnSelect'));
  180. // =========================== Values ===========================
  181. const missingValuesInfo = useMissingValues(mergedOptions, mergedFieldNames, rawValues);
  182. // Fill `rawValues` with checked conduction values
  183. const [checkedValues, halfCheckedValues, missingCheckedValues] = [ref([]), ref([]), ref([])];
  184. const {
  185. maxLevel,
  186. levelEntities
  187. } = useMaxLevel(pathKeyEntities);
  188. watchEffect(() => {
  189. const [existValues, missingValues] = missingValuesInfo.value;
  190. if (!multiple.value || !rawValues.value.length) {
  191. [checkedValues.value, halfCheckedValues.value, missingCheckedValues.value] = [existValues, [], missingValues];
  192. return;
  193. }
  194. const keyPathValues = toPathKeys(existValues);
  195. const keyPathEntities = pathKeyEntities.value;
  196. const {
  197. checkedKeys,
  198. halfCheckedKeys
  199. } = conductCheck(keyPathValues, true, keyPathEntities, maxLevel.value, levelEntities.value);
  200. // Convert key back to value cells
  201. [checkedValues.value, halfCheckedValues.value, missingCheckedValues.value] = [getValueByKeyPath(checkedKeys), getValueByKeyPath(halfCheckedKeys), missingValues];
  202. });
  203. const deDuplicatedValues = computed(() => {
  204. const checkedKeys = toPathKeys(checkedValues.value);
  205. const deduplicateKeys = formatStrategyValues(checkedKeys, pathKeyEntities.value, props.showCheckedStrategy);
  206. return [...missingCheckedValues.value, ...getValueByKeyPath(deduplicateKeys)];
  207. });
  208. const displayValues = useDisplayValues(deDuplicatedValues, mergedOptions, mergedFieldNames, multiple, toRef(props, 'displayRender'));
  209. // =========================== Change ===========================
  210. const triggerChange = nextValues => {
  211. setRawValues(nextValues);
  212. // Save perf if no need trigger event
  213. if (props.onChange) {
  214. const nextRawValues = toRawValues(nextValues);
  215. const valueOptions = nextRawValues.map(valueCells => toPathOptions(valueCells, mergedOptions.value, mergedFieldNames.value).map(valueOpt => valueOpt.option));
  216. const triggerValues = multiple.value ? nextRawValues : nextRawValues[0];
  217. const triggerOptions = multiple.value ? valueOptions : valueOptions[0];
  218. props.onChange(triggerValues, triggerOptions);
  219. }
  220. };
  221. // =========================== Select ===========================
  222. const onInternalSelect = valuePath => {
  223. setSearchValue('');
  224. if (!multiple.value) {
  225. triggerChange(valuePath);
  226. } else {
  227. // Prepare conduct required info
  228. const pathKey = toPathKey(valuePath);
  229. const checkedPathKeys = toPathKeys(checkedValues.value);
  230. const halfCheckedPathKeys = toPathKeys(halfCheckedValues.value);
  231. const existInChecked = checkedPathKeys.includes(pathKey);
  232. const existInMissing = missingCheckedValues.value.some(valueCells => toPathKey(valueCells) === pathKey);
  233. // Do update
  234. let nextCheckedValues = checkedValues.value;
  235. let nextMissingValues = missingCheckedValues.value;
  236. if (existInMissing && !existInChecked) {
  237. // Missing value only do filter
  238. nextMissingValues = missingCheckedValues.value.filter(valueCells => toPathKey(valueCells) !== pathKey);
  239. } else {
  240. // Update checked key first
  241. const nextRawCheckedKeys = existInChecked ? checkedPathKeys.filter(key => key !== pathKey) : [...checkedPathKeys, pathKey];
  242. // Conduction by selected or not
  243. let checkedKeys;
  244. if (existInChecked) {
  245. ({
  246. checkedKeys
  247. } = conductCheck(nextRawCheckedKeys, {
  248. checked: false,
  249. halfCheckedKeys: halfCheckedPathKeys
  250. }, pathKeyEntities.value, maxLevel.value, levelEntities.value));
  251. } else {
  252. ({
  253. checkedKeys
  254. } = conductCheck(nextRawCheckedKeys, true, pathKeyEntities.value, maxLevel.value, levelEntities.value));
  255. }
  256. // Roll up to parent level keys
  257. const deDuplicatedKeys = formatStrategyValues(checkedKeys, pathKeyEntities.value, props.showCheckedStrategy);
  258. nextCheckedValues = getValueByKeyPath(deDuplicatedKeys);
  259. }
  260. triggerChange([...nextMissingValues, ...nextCheckedValues]);
  261. }
  262. };
  263. // Display Value change logic
  264. const onDisplayValuesChange = (_, info) => {
  265. if (info.type === 'clear') {
  266. triggerChange([]);
  267. return;
  268. }
  269. // Cascader do not support `add` type. Only support `remove`
  270. const {
  271. valueCells
  272. } = info.values[0];
  273. onInternalSelect(valueCells);
  274. };
  275. // ============================ Open ============================
  276. if (process.env.NODE_ENV !== 'production') {
  277. watchEffect(() => {
  278. devWarning(!props.onPopupVisibleChange, 'Cascader', '`popupVisibleChange` is deprecated. Please use `dropdownVisibleChange` instead.');
  279. devWarning(props.popupVisible === undefined, 'Cascader', '`popupVisible` is deprecated. Please use `open` instead.');
  280. devWarning(props.popupPlacement === undefined, 'Cascader', '`popupPlacement` is deprecated. Please use `placement` instead.');
  281. devWarning(props.popupStyle === undefined, 'Cascader', '`popupStyle` is deprecated. Please use `dropdownStyle` instead.');
  282. });
  283. }
  284. const mergedOpen = computed(() => props.open !== undefined ? props.open : props.popupVisible);
  285. const mergedDropdownStyle = computed(() => props.dropdownStyle || props.popupStyle || {});
  286. const mergedPlacement = computed(() => props.placement || props.popupPlacement);
  287. const onInternalDropdownVisibleChange = nextVisible => {
  288. var _a, _b;
  289. (_a = props.onDropdownVisibleChange) === null || _a === void 0 ? void 0 : _a.call(props, nextVisible);
  290. (_b = props.onPopupVisibleChange) === null || _b === void 0 ? void 0 : _b.call(props, nextVisible);
  291. };
  292. const {
  293. changeOnSelect,
  294. checkable,
  295. dropdownPrefixCls,
  296. loadData,
  297. expandTrigger,
  298. expandIcon,
  299. loadingIcon,
  300. dropdownMenuColumnStyle,
  301. customSlots,
  302. dropdownClassName
  303. } = toRefs(props);
  304. useProvideCascader({
  305. options: mergedOptions,
  306. fieldNames: mergedFieldNames,
  307. values: checkedValues,
  308. halfValues: halfCheckedValues,
  309. changeOnSelect,
  310. onSelect: onInternalSelect,
  311. checkable,
  312. searchOptions,
  313. dropdownPrefixCls,
  314. loadData,
  315. expandTrigger,
  316. expandIcon,
  317. loadingIcon,
  318. dropdownMenuColumnStyle,
  319. customSlots
  320. });
  321. const selectRef = ref();
  322. expose({
  323. focus() {
  324. var _a;
  325. (_a = selectRef.value) === null || _a === void 0 ? void 0 : _a.focus();
  326. },
  327. blur() {
  328. var _a;
  329. (_a = selectRef.value) === null || _a === void 0 ? void 0 : _a.blur();
  330. },
  331. scrollTo(arg) {
  332. var _a;
  333. (_a = selectRef.value) === null || _a === void 0 ? void 0 : _a.scrollTo(arg);
  334. }
  335. });
  336. const pickProps = computed(() => {
  337. return omit(props, ['id', 'prefixCls', 'fieldNames',
  338. // Value
  339. 'defaultValue', 'value', 'changeOnSelect', 'onChange', 'displayRender', 'checkable',
  340. // Search
  341. 'searchValue', 'onSearch', 'showSearch',
  342. // Trigger
  343. 'expandTrigger',
  344. // Options
  345. 'options', 'dropdownPrefixCls', 'loadData',
  346. // Open
  347. 'popupVisible', 'open', 'dropdownClassName', 'dropdownMenuColumnStyle', 'popupPlacement', 'placement', 'onDropdownVisibleChange', 'onPopupVisibleChange',
  348. // Icon
  349. 'expandIcon', 'loadingIcon', 'customSlots', 'showCheckedStrategy',
  350. // Children
  351. 'children']);
  352. });
  353. return () => {
  354. const emptyOptions = !(mergedSearchValue.value ? searchOptions.value : mergedOptions.value).length;
  355. const {
  356. dropdownMatchSelectWidth = false
  357. } = props;
  358. const dropdownStyle =
  359. // Search to match width
  360. mergedSearchValue.value && mergedSearchConfig.value.matchInputWidth ||
  361. // Empty keep the width
  362. emptyOptions ? {} : {
  363. minWidth: 'auto'
  364. };
  365. return _createVNode(BaseSelect, _objectSpread(_objectSpread(_objectSpread({}, pickProps.value), attrs), {}, {
  366. "ref": selectRef,
  367. "id": mergedId,
  368. "prefixCls": props.prefixCls,
  369. "dropdownMatchSelectWidth": dropdownMatchSelectWidth,
  370. "dropdownStyle": _extends(_extends({}, mergedDropdownStyle.value), dropdownStyle),
  371. "displayValues": displayValues.value,
  372. "onDisplayValuesChange": onDisplayValuesChange,
  373. "mode": multiple.value ? 'multiple' : undefined,
  374. "searchValue": mergedSearchValue.value,
  375. "onSearch": onInternalSearch,
  376. "showSearch": mergedShowSearch.value,
  377. "OptionList": OptionList,
  378. "emptyOptions": emptyOptions,
  379. "open": mergedOpen.value,
  380. "dropdownClassName": dropdownClassName.value,
  381. "placement": mergedPlacement.value,
  382. "onDropdownVisibleChange": onInternalDropdownVisibleChange,
  383. "getRawInputElement": () => {
  384. var _a;
  385. return (_a = slots.default) === null || _a === void 0 ? void 0 : _a.call(slots);
  386. }
  387. }), slots);
  388. };
  389. }
  390. });