TreeSelect.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  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 OptionList from './OptionList';
  5. import { formatStrategyValues, SHOW_CHILD } from './utils/strategyUtil';
  6. import { useProvideSelectContext } from './TreeSelectContext';
  7. import { useProvideLegacySelectContext } from './LegacyContext';
  8. import useTreeData from './hooks/useTreeData';
  9. import { toArray, fillFieldNames, isNil } from './utils/valueUtil';
  10. import useCache from './hooks/useCache';
  11. import useDataEntities from './hooks/useDataEntities';
  12. import { fillAdditionalInfo, fillLegacyProps } from './utils/legacyUtil';
  13. import useCheckedKeys from './hooks/useCheckedKeys';
  14. import useFilterTreeData from './hooks/useFilterTreeData';
  15. import warningProps from './utils/warningPropsUtil';
  16. import { baseSelectPropsWithoutPrivate } from '../vc-select/BaseSelect';
  17. import { computed, defineComponent, ref, shallowRef, toRaw, toRef, toRefs, watchEffect } from 'vue';
  18. import omit from '../_util/omit';
  19. import PropTypes from '../_util/vue-types';
  20. import { BaseSelect } from '../vc-select';
  21. import { initDefaultProps } from '../_util/props-util';
  22. import useId from '../vc-select/hooks/useId';
  23. import useMergedState from '../_util/hooks/useMergedState';
  24. import { conductCheck } from '../vc-tree/utils/conductUtil';
  25. import { warning } from '../vc-util/warning';
  26. import { toReactive } from '../_util/toReactive';
  27. import useMaxLevel from '../vc-tree/useMaxLevel';
  28. export function treeSelectProps() {
  29. return _extends(_extends({}, omit(baseSelectPropsWithoutPrivate(), ['mode'])), {
  30. prefixCls: String,
  31. id: String,
  32. value: {
  33. type: [String, Number, Object, Array]
  34. },
  35. defaultValue: {
  36. type: [String, Number, Object, Array]
  37. },
  38. onChange: {
  39. type: Function
  40. },
  41. searchValue: String,
  42. /** @deprecated Use `searchValue` instead */
  43. inputValue: String,
  44. onSearch: {
  45. type: Function
  46. },
  47. autoClearSearchValue: {
  48. type: Boolean,
  49. default: undefined
  50. },
  51. filterTreeNode: {
  52. type: [Boolean, Function],
  53. default: undefined
  54. },
  55. treeNodeFilterProp: String,
  56. // >>> Select
  57. onSelect: Function,
  58. onDeselect: Function,
  59. showCheckedStrategy: {
  60. type: String
  61. },
  62. treeNodeLabelProp: String,
  63. fieldNames: {
  64. type: Object
  65. },
  66. // >>> Mode
  67. multiple: {
  68. type: Boolean,
  69. default: undefined
  70. },
  71. treeCheckable: {
  72. type: Boolean,
  73. default: undefined
  74. },
  75. treeCheckStrictly: {
  76. type: Boolean,
  77. default: undefined
  78. },
  79. labelInValue: {
  80. type: Boolean,
  81. default: undefined
  82. },
  83. // >>> Data
  84. treeData: {
  85. type: Array
  86. },
  87. treeDataSimpleMode: {
  88. type: [Boolean, Object],
  89. default: undefined
  90. },
  91. loadData: {
  92. type: Function
  93. },
  94. treeLoadedKeys: {
  95. type: Array
  96. },
  97. onTreeLoad: {
  98. type: Function
  99. },
  100. // >>> Expanded
  101. treeDefaultExpandAll: {
  102. type: Boolean,
  103. default: undefined
  104. },
  105. treeExpandedKeys: {
  106. type: Array
  107. },
  108. treeDefaultExpandedKeys: {
  109. type: Array
  110. },
  111. onTreeExpand: {
  112. type: Function
  113. },
  114. // >>> Options
  115. virtual: {
  116. type: Boolean,
  117. default: undefined
  118. },
  119. listHeight: Number,
  120. listItemHeight: Number,
  121. onDropdownVisibleChange: {
  122. type: Function
  123. },
  124. // >>> Tree
  125. treeLine: {
  126. type: [Boolean, Object],
  127. default: undefined
  128. },
  129. treeIcon: PropTypes.any,
  130. showTreeIcon: {
  131. type: Boolean,
  132. default: undefined
  133. },
  134. switcherIcon: PropTypes.any,
  135. treeMotion: PropTypes.any,
  136. children: Array,
  137. treeExpandAction: String,
  138. showArrow: {
  139. type: Boolean,
  140. default: undefined
  141. },
  142. showSearch: {
  143. type: Boolean,
  144. default: undefined
  145. },
  146. open: {
  147. type: Boolean,
  148. default: undefined
  149. },
  150. defaultOpen: {
  151. type: Boolean,
  152. default: undefined
  153. },
  154. disabled: {
  155. type: Boolean,
  156. default: undefined
  157. },
  158. placeholder: PropTypes.any,
  159. maxTagPlaceholder: {
  160. type: Function
  161. },
  162. dropdownPopupAlign: PropTypes.any,
  163. customSlots: Object
  164. });
  165. }
  166. function isRawValue(value) {
  167. return !value || typeof value !== 'object';
  168. }
  169. export default defineComponent({
  170. compatConfig: {
  171. MODE: 3
  172. },
  173. name: 'TreeSelect',
  174. inheritAttrs: false,
  175. props: initDefaultProps(treeSelectProps(), {
  176. treeNodeFilterProp: 'value',
  177. autoClearSearchValue: true,
  178. showCheckedStrategy: SHOW_CHILD,
  179. listHeight: 200,
  180. listItemHeight: 20,
  181. prefixCls: 'vc-tree-select'
  182. }),
  183. setup(props, _ref) {
  184. let {
  185. attrs,
  186. expose,
  187. slots
  188. } = _ref;
  189. const mergedId = useId(toRef(props, 'id'));
  190. const treeConduction = computed(() => props.treeCheckable && !props.treeCheckStrictly);
  191. const mergedCheckable = computed(() => props.treeCheckable || props.treeCheckStrictly);
  192. const mergedLabelInValue = computed(() => props.treeCheckStrictly || props.labelInValue);
  193. const mergedMultiple = computed(() => mergedCheckable.value || props.multiple);
  194. // ========================== Warning ===========================
  195. if (process.env.NODE_ENV !== 'production') {
  196. watchEffect(() => {
  197. warningProps(props);
  198. });
  199. }
  200. // ========================= FieldNames =========================
  201. const mergedFieldNames = computed(() => fillFieldNames(props.fieldNames));
  202. // =========================== Search ===========================
  203. const [mergedSearchValue, setSearchValue] = useMergedState('', {
  204. value: computed(() => props.searchValue !== undefined ? props.searchValue : props.inputValue),
  205. postState: search => search || ''
  206. });
  207. const onInternalSearch = searchText => {
  208. var _a;
  209. setSearchValue(searchText);
  210. (_a = props.onSearch) === null || _a === void 0 ? void 0 : _a.call(props, searchText);
  211. };
  212. // ============================ Data ============================
  213. // `useTreeData` only do convert of `children` or `simpleMode`.
  214. // Else will return origin `treeData` for perf consideration.
  215. // Do not do anything to loop the data.
  216. const mergedTreeData = useTreeData(toRef(props, 'treeData'), toRef(props, 'children'), toRef(props, 'treeDataSimpleMode'));
  217. const {
  218. keyEntities,
  219. valueEntities
  220. } = useDataEntities(mergedTreeData, mergedFieldNames);
  221. /** Get `missingRawValues` which not exist in the tree yet */
  222. const splitRawValues = newRawValues => {
  223. const missingRawValues = [];
  224. const existRawValues = [];
  225. // Keep missing value in the cache
  226. newRawValues.forEach(val => {
  227. if (valueEntities.value.has(val)) {
  228. existRawValues.push(val);
  229. } else {
  230. missingRawValues.push(val);
  231. }
  232. });
  233. return {
  234. missingRawValues,
  235. existRawValues
  236. };
  237. };
  238. // Filtered Tree
  239. const filteredTreeData = useFilterTreeData(mergedTreeData, mergedSearchValue, {
  240. fieldNames: mergedFieldNames,
  241. treeNodeFilterProp: toRef(props, 'treeNodeFilterProp'),
  242. filterTreeNode: toRef(props, 'filterTreeNode')
  243. });
  244. // =========================== Label ============================
  245. const getLabel = item => {
  246. if (item) {
  247. if (props.treeNodeLabelProp) {
  248. return item[props.treeNodeLabelProp];
  249. }
  250. // Loop from fieldNames
  251. const {
  252. _title: titleList
  253. } = mergedFieldNames.value;
  254. for (let i = 0; i < titleList.length; i += 1) {
  255. const title = item[titleList[i]];
  256. if (title !== undefined) {
  257. return title;
  258. }
  259. }
  260. }
  261. };
  262. // ========================= Wrap Value =========================
  263. const toLabeledValues = draftValues => {
  264. const values = toArray(draftValues);
  265. return values.map(val => {
  266. if (isRawValue(val)) {
  267. return {
  268. value: val
  269. };
  270. }
  271. return val;
  272. });
  273. };
  274. const convert2LabelValues = draftValues => {
  275. const values = toLabeledValues(draftValues);
  276. return values.map(item => {
  277. let {
  278. label: rawLabel
  279. } = item;
  280. const {
  281. value: rawValue,
  282. halfChecked: rawHalfChecked
  283. } = item;
  284. let rawDisabled;
  285. const entity = valueEntities.value.get(rawValue);
  286. // Fill missing label & status
  287. if (entity) {
  288. rawLabel = rawLabel !== null && rawLabel !== void 0 ? rawLabel : getLabel(entity.node);
  289. rawDisabled = entity.node.disabled;
  290. }
  291. return {
  292. label: rawLabel,
  293. value: rawValue,
  294. halfChecked: rawHalfChecked,
  295. disabled: rawDisabled
  296. };
  297. });
  298. };
  299. // =========================== Values ===========================
  300. const [internalValue, setInternalValue] = useMergedState(props.defaultValue, {
  301. value: toRef(props, 'value')
  302. });
  303. const rawMixedLabeledValues = computed(() => toLabeledValues(internalValue.value));
  304. // Split value into full check and half check
  305. const rawLabeledValues = shallowRef([]);
  306. const rawHalfLabeledValues = shallowRef([]);
  307. watchEffect(() => {
  308. const fullCheckValues = [];
  309. const halfCheckValues = [];
  310. rawMixedLabeledValues.value.forEach(item => {
  311. if (item.halfChecked) {
  312. halfCheckValues.push(item);
  313. } else {
  314. fullCheckValues.push(item);
  315. }
  316. });
  317. rawLabeledValues.value = fullCheckValues;
  318. rawHalfLabeledValues.value = halfCheckValues;
  319. });
  320. // const [mergedValues] = useCache(rawLabeledValues);
  321. const rawValues = computed(() => rawLabeledValues.value.map(item => item.value));
  322. const {
  323. maxLevel,
  324. levelEntities
  325. } = useMaxLevel(keyEntities);
  326. // Convert value to key. Will fill missed keys for conduct check.
  327. const [rawCheckedValues, rawHalfCheckedValues] = useCheckedKeys(rawLabeledValues, rawHalfLabeledValues, treeConduction, keyEntities, maxLevel, levelEntities);
  328. // Convert rawCheckedKeys to check strategy related values
  329. const displayValues = computed(() => {
  330. // Collect keys which need to show
  331. const displayKeys = formatStrategyValues(rawCheckedValues.value, props.showCheckedStrategy, keyEntities.value, mergedFieldNames.value);
  332. // Convert to value and filled with label
  333. const values = displayKeys.map(key => {
  334. var _a, _b, _c;
  335. return (_c = (_b = (_a = keyEntities.value[key]) === null || _a === void 0 ? void 0 : _a.node) === null || _b === void 0 ? void 0 : _b[mergedFieldNames.value.value]) !== null && _c !== void 0 ? _c : key;
  336. });
  337. // Back fill with origin label
  338. const labeledValues = values.map(val => {
  339. const targetItem = rawLabeledValues.value.find(item => item.value === val);
  340. return {
  341. value: val,
  342. label: targetItem === null || targetItem === void 0 ? void 0 : targetItem.label
  343. };
  344. });
  345. const rawDisplayValues = convert2LabelValues(labeledValues);
  346. const firstVal = rawDisplayValues[0];
  347. if (!mergedMultiple.value && firstVal && isNil(firstVal.value) && isNil(firstVal.label)) {
  348. return [];
  349. }
  350. return rawDisplayValues.map(item => {
  351. var _a;
  352. return _extends(_extends({}, item), {
  353. label: (_a = item.label) !== null && _a !== void 0 ? _a : item.value
  354. });
  355. });
  356. });
  357. const [cachedDisplayValues] = useCache(displayValues);
  358. // =========================== Change ===========================
  359. const triggerChange = (newRawValues, extra, source) => {
  360. const labeledValues = convert2LabelValues(newRawValues);
  361. setInternalValue(labeledValues);
  362. // Clean up if needed
  363. if (props.autoClearSearchValue) {
  364. setSearchValue('');
  365. }
  366. // Generate rest parameters is costly, so only do it when necessary
  367. if (props.onChange) {
  368. let eventValues = newRawValues;
  369. if (treeConduction.value) {
  370. const formattedKeyList = formatStrategyValues(newRawValues, props.showCheckedStrategy, keyEntities.value, mergedFieldNames.value);
  371. eventValues = formattedKeyList.map(key => {
  372. const entity = valueEntities.value.get(key);
  373. return entity ? entity.node[mergedFieldNames.value.value] : key;
  374. });
  375. }
  376. const {
  377. triggerValue,
  378. selected
  379. } = extra || {
  380. triggerValue: undefined,
  381. selected: undefined
  382. };
  383. let returnRawValues = eventValues;
  384. // We need fill half check back
  385. if (props.treeCheckStrictly) {
  386. const halfValues = rawHalfLabeledValues.value.filter(item => !eventValues.includes(item.value));
  387. returnRawValues = [...returnRawValues, ...halfValues];
  388. }
  389. const returnLabeledValues = convert2LabelValues(returnRawValues);
  390. const additionalInfo = {
  391. // [Legacy] Always return as array contains label & value
  392. preValue: rawLabeledValues.value,
  393. triggerValue
  394. };
  395. // [Legacy] Fill legacy data if user query.
  396. // This is expansive that we only fill when user query
  397. // https://github.com/react-component/tree-select/blob/fe33eb7c27830c9ac70cd1fdb1ebbe7bc679c16a/src/Select.jsx
  398. let showPosition = true;
  399. if (props.treeCheckStrictly || source === 'selection' && !selected) {
  400. showPosition = false;
  401. }
  402. fillAdditionalInfo(additionalInfo, triggerValue, newRawValues, mergedTreeData.value, showPosition, mergedFieldNames.value);
  403. if (mergedCheckable.value) {
  404. additionalInfo.checked = selected;
  405. } else {
  406. additionalInfo.selected = selected;
  407. }
  408. const returnValues = mergedLabelInValue.value ? returnLabeledValues : returnLabeledValues.map(item => item.value);
  409. props.onChange(mergedMultiple.value ? returnValues : returnValues[0], mergedLabelInValue.value ? null : returnLabeledValues.map(item => item.label), additionalInfo);
  410. }
  411. };
  412. // ========================== Options ===========================
  413. /** Trigger by option list */
  414. const onOptionSelect = (selectedKey, _ref2) => {
  415. let {
  416. selected,
  417. source
  418. } = _ref2;
  419. var _a, _b, _c;
  420. const keyEntitiesValue = toRaw(keyEntities.value);
  421. const valueEntitiesValue = toRaw(valueEntities.value);
  422. const entity = keyEntitiesValue[selectedKey];
  423. const node = entity === null || entity === void 0 ? void 0 : entity.node;
  424. const selectedValue = (_a = node === null || node === void 0 ? void 0 : node[mergedFieldNames.value.value]) !== null && _a !== void 0 ? _a : selectedKey;
  425. // Never be falsy but keep it safe
  426. if (!mergedMultiple.value) {
  427. // Single mode always set value
  428. triggerChange([selectedValue], {
  429. selected: true,
  430. triggerValue: selectedValue
  431. }, 'option');
  432. } else {
  433. let newRawValues = selected ? [...rawValues.value, selectedValue] : rawCheckedValues.value.filter(v => v !== selectedValue);
  434. // Add keys if tree conduction
  435. if (treeConduction.value) {
  436. // Should keep missing values
  437. const {
  438. missingRawValues,
  439. existRawValues
  440. } = splitRawValues(newRawValues);
  441. const keyList = existRawValues.map(val => valueEntitiesValue.get(val).key);
  442. // Conduction by selected or not
  443. let checkedKeys;
  444. if (selected) {
  445. ({
  446. checkedKeys
  447. } = conductCheck(keyList, true, keyEntitiesValue, maxLevel.value, levelEntities.value));
  448. } else {
  449. ({
  450. checkedKeys
  451. } = conductCheck(keyList, {
  452. checked: false,
  453. halfCheckedKeys: rawHalfCheckedValues.value
  454. }, keyEntitiesValue, maxLevel.value, levelEntities.value));
  455. }
  456. // Fill back of keys
  457. newRawValues = [...missingRawValues, ...checkedKeys.map(key => keyEntitiesValue[key].node[mergedFieldNames.value.value])];
  458. }
  459. triggerChange(newRawValues, {
  460. selected,
  461. triggerValue: selectedValue
  462. }, source || 'option');
  463. }
  464. // Trigger select event
  465. if (selected || !mergedMultiple.value) {
  466. (_b = props.onSelect) === null || _b === void 0 ? void 0 : _b.call(props, selectedValue, fillLegacyProps(node));
  467. } else {
  468. (_c = props.onDeselect) === null || _c === void 0 ? void 0 : _c.call(props, selectedValue, fillLegacyProps(node));
  469. }
  470. };
  471. // ========================== Dropdown ==========================
  472. const onInternalDropdownVisibleChange = open => {
  473. if (props.onDropdownVisibleChange) {
  474. const legacyParam = {};
  475. Object.defineProperty(legacyParam, 'documentClickClose', {
  476. get() {
  477. warning(false, 'Second param of `onDropdownVisibleChange` has been removed.');
  478. return false;
  479. }
  480. });
  481. props.onDropdownVisibleChange(open, legacyParam);
  482. }
  483. };
  484. // ====================== Display Change ========================
  485. const onDisplayValuesChange = (newValues, info) => {
  486. const newRawValues = newValues.map(item => item.value);
  487. if (info.type === 'clear') {
  488. triggerChange(newRawValues, {}, 'selection');
  489. return;
  490. }
  491. // TreeSelect only have multiple mode which means display change only has remove
  492. if (info.values.length) {
  493. onOptionSelect(info.values[0].value, {
  494. selected: false,
  495. source: 'selection'
  496. });
  497. }
  498. };
  499. const {
  500. treeNodeFilterProp,
  501. // Data
  502. loadData,
  503. treeLoadedKeys,
  504. onTreeLoad,
  505. // Expanded
  506. treeDefaultExpandAll,
  507. treeExpandedKeys,
  508. treeDefaultExpandedKeys,
  509. onTreeExpand,
  510. // Options
  511. virtual,
  512. listHeight,
  513. listItemHeight,
  514. // Tree
  515. treeLine,
  516. treeIcon,
  517. showTreeIcon,
  518. switcherIcon,
  519. treeMotion,
  520. customSlots,
  521. dropdownMatchSelectWidth,
  522. treeExpandAction
  523. } = toRefs(props);
  524. useProvideLegacySelectContext(toReactive({
  525. checkable: mergedCheckable,
  526. loadData,
  527. treeLoadedKeys,
  528. onTreeLoad,
  529. checkedKeys: rawCheckedValues,
  530. halfCheckedKeys: rawHalfCheckedValues,
  531. treeDefaultExpandAll,
  532. treeExpandedKeys,
  533. treeDefaultExpandedKeys,
  534. onTreeExpand,
  535. treeIcon,
  536. treeMotion,
  537. showTreeIcon,
  538. switcherIcon,
  539. treeLine,
  540. treeNodeFilterProp,
  541. keyEntities,
  542. customSlots
  543. }));
  544. useProvideSelectContext(toReactive({
  545. virtual,
  546. listHeight,
  547. listItemHeight,
  548. treeData: filteredTreeData,
  549. fieldNames: mergedFieldNames,
  550. onSelect: onOptionSelect,
  551. dropdownMatchSelectWidth,
  552. treeExpandAction
  553. }));
  554. const selectRef = ref();
  555. expose({
  556. focus() {
  557. var _a;
  558. (_a = selectRef.value) === null || _a === void 0 ? void 0 : _a.focus();
  559. },
  560. blur() {
  561. var _a;
  562. (_a = selectRef.value) === null || _a === void 0 ? void 0 : _a.blur();
  563. },
  564. scrollTo(arg) {
  565. var _a;
  566. (_a = selectRef.value) === null || _a === void 0 ? void 0 : _a.scrollTo(arg);
  567. }
  568. });
  569. return () => {
  570. var _a;
  571. const restProps = omit(props, ['id', 'prefixCls', 'customSlots',
  572. // Value
  573. 'value', 'defaultValue', 'onChange', 'onSelect', 'onDeselect',
  574. // Search
  575. 'searchValue', 'inputValue', 'onSearch', 'autoClearSearchValue', 'filterTreeNode', 'treeNodeFilterProp',
  576. // Selector
  577. 'showCheckedStrategy', 'treeNodeLabelProp',
  578. // Mode
  579. 'multiple', 'treeCheckable', 'treeCheckStrictly', 'labelInValue',
  580. // FieldNames
  581. 'fieldNames',
  582. // Data
  583. 'treeDataSimpleMode', 'treeData', 'children', 'loadData', 'treeLoadedKeys', 'onTreeLoad',
  584. // Expanded
  585. 'treeDefaultExpandAll', 'treeExpandedKeys', 'treeDefaultExpandedKeys', 'onTreeExpand',
  586. // Options
  587. 'virtual', 'listHeight', 'listItemHeight', 'onDropdownVisibleChange',
  588. // Tree
  589. 'treeLine', 'treeIcon', 'showTreeIcon', 'switcherIcon', 'treeMotion']);
  590. return _createVNode(BaseSelect, _objectSpread(_objectSpread(_objectSpread({
  591. "ref": selectRef
  592. }, attrs), restProps), {}, {
  593. "id": mergedId,
  594. "prefixCls": props.prefixCls,
  595. "mode": mergedMultiple.value ? 'multiple' : undefined,
  596. "displayValues": cachedDisplayValues.value,
  597. "onDisplayValuesChange": onDisplayValuesChange,
  598. "searchValue": mergedSearchValue.value,
  599. "onSearch": onInternalSearch,
  600. "OptionList": OptionList,
  601. "emptyOptions": !mergedTreeData.value.length,
  602. "onDropdownVisibleChange": onInternalDropdownVisibleChange,
  603. "tagRender": props.tagRender || slots.tagRender,
  604. "dropdownMatchSelectWidth": (_a = props.dropdownMatchSelectWidth) !== null && _a !== void 0 ? _a : true
  605. }), slots);
  606. };
  607. }
  608. });