Select.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
  2. import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
  3. import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
  4. import _typeof from "@babel/runtime/helpers/esm/typeof";
  5. import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
  6. import { createVNode as _createVNode, resolveDirective as _resolveDirective } from "vue";
  7. /**
  8. * To match accessibility requirement, we always provide an input in the component.
  9. * Other element will not set `tabindex` to avoid `onBlur` sequence problem.
  10. * For focused select, we set `aria-live="polite"` to update the accessibility content.
  11. *
  12. * ref:
  13. * - keyboard: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#Keyboard_interactions
  14. *
  15. * New api:
  16. * - listHeight
  17. * - listItemHeight
  18. * - component
  19. *
  20. * Remove deprecated api:
  21. * - multiple
  22. * - tags
  23. * - combobox
  24. * - firstActiveValue
  25. * - dropdownMenuStyle
  26. * - openClassName (Not list in api)
  27. *
  28. * Update:
  29. * - `backfill` only support `combobox` mode
  30. * - `combobox` mode not support `labelInValue` since it's meaningless
  31. * - `getInputElement` only support `combobox` mode
  32. * - `onChange` return OptionData instead of ReactNode
  33. * - `filterOption` `onChange` `onSelect` accept OptionData instead of ReactNode
  34. * - `combobox` mode trigger `onChange` will get `undefined` if no `value` match in Option
  35. * - `combobox` mode not support `optionLabelProp`
  36. */
  37. import BaseSelect, { baseSelectPropsWithoutPrivate, isMultiple } from './BaseSelect';
  38. import OptionList from './OptionList';
  39. import useOptions from './hooks/useOptions';
  40. import { useProvideSelectProps } from './SelectContext';
  41. import useId from './hooks/useId';
  42. import { fillFieldNames, flattenOptions, injectPropsWithOption } from './utils/valueUtil';
  43. import warningProps from './utils/warningPropsUtil';
  44. import { toArray } from './utils/commonUtil';
  45. import useFilterOptions from './hooks/useFilterOptions';
  46. import useCache from './hooks/useCache';
  47. import { computed, defineComponent, ref, shallowRef, toRef, watchEffect } from 'vue';
  48. import PropTypes from '../_util/vue-types';
  49. import { initDefaultProps } from '../_util/props-util';
  50. import useMergedState from '../_util/hooks/useMergedState';
  51. import useState from '../_util/hooks/useState';
  52. import { toReactive } from '../_util/toReactive';
  53. import omit from '../_util/omit';
  54. var OMIT_DOM_PROPS = ['inputValue'];
  55. export function selectProps() {
  56. return _objectSpread(_objectSpread({}, baseSelectPropsWithoutPrivate()), {}, {
  57. prefixCls: String,
  58. id: String,
  59. backfill: {
  60. type: Boolean,
  61. default: undefined
  62. },
  63. // >>> Field Names
  64. fieldNames: Object,
  65. // >>> Search
  66. /** @deprecated Use `searchValue` instead */
  67. inputValue: String,
  68. searchValue: String,
  69. onSearch: Function,
  70. autoClearSearchValue: {
  71. type: Boolean,
  72. default: undefined
  73. },
  74. // >>> Select
  75. onSelect: Function,
  76. onDeselect: Function,
  77. // >>> Options
  78. /**
  79. * In Select, `false` means do nothing.
  80. * In TreeSelect, `false` will highlight match item.
  81. * It's by design.
  82. */
  83. filterOption: {
  84. type: [Boolean, Function],
  85. default: undefined
  86. },
  87. filterSort: Function,
  88. optionFilterProp: String,
  89. optionLabelProp: String,
  90. options: Array,
  91. defaultActiveFirstOption: {
  92. type: Boolean,
  93. default: undefined
  94. },
  95. virtual: {
  96. type: Boolean,
  97. default: undefined
  98. },
  99. listHeight: Number,
  100. listItemHeight: Number,
  101. // >>> Icon
  102. menuItemSelectedIcon: PropTypes.any,
  103. mode: String,
  104. labelInValue: {
  105. type: Boolean,
  106. default: undefined
  107. },
  108. value: PropTypes.any,
  109. defaultValue: PropTypes.any,
  110. onChange: Function,
  111. children: Array
  112. });
  113. }
  114. function isRawValue(value) {
  115. return !value || _typeof(value) !== 'object';
  116. }
  117. export default defineComponent({
  118. compatConfig: {
  119. MODE: 3
  120. },
  121. name: 'Select',
  122. inheritAttrs: false,
  123. props: initDefaultProps(selectProps(), {
  124. prefixCls: 'vc-select',
  125. autoClearSearchValue: true,
  126. listHeight: 200,
  127. listItemHeight: 20,
  128. dropdownMatchSelectWidth: true
  129. }),
  130. setup: function setup(props, _ref) {
  131. var expose = _ref.expose,
  132. attrs = _ref.attrs,
  133. slots = _ref.slots;
  134. var mergedId = useId(toRef(props, 'id'));
  135. var multiple = computed(function () {
  136. return isMultiple(props.mode);
  137. });
  138. var childrenAsData = computed(function () {
  139. return !!(!props.options && props.children);
  140. });
  141. var mergedFilterOption = computed(function () {
  142. if (props.filterOption === undefined && props.mode === 'combobox') {
  143. return false;
  144. }
  145. return props.filterOption;
  146. });
  147. // ========================= FieldNames =========================
  148. var mergedFieldNames = computed(function () {
  149. return fillFieldNames(props.fieldNames, childrenAsData.value);
  150. });
  151. // =========================== Search ===========================
  152. var _useMergedState = useMergedState('', {
  153. value: computed(function () {
  154. return props.searchValue !== undefined ? props.searchValue : props.inputValue;
  155. }),
  156. postState: function postState(search) {
  157. return search || '';
  158. }
  159. }),
  160. _useMergedState2 = _slicedToArray(_useMergedState, 2),
  161. mergedSearchValue = _useMergedState2[0],
  162. setSearchValue = _useMergedState2[1];
  163. // =========================== Option ===========================
  164. var parsedOptions = useOptions(toRef(props, 'options'), toRef(props, 'children'), mergedFieldNames);
  165. var valueOptions = parsedOptions.valueOptions,
  166. labelOptions = parsedOptions.labelOptions,
  167. mergedOptions = parsedOptions.options;
  168. // ========================= Wrap Value =========================
  169. var convert2LabelValues = function convert2LabelValues(draftValues) {
  170. // Convert to array
  171. var valueList = toArray(draftValues);
  172. // Convert to labelInValue type
  173. return valueList.map(function (val) {
  174. var rawValue;
  175. var rawLabel;
  176. var rawKey;
  177. var rawDisabled;
  178. // Fill label & value
  179. if (isRawValue(val)) {
  180. rawValue = val;
  181. } else {
  182. var _val$value;
  183. rawKey = val.key;
  184. rawLabel = val.label;
  185. rawValue = (_val$value = val.value) !== null && _val$value !== void 0 ? _val$value : rawKey;
  186. }
  187. var option = valueOptions.value.get(rawValue);
  188. if (option) {
  189. var _option$key;
  190. // Fill missing props
  191. if (rawLabel === undefined) rawLabel = option === null || option === void 0 ? void 0 : option[props.optionLabelProp || mergedFieldNames.value.label];
  192. if (rawKey === undefined) rawKey = (_option$key = option === null || option === void 0 ? void 0 : option.key) !== null && _option$key !== void 0 ? _option$key : rawValue;
  193. rawDisabled = option === null || option === void 0 ? void 0 : option.disabled;
  194. // Warning if label not same as provided
  195. // if (process.env.NODE_ENV !== 'production' && !isRawValue(val)) {
  196. // const optionLabel = option?.[mergedFieldNames.value.label];
  197. // if (optionLabel !== undefined && optionLabel !== rawLabel) {
  198. // warning(false, '`label` of `value` is not same as `label` in Select options.');
  199. // }
  200. // }
  201. }
  202. return {
  203. label: rawLabel,
  204. value: rawValue,
  205. key: rawKey,
  206. disabled: rawDisabled,
  207. option: option
  208. };
  209. });
  210. };
  211. // =========================== Values ===========================
  212. var _useMergedState3 = useMergedState(props.defaultValue, {
  213. value: toRef(props, 'value')
  214. }),
  215. _useMergedState4 = _slicedToArray(_useMergedState3, 2),
  216. internalValue = _useMergedState4[0],
  217. setInternalValue = _useMergedState4[1];
  218. // Merged value with LabelValueType
  219. var rawLabeledValues = computed(function () {
  220. var _values$;
  221. var values = convert2LabelValues(internalValue.value);
  222. // combobox no need save value when it's empty
  223. if (props.mode === 'combobox' && !((_values$ = values[0]) !== null && _values$ !== void 0 && _values$.value)) {
  224. return [];
  225. }
  226. return values;
  227. });
  228. // Fill label with cache to avoid option remove
  229. var _useCache = useCache(rawLabeledValues, valueOptions),
  230. _useCache2 = _slicedToArray(_useCache, 2),
  231. mergedValues = _useCache2[0],
  232. getMixedOption = _useCache2[1];
  233. var displayValues = computed(function () {
  234. // `null` need show as placeholder instead
  235. // https://github.com/ant-design/ant-design/issues/25057
  236. if (!props.mode && mergedValues.value.length === 1) {
  237. var firstValue = mergedValues.value[0];
  238. if (firstValue.value === null && (firstValue.label === null || firstValue.label === undefined)) {
  239. return [];
  240. }
  241. }
  242. return mergedValues.value.map(function (item) {
  243. var _ref2;
  244. return _objectSpread(_objectSpread({}, item), {}, {
  245. label: (_ref2 = typeof item.label === 'function' ? item.label() : item.label) !== null && _ref2 !== void 0 ? _ref2 : item.value
  246. });
  247. });
  248. });
  249. /** Convert `displayValues` to raw value type set */
  250. var rawValues = computed(function () {
  251. return new Set(mergedValues.value.map(function (val) {
  252. return val.value;
  253. }));
  254. });
  255. watchEffect(function () {
  256. if (props.mode === 'combobox') {
  257. var _mergedValues$value$;
  258. var strValue = (_mergedValues$value$ = mergedValues.value[0]) === null || _mergedValues$value$ === void 0 ? void 0 : _mergedValues$value$.value;
  259. if (strValue !== undefined && strValue !== null) {
  260. setSearchValue(String(strValue));
  261. }
  262. }
  263. }, {
  264. flush: 'post'
  265. });
  266. // ======================= Display Option =======================
  267. // Create a placeholder item if not exist in `options`
  268. var createTagOption = function createTagOption(val, label) {
  269. var _ref3;
  270. var mergedLabel = label !== null && label !== void 0 ? label : val;
  271. return _ref3 = {}, _defineProperty(_ref3, mergedFieldNames.value.value, val), _defineProperty(_ref3, mergedFieldNames.value.label, mergedLabel), _ref3;
  272. };
  273. // Fill tag as option if mode is `tags`
  274. var filledTagOptions = shallowRef();
  275. watchEffect(function () {
  276. if (props.mode !== 'tags') {
  277. filledTagOptions.value = mergedOptions.value;
  278. return;
  279. }
  280. // >>> Tag mode
  281. var cloneOptions = mergedOptions.value.slice();
  282. // Check if value exist in options (include new patch item)
  283. var existOptions = function existOptions(val) {
  284. return valueOptions.value.has(val);
  285. };
  286. // Fill current value as option
  287. _toConsumableArray(mergedValues.value).sort(function (a, b) {
  288. return a.value < b.value ? -1 : 1;
  289. }).forEach(function (item) {
  290. var val = item.value;
  291. if (!existOptions(val)) {
  292. cloneOptions.push(createTagOption(val, item.label));
  293. }
  294. });
  295. filledTagOptions.value = cloneOptions;
  296. });
  297. var filteredOptions = useFilterOptions(filledTagOptions, mergedFieldNames, mergedSearchValue, mergedFilterOption, toRef(props, 'optionFilterProp'));
  298. // Fill options with search value if needed
  299. var filledSearchOptions = computed(function () {
  300. if (props.mode !== 'tags' || !mergedSearchValue.value || filteredOptions.value.some(function (item) {
  301. return item[props.optionFilterProp || 'value'] === mergedSearchValue.value;
  302. })) {
  303. return filteredOptions.value;
  304. }
  305. // Fill search value as option
  306. return [createTagOption(mergedSearchValue.value)].concat(_toConsumableArray(filteredOptions.value));
  307. });
  308. var orderedFilteredOptions = computed(function () {
  309. if (!props.filterSort) {
  310. return filledSearchOptions.value;
  311. }
  312. return _toConsumableArray(filledSearchOptions.value).sort(function (a, b) {
  313. return props.filterSort(a, b);
  314. });
  315. });
  316. var displayOptions = computed(function () {
  317. return flattenOptions(orderedFilteredOptions.value, {
  318. fieldNames: mergedFieldNames.value,
  319. childrenAsData: childrenAsData.value
  320. });
  321. });
  322. // =========================== Change ===========================
  323. var triggerChange = function triggerChange(values) {
  324. var labeledValues = convert2LabelValues(values);
  325. setInternalValue(labeledValues);
  326. if (props.onChange && (
  327. // Trigger event only when value changed
  328. labeledValues.length !== mergedValues.value.length || labeledValues.some(function (newVal, index) {
  329. var _mergedValues$value$i;
  330. return ((_mergedValues$value$i = mergedValues.value[index]) === null || _mergedValues$value$i === void 0 ? void 0 : _mergedValues$value$i.value) !== (newVal === null || newVal === void 0 ? void 0 : newVal.value);
  331. }))) {
  332. var returnValues = props.labelInValue ? labeledValues.map(function (v) {
  333. return _objectSpread(_objectSpread({}, v), {}, {
  334. originLabel: v.label,
  335. label: typeof v.label === 'function' ? v.label() : v.label
  336. });
  337. }) : labeledValues.map(function (v) {
  338. return v.value;
  339. });
  340. var returnOptions = labeledValues.map(function (v) {
  341. return injectPropsWithOption(getMixedOption(v.value));
  342. });
  343. props.onChange(
  344. // Value
  345. multiple.value ? returnValues : returnValues[0],
  346. // Option
  347. multiple.value ? returnOptions : returnOptions[0]);
  348. }
  349. };
  350. // ======================= Accessibility ========================
  351. var _useState = useState(null),
  352. _useState2 = _slicedToArray(_useState, 2),
  353. activeValue = _useState2[0],
  354. setActiveValue = _useState2[1];
  355. var _useState3 = useState(0),
  356. _useState4 = _slicedToArray(_useState3, 2),
  357. accessibilityIndex = _useState4[0],
  358. setAccessibilityIndex = _useState4[1];
  359. var mergedDefaultActiveFirstOption = computed(function () {
  360. return props.defaultActiveFirstOption !== undefined ? props.defaultActiveFirstOption : props.mode !== 'combobox';
  361. });
  362. var onActiveValue = function onActiveValue(active, index) {
  363. var _ref4 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
  364. _ref4$source = _ref4.source,
  365. source = _ref4$source === void 0 ? 'keyboard' : _ref4$source;
  366. setAccessibilityIndex(index);
  367. if (props.backfill && props.mode === 'combobox' && active !== null && source === 'keyboard') {
  368. setActiveValue(String(active));
  369. }
  370. };
  371. // ========================= OptionList =========================
  372. var triggerSelect = function triggerSelect(val, selected) {
  373. var getSelectEnt = function getSelectEnt() {
  374. var _option$key2;
  375. var option = getMixedOption(val);
  376. var originLabel = option === null || option === void 0 ? void 0 : option[mergedFieldNames.value.label];
  377. return [props.labelInValue ? {
  378. label: typeof originLabel === 'function' ? originLabel() : originLabel,
  379. originLabel: originLabel,
  380. value: val,
  381. key: (_option$key2 = option === null || option === void 0 ? void 0 : option.key) !== null && _option$key2 !== void 0 ? _option$key2 : val
  382. } : val, injectPropsWithOption(option)];
  383. };
  384. if (selected && props.onSelect) {
  385. var _getSelectEnt = getSelectEnt(),
  386. _getSelectEnt2 = _slicedToArray(_getSelectEnt, 2),
  387. wrappedValue = _getSelectEnt2[0],
  388. option = _getSelectEnt2[1];
  389. props.onSelect(wrappedValue, option);
  390. } else if (!selected && props.onDeselect) {
  391. var _getSelectEnt3 = getSelectEnt(),
  392. _getSelectEnt4 = _slicedToArray(_getSelectEnt3, 2),
  393. _wrappedValue = _getSelectEnt4[0],
  394. _option = _getSelectEnt4[1];
  395. props.onDeselect(_wrappedValue, _option);
  396. }
  397. };
  398. // Used for OptionList selection
  399. var onInternalSelect = function onInternalSelect(val, info) {
  400. var cloneValues;
  401. // Single mode always trigger select only with option list
  402. var mergedSelect = multiple.value ? info.selected : true;
  403. if (mergedSelect) {
  404. cloneValues = multiple.value ? [].concat(_toConsumableArray(mergedValues.value), [val]) : [val];
  405. } else {
  406. cloneValues = mergedValues.value.filter(function (v) {
  407. return v.value !== val;
  408. });
  409. }
  410. triggerChange(cloneValues);
  411. triggerSelect(val, mergedSelect);
  412. // Clean search value if single or configured
  413. if (props.mode === 'combobox') {
  414. // setSearchValue(String(val));
  415. setActiveValue('');
  416. } else if (!multiple.value || props.autoClearSearchValue) {
  417. setSearchValue('');
  418. setActiveValue('');
  419. }
  420. };
  421. // ======================= Display Change =======================
  422. // BaseSelect display values change
  423. var onDisplayValuesChange = function onDisplayValuesChange(nextValues, info) {
  424. triggerChange(nextValues);
  425. if (info.type === 'remove' || info.type === 'clear') {
  426. info.values.forEach(function (item) {
  427. triggerSelect(item.value, false);
  428. });
  429. }
  430. };
  431. // =========================== Search ===========================
  432. var onInternalSearch = function onInternalSearch(searchText, info) {
  433. setSearchValue(searchText);
  434. setActiveValue(null);
  435. // [Submit] Tag mode should flush input
  436. if (info.source === 'submit') {
  437. var formatted = (searchText || '').trim();
  438. // prevent empty tags from appearing when you click the Enter button
  439. if (formatted) {
  440. var newRawValues = Array.from(new Set([].concat(_toConsumableArray(rawValues.value), [formatted])));
  441. triggerChange(newRawValues);
  442. triggerSelect(formatted, true);
  443. setSearchValue('');
  444. }
  445. return;
  446. }
  447. if (info.source !== 'blur') {
  448. var _props$onSearch;
  449. if (props.mode === 'combobox') {
  450. triggerChange(searchText);
  451. }
  452. (_props$onSearch = props.onSearch) === null || _props$onSearch === void 0 ? void 0 : _props$onSearch.call(props, searchText);
  453. }
  454. };
  455. var onInternalSearchSplit = function onInternalSearchSplit(words) {
  456. var patchValues = words;
  457. if (props.mode !== 'tags') {
  458. patchValues = words.map(function (word) {
  459. var opt = labelOptions.value.get(word);
  460. return opt === null || opt === void 0 ? void 0 : opt.value;
  461. }).filter(function (val) {
  462. return val !== undefined;
  463. });
  464. }
  465. var newRawValues = Array.from(new Set([].concat(_toConsumableArray(rawValues.value), _toConsumableArray(patchValues))));
  466. triggerChange(newRawValues);
  467. newRawValues.forEach(function (newRawValue) {
  468. triggerSelect(newRawValue, true);
  469. });
  470. };
  471. var realVirtual = computed(function () {
  472. return props.virtual !== false && props.dropdownMatchSelectWidth !== false;
  473. });
  474. useProvideSelectProps(toReactive(_objectSpread(_objectSpread({}, parsedOptions), {}, {
  475. flattenOptions: displayOptions,
  476. onActiveValue: onActiveValue,
  477. defaultActiveFirstOption: mergedDefaultActiveFirstOption,
  478. onSelect: onInternalSelect,
  479. menuItemSelectedIcon: toRef(props, 'menuItemSelectedIcon'),
  480. rawValues: rawValues,
  481. fieldNames: mergedFieldNames,
  482. virtual: realVirtual,
  483. listHeight: toRef(props, 'listHeight'),
  484. listItemHeight: toRef(props, 'listItemHeight'),
  485. childrenAsData: childrenAsData
  486. })));
  487. // ========================== Warning ===========================
  488. if (process.env.NODE_ENV !== 'production') {
  489. watchEffect(function () {
  490. warningProps(props);
  491. }, {
  492. flush: 'post'
  493. });
  494. }
  495. var selectRef = ref();
  496. expose({
  497. focus: function focus() {
  498. var _selectRef$value;
  499. (_selectRef$value = selectRef.value) === null || _selectRef$value === void 0 ? void 0 : _selectRef$value.focus();
  500. },
  501. blur: function blur() {
  502. var _selectRef$value2;
  503. (_selectRef$value2 = selectRef.value) === null || _selectRef$value2 === void 0 ? void 0 : _selectRef$value2.blur();
  504. },
  505. scrollTo: function scrollTo(arg) {
  506. var _selectRef$value3;
  507. (_selectRef$value3 = selectRef.value) === null || _selectRef$value3 === void 0 ? void 0 : _selectRef$value3.scrollTo(arg);
  508. }
  509. });
  510. var pickProps = computed(function () {
  511. return omit(props, ['id', 'mode', 'prefixCls', 'backfill', 'fieldNames',
  512. // Search
  513. 'inputValue', 'searchValue', 'onSearch', 'autoClearSearchValue',
  514. // Select
  515. 'onSelect', 'onDeselect', 'dropdownMatchSelectWidth',
  516. // Options
  517. 'filterOption', 'filterSort', 'optionFilterProp', 'optionLabelProp', 'options', 'children', 'defaultActiveFirstOption', 'menuItemSelectedIcon', 'virtual', 'listHeight', 'listItemHeight',
  518. // Value
  519. 'value', 'defaultValue', 'labelInValue', 'onChange']);
  520. });
  521. return function () {
  522. return _createVNode(BaseSelect, _objectSpread(_objectSpread(_objectSpread({}, pickProps.value), attrs), {}, {
  523. "id": mergedId,
  524. "prefixCls": props.prefixCls,
  525. "ref": selectRef,
  526. "omitDomProps": OMIT_DOM_PROPS,
  527. "mode": props.mode,
  528. "displayValues": displayValues.value,
  529. "onDisplayValuesChange": onDisplayValuesChange,
  530. "searchValue": mergedSearchValue.value,
  531. "onSearch": onInternalSearch,
  532. "onSearchSplit": onInternalSearchSplit,
  533. "dropdownMatchSelectWidth": props.dropdownMatchSelectWidth,
  534. "OptionList": OptionList,
  535. "emptyOptions": !displayOptions.value.length,
  536. "activeValue": activeValue.value,
  537. "activeDescendantId": "".concat(mergedId, "_list_").concat(accessibilityIndex.value)
  538. }), slots);
  539. };
  540. }
  541. });