Select.js 21 KB

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